From 185af604ad5a9e416641de1c1d7dfa6d18c19d9e Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 12 Jun 2026 09:07:52 -0500 Subject: [PATCH] Use reference URLs for resource visuals --- .../endpoint_modules/resources/static_map.py | 64 +++++++--- backend/app/api/v1/presenters/resource.py | 38 ++++-- backend/app/api/v1/utils.py | 21 ++++ backend/app/services/image_service.py | 6 + backend/app/services/static_map_service.py | 79 +++++++++++++ .../tests/api/v1/test_resource_presenter.py | 45 ++++++++ .../tests/api/v1/test_static_map_endpoints.py | 109 +++++++++++++++++- backend/tests/api/v1/test_utils.py | 33 ++++++ backend/tests/services/test_image_service.py | 51 ++++++++ .../tests/services/test_static_map_service.py | 63 ++++++++++ 10 files changed, 480 insertions(+), 29 deletions(-) diff --git a/backend/app/api/v1/endpoint_modules/resources/static_map.py b/backend/app/api/v1/endpoint_modules/resources/static_map.py index b5d11bf..aea7880 100644 --- a/backend/app/api/v1/endpoint_modules/resources/static_map.py +++ b/backend/app/api/v1/endpoint_modules/resources/static_map.py @@ -5,6 +5,7 @@ from sqlalchemy.sql import select from app.services.cache_service import alias_redirect_cache_control_header +from app.services.distribution_repository import fetch_distribution_context from app.services.static_map_service import StaticMapService from db.models import resources @@ -40,6 +41,31 @@ async def get_resource_static_map( """Compatibility route for the geometry-overlay static map asset.""" try: map_service = StaticMapService() + async with async_session() as session: + query = select( + resources.c.id, + resources.c.locn_geometry, + resources.c.dcat_bbox, + resources.c.dct_references_s, + ).where(resources.c.id == id) + result = await session.execute(query) + row = result.fetchone() + + if not row: + return _svg_placeholder(title="Map unavailable", subtitle="Resource not found") + + resource_dict = dict(row._mapping) + distribution_context = await fetch_distribution_context(id) + if external_static_map_url := map_service.external_static_map_url( + resource_dict, + distribution_context=distribution_context, + ): + return RedirectResponse( + url=external_static_map_url, + status_code=302, + headers={"Cache-Control": "no-store"}, + ) + hot_map_hash = await map_service.materialize_cached_variant( id, variant=map_service.geometry_variant(), @@ -54,18 +80,8 @@ async def get_resource_static_map( }, ) - async with async_session() as session: - query = select(resources.c.id, resources.c.locn_geometry, resources.c.dcat_bbox).where( - resources.c.id == id - ) - result = await session.execute(query) - row = result.fetchone() - - if not row: - return _svg_placeholder(title="Map unavailable", subtitle="Resource not found") - - resolved_id = str(row._mapping["id"]) - geometry = row._mapping.get("locn_geometry") or row._mapping.get("dcat_bbox") + resolved_id = str(resource_dict["id"]) + geometry = resource_dict.get("locn_geometry") or resource_dict.get("dcat_bbox") source_signature = map_service.geometry_signature(geometry) current_map_hash = await map_service.materialize_cached_variant( id, @@ -122,9 +138,12 @@ async def get_resource_static_map_no_cache( try: # Fetch geometry directly async with async_session() as session: - query = select(resources.c.id, resources.c.locn_geometry, resources.c.dcat_bbox).where( - resources.c.id == id - ) + query = select( + resources.c.id, + resources.c.locn_geometry, + resources.c.dcat_bbox, + resources.c.dct_references_s, + ).where(resources.c.id == id) result = await session.execute(query) row = result.fetchone() @@ -132,10 +151,21 @@ async def get_resource_static_map_no_cache( return _svg_placeholder(title="Map unavailable", subtitle="Resource not found") resource_dict = dict(row._mapping) - geometry = resource_dict.get("locn_geometry") or resource_dict.get("dcat_bbox") - # Generate map synchronously and update cache (geometry or global) map_service = StaticMapService() + distribution_context = await fetch_distribution_context(id) + if external_static_map_url := map_service.external_static_map_url( + resource_dict, + distribution_context=distribution_context, + ): + return RedirectResponse( + url=external_static_map_url, + status_code=302, + headers={"Cache-Control": "no-store"}, + ) + + # Generate map synchronously and update cache (geometry or global) + geometry = resource_dict.get("locn_geometry") or resource_dict.get("dcat_bbox") source_signature = map_service.geometry_signature(geometry) if not geometry: map_bytes = map_service.generate_global_map(id, source_signature=source_signature) diff --git a/backend/app/api/v1/presenters/resource.py b/backend/app/api/v1/presenters/resource.py index c0a25d3..62cc4c5 100644 --- a/backend/app/api/v1/presenters/resource.py +++ b/backend/app/api/v1/presenters/resource.py @@ -250,7 +250,11 @@ async def present_full( allow_resource_fallback=True, ) self._attach_allmaps(resource, allmaps_attributes) - self._attach_static_map(resource, resource_dict) + self._attach_static_map( + resource, + resource_dict, + distribution_context=distribution_context, + ) if include_similar_items: resource = await api_utils.add_similar_items_to_resource( @@ -391,7 +395,11 @@ async def present_search_result( allow_resource_fallback=not hot_only_thumbnail_url, ) self._attach_allmaps(resource, allmaps_attributes) - self._attach_static_map(resource, resource_dict) + self._attach_static_map( + resource, + resource_dict, + distribution_context=distribution_context, + ) return resource @@ -511,15 +519,25 @@ def _attach_allmaps( resource["meta"].setdefault("ui", {}) resource["meta"]["ui"]["allmaps"] = allmaps_attributes - def _attach_static_map(self, resource: dict[str, Any], resource_dict: dict[str, Any]) -> None: + def _attach_static_map( + self, + resource: dict[str, Any], + resource_dict: dict[str, Any], + *, + distribution_context: DistributionContext | None = None, + ) -> None: api_utils = _api_utils() - geometry = resource_dict.get("locn_geometry") or resource_dict.get("dcat_bbox") - if not geometry: - return - - static_map_url = api_utils._hot_static_map_url( - resource_dict - ) or api_utils._build_static_map_url(resource_dict["id"]) + static_map_url = api_utils._reference_static_map_url( + resource_dict, + distribution_context=distribution_context, + ) + if not static_map_url: + geometry = resource_dict.get("locn_geometry") or resource_dict.get("dcat_bbox") + if not geometry: + return + static_map_url = api_utils._hot_static_map_url( + resource_dict + ) or api_utils._build_static_map_url(resource_dict["id"]) resource.setdefault("meta", {}) resource["meta"].setdefault("ui", {}) diff --git a/backend/app/api/v1/utils.py b/backend/app/api/v1/utils.py index 046add0..8eabf0b 100644 --- a/backend/app/api/v1/utils.py +++ b/backend/app/api/v1/utils.py @@ -150,6 +150,27 @@ def _build_static_map_asset_url(map_hash: str, *, kind: str | None = None) -> st return f"{asset_url}?kind={kind}" if kind else asset_url +def _reference_static_map_url( + resource_dict: Dict[str, Any], + *, + distribution_context: DistributionContext | None = None, +) -> Optional[str]: + try: + from app.services.static_map_service import StaticMapService + + return StaticMapService().external_static_map_url( + resource_dict, + distribution_context=distribution_context, + ) + except Exception as exc: + logger.debug( + "Failed resolving reference static map for %s: %s", + resource_dict.get("id"), + exc, + ) + return None + + def _hot_static_map_url(resource_dict: Dict[str, Any]) -> Optional[str]: geometry = resource_dict.get("locn_geometry") or resource_dict.get("dcat_bbox") if not geometry: diff --git a/backend/app/services/image_service.py b/backend/app/services/image_service.py index 89773bb..6ed591e 100644 --- a/backend/app/services/image_service.py +++ b/backend/app/services/image_service.py @@ -882,6 +882,9 @@ def _first_url(self, uri: str, references: Optional[Dict[str, Any]] = None) -> O records = self.by_uri.get(uri, []) if records: return records[0].url + legacy_references = self._parse_legacy_references() + if legacy_references: + return self._first_url(uri, references=legacy_references) else: # If explicit references are provided, consult those first val = references.get(uri) @@ -923,6 +926,9 @@ def _all_reference_urls(self, references: Optional[Dict[str, Any]] = None) -> Li for records in self.by_uri.values(): for record in records: urls.append(record.url) + legacy_references = self._parse_legacy_references() + if legacy_references: + urls.extend(self._all_reference_urls(references=legacy_references)) return urls def _queue_thumbnail_processing(self, thumbnail_url: str, doc_id: str) -> None: diff --git a/backend/app/services/static_map_service.py b/backend/app/services/static_map_service.py index 23bae93..684de87 100644 --- a/backend/app/services/static_map_service.py +++ b/backend/app/services/static_map_service.py @@ -488,6 +488,85 @@ def centered_basemap_signature(self, *, latitude: float, longitude: float, zoom: ) return hashlib.sha256(payload.encode("utf-8")).hexdigest() + def external_static_map_url( + self, + resource_dict: Dict[str, Any] | None, + *, + distribution_context: Any | None = None, + ) -> Optional[str]: + """ + Return an explicit schema.org hasMap URL. + + Structured distribution rows are authoritative. Legacy dct_references_s + is only a fallback for resources that do not have a distribution-derived + hasMap URL. + """ + for key in ("http://schema.org/hasMap", "https://schema.org/hasMap"): + if url := self._first_distribution_url(distribution_context, key): + return url + + references = self._parse_reference_payload(resource_dict) + if not references: + return None + + for key in ("http://schema.org/hasMap", "https://schema.org/hasMap"): + if url := self._first_reference_url(references, key): + return url + return None + + def _parse_reference_payload( + self, resource_dict: Dict[str, Any] | None + ) -> Optional[Dict[str, Any]]: + if not resource_dict: + return None + raw = resource_dict.get("dct_references_s") + if not raw: + return None + if isinstance(raw, dict): + references = raw + elif isinstance(raw, str): + try: + references = json.loads(raw) + except (json.JSONDecodeError, TypeError): + return None + else: + return None + return references if isinstance(references, dict) else None + + def _first_reference_url(self, references: Dict[str, Any], uri: str) -> Optional[str]: + value = references.get(uri) + return self._clean_external_static_map_url(self._reference_url_value(value)) + + def _first_distribution_url(self, distribution_context: Any | None, uri: str) -> Optional[str]: + if not distribution_context: + return None + records_by_uri = getattr(distribution_context, "by_uri", {}) or {} + records = records_by_uri.get(uri, []) + for record in records: + if url := self._clean_external_static_map_url(getattr(record, "url", None)): + return url + return None + + def _reference_url_value(self, value: Any) -> Optional[str]: + if isinstance(value, str): + return value + if isinstance(value, dict): + candidate = value.get("url") or value.get("@id") or value.get("id") + return candidate if isinstance(candidate, str) else None + if isinstance(value, list): + for item in value: + if candidate := self._reference_url_value(item): + return candidate + return None + + def _clean_external_static_map_url(self, url: Optional[str]) -> Optional[str]: + if not isinstance(url, str): + return None + cleaned = url.strip() + if cleaned.startswith(("http://", "https://")): + return cleaned + return None + def get_asset_hash_sync( self, resource_id: str, diff --git a/backend/tests/api/v1/test_resource_presenter.py b/backend/tests/api/v1/test_resource_presenter.py index d125c12..e8b2683 100644 --- a/backend/tests/api/v1/test_resource_presenter.py +++ b/backend/tests/api/v1/test_resource_presenter.py @@ -17,6 +17,51 @@ def _thumbnail_url(item, distribution_context=None, hot_only=False): return {**item, "ui_thumbnail_url": "https://images.example.edu/res-1-thumb.jpg"} +def test_resource_presenter_static_map_uses_schema_has_map_without_geometry(): + presenter = ResourcePresenter(session=None) + external_map_url = "https://maps.example.edu/static/res-1.png" + resource = {} + + presenter._attach_static_map( + resource, + { + "id": "res-1", + "dct_references_s": f'{{"http://schema.org/hasMap": "{external_map_url}"}}', + "locn_geometry": None, + "dcat_bbox": None, + }, + ) + + assert resource["meta"]["ui"]["static_map"] == external_map_url + + +def test_resource_presenter_static_map_prefers_distribution_has_map_over_legacy_reference(): + presenter = ResourcePresenter(session=None) + distribution_map_url = "https://maps.example.edu/static/distribution.png" + legacy_map_url = "https://maps.example.edu/static/legacy.png" + distribution_context = SimpleNamespace( + by_uri={ + "http://schema.org/hasMap": [ + SimpleNamespace(url=distribution_map_url), + ] + } + ) + resource = {} + + presenter._attach_static_map( + resource, + { + "id": "res-1", + "dct_references_s": f'{{"http://schema.org/hasMap": "{legacy_map_url}"}}', + "locn_geometry": None, + "dcat_bbox": None, + }, + distribution_context=distribution_context, + ) + + assert resource["meta"]["ui"]["static_map"] == distribution_map_url + + @pytest.mark.asyncio async def test_resource_presenter_full_profile_contract_snapshot(): presenter = ResourcePresenter(session=None) diff --git a/backend/tests/api/v1/test_static_map_endpoints.py b/backend/tests/api/v1/test_static_map_endpoints.py index 814f450..d702365 100644 --- a/backend/tests/api/v1/test_static_map_endpoints.py +++ b/backend/tests/api/v1/test_static_map_endpoints.py @@ -7,6 +7,8 @@ """ import io +import json +from types import SimpleNamespace from unittest.mock import ANY, AsyncMock, MagicMock, call, patch import pytest @@ -47,6 +49,7 @@ def create_static_map_service_mock(*, basemap_bytes=None, geometry_bytes=None) - svc.geometry_variant.return_value = "geometry" svc.geometry_signature.return_value = "geometry-signature" svc.centered_basemap_signature.return_value = "centered-signature" + svc.external_static_map_url.return_value = None return svc @@ -63,6 +66,16 @@ def client(app): return TestClient(app) +@pytest.fixture(autouse=True) +def resource_static_map_distribution_context(): + mock_fetch = AsyncMock(return_value=SimpleNamespace(by_uri={})) + with patch( + "app.api.v1.endpoint_modules.resources.static_map.fetch_distribution_context", + mock_fetch, + ): + yield mock_fetch + + class TestStaticMapsEndpoint: @patch("app.api.v1.endpoint_modules.static_maps._fetch_resource_dict") def test_get_static_map_latest_alias_short_circuits_before_db(self, mock_resource, client): @@ -284,19 +297,71 @@ def test_get_institution_static_map_generates_and_serves_png(self, client): class TestResourceStaticMapEndpoint: @patch("app.api.v1.endpoint_modules.resources.static_map.async_session") - def test_resource_static_map_latest_alias_short_circuits_before_db(self, mock_session, client): + def test_resource_static_map_uses_schema_has_map_before_generated_alias( + self, mock_session, client, resource_static_map_distribution_context + ): + external_map_url = "https://maps.example.edu/static/resource.png" + mock_session_instance = AsyncMock() + mock_session.return_value.__aenter__.return_value = mock_session_instance + + mock_row = MagicMock() + resource_payload = { + "id": "test-resource-id", + "locn_geometry": None, + "dcat_bbox": None, + "dct_references_s": json.dumps({"http://schema.org/hasMap": external_map_url}), + } + mock_row._mapping = resource_payload + mock_result = MagicMock() + mock_result.fetchone.return_value = mock_row + mock_session_instance.execute.return_value = mock_result + + with patch("app.api.v1.endpoint_modules.resources.static_map.StaticMapService") as svc_cls: + svc = create_static_map_service_mock() + svc.external_static_map_url.return_value = external_map_url + svc_cls.return_value = svc + + resp = client.get("/resources/test-resource-id/static-map", follow_redirects=False) + + assert resp.status_code == 302 + assert resp.headers["location"] == external_map_url + assert resp.headers["cache-control"] == "no-store" + resource_static_map_distribution_context.assert_awaited_once_with("test-resource-id") + svc.external_static_map_url.assert_called_once_with( + resource_payload, + distribution_context=resource_static_map_distribution_context.return_value, + ) + svc.materialize_cached_variant.assert_not_awaited() + + @patch("app.api.v1.endpoint_modules.resources.static_map.async_session") + def test_resource_static_map_latest_alias_redirects_when_no_schema_has_map( + self, mock_session, client + ): asset_hash = "deadbeef" * 8 + mock_session_instance = AsyncMock() + mock_session.return_value.__aenter__.return_value = mock_session_instance + + mock_row = MagicMock() + mock_row._mapping = { + "id": "test-resource-id", + "locn_geometry": "ENVELOPE(-10,10,10,-10)", + "dcat_bbox": "ENVELOPE(-10,10,10,-10)", + "dct_references_s": "{}", + } + mock_result = MagicMock() + mock_result.fetchone.return_value = mock_row + mock_session_instance.execute.return_value = mock_result with patch("app.api.v1.endpoint_modules.resources.static_map.StaticMapService") as svc_cls: svc = create_static_map_service_mock() svc.materialize_cached_variant = AsyncMock(return_value=asset_hash) + svc.external_static_map_url.return_value = None svc_cls.return_value = svc resp = client.get("/resources/test-resource-id/static-map", follow_redirects=False) assert resp.status_code == 302 assert resp.headers["location"] == f"/api/v1/static-map-assets/{asset_hash}" - mock_session.assert_not_called() svc.materialize_cached_variant.assert_awaited_once_with( "test-resource-id", variant="geometry", @@ -414,3 +479,43 @@ def test_resource_static_map_no_geometry_generates_global_map(self, mock_session assert resp.status_code == 302 assert resp.headers["location"] == "/api/v1/static-maps/no-geometry-resource/geometry" + + @patch("app.api.v1.endpoint_modules.resources.static_map.async_session") + def test_resource_static_map_no_cache_uses_schema_has_map( + self, mock_session, client, resource_static_map_distribution_context + ): + external_map_url = "https://maps.example.edu/static/no-cache.png" + mock_session_instance = AsyncMock() + mock_session.return_value.__aenter__.return_value = mock_session_instance + + mock_row = MagicMock() + resource_payload = { + "id": "test-resource-id", + "locn_geometry": "ENVELOPE(-10,10,10,-10)", + "dcat_bbox": "ENVELOPE(-10,10,10,-10)", + "dct_references_s": json.dumps({"http://schema.org/hasMap": external_map_url}), + } + mock_row._mapping = resource_payload + mock_result = MagicMock() + mock_result.fetchone.return_value = mock_row + mock_session_instance.execute.return_value = mock_result + + with patch("app.api.v1.endpoint_modules.resources.static_map.StaticMapService") as svc_cls: + svc = create_static_map_service_mock() + svc.external_static_map_url.return_value = external_map_url + svc_cls.return_value = svc + + resp = client.get( + "/resources/test-resource-id/static-map/no-cache", + follow_redirects=False, + ) + + assert resp.status_code == 302 + assert resp.headers["location"] == external_map_url + assert resp.headers["cache-control"] == "no-store" + resource_static_map_distribution_context.assert_awaited_once_with("test-resource-id") + svc.external_static_map_url.assert_called_once_with( + resource_payload, + distribution_context=resource_static_map_distribution_context.return_value, + ) + svc.generate_map.assert_not_called() diff --git a/backend/tests/api/v1/test_utils.py b/backend/tests/api/v1/test_utils.py index a7cf4cb..05e1d4e 100644 --- a/backend/tests/api/v1/test_utils.py +++ b/backend/tests/api/v1/test_utils.py @@ -13,6 +13,7 @@ from app.api.v1.utils import ( _hot_resource_class_icon_url, _hot_static_map_url, + _reference_static_map_url, add_citations, add_thumbnail_url, add_ui_attributes, @@ -200,6 +201,38 @@ def test_add_thumbnail_url_hot_only_uses_hot_thumbnail_url(self): class TestHotVisualAssetUrls: + def test_reference_static_map_url_uses_schema_has_map_without_geometry(self): + url = "https://maps.example.edu/static/no-geometry.png" + resource = { + "id": "resource-1", + "dct_references_s": f'{{"http://schema.org/hasMap": "{url}"}}', + } + + assert _reference_static_map_url(resource) == url + + def test_reference_static_map_url_prefers_distribution_has_map_over_legacy_reference(self): + distribution_url = "https://maps.example.edu/static/distribution.png" + legacy_url = "https://maps.example.edu/static/legacy.png" + distribution_context = SimpleNamespace( + by_uri={ + "http://schema.org/hasMap": [ + SimpleNamespace(url=distribution_url), + ] + } + ) + resource = { + "id": "resource-1", + "dct_references_s": f'{{"http://schema.org/hasMap": "{legacy_url}"}}', + } + + assert ( + _reference_static_map_url( + resource, + distribution_context=distribution_context, + ) + == distribution_url + ) + def test_hot_static_map_url_rehydrates_alias_without_redis_asset_body(self): class FakeStaticMapService: def __init__(self): diff --git a/backend/tests/services/test_image_service.py b/backend/tests/services/test_image_service.py index 3b8af29..99ae1d2 100644 --- a/backend/tests/services/test_image_service.py +++ b/backend/tests/services/test_image_service.py @@ -4,6 +4,7 @@ import hashlib import json +from types import SimpleNamespace from unittest.mock import patch import pytest @@ -333,6 +334,28 @@ def test_get_thumbnail_source_url_thumbnailurl_overrides_schema_image(self): except Exception as e: assert _is_redis_connection_error(e) + def test_get_thumbnail_source_url_prefers_distribution_thumbnail_over_dct_references(self): + """Use legacy thumbnailUrl only after distribution-derived thumbnail sources.""" + distribution_url = "https://example.com/distribution-thumb.jpg" + legacy_url = "https://example.com/legacy-thumb.jpg" + metadata = { + "id": "test-doc", + "dct_references_s": json.dumps({"http://schema.org/thumbnailUrl": legacy_url}), + } + distribution_context = SimpleNamespace( + by_uri={ + "http://schema.org/thumbnailUrl": [ + SimpleNamespace(url=distribution_url), + ] + } + ) + + try: + service = ImageService(metadata, distribution_context=distribution_context) + assert service._get_thumbnail_source_url() == distribution_url + except Exception as e: + assert _is_redis_connection_error(e) + def test_get_thumbnail_source_url_iiif_image(self): """Test extraction of IIIF image URL.""" metadata = {"id": "test-doc"} @@ -852,6 +875,10 @@ def test_get_hot_thumbnail_url_reuses_current_alias_hash(self): "app.services.image_service.thumbnail_alias_service.get_hash_sync", return_value=image_hash, ), + patch( + "app.services.image_service.thumbnail_state_service.get_state_sync", + return_value=None, + ), patch.object( ImageService, "_candidate_cached_thumbnail_hash_sync", @@ -960,6 +987,30 @@ def test_get_thumbnail_source_url_fallback_to_dct_references_s(self): except Exception as e: assert _is_redis_connection_error(e) + def test_get_thumbnail_source_url_checks_dct_references_when_distributions_exist(self): + """Fallback to dct_references_s if distribution rows lack a thumbnail reference.""" + thumbnail_url = "https://images.example.edu/resource-thumb.jpg" + metadata = { + "id": "resource-with-distributions", + "dct_references_s": json.dumps( + {"http://schema.org/thumbnailUrl": thumbnail_url} + ), + } + distribution_context = SimpleNamespace( + by_uri={ + "http://schema.org/url": [ + SimpleNamespace(url="https://catalog.example.edu/resource") + ] + } + ) + + try: + service = ImageService(metadata, distribution_context=distribution_context) + source = service._get_thumbnail_source_url() + assert source == thumbnail_url + except Exception as e: + assert _is_redis_connection_error(e) + def test_get_thumbnail_url_references_as_dict(self): """Test handling when references is already a dict.""" metadata = { diff --git a/backend/tests/services/test_static_map_service.py b/backend/tests/services/test_static_map_service.py index 5e06826..7c3759e 100644 --- a/backend/tests/services/test_static_map_service.py +++ b/backend/tests/services/test_static_map_service.py @@ -1,7 +1,9 @@ +import json from unittest.mock import MagicMock, call, patch from app.services.static_map_service import StaticMapService from app.services.visual_asset_cache import cache_visual_asset +from tests.utils.distribution_helpers import make_distribution_context, make_distribution_record def test_get_asset_hash_recovers_alias_from_durable_link(): @@ -184,6 +186,67 @@ def set(self, _key, _value): mock_sleep.assert_called_once() +def test_external_static_map_url_reads_schema_has_map_from_dct_references_string(): + service = StaticMapService() + map_url = "https://maps.example.edu/static/res-1.png" + resource = { + "id": "resource-1", + "dct_references_s": json.dumps({"http://schema.org/hasMap": map_url}), + } + + assert service.external_static_map_url(resource) == map_url + + +def test_external_static_map_url_accepts_https_schema_has_map_dict_value(): + service = StaticMapService() + map_url = "https://maps.example.edu/static/res-2.png" + resource = { + "id": "resource-2", + "dct_references_s": {"https://schema.org/hasMap": {"url": map_url}}, + } + + assert service.external_static_map_url(resource) == map_url + + +def test_external_static_map_url_prefers_distribution_has_map_over_dct_references(): + service = StaticMapService() + resource_id = "resource-3" + distribution_url = "https://maps.example.edu/static/distribution.png" + legacy_url = "https://maps.example.edu/static/legacy.png" + distribution_context = make_distribution_context( + resource_id, + [ + make_distribution_record( + resource_id, + "http://schema.org/hasMap", + distribution_url, + ) + ], + ) + resource = { + "id": resource_id, + "dct_references_s": json.dumps({"http://schema.org/hasMap": legacy_url}), + } + + assert ( + service.external_static_map_url( + resource, + distribution_context=distribution_context, + ) + == distribution_url + ) + + +def test_external_static_map_url_ignores_non_http_has_map_values(): + service = StaticMapService() + resource = { + "id": "resource-4", + "dct_references_s": json.dumps({"http://schema.org/hasMap": "urn:not-a-url"}), + } + + assert service.external_static_map_url(resource) is None + + def test_generate_map_uses_global_fallback_for_unrenderable_polar_extent(): service = StaticMapService() geometry = {