From dd5098509316aef936e4fded595689cf23b5503a Mon Sep 17 00:00:00 2001 From: MarkCalvert <37602611+MarkCalvert@users.noreply.github.com> Date: Fri, 27 Oct 2023 07:17:16 +1300 Subject: [PATCH] Migrate QESD spatial customisations (#267) * [SUPDESQ-135] - Migrated QESD customisations from forked repo ckan-spatial - Refactored spatial customisations into their own spatial folder for templates and webassets * Added check and warning message if dataset identifier does not exist --- .../fanstatic/spatial/spatial_map_input.css | 77 +++ .../fanstatic/spatial/spatial_map_input.js | 420 +++++++++++++++ .../fanstatic/spatial/spatial_query.css | 286 +++++++++++ .../fanstatic/spatial/spatial_query.js | 481 ++++++++++++++++++ ckanext/qdes_schema/fanstatic/webassets.yml | 34 ++ ckanext/qdes_schema/jobs.py | 10 +- ckanext/qdes_schema/templates/group/read.html | 2 +- .../templates/organization/read.html | 2 +- .../qdes_schema/templates/package/search.html | 2 +- .../package/snippets/additional_info.html | 2 +- .../package/snippets/package_form.html | 4 +- .../templates/snippets/spatial_query.html | 41 -- .../spatial/snippets/accessibility_panel.html | 50 ++ .../snippets/dataset_map_qdes.html | 0 .../snippets/dataset_spatial_table.html | 2 +- .../spatial/snippets/spatial_query.html | 41 ++ .../spatial/snippets/spatial_query_asset.html | 2 + 17 files changed, 1405 insertions(+), 51 deletions(-) create mode 100644 ckanext/qdes_schema/fanstatic/spatial/spatial_map_input.css create mode 100644 ckanext/qdes_schema/fanstatic/spatial/spatial_map_input.js create mode 100644 ckanext/qdes_schema/fanstatic/spatial/spatial_query.css create mode 100644 ckanext/qdes_schema/fanstatic/spatial/spatial_query.js delete mode 100644 ckanext/qdes_schema/templates/snippets/spatial_query.html create mode 100644 ckanext/qdes_schema/templates/spatial/snippets/accessibility_panel.html rename ckanext/qdes_schema/templates/{ => spatial}/snippets/dataset_map_qdes.html (100%) rename ckanext/qdes_schema/templates/{ => spatial}/snippets/dataset_spatial_table.html (92%) create mode 100644 ckanext/qdes_schema/templates/spatial/snippets/spatial_query.html create mode 100644 ckanext/qdes_schema/templates/spatial/snippets/spatial_query_asset.html diff --git a/ckanext/qdes_schema/fanstatic/spatial/spatial_map_input.css b/ckanext/qdes_schema/fanstatic/spatial/spatial_map_input.css new file mode 100644 index 00000000..88882491 --- /dev/null +++ b/ckanext/qdes_schema/fanstatic/spatial/spatial_map_input.css @@ -0,0 +1,77 @@ +#spatial-map-input #spatial-map-input-container { + height: 400px; + width: 100%; + margin-bottom: 30px; +} + +@media (min-width: 768px){ + #spatial-map-input #spatial-map-input-container { + height: 600px; + } +} + +.leaflet-control-arrow{ + border: none; + box-shadow: none; + padding: 3px; +} +.leaflet-control-arrow-left, +.leaflet-control-arrow-right, +.leaflet-control-arrow-up, +.leaflet-control-arrow-down{ + width: 22px; + height: 22px; + border-radius: 4px; + display: block; + border: 1px solid #aaa; + font-size: 15px; + text-align: center; + position: relative; +} +.leaflet-control-arrow-left{ + left: -12px; + float: left; +} +.leaflet-control-arrow-right{ + right: 12px; + float: right; +} +.leaflet-control-arrow-down{ + clear: both; +} +.leaflet-control-arrow-left:before, +.leaflet-control-arrow-right:before, +.leaflet-control-arrow-up:before, +.leaflet-control-arrow-down:before{ + color: #000; + font-family: "FontAwesome"; +} +.leaflet-control-arrow-left:hover:before, +.leaflet-control-arrow-right:hover:before, +.leaflet-control-arrow-up:hover:before, +.leaflet-control-arrow-down:hover:before{ + color: #777; +} +.leaflet-control-arrow-left:hover, +.leaflet-control-arrow-right:hover, +.leaflet-control-arrow-up:hover, +.leaflet-control-arrow-down:hover, +.leaflet-control-arrow-left:focus, +.leaflet-control-arrow-right:focus, +.leaflet-control-arrow-up:focus, +.leaflet-control-arrow-down:focus{ + background: #fff; + text-decoration: none; +} +.leaflet-control-arrow-left:before{ + content: "\f060"; +} +.leaflet-control-arrow-right:before{ + content: "\f061"; +} +.leaflet-control-arrow-up:before{ + content: "\f062"; +} +.leaflet-control-arrow-down:before{ + content: "\f063"; +} diff --git a/ckanext/qdes_schema/fanstatic/spatial/spatial_map_input.js b/ckanext/qdes_schema/fanstatic/spatial/spatial_map_input.js new file mode 100644 index 00000000..727dd353 --- /dev/null +++ b/ckanext/qdes_schema/fanstatic/spatial/spatial_map_input.js @@ -0,0 +1,420 @@ +/** + * Create spatial map input. + * The rectangle will populate the lower left and upper right, + * and marker will populate the centroid. + * + * All those fields id selector can be adjustable via data attributes: + * - data-module-lower-left => field selector + * - data-module-upper-right => field selector + * - data-module-centroid => field selector + * - data-module-geometry => field selector + * - data-module-extent => geojson + * - data-module-max-bounds => geojson + * + * Things to note, GeoSpatial software (Leaflet and Leaflet Draw) has inconsistency, + * in this case, when drawing (Leaflet Draw lib) will return lng lat BUT labeled as lat lng, it is soo misleading. + * And when the value passed to Leaflet, it assume accept the correct lat lng format. + * This make the GeoJSON result inconsistent and hard to work with, + * for workaround there is a method _convertBoundsFromLngLatToLatLng() to handle this mapping. + * + * See more here https://macwright.com/lonlat/. + */ +this.ckan.module('spatial-map-input', function (jQuery, _) { + // Add arrow control. + // This arrow buttons are not OOTB feature from leaflet, + // below code was copied from spatial_query.js + L.Control.Arrow = L.Control.extend({ + options: { + position: 'topleft' + }, + + onAdd: function (map) { + var arrowName = 'leaflet-control-arrow', + barName = 'leaflet-bar', + partName = barName + '-part', + container = L.DomUtil.create('div', arrowName + ' ' + barName); + + this._map = map; + + this._moveUpButton = this._createButton('', 'Move up', + arrowName + '-up ' + + partName + ' ' + + partName + '-up', + container, this._move('up'), this); + + this._moveLeftButton = this._createButton('', 'Move left', + arrowName + '-left ' + + partName + ' ' + + partName + '-left', + container, this._move('left'), this); + + this._moveRightButton = this._createButton('', 'Move right', + arrowName + '-right ' + + partName + ' ' + + partName + '-right', + container, this._move('right'), this); + + this._moveDownButton = this._createButton('', 'Move down', + arrowName + '-down ' + + partName + ' ' + + partName + '-down', + container, this._move('down'), this); + + + return container; + }, + + onRemove: function () { + + }, + + _move: function (direction) { + var d = [0, 0]; + var self = this; + + switch (direction) { + case 'up': + d[1] = -10; + break; + case 'down': + d[1] = 10; + break; + case 'left': + d[0] = -10; + break; + case 'right': + d[0] = 10; + break; + } + return function () { + self._map.panBy(d); + }; + }, + + _createButton: function (html, title, className, container, fn, context) { + var link = L.DomUtil.create('a', className, container); + link.innerHTML = html; + link.href = '#'; + link.title = title; + + var stop = L.DomEvent.stopPropagation; + + L.DomEvent + .on(link, 'click', stop) + .on(link, 'mousedown', stop) + .on(link, 'dblclick', stop) + .on(link, 'click', L.DomEvent.preventDefault) + .on(link, 'click', fn, context); + + return link; + } + }); + + var mapContainer = 'spatial-map-input-container'; + + return { + // Define the spatial map input related variables. + markerLayer: null, + rectangleLayer: null, + baseLayerUrl: 'https://stamen-tiles-c.a.ssl.fastly.net/terrain/{z}/{x}/{y}.png', + leafletBaseLayerOptions: {attribution: false}, + editableLayers: new L.FeatureGroup(), + map: new L.Map(mapContainer, { + attributionControl: false, + zoom: 5, + zoomDelta: 0.25, + zoomSnap: 0.25, + minZoom: 3.75 + }), + options: {}, + lowerLeftFieldElement: null, + upperRightFieldElement: null, + centroidFieldElement: null, + geometryFieldElement: null, + mapContainerVisible: false, + + // This is the first method that get triggered. + initialize: function () { + var spatialMapInput = this; + + // Init field elements. + this.lowerLeftFieldElement = jQuery(this.options.lowerLeft); + this.upperRightFieldElement = jQuery(this.options.upperRight); + this.centroidFieldElement = jQuery(this.options.centroid); + this.geometryFieldElement = jQuery(this.options.geometry); + + // Hack to make leaflet use a particular location to look for images + L.Icon.Default.imagePath = this.options.site_url + 'js/vendor/leaflet/1.9.3/images/'; + + jQuery.proxyAll(this, /_on/); + this.el.ready(this._onReady); + + // Specific to QDES. + // Since the map container is collapse within an accordion, + // the leatlet not able to correctly calculate the canvas to draw the map, + // so let's listen to visibility changes and trigger browser resize, and reset the bounds. + jQuery('.display-group-spatial > input[name=tabs]').on('change', function (e) { + if (jQuery(this).is(':checked')) { + spatialMapInput.map.invalidateSize(true); + if (spatialMapInput.lowerLeftFieldElement.val().length > 0 && spatialMapInput.upperRightFieldElement.val().length > 0) { + spatialMapInput.map.fitBounds(spatialMapInput.editableLayers.getBounds()); + } + else { + spatialMapInput._setDefaultBounds(); + } + } + }); + }, + + _onReady: function () { + // Setup map. + this.map.addControl(new L.Control.Arrow); + this._setDefaultBounds(); + this._setMaxBounds(); + + // Add tile layer. + L.tileLayer(this.options.map_config['custom.url'] || baseLayerUrl, this.leafletBaseLayerOptions).addTo(this.map); + + // Store editable layers. + this.map.addLayer(this.editableLayers); + + // Add draw controller. + var drawControl = new L.Control.Draw({ + position: 'topright', + draw: { + circle: false, + circlemarker: false, + polygon: false, + polyline: false + }, + edit: { + featureGroup: this.editableLayers + } + }); + this.map.addControl(drawControl); + + // Draw default rectangle and centroid if data exist. + if (this.lowerLeftFieldElement.val().length > 0 && this.upperRightFieldElement.val().length > 0) { + this._drawRectangle(); + } + if (this.centroidFieldElement.val().length > 0) { + this._drawMarker(); + } + + // Listen to draw:created event. + this.map.on('draw:created', this._onDrawCreated); + + // Listen to edited map draw:edited event. + this.map.on('draw:edited', this._onDrawEdited); + + // // Listen to deleted map draw:deleted event. + this.map.on('draw:deleted', this._onDrawDeleted); + + // Setup event listener to lower left, upper right, centroid and geometry field. + this.lowerLeftFieldElement.on('change', this._onChangeLowerLeftUpperRight); + this.upperRightFieldElement.on('change', this._onChangeLowerLeftUpperRight); + this.centroidFieldElement.on('change', this._onChangeCentroid); + this.geometryFieldElement.on('change', this._onChangeGeometry); + }, + + _setDefaultBounds: function () { + // Set a map view that contains the given geographical bounds with the maximum zoom level possible. + if (this.options.extent) { + var defaultBounds = new L.GeoJSON(this.options.extent).getBounds(); + this.map.fitBounds(defaultBounds); + } + }, + + _setMaxBounds: function () { + // Restrict the view to the given geographical bounds, + // bouncing the user back if the user tries to pan outside the view. + if (this.options.maxBounds) { + var maximumBounds = new L.GeoJSON(this.options.maxBounds).getBounds(); + this.map.setMaxBounds(maximumBounds); + } + }, + + _onDrawCreated: function (e) { + var type = e.layerType, + layer = e.layer, + existingLayer = null; + + // Need to check if the rectangle is exist. + // If so, replace it with the new drawn marker. + switch (type) { + case 'rectangle': + existingLayer = this.rectangleLayer; + + // Let's beat the inconsistency by mapping them lat lng. + // The Leaflet Draw (layer variable) return lng as lat, and lat as lng. + var correctLatLngBounds = this._convertBoundsFromLngLatToLatLng(layer.getBounds()); + var rectangle = L.rectangle(correctLatLngBounds); + this.rectangleLayer = layer; + this._populateLowerLeft(rectangle.getBounds()); + this._populateUpperRight(rectangle.getBounds()); + break; + case 'marker': + existingLayer = this.markerLayer; + this.markerLayer = layer; + this._populateCentroid(layer); + break + default: + existingLayer = null; + } + if (existingLayer) { + this.editableLayers.removeLayer(existingLayer); + } + + // At this line, we assume the layer is clean, + // let's add the newly drawn marker/rectangle to the layer. + this.editableLayers.addLayer(layer); + }, + + _onDrawEdited: function (e) { + var spatialMapInput = this; + e.layers.eachLayer(function (layer) { + if (layer instanceof L.Rectangle) { + // Let's beat the inconsistency by mapping them lat lng. + // The Leaflet Draw (layer variable) return lng as lat, and lat as lng. + var correctLatLngBounds = spatialMapInput._convertBoundsFromLngLatToLatLng(layer.getBounds()); + var rectangle = L.rectangle(correctLatLngBounds); + spatialMapInput.rectangleLayer = layer; + spatialMapInput._populateLowerLeft(rectangle.getBounds()); + spatialMapInput._populateUpperRight(rectangle.getBounds()); + } + + if (layer instanceof L.Marker) { + spatialMapInput.markerLayer = layer; + spatialMapInput._populateCentroid(layer); + } + }); + }, + + _onDrawDeleted: function (e) { + var spatialMapInput = this; + e.layers.eachLayer(function (layer) { + if (layer instanceof L.Rectangle) { + spatialMapInput.rectangleLayer = null; + spatialMapInput.lowerLeftFieldElement.val(''); + spatialMapInput.upperRightFieldElement.val(''); + } + + if (layer instanceof L.Marker) { + spatialMapInput.markerLayer = null; + spatialMapInput.centroidFieldElement.val(''); + } + }); + }, + + _convertBoundsFromLngLatToLatLng: function (boundsLngLat) { + var southWest = boundsLngLat.getSouthWest(); + var northEast = boundsLngLat.getNorthEast(); + + return L.latLngBounds([southWest.lng, southWest.lat], [northEast.lng, northEast.lat]) + }, + + _populateLowerLeft: function (bounds) { + var southWest = bounds.getSouthWest(); + var geometry = { + "type":"Point", + "coordinates": [southWest.lat, southWest.lng] + } + this.lowerLeftFieldElement.val(JSON.stringify(geometry)); + }, + + _populateUpperRight: function (bounds) { + var northEast = bounds.getNorthEast(); + var geometry = { + "type":"Point", + "coordinates": [northEast.lat, northEast.lng] + } + this.upperRightFieldElement.val(JSON.stringify(geometry)); + }, + + _populateCentroid: function (marker) { + this.centroidFieldElement.val(JSON.stringify(marker.toGeoJSON().geometry)); + }, + + _onChangeLowerLeftUpperRight: function (e) { + // Do not re-draw based on lower left/upper right IF there is geometry value. + if (this.geometryFieldElement.val().length > 0) { + this._onChangeGeometry() + } + else { + this._drawRectangle(); + } + }, + + _onChangeCentroid: function (e) { + this._drawMarker(); + }, + + _onChangeGeometry: function (e) { + // Auto calculate the lower left and upper right + // and then trigger _onChangeLowerLeftUpperRight so that the map will be redrew. + if (this.geometryFieldElement.val().trim().length > 0) { + var geometry = JSON.parse(this.geometryFieldElement.val()).coordinates; + var bounds = L.polygon(geometry).getBounds(); + this._populateLowerLeft(bounds); + this._populateUpperRight(bounds); + } + + // Trigger map re-draw. + this._drawRectangle(); + }, + + _lowerLeftUpperRightToRectangle: function () { + // Get lower left and upper right geoJSON coordinates. + if (this.lowerLeftFieldElement.val().trim().length > 0 && this.upperRightFieldElement.val().trim().length > 0) { + var lowerLeft = L.GeoJSON.coordsToLatLng(JSON.parse(this.lowerLeftFieldElement.val()).coordinates); + var upperRight = L.GeoJSON.coordsToLatLng(JSON.parse(this.upperRightFieldElement.val()).coordinates); + if (lowerLeft && upperRight) { + var bounds = L.latLngBounds(lowerLeft, upperRight); + return L.rectangle(bounds); + } + } + + return null; + }, + + _centroidToMarker: function () { + // Get centroid geoJSON coordinates. + if (this.centroidFieldElement.val().length > 0) { + var centroid = L.GeoJSON.coordsToLatLng(JSON.parse(this.centroidFieldElement.val()).coordinates); + if (centroid) { + return L.marker(centroid); + } + } + + return null; + }, + + _drawRectangle: function () { + var rectangle = this._lowerLeftUpperRightToRectangle(); + if (rectangle) { + if (this.rectangleLayer) { + this.editableLayers.removeLayer(this.rectangleLayer); + } + + this.rectangleLayer = rectangle; + this.editableLayers.addLayer(this.rectangleLayer); + + // Let's zoom it to this newly drawn rectangle. + this.map.fitBounds(this.editableLayers.getBounds()); + } + }, + + _drawMarker: function () { + // Get centroid geoJSON coordinates. + var centroid = this._centroidToMarker(); + if (centroid) { + if (this.markerLayer) { + this.editableLayers.removeLayer(this.markerLayer); + } + + this.markerLayer = centroid; + this.editableLayers.addLayer(this.markerLayer); + } + } + } +}); diff --git a/ckanext/qdes_schema/fanstatic/spatial/spatial_query.css b/ckanext/qdes_schema/fanstatic/spatial/spatial_query.css new file mode 100644 index 00000000..137fc73b --- /dev/null +++ b/ckanext/qdes_schema/fanstatic/spatial/spatial_query.css @@ -0,0 +1,286 @@ +.module-narrow #dataset-map-container { + height: 290px; +} +.module-content #dataset-map-container { + height: 250px; +} +#dataset-map-attribution { + font-size: 11px; + line-height: 1.5; +} +.module-heading .action { + float: right; + color: #888888; + font-size: 12px; + line-height: 20px; + text-decoration: underline; +} +.module-narrow #dataset-map-attribution { + margin: 5px 8px; + color: #666; +} +.leaflet-draw-label-single { + display: none; +} +.leaflet-draw-label-subtext { + display: none; +} +#field-location { + width: 190px; +} +.select2-results .select2-no-results { + padding: 3px 6px; +} +#dataset-map-edit { + margin: 5px 8px; +} +.module-narrow #dataset-map-container{ + position: relative; +} +.extended-map-form, +.module-narrow #dataset-map-attribution, +.module-narrow #dataset-map-clear, +#dataset-map-edit-buttons, +#extended-map-panel{ + display: none; +} +.extended-map-show-form{ + text-align: center; + padding-bottom: 10px; +} +.leaflet-control-draw-rectangle { + background-image: url("../img/pencil.png"); + background-repeat: no-repeat; + background-position: 50% 50%; + background-size: 12px 12px; + border-radius: 4px; +} +.leaflet-draw-actions li { + display: none; +} +div[style*="crosshair"] .leaflet-clickable{ + cursor: crosshair; +} +.dataset-map-expanded .secondary { + position: static; +} +.dataset-map-expanded #dataset-map { + position: absolute; + width: 100%; + height: 467px; + top: -469px; + left: 0; + background-color: white; + border: 1px solid #CCC; + box-sizing: content-box; + margin: 0; +} +.dataset-map-expanded #dataset-map #dataset-map-container { + height: 467px; +} +.dataset-map-expanded #dataset-map #dataset-map-attribution { + *zoom: 1; +} +.dataset-map-expanded #dataset-map #dataset-map-attribution:before, +.dataset-map-expanded #dataset-map #dataset-map-attribution:after { + display: table; + content: ""; + line-height: 0; +} +.dataset-map-expanded #dataset-map #dataset-map-attribution:after { + clear: both; +} +.dataset-map-expanded #dataset-map #dataset-map-attribution div { + float: left; + margin-right: 10px; +} +.dataset-map-layer-drawn #dataset-map-container, +.dataset-map-expanded #dataset-map-container{ + position: relative; +} +.dataset-map-expanded #dataset-map-container{ + display: inline-block; + width: 40%; +} +.dataset-map-expanded #extended-map-panel{ + display: inline-block; + width: 60%; + float: right; + box-sizing: border-box; + padding: 65px 15px 0; +} +.dataset-map-expanded .dataset-map{ + height: 467px; +} +.extended-map-help li{ + margin-left: 15px; +} +#dataset-map .module-footer{ + border-top: none; +} +.dataset-map-expanded #dataset-map, +.dataset-map-expanded #dataset-map-clear, +.dataset-map-layer-drawn #dataset-map #dataset-map-attribution, +.dataset-map-expanded #dataset-map #dataset-map-attribution, +.dataset-map-expanded #dataset-map #dataset-map-edit-buttons { + display: block; +} +.dataset-map-expanded #dataset-map #dataset-map-edit-buttons { + float: right; + padding: 15px; + margin-top: -68px; +} +.dataset-map-expanded #dataset-map #dataset-map-edit { + display: none; +} +.dataset-map-expanded #dataset-map .module-heading { + border-top-color: #000; + position: absolute; + top: 0; + left: 40%; + right: 0; +} +.dataset-map-expanded .wrapper { + margin-top: 470px; +} +.extended-map-help ul{ + margin: 10px 0; +} +/* .dataset-map-layer-drawn .extended-map-help, */ +.active ~ .extended-map-help{ + display: none; +} +.dataset-map-layer-drawn .extended-map-form, +.active ~ .extended-map-form{ + display: block; +} +.dataset-map-layer-drawn .extended-map-control{ + display: none; +} +.dataset-map-layer-drawn .extended-map-form input{ + display: none; +} +.active ~ .extended-map-form input{ + display: inline; +} +.active ~ .extended-map-form .span-input{ + display: none; +} +.active ~ .extended-map-form .extended-map-control{ + display: block; +} +.extended-map-form .coord-field{ + display: inline-block; + width: 47.5%; +} +.coord-field .span-input{ + margin-left: 0; +} +.coord-field .span-input:after{ + content: "\00b0"; +} +.extended-map-form .coord-field input{ + max-width: 100%; + width: 315px; + box-sizing: border-box; + height: 2em; + padding-right: 0; +} +.active .show-map-link i{ + display: inline-block; +} +.show-map-link i{ + position: absolute; + right: 2px; + padding-top: 3px; + font-size: 16px; + display: none; +} +.nav-facet .nav-item.active .show-map-link:after{ + display: none; +} + +.extended-map-control .btn{ + margin-top: 15px; + width: 100%; + box-sizing: border-box; +} +.leaflet-control { + display: none; +} +.dataset-map-expanded .leaflet-control { + display: block; +} +.leaflet-control-arrow{ + border: none; + box-shadow: none; + padding: 3px; +} +.leaflet-control-arrow-left, +.leaflet-control-arrow-right, +.leaflet-control-arrow-up, +.leaflet-control-arrow-down{ + width: 22px; + height: 22px; + border-radius: 4px; + display: block; + border: 1px solid #aaa; + font-size: 15px; + text-align: center; + position: relative; +} +.leaflet-control-arrow-left{ + left: -12px; + float: left; +} +.leaflet-control-arrow-right{ + right: 12px; + float: right; +} +.leaflet-control-arrow-down{ + clear: both; +} +.leaflet-control-arrow-left:before, +.leaflet-control-arrow-right:before, +.leaflet-control-arrow-up:before, +.leaflet-control-arrow-down:before{ + color: #000; + font-family: "FontAwesome"; +} +.leaflet-control-arrow-left:hover:before, +.leaflet-control-arrow-right:hover:before, +.leaflet-control-arrow-up:hover:before, +.leaflet-control-arrow-down:hover:before{ + color: #777; +} +.leaflet-control-arrow-left:hover, +.leaflet-control-arrow-right:hover, +.leaflet-control-arrow-up:hover, +.leaflet-control-arrow-down:hover, +.leaflet-control-arrow-left:focus, +.leaflet-control-arrow-right:focus, +.leaflet-control-arrow-up:focus, +.leaflet-control-arrow-down:focus{ + background: #fff; + text-decoration: none; +} +.leaflet-control-arrow-left:before{ + content: "\f060"; +} +.leaflet-control-arrow-right:before{ + content: "\f061"; +} +.leaflet-control-arrow-up:before{ + content: "\f062"; +} +.leaflet-control-arrow-down:before{ + content: "\f063"; +} +@media (max-width: 768px){ + .dataset-map-expanded #dataset-map-container{ + width: 100%; + } + .dataset-map-expanded #extended-map-panel{ + display: none; + } +} \ No newline at end of file diff --git a/ckanext/qdes_schema/fanstatic/spatial/spatial_query.js b/ckanext/qdes_schema/fanstatic/spatial/spatial_query.js new file mode 100644 index 00000000..374ac20f --- /dev/null +++ b/ckanext/qdes_schema/fanstatic/spatial/spatial_query.js @@ -0,0 +1,481 @@ +/* Module for handling the spatial querying + */ +this.ckan.module('spatial-query', function ($, _) { + + L.Control.Arrow = L.Control.extend({ + options: { + position: 'topleft' + }, + + onAdd: function (map) { + var arrowName = 'leaflet-control-arrow', + barName = 'leaflet-bar', + partName = barName + '-part', + container = L.DomUtil.create('div', arrowName + ' ' + barName); + + this._map = map; + + this._moveUpButton = this._createButton('', 'Move up', + arrowName + '-up ' + + partName + ' ' + + partName + '-up', + container, this._move('up'), this); + + this._moveLeftButton = this._createButton('', 'Move left', + arrowName + '-left ' + + partName + ' ' + + partName + '-left', + container, this._move('left'), this); + + this._moveRightButton = this._createButton('', 'Move right', + arrowName + '-right ' + + partName + ' ' + + partName + '-right', + container, this._move('right'), this); + + this._moveDownButton = this._createButton('', 'Move down', + arrowName + '-down ' + + partName + ' ' + + partName + '-down', + container, this._move('down'), this); + + + return container; + }, + + onRemove: function () { + + }, + + _move: function (direction) { + var d = [0, 0]; + var self = this; + + switch (direction){ + case 'up': + d[1] = -10; + break; + case 'down': + d[1] = 10; + break; + case 'left': + d[0] = -10; + break; + case 'right': + d[0] = 10; + break; + } + return function(){ + self._map.panBy(d); + }; + }, + + _createButton: function (html, title, className, container, fn, context) { + var link = L.DomUtil.create('a', className, container); + link.innerHTML = html; + link.href = '#'; + link.title = title; + + var stop = L.DomEvent.stopPropagation; + + L.DomEvent + .on(link, 'click', stop) + .on(link, 'mousedown', stop) + .on(link, 'dblclick', stop) + .on(link, 'click', L.DomEvent.preventDefault) + .on(link, 'click', fn, context); + + return link; + } + }); + + + + + return { + options: { + i18n: { + }, + style: { + color: '#F06F64', + weight: 2, + opacity: 1, + fillColor: '#F06F64', + fillOpacity: 0.1, + clickable: false + }, + default_extent: [[90, 180], [-90, -180]], + draw_default: false + }, + template: { + buttons: [ + '
' + ].join('') + }, + + initialize: function () { + $.proxyAll(this, /_on/); + + var user_default_extent = this.el.data('default_extent'); + if (user_default_extent ){ + if (user_default_extent instanceof Array) { + // Assume it's a pair of coords like [[90, 180], [-90, -180]] + this.options.default_extent = user_default_extent; + } else if (user_default_extent instanceof Object) { + // Assume it's a GeoJSON bbox + this.options.default_extent = new L.GeoJSON(user_default_extent).getBounds(); + } + } + this.el.ready(this._onReady); + }, + + _getParameterByName: function (name) { + var match = RegExp('[?&]' + name + '=([^&]*)') + .exec(window.location.search); + return match ? + decodeURIComponent(match[1].replace(/\+/g, ' ')) + : null; + }, + + _drawExtentFromCoords: function(xmin, ymin, xmax, ymax) { + if ($.isArray(xmin)) { + var coords = xmin; + xmin = coords[0]; ymin = coords[1]; xmax = coords[2]; ymax = coords[3]; + } + return new L.Rectangle([[ymin, xmin], [ymax, xmax]], + this.options.style); + }, + + _drawExtentFromGeoJSON: function(geom) { + return new L.GeoJSON(geom, {style: this.options.style}); + }, + + _onReady: function() { + var module = this; + var map; + var defaultMapZoom = this.options.defaultMapZoom; + var extentLayer; + var previous_extent; + var is_exanded = false; + var should_zoom = true; + var default_drawn = false; + var form = $("#dataset-search"); + var map_attribution = $('#dataset-map-attribution'); + var map_nav = $('#dataset-map-nav'); + var show_map_link = $('.show-map-link', map_nav); + // CKAN 2.1 + if (!form.length) { + form = $(".search-form"); + } + var aFields = ['west-lng', 'south-lat', 'east-lng', 'north-lat']; + var aForm = []; + for (var f in aFields){ + aForm.push($('#' + aFields[f])); + } + + var buttons; + + var jqaForm = $(); // empty jQuery object + $.each(aForm, function(i, o) { + jqaForm = jqaForm.add(o); + }); + jqaForm.on('change', function(e){ + $(e.target).next().text(parseFloat(e.target.value, 5).toFixed(1)); + }); + + // Add necessary fields to the search form if not already created + $(['ext_bbox', 'ext_prev_extent']).each(function(index, item){ + if ($("#" + item).length === 0) { + $('').attr({'id': item, 'name': item}).appendTo(form); + } + }); + + // OK map time + var northEast = L.latLng(-0.753707504067988, 157.21076121765617), + southWest = L.latLng(-44.33550977339949, 108.39984808917536), + maxBounds = L.latLngBounds(northEast, southWest); + map = ckan.commonLeafletMap( + 'dataset-map-container', + this.options.map_config, + { + attributionControl: false, + drawControlTooltips: false, + zoomDelta: 0.25, + zoomSnap: 0.25, + maxBounds: maxBounds + }, + { + minZoom: 3.75 + } + ); + + // Initialize the draw control + map.addControl(new L.Control.Draw({ + position: 'topright', + draw: { + polyline: false, + polygon: false, + circle: false, + circlemarker: false, + marker: false, + rectangle: {shapeOptions: module.options.style} + } + })); + map.addControl(new L.Control.Arrow()); + + $('#dataset-map-clear').on('click', clearMap) + + // OK add the expander + $('.leaflet-control-draw a', module.el) + .add($('.show-map-link', map_nav)) + .on('click', function() { + if (!is_exanded) { + map_nav.hide(); + $('body').addClass('dataset-map-expanded'); + + if (!extentLayer) { + if (should_zoom){ + map.setZoom(defaultMapZoom); + } + } else if (extentLayer){ + map.fitBounds(extentLayer.getBounds()); + } + resetMap(); + is_exanded = true; + } + }); + $('.show-map-link i', map_nav).on('click', function(e){ + window.location.href = $('#dataset-map-clear').attr('href'); + e.stopPropagation(); + }); + + $('.extended-map-show-form a', module.el).on('click', toggleCoordinateForm); + + // Setup the expanded buttons + buttons = $(module.template.buttons).insertBefore(map_attribution); + + // Handle the cancel expanded action + $('.cancel', buttons).on('click', function() { + + map_nav.show(); + $('body').removeClass('dataset-map-expanded dataset-map-layer-drawn'); + show_map_link.parent().removeClass('active'); + + var show_form = $('.extended-map-show-form a'); + if (show_form.hasClass('active')) { + show_form.trigger('click'); + } + + if (extentLayer) { + map.removeLayer(extentLayer); + extentLayer = null; + } + setPreviousBBBox(); + setPreviousExtent(); + resetMap(); + is_exanded = false; + }); + + // Handle the apply expanded action + $('.apply', buttons).on('click', function(event) { + if ($(event.target).hasClass('disabled')) return; + if (extentLayer) { + $('body').removeClass('dataset-map-expanded'); + is_exanded = false; + resetMap(); + // Eugh, hacky hack. + setTimeout(function() { + map.fitBounds(extentLayer.getBounds()); + submitForm(); + }, 200); + } + }); + + $('#extended-map-reset').on('click', resetBBoxToCurrentView); + $('#extended-map-update').on('click', function(){ + var c = []; + for (var i in aForm){ + c.push(aForm[i].val()); + } + if (c.every(function(e){ + return e.length; + })){ + var rect = getRectFromCoordinates([ + [c[3], c[0]], + [c[1], c[2]] + ]); + + drawRect(rect); + default_drawn = false; + } + }); + + // When user finishes drawing the box, record it and add it to the map + map.on('draw:created', function (e) { + bbox_preparations(); + + drawRect(e.layer); + + var drawSelectedBtn = $('.extended-map-show-form a'); + if (drawSelectedBtn.hasClass('active')){ + drawSelectedBtn.trigger('click'); + } + }); + + // Record the current map view so we can replicate it after submitting + map.on('moveend', function() { + $('#ext_prev_extent').val(map.getBounds().toBBoxString()); + }); + + // Ok setup the default state for the map + var previous_bbox; + + setPreviousBBBox(); + setPreviousExtent(); + + // OK, when we expand we shouldn't zoom then + map.on('zoomstart', function() { + should_zoom = false; + }); + + function getRectFromCoordinates(c){ + + return new L.Rectangle( + new L.LatLngBounds(L.latLng(c[0]), L.latLng(c[1])), + module.options.style + ); + } + + function resetBBoxToCurrentView() { + if (extentLayer) { + map.removeLayer(extentLayer); + } + drawBBox(map.getBounds().toBBoxString()); + $('body').addClass('dataset-map-layer-drawn'); + } + + function drawRect(rect) { + if (extentLayer) { + map.removeLayer(extentLayer); + } + extentLayer = rect; + var bbox_string = extentLayer.getBounds().toBBoxString(); + $('#ext_bbox').val(bbox_string); + fillForm(bbox_string); + map.addLayer(extentLayer); + map.fitBounds(extentLayer.getBounds()); + apply_switch(true); + } + + // Is there an existing box from a previous search? + function setPreviousBBBox() { + previous_bbox = module._getParameterByName('ext_bbox'); + if (previous_bbox) { + bbox_preparations(); + drawBBox(previous_bbox); + } else { + fillForm(null); + } + } + + function drawBBox(bbox, is_default) { + default_drawn = is_default; + $('#ext_bbox').val(bbox); + extentLayer = module._drawExtentFromCoords(bbox.split(',')); + fillForm(bbox); + map.addLayer(extentLayer); + apply_switch(true); + } + + // Is there an existing extent from a previous search? + function setPreviousExtent() { + previous_extent = module._getParameterByName('ext_bbox') || + module._getParameterByName('ext_prev_extent'); + if (previous_extent) { + var coords = previous_extent.split(','); + var prev_bounds = module._drawExtentFromCoords(coords).getBounds(); + setTimeout(function() { + map.fitBounds(prev_bounds); + }, 0); + + } else { + if (!previous_bbox){ + map.fitBounds(module.options.default_extent); + } + } + } + + // Reset map view + function resetMap() { + L.Util.requestAnimFrame(map.invalidateSize, map, !1, map._container); + } + + // Add the loading class and submit the form + function submitForm() { + setTimeout(function() { + form.submit(); + }, 800); + } + + function bbox_preparations() { + $('body').addClass('dataset-map-layer-drawn'); + show_map_link.parent().addClass('active'); + } + + function toggleCoordinateForm(event) { + $(event.target).parent().toggleClass('active'); + + if (module.options.draw_default && module.options.default_extent) { + if (!default_drawn && !extentLayer) { + fallback_default = getRectFromCoordinates( + module.options.default_extent) + .getBounds(); + + drawBBox(fallback_default.toBBoxString(), true); + } + } + } + + function clearMap(event) { + event && event.preventDefault(); + $('body').removeClass('dataset-map-layer-drawn'); + if (extentLayer) { + map.removeLayer(extentLayer); + } + var ext_bb = $('#ext_bbox'); + $('#ext_prev_extent').val(ext_bb.val()); + ext_bb.val(''); + fillForm(null); + } + + function fillForm(bounds){ + if (bounds === null) { + $('.extended-map-form input').val(''); + $('#ext_bbox').val(''); + return; + } + var b = $.map(bounds.split(','), function(e){ + return parseFloat(e).toFixed(1); + }); + + for (var i in b){ + aForm[i].val(b[i]).trigger('change'); + } + + } + + function apply_switch(state) { + var ab = $('.apply', buttons); + if (state){ + ab.removeClass('disabled').addClass('btn-primary'); + } else { + ab.removeClass('btn-primary').addClass('disabled'); + } + } + + } + }; +}); \ No newline at end of file diff --git a/ckanext/qdes_schema/fanstatic/webassets.yml b/ckanext/qdes_schema/fanstatic/webassets.yml index 0bf0eb21..b9abbbc6 100644 --- a/ckanext/qdes_schema/fanstatic/webassets.yml +++ b/ckanext/qdes_schema/fanstatic/webassets.yml @@ -96,3 +96,37 @@ spinner: calculated_quality_measure: contents: - calculated_quality_measure.js + +spatial_map_input_js: + filter: rjsmin + extra: + preload: + - base/main + contents: + - js/vendor/leaflet/1.9.3/leaflet.js + - js/vendor/leaflet.draw/0.4.14/leaflet.draw.js + - js/common_map.js + - spatial/spatial_map_input.js + +spatial_map_input_css: + contents: + - js/vendor/leaflet/1.9.3/leaflet.css + - js/vendor/leaflet.draw/0.4.14/leaflet.draw.css + - spatial/spatial_map_input.css + +spatial_query_js: + filter: rjsmin + extra: + preload: + - base/main + contents: + - js/vendor/leaflet/1.9.3/leaflet.js + - js/vendor/leaflet.draw/0.4.14/leaflet.draw.js + - js/common_map.js + - spatial/spatial_query.js + +spatial_query_css: + contents: + - js/vendor/leaflet/1.9.3/leaflet.css + - js/vendor/leaflet.draw/0.4.14/leaflet.draw.css + - spatial/spatial_query.css \ No newline at end of file diff --git a/ckanext/qdes_schema/jobs.py b/ckanext/qdes_schema/jobs.py index efd09df3..82ba50b4 100644 --- a/ckanext/qdes_schema/jobs.py +++ b/ckanext/qdes_schema/jobs.py @@ -136,9 +136,13 @@ def unpublish_external_distribution(publish_log_id, user): status = constants.PUBLISH_STATUS_SUCCESS # Remove dataset identifier. identifiers = json.loads(package_dict['identifiers']) if package_dict.get('identifiers') else [] - identifiers.remove(destination.address + '/dataset/' + external_dataset_id) - identifiers = list(set(identifiers)) - package_dict['identifiers'] = json.dumps(identifiers) + identifier = f"{destination.address}/dataset/{external_dataset_id}" + if identifier in identifiers: + identifiers.remove(identifier) + identifiers = list(set(identifiers)) + package_dict['identifiers'] = json.dumps(identifiers) + else: + log.warning(f"unpublish_external_distribution: Identifier {identifier} not found in dataset {package_dict.get('name')}") elif resources: # Remove the resource. diff --git a/ckanext/qdes_schema/templates/group/read.html b/ckanext/qdes_schema/templates/group/read.html index d7d56450..bde782f7 100644 --- a/ckanext/qdes_schema/templates/group/read.html +++ b/ckanext/qdes_schema/templates/group/read.html @@ -4,7 +4,7 @@ {% snippet "group/snippets/info.html", group=group_dict, show_nums=true %} {% set default_extent = h.get_qld_bounding_box_config() %} - {% snippet "snippets/spatial_query.html", default_extent=default_extent, extras={'id':group_dict.id} %} + {% snippet "spatial/snippets/spatial_query.html", default_extent=default_extent, extras={'id':group_dict.id} %}