UNPKG

esri-leaflet-geocoder

Version:

Esri Geocoding utility and search plugin for Leaflet.

1,326 lines (1,108 loc) 45.8 kB
/* esri-leaflet-geocoder - v3.1.4 - Thu Feb 23 2023 13:29:24 GMT-0600 (Central Standard Time) * Copyright (c) 2023 Environmental Systems Research Institute, Inc. * Apache-2.0 */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('leaflet'), require('esri-leaflet')) : typeof define === 'function' && define.amd ? define(['exports', 'leaflet', 'esri-leaflet'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.L = global.L || {}, global.L.esri = global.L.esri || {}, global.L.esri.Geocoding = {}), global.L, global.L.esri)); })(this, (function (exports, leaflet, esriLeaflet) { 'use strict'; var version = "3.1.4"; var WorldGeocodingServiceUrl = 'https://geocode-api.arcgis.com/arcgis/rest/services/World/GeocodeServer/'; var Geocode = esriLeaflet.Task.extend({ path: 'findAddressCandidates', params: { outSr: 4326, forStorage: false, outFields: '*', maxLocations: 20 }, setters: { address: 'address', neighborhood: 'neighborhood', city: 'city', subregion: 'subregion', region: 'region', postal: 'postal', country: 'country', text: 'singleLine', category: 'category', token: 'token', apikey: 'apikey', key: 'magicKey', fields: 'outFields', forStorage: 'forStorage', maxLocations: 'maxLocations', // World Geocoding Service (only works with singleLine) countries: 'sourceCountry' }, initialize: function (options) { options = options || {}; options.url = options.url || WorldGeocodingServiceUrl; esriLeaflet.Task.prototype.initialize.call(this, options); }, within: function (bounds) { bounds = leaflet.latLngBounds(bounds); this.params.searchExtent = esriLeaflet.Util.boundsToExtent(bounds); return this; }, nearby: function (coords, radius) { var centroid = leaflet.latLng(coords); this.params.location = centroid.lng + ',' + centroid.lat; if (radius) { this.params.distance = Math.min(Math.max(radius, 2000), 50000); } return this; }, run: function (callback, context) { if (this.options.token) { this.params.token = this.options.token; } if (this.options.apikey) { this.params.token = this.options.apikey; } if (this.options.customParam) { this.params[this.options.customParam] = this.params.singleLine; delete this.params.singleLine; } return this.request(function (error, response) { var processor = this._processGeocoderResponse; var results = (!error) ? processor(response) : undefined; callback.call(context, error, { results: results }, response); }, this); }, _processGeocoderResponse: function (response) { var results = []; for (var i = 0; i < response.candidates.length; i++) { var candidate = response.candidates[i]; if (candidate.extent) { var bounds = esriLeaflet.Util.extentToBounds(candidate.extent); } results.push({ text: candidate.address, bounds: bounds, score: candidate.score, latlng: leaflet.latLng(candidate.location.y, candidate.location.x), properties: candidate.attributes }); } return results; } }); function geocode (options) { return new Geocode(options); } var ReverseGeocode = esriLeaflet.Task.extend({ path: 'reverseGeocode', params: { outSR: 4326, returnIntersection: false }, setters: { distance: 'distance', language: 'langCode', intersection: 'returnIntersection', apikey: 'apikey' }, initialize: function (options) { options = options || {}; options.url = options.url || WorldGeocodingServiceUrl; esriLeaflet.Task.prototype.initialize.call(this, options); }, latlng: function (coords) { var centroid = leaflet.latLng(coords); this.params.location = centroid.lng + ',' + centroid.lat; return this; }, run: function (callback, context) { if (this.options.token) { this.params.token = this.options.token; } if (this.options.apikey) { this.params.token = this.options.apikey; } return this.request(function (error, response) { var result; if (!error) { result = { latlng: leaflet.latLng(response.location.y, response.location.x), address: response.address }; } else { result = undefined; } callback.call(context, error, result, response); }, this); } }); function reverseGeocode (options) { return new ReverseGeocode(options); } var Suggest = esriLeaflet.Task.extend({ path: 'suggest', params: {}, setters: { text: 'text', category: 'category', countries: 'countryCode', maxSuggestions: 'maxSuggestions' }, initialize: function (options) { options = options || {}; if (!options.url) { options.url = WorldGeocodingServiceUrl; options.supportsSuggest = true; } esriLeaflet.Task.prototype.initialize.call(this, options); }, within: function (bounds) { bounds = leaflet.latLngBounds(bounds); var center = bounds.getCenter(); var ne = bounds.getNorthWest(); this.params.location = center.lng + ',' + center.lat; this.params.distance = Math.min(Math.max(center.distanceTo(ne), 2000), 50000); this.params.searchExtent = esriLeaflet.Util.boundsToExtent(bounds); return this; }, nearby: function (coords, radius) { var centroid = leaflet.latLng(coords); this.params.location = centroid.lng + ',' + centroid.lat; if (radius) { this.params.distance = Math.min(Math.max(radius, 2000), 50000); } return this; }, run: function (callback, context) { if (this.options.supportsSuggest) { return this.request(function (error, response) { callback.call(context, error, response, response); }, this); } else { console.warn('this geocoding service does not support asking for suggestions'); } } }); function suggest (options) { return new Suggest(options); } var GeocodeService = esriLeaflet.Service.extend({ initialize: function (options) { options = options || {}; if (options.apikey) { options.token = options.apikey; } if (options.url) { esriLeaflet.Service.prototype.initialize.call(this, options); this._confirmSuggestSupport(); } else { options.url = WorldGeocodingServiceUrl; options.supportsSuggest = true; esriLeaflet.Service.prototype.initialize.call(this, options); } }, geocode: function () { return geocode(this); }, reverse: function () { return reverseGeocode(this); }, suggest: function () { // requires either the Esri World Geocoding Service or a <10.3 ArcGIS Server Geocoding Service that supports suggest. return suggest(this); }, _confirmSuggestSupport: function () { this.metadata(function (error, response) { if (error) { return; } // pre 10.3 geocoding services dont list capabilities (and dont support maxLocations) // only SOME individual services have been configured to support asking for suggestions if (!response.capabilities) { this.options.supportsSuggest = false; } else if (response.capabilities.indexOf('Suggest') > -1) { this.options.supportsSuggest = true; } else { this.options.supportsSuggest = false; } // whether the service supports suggest or not, utilize the metadata response to determine the appropriate parameter name for single line geocoding requests this.options.customParam = response.singleLineAddressField.name; }, this); } }); function geocodeService (options) { return new GeocodeService(options); } var GeosearchCore = leaflet.Evented.extend({ options: { zoomToResult: true, useMapBounds: 12, searchBounds: null }, initialize: function (control, options) { leaflet.Util.setOptions(this, options); this._control = control; if (!options || !options.providers || !options.providers.length) { throw new Error('You must specify at least one provider'); } this._providers = options.providers; }, _geocode: function (text, key, provider) { var activeRequests = 0; var allResults = []; var bounds; var callback = leaflet.Util.bind(function (error, results) { activeRequests--; if (error) { return; } if (results) { allResults = allResults.concat(results); } if (activeRequests <= 0) { bounds = this._boundsFromResults(allResults); this.fire('results', { results: allResults, bounds: bounds, latlng: (bounds) ? bounds.getCenter() : undefined, text: text }, true); if (this.options.zoomToResult && bounds) { this._control._map.fitBounds(bounds); } this.fire('load'); } }, this); if (key) { activeRequests++; provider.results(text, key, this._searchBounds(), callback); } else { for (var i = 0; i < this._providers.length; i++) { activeRequests++; this._providers[i].results(text, key, this._searchBounds(), callback); } } }, _suggest: function (text) { var activeRequests = this._providers.length; var suggestionsLength = 0; var createCallback = leaflet.Util.bind(function (text, provider) { return leaflet.Util.bind(function (error, suggestions) { activeRequests = activeRequests - 1; suggestionsLength += suggestions.length; if (error) { // an error occurred for one of the providers' suggest requests this._control._clearProviderSuggestions(provider); // perform additional cleanup when all requests are finished this._control._finalizeSuggestions(activeRequests, suggestionsLength); return; } if (suggestions.length) { for (var i = 0; i < suggestions.length; i++) { suggestions[i].provider = provider; } } else { // we still need to update the UI this._control._renderSuggestions(suggestions); } if (provider._lastRender !== text) { this._control._clearProviderSuggestions(provider); } if (suggestions.length && this._control._input.value === text) { provider._lastRender = text; this._control._renderSuggestions(suggestions); } // perform additional cleanup when all requests are finished this._control._finalizeSuggestions(activeRequests, suggestionsLength); }, this); }, this); this._pendingSuggestions = []; for (var i = 0; i < this._providers.length; i++) { var provider = this._providers[i]; var request = provider.suggestions(text, this._searchBounds(), createCallback(text, provider)); this._pendingSuggestions.push(request); } }, _searchBounds: function () { if (this.options.searchBounds !== null) { return this.options.searchBounds; } if (this.options.useMapBounds === false) { return null; } if (this.options.useMapBounds === true) { return this._control._map.getBounds(); } if (this.options.useMapBounds <= this._control._map.getZoom()) { return this._control._map.getBounds(); } return null; }, _boundsFromResults: function (results) { if (!results.length) { return; } var nullIsland = leaflet.latLngBounds([0, 0], [0, 0]); var resultBounds = []; var resultLatlngs = []; // collect the bounds and center of each result for (var i = results.length - 1; i >= 0; i--) { var result = results[i]; resultLatlngs.push(result.latlng); // make sure bounds are valid and not 0,0. sometimes bounds are incorrect or not present if (result.bounds && result.bounds.isValid() && !result.bounds.equals(nullIsland)) { resultBounds.push(result.bounds); } } // form a bounds object containing all center points var bounds = leaflet.latLngBounds(resultLatlngs); // and extend it to contain all bounds objects for (var j = 0; j < resultBounds.length; j++) { bounds.extend(resultBounds[j]); } return bounds; }, _getAttribution: function () { var attribs = []; var providers = this._providers; for (var i = 0; i < providers.length; i++) { if (providers[i].options.attribution) { attribs.push(providers[i].options.attribution); } } return attribs.join(', '); } }); function geosearchCore (control, options) { return new GeosearchCore(control, options); } var ArcgisOnlineProvider = GeocodeService.extend({ options: { label: 'Places and Addresses', maxResults: 5 }, suggestions: function (text, bounds, callback) { var request = this.suggest().text(text); if (bounds) { request.within(bounds); } if (this.options.nearby) { // "distance"/"radius" is not supported by the ArcGIS Online Geocoder, // so that is intentionally not passed here: request.nearby(this.options.nearby); } if (this.options.countries) { request.countries(this.options.countries); } if (this.options.categories) { request.category(this.options.categories); } // 15 is the maximum number of suggestions that can be returned request.maxSuggestions(this.options.maxResults); return request.run(function (error, results, response) { var suggestions = []; if (!error) { while (response.suggestions.length && suggestions.length <= (this.options.maxResults - 1)) { var suggestion = response.suggestions.shift(); if (!suggestion.isCollection) { suggestions.push({ text: suggestion.text, unformattedText: suggestion.text, magicKey: suggestion.magicKey }); } } } callback(error, suggestions); }, this); }, results: function (text, key, bounds, callback) { var request = this.geocode().text(text); if (key) { request.key(key); } // in the future Address/StreetName geocoding requests that include a magicKey will always only return one match request.maxLocations(this.options.maxResults); if (bounds) { request.within(bounds); } if (this.options.forStorage) { request.forStorage(true); } if (this.options.nearby) { // "distance"/"radius" is not supported by the ArcGIS Online Geocoder, // so that is intentionally not passed here: request.nearby(this.options.nearby); } if (this.options.countries) { request.countries(this.options.countries); } if (this.options.categories) { request.category(this.options.categories); } return request.run(function (error, response) { callback(error, response.results); }, this); } }); function arcgisOnlineProvider (options) { return new ArcgisOnlineProvider(options); } var Geosearch = leaflet.Control.extend({ includes: leaflet.Evented.prototype, options: { position: 'topleft', collapseAfterResult: true, expanded: false, allowMultipleResults: true, placeholder: 'Search for places or addresses', title: 'Location Search' }, initialize: function (options) { leaflet.Util.setOptions(this, options); if (!options || !options.providers || !options.providers.length) { if (!options) { options = {}; } options.providers = [arcgisOnlineProvider()]; } // instantiate the underlying class and pass along options this._geosearchCore = geosearchCore(this, options); this._geosearchCore._providers = options.providers; // bubble each providers events to the control this._geosearchCore.addEventParent(this); for (var i = 0; i < this._geosearchCore._providers.length; i++) { this._geosearchCore._providers[i].addEventParent(this); } this._geosearchCore._pendingSuggestions = []; leaflet.Control.prototype.initialize.call(this, options); }, _renderSuggestions: function (suggestions) { var currentGroup; if (suggestions.length > 0) { this._suggestions.style.display = 'block'; } var list; var header; var suggestionTextArray = []; for (var i = 0; i < suggestions.length; i++) { var suggestion = suggestions[i]; if (!header && this._geosearchCore._providers.length > 1 && currentGroup !== suggestion.provider.options.label) { header = leaflet.DomUtil.create('div', 'geocoder-control-header', suggestion.provider._contentsElement); header.textContent = suggestion.provider.options.label; header.innerText = suggestion.provider.options.label; currentGroup = suggestion.provider.options.label; } if (!list) { list = leaflet.DomUtil.create('ul', 'geocoder-control-list', suggestion.provider._contentsElement); } if (suggestionTextArray.indexOf(suggestion.text) === -1) { var suggestionItem = leaflet.DomUtil.create('li', 'geocoder-control-suggestion', list); suggestionItem.innerHTML = suggestion.text; suggestionItem.provider = suggestion.provider; suggestionItem['data-magic-key'] = suggestion.magicKey; suggestionItem.unformattedText = suggestion.unformattedText; } else { for (var j = 0; j < list.childNodes.length; j++) { // if the same text already appears in the list of suggestions, append an additional ObjectID to its magicKey instead if (list.childNodes[j].innerHTML === suggestion.text) { list.childNodes[j]['data-magic-key'] += ',' + suggestion.magicKey; } } } suggestionTextArray.push(suggestion.text); } // when the geocoder position is either "topleft" or "topright": // set the maxHeight of the suggestions box to: // map height // - suggestions offset (distance from top of suggestions to top of control) // - control offset (distance from top of control to top of map) // - 10 (extra padding) if (this.getPosition().indexOf('top') > -1) { this._suggestions.style.maxHeight = (this._map.getSize().y - this._suggestions.offsetTop - this._wrapper.offsetTop - 10) + 'px'; } // when the geocoder position is either "bottomleft" or "bottomright": // 1. set the maxHeight of the suggestions box to: // map height // - corner control container offsetHeight (height of container of bottom corner) // - control offsetHeight (height of geocoder control wrapper, the main expandable button) // 2. to move it up, set the top of the suggestions box to: // negative offsetHeight of suggestions box (its own negative height now that it has children elements // - control offsetHeight (height of geocoder control wrapper, the main expandable button) // + 20 (extra spacing) if (this.getPosition().indexOf('bottom') > -1) { this._setSuggestionsBottomPosition(); } }, _setSuggestionsBottomPosition: function () { this._suggestions.style.maxHeight = (this._map.getSize().y - this._map._controlCorners[this.getPosition()].offsetHeight - this._wrapper.offsetHeight) + 'px'; this._suggestions.style.top = (-this._suggestions.offsetHeight - this._wrapper.offsetHeight + 20) + 'px'; }, _boundsFromResults: function (results) { if (!results.length) { return; } var nullIsland = leaflet.latLngBounds([0, 0], [0, 0]); var resultBounds = []; var resultLatlngs = []; // collect the bounds and center of each result for (var i = results.length - 1; i >= 0; i--) { var result = results[i]; resultLatlngs.push(result.latlng); // make sure bounds are valid and not 0,0. sometimes bounds are incorrect or not present if (result.bounds && result.bounds.isValid() && !result.bounds.equals(nullIsland)) { resultBounds.push(result.bounds); } } // form a bounds object containing all center points var bounds = leaflet.latLngBounds(resultLatlngs); // and extend it to contain all bounds objects for (var j = 0; j < resultBounds.length; j++) { bounds.extend(resultBounds[j]); } return bounds; }, clear: function () { this._clearAllSuggestions(); if (this.options.collapseAfterResult) { this._input.value = ''; this._lastValue = ''; this._input.placeholder = ''; leaflet.DomUtil.removeClass(this._wrapper, 'geocoder-control-expanded'); } if (!this._map.scrollWheelZoom.enabled() && this._map.options.scrollWheelZoom) { this._map.scrollWheelZoom.enable(); } }, _clearAllSuggestions: function () { this._suggestions.style.display = 'none'; for (var i = 0; i < this.options.providers.length; i++) { this._clearProviderSuggestions(this.options.providers[i]); } }, _clearProviderSuggestions: function (provider) { provider._contentsElement.innerHTML = ''; }, _finalizeSuggestions: function (activeRequests, suggestionsLength) { // check if all requests are finished to remove the loading indicator if (!activeRequests) { leaflet.DomUtil.removeClass(this._input, 'geocoder-control-loading'); // when the geocoder position is either "bottomleft" or "bottomright", // it is necessary in some cases to recalculate the maxHeight and top values of the this._suggestions element, // even though this is also being done after each provider returns their own suggestions if (this.getPosition().indexOf('bottom') > -1) { this._setSuggestionsBottomPosition(); } // also check if there were 0 total suggest results to clear the parent suggestions element // otherwise its display value may be "block" instead of "none" if (!suggestionsLength) { this._clearAllSuggestions(); } } }, _setupClick: function () { leaflet.DomUtil.addClass(this._wrapper, 'geocoder-control-expanded'); this._input.focus(); }, disable: function () { this._input.disabled = true; leaflet.DomUtil.addClass(this._input, 'geocoder-control-input-disabled'); leaflet.DomEvent.removeListener(this._wrapper, 'click', this._setupClick, this); }, enable: function () { this._input.disabled = false; leaflet.DomUtil.removeClass(this._input, 'geocoder-control-input-disabled'); leaflet.DomEvent.addListener(this._wrapper, 'click', this._setupClick, this); }, getAttribution: function () { var attribs = []; for (var i = 0; i < this._providers.length; i++) { if (this._providers[i].options.attribution) { attribs.push(this._providers[i].options.attribution); } } return attribs.join(', '); }, geocodeSuggestion: function (e) { var suggestionItem = e.target || e.srcElement; if ( suggestionItem.classList.contains('geocoder-control-suggestions') || suggestionItem.classList.contains('geocoder-control-header') ) { return; } // make sure and point at the actual 'geocoder-control-suggestion' if (suggestionItem.classList.length < 1) { suggestionItem = suggestionItem.parentNode; } this._geosearchCore._geocode(suggestionItem.unformattedText, suggestionItem['data-magic-key'], suggestionItem.provider); this.clear(); }, onAdd: function (map) { // include 'Powered by Esri' in map attribution esriLeaflet.Util.setEsriAttribution(map); this._map = map; this._wrapper = leaflet.DomUtil.create('div', 'geocoder-control'); this._input = leaflet.DomUtil.create('input', 'geocoder-control-input leaflet-bar', this._wrapper); this._input.title = this.options.title; if (this.options.expanded) { leaflet.DomUtil.addClass(this._wrapper, 'geocoder-control-expanded'); this._input.placeholder = this.options.placeholder; } // create the main suggested results container element this._suggestions = leaflet.DomUtil.create('div', 'geocoder-control-suggestions leaflet-bar', this._wrapper); // create a child contents container element for each provider inside of this._suggestions // to maintain the configured order of providers for suggested results for (var i = 0; i < this.options.providers.length; i++) { this.options.providers[i]._contentsElement = leaflet.DomUtil.create('div', null, this._suggestions); } var credits = this._geosearchCore._getAttribution(); if (map.attributionControl) { map.attributionControl.addAttribution(credits); } leaflet.DomEvent.addListener(this._input, 'focus', function (e) { this._input.placeholder = this.options.placeholder; leaflet.DomUtil.addClass(this._wrapper, 'geocoder-control-expanded'); }, this); leaflet.DomEvent.addListener(this._wrapper, 'click', this._setupClick, this); // make sure both click and touch spawn an address/poi search leaflet.DomEvent.addListener(this._suggestions, 'mousedown', this.geocodeSuggestion, this); leaflet.DomEvent.addListener(this._input, 'blur', function (e) { // TODO: this is too greedy and should not "clear" // when trying to use the scrollbar or clicking on a non-suggestion item (such as a provider header) this.clear(); }, this); leaflet.DomEvent.addListener(this._input, 'keydown', function (e) { var text = (e.target || e.srcElement).value; leaflet.DomUtil.addClass(this._wrapper, 'geocoder-control-expanded'); var list = this._suggestions.querySelectorAll('.' + 'geocoder-control-suggestion'); var selected = this._suggestions.querySelectorAll('.' + 'geocoder-control-selected')[0]; var selectedPosition; for (var i = 0; i < list.length; i++) { if (list[i] === selected) { selectedPosition = i; break; } } switch (e.keyCode) { case 13: /* if an item has been selected, geocode it if focus is on the input textbox, geocode only if multiple results are allowed and more than two characters are present, or if a single suggestion is displayed. if less than two characters have been typed, abort the geocode */ if (selected) { this._input.value = selected.innerText; this._geosearchCore._geocode(selected.unformattedText, selected['data-magic-key'], selected.provider); this.clear(); } else if (this.options.allowMultipleResults && text.length >= 2) { this._geosearchCore._geocode(this._input.value, undefined); this.clear(); } else { if (list.length === 1) { leaflet.DomUtil.addClass(list[0], 'geocoder-control-selected'); this._geosearchCore._geocode(list[0].innerHTML, list[0]['data-magic-key'], list[0].provider); } else { this.clear(); this._input.blur(); } } leaflet.DomEvent.preventDefault(e); break; case 38: if (selected) { leaflet.DomUtil.removeClass(selected, 'geocoder-control-selected'); } var previousItem = list[selectedPosition - 1]; if (selected && previousItem) { leaflet.DomUtil.addClass(previousItem, 'geocoder-control-selected'); } else { leaflet.DomUtil.addClass(list[list.length - 1], 'geocoder-control-selected'); } leaflet.DomEvent.preventDefault(e); break; case 40: if (selected) { leaflet.DomUtil.removeClass(selected, 'geocoder-control-selected'); } var nextItem = list[selectedPosition + 1]; if (selected && nextItem) { leaflet.DomUtil.addClass(nextItem, 'geocoder-control-selected'); } else { leaflet.DomUtil.addClass(list[0], 'geocoder-control-selected'); } leaflet.DomEvent.preventDefault(e); break; default: // when the input changes we should cancel all pending suggestion requests if possible to avoid result collisions for (var x = 0; x < this._geosearchCore._pendingSuggestions.length; x++) { var request = this._geosearchCore._pendingSuggestions[x]; if (request && request.abort && !request.id) { request.abort(); } } break; } }, this); leaflet.DomEvent.addListener(this._input, 'keyup', leaflet.Util.throttle(function (e) { var key = e.which || e.keyCode; var text = (e.target || e.srcElement).value; // require at least 2 characters for suggestions if (text.length < 2) { this._lastValue = this._input.value; this._clearAllSuggestions(); leaflet.DomUtil.removeClass(this._input, 'geocoder-control-loading'); return; } // if this is the escape key it will clear the input so clear suggestions if (key === 27) { this._clearAllSuggestions(); return; } // if this is NOT the up/down arrows or enter make a suggestion if (key !== 13 && key !== 38 && key !== 40) { if (this._input.value !== this._lastValue) { this._lastValue = this._input.value; leaflet.DomUtil.addClass(this._input, 'geocoder-control-loading'); this._geosearchCore._suggest(text); } } }, 50, this), this); leaflet.DomEvent.disableClickPropagation(this._wrapper); // when mouse moves over suggestions disable scroll wheel zoom if its enabled leaflet.DomEvent.addListener(this._suggestions, 'mouseover', function (e) { if (map.scrollWheelZoom.enabled() && map.options.scrollWheelZoom) { map.scrollWheelZoom.disable(); } }); // when mouse moves leaves suggestions enable scroll wheel zoom if its disabled leaflet.DomEvent.addListener(this._suggestions, 'mouseout', function (e) { if (!map.scrollWheelZoom.enabled() && map.options.scrollWheelZoom) { map.scrollWheelZoom.enable(); } }); this._geosearchCore.on('load', function (e) { leaflet.DomUtil.removeClass(this._input, 'geocoder-control-loading'); this.clear(); this._input.blur(); }, this); return this._wrapper; } }); function geosearch (options) { return new Geosearch(options); } var FeatureLayerProvider = esriLeaflet.FeatureLayerService.extend({ options: { label: 'Feature Layer', maxResults: 5, bufferRadius: 1000, searchMode: 'contain', formatSuggestion: function (feature) { return feature.properties[this.options.searchFields[0]]; } }, initialize: function (options) { if (options.apikey) { options.token = options.apikey; } esriLeaflet.FeatureLayerService.prototype.initialize.call(this, options); if (typeof this.options.searchFields === 'string') { this.options.searchFields = [this.options.searchFields]; } this._suggestionsQuery = this.query(); this._resultsQuery = this.query(); }, suggestions: function (text, bounds, callback) { var query = this._suggestionsQuery.where(this._buildQuery(text)) .returnGeometry(false); if (bounds) { query.intersects(bounds); } if (this.options.idField) { query.fields([this.options.idField].concat(this.options.searchFields)); } var request = query.run(function (error, results, raw) { if (error) { callback(error, []); } else { this.options.idField = raw.objectIdFieldName; var suggestions = []; for (var i = results.features.length - 1; i >= 0; i--) { var feature = results.features[i]; suggestions.push({ text: this.options.formatSuggestion.call(this, feature), unformattedText: feature.properties[this.options.searchFields[0]], magicKey: feature.id }); } callback(error, suggestions.slice(0, this.options.maxResults)); } }, this); return request; }, results: function (text, key, bounds, callback) { var query = this._resultsQuery; if (key) { // if there are 1 or more keys available, use query.featureIds() delete query.params.where; query.featureIds([key]); } else { // if there are no keys available, use query.where() query.where(this._buildQuery(text)); } if (bounds) { query.within(bounds); } return query.run(leaflet.Util.bind(function (error, features) { var results = []; for (var i = 0; i < features.features.length; i++) { var feature = features.features[i]; if (feature) { var bounds = this._featureBounds(feature); var result = { latlng: bounds.getCenter(), bounds: bounds, text: this.options.formatSuggestion.call(this, feature), properties: feature.properties, geojson: feature }; results.push(result); // clear query parameters for the next search delete this._resultsQuery.params['objectIds']; } } callback(error, results); }, this)); }, orderBy: function (fieldName, order) { this._suggestionsQuery.orderBy(fieldName, order); }, _buildQuery: function (text) { var queryString = []; for (var i = this.options.searchFields.length - 1; i >= 0; i--) { var field = 'upper("' + this.options.searchFields[i] + '")'; if (this.options.searchMode === 'contain') { queryString.push(field + " LIKE upper('%" + text + "%')"); } else if (this.options.searchMode === 'startWith') { queryString.push(field + " LIKE upper('" + text + "%')"); } else if (this.options.searchMode === 'endWith') { queryString.push(field + " LIKE upper('%" + text + "')"); } else if (this.options.searchMode === 'strict') { queryString.push(field + " LIKE upper('" + text + "')"); } else { throw new Error('L.esri.Geocoding.featureLayerProvider: Invalid parameter for "searchMode". Use one of "contain", "startWith", "endWith", or "strict"'); } } if (this.options.where) { return this.options.where + ' AND (' + queryString.join(' OR ') + ')'; } else { return queryString.join(' OR '); } }, _featureBounds: function (feature) { var geojson = leaflet.geoJson(feature); if (feature.geometry.type === 'Point') { var center = geojson.getBounds().getCenter(); var lngRadius = ((this.options.bufferRadius / 40075017) * 360) / Math.cos((180 / Math.PI) * center.lat); var latRadius = (this.options.bufferRadius / 40075017) * 360; return leaflet.latLngBounds([center.lat - latRadius, center.lng - lngRadius], [center.lat + latRadius, center.lng + lngRadius]); } else { return geojson.getBounds(); } } }); function featureLayerProvider (options) { return new FeatureLayerProvider(options); } var MapServiceProvider = esriLeaflet.MapService.extend({ options: { layers: [0], label: 'Map Service', bufferRadius: 1000, maxResults: 5, formatSuggestion: function (feature) { return feature.properties[feature.displayFieldName] + ' <small>' + feature.layerName + '</small>'; } }, initialize: function (options) { if (options.apikey) { options.token = options.apikey; } esriLeaflet.MapService.prototype.initialize.call(this, options); this._getIdFields(); }, suggestions: function (text, bounds, callback) { var request = this.find().text(text).fields(this.options.searchFields).returnGeometry(false).layers(this.options.layers); return request.run(function (error, results, raw) { var suggestions = []; if (!error) { var count = Math.min(this.options.maxResults, results.features.length); raw.results = raw.results.reverse(); for (var i = 0; i < count; i++) { var feature = results.features[i]; var result = raw.results[i]; var layer = result.layerId; var idField = this._idFields[layer]; feature.layerId = layer; feature.layerName = this._layerNames[layer]; feature.displayFieldName = this._displayFields[layer]; if (idField) { suggestions.push({ text: this.options.formatSuggestion.call(this, feature), unformattedText: feature.properties[feature.displayFieldName], magicKey: result.attributes[idField] + ':' + layer }); } } } callback(error, suggestions.reverse()); }, this); }, results: function (text, key, bounds, callback) { var results = []; var request; if (key && !key.includes(',')) { // if there is only 1 key available, use query() var featureId = key.split(':')[0]; var layer = key.split(':')[1]; request = this.query().layer(layer).featureIds(featureId); } else { // if there are no keys or more than 1 keys available, use find() request = this.find().text(text).fields(this.options.searchFields).layers(this.options.layers); } return request.run(function (error, features, response) { if (!error) { if (response.results) { response.results = response.results.reverse(); } for (var i = 0; i < features.features.length; i++) { var feature = features.features[i]; layer = layer || response.results[i].layerId; if (feature && layer !== undefined) { var bounds = this._featureBounds(feature); feature.layerId = layer; feature.layerName = this._layerNames[layer]; feature.displayFieldName = this._displayFields[layer]; var result = { latlng: bounds.getCenter(), bounds: bounds, text: this.options.formatSuggestion.call(this, feature), properties: feature.properties, geojson: feature }; results.push(result); } } } callback(error, results.reverse()); }, this); }, _featureBounds: function (feature) { var geojson = leaflet.geoJson(feature); if (feature.geometry.type === 'Point') { var center = geojson.getBounds().getCenter(); var lngRadius = ((this.options.bufferRadius / 40075017) * 360) / Math.cos((180 / Math.PI) * center.lat); var latRadius = (this.options.bufferRadius / 40075017) * 360; return leaflet.latLngBounds([center.lat - latRadius, center.lng - lngRadius], [center.lat + latRadius, center.lng + lngRadius]); } else { return geojson.getBounds(); } }, _layerMetadataCallback: function (layerid) { return leaflet.Util.bind(function (error, metadata) { if (error) { return; } this._displayFields[layerid] = metadata.displayField; this._layerNames[layerid] = metadata.name; for (var i = 0; i < metadata.fields.length; i++) { var field = metadata.fields[i]; if (field.type === 'esriFieldTypeOID') { this._idFields[layerid] = field.name; break; } } }, this); }, _getIdFields: function () { this._idFields = {}; this._displayFields = {}; this._layerNames = {}; for (var i = 0; i < this.options.layers.length; i++) { var layer = this.options.layers[i]; this.get(layer, {}, this._layerMetadataCallback(layer)); } } }); function mapServiceProvider (options) { return new MapServiceProvider(options); } var GeocodeServiceProvider = GeocodeService.extend({ options: { label: 'Geocode Server', maxResults: 5 }, suggestions: function (text, bounds, callback) { if (this.options.supportsSuggest) { var request = this.suggest().text(text); if (bounds) { request.within(bounds); } return request.run(function (error, results, response) { var suggestions = []; if (!error) { while (response.suggestions.length && suggestions.length <= (this.options.maxResults - 1)) { var suggestion = response.suggestions.shift(); if (!suggestion.isCollection) { suggestions.push({ text: suggestion.text, unformattedText: suggestion.text, magicKey: suggestion.magicKey }); } } } callback(error, suggestions); }, this); } else { callback(null, []); return false; } }, results: function (text, key, bounds, callback) { var request = this.geocode().text(text); if (key) { request.key(key); } request.maxLocations(this.options.maxResults); if (bounds) { request.within(bounds); } return request.run(function (error, response) { callback(error, response.results); }, this); } }); function geocodeServiceProvider (options) { return new GeocodeServiceProvider(options); } exports.ArcgisOnlineProvider = ArcgisOnlineProvider; exports.FeatureLayerProvider = FeatureLayerProvider; exports.Geocode = Geocode; exports.GeocodeService = GeocodeService; exports.GeocodeServiceProvider = GeocodeServiceProvider; exports.Geosearch = Geosearch; exports.GeosearchCore = GeosearchCore; exports.MapServiceProvider = MapServiceProvider; exports.ReverseGeocode = ReverseGeocode; exports.Suggest = Suggest; exports.VERSION = version; exports.WorldGeocodingServiceUrl = WorldGeocodingServiceUrl; exports.arcgisOnlineProvider = arcgisOnlineProvider; exports.featureLayerProvider = featureLayerProvider; exports.geocode = geocode; exports.geocodeService = geocodeService; exports.geocodeServiceProvider = geocodeServiceProvider; exports.geosearch = geosearch; exports.geosearchCore = geosearchCore; exports.mapServiceProvider = mapServiceProvider; exports.reverseGeocode = reverseGeocode; exports.suggest = suggest; Object.defineProperty(exports, '__esModule', { value: true }); })); //# sourceMappingURL=esri-leaflet-geocoder-debug.js.map