Skip to content

Commit

Permalink
[Fixes #12455] Map thumbnails extent always cover the whole globe (#1…
Browse files Browse the repository at this point in the history
…2460)

* [Fixes #12455] Map thumbnails extent always cover the whole globe

* [Fixes #12455] Map thumbnails extent always cover the whole globe

* [Fixes #12455] Map thumbnails extent always cover the whole globe

* Fixed bbox calculation and repositioned utils

* [Fixes #12455] fix test

---------

Co-authored-by: G. Allegri <[email protected]>
  • Loading branch information
mattiagiupponi and giohappy authored Sep 18, 2024
1 parent f8c3f2b commit 5bcbd82
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 136 deletions.
98 changes: 98 additions & 0 deletions geonode/base/bbox_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,24 @@
import math
import copy
import json
import re
import logging

from decimal import Decimal
from typing import Union, List, Generator

from pyproj import CRS
from shapely import affinity
from shapely.ops import split
from shapely.geometry import mapping, Polygon, LineString, GeometryCollection

from django.contrib.gis.geos import Polygon as DjangoPolygon

from geonode import GeoNodeException
from geonode.utils import bbox_to_projection

logger = logging.getLogger(__name__)


class BBOXHelper:
"""
Expand Down Expand Up @@ -228,3 +237,92 @@ def split_polygon(
return GeometryCollection(geo_polygons)
else:
return geo_polygons


def transform_bbox(bbox: List, target_crs: str = "EPSG:3857"):
"""
Function transforming BBOX in dataset compliant format (xmin, xmax, ymin, ymax, 'EPSG:xxxx') to another CRS,
preserving overflow values.
"""
match = re.match(r"^(EPSG:)?(?P<srid>\d{4,6})$", str(target_crs))
target_srid = int(match.group("srid")) if match else 4326
return list(bbox_to_projection(bbox, target_srid=target_srid))[:-1] + [target_crs]


def epsg_3857_area_of_use(target_crs="EPSG:4326"):
"""
Shortcut function, returning area of use of EPSG:3857 (in EPSG:4326) in a dataset compliant BBOX
"""
epsg3857 = CRS.from_user_input("EPSG:3857")
area_of_use = [
getattr(epsg3857.area_of_use, "west"),
getattr(epsg3857.area_of_use, "east"),
getattr(epsg3857.area_of_use, "south"),
getattr(epsg3857.area_of_use, "north"),
"EPSG:4326",
]
if target_crs != "EPSG:4326":
return transform_bbox(area_of_use, target_crs)
return area_of_use


def crop_to_3857_area_of_use(bbox: List) -> List:
# perform the comparison in EPSG:4326 (the pivot for EPSG:3857)
bbox4326 = transform_bbox(bbox, target_crs="EPSG:4326")

# get area of use of EPSG:3857 in EPSG:4326
epsg3857_bounds_bbox = epsg_3857_area_of_use()

bbox = []
for coord, bound_coord in zip(bbox4326[:-1], epsg3857_bounds_bbox[:-1]):
if abs(coord) > abs(bound_coord):
logger.debug("Thumbnail generation: cropping BBOX's coord to EPSG:3857 area of use.")
bbox.append(bound_coord)
else:
bbox.append(coord)

bbox.append("EPSG:4326")

return bbox


def exceeds_epsg3857_area_of_use(bbox: List) -> bool:
"""
Function checking if a provided BBOX extends the are of use of EPSG:3857. Comparison is performed after casting
the BBOX to EPSG:4326 (pivot for EPSG:3857).
:param bbox: a dataset compliant BBOX in a certain CRS, in (xmin, xmax, ymin, ymax, 'EPSG:xxxx') order
:returns: List of indicators whether BBOX's coord exceeds the area of use of EPSG:3857
"""

# perform the comparison in EPSG:4326 (the pivot for EPSG:3857)
bbox4326 = transform_bbox(bbox, target_crs="EPSG:4326")

# get area of use of EPSG:3857 in EPSG:4326
epsg3857_bounds_bbox = epsg_3857_area_of_use()

exceeds = False
for coord, bound_coord in zip(bbox4326[:-1], epsg3857_bounds_bbox[:-1]):
if abs(coord) > abs(bound_coord):
exceeds = True

return exceeds


def clean_bbox(bbox, target_crs):
# make sure BBOX is provided with the CRS in a correct format
source_crs = bbox[-1]

srid_regex = re.match(r"EPSG:\d+", source_crs)
if not srid_regex:
logger.error(f"Thumbnail bbox is in a wrong format: {bbox}")
raise GeoNodeException("Wrong BBOX format")

# for the EPSG:3857 (default thumb's CRS) - make sure received BBOX can be transformed to the target CRS;
# if it can't be (original coords are outside of the area of use of EPSG:3857), thumbnail generation with
# the provided bbox is impossible.
if target_crs == "EPSG:3857" and bbox[-1].upper() != "EPSG:3857":
bbox = crop_to_3857_area_of_use(bbox)

bbox = transform_bbox(bbox, target_crs=target_crs)
return bbox
46 changes: 30 additions & 16 deletions geonode/maps/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from django.template.defaultfilters import slugify
from django.urls import reverse
from django.utils.translation import gettext_lazy as _

from geonode.base import bbox_utils
from geonode import geoserver # noqa
from geonode.base.models import ResourceBase, LinkedResource
from geonode.client.hooks import hookset
Expand Down Expand Up @@ -129,23 +129,37 @@ def get_absolute_url(self):
def embed_url(self):
return reverse("map_embed", kwargs={"mapid": self.pk})

def get_bbox_from_datasets(self, layers):
def compute_bbox(self, target_crs="EPSG:3857"):
"""
Calculate the bbox from a given list of Dataset objects
bbox format: [xmin, xmax, ymin, ymax]
Compute bbox for maps by looping on all maplayers and getting the max
bbox of all the datasets
"""
bbox = None
for layer in layers:
dataset_bbox = layer.bbox
if bbox is None:
bbox = list(dataset_bbox[0:4])
else:
bbox[0] = min(bbox[0], dataset_bbox[0])
bbox[1] = max(bbox[1], dataset_bbox[1])
bbox[2] = min(bbox[2], dataset_bbox[2])
bbox[3] = max(bbox[3], dataset_bbox[3])

bbox = bbox_utils.epsg_3857_area_of_use(target_crs="EPSG:3857")
for layer in self.maplayers.filter(visibility=True).order_by("order").iterator():
dataset = layer.dataset
if dataset is not None:
if dataset.ll_bbox_polygon:
dataset_bbox = bbox_utils.clean_bbox(dataset.ll_bbox, target_crs)
elif (
dataset.bbox[-1].upper() != "EPSG:3857"
and target_crs.upper() == "EPSG:3857"
and bbox_utils.exceeds_epsg3857_area_of_use(dataset.bbox)
):
# handle exceeding the area of use of the default thumb's CRS
dataset_bbox = bbox_utils.transform_bbox(
bbox_utils.crop_to_3857_area_of_use(dataset.bbox), target_crs
)
else:
dataset_bbox = bbox_utils.transform_bbox(dataset.bbox, target_crs)

bbox = [
max(bbox[0], dataset_bbox[0]),
min(bbox[1], dataset_bbox[1]),
max(bbox[2], dataset_bbox[2]),
min(bbox[3], dataset_bbox[3]),
]

self.set_bbox_polygon([bbox[0], bbox[2], bbox[1], bbox[3]], target_crs)
return bbox

@property
Expand Down
8 changes: 7 additions & 1 deletion geonode/resource/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
HierarchicalKeyword,
SpatialRepresentationType,
)

from geonode.maps.models import Map
from ..layers.models import Dataset
from ..documents.models import Document
from ..documents.enumerations import DOCUMENT_TYPE_MAP, DOCUMENT_MIMETYPE_MAP
Expand Down Expand Up @@ -408,6 +408,12 @@ def metadata_post_save(instance, *args, **kwargs):
instance.uuid = _uuid
Dataset.objects.filter(id=instance.id).update(uuid=_uuid)

if isinstance(instance, Map):
"""
For maps, we can calculate the bbox based on the maplayers
"""
instance.compute_bbox()

# Set a default user for accountstream to work correctly.
if instance.owner is None:
instance.owner = get_valid_user()
Expand Down
2 changes: 1 addition & 1 deletion geonode/thumbs/tests/test_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ def test_datasets_locations_simple_map(self):
)

def test_datasets_locations_simple_map_default_bbox(self):
expected_bbox = [-8238681.374829309, -8220320.783295829, 4969844.0930337105, 4984363.884452854, "EPSG:3857"]
expected_bbox = [-20037397.023298446, 20037397.023298446, -20048966.104014594, 20048966.104014594, "EPSG:3857"]

dataset = Dataset.objects.get(title_en="theaters_nyc")
map = Map.objects.get(title_en="theaters_nyc_map")
Expand Down
41 changes: 12 additions & 29 deletions geonode/thumbs/thumbnails.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from geonode.geoserver.helpers import ogc_server_settings
from geonode.utils import get_dataset_name, get_dataset_workspace
from geonode.thumbs import utils
from geonode.base import bbox_utils
from geonode.thumbs.exceptions import ThumbnailError

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -110,10 +111,12 @@ def create_thumbnail(

if isinstance(instance, Map):
is_map_with_datasets = MapLayer.objects.filter(map=instance, local=True).exclude(dataset=None).exists()
if is_map_with_datasets:
compute_bbox_from_datasets = True
if bbox:
bbox = utils.clean_bbox(bbox, target_crs)
bbox = bbox_utils.clean_bbox(bbox, target_crs)
elif instance.ll_bbox_polygon:
bbox = utils.clean_bbox(instance.ll_bbox, target_crs)
bbox = bbox_utils.clean_bbox(instance.ll_bbox, target_crs)
else:
compute_bbox_from_datasets = True

Expand Down Expand Up @@ -268,16 +271,16 @@ def _datasets_locations(
locations.append([instance.ows_url or ogc_server_settings.LOCATION, [instance.alternate], []])
if compute_bbox:
if instance.ll_bbox_polygon:
bbox = utils.clean_bbox(instance.ll_bbox, target_crs)
bbox = bbox_utils.clean_bbox(instance.ll_bbox, target_crs)
elif (
instance.bbox[-1].upper() != "EPSG:3857"
and target_crs.upper() == "EPSG:3857"
and utils.exceeds_epsg3857_area_of_use(instance.bbox)
and bbox_utils.exceeds_epsg3857_area_of_use(instance.bbox)
):
# handle exceeding the area of use of the default thumb's CRS
bbox = utils.transform_bbox(utils.crop_to_3857_area_of_use(instance.bbox), target_crs)
bbox = bbox_utils.transform_bbox(bbox_utils.crop_to_3857_area_of_use(instance.bbox), target_crs)
else:
bbox = utils.transform_bbox(instance.bbox, target_crs)
bbox = bbox_utils.transform_bbox(instance.bbox, target_crs)
elif isinstance(instance, Map):
for maplayer in instance.maplayers.filter(visibility=True).order_by("order").iterator():
if maplayer.dataset and maplayer.dataset.sourcetype == SOURCE_TYPE_REMOTE and not maplayer.dataset.ows_url:
Expand Down Expand Up @@ -335,29 +338,9 @@ def _datasets_locations(
]
)

if compute_bbox:
if dataset.ll_bbox_polygon:
dataset_bbox = utils.clean_bbox(dataset.ll_bbox, target_crs)
elif (
dataset.bbox[-1].upper() != "EPSG:3857"
and target_crs.upper() == "EPSG:3857"
and utils.exceeds_epsg3857_area_of_use(dataset.bbox)
):
# handle exceeding the area of use of the default thumb's CRS
dataset_bbox = utils.transform_bbox(utils.crop_to_3857_area_of_use(dataset.bbox), target_crs)
else:
dataset_bbox = utils.transform_bbox(dataset.bbox, target_crs)

if not bbox:
bbox = dataset_bbox
else:
# dataset's BBOX: (left, right, bottom, top)
bbox = [
min(bbox[0], dataset_bbox[0]),
max(bbox[1], dataset_bbox[1]),
min(bbox[2], dataset_bbox[2]),
max(bbox[3], dataset_bbox[3]),
]
if compute_bbox:
instance.compute_bbox(target_crs)
bbox = instance.bbox

if bbox and len(bbox) < 5:
bbox = list(bbox) + [target_crs] # convert bbox to list, if it's tuple
Expand Down
Loading

0 comments on commit 5bcbd82

Please sign in to comment.