Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds Geospatial custom fields and Map/Attribute viewer to Record view #221

Open
wants to merge 16 commits into
base: geospatial
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions invenio.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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, BooleanCF
#required to use custom permissions
from ultraviolet_saml.handlers import acs_handler_factory
from ultraviolet_permissions.policies import UltraVioletPermissionPolicy
Expand Down Expand Up @@ -71,6 +72,13 @@ APP_DEFAULT_SECURE_HEADERS = {
"blob:", # for pdf preview
# Add your own policies here (e.g. analytics)
],
'img-src': [
"'self'",
'data:',
"https://*.basemaps.cartocdn.com",
"https://maps-public.geo.nyu.edu",
"https://maps-restricted.geo.nyu.edu",
Comment on lines +78 to +80
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are here to allow pulling content from external sources on the front-end.

]
},
'content_security_policy_report_only': False,
'content_security_policy_report_uri': None,
Expand Down Expand Up @@ -421,6 +429,8 @@ if APP_ENVIRONMENT == 'local':
'[email protected]': 'adminUV'
}

RATELIMIT_ENABLED = False
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only enabled in local and to allow bulk loading data via the REST API.



# UltraViolet custom vars
# =======================
Expand Down Expand Up @@ -451,3 +461,72 @@ 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 = [
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: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:has_wms_layer",
ui_widget="BooleanCheckbox",
props=dict(
label="WMS Layer?",
icon="check",
description="Does this record have a WMS layer in GeoServer?",
required=False,
trueLabel="Yes",
falseLabel="No",
)
),
dict(
field="geoserver:has_wfs_layer",
ui_widget="BooleanCheckbox",
props=dict(
label="WFS Layer?",
icon="check",
description="Does this record have a WFS layer in GeoServer?",
required=False,
trueLabel="Yes",
falseLabel="No",
)
),
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
)
)
]
}
]
Empty file.
15 changes: 15 additions & 0 deletions site/ultraviolet/assets/semantic-ui/css/ultraviolet/geoserver.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@import "leaflet/dist/leaflet.css";
@import 'ol/ol.css';

#map {
width: 100%;
height: 440px;
margin-top: 1em;
cursor: crosshair !important;
border: 1px solid #ced4da;
}

.attributes {
max-height: 450px;
overflow-y: scroll;
}
149 changes: 149 additions & 0 deletions site/ultraviolet/assets/semantic-ui/js/ultraviolet/geoserver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
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.sort((a, b) => {
return a.name.localeCompare(b.name)
})

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 = '<tr><td colspan="2">Loading...</td>';

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 = '<tr><td colspan="2">No feature found</td>';

return;
}
const data = response_data.features[0];

populateAttributeTable(data);
} catch (error) {
console.error("Fetch error: ", error);
}
});
}

document.addEventListener("DOMContentLoaded", async () => {
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: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, &copy; <a href="http://carto.com/attributions">Carto</a>',
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]);

const bounds = [[minLat, minLon], [maxLat, maxLon]];

map.fitBounds(bounds);

if (preview === "False") {
map.addLayer(L.rectangle(bounds, {color: "#3388FF", weight: 3}));
}
}
});
Empty file.
27 changes: 27 additions & 0 deletions site/ultraviolet/geoserver/describe_feature_type.py
Original file line number Diff line number Diff line change
@@ -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')
35 changes: 35 additions & 0 deletions site/ultraviolet/geoserver/get_feature_info.py
Original file line number Diff line number Diff line change
@@ -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')
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{% 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"] %}
<h2>Map</h2>

<div id="map"
data-preview="{{ previewable }}"
data-bounds="{{ record.custom_fields["geoserver:bounds"] }}"
{% if previewable %}
data-wms-url="{{ geoserver_base_url }}/wms"
data-layer-name="{{ record.custom_fields["geoserver:layer_name"] }}"
{% endif %}
></div>

{% if record.custom_fields["geoserver:has_wfs_layer"] and (previewable) %}
<h3>Attributes</h3>

<div class="attributes">
<table class="ui unstackable very compact table striped selectable">
<thead>
<tr>
<th>Attribute</th>
<th>Type/Value</th>
</tr>
</thead>
<tbody id="attributes"
data-wfs-url="{{ geoserver_base_url }}/wfs"
data-layer-names="{{ record.custom_fields["geoserver:layer_name"] }}"></tbody>
</table>
</div>
{% endif %}

{% if previewable %}
<h3>Web Services</h3>

<dl class="details-list">
<dt class="ui tiny header">Layer Name</dt>
<dd><code>{{ record.custom_fields["geoserver:layer_name"] }}</code></dd>
{% if record.custom_fields["geoserver:has_wfs_layer"] %}
<dt class="ui tiny header">Web Feature Service (WFS)</dt>
<dd><code>{{ geoserver_base_url }}/wfs</code></dd>
{% endif %}
{% if record.custom_fields["geoserver:has_wms_layer"] %}
<dt class="ui tiny header">Web Mapping Service (WMS)</dt>
<dd><code>{{ geoserver_base_url }}/wms</code></dd>
{% endif %}
</dl>
{% endif %}
{% endif %}

{% block javascript %}
{{ webpack['geoserver_js.js'] }}
{{ webpack['geoserver_css.css'] }}
{% endblock %}
Loading