esri-leaflet-geocoder
Version:
Esri Geocoding utility and search plugin for Leaflet.
433 lines (356 loc) • 15.8 kB
JavaScript
import {
Control,
DomEvent,
DomUtil,
Evented,
Util,
latLngBounds
} from 'leaflet';
import { geosearchCore } from '../Classes/GeosearchCore';
import { arcgisOnlineProvider } from '../Providers/ArcgisOnlineGeocoder';
import { Util as EsriUtil } from 'esri-leaflet';
export var Geosearch = Control.extend({
includes: Evented.prototype,
options: {
position: 'topleft',
collapseAfterResult: true,
expanded: false,
allowMultipleResults: true,
placeholder: 'Search for places or addresses',
title: 'Location Search'
},
initialize: function (options) {
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 = [];
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 = 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 = DomUtil.create('ul', 'geocoder-control-list', suggestion.provider._contentsElement);
}
if (suggestionTextArray.indexOf(suggestion.text) === -1) {
var suggestionItem = 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 = 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 = 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 = '';
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) {
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 () {
DomUtil.addClass(this._wrapper, 'geocoder-control-expanded');
this._input.focus();
},
disable: function () {
this._input.disabled = true;
DomUtil.addClass(this._input, 'geocoder-control-input-disabled');
DomEvent.removeListener(this._wrapper, 'click', this._setupClick, this);
},
enable: function () {
this._input.disabled = false;
DomUtil.removeClass(this._input, 'geocoder-control-input-disabled');
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
EsriUtil.setEsriAttribution(map);
this._map = map;
this._wrapper = DomUtil.create('div', 'geocoder-control');
this._input = DomUtil.create('input', 'geocoder-control-input leaflet-bar', this._wrapper);
this._input.title = this.options.title;
if (this.options.expanded) {
DomUtil.addClass(this._wrapper, 'geocoder-control-expanded');
this._input.placeholder = this.options.placeholder;
}
// create the main suggested results container element
this._suggestions = 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 = DomUtil.create('div', null, this._suggestions);
}
var credits = this._geosearchCore._getAttribution();
if (map.attributionControl) {
map.attributionControl.addAttribution(credits);
}
DomEvent.addListener(this._input, 'focus', function (e) {
this._input.placeholder = this.options.placeholder;
DomUtil.addClass(this._wrapper, 'geocoder-control-expanded');
}, this);
DomEvent.addListener(this._wrapper, 'click', this._setupClick, this);
// make sure both click and touch spawn an address/poi search
DomEvent.addListener(this._suggestions, 'mousedown', this.geocodeSuggestion, this);
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);
DomEvent.addListener(this._input, 'keydown', function (e) {
var text = (e.target || e.srcElement).value;
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) {
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();
}
}
DomEvent.preventDefault(e);
break;
case 38:
if (selected) {
DomUtil.removeClass(selected, 'geocoder-control-selected');
}
var previousItem = list[selectedPosition - 1];
if (selected && previousItem) {
DomUtil.addClass(previousItem, 'geocoder-control-selected');
} else {
DomUtil.addClass(list[list.length - 1], 'geocoder-control-selected');
}
DomEvent.preventDefault(e);
break;
case 40:
if (selected) {
DomUtil.removeClass(selected, 'geocoder-control-selected');
}
var nextItem = list[selectedPosition + 1];
if (selected && nextItem) {
DomUtil.addClass(nextItem, 'geocoder-control-selected');
} else {
DomUtil.addClass(list[0], 'geocoder-control-selected');
}
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);
DomEvent.addListener(this._input, 'keyup', 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();
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;
DomUtil.addClass(this._input, 'geocoder-control-loading');
this._geosearchCore._suggest(text);
}
}
}, 50, this), this);
DomEvent.disableClickPropagation(this._wrapper);
// when mouse moves over suggestions disable scroll wheel zoom if its enabled
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
DomEvent.addListener(this._suggestions, 'mouseout', function (e) {
if (!map.scrollWheelZoom.enabled() && map.options.scrollWheelZoom) {
map.scrollWheelZoom.enable();
}
});
this._geosearchCore.on('load', function (e) {
DomUtil.removeClass(this._input, 'geocoder-control-loading');
this.clear();
this._input.blur();
}, this);
return this._wrapper;
}
});
export function geosearch (options) {
return new Geosearch(options);
}
export default geosearch;