UNPKG

@geoext/geoext

Version:

GeoExt is Open Source and enables building desktop-like GIS applications through the web. It is a JavaScript framework that combines the GIS functionality of OpenLayers with the user interface savvy of the ExtJS library provided by Sencha.

430 lines (385 loc) 12.1 kB
/* Copyright (c) 2015-present The Open Source Geospatial Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /** * Creates a combo box that handles results from a geocoding service. By * default it uses OSM Nominatim, but the component offers all config options * to overwrite in order to other custom services. * If the user enters a valid address in the search box, the combo's store will * be populated with records that match the * address. By default, records have the following fields: * * * name - `String` The formatted address. * * extent - `ol.Extent` The extent of the matching address * * bounds - `ol.Coordinate` The point coordinate of the matching address * * **CAUTION: This class is only usable in applications using the classic * toolkit of ExtJS 6.** * * @class GeoExt.form.field.GeocoderComboBox */ Ext.define('GeoExt.form.field.GeocoderComboBox', { extend: 'Ext.form.field.ComboBox', alias: [ 'widget.gx_geocoder_combo', 'widget.gx_geocoder_combobox', 'widget.gx_geocoder_field', ], requires: ['Ext.data.JsonStore'], mixins: ['GeoExt.mixin.SymbolCheck'], /** * The OpenLayers map to work on. If not provided the selection of an * address would have no effect. * * @cfg {ol.Map} */ map: null, /** * Vector layer to visualize the selected address. * Will be created if not provided. * * @cfg {ol.layer.Vector} locationLayer The layer used for displaying the selected address. * @property {ol.layer.Vector} locationLayer The layer used for displaying the selected address. */ locationLayer: null, /** * The style of the #locationLayer. Only has an effect if the layer is not * passed in while creation. * * @cfg {ol.style.Style} */ locationLayerStyle: null, /** * The store used for this combo box. Default is a * store with the url configured as #url * config. * * @cfg {Ext.data.JsonStore} store The store used for this combo box. * @property {Ext.data.JsonStore} store The store used for this combo box. */ store: null, /** * The property in the JSON response of the geocoding service used in * the store's proxy as root object. * * @cfg {string} */ proxyRootProperty: null, /** * The field to display in the combobox result. Default is * "name" for instant use with the default store for this component. * * @cfg {string} */ displayField: 'name', /** * The field in the GeoCoder service repsonse to be used as mapping for the * 'name' field in the #store. * Ignored when a store is passed in. * * @cfg {string} */ displayValueMapping: 'display_name', /** * Field from selected record to use when the combo's * #getValue method is called. Default is "extent". This field is * supposed to contain an ol.Extent. * By setting this to 'coordinate' a field holding an ol.Coordinate is used. * * @cfg {string} */ valueField: 'extent', /** * The query parameter for the user entered search text. * Default is 'q' for instant use with OSM Nominatim. * * @cfg {string} */ queryParam: 'q', /**'Search' * Text to display for an empty field. * * @cfg {String} */ emptyText: 'Search for a location', /** * Minimum number of entered characters to trigger a search. * * @cfg {number} */ minChars: 3, /** * Delay before the search occurs in ms. * * @cfg {number} */ queryDelay: 100, /** * URL template for querying the geocoding service. If a store is * configured, this will be ignored. Note that the #queryParam will be used * to append the user's combo box input to the url. * * @cfg {string} */ url: 'https://nominatim.openstreetmap.org/search?format=json', /** * The SRS used by the geocoder service. * * @cfg {string} */ srs: 'EPSG:4326', /** * Zoom level when zooming to a location (#valueField='coordinate') * Not used when zooming to extent. * * @cfg {number} */ zoom: 10, /** * Flag to steer if selected address feature is drawn on #map * (by #locationLayer). * * @cfg {boolean} */ showLocationOnMap: true, /** * Flag to restrict nomination query to current map extent * * @cfg {boolean} */ restrictToMapExtent: false, /** * @private */ initComponent: function () { const me = this; me.updateExtraParams = me.updateExtraParams.bind(me); if (!me.store) { me.store = Ext.create('Ext.data.JsonStore', { fields: [ {name: 'name', mapping: me.displayValueMapping}, {name: 'extent', convert: me.convertToExtent}, {name: 'coordinate', convert: me.convertToCoordinate}, ], proxy: { type: 'ajax', url: me.url, reader: { type: 'json', rootProperty: me.proxyRootProperty, }, }, }); } if (!me.locationLayer) { me.locationLayer = new ol.layer.Vector({ source: new ol.source.Vector(), style: me.locationLayerStyle !== null ? me.locationLayerStyle : undefined, }); if (me.map) { me.map.addLayer(me.locationLayer); } } me.callParent(arguments); me.on({ unRestrictMapExtent: me.unRestrictExtent, restrictToMapExtent: me.restrictExtent, select: me.onSelect, focus: me.onFocus, scope: me, }); if (me.restrictToMapExtent) { me.restrictExtent(); } }, /** * Handle restriction to viewbox: register moveend event * and update params of AJAX proxy */ restrictExtent: function () { const me = this; me.map.on('moveend', me.updateExtraParams); me.updateExtraParams(); }, /** * Update viewbox parameter based on the current map extent */ updateExtraParams: function () { const me = this; const mapSize = me.map.getSize(); const mv = me.map.getView(); const extent = mv.calculateExtent(mapSize); me.addMapExtentParams(extent, mv.getProjection()); }, /** * Update map extent params of AJAX proxy. * * By default, 'viewbox' and 'bounded' are updated since Nominatim is the * default geocoder in this class. If no projection is passed the one of * the map view is used. * * @param {ol.Extent} extent The extend to restrict the geocoder to * @param {ol.proj.Projection} projection The projection of given extent */ addMapExtentParams: function (extent, projection) { const me = this; if (!projection) { projection = me.map.getView().getProjection(); } let ll = ol.proj.transform([extent[0], extent[1]], projection, 'EPSG:4326'); let ur = ol.proj.transform([extent[2], extent[3]], projection, 'EPSG:4326'); ll = Ext.Array.map(ll, function (val) { return Math.min(Math.max(val, -180), 180); }); ur = Ext.Array.map(ur, function (val) { return Math.min(Math.max(val, -180), 180); }); const viewBoxStr = [ll.join(','), ur.join(',')].join(','); if (me.store && me.store.getProxy()) { me.store.getProxy().setExtraParam('viewbox', viewBoxStr); me.store.getProxy().setExtraParam('bounded', '1'); } }, /** * Cleanup if extent restriction is omitted. * -> moveend event from map * -> call removeMapExtentParams to reset params set in store */ unRestrictExtent: function () { const me = this; // unbinding moveend event me.map.un('moveend', me.updateExtraParams); // cleanup params in store me.removeMapExtentParams(); }, /** * Remove restriction to viewbox, in particular remove viewbox * and bounded parameters from AJAX proxy for nominatim queries */ removeMapExtentParams: function () { const me = this; if (me.store && me.store.getProxy()) { me.store.getProxy().setExtraParam('viewbox', undefined); me.store.getProxy().setExtraParam('bounded', undefined); } }, /** * Function to convert the data delivered by the geocoder service to an * ol.Extent ([minx, miny, maxx, maxy]). * Default implementation converts the Nominatim response. * Can be overwritten to work with other services. * * @param {Mixed} v The data value as read by the Reader * @param {Ext.data.Model} rec The data record containing raw data * @return {ol.Extent} The created ol.Extent */ convertToExtent: function (v, rec) { const rawExtent = rec.get('boundingbox'); const minx = parseFloat(rawExtent[2], 10); const miny = parseFloat(rawExtent[0], 10); const maxx = parseFloat(rawExtent[3], 10); const maxy = parseFloat(rawExtent[1], 10); return [minx, miny, maxx, maxy]; }, /** * Function to convert the data delivered by the geocoder service to an * ol.Coordinate ([x, y]). * Default implementation converts the Nominatim response. * Can be overwritten to work with other services. * * @param {Mixed} v The data value as read by the Reader * @param {Ext.data.Model} rec The data record containing raw data * @return {ol.Coordinate} The created ol.Coordinate */ convertToCoordinate: function (v, rec) { return [parseFloat(rec.get('lon'), 10), parseFloat(rec.get('lat'), 10)]; }, /** * Draws the selected address feature on the map. * * @param {ol.Coordinate | ol.Extent} coordOrExtent Location feature to be * drawn on the map */ drawLocationFeatureOnMap: function (coordOrExtent) { const me = this; let geom; if (coordOrExtent.length === 2) { geom = new ol.geom.Point(coordOrExtent); } else if (coordOrExtent.length === 4) { geom = ol.geom.Polygon.fromExtent(coordOrExtent); } if (geom) { const feat = new ol.Feature({ geometry: geom, }); me.locationLayer.getSource().clear(); me.locationLayer.getSource().addFeature(feat); } }, /** * Removes the drawn address feature from the map. */ removeLocationFeature: function () { this.locationLayer.getSource().clear(); }, /** * Handles the 'focus' event of this ComboBox. */ onFocus: function () { const me = this; me.clearValue(); me.removeLocationFeature(); }, /** * Handles the 'select' event of this ComboBox. * Zooms to the selected address and draws the address feature on the map * (if configured in #showLocationOnMap) * * @param {GeoExt.form.field.GeocoderComboBox} combo [description] * @param {Ext.data.Model} record [description] * * @private */ onSelect: function (combo, record) { const me = this; if (!me.map) { Ext.Logger.warn( 'No map configured in ' + 'GeoExt.form.field.GeocoderComboBox. Skip zoom to selection.', ); return; } const value = record.get(me.valueField); let projValue; const olMapView = me.map.getView(); const targetProj = olMapView.getProjection().getCode(); if (value.length === 2) { // point based value projValue = ol.proj.transform(value, me.srs, targetProj); // adjust the map olMapView.setCenter(projValue); olMapView.setZoom(me.zoom); } else if (value.length === 4) { // bbox based value projValue = ol.proj.transformExtent(value, me.srs, targetProj); // adjust the map olMapView.fit(projValue); } if (me.showLocationOnMap) { me.drawLocationFeatureOnMap(projValue); } }, });