From fd9f02d510c32f4ea31459d895ec7457ea79cba2 Mon Sep 17 00:00:00 2001 From: Matthias Mohr Date: Thu, 26 Jan 2023 14:20:53 +0100 Subject: [PATCH] Add geocoding support #298 --- config.js | 3 ++ package.json | 5 +- src/components/datatypes/MapAreaSelect.vue | 42 +++++++--------- src/components/maps/GeoJsonMapEditor.vue | 18 ++++++- src/components/maps/GeocoderMixin.vue | 58 ++++++++++++++++++++++ src/components/maps/MapMixin.scss | 1 - src/components/maps/MapMixin.vue | 11 ++++ src/components/maps/osmgeocoder.js | 41 +++++++++++++++ src/components/viewer/MapViewer.vue | 18 ++++++- src/export/r.js | 2 +- 10 files changed, 168 insertions(+), 31 deletions(-) create mode 100644 src/components/maps/GeocoderMixin.vue create mode 100644 src/components/maps/osmgeocoder.js diff --git a/config.js b/config.js index 67f4b3883..0257d425a 100644 --- a/config.js +++ b/config.js @@ -19,6 +19,9 @@ export default { mapLocation: [49.8, 9.9], mapZoom: 4, + // OSM Nominatim compliant geocoder URL, remove to disable + geocoder: "https://nominatim.openstreetmap.org/search/", + // A message shown on the login page loginMessage: '', diff --git a/package.json b/package.json index 8bf9d7066..771754a55 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "build": "npm run build:database && npx vue-cli-service build --report" }, "dependencies": { + "@kirtandesai/ol-geocoder": "^5.0.6", "@musement/iso-duration": "^1.0.0", "@openeo/js-client": "^2.5.1", "@openeo/js-commons": "^1.4.1", @@ -57,8 +58,8 @@ "jsonlint-mod": "^1.7.6", "luxon": "^2.4.0", "node-polyfill-webpack-plugin": "^2.0.0", - "ol": "^6.14.1", - "ol-ext": "^3.2.20", + "ol": "^7.2.2", + "ol-ext": "^4.0.4", "proj4": "^2.7.5", "splitpanes": "^2.3.6", "v-clipboard": "^2.2.3", diff --git a/src/components/datatypes/MapAreaSelect.vue b/src/components/datatypes/MapAreaSelect.vue index b6bf0f1cb..1239717f6 100644 --- a/src/components/datatypes/MapAreaSelect.vue +++ b/src/components/datatypes/MapAreaSelect.vue @@ -14,10 +14,14 @@ import { transformExtent } from 'ol/proj'; import { containsXY } from 'ol/extent'; import { createDefaultStyle } from 'ol/style/Style'; import TextControl from '../maps/TextControl.vue'; +import GeocoderMixin from '../maps/GeocoderMixin.vue'; export default { name: 'MapAreaSelect', - mixins: [MapMixin], + mixins: [ + GeocoderMixin, + MapMixin + ], components: { TextControl }, @@ -31,17 +35,9 @@ export default { } }, data() { - let extent = null; - if (Utils.isObject(this.value) && "west" in this.value && "south" in this.value && "east" in this.value && "north" in this.value) { - extent = [this.value.west, this.value.south, this.value.east, this.value.north]; - } - else if (Array.isArray(this.value) && value.length >= 4) { - extent = this.value; - } - return { interaction: null, - extent + extent: this.toExtent(this.value) }; }, computed: { @@ -71,22 +67,20 @@ export default { } this.$emit('input', this.returnAsObject ? this.bbox : this.extent); }, - ensureValidExtent(extent) { - if (!extent) { - return extent; - } - return [ - Math.max(extent[0], -180), - Math.max(extent[1], -90), - Math.min(extent[2], 180), - Math.min(extent[3], 90) - ]; - }, async renderMap() { let isWebMercatorCompatible = Utils.isBboxInWebMercator(this.bbox) !== false; await this.createMap(isWebMercatorCompatible ? 'EPSG:3857' : 'EPSG:4326'); this.addBasemaps(); + this.addGeocoder(bbox => { + if (!bbox) { + return; + } + let extent = this.toExtent(bbox); + extent = transformExtent(extent, 'EPSG:4326', this.map.getView().getProjection()); + this.interaction.setExtent(extent); + this.fitMap(); + }); let condition = (event) => { if (!this.editable) { @@ -129,14 +123,14 @@ export default { pixelTolerance: 15 }); - // let oldImplementation = this.interaction.setExtent.bind(this.interaction); - // this.interaction.setExtent = extent => oldImplementation(this.ensureValidExtent(extent)); if (this.editable) { this.interaction.on('extentchanged', this.update); } this.map.addInteraction(this.interaction); - + this.fitMap(); + }, + fitMap() { // If not ediable, make a bigger extent visible so that user can get a better overview if (this.projectedExtent) { var fitOptions = this.getFitOptions(this.editable ? 10 : 33); diff --git a/src/components/maps/GeoJsonMapEditor.vue b/src/components/maps/GeoJsonMapEditor.vue index a55546991..ab26d1f69 100644 --- a/src/components/maps/GeoJsonMapEditor.vue +++ b/src/components/maps/GeoJsonMapEditor.vue @@ -7,7 +7,8 @@ + + + + \ No newline at end of file diff --git a/src/components/maps/MapMixin.scss b/src/components/maps/MapMixin.scss index e96cd794c..9d6ca0919 100644 --- a/src/components/maps/MapMixin.scss +++ b/src/components/maps/MapMixin.scss @@ -1,7 +1,6 @@ /* Customize layerswitcher control */ .ol-layerswitcher > button { font-size: 1.14em; - background-color: rgba(0,60,136,.5); &:before, &:after { diff --git a/src/components/maps/MapMixin.vue b/src/components/maps/MapMixin.vue index 9b9f3ba28..dfc6c58de 100644 --- a/src/components/maps/MapMixin.vue +++ b/src/components/maps/MapMixin.vue @@ -241,7 +241,18 @@ export default { fromLonLat(coords) { return fromLonLat(coords, this.map.getView().getProjection()); + }, + toExtent(value) { + let extent = null; + if (Utils.isObject(value) && "west" in value && "south" in value && "east" in value && "north" in value) { + extent = [value.west, value.south, value.east, value.north]; + } + else if (Array.isArray(value) && value.length >= 4) { + extent = value; + } + return extent; } + } } \ No newline at end of file diff --git a/src/components/maps/osmgeocoder.js b/src/components/maps/osmgeocoder.js new file mode 100644 index 000000000..4898043aa --- /dev/null +++ b/src/components/maps/osmgeocoder.js @@ -0,0 +1,41 @@ +export default class OSMGeocoder { + constructor(url, geojson = false) { + this.url = url; + this.geojson = geojson; + } + + getParameters(opt) { + return { + url: this.url, + params: { + q: opt.query, + format: 'json', + limit: 10, + 'accept-language': 'en', + polygon_geojson: this.geojson ? 1 : 0, + polygon_threshold: 0.001, + }, + }; + } + + handleResponse(results) { + if (results.length === 0) { + return []; + } + return results + .filter(result => ["boundary", "geological", "leisure", "natural", "place", "water", "waterway"].includes(result.class)) + .map(result => ({ + lon: result.lon, + lat: result.lat, + bbox: result.boundingbox, + address: { + name: result.display_name + }, + original: { + formatted: result.display_name, + details: result.address, + geojson: result.geojson + } + })); + } +} \ No newline at end of file diff --git a/src/components/viewer/MapViewer.vue b/src/components/viewer/MapViewer.vue index 928486238..c28bfac8b 100644 --- a/src/components/viewer/MapViewer.vue +++ b/src/components/viewer/MapViewer.vue @@ -28,6 +28,7 @@ import JSON_ from '../../formats/json'; import { Splitpanes, Pane } from 'splitpanes'; import ExtentMixin from '../maps/ExtentMixin.vue'; +import GeocoderMixin from '../maps/GeocoderMixin.vue'; import GeoTiffMixin from '../maps/GeoTiffMixin.vue'; import MapMixin from '../maps/MapMixin.vue'; import ScatterChart from './ScatterChart.vue'; @@ -37,6 +38,7 @@ import { Service } from '@openeo/js-client'; import Feature from 'ol/Feature'; import { fromExtent as PolygonFromExtent } from 'ol/geom/Polygon'; +import { transformExtent } from 'ol/proj'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; @@ -112,7 +114,13 @@ GeoTIFFImage.prototype.getBitsPerSample = function(sampleIndex = 0) { export default { name: 'MapViewer', - mixins: [ExtentMixin, GeoTiffMixin, MapMixin, WebServiceMixin], + mixins: [ + ExtentMixin, + GeocoderMixin, + GeoTiffMixin, + MapMixin, + WebServiceMixin + ], components: { Pane, ScatterChart, @@ -169,6 +177,14 @@ export default { await this.createMap(view); this.addLayerSwitcher(); + this.addGeocoder(data => { + if (!data) { + return; + } + let extent = this.toExtent(data); + extent = transformExtent(extent, 'EPSG:4326', this.map.getView().getProjection()); + this.map.getView().fit(extent, this.getFitOptions()); + }); if (this.isGeoJson) { this.addBasemaps(); diff --git a/src/export/r.js b/src/export/r.js index 9d5a5f3ee..a31f150ae 100644 --- a/src/export/r.js +++ b/src/export/r.js @@ -60,9 +60,9 @@ export default class R extends Exporter { async generateFunction(node) { let variable = this.var(node.id, this.varPrefix()); let args = await this.generateArguments(node); - // ToDo: This doesn't seem to be supported in R yet if (node.namespace) { throw new Error("The R client doesn't support namespaced processes yet"); + // ToDo: This doesn't seem to be supported in R yet // args.namespace = this.e(node.namespace); } args = Utils.mapObject(args, (value, name) => `${name} = ${this.e(value)}`);