From 493b553ca9f66a1e5097102d86d62e820b9c63c9 Mon Sep 17 00:00:00 2001 From: Brian Kelly Date: Thu, 7 Nov 2024 12:58:26 -0600 Subject: [PATCH 01/16] Leaflet map with WMS visuals and clicking for feature attributes --- invenio.cfg | 70 +++++++++ .../semantic-ui/css/ultraviolet/.gitkeep | 0 .../semantic-ui/css/ultraviolet/geoserver.css | 8 ++ .../semantic-ui/js/ultraviolet/geoserver.js | 135 ++++++++++++++++++ site/ultraviolet/geoserver/__init__.py | 0 .../geoserver/describe_feature_type.py | 27 ++++ .../ultraviolet/geoserver/get_feature_info.py | 35 +++++ .../ultraviolet/records/details/map.html | 25 ++++ site/ultraviolet/views.py | 14 +- site/ultraviolet/webpack.py | 7 +- .../records/details/description.html | 25 ++++ tests/ui/test_map_view.py | 22 +++ 12 files changed, 366 insertions(+), 2 deletions(-) create mode 100644 site/ultraviolet/assets/semantic-ui/css/ultraviolet/.gitkeep create mode 100644 site/ultraviolet/assets/semantic-ui/css/ultraviolet/geoserver.css create mode 100644 site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js create mode 100644 site/ultraviolet/geoserver/__init__.py create mode 100644 site/ultraviolet/geoserver/describe_feature_type.py create mode 100644 site/ultraviolet/geoserver/get_feature_info.py create mode 100644 site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html create mode 100644 templates/semantic-ui/invenio_app_rdm/records/details/description.html create mode 100644 tests/ui/test_map_view.py diff --git a/invenio.cfg b/invenio.cfg index 5482f1e..78a0503 100644 --- a/invenio.cfg +++ b/invenio.cfg @@ -6,6 +6,7 @@ This file was automatically generated by 'invenio-cli init'. For the full list of settings and their values, see https://inveniordm.docs.cern.ch/reference/configuration/. """ +from invenio_records_resources.services.custom_fields import TextCF #required to use custom permissions from ultraviolet_saml.handlers import acs_handler_factory from ultraviolet_permissions.policies import UltraVioletPermissionPolicy @@ -71,6 +72,12 @@ APP_DEFAULT_SECURE_HEADERS = { "blob:", # for pdf preview # Add your own policies here (e.g. analytics) ], + 'img-src': [ + "'self'", + 'data:', + "https://*.openstreetmap.org", + "https://maps-public.geo.nyu.edu/geoserver/sdr/wms", + ] }, 'content_security_policy_report_only': False, 'content_security_policy_report_uri': None, @@ -451,3 +458,66 @@ NYU_LIBRARIES_HOMEPAGE = 'https://library.nyu.edu' # Max file size for displaying download buttons and link (50 GB) MAX_FILE_SIZE = 50 * 1024 * 1024 * 1024 + +RDM_NAMESPACES = { + "geoserver": "https://geoserver.org/" +} + +RDM_CUSTOM_FIELDS = [ + TextCF(name="geoserver:wms_url"), + TextCF(name="geoserver:wfs_url"), + TextCF(name="geoserver:layer_name"), + TextCF(name="geoserver:bounds"), +] + +RDM_CUSTOM_FIELDS_UI = [ + { + "section": _("GeoServer"), + "fields": [ + dict( + field="geoserver:wms_url", + ui_widget="Input", + props=dict( + label="WMS URL", + placeholder="https://maps-public.geo.nyu.edu/geoserver/sdr/wms", + icon="linkify", + description="GeoServer WMS Service Base URL", + required=False + ) + ), + dict( + field="geoserver:wfs_url", + ui_widget="Input", + props=dict( + label="WFS URL", + placeholder="https://maps-public.geo.nyu.edu/geoserver/sdr/wfs", + icon="linkify", + description="GeoServer WFS Service Base URL", + required=False + ) + ), + dict( + field="geoserver:layer_name", + ui_widget="Input", + props=dict( + label="Layer Name", + placeholder="sdr:nyu_2451_12345", + icon="pencil", + description="Name of the GeoServer Layer this data can be found in", + required=False + ) + ), + dict( + field="geoserver:bounds", + ui_widget="Input", + props=dict( + label="Bounds", + placeholder="ENVELOPE(-178.2176, -66.969275, 71.406235818, 18.921781818)", + icon="pencil", + description="The envelope for the bounds of this layer", + required=False + ) + ) + ] + } +] diff --git a/site/ultraviolet/assets/semantic-ui/css/ultraviolet/.gitkeep b/site/ultraviolet/assets/semantic-ui/css/ultraviolet/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/site/ultraviolet/assets/semantic-ui/css/ultraviolet/geoserver.css b/site/ultraviolet/assets/semantic-ui/css/ultraviolet/geoserver.css new file mode 100644 index 0000000..ca05ebf --- /dev/null +++ b/site/ultraviolet/assets/semantic-ui/css/ultraviolet/geoserver.css @@ -0,0 +1,8 @@ +@import "leaflet/dist/leaflet.css"; +@import 'ol/ol.css'; + +#map { + width: 100%; + height: 480px; + margin-top: 1em; +} diff --git a/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js b/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js new file mode 100644 index 0000000..4a890c0 --- /dev/null +++ b/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js @@ -0,0 +1,135 @@ +import L from "leaflet" +import "leaflet/dist/leaflet.css"; + +const populateAttributeTable = (data) => { + const attributesElement = document.getElementById("attributes"); + attributesElement.innerHTML = ''; + + Object.keys(data.properties).sort().forEach(property => { + const nameTd = document.createElement('td'); + nameTd.textContent = property + const typeTd = document.createElement("td") + typeTd.textContent = data.properties[property] + + const tr = document.createElement("tr") + tr.appendChild(nameTd) + tr.appendChild(typeTd) + attributesElement.appendChild(tr) + }) + +} + +const describeFeatureType = (wfsUrl, layerNames) => { + const attributesElement = document.getElementById("attributes"); + + const formData = new FormData(); + formData.append("url", wfsUrl); + formData.append("layers", layerNames); + + fetch("/geoserver/describe_feature_type", { + method: "POST", body: formData + }) + .then(response => response.json()) + .then(data => { + const attributes = data.featureTypes[0].properties + attributes.forEach(attribute => { + const nameTd = document.createElement('td'); + nameTd.textContent = attribute.name + const typeTd = document.createElement("td") + typeTd.textContent = attribute.localType + + const tr = document.createElement("tr") + tr.appendChild(nameTd) + tr.appendChild(typeTd) + attributesElement.appendChild(tr) + }) + }) + .catch(error => { + console.error('Error:', error); + }); +}; + +const addInspection = (map, url, layerId) => { + map.on("click", async (e) => { + const attributesElement = document.getElementById("attributes"); + attributesElement.innerHTML = 'Loading...'; + + const wmsoptions = { + url: url, + layers: layerId, + bbox: map.getBounds().toBBoxString(), + width: Math.round(document.getElementById("map").clientWidth), + height: Math.round(document.getElementById("map").clientHeight), + query_layers: layerId, + x: Math.round(e.containerPoint.x), + y: Math.round(e.containerPoint.y), + }; + + try { + const response = await fetch("/geoserver/get_feature_info", { + method: "POST", headers: { + "Content-Type": "application/json", + }, body: JSON.stringify(wmsoptions), + }); + + if (!response.ok) throw new Error("Network response was not ok."); + + const response_data = await response.json(); + + if (response_data.hasOwnProperty("error") || response_data.hasOwnProperty('exceptions') || response_data.features.length === 0) { + const attributesElement = document.getElementById("attributes"); + attributesElement.innerHTML = 'No feature found'; + + return; + } + const data = response_data.features[0]; + // if (!data.isHTML) overlayLayer(map, data, layer); + + populateAttributeTable(data); + } catch (error) { + console.error("Fetch error: ", error); + } + }); +} + +document.addEventListener("DOMContentLoaded", async () => { + const attributesElement = document.getElementById("attributes"); + const wfsUrl = attributesElement.getAttribute("data-wfs-url"); + const layerNames = attributesElement.getAttribute("data-layer-names"); + + + const mapElement = document.getElementById("map"); + const baseUrl = mapElement.getAttribute("data-base-url") + const layerName = mapElement.getAttribute("data-layer-name") + const bounds = mapElement.getAttribute("data-bounds") + + const map = L.map('map').setView([0, 0], 13); + + L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 19, attribution: '© OpenStreetMap' + }).addTo(map); + + const wmsLayer = L.tileLayer.wms(baseUrl, { + layers: layerName, format: 'image/png', transparent: true, + }); + + wmsLayer.addTo(map); + wmsLayer.setOpacity(0.75); + + const regex = /ENVELOPE\(([-\d.]+), ([-\d.]+), ([-\d.]+), ([-\d.]+)\)/; + const match = bounds.match(regex); + + if (match) { + const minLon = parseFloat(match[1]); + const maxLon = parseFloat(match[2]); + const minLat = parseFloat(match[3]); + const maxLat = parseFloat(match[4]); + + const bounds = [[minLat, minLon], [maxLat, maxLon]]; + + map.fitBounds(bounds); + } + + addInspection(map, wfsUrl, layerNames); + describeFeatureType(wfsUrl, layerNames, attributesElement); +}); diff --git a/site/ultraviolet/geoserver/__init__.py b/site/ultraviolet/geoserver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/site/ultraviolet/geoserver/describe_feature_type.py b/site/ultraviolet/geoserver/describe_feature_type.py new file mode 100644 index 0000000..4862e0f --- /dev/null +++ b/site/ultraviolet/geoserver/describe_feature_type.py @@ -0,0 +1,27 @@ +import urllib + +from flask import request, Response +from flask.views import MethodView + +import requests + + +class DescribeFeatureType(MethodView): + """GeoServer view.""" + + def post(self): + """Return JSON""" + url = request.form.get('url', default=None) + layers = request.form.get('layers', default=None) + + query_string = urllib.parse.urlencode({ + "outputFormat": "application/json", + "request": "DescribeFeatureType", + "service": "WFS", + "typeName": layers, + "version": "1.1.0", + }) + + response = requests.get("{0}?{1}".format(url, query_string)) + + return Response(response.text, mimetype='application/json') diff --git a/site/ultraviolet/geoserver/get_feature_info.py b/site/ultraviolet/geoserver/get_feature_info.py new file mode 100644 index 0000000..bd42bc7 --- /dev/null +++ b/site/ultraviolet/geoserver/get_feature_info.py @@ -0,0 +1,35 @@ +import urllib + +from flask import request, Response +from flask.views import MethodView + +import requests + + +class GetFeatureInfo(MethodView): + """GeoServer view.""" + + def post(self): + """Return JSON""" + data = request.get_json() + + url = data.get('url', None) + query_string = urllib.parse.urlencode({ + "service": "WMS", + "version": "1.1.1", + "request": "GetFeatureInfo", + "layers": data.get('layers', None), + "query_layers": data.get('layers', None), + "bbox": data.get('bbox', None), + "width": data.get('width', None), + "height": data.get('height', None), + "x": data.get('x', None), + "y": data.get('y', None), + "srs": "EPSG:4326", + "info_format": "application/json", + "styles": "" + }) + + response = requests.get("{0}?{1}".format(url, query_string)) + + return Response(response.text, mimetype='application/json') diff --git a/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html b/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html new file mode 100644 index 0000000..0ca0481 --- /dev/null +++ b/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html @@ -0,0 +1,25 @@ +{% if record.custom_fields["geoserver:wms_url"] and record.custom_fields["geoserver:layer_name"] %} +
+ +

Attributes

+ + + + + + + + +
AttributeValue
+{% endif %} + +{% block javascript %} + {{ webpack['geoserver_js.js'] }} + {{ webpack['geoserver_css.css'] }} +{% endblock %} diff --git a/site/ultraviolet/views.py b/site/ultraviolet/views.py index 89f90fa..4e21e38 100644 --- a/site/ultraviolet/views.py +++ b/site/ultraviolet/views.py @@ -2,6 +2,9 @@ from flask import Blueprint +from .geoserver.describe_feature_type import DescribeFeatureType +from .geoserver.get_feature_info import GetFeatureInfo + # # Registration # @@ -13,5 +16,14 @@ def create_blueprint(app): template_folder="./templates", ) - # Add URL rules + blueprint.add_url_rule( + "/geoserver/describe_feature_type", + view_func=DescribeFeatureType.as_view("describe_feature_type"), + ) + + blueprint.add_url_rule( + "/geoserver/get_feature_info", + view_func=GetFeatureInfo.as_view("get_feature_info"), + ) + return blueprint diff --git a/site/ultraviolet/webpack.py b/site/ultraviolet/webpack.py index c277969..d79ecfb 100644 --- a/site/ultraviolet/webpack.py +++ b/site/ultraviolet/webpack.py @@ -9,8 +9,13 @@ themes={ "semantic-ui": dict( entry={ - # Add your webpack entrypoints + "geoserver_js": "./js/ultraviolet/geoserver.js", + "geoserver_css": "./css/ultraviolet/geoserver.css", }, + dependencies={ + "leaflet": "^1.9.4", + "ol": "^10.2.1" + } ), }, ) diff --git a/templates/semantic-ui/invenio_app_rdm/records/details/description.html b/templates/semantic-ui/invenio_app_rdm/records/details/description.html new file mode 100644 index 0000000..7fd5a4b --- /dev/null +++ b/templates/semantic-ui/invenio_app_rdm/records/details/description.html @@ -0,0 +1,25 @@ +{# + Copyright (C) 2020 CERN. + Copyright (C) 2020 Northwestern University. + Copyright (C) 2021 New York University. + + Invenio RDM Records is free software; you can redistribute it and/or modify + it under the terms of the MIT License; see LICENSE file for more details. + #} + +{%- from "invenio_app_rdm/records/macros/detail.html" import show_add_descriptions %} + +{% set description = metadata.get('description') %} +{% if description %} +
+

{{ _('Description') }}

+ {# description data is being sanitized by marshmallow in the backend #} +
+

{{ description | safe }}

+
+
+{% endif %} + +{{ show_add_descriptions(record.ui.additional_descriptions) if record.ui.additional_descriptions }} + +{% include "ultraviolet/records/details/map.html" %} diff --git a/tests/ui/test_map_view.py b/tests/ui/test_map_view.py new file mode 100644 index 0000000..d1fe904 --- /dev/null +++ b/tests/ui/test_map_view.py @@ -0,0 +1,22 @@ +from invenio_access.permissions import system_identity + + +def test_map_view(app, service, minimal_record, client_with_login): + data = minimal_record.copy() + data["metadata"]["title"] = "Geospatial Data" + data["custom_fields"] = { + "geoserver:wms_url": "https://maps-public.geo.nyu.edu/geoserver/sdr/wms", + "geoserver:layer_name": "sdr:nyu_2451_34156", + "geoserver:bounds": "ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)" + } + + draft = service.create(system_identity, data) + record = service.publish(system_identity, draft.id) + + record_view = client_with_login.get("/records/" + record['id']).data + html = record_view.decode("utf-8") + + assert "Geospatial Data" in html + assert "data-base-url=\"https://maps-public.geo.nyu.edu/geoserver/sdr/wms\"" in html + assert "data-layer-name=\"sdr:nyu_2451_34156\"" in html + assert "data-bounds=\"ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)\"" in html From b70c2e0b2dbbbfb87f492f8ff3022c3248cb66c8 Mon Sep 17 00:00:00 2001 From: Brian Kelly Date: Fri, 22 Nov 2024 11:23:17 -0500 Subject: [PATCH 02/16] Styling tweaks --- invenio.cfg | 2 +- .../semantic-ui/css/ultraviolet/geoserver.css | 2 ++ .../semantic-ui/js/ultraviolet/geoserver.js | 17 ++++++++++------- .../ultraviolet/records/details/map.html | 7 +++++-- tests/ui/test_map_view.py | 2 +- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/invenio.cfg b/invenio.cfg index 78a0503..28d95b8 100644 --- a/invenio.cfg +++ b/invenio.cfg @@ -75,7 +75,7 @@ APP_DEFAULT_SECURE_HEADERS = { 'img-src': [ "'self'", 'data:', - "https://*.openstreetmap.org", + "https://*.basemaps.cartocdn.com", "https://maps-public.geo.nyu.edu/geoserver/sdr/wms", ] }, diff --git a/site/ultraviolet/assets/semantic-ui/css/ultraviolet/geoserver.css b/site/ultraviolet/assets/semantic-ui/css/ultraviolet/geoserver.css index ca05ebf..0356601 100644 --- a/site/ultraviolet/assets/semantic-ui/css/ultraviolet/geoserver.css +++ b/site/ultraviolet/assets/semantic-ui/css/ultraviolet/geoserver.css @@ -5,4 +5,6 @@ width: 100%; height: 480px; margin-top: 1em; + cursor: crosshair !important; + border: 1px solid #ced4da; } diff --git a/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js b/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js index 4a890c0..f5bf0d7 100644 --- a/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js +++ b/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js @@ -16,7 +16,6 @@ const populateAttributeTable = (data) => { tr.appendChild(typeTd) attributesElement.appendChild(tr) }) - } const describeFeatureType = (wfsUrl, layerNames) => { @@ -31,7 +30,10 @@ const describeFeatureType = (wfsUrl, layerNames) => { }) .then(response => response.json()) .then(data => { - const attributes = data.featureTypes[0].properties + const attributes = data.featureTypes[0].properties.sort((a, b) => { + return a.name.localeCompare(b.name) + }) + attributes.forEach(attribute => { const nameTd = document.createElement('td'); nameTd.textContent = attribute.name @@ -83,7 +85,6 @@ const addInspection = (map, url, layerId) => { return; } const data = response_data.features[0]; - // if (!data.isHTML) overlayLayer(map, data, layer); populateAttributeTable(data); } catch (error) { @@ -97,16 +98,18 @@ document.addEventListener("DOMContentLoaded", async () => { const wfsUrl = attributesElement.getAttribute("data-wfs-url"); const layerNames = attributesElement.getAttribute("data-layer-names"); - const mapElement = document.getElementById("map"); - const baseUrl = mapElement.getAttribute("data-base-url") + const baseUrl = mapElement.getAttribute("data-wms-url") const layerName = mapElement.getAttribute("data-layer-name") const bounds = mapElement.getAttribute("data-bounds") const map = L.map('map').setView([0, 0], 13); - L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { - maxZoom: 19, attribution: '© OpenStreetMap' + L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{retina}.png', { + attribution: '© OpenStreetMap contributors, © Carto', + maxZoom: 18, + worldCopyJump: true, + retina: "@2x", }).addTo(map); const wmsLayer = L.tileLayer.wms(baseUrl, { diff --git a/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html b/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html index 0ca0481..8c27d2f 100644 --- a/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html +++ b/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html @@ -1,11 +1,14 @@ {% if record.custom_fields["geoserver:wms_url"] and record.custom_fields["geoserver:layer_name"] %} +

Map

+
-

Attributes

+

Attributes

+ diff --git a/tests/ui/test_map_view.py b/tests/ui/test_map_view.py index d1fe904..a4aed31 100644 --- a/tests/ui/test_map_view.py +++ b/tests/ui/test_map_view.py @@ -17,6 +17,6 @@ def test_map_view(app, service, minimal_record, client_with_login): html = record_view.decode("utf-8") assert "Geospatial Data" in html - assert "data-base-url=\"https://maps-public.geo.nyu.edu/geoserver/sdr/wms\"" in html + assert "data-wms-url=\"https://maps-public.geo.nyu.edu/geoserver/sdr/wms\"" in html assert "data-layer-name=\"sdr:nyu_2451_34156\"" in html assert "data-bounds=\"ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)\"" in html From 7108560cc8df1312833b9addc2cad57904915a6b Mon Sep 17 00:00:00 2001 From: Brian Kelly Date: Wed, 4 Dec 2024 14:49:37 -0600 Subject: [PATCH 03/16] Prevent Attributes table from stacking on narrow displays --- .../templates/semantic-ui/ultraviolet/records/details/map.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html b/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html index 8c27d2f..d0ebcf4 100644 --- a/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html +++ b/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html @@ -9,7 +9,7 @@

Map

Attributes

-
+
From d430ff8d1d4ef7e797581a6d9f568f05ae57e548 Mon Sep 17 00:00:00 2001 From: Brian Kelly Date: Thu, 5 Dec 2024 08:46:48 -0600 Subject: [PATCH 04/16] Map and Attributes table design tweaks --- .../semantic-ui/css/ultraviolet/geoserver.css | 7 +++++- .../ultraviolet/records/details/map.html | 24 ++++++++++--------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/site/ultraviolet/assets/semantic-ui/css/ultraviolet/geoserver.css b/site/ultraviolet/assets/semantic-ui/css/ultraviolet/geoserver.css index 0356601..1d02a0c 100644 --- a/site/ultraviolet/assets/semantic-ui/css/ultraviolet/geoserver.css +++ b/site/ultraviolet/assets/semantic-ui/css/ultraviolet/geoserver.css @@ -3,8 +3,13 @@ #map { width: 100%; - height: 480px; + height: 440px; margin-top: 1em; cursor: crosshair !important; border: 1px solid #ced4da; } + +.attributes { + max-height: 450px; + overflow-y: scroll; +} diff --git a/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html b/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html index d0ebcf4..4e0cf69 100644 --- a/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html +++ b/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html @@ -9,17 +9,19 @@

Map

Attributes

-
Attribute
- - - - - - - -
AttributeValue
+
+ + + + + + + + +
AttributeType/Value
+
{% endif %} {% block javascript %} From f0b16e226d86ec648dc3f3c47a98ce6d7983208d Mon Sep 17 00:00:00 2001 From: Brian Kelly Date: Mon, 9 Dec 2024 08:20:22 -0600 Subject: [PATCH 05/16] More map and attribute styling tweaks --- .../semantic-ui/js/ultraviolet/geoserver.js | 5 +++- .../ultraviolet/records/details/map.html | 30 ++++++++++--------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js b/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js index f5bf0d7..023079b 100644 --- a/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js +++ b/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js @@ -113,7 +113,10 @@ document.addEventListener("DOMContentLoaded", async () => { }).addTo(map); const wmsLayer = L.tileLayer.wms(baseUrl, { - layers: layerName, format: 'image/png', transparent: true, + layers: layerName, + format: 'image/png', + transparent: true, + opacity: 0.75 }); wmsLayer.addTo(map); diff --git a/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html b/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html index 4e0cf69..1792223 100644 --- a/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html +++ b/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html @@ -7,21 +7,23 @@

Map

data-bounds="{{ record.custom_fields["geoserver:bounds"] }}" > -

Attributes

+ {% if record.custom_fields["geoserver:wfs_url"] %} +

Attributes

-
- - - - - - - - -
AttributeType/Value
-
+
+ + + + + + + + +
AttributeType/Value
+
+ {% endif %} {% endif %} {% block javascript %} From eca1901f2dc8daa8cf7f638d563955d0d209f710 Mon Sep 17 00:00:00 2001 From: Brian Kelly Date: Tue, 10 Dec 2024 15:06:42 -0600 Subject: [PATCH 06/16] Switch to booleans and config vars for GeoServer metadata and URLs --- invenio.cfg | 55 +++++++++++-------- .../ultraviolet/records/details/map.html | 10 ++-- tests/ui/test_map_view.py | 27 ++++++++- 3 files changed, 63 insertions(+), 29 deletions(-) diff --git a/invenio.cfg b/invenio.cfg index 28d95b8..ae490ee 100644 --- a/invenio.cfg +++ b/invenio.cfg @@ -6,7 +6,7 @@ This file was automatically generated by 'invenio-cli init'. For the full list of settings and their values, see https://inveniordm.docs.cern.ch/reference/configuration/. """ -from invenio_records_resources.services.custom_fields import TextCF +from invenio_records_resources.services.custom_fields import TextCF, BooleanCF #required to use custom permissions from ultraviolet_saml.handlers import acs_handler_factory from ultraviolet_permissions.policies import UltraVioletPermissionPolicy @@ -76,7 +76,8 @@ APP_DEFAULT_SECURE_HEADERS = { "'self'", 'data:', "https://*.basemaps.cartocdn.com", - "https://maps-public.geo.nyu.edu/geoserver/sdr/wms", + "https://maps-public.geo.nyu.edu", + "https://maps-restricted.geo.nyu.edu", ] }, 'content_security_policy_report_only': False, @@ -428,6 +429,8 @@ if APP_ENVIRONMENT == 'local': 'adminUV@test.com': 'adminUV' } + RATELIMIT_ENABLED = False + # UltraViolet custom vars # ======================= @@ -464,47 +467,53 @@ RDM_NAMESPACES = { } RDM_CUSTOM_FIELDS = [ - TextCF(name="geoserver:wms_url"), - TextCF(name="geoserver:wfs_url"), + BooleanCF(name="geoserver:has_wms_layer"), + BooleanCF(name="geoserver:has_wfs_layer"), TextCF(name="geoserver:layer_name"), TextCF(name="geoserver:bounds"), ] +GEOSERVER_PUBLIC_URL = "https://maps-public.geo.nyu.edu/geoserver/sdr" +GEOSERVER_RESTRICTED_URL = "https://maps-restricted.geo.nyu.edu/geoserver/sdr" + RDM_CUSTOM_FIELDS_UI = [ { "section": _("GeoServer"), + "hide_from_landing_page": True, "fields": [ dict( - field="geoserver:wms_url", + field="geoserver:layer_name", ui_widget="Input", props=dict( - label="WMS URL", - placeholder="https://maps-public.geo.nyu.edu/geoserver/sdr/wms", - icon="linkify", - description="GeoServer WMS Service Base URL", + label="Layer Name", + placeholder="sdr:nyu_2451_12345", + icon="pencil", + description="Name of the GeoServer Layer this data can be found in", required=False ) ), dict( - field="geoserver:wfs_url", - ui_widget="Input", + field="geoserver:has_wms_layer", + ui_widget="BooleanCheckbox", props=dict( - label="WFS URL", - placeholder="https://maps-public.geo.nyu.edu/geoserver/sdr/wfs", - icon="linkify", - description="GeoServer WFS Service Base URL", - required=False + label="WMS Layer?", + icon="check", + description="Does this record have a WMS layer in GeoServer?", + required=False, + trueLabel="Yes", + falseLabel="No", ) ), dict( - field="geoserver:layer_name", - ui_widget="Input", + field="geoserver:has_wfs_layer", + ui_widget="BooleanCheckbox", props=dict( - label="Layer Name", - placeholder="sdr:nyu_2451_12345", - icon="pencil", - description="Name of the GeoServer Layer this data can be found in", - required=False + label="WFS Layer?", + icon="check", + description="Does this record have a WFS layer in GeoServer?", + required=False, + trueLabel="Yes", + falseLabel="No", ) ), dict( diff --git a/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html b/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html index 1792223..75797c9 100644 --- a/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html +++ b/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html @@ -1,13 +1,15 @@ -{% if record.custom_fields["geoserver:wms_url"] and record.custom_fields["geoserver:layer_name"] %} +{% set geoserver_base_url = config.GEOSERVER_PUBLIC_URL if (record.access.files == 'public') else config.GEOSERVER_RESTRICTED_URL %} + +{% if record.custom_fields["geoserver:layer_name"] and record.custom_fields["geoserver:has_wms_layer"] %}

Map

- {% if record.custom_fields["geoserver:wfs_url"] %} + {% if record.custom_fields["geoserver:has_wfs_layer"] %}

Attributes

@@ -19,7 +21,7 @@

Attributes

diff --git a/tests/ui/test_map_view.py b/tests/ui/test_map_view.py index a4aed31..e37d191 100644 --- a/tests/ui/test_map_view.py +++ b/tests/ui/test_map_view.py @@ -1,11 +1,12 @@ from invenio_access.permissions import system_identity -def test_map_view(app, service, minimal_record, client_with_login): +def test_public_map_view(app, service, minimal_record, client_with_login): data = minimal_record.copy() data["metadata"]["title"] = "Geospatial Data" data["custom_fields"] = { - "geoserver:wms_url": "https://maps-public.geo.nyu.edu/geoserver/sdr/wms", + "geoserver:has_wms_layer": True, + "geoserver:has_wfs_layer": True, "geoserver:layer_name": "sdr:nyu_2451_34156", "geoserver:bounds": "ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)" } @@ -20,3 +21,25 @@ def test_map_view(app, service, minimal_record, client_with_login): assert "data-wms-url=\"https://maps-public.geo.nyu.edu/geoserver/sdr/wms\"" in html assert "data-layer-name=\"sdr:nyu_2451_34156\"" in html assert "data-bounds=\"ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)\"" in html + +def test_restricted_map_view(app, service, minimal_record, client_with_login): + data = minimal_record.copy() + data["access"]["files"] = "restricted" + data["metadata"]["title"] = "Geospatial Data" + data["custom_fields"] = { + "geoserver:has_wms_layer": True, + "geoserver:has_wfs_layer": True, + "geoserver:layer_name": "sdr:nyu_2451_34156", + "geoserver:bounds": "ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)" + } + + draft = service.create(system_identity, data) + record = service.publish(system_identity, draft.id) + + record_view = client_with_login.get("/records/" + record['id']).data + html = record_view.decode("utf-8") + + assert "Geospatial Data" in html + assert "data-wms-url=\"https://maps-restricted.geo.nyu.edu/geoserver/sdr/wms\"" in html + assert "data-layer-name=\"sdr:nyu_2451_34156\"" in html + assert "data-bounds=\"ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)\"" in html From 0f21920510ebec3f3dfbb0977d121217b61df5e5 Mon Sep 17 00:00:00 2001 From: Brian Kelly Date: Tue, 10 Dec 2024 15:45:15 -0600 Subject: [PATCH 07/16] Don't show map preview when files are restricted and user is anonymous --- .../ultraviolet/records/details/map.html | 56 ++++++++++--------- tests/conftest.py | 12 ++++ tests/ui/test_map_view.py | 42 +++++++------- 3 files changed, 61 insertions(+), 49 deletions(-) diff --git a/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html b/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html index 75797c9..16c67fc 100644 --- a/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html +++ b/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html @@ -1,34 +1,36 @@ {% set geoserver_base_url = config.GEOSERVER_PUBLIC_URL if (record.access.files == 'public') else config.GEOSERVER_RESTRICTED_URL %} -{% if record.custom_fields["geoserver:layer_name"] and record.custom_fields["geoserver:has_wms_layer"] %} -

Map

+{% if record.access.files == 'public' or (current_user and current_user.is_authenticated) %} + {% if record.custom_fields["geoserver:layer_name"] and record.custom_fields["geoserver:has_wms_layer"] %} +

Map

-
+
- {% if record.custom_fields["geoserver:has_wfs_layer"] %} -

Attributes

+ {% if record.custom_fields["geoserver:has_wfs_layer"] %} +

Attributes

-
- - - - - - - - -
AttributeType/Value
-
+
+ + + + + + + + +
AttributeType/Value
+
+ {% endif %} {% endif %} -{% endif %} -{% block javascript %} - {{ webpack['geoserver_js.js'] }} - {{ webpack['geoserver_css.css'] }} -{% endblock %} + {% block javascript %} + {{ webpack['geoserver_js.js'] }} + {{ webpack['geoserver_css.css'] }} + {% endblock %} +{% endif %} diff --git a/tests/conftest.py b/tests/conftest.py index 6ac0e8d..d87ed74 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -326,6 +326,18 @@ def minimal_record(): }, } +@pytest.fixture() +def geospatial_record(minimal_record): + data = minimal_record.copy() + data["metadata"]["title"] = "Geospatial Data" + data["custom_fields"] = { + "geoserver:has_wms_layer": True, + "geoserver:has_wfs_layer": True, + "geoserver:layer_name": "sdr:nyu_2451_34156", + "geoserver:bounds": "ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)" + } + + return data @pytest.fixture() def client_with_login(app, client, users): diff --git a/tests/ui/test_map_view.py b/tests/ui/test_map_view.py index e37d191..5430c0c 100644 --- a/tests/ui/test_map_view.py +++ b/tests/ui/test_map_view.py @@ -1,20 +1,10 @@ from invenio_access.permissions import system_identity - -def test_public_map_view(app, service, minimal_record, client_with_login): - data = minimal_record.copy() - data["metadata"]["title"] = "Geospatial Data" - data["custom_fields"] = { - "geoserver:has_wms_layer": True, - "geoserver:has_wfs_layer": True, - "geoserver:layer_name": "sdr:nyu_2451_34156", - "geoserver:bounds": "ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)" - } - - draft = service.create(system_identity, data) +def test_public_map_view_when_anonymous(app, service, geospatial_record, client): + draft = service.create(system_identity, geospatial_record) record = service.publish(system_identity, draft.id) - record_view = client_with_login.get("/records/" + record['id']).data + record_view = client.get("/records/" + record['id']).data html = record_view.decode("utf-8") assert "Geospatial Data" in html @@ -22,16 +12,9 @@ def test_public_map_view(app, service, minimal_record, client_with_login): assert "data-layer-name=\"sdr:nyu_2451_34156\"" in html assert "data-bounds=\"ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)\"" in html -def test_restricted_map_view(app, service, minimal_record, client_with_login): - data = minimal_record.copy() +def test_restricted_map_view_when_logged_in(app, service, geospatial_record, client_with_login): + data = geospatial_record.copy() data["access"]["files"] = "restricted" - data["metadata"]["title"] = "Geospatial Data" - data["custom_fields"] = { - "geoserver:has_wms_layer": True, - "geoserver:has_wfs_layer": True, - "geoserver:layer_name": "sdr:nyu_2451_34156", - "geoserver:bounds": "ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)" - } draft = service.create(system_identity, data) record = service.publish(system_identity, draft.id) @@ -43,3 +26,18 @@ def test_restricted_map_view(app, service, minimal_record, client_with_login): assert "data-wms-url=\"https://maps-restricted.geo.nyu.edu/geoserver/sdr/wms\"" in html assert "data-layer-name=\"sdr:nyu_2451_34156\"" in html assert "data-bounds=\"ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)\"" in html + +def test_restricted_map_view_when_anonymous(app, service, geospatial_record, client): + data = geospatial_record.copy() + data["access"]["files"] = "restricted" + + draft = service.create(system_identity, data) + record = service.publish(system_identity, draft.id) + + record_view = client.get("/records/" + record['id']).data + html = record_view.decode("utf-8") + + assert "Geospatial Data" in html + assert "data-wms-url=\"https://maps-restricted.geo.nyu.edu/geoserver/sdr/wms\"" not in html + assert "data-layer-name=\"sdr:nyu_2451_34156\"" not in html + assert "data-bounds=\"ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)\"" not in html From 1eb9322c44e2cb227a58b1b6dc11d5925971bed7 Mon Sep 17 00:00:00 2001 From: Brian Kelly Date: Wed, 11 Dec 2024 11:13:55 -0600 Subject: [PATCH 08/16] Only show map previws for Restricted files to logged in users --- .../semantic-ui/js/ultraviolet/geoserver.js | 42 +++++++------ .../ultraviolet/records/details/map.html | 60 ++++++++++--------- tests/ui/test_map_view.py | 6 +- 3 files changed, 61 insertions(+), 47 deletions(-) diff --git a/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js b/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js index 023079b..d137d34 100644 --- a/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js +++ b/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js @@ -94,13 +94,8 @@ const addInspection = (map, url, layerId) => { } document.addEventListener("DOMContentLoaded", async () => { - const attributesElement = document.getElementById("attributes"); - const wfsUrl = attributesElement.getAttribute("data-wfs-url"); - const layerNames = attributesElement.getAttribute("data-layer-names"); - const mapElement = document.getElementById("map"); - const baseUrl = mapElement.getAttribute("data-wms-url") - const layerName = mapElement.getAttribute("data-layer-name") + const preview = mapElement.getAttribute("data-preview"); const bounds = mapElement.getAttribute("data-bounds") const map = L.map('map').setView([0, 0], 13); @@ -112,15 +107,27 @@ document.addEventListener("DOMContentLoaded", async () => { retina: "@2x", }).addTo(map); - const wmsLayer = L.tileLayer.wms(baseUrl, { - layers: layerName, - format: 'image/png', - transparent: true, - opacity: 0.75 - }); + if (preview === "True") { + const baseUrl = mapElement.getAttribute("data-wms-url") + const layerName = mapElement.getAttribute("data-layer-name") + + const wmsLayer = L.tileLayer.wms(baseUrl, { + layers: layerName, + format: 'image/png', + transparent: true, + opacity: 0.75 + }); + + wmsLayer.addTo(map); + wmsLayer.setOpacity(0.75); - wmsLayer.addTo(map); - wmsLayer.setOpacity(0.75); + const attributesElement = document.getElementById("attributes"); + const wfsUrl = attributesElement.getAttribute("data-wfs-url"); + const layerNames = attributesElement.getAttribute("data-layer-names"); + + addInspection(map, wfsUrl, layerNames); + describeFeatureType(wfsUrl, layerNames, attributesElement); + } const regex = /ENVELOPE\(([-\d.]+), ([-\d.]+), ([-\d.]+), ([-\d.]+)\)/; const match = bounds.match(regex); @@ -134,8 +141,9 @@ document.addEventListener("DOMContentLoaded", async () => { const bounds = [[minLat, minLon], [maxLat, maxLon]]; map.fitBounds(bounds); - } - addInspection(map, wfsUrl, layerNames); - describeFeatureType(wfsUrl, layerNames, attributesElement); + if (preview === "False") { + map.addLayer(L.rectangle(bounds, {color: "#3388FF", weight: 3})); + } + } }); diff --git a/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html b/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html index 16c67fc..81fe7ad 100644 --- a/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html +++ b/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html @@ -1,36 +1,38 @@ +{% set preview = record.access.files == 'public' or (current_user and current_user.is_authenticated) %} {% set geoserver_base_url = config.GEOSERVER_PUBLIC_URL if (record.access.files == 'public') else config.GEOSERVER_RESTRICTED_URL %} -{% if record.access.files == 'public' or (current_user and current_user.is_authenticated) %} - {% if record.custom_fields["geoserver:layer_name"] and record.custom_fields["geoserver:has_wms_layer"] %} -

Map

+{% if record.custom_fields["geoserver:layer_name"] and record.custom_fields["geoserver:has_wms_layer"] %} +

Map

-
+
- {% if record.custom_fields["geoserver:has_wfs_layer"] %} -

Attributes

+ {% if record.custom_fields["geoserver:has_wfs_layer"] and (record.access.files == 'public' or (current_user and current_user.is_authenticated)) %} +

Attributes

-
- - - - - - - - -
AttributeType/Value
-
- {% endif %} +
+ + + + + + + + +
AttributeType/Value
+
{% endif %} - - {% block javascript %} - {{ webpack['geoserver_js.js'] }} - {{ webpack['geoserver_css.css'] }} - {% endblock %} {% endif %} + +{% block javascript %} + {{ webpack['geoserver_js.js'] }} + {{ webpack['geoserver_css.css'] }} +{% endblock %} diff --git a/tests/ui/test_map_view.py b/tests/ui/test_map_view.py index 5430c0c..f48fd03 100644 --- a/tests/ui/test_map_view.py +++ b/tests/ui/test_map_view.py @@ -8,6 +8,7 @@ def test_public_map_view_when_anonymous(app, service, geospatial_record, client) html = record_view.decode("utf-8") assert "Geospatial Data" in html + assert "data-preview=\"True\"" in html assert "data-wms-url=\"https://maps-public.geo.nyu.edu/geoserver/sdr/wms\"" in html assert "data-layer-name=\"sdr:nyu_2451_34156\"" in html assert "data-bounds=\"ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)\"" in html @@ -23,6 +24,7 @@ def test_restricted_map_view_when_logged_in(app, service, geospatial_record, cli html = record_view.decode("utf-8") assert "Geospatial Data" in html + assert "data-preview=\"True\"" in html assert "data-wms-url=\"https://maps-restricted.geo.nyu.edu/geoserver/sdr/wms\"" in html assert "data-layer-name=\"sdr:nyu_2451_34156\"" in html assert "data-bounds=\"ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)\"" in html @@ -38,6 +40,8 @@ def test_restricted_map_view_when_anonymous(app, service, geospatial_record, cli html = record_view.decode("utf-8") assert "Geospatial Data" in html + assert "data-preview=\"False\"" in html + assert "data-bounds=\"ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)\"" in html + assert "data-wms-url=\"https://maps-restricted.geo.nyu.edu/geoserver/sdr/wms\"" not in html assert "data-layer-name=\"sdr:nyu_2451_34156\"" not in html - assert "data-bounds=\"ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)\"" not in html From c87dd63f3f7ecec6d448aa396467fbc82b5e5fd9 Mon Sep 17 00:00:00 2001 From: Brian Kelly Date: Wed, 11 Dec 2024 11:39:22 -0600 Subject: [PATCH 09/16] Adds section display Web Services information --- .../ultraviolet/records/details/map.html | 25 ++++++++++++++++--- tests/ui/test_map_view.py | 5 ++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html b/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html index 81fe7ad..17e0348 100644 --- a/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html +++ b/site/ultraviolet/templates/semantic-ui/ultraviolet/records/details/map.html @@ -1,19 +1,19 @@ -{% set preview = record.access.files == 'public' or (current_user and current_user.is_authenticated) %} +{% set previewable = record.access.files == 'public' or (current_user and current_user.is_authenticated) %} {% set geoserver_base_url = config.GEOSERVER_PUBLIC_URL if (record.access.files == 'public') else config.GEOSERVER_RESTRICTED_URL %} {% if record.custom_fields["geoserver:layer_name"] and record.custom_fields["geoserver:has_wms_layer"] %}

Map

- {% if record.custom_fields["geoserver:has_wfs_layer"] and (record.access.files == 'public' or (current_user and current_user.is_authenticated)) %} + {% if record.custom_fields["geoserver:has_wfs_layer"] and (previewable) %}

Attributes

@@ -30,6 +30,23 @@

Attributes

{% endif %} + + {% if previewable %} +

Web Services

+ +
+
Layer Name
+
{{ record.custom_fields["geoserver:layer_name"] }}
+ {% if record.custom_fields["geoserver:has_wfs_layer"] %} +
Web Feature Service (WFS)
+
{{ geoserver_base_url }}/wfs
+ {% endif %} + {% if record.custom_fields["geoserver:has_wms_layer"] %} +
Web Mapping Service (WMS)
+
{{ geoserver_base_url }}/wms
+ {% endif %} +
+ {% endif %} {% endif %} {% block javascript %} diff --git a/tests/ui/test_map_view.py b/tests/ui/test_map_view.py index f48fd03..e3507f7 100644 --- a/tests/ui/test_map_view.py +++ b/tests/ui/test_map_view.py @@ -8,6 +8,8 @@ def test_public_map_view_when_anonymous(app, service, geospatial_record, client) html = record_view.decode("utf-8") assert "Geospatial Data" in html + assert "Web Services" in html + assert "data-preview=\"True\"" in html assert "data-wms-url=\"https://maps-public.geo.nyu.edu/geoserver/sdr/wms\"" in html assert "data-layer-name=\"sdr:nyu_2451_34156\"" in html @@ -24,6 +26,8 @@ def test_restricted_map_view_when_logged_in(app, service, geospatial_record, cli html = record_view.decode("utf-8") assert "Geospatial Data" in html + assert "Web Services" in html + assert "data-preview=\"True\"" in html assert "data-wms-url=\"https://maps-restricted.geo.nyu.edu/geoserver/sdr/wms\"" in html assert "data-layer-name=\"sdr:nyu_2451_34156\"" in html @@ -43,5 +47,6 @@ def test_restricted_map_view_when_anonymous(app, service, geospatial_record, cli assert "data-preview=\"False\"" in html assert "data-bounds=\"ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)\"" in html + assert "Web Services" not in html assert "data-wms-url=\"https://maps-restricted.geo.nyu.edu/geoserver/sdr/wms\"" not in html assert "data-layer-name=\"sdr:nyu_2451_34156\"" not in html From a041aa7621fa126facfb9453a5ef4439055a626f Mon Sep 17 00:00:00 2001 From: Brian Kelly Date: Wed, 11 Dec 2024 12:40:44 -0600 Subject: [PATCH 10/16] Leverage more test fixtures --- tests/conftest.py | 24 +++++++++++++++++------- tests/ui/test_map_view.py | 38 ++++++++++++++++++-------------------- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index d87ed74..f3c79ea 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,6 @@ # conftest.py """Pytest fixtures for ultraviolet testing""" - import sys import pytest import os @@ -68,11 +67,13 @@ def app_config(app_config): } return app_config + # overriding instance path allows us to make sure we use ultraviolet templates @pytest.fixture(scope="module") def instance_path(): return os.path.join(sys.prefix, "var", "instance") + # Copied from https://github.dev/inveniosoftware/invenio-rdm-records/tree/maint-1.3.x/tests/records # -*- coding: utf-8 -*- @@ -132,6 +133,7 @@ def instance_path(): from invenio_vocabularies.records.api import Vocabulary from invenio_access.models import ActionUsers + @pytest.fixture(scope="function") def full_record(users): """Full record data as dict coming from the external world.""" @@ -326,18 +328,26 @@ def minimal_record(): }, } + @pytest.fixture() def geospatial_record(minimal_record): - data = minimal_record.copy() - data["metadata"]["title"] = "Geospatial Data" - data["custom_fields"] = { + minimal_record["metadata"]["title"] = "Geospatial Data" + minimal_record["custom_fields"] = { "geoserver:has_wms_layer": True, "geoserver:has_wfs_layer": True, "geoserver:layer_name": "sdr:nyu_2451_34156", "geoserver:bounds": "ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)" } - return data + return minimal_record + + +@pytest.fixture() +def restricted_geospatial_record(geospatial_record): + geospatial_record["access"]["files"] = "restricted" + + return geospatial_record + @pytest.fixture() def client_with_login(app, client, users): @@ -666,8 +676,8 @@ def licenses_v(app, licenses): "tags": ["recommended", "all"], "description": { "en": "The Creative Commons Attribution license allows" - " re-distribution and re-use of a licensed work on" - " the condition that the creator is appropriately credited." + " re-distribution and re-use of a licensed work on" + " the condition that the creator is appropriately credited." }, "type": "licenses", }, diff --git a/tests/ui/test_map_view.py b/tests/ui/test_map_view.py index e3507f7..edc36f6 100644 --- a/tests/ui/test_map_view.py +++ b/tests/ui/test_map_view.py @@ -1,29 +1,24 @@ from invenio_access.permissions import system_identity -def test_public_map_view_when_anonymous(app, service, geospatial_record, client): - draft = service.create(system_identity, geospatial_record) - record = service.publish(system_identity, draft.id) - record_view = client.get("/records/" + record['id']).data - html = record_view.decode("utf-8") +def test_public_map_view_when_anonymous(service, geospatial_record, client): + published_record = publish_draft(geospatial_record, service) + + html = client.get("/records/" + published_record['id']).data.decode("utf-8") assert "Geospatial Data" in html assert "Web Services" in html - + assert "data-preview=\"True\"" in html assert "data-wms-url=\"https://maps-public.geo.nyu.edu/geoserver/sdr/wms\"" in html assert "data-layer-name=\"sdr:nyu_2451_34156\"" in html assert "data-bounds=\"ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)\"" in html -def test_restricted_map_view_when_logged_in(app, service, geospatial_record, client_with_login): - data = geospatial_record.copy() - data["access"]["files"] = "restricted" - draft = service.create(system_identity, data) - record = service.publish(system_identity, draft.id) +def test_restricted_map_view_when_logged_in(service, restricted_geospatial_record, client_with_login): + published_record = publish_draft(restricted_geospatial_record, service) - record_view = client_with_login.get("/records/" + record['id']).data - html = record_view.decode("utf-8") + html = client_with_login.get("/records/" + published_record['id']).data.decode("utf-8") assert "Geospatial Data" in html assert "Web Services" in html @@ -33,15 +28,11 @@ def test_restricted_map_view_when_logged_in(app, service, geospatial_record, cli assert "data-layer-name=\"sdr:nyu_2451_34156\"" in html assert "data-bounds=\"ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)\"" in html -def test_restricted_map_view_when_anonymous(app, service, geospatial_record, client): - data = geospatial_record.copy() - data["access"]["files"] = "restricted" - draft = service.create(system_identity, data) - record = service.publish(system_identity, draft.id) +def test_restricted_map_view_when_anonymous(service, restricted_geospatial_record, client): + published_record = publish_draft(restricted_geospatial_record, service) - record_view = client.get("/records/" + record['id']).data - html = record_view.decode("utf-8") + html = client.get("/records/" + published_record['id']).data.decode("utf-8") assert "Geospatial Data" in html assert "data-preview=\"False\"" in html @@ -50,3 +41,10 @@ def test_restricted_map_view_when_anonymous(app, service, geospatial_record, cli assert "Web Services" not in html assert "data-wms-url=\"https://maps-restricted.geo.nyu.edu/geoserver/sdr/wms\"" not in html assert "data-layer-name=\"sdr:nyu_2451_34156\"" not in html + + +def publish_draft(data, service): + draft = service.create(system_identity, data) + record = service.publish(system_identity, draft.id) + + return record From 3a6d084b6312d95e91f6a4ac0c174c79d011170d Mon Sep 17 00:00:00 2001 From: Brian Kelly Date: Wed, 11 Dec 2024 14:05:44 -0600 Subject: [PATCH 11/16] Fix tests after rebase --- tests/ui/test_map_view.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/ui/test_map_view.py b/tests/ui/test_map_view.py index edc36f6..aeb25e9 100644 --- a/tests/ui/test_map_view.py +++ b/tests/ui/test_map_view.py @@ -1,8 +1,8 @@ from invenio_access.permissions import system_identity -def test_public_map_view_when_anonymous(service, geospatial_record, client): - published_record = publish_draft(geospatial_record, service) +def test_public_map_view_when_anonymous(services, geospatial_record, client): + published_record = publish_draft(geospatial_record, services) html = client.get("/records/" + published_record['id']).data.decode("utf-8") @@ -15,8 +15,8 @@ def test_public_map_view_when_anonymous(service, geospatial_record, client): assert "data-bounds=\"ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)\"" in html -def test_restricted_map_view_when_logged_in(service, restricted_geospatial_record, client_with_login): - published_record = publish_draft(restricted_geospatial_record, service) +def test_restricted_map_view_when_logged_in(services, restricted_geospatial_record, client_with_login): + published_record = publish_draft(restricted_geospatial_record, services) html = client_with_login.get("/records/" + published_record['id']).data.decode("utf-8") @@ -29,8 +29,8 @@ def test_restricted_map_view_when_logged_in(service, restricted_geospatial_recor assert "data-bounds=\"ENVELOPE(-74.2556640887564, -73.700009054899, 40.9157739339836, 40.4960925239255)\"" in html -def test_restricted_map_view_when_anonymous(service, restricted_geospatial_record, client): - published_record = publish_draft(restricted_geospatial_record, service) +def test_restricted_map_view_when_anonymous(services, restricted_geospatial_record, client): + published_record = publish_draft(restricted_geospatial_record, services) html = client.get("/records/" + published_record['id']).data.decode("utf-8") @@ -43,8 +43,8 @@ def test_restricted_map_view_when_anonymous(service, restricted_geospatial_recor assert "data-layer-name=\"sdr:nyu_2451_34156\"" not in html -def publish_draft(data, service): - draft = service.create(system_identity, data) - record = service.publish(system_identity, draft.id) +def publish_draft(data, services): + draft = services.create(system_identity, data) + record = services.publish(system_identity, draft.id) return record From fba234e5d4d9d98e41b9c4571dac9aab6d510ade Mon Sep 17 00:00:00 2001 From: Brian Kelly Date: Thu, 12 Dec 2024 07:56:34 -0600 Subject: [PATCH 12/16] Update NYU copyright year in description.html --- .../invenio_app_rdm/records/details/description.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/semantic-ui/invenio_app_rdm/records/details/description.html b/templates/semantic-ui/invenio_app_rdm/records/details/description.html index 7fd5a4b..7c364b2 100644 --- a/templates/semantic-ui/invenio_app_rdm/records/details/description.html +++ b/templates/semantic-ui/invenio_app_rdm/records/details/description.html @@ -1,7 +1,7 @@ {# Copyright (C) 2020 CERN. Copyright (C) 2020 Northwestern University. - Copyright (C) 2021 New York University. + Copyright (C) 2024 New York University. Invenio RDM Records is free software; you can redistribute it and/or modify it under the terms of the MIT License; see LICENSE file for more details. From 21e986b6496fba32af2b01a07574bff1419ff50e Mon Sep 17 00:00:00 2001 From: Brian Kelly Date: Thu, 19 Dec 2024 09:24:21 -0600 Subject: [PATCH 13/16] Place map underneath invenio-menu --- .../ultraviolet/assets/semantic-ui/css/ultraviolet/geoserver.css | 1 + 1 file changed, 1 insertion(+) diff --git a/site/ultraviolet/assets/semantic-ui/css/ultraviolet/geoserver.css b/site/ultraviolet/assets/semantic-ui/css/ultraviolet/geoserver.css index 1d02a0c..6747d5e 100644 --- a/site/ultraviolet/assets/semantic-ui/css/ultraviolet/geoserver.css +++ b/site/ultraviolet/assets/semantic-ui/css/ultraviolet/geoserver.css @@ -7,6 +7,7 @@ margin-top: 1em; cursor: crosshair !important; border: 1px solid #ced4da; + z-index: 99; /* Place map under invenio-menu */ } .attributes { From 72e948f31c0eea2158bc96f4521082d65814828d Mon Sep 17 00:00:00 2001 From: Brian Kelly Date: Fri, 20 Dec 2024 12:43:48 -0600 Subject: [PATCH 14/16] Refactor JavaScript for clarity and simplifcation --- .../semantic-ui/js/ultraviolet/geoserver.js | 110 ++++++++++-------- 1 file changed, 60 insertions(+), 50 deletions(-) diff --git a/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js b/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js index d137d34..4f2b1a7 100644 --- a/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js +++ b/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js @@ -18,7 +18,7 @@ const populateAttributeTable = (data) => { }) } -const describeFeatureType = (wfsUrl, layerNames) => { +const retrieveAttributeTypes = (wfsUrl, layerNames) => { const attributesElement = document.getElementById("attributes"); const formData = new FormData(); @@ -51,27 +51,25 @@ const describeFeatureType = (wfsUrl, layerNames) => { }); }; -const addInspection = (map, url, layerId) => { +const addFeatureInspectionHandler = (map, url, layerNames) => { map.on("click", async (e) => { const attributesElement = document.getElementById("attributes"); attributesElement.innerHTML = 'Loading...'; - const wmsoptions = { - url: url, - layers: layerId, - bbox: map.getBounds().toBBoxString(), - width: Math.round(document.getElementById("map").clientWidth), - height: Math.round(document.getElementById("map").clientHeight), - query_layers: layerId, - x: Math.round(e.containerPoint.x), - y: Math.round(e.containerPoint.y), - }; - try { const response = await fetch("/geoserver/get_feature_info", { method: "POST", headers: { "Content-Type": "application/json", - }, body: JSON.stringify(wmsoptions), + }, body: JSON.stringify({ + url: url, + layers: layerNames, + bbox: map.getBounds().toBBoxString(), + width: Math.round(document.getElementById("map").clientWidth), + height: Math.round(document.getElementById("map").clientHeight), + query_layers: layerNames, + x: Math.round(e.containerPoint.x), + y: Math.round(e.containerPoint.y), + }), }); if (!response.ok) throw new Error("Network response was not ok."); @@ -93,50 +91,16 @@ const addInspection = (map, url, layerId) => { }); } -document.addEventListener("DOMContentLoaded", async () => { +const zoomMapToBoundingBox = (map) => { const mapElement = document.getElementById("map"); const preview = mapElement.getAttribute("data-preview"); const bounds = mapElement.getAttribute("data-bounds") - const map = L.map('map').setView([0, 0], 13); - - L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{retina}.png', { - attribution: '© OpenStreetMap contributors, © Carto', - maxZoom: 18, - worldCopyJump: true, - retina: "@2x", - }).addTo(map); - - if (preview === "True") { - const baseUrl = mapElement.getAttribute("data-wms-url") - const layerName = mapElement.getAttribute("data-layer-name") - - const wmsLayer = L.tileLayer.wms(baseUrl, { - layers: layerName, - format: 'image/png', - transparent: true, - opacity: 0.75 - }); - - wmsLayer.addTo(map); - wmsLayer.setOpacity(0.75); - - const attributesElement = document.getElementById("attributes"); - const wfsUrl = attributesElement.getAttribute("data-wfs-url"); - const layerNames = attributesElement.getAttribute("data-layer-names"); - - addInspection(map, wfsUrl, layerNames); - describeFeatureType(wfsUrl, layerNames, attributesElement); - } - const regex = /ENVELOPE\(([-\d.]+), ([-\d.]+), ([-\d.]+), ([-\d.]+)\)/; const match = bounds.match(regex); if (match) { - const minLon = parseFloat(match[1]); - const maxLon = parseFloat(match[2]); - const minLat = parseFloat(match[3]); - const maxLat = parseFloat(match[4]); + let [_, minLon, maxLon, minLat, maxLat] = match; const bounds = [[minLat, minLon], [maxLat, maxLon]]; @@ -146,4 +110,50 @@ document.addEventListener("DOMContentLoaded", async () => { map.addLayer(L.rectangle(bounds, {color: "#3388FF", weight: 3})); } } +}; + +const addWmsLayer = (map) => { + const mapElement = document.getElementById("map"); + const baseUrl = mapElement.getAttribute("data-wms-url") + const layerName = mapElement.getAttribute("data-layer-name") + + const wmsLayer = L.tileLayer.wms(baseUrl, { + layers: layerName, + format: 'image/png', + transparent: true, + opacity: 0.75 + }); + + wmsLayer.addTo(map); + wmsLayer.setOpacity(0.75); +}; + +const addWfsInspection = map => { + const attributesElement = document.getElementById("attributes"); + const wfsUrl = attributesElement.getAttribute("data-wfs-url"); + const layerNames = attributesElement.getAttribute("data-layer-names"); + + addFeatureInspectionHandler(map, wfsUrl, layerNames); + retrieveAttributeTypes(wfsUrl, layerNames); +}; + +document.addEventListener("DOMContentLoaded", async () => { + const mapElement = document.getElementById("map"); + const preview = mapElement.getAttribute("data-preview"); + + const map = L.map('map').setView([0, 0], 13); + + L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{retina}.png', { + attribution: '© OpenStreetMap contributors, © Carto', + maxZoom: 18, + worldCopyJump: true, + retina: "@2x", + }).addTo(map); + + if (preview === "True") { + addWmsLayer(map); + addWfsInspection(map); + } + + zoomMapToBoundingBox(map); }); From 3287977750deb8d4dc731c93360fb9caa36ba5a8 Mon Sep 17 00:00:00 2001 From: Brian Kelly Date: Fri, 20 Dec 2024 14:27:09 -0600 Subject: [PATCH 15/16] Only attempt to load attributes if there's a WFS URL --- .../assets/semantic-ui/js/ultraviolet/geoserver.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js b/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js index 4f2b1a7..a9f6564 100644 --- a/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js +++ b/site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js @@ -130,11 +130,14 @@ const addWmsLayer = (map) => { const addWfsInspection = map => { const attributesElement = document.getElementById("attributes"); - const wfsUrl = attributesElement.getAttribute("data-wfs-url"); - const layerNames = attributesElement.getAttribute("data-layer-names"); - addFeatureInspectionHandler(map, wfsUrl, layerNames); - retrieveAttributeTypes(wfsUrl, layerNames); + if (attributesElement) { + const wfsUrl = attributesElement.getAttribute("data-wfs-url"); + const layerNames = attributesElement.getAttribute("data-layer-names"); + + addFeatureInspectionHandler(map, wfsUrl, layerNames); + retrieveAttributeTypes(wfsUrl, layerNames); + } }; document.addEventListener("DOMContentLoaded", async () => { From 8adcae383297c3e0cae707ca127393308fd315dd Mon Sep 17 00:00:00 2001 From: Brian Kelly Date: Fri, 20 Dec 2024 14:27:31 -0600 Subject: [PATCH 16/16] Improve description of proxy endpoints --- site/ultraviolet/geoserver/describe_feature_type.py | 4 ++-- site/ultraviolet/geoserver/get_feature_info.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/site/ultraviolet/geoserver/describe_feature_type.py b/site/ultraviolet/geoserver/describe_feature_type.py index 4862e0f..132005e 100644 --- a/site/ultraviolet/geoserver/describe_feature_type.py +++ b/site/ultraviolet/geoserver/describe_feature_type.py @@ -7,10 +7,10 @@ class DescribeFeatureType(MethodView): - """GeoServer view.""" + """Proxy for GeoServer DescribeFeatureType requests.""" def post(self): - """Return JSON""" + """Pass DescribeFeatureType requests to GeoServer and hand back results.""" url = request.form.get('url', default=None) layers = request.form.get('layers', default=None) diff --git a/site/ultraviolet/geoserver/get_feature_info.py b/site/ultraviolet/geoserver/get_feature_info.py index bd42bc7..c88bfbd 100644 --- a/site/ultraviolet/geoserver/get_feature_info.py +++ b/site/ultraviolet/geoserver/get_feature_info.py @@ -7,10 +7,10 @@ class GetFeatureInfo(MethodView): - """GeoServer view.""" + """Proxy for GeoServer GetFeatureInfo requests.""" def post(self): - """Return JSON""" + """Pass GetFeatureInfo requests to GeoServer and hand back results.""" data = request.get_json() url = data.get('url', None)