Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 4 additions & 12 deletions geonode/geoserver/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
get_legend_url,
is_monochromatic_image,
set_resource_default_links,
normalize_bbox_to_float_list,
)

from .geofence import GeoFenceClient, GeoFenceUtils
Expand Down Expand Up @@ -2026,18 +2027,9 @@ def sync_instance_with_geoserver(instance_id, *args, **kwargs):
# are bypassed by custom create/updates we need to ensure the
# bbox is calculated properly.
srid = gs_resource.projection
bbox = gs_resource.native_bbox
ll_bbox = gs_resource.latlon_bbox
try:
instance.set_bbox_polygon([bbox[0], bbox[2], bbox[1], bbox[3]], srid)
except GeoNodeException as e:
if not ll_bbox:
raise
else:
logger.exception(e)
instance.srid = "EPSG:4326"
Dataset.objects.filter(id=instance.id).update(srid=instance.srid)
instance.set_ll_bbox_polygon([ll_bbox[0], ll_bbox[2], ll_bbox[1], ll_bbox[3]])
bbox = normalize_bbox_to_float_list(gs_resource.native_bbox)
ll_bbox = normalize_bbox_to_float_list(gs_resource.latlon_bbox)
instance.set_bbox_and_srid(bbox=bbox, ll_bbox=ll_bbox, srid=srid)

if instance.srid:
instance.srid_url = (
Expand Down
50 changes: 43 additions & 7 deletions geonode/layers/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import itertools
import re
import logging
from typing import List, Union

from django.conf import settings
from django.db import models, transaction
Expand All @@ -30,7 +31,8 @@
from tinymce.models import HTMLField

from geonode.client.hooks import hookset
from geonode.utils import build_absolute_uri, check_shp_columnnames
from geonode import GeoNodeException
from geonode.utils import build_absolute_uri, check_shp_columnnames, check_bbox_validity, normalize_bbox_to_float_list
from geonode.security.models import PermissionLevelMixin
from geonode.groups.conf import settings as groups_settings
from geonode.security.permissions import (
Expand Down Expand Up @@ -340,23 +342,57 @@ def recalc_bbox_on_geoserver(self, force_bbox=None):
logger.error("No resource returned from GeoServer after bbox update")
return False

bbox = resource.native_bbox
ll = resource.latlon_bbox
bbox = normalize_bbox_to_float_list(resource.native_bbox)
ll = normalize_bbox_to_float_list(resource.latlon_bbox)
srid = resource.projection

if not bbox or not ll:
logger.error("GeoServer did not return updated bbox values")
return False

# bbox order from GeoServer: [minx, maxx, miny, maxy]
with transaction.atomic():
self.set_bbox_polygon([bbox[0], bbox[2], bbox[1], bbox[3]], srid)
self.set_ll_bbox_polygon([ll[0], ll[2], ll[1], ll[3]])
self.srid = srid or self.srid
self.set_bbox_and_srid(bbox=bbox, ll_bbox=ll, srid=srid)
self.save(update_fields=["srid"])

return True

def set_bbox_and_srid(
self,
bbox: List[Union[int, float]],
ll_bbox: List[Union[int, float]],
srid: str,
) -> None:
"""
Set dataset bbox, ll_bbox and spatial reference identifier.
Args:
bbox: Bounding box as [minx, maxx, miny, maxy], each element is int or float.
If invalid or unavailable, the method falls back to ll_bbox with EPSG:4326.
ll_bbox: Lat/Lon bounding box as [minx, maxx, miny, maxy], each element is int or float.
Must be valid, otherwise a GeoNodeException is raised.
srid: Spatial Reference Identifier (string).
Returns:
None
"""
if not check_bbox_validity(ll_bbox):
raise GeoNodeException("Lat/Lon BBox was not provided or is invalid")

ll_bbox_gn = [ll_bbox[0], ll_bbox[2], ll_bbox[1], ll_bbox[3]]
# Try to use native bbox if available
if check_bbox_validity(bbox) and srid:
try:
native_bbox_gn = [bbox[0], bbox[2], bbox[1], bbox[3]]
self.set_bbox_polygon(native_bbox_gn, srid)
self.set_ll_bbox_polygon(ll_bbox_gn)
self.srid = srid
return
except GeoNodeException as e:
logger.warning(f"Failed to set bbox with SRID {srid}, falling back to EPSG:4326: {e}")

# Fallback to lat/lon bbox
self.set_bbox_polygon(ll_bbox_gn, "EPSG:4326")
self.set_ll_bbox_polygon(ll_bbox_gn)
self.srid = "EPSG:4326"

def __str__(self):
return str(self.alternate)

Expand Down
25 changes: 25 additions & 0 deletions geonode/layers/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,31 @@ def test_dataset_recalc_bbox_on_geoserver_success(self, mock_get_layer, mock_get
self.assertIsNotNone(self.dataset.bbox_polygon)
self.assertIsNotNone(self.dataset.ll_bbox_polygon)

@patch("geonode.base.models.bbox_to_projection", side_effect=Exception("Unsupported CRS"))
def test_set_bbox_and_srid_fallback_to_ll_bbox(self, _mock_bbox_to_projection):
"""
If native bbox reprojection fails, fallback to EPSG:4326 and ll_bbox for both polygons.
"""
native_bbox = [1535760, 1786050, 4652670, 5126620]
ll_bbox = [8.7, 9.2, 45.1, 45.5]

self.dataset.set_bbox_and_srid(bbox=native_bbox, ll_bbox=ll_bbox, srid="EPSG:3003")

self.assertEqual(self.dataset.srid, "EPSG:4326")
self.assertIsNotNone(self.dataset.bbox_polygon)
self.assertIsNotNone(self.dataset.ll_bbox_polygon)
self.assertEqual(self.dataset.bbox_polygon.wkt, self.dataset.ll_bbox_polygon.wkt)

def test_set_bbox_and_srid_uses_native_bbox_when_supported(self):
native_bbox = [0, 10, 0, 10]
ll_bbox = [0, 10, 0, 10]

self.dataset.set_bbox_and_srid(bbox=native_bbox, ll_bbox=ll_bbox, srid="EPSG:4326")

self.assertEqual(self.dataset.srid, "EPSG:4326")
self.assertIsNotNone(self.dataset.bbox_polygon)
self.assertIsNotNone(self.dataset.ll_bbox_polygon)

@patch("geonode.layers.models.Dataset.recalc_bbox_on_geoserver")
def test_recalc_bbox_view_success(self, mock_recalc_bbox):
"""Test the recalc_bbox view returns 200 when successful."""
Expand Down
32 changes: 32 additions & 0 deletions geonode/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
from math import atan, exp, log, pi, tan
from zipfile import ZipFile, is_zipfile, ZIP_DEFLATED
from geonode.upload.api.exceptions import GeneralUploadException
from typing import List, Optional, Union

from django.conf import settings
from django.db.models import signals
Expand Down Expand Up @@ -422,6 +423,37 @@ def bbox_swap(bbox):
return [_bbox[0], _bbox[2], _bbox[1], _bbox[3]]


def check_bbox_validity(value: List[Union[int, float]]) -> bool:
"""
Validate that a bounding box value is a non-empty list of at least 4 numeric values.

Args:
value: Bounding box to validate, expected as [minx, maxx, miny, maxy].
Accepts int or float values. None is treated as invalid.

Returns:
True if `value` is a list with at least 4 numeric (int or float) elements, False otherwise.
"""
if (
not value
or not isinstance(value, list)
or len(value) < 4
or not all(isinstance(x, (int, float)) for x in value)
):
return False
return True


def normalize_bbox_to_float_list(value) -> Optional[List[float]]:
"""Normalize bbox input to a 4-item float list or return None when invalid."""
if not value or len(value) < 4:
return None
try:
return [float(x) for x in value[:4]]
except (TypeError, ValueError):
return None


def bbox_to_wkt(x0, x1, y0, y1, srid="4326", include_srid=True):
if srid and str(srid).startswith("EPSG:"):
srid = srid[5:]
Expand Down
Loading