/* * L.Control.GeoSearch - search for an address and zoom to it's location * https://github.com/smeijer/leaflet.control.geosearch */ L.GeoSearch = {}; L.GeoSearch.Provider = {}; // MSIE needs cors support jQuery.support.cors = true; L.GeoSearch.Result = function (x, y, label) { this.X = x; this.Y = y; this.Label = label; }; L.Control.GeoSearch = L.Control.extend({ options: { position: 'topcenter' }, initialize: function (options) { this._config = {}; L.Util.extend(this.options, options); this.setConfig(options); }, setConfig: function (options) { this._config = { 'country': options.country || '', 'provider': options.provider, 'searchLabel': options.searchLabel || 'search for address...', 'notFoundMessage' : options.notFoundMessage || 'Sorry, that address could not be found.', 'messageHideDelay': options.messageHideDelay || 3000, 'zoomLevel': options.zoomLevel || 18, 'maxMarkers': options.maxMarkers || 1, 'enableButtons': options.enableButtons || false, 'enableAutocomplete': options.enableAutocomplete || false, 'autocompleteMinQueryLen': options.autocompleteMinQueryLen || 3, // query length request threshold 'autocompleteQueryDelay_ms': options.autocompleteQueryDelay_ms || 800 }; }, onAdd: function (map) { var $controlContainer = $(map._controlContainer); if ($controlContainer.children('.leaflet-top.leaflet-center').length == 0) { $controlContainer.append('
'); map._controlCorners.topcenter = $controlContainer.children('.leaflet-top.leaflet-center').first()[0]; } this._map = map; this._container = L.DomUtil.create('div', 'leaflet-control-geosearch'); var searchbox = document.createElement('input'); searchbox.id = 'leaflet-control-geosearch-qry'; searchbox.type = 'text'; searchbox.placeholder = this._config.searchLabel; this._searchbox = searchbox; if (this._autocomplete) { this._autocomplete.recordLastUserInput(''); } if (this._config.enableButtons) { var submitContainer = L.DomUtil.create('div', 'leaflet-control-geosearch-button-submit-container', this._container); L.DomUtil.create('span', 'leaflet-geosearch-submit-button', submitContainer); var cancelButton = L.DomUtil.create('span', 'leaflet-geosearch-cancel-button', this._container); L.DomEvent.on(submitContainer, 'click', this._submitRequest, this); L.DomEvent.on(cancelButton, 'click', this._clearUserSearchInput, this); } var msgbox = document.createElement('div'); msgbox.id = 'leaflet-control-geosearch-msg'; msgbox.className = 'leaflet-control-geosearch-msg'; this._msgbox = msgbox; var resultslist = document.createElement('ul'); resultslist.id = 'leaflet-control-geosearch-results'; this._resultslist = resultslist; $(this._msgbox).append(this._resultslist); $(this._container).append(this._searchbox, this._msgbox); if (this._config.enableAutocomplete) { this._autocomplete = new L.AutoComplete(this.options).addTo(this._container, function (suggestionText) { this._searchbox.value = suggestionText; }.bind(this)); $(this._container).append(this._autocomplete); } // TODO This will result in duplicate processing of events. Options? L.DomEvent .addListener(this._container, 'click', L.DomEvent.stop) .addListener(this._container, 'keyup', this._onKeyUp, this) .addListener(this._container, 'change', this._onInputUpdate, this) .addListener(this._container, 'paste', this._onPasteToInput, this); L.DomEvent.disableClickPropagation(this._container); return this._container; }, geosearch: function (qry) { this.geosearch_ext(qry, this._processResults.bind(this), this._printError.bind(this)); }, geosearch_ext: function(qry, onSuccess, onFailure) { try { var provider = this._config.provider; if(typeof provider.GetLocations == 'function') { var results = provider.GetLocations(qry, function(results) { onSuccess(results); }.bind(this)); } else { var url = provider.GetServiceUrl(qry); $.getJSON(url, function (data) { try { var results = provider.ParseJSON(data); onSuccess(results); } catch (error) { onFailure(error); } }.bind(this)); } } catch (error) { onFailure(error); } }, // qry may be a String or a function geosearch_autocomplete: function (qry, requestDelay_ms) { if (!this._config.enableAutocomplete) { return; } clearTimeout(this._autocompleteRequestTimer); this._autocompleteRequestTimer = setTimeout(function () { var q = qry; if (typeof qry === 'function') { q = qry(); } if (q.length >= this._config.autocompleteMinQueryLen) { this.geosearch_ext(q, this._autocomplete.show.bind(this._autocomplete), this._autocomplete.hide.bind(this._autocomplete)); } else { this._autocomplete.hide(); } }.bind(this), requestDelay_ms); }, _processResults: function(results) { if (results.length == 0) throw this._config.notFoundMessage; this._map.fireEvent('geosearch_foundlocations', {Locations: results}); this._showLocations(results); }, _showLocations: function (results) { if (typeof this._layer !== 'undefined') { this._map.removeLayer(this._layer); this._layer = null; } this._markerList = [] for (var ii=0; ii < results.length && ii < this._config.maxMarkers; ii++) { var location = results[ii]; var marker = L.marker([location.Y, location.X]).bindPopup(location.Label); this._markerList.push(marker); } this._layer = L.layerGroup(this._markerList).addTo(this._map); this._printError('Displaying ' + Math.min(this._autocomplete._config.maxResultCount, results.length) + ' of ' + results.length +' results.'); var premierResult = results[0]; this._map.setView([premierResult.Y, premierResult.X], this._config.zoomLevel, false); this._map.fireEvent('geosearch_showlocation', {Location: premierResult}); }, _printError: function(message) { $(this._resultslist) .html('
  • '+message+'
  • ') .fadeIn('slow').delay(this._config.messageHideDelay).fadeOut('slow', function () { $(this).html(''); }); }, _submitRequest: function () { var q = $('#leaflet-control-geosearch-qry').val(); if (q.length > 0) { this._hideAutocomplete(); this.geosearch(q); } }, _hideAutocomplete: function () { clearTimeout(this._autocompleteRequestTimer); if (this._config.enableAutocomplete && this._autocomplete.isVisible()) { this._autocomplete.hide(); return true; } return false; }, _clearUserSearchInput: function () { this._hideAutocomplete(); $('#leaflet-control-geosearch-qry').val(''); $('.leaflet-geosearch-cancel-button').hide(); }, _onPasteToInput: function () { // onpaste requires callback to allow for input update do this by default. setTimeout(this._onInputUpdate.bind(this), 0); }, _onInputUpdate: function () { // define function for requery of user input after delay function getQuery() { return $('#leaflet-control-geosearch-qry').val(); } var qry = getQuery(); if (this._config.enableAutocomplete) { this._autocomplete.recordLastUserInput(qry); if (qry.length >= this._config.autocompleteMinQueryLen) { this.geosearch_autocomplete(getQuery, this._config.autocompleteQueryDelay_ms); } else { this._autocomplete.hide(); } } if (qry.length > 0) { $('.leaflet-geosearch-cancel-button').show(); } else { $('.leaflet-geosearch-cancel-button').hide(); } }, _onKeyUp: function (e) { var REQ_DELAY_MS = 800; var MIN_AUTOCOMPLETE_LEN = 3; var enterKey = 13; var shift = 16; var ctrl = 17; var escapeKey = 27; var leftArrow = 37; var upArrow = 38; var rightArrow = 39; var downArrow = 40; switch (e.keyCode) { case escapeKey: // ESC first closes autocomplete if open. If closed then clears input. if (!this._hideAutocomplete()) { this._clearUserSearchInput(); } break; case enterKey: this._submitRequest(); break; case upArrow: if (this._config.enableAutocomplete && this._autocomplete.isVisible()) { this._autocomplete.moveUp(); } break; case downArrow: if (this._config.enableAutocomplete && this._autocomplete.isVisible()) { this._autocomplete.moveDown(); } break; case leftArrow: case rightArrow: case shift: case ctrl: break; default: this._onInputUpdate(); } } }); L.AutoComplete = L.Class.extend({ initialize: function (options) { this._config = {}; this.setConfig(options); }, setConfig: function (options) { this._config = { 'maxResultCount': options.maxResultCount || 10, 'onMakeSuggestionHTML': options.onMakeSuggestionHTML || function (geosearchResult) { return this._htmlEscape(geosearchResult.Label); }.bind(this), }; }, addTo: function (container, onSelectionCallback) { this._container = container; this._onSelection = onSelectionCallback; return this._createUI(container, 'leaflet-geosearch-autocomplete'); }, recordLastUserInput: function (str) { this._lastUserInput = str; }, _createUI: function (container, className) { this._tool = L.DomUtil.create('div', className, container); this._tool.style.display = 'none'; L.DomEvent .disableClickPropagation(this._tool) // consider whether to make delayed hide onBlur. // If so, consider canceling timer on mousewheel and mouseover. .on(this._tool, 'blur', this.hide, this) .on(this._tool, 'mousewheel', function(e) { L.DomEvent.stopPropagation(e); // to prevent map zoom if (e.axis === e.VERTICAL_AXIS) { if (e.detail > 0) { this.moveDown(); } else { this.moveUp(); } } }, this); return this; }, show: function (results) { this._tool.innerHTML = ''; this._tool.currentSelection = -1; var count = 0; while (count < results.length && count < this._config.maxResultCount) { var entry = this._newSuggestion(results[count]); this._tool.appendChild(entry); ++count; } if (count > 0) { this._tool.style.display = 'block'; } else { this.hide(); } return count; }, hide: function () { this._tool.style.display = 'none'; this._tool.innerHTML = ''; }, isVisible: function() { return this._tool.style.display !== 'none'; }, _htmlEscape: function (str) { // implementation courtesy of http://stackoverflow.com/a/7124052 return String(str) .replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, ''') .replace(//g, '>'); }, _newSuggestion: function (result) { var tip = L.DomUtil.create('li', 'leaflet-geosearch-suggestion'); tip.innerHTML = this._config.onMakeSuggestionHTML(result); tip._text = result.Label; L.DomEvent .disableClickPropagation(tip) .on(tip, 'click', function(e) { this._onSelection(tip._text); }.bind(this), this); return tip; }, _onSelectedUpdate: function () { var entries = this._tool.hasChildNodes() ? this._tool.childNodes : []; for (var ii=0; ii < entries.length; ++ii) { L.DomUtil.removeClass(entries[ii], 'leaflet-geosearch-suggestion-selected'); } // if selection is -1, then show last user typed text if (this._tool.currentSelection >= 0) { L.DomUtil.addClass(entries[this._tool.currentSelection], 'leaflet-geosearch-suggestion-selected'); // scroll: var tipOffsetTop = entries[this._tool.currentSelection].offsetTop; if (tipOffsetTop + entries[this._tool.currentSelection].clientHeight >= this._tool.scrollTop + this._tool.clientHeight) { this._tool.scrollTop = tipOffsetTop - this._tool.clientHeight + entries[this._tool.currentSelection].clientHeight; } else if (tipOffsetTop <= this._tool.scrollTop) { this._tool.scrollTop = tipOffsetTop; } this._onSelection(entries[this._tool.currentSelection]._text); } else { this._onSelection(this._lastUserInput); } }, moveUp: function () { // permit selection to decrement down to -1 (none selected) if (this.isVisible() && this._tool.currentSelection >= 0) { --this._tool.currentSelection; this._onSelectedUpdate(); } return this; }, moveDown: function () { if (this.isVisible()) { this._tool.currentSelection = (this._tool.currentSelection + 1) % this.suggestionCount(); this._onSelectedUpdate(); } return this; }, suggestionCount: function () { return this._tool.hasChildNodes() ? this._tool.childNodes.length : 0; }, });