UNPKG

nextzen.js

Version:

Javascript SDK for Nextzen products

1,543 lines (1,338 loc) 2.88 MB
(function(){function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s}return e})()({1:[function(require,module,exports){ function corslite(url, callback, cors) { var sent = false; if (typeof window.XMLHttpRequest === 'undefined') { return callback(Error('Browser not supported')); } if (typeof cors === 'undefined') { var m = url.match(/^\s*https?:\/\/[^\/]*/); cors = m && (m[0] !== location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : '')); } var x = new window.XMLHttpRequest(); function isSuccessful(status) { return status >= 200 && status < 300 || status === 304; } if (cors && !('withCredentials' in x)) { // IE8-9 x = new window.XDomainRequest(); // Ensure callback is never called synchronously, i.e., before // x.send() returns (this has been observed in the wild). // See https://github.com/mapbox/mapbox.js/issues/472 var original = callback; callback = function() { if (sent) { original.apply(this, arguments); } else { var that = this, args = arguments; setTimeout(function() { original.apply(that, args); }, 0); } } } function loaded() { if ( // XDomainRequest x.status === undefined || // modern browsers isSuccessful(x.status)) callback.call(x, null, x); else callback.call(x, x, null); } // Both `onreadystatechange` and `onload` can fire. `onreadystatechange` // has [been supported for longer](http://stackoverflow.com/a/9181508/229001). if ('onload' in x) { x.onload = loaded; } else { x.onreadystatechange = function readystate() { if (x.readyState === 4) { loaded(); } }; } // Call the callback with the XMLHttpRequest object as an error and prevent // it from ever being called again by reassigning it to `noop` x.onerror = function error(evt) { // XDomainRequest provides no evt parameter callback.call(this, evt || true, null); callback = function() { }; }; // IE9 must have onprogress be set to a unique function. x.onprogress = function() { }; x.ontimeout = function(evt) { callback.call(this, evt, null); callback = function() { }; }; x.onabort = function(evt) { callback.call(this, evt, null); callback = function() { }; }; // GET is the only supported HTTP Verb by XDomainRequest and is the // only one supported here. x.open('GET', url, true); // Send the request. Sending data is not supported. x.send(null); sent = true; return x; } if (typeof module !== 'undefined') module.exports = corslite; },{}],2:[function(require,module,exports){ 'use strict'; /** * Based off of [the offical Google document](https://developers.google.com/maps/documentation/utilities/polylinealgorithm) * * Some parts from [this implementation](http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/PolylineEncoder.js) * by [Mark McClure](http://facstaff.unca.edu/mcmcclur/) * * @module polyline */ var polyline = {}; function py2_round(value) { // Google's polyline algorithm uses the same rounding strategy as Python 2, which is different from JS for negative values return Math.floor(Math.abs(value) + 0.5) * Math.sign(value); } function encode(current, previous, factor) { current = py2_round(current * factor); previous = py2_round(previous * factor); var coordinate = current - previous; coordinate <<= 1; if (current - previous < 0) { coordinate = ~coordinate; } var output = ''; while (coordinate >= 0x20) { output += String.fromCharCode((0x20 | (coordinate & 0x1f)) + 63); coordinate >>= 5; } output += String.fromCharCode(coordinate + 63); return output; } /** * Decodes to a [latitude, longitude] coordinates array. * * This is adapted from the implementation in Project-OSRM. * * @param {String} str * @param {Number} precision * @returns {Array} * * @see https://github.com/Project-OSRM/osrm-frontend/blob/master/WebContent/routing/OSRM.RoutingGeometry.js */ polyline.decode = function(str, precision) { var index = 0, lat = 0, lng = 0, coordinates = [], shift = 0, result = 0, byte = null, latitude_change, longitude_change, factor = Math.pow(10, precision || 5); // Coordinates have variable length when encoded, so just keep // track of whether we've hit the end of the string. In each // loop iteration, a single coordinate is decoded. while (index < str.length) { // Reset shift, result, and byte byte = null; shift = 0; result = 0; do { byte = str.charCodeAt(index++) - 63; result |= (byte & 0x1f) << shift; shift += 5; } while (byte >= 0x20); latitude_change = ((result & 1) ? ~(result >> 1) : (result >> 1)); shift = result = 0; do { byte = str.charCodeAt(index++) - 63; result |= (byte & 0x1f) << shift; shift += 5; } while (byte >= 0x20); longitude_change = ((result & 1) ? ~(result >> 1) : (result >> 1)); lat += latitude_change; lng += longitude_change; coordinates.push([lat / factor, lng / factor]); } return coordinates; }; /** * Encodes the given [latitude, longitude] coordinates array. * * @param {Array.<Array.<Number>>} coordinates * @param {Number} precision * @returns {String} */ polyline.encode = function(coordinates, precision) { if (!coordinates.length) { return ''; } var factor = Math.pow(10, precision || 5), output = encode(coordinates[0][0], 0, factor) + encode(coordinates[0][1], 0, factor); for (var i = 1; i < coordinates.length; i++) { var a = coordinates[i], b = coordinates[i - 1]; output += encode(a[0], b[0], factor); output += encode(a[1], b[1], factor); } return output; }; function flipped(coords) { var flipped = []; for (var i = 0; i < coords.length; i++) { flipped.push(coords[i].slice().reverse()); } return flipped; } /** * Encodes a GeoJSON LineString feature/geometry. * * @param {Object} geojson * @param {Number} precision * @returns {String} */ polyline.fromGeoJSON = function(geojson, precision) { if (geojson && geojson.type === 'Feature') { geojson = geojson.geometry; } if (!geojson || geojson.type !== 'LineString') { throw new Error('Input must be a GeoJSON LineString'); } return polyline.encode(flipped(geojson.coordinates), precision); }; /** * Decodes to a GeoJSON LineString geometry. * * @param {String} str * @param {Number} precision * @returns {Object} */ polyline.toGeoJSON = function(str, precision) { var coords = polyline.decode(str, precision); return { type: 'LineString', coordinates: flipped(coords) }; }; if (typeof module === 'object' && module.exports) { module.exports = polyline; } },{}],3:[function(require,module,exports){ // Console-polyfill. MIT license. // https://github.com/paulmillr/console-polyfill // Make it safe to do console.log() always. (function(global) { 'use strict'; if (!global.console) { global.console = {}; } var con = global.console; var prop, method; var dummy = function() {}; var properties = ['memory']; var methods = ('assert,clear,count,debug,dir,dirxml,error,exception,group,' + 'groupCollapsed,groupEnd,info,log,markTimeline,profile,profiles,profileEnd,' + 'show,table,time,timeEnd,timeline,timelineEnd,timeStamp,trace,warn').split(','); while (prop = properties.pop()) if (!con[prop]) con[prop] = {}; while (method = methods.pop()) if (typeof con[method] !== 'function') con[method] = dummy; // Using `this` for web workers & supports Browserify / Webpack. })(typeof window === 'undefined' ? this : window); },{}],4:[function(require,module,exports){ var L = require('leaflet'), Util = require('../util'); module.exports = { class: L.Class.extend({ options: { serviceUrl: 'https://search.mapzen.com/v1', geocodingQueryParams: {}, reverseQueryParams: {} }, initialize: function(apiKey, options) { L.Util.setOptions(this, options); this._apiKey = apiKey; this._lastSuggest = 0; }, geocode: function(query, cb, context) { var _this = this; Util.getJSON(this.options.serviceUrl + "/search", L.extend({ 'api_key': this._apiKey, 'text': query }, this.options.geocodingQueryParams), function(data) { cb.call(context, _this._parseResults(data, "bbox")); }); }, suggest: function(query, cb, context) { var _this = this; Util.getJSON(this.options.serviceUrl + "/autocomplete", L.extend({ 'api_key': this._apiKey, 'text': query }, this.options.geocodingQueryParams), L.bind(function(data) { if (data.geocoding.timestamp > this._lastSuggest) { this._lastSuggest = data.geocoding.timestamp; cb.call(context, _this._parseResults(data, "bbox")); } }, this)); }, reverse: function(location, scale, cb, context) { var _this = this; Util.getJSON(this.options.serviceUrl + "/reverse", L.extend({ 'api_key': this._apiKey, 'point.lat': location.lat, 'point.lon': location.lng }, this.options.reverseQueryParams), function(data) { cb.call(context, _this._parseResults(data, "bounds")); }); }, _parseResults: function(data, bboxname) { var results = []; L.geoJson(data, { pointToLayer: function (feature, latlng) { return L.circleMarker(latlng); }, onEachFeature: function(feature, layer) { var result = {}, bbox, center; if (layer.getBounds) { bbox = layer.getBounds(); center = bbox.getCenter(); } else { center = layer.getLatLng(); bbox = L.latLngBounds(center, center); } result.name = layer.feature.properties.label; result.center = center; result[bboxname] = bbox; result.properties = layer.feature.properties; results.push(result); } }); return results; } }), factory: function(apiKey, options) { return new L.Control.Geocoder.Mapzen(apiKey, options); } }; },{"../util":5,"leaflet":22}],5:[function(require,module,exports){ var L = require('leaflet'), lastCallbackId = 0, htmlEscape = (function() { // Adapted from handlebars.js // https://github.com/wycats/handlebars.js/ var badChars = /[&<>"'`]/g; var possible = /[&<>"'`]/; var escape = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', '\'': '&#x27;', '`': '&#x60;' }; function escapeChar(chr) { return escape[chr]; } return function(string) { if (string == null) { return ''; } else if (!string) { return string + ''; } // Force a string conversion as this will be done by the append regardless and // the regex test will do this transparently behind the scenes, causing issues if // an object's to string has escaped characters in it. string = '' + string; if (!possible.test(string)) { return string; } return string.replace(badChars, escapeChar); }; })(); module.exports = { jsonp: function(url, params, callback, context, jsonpParam) { var callbackId = '_l_geocoder_' + (lastCallbackId++); params[jsonpParam || 'callback'] = callbackId; window[callbackId] = L.Util.bind(callback, context); var script = document.createElement('script'); script.type = 'text/javascript'; script.src = url + L.Util.getParamString(params); script.id = callbackId; document.getElementsByTagName('head')[0].appendChild(script); }, getJSON: function(url, params, callback) { var xmlHttp = new XMLHttpRequest(); xmlHttp.onreadystatechange = function () { if (xmlHttp.readyState !== 4){ return; } if (xmlHttp.status !== 200 && xmlHttp.status !== 304){ callback(''); return; } callback(JSON.parse(xmlHttp.response)); }; xmlHttp.open('GET', url + L.Util.getParamString(params), true); xmlHttp.setRequestHeader('Accept', 'application/json'); xmlHttp.send(null); }, template: function (str, data) { return str.replace(/\{ *([\w_]+) *\}/g, function (str, key) { var value = data[key]; if (value === undefined) { value = ''; } else if (typeof value === 'function') { value = value(data); } return htmlEscape(value); }); }, htmlEscape: htmlEscape }; },{"leaflet":22}],6:[function(require,module,exports){ /* * leaflet-geocoder-mapzen * Leaflet plugin to search (geocode) using Mapzen Search or your * own hosted version of the Pelias Geocoder API. * * License: MIT * (c) Mapzen */ 'use strict'; // Polyfill console and its methods, if missing. (As it tends to be on IE8 (or lower)) // when the developer console is not open. require('console-polyfill'); var L = require('leaflet'); var corslite = require('@mapbox/corslite'); // Import utility functions. TODO: switch to Lodash (no IE8 support) in v2 var throttle = require('./utils/throttle'); var escapeRegExp = require('./utils/escapeRegExp'); var VERSION = '1.9.4'; var MINIMUM_INPUT_LENGTH_FOR_AUTOCOMPLETE = 1; var FULL_WIDTH_MARGIN = 20; // in pixels var FULL_WIDTH_TOUCH_ADJUSTED_MARGIN = 4; // in pixels var RESULTS_HEIGHT_MARGIN = 20; // in pixels var API_RATE_LIMIT = 250; // in ms, throttled time between subsequent requests to API // Text strings in this geocoder. var TEXT_STRINGS = { 'INPUT_PLACEHOLDER': 'Search', 'INPUT_TITLE_ATTRIBUTE': 'Search', 'RESET_TITLE_ATTRIBUTE': 'Reset', 'NO_RESULTS': 'No results were found.', // Error codes. // https://mapzen.com/documentation/search/http-status-codes/ 'ERROR_403': 'A valid API key is needed for this search feature.', 'ERROR_404': 'The search service cannot be found. :-(', 'ERROR_408': 'The search service took too long to respond. Try again in a second.', 'ERROR_429': 'There were too many requests. Try again in a second.', 'ERROR_500': 'The search service is not working right now. Please try again later.', 'ERROR_502': 'Connection lost. Please try again later.', // Unhandled error code 'ERROR_DEFAULT': 'The search service is having problems :-(' }; var Geocoder = L.Control.extend({ version: VERSION, // L.Evented is present in Leaflet v1+ // L.Mixin.Events is legacy; was deprecated in Leaflet v1 and will start // logging deprecation warnings in console in v1.1 includes: L.Evented ? L.Evented.prototype : L.Mixin.Events, options: { position: 'topleft', attribution: 'Geocoding by <a href="https://mapzen.com/projects/search/">Mapzen</a>', url: 'https://search.mapzen.com/v1', placeholder: null, // Note: this is now just an alias for textStrings.INPUT_PLACEHOLDER bounds: false, focus: true, layers: null, panToPoint: true, pointIcon: true, // 'images/point_icon.png', polygonIcon: true, // 'images/polygon_icon.png', fullWidth: 650, markers: true, overrideBbox: false, expanded: false, autocomplete: true, place: false, textStrings: TEXT_STRINGS }, initialize: function (apiKey, options) { // For IE8 compatibility (if XDomainRequest is present), // we set the default value of options.url to the protocol-relative // version, because XDomainRequest does not allow http-to-https requests // This is set first so it can always be overridden by the user if (window.XDomainRequest) { this.options.url = '//search.mapzen.com/v1'; } // If the apiKey is omitted entirely and the // first parameter is actually the options if (typeof apiKey === 'object' && !!apiKey) { options = apiKey; } else { this.apiKey = apiKey; } // Deprecation warnings // If options.latlng is defined, warn. (Do not check for falsy values, because it can be set to false.) if (options && typeof options.latlng !== 'undefined') { // Set user-specified latlng to focus option, but don't overwrite if it's already there if (typeof options.focus === 'undefined') { options.focus = options.latlng; } console.warn('[leaflet-geocoder-mapzen] DEPRECATION WARNING:', 'As of v1.6.0, the `latlng` option is deprecated. It has been renamed to `focus`. `latlng` will be removed in a future version.'); } // Deprecate `title` option if (options && typeof options.title !== 'undefined') { options.textStrings = options.textStrings || {}; options.textStrings.INPUT_TITLE_ATTRIBUTE = options.title; console.warn('[leaflet-geocoder-mapzen] DEPRECATION WARNING:', 'As of v1.8.0, the `title` option is deprecated. Please set the property `INPUT_TITLE_ATTRIBUTE` on the `textStrings` option instead. `title` will be removed in a future version.'); } // `placeholder` is not deprecated, but it is an alias for textStrings.INPUT_PLACEHOLDER if (options && typeof options.placeholder !== 'undefined') { // textStrings.INPUT_PLACEHOLDER has priority, if defined. if (!(options.textStrings && typeof options.textStrings.INPUT_PLACEHOLDER !== 'undefined')) { options.textStrings = options.textStrings || {}; options.textStrings.INPUT_PLACEHOLDER = options.placeholder; } } // Merge any strings that are not customized if (options && typeof options.textStrings === 'object') { for (var prop in this.options.textStrings) { if (typeof options.textStrings[prop] === 'undefined') { options.textStrings[prop] = this.options.textStrings[prop]; } } } // Now merge user-specified options L.Util.setOptions(this, options); this.markers = []; }, /** * Resets the geocoder control to an empty state. * * @public */ reset: function () { this._input.value = ''; L.DomUtil.addClass(this._reset, 'leaflet-pelias-hidden'); this.removeMarkers(); this.clearResults(); this.fire('reset'); }, getLayers: function (params) { var layers = this.options.layers; if (!layers) { return params; } params.layers = layers; return params; }, getBoundingBoxParam: function (params) { /* * this.options.bounds can be one of the following * true //Boolean - take the map bounds * false //Boolean - no bounds * L.latLngBounds(...) //Object * [[10, 10], [40, 60]] //Array */ var bounds = this.options.bounds; // If falsy, bail if (!bounds) { return params; } // If set to true, use map bounds // If it is a valid L.LatLngBounds object, get its values // If it is an array, try running it through L.LatLngBounds if (bounds === true && this._map) { bounds = this._map.getBounds(); params = makeParamsFromLeaflet(params, bounds); } else if (typeof bounds === 'object' && bounds.isValid && bounds.isValid()) { params = makeParamsFromLeaflet(params, bounds); } else if (L.Util.isArray(bounds)) { var latLngBounds = L.latLngBounds(bounds); if (latLngBounds.isValid && latLngBounds.isValid()) { params = makeParamsFromLeaflet(params, latLngBounds); } } function makeParamsFromLeaflet (params, latLngBounds) { params['boundary.rect.min_lon'] = latLngBounds.getWest(); params['boundary.rect.min_lat'] = latLngBounds.getSouth(); params['boundary.rect.max_lon'] = latLngBounds.getEast(); params['boundary.rect.max_lat'] = latLngBounds.getNorth(); return params; } return params; }, getFocusParam: function (params) { /** * this.options.focus can be one of the following * [50, 30] // Array * {lon: 30, lat: 50} // Object * {lat: 50, lng: 30} // Object * L.latLng(50, 30) // Object * true // Boolean - take the map center * false // Boolean - No latlng to be considered */ var focus = this.options.focus; if (!focus) { return params; } if (focus === true && this._map) { // If focus option is Boolean true, use current map center var mapCenter = this._map.getCenter(); params['focus.point.lat'] = mapCenter.lat; params['focus.point.lon'] = mapCenter.lng; } else if (typeof focus === 'object') { // Accepts array, object and L.latLng form // Constructs the latlng object using Leaflet's L.latLng() // [50, 30] // {lon: 30, lat: 50} // {lat: 50, lng: 30} // L.latLng(50, 30) var latlng = L.latLng(focus); params['focus.point.lat'] = latlng.lat; params['focus.point.lon'] = latlng.lng; } return params; }, // @method getParams(params: Object) // Collects all the parameters in a single object from various options, // including options.bounds, options.focus, options.layers, the api key, // and any params that are provided as a argument to this function. // Note that options.params will overwrite any of these getParams: function (params) { params = params || {}; params = this.getBoundingBoxParam(params); params = this.getFocusParam(params); params = this.getLayers(params); // Search API key if (this.apiKey) { params.api_key = this.apiKey; } var newParams = this.options.params; if (!newParams) { return params; } if (typeof newParams === 'object') { for (var prop in newParams) { params[prop] = newParams[prop]; } } return params; }, serialize: function (params) { var data = ''; for (var key in params) { if (params.hasOwnProperty(key)) { var param = params[key]; var type = param.toString(); var value; if (data.length) { data += '&'; } switch (type) { case '[object Array]': value = (param[0].toString() === '[object Object]') ? JSON.stringify(param) : param.join(','); break; case '[object Object]': value = JSON.stringify(param); break; case '[object Date]': value = param.valueOf(); break; default: value = param; break; } data += encodeURIComponent(key) + '=' + encodeURIComponent(value); } } return data; }, search: function (input) { // Prevent lack of input from sending a malformed query to Pelias if (!input) return; var url = this.options.url + '/search'; var params = { text: input }; this.callPelias(url, params, 'search'); }, autocomplete: throttle(function (input) { // Prevent lack of input from sending a malformed query to Pelias if (!input) return; var url = this.options.url + '/autocomplete'; var params = { text: input }; this.callPelias(url, params, 'autocomplete'); }, API_RATE_LIMIT), place: function (id) { // Prevent lack of input from sending a malformed query to Pelias if (!id) return; var url = this.options.url + '/place'; var params = { ids: id }; this.callPelias(url, params, 'place'); }, handlePlaceResponse: function (response) { // Placeholder for handling place response }, // Timestamp of the last response which was successfully rendered to the UI. // The time represents when the request was *sent*, not when it was recieved. maxReqTimestampRendered: new Date().getTime(), callPelias: function (endpoint, params, type) { params = this.getParams(params); L.DomUtil.addClass(this._search, 'leaflet-pelias-loading'); // Track when the request began var reqStartedAt = new Date().getTime(); var paramString = this.serialize(params); var url = endpoint + '?' + paramString; var self = this; // IE8 cannot .bind(this) without a polyfill. function handleResponse (err, response) { L.DomUtil.removeClass(self._search, 'leaflet-pelias-loading'); var results; try { results = JSON.parse(response.responseText); } catch (e) { err = { code: 500, message: 'Parse Error' // TODO: string }; } if (err) { var errorMessage; switch (err.code) { // Error codes. // https://mapzen.com/documentation/search/http-status-codes/ case 403: errorMessage = self.options.textStrings['ERROR_403']; break; case 404: errorMessage = self.options.textStrings['ERROR_404']; break; case 408: errorMessage = self.options.textStrings['ERROR_408']; break; case 429: errorMessage = self.options.textStrings['ERROR_429']; break; case 500: errorMessage = self.options.textStrings['ERROR_500']; break; case 502: errorMessage = self.options.textStrings['ERROR_502']; break; // Note the status code is 0 if CORS is not enabled on the error response default: errorMessage = self.options.textStrings['ERROR_DEFAULT']; break; } self.showMessage(errorMessage); self.fire('error', { results: results, endpoint: endpoint, requestType: type, params: params, errorCode: err.code, errorMessage: errorMessage }); } // There might be an error message from the geocoding service itself if (results && results.geocoding && results.geocoding.errors) { errorMessage = results.geocoding.errors[0]; self.showMessage(errorMessage); self.fire('error', { results: results, endpoint: endpoint, requestType: type, params: params, errorCode: err.code, errorMessage: errorMessage }); return; } // Autocomplete and search responses if (results && results.features) { // Check if request is stale: // Only for autocomplete or search endpoints // Ignore requests if input is currently blank // Ignore requests that started before a request which has already // been successfully rendered on to the UI. if (type === 'autocomplete' || type === 'search') { if (self._input.value === '' || self.maxReqTimestampRendered >= reqStartedAt) { return; } else { // Record the timestamp of the request. self.maxReqTimestampRendered = reqStartedAt; } } // Placeholder: handle place response if (type === 'place') { self.handlePlaceResponse(results); } // Show results if (type === 'autocomplete' || type === 'search') { self.showResults(results.features, params.text); } // Fire event self.fire('results', { results: results, endpoint: endpoint, requestType: type, params: params }); } } corslite(url, handleResponse, true); }, highlight: function (text, focus) { var r = RegExp('(' + escapeRegExp(focus) + ')', 'gi'); return text.replace(r, '<strong>$1</strong>'); }, getIconType: function (layer) { var pointIcon = this.options.pointIcon; var polygonIcon = this.options.polygonIcon; var classPrefix = 'leaflet-pelias-layer-icon-'; if (layer.match('venue') || layer.match('address')) { if (pointIcon === true) { return { type: 'class', value: classPrefix + 'point' }; } else if (pointIcon === false) { return false; } else { return { type: 'image', value: pointIcon }; } } else { if (polygonIcon === true) { return { type: 'class', value: classPrefix + 'polygon' }; } else if (polygonIcon === false) { return false; } else { return { type: 'image', value: polygonIcon }; } } }, showResults: function (features, input) { // Exit function if there are no features if (features.length === 0) { this.showMessage(this.options.textStrings['NO_RESULTS']); return; } var resultsContainer = this._results; // Reset and display results container resultsContainer.innerHTML = ''; resultsContainer.style.display = 'block'; // manage result box height resultsContainer.style.maxHeight = (this._map.getSize().y - resultsContainer.offsetTop - this._container.offsetTop - RESULTS_HEIGHT_MARGIN) + 'px'; var list = L.DomUtil.create('ul', 'leaflet-pelias-list', resultsContainer); for (var i = 0, j = features.length; i < j; i++) { var feature = features[i]; var resultItem = L.DomUtil.create('li', 'leaflet-pelias-result', list); resultItem.feature = feature; resultItem.layer = feature.properties.layer; // Deprecated // Use L.GeoJSON.coordsToLatLng(resultItem.feature.geometry.coordinates) instead // This returns a L.LatLng object that can be used throughout Leaflet resultItem.coords = feature.geometry.coordinates; var icon = this.getIconType(feature.properties.layer); if (icon) { // Point or polygon icon // May be a class or an image path var layerIconContainer = L.DomUtil.create('span', 'leaflet-pelias-layer-icon-container', resultItem); var layerIcon; if (icon.type === 'class') { layerIcon = L.DomUtil.create('div', 'leaflet-pelias-layer-icon ' + icon.value, layerIconContainer); } else { layerIcon = L.DomUtil.create('img', 'leaflet-pelias-layer-icon', layerIconContainer); layerIcon.src = icon.value; } layerIcon.title = 'layer: ' + feature.properties.layer; } resultItem.innerHTML += this.highlight(feature.properties.label, input); } }, showMessage: function (text) { var resultsContainer = this._results; // Reset and display results container resultsContainer.innerHTML = ''; resultsContainer.style.display = 'block'; var messageEl = L.DomUtil.create('div', 'leaflet-pelias-message', resultsContainer); // Set text. This is the most cross-browser compatible method // and avoids the issues we have detecting either innerText vs textContent // (e.g. Firefox cannot detect textContent property on elements, but it's there) messageEl.appendChild(document.createTextNode(text)); }, removeMarkers: function () { if (this.options.markers) { for (var i = 0; i < this.markers.length; i++) { this._map.removeLayer(this.markers[i]); } this.markers = []; } }, showMarker: function (text, latlng) { this._map.setView(latlng, this._map.getZoom() || 8); var markerOptions = (typeof this.options.markers === 'object') ? this.options.markers : {}; if (this.options.markers) { var marker = new L.marker(latlng, markerOptions).bindPopup(text); // eslint-disable-line new-cap this._map.addLayer(marker); this.markers.push(marker); marker.openPopup(); } }, /** * Fits the map view to a given bounding box. * Mapzen Search / Pelias returns the 'bbox' property on 'feature'. It is * as an array of four numbers: * [ * 0: southwest longitude, * 1: southwest latitude, * 2: northeast longitude, * 3: northeast latitude * ] * This method expects the array to be passed directly and it will be converted * to a boundary parameter for Leaflet's fitBounds(). */ fitBoundingBox: function (bbox) { this._map.fitBounds([ [ bbox[1], bbox[0] ], [ bbox[3], bbox[2] ] ], { animate: true, maxZoom: 16 }); }, setSelectedResult: function (selected, originalEvent) { var latlng = L.GeoJSON.coordsToLatLng(selected.feature.geometry.coordinates); this._input.value = selected.textContent || selected.innerText; var layer = selected.feature.properties.layer; // "point" layers (venue and address in Pelias) must always display markers if ((layer !== 'venue' && layer !== 'address') && selected.feature.bbox && !this.options.overrideBbox) { this.removeMarkers(); this.fitBoundingBox(selected.feature.bbox); } else { this.removeMarkers(); this.showMarker(selected.innerHTML, latlng); } this.fire('select', { originalEvent: originalEvent, latlng: latlng, feature: selected.feature }); this.blur(); // Not all features will be guaranteed to have `gid` property - interpolated // addresses, for example, cannot be retrieved with `/place` and so the `gid` // property for them may be dropped in the future. if (this.options.place && selected.feature.properties.gid) { this.place(selected.feature.properties.gid); } }, /** * Convenience function for focusing on the input * A `focus` event is fired, but it is not fired here. An event listener * was added to the _input element to forward the native `focus` event. * * @public */ focus: function () { // If not expanded, expand this first if (!L.DomUtil.hasClass(this._container, 'leaflet-pelias-expanded')) { this.expand(); } this._input.focus(); }, /** * Removes focus from geocoder control * A `blur` event is fired, but it is not fired here. An event listener * was added on the _input element to forward the native `blur` event. * * @public */ blur: function () { this._input.blur(); this.clearResults(); if (this._input.value === '' && this._results.style.display !== 'none') { L.DomUtil.addClass(this._reset, 'leaflet-pelias-hidden'); if (!this.options.expanded) { this.collapse(); } } }, clearResults: function (force) { // Hide results from view this._results.style.display = 'none'; // Destroy contents if input has also cleared // OR if force is true if (this._input.value === '' || force === true) { this._results.innerHTML = ''; } // Turn on scrollWheelZoom, if disabled. (`mouseout` does not fire on // the results list when it's closed in this way.) this._enableMapScrollWheelZoom(); }, expand: function () { L.DomUtil.addClass(this._container, 'leaflet-pelias-expanded'); this.setFullWidth(); this.fire('expand'); }, collapse: function () { // 'expanded' options check happens outside of this function now // So it's now possible for a script to force-collapse a geocoder // that otherwise defaults to the always-expanded state L.DomUtil.removeClass(this._container, 'leaflet-pelias-expanded'); this._input.blur(); this.clearFullWidth(); this.clearResults(); this.fire('collapse'); }, // Set full width of expanded input, if enabled setFullWidth: function () { if (this.options.fullWidth) { // If fullWidth setting is a number, only expand if map container // is smaller than that breakpoint. Otherwise, clear width // Always ask map to invalidate and recalculate size first this._map.invalidateSize(); var mapWidth = this._map.getSize().x; var touchAdjustment = L.Browser.touch ? FULL_WIDTH_TOUCH_ADJUSTED_MARGIN : 0; var width = mapWidth - FULL_WIDTH_MARGIN - touchAdjustment; if (typeof this.options.fullWidth === 'number' && mapWidth >= window.parseInt(this.options.fullWidth, 10)) { this.clearFullWidth(); return; } this._container.style.width = width.toString() + 'px'; } }, clearFullWidth: function () { // Clear set width, if any if (this.options.fullWidth) { this._container.style.width = ''; } }, onAdd: function (map) { var container = L.DomUtil.create('div', 'leaflet-pelias-control leaflet-bar leaflet-control'); this._body = document.body || document.getElementsByTagName('body')[0]; this._container = container; this._input = L.DomUtil.create('input', 'leaflet-pelias-input', this._container); this._input.spellcheck = false; // Forwards focus and blur events from input to geocoder L.DomEvent.addListener(this._input, 'focus', function (e) { this.fire('focus', { originalEvent: e }); }, this); L.DomEvent.addListener(this._input, 'blur', function (e) { this.fire('blur', { originalEvent: e }); }, this); // Only set if title option is not null or falsy if (this.options.textStrings['INPUT_TITLE_ATTRIBUTE']) { this._input.title = this.options.textStrings['INPUT_TITLE_ATTRIBUTE']; } // Only set if placeholder option is not null or falsy if (this.options.textStrings['INPUT_PLACEHOLDER']) { this._input.placeholder = this.options.textStrings['INPUT_PLACEHOLDER']; } this._search = L.DomUtil.create('a', 'leaflet-pelias-search-icon', this._container); this._reset = L.DomUtil.create('div', 'leaflet-pelias-close leaflet-pelias-hidden', this._container); this._reset.innerHTML = '×'; this._reset.title = this.options.textStrings['RESET_TITLE_ATTRIBUTE']; this._results = L.DomUtil.create('div', 'leaflet-pelias-results leaflet-bar', this._container); if (this.options.expanded) { this.expand(); } L.DomEvent .on(this._container, 'click', function (e) { // Child elements with 'click' listeners should call // stopPropagation() to prevent that event from bubbling to // the container & causing it to fire too greedily this._input.focus(); }, this) .on(this._input, 'focus', function (e) { if (this._input.value && this._results.children.length) { this._results.style.display = 'block'; } }, this) .on(this._map, 'click', function (e) { // Does what you might expect a _input.blur() listener might do, // but since that would fire for any reason (e.g. clicking a result) // what you really want is to blur from the control by listening to clicks on the map this.blur(); }, this) .on(this._search, 'click', function (e) { L.DomEvent.stopPropagation(e); // Toggles expanded state of container on click of search icon if (L.DomUtil.hasClass(this._container, 'leaflet-pelias-expanded')) { // If expanded option is true, just focus the input if (this.options.expanded === true) { this._input.focus(); } else { // Otherwise, toggle to hidden state L.DomUtil.addClass(this._reset, 'leaflet-pelias-hidden'); this.collapse(); } } else { // If not currently expanded, clicking here always expands it if (this._input.value.length > 0) { L.DomUtil.removeClass(this._reset, 'leaflet-pelias-hidden'); } this.expand(); this._input.focus(); } }, this) .on(this._reset, 'click', function (e) { this.reset(); this._input.focus(); L.DomEvent.stopPropagation(e); }, this) .on(this._input, 'keydown', function (e) { var list = this._results.querySelectorAll('.leaflet-pelias-result'); var selected = this._results.querySelectorAll('.leaflet-pelias-selected')[0]; var selectedPosition; var self = this; var panToPoint = function (selected, options) { if (selected && options.panToPoint) { var layer = selected.feature.properties.layer; // "point" layers (venue and address in Pelias) must always display markers if ((layer !== 'venue' && layer !== 'address') && selected.feature.bbox && !options.overrideBbox) { self.removeMarkers(); self.fitBoundingBox(selected.feature.bbox); } else { self.removeMarkers(); self.showMarker(selected.innerHTML, L.GeoJSON.coordsToLatLng(selected.feature.geometry.coordinates)); } } }; var scrollSelectedResultIntoView = function (selected) { var selectedRect = selected.getBoundingClientRect(); var resultsRect = self._results.getBoundingClientRect(); // Is the selected element not visible? if (selectedRect.bottom > resultsRect.bottom) { self._results.scrollTop = selected.offsetTop + selected.offsetHeight - self._results.offsetHeight; } else if (selectedRect.top < resultsRect.top) { self._results.scrollTop = selected.offsetTop; } }; for (var i = 0; i < list.length; i++) { if (list[i] === selected) { selectedPosition = i; break; } } // TODO cleanup switch (e.keyCode) { // 13 = enter case 13: if (selected) { this.setSelectedResult(selected, e); } else { // perform a full text search on enter var text = (e.target || e.srcElement).value; this.search(text); } L.DomEvent.preventDefault(e); break; // 38 = up arrow case 38: // Ignore key if there are no results or if list is not visible if (list.length === 0 || this._results.style.display === 'none') { return; } if (selected) { L.DomUtil.removeClass(selected, 'leaflet-pelias-selected'); } var previousItem = list[selectedPosition - 1]; var highlighted = (selected && previousItem) ? previousItem : list[list.length - 1]; // eslint-disable-line no-redeclare L.DomUtil.addClass(highlighted, 'leaflet-pelias-selected'); scrollSelectedResultIntoView(highlighted); panToPoint(highlighted, this.options); this._input.value = highlighted.textContent || highlighted.innerText; this.fire('highlight', { originalEvent: e, latlng: L.GeoJSON.coordsToLatLng(highlighted.feature.geometry.coordinates), feature: highlighted.feature }); L.DomEvent.preventDefault(e); break; // 40 = down arrow case 40: // Ignore key if there are no results or if list is not visible if (list.length === 0 || this._results.style.display === 'none') { return; } if (selected) { L.DomUtil.removeClass(selected, 'leaflet-pelias-selected'); } var nextItem = list[selectedPosition + 1]; var highlighted = (selected && nextItem) ? nextItem : list[0]; // eslint-disable-line no-redeclare L.DomUtil.addClass(highlighted, 'leaflet-pelias-selected'); scrollSelectedResultIntoView(highlighted); panToPoint(highlighted, this.options); this._input.value = highlighted.textContent || highlighted.innerText; this.fire('highlight', { originalEvent: e, latlng: L.GeoJSON.coordsToLatLng(highlighted.feature.geometry.coordinates), feature: highlighted.feature }); L.DomEvent.preventDefault(e); break; // all other keys default: break; } }, this) .on(this._input, 'keyup', function (e) { var key = e.which || e.keyCode; var text = (e.target || e.srcElement).value; if (text.length > 0) { L.DomUtil.removeClass(this._reset, 'leaflet-pelias-hidden'); } else { L.DomUtil.addClass(this._reset, 'leaflet-pelias-hidden'); } // Ignore all further action if the keycode matches an arrow // key (handled via keydown event) if (key === 13 || key === 38 || key === 40) { return; } // keyCode 27 = esc key (esc should clear results) if (key === 27) { // If input is blank or results have already been cleared // (perhaps due to a previous 'esc') then pressing esc at // this point will blur from input as well. if (text.length === 0 || this._results.style.display === 'none') { this._input.blur(); if (!this.options.expanded && L.DomUtil.hasClass(this._container, 'leaflet-pelias-expanded')) { this.collapse(); } } // Clears results this.clearResults(true); L.DomUtil.removeClass(this._search, 'leaflet-pelias-loading'); return; } if (text !== this._lastValue) { this._lastValue = text; if (text.length >= MINIMUM_INPUT_LENGTH_FOR_AUTOCOMPLETE && this.options.autocomplete === true) { this.autocomplete(text); } else { this.clearResults(true); } } }, this) .on(this._results, 'click', function (e) { L.DomEvent.preventDefault(e); L.DomEvent.stopPropagation(e); var _selected = this._results.querySelectorAll('.leaflet-pelias-selected')[0]; if (_selected) { L.DomUtil.removeClass(_selected, 'leaflet-pelias-selected'); } var selected = e.target || e.srcElement; /* IE8 */ var findParent = function () { if (!L.DomUtil.hasClass(selected, 'leaflet-pelias-result')) { selected = selected.parentElement; if (selected) { findParent(); } } return selected; }; // click event can be registered on the child nodes // that does not have the required coords prop // so its important to find the parent. findParent(); // If nothing is selected, (e.g. it's a message, not a result), // do nothing. if (selected) { L.DomUtil.addClass(selected, 'leaflet-pelias-selected'); this.setSelectedResult(selected, e); } }, this); // Recalculate width of the input bar when window resizes if (this.options.fullWidth) { L.DomEvent.on(window, 'resize', function (e) { if (L.DomUtil.hasClass(this._container, 'leaflet-pelias-expanded')) { this.setFullWidth(); } }, this); } L.DomEvent.on(this._results, 'mouseover', this._disableMapScrollWheelZoom, this); L.DomEvent.on(this._results, 'mouseout', this._enableMapScrollWheelZoom, this); L.DomEvent.on(this._map, 'mousedown', this._onMapInteraction, this); L.DomEvent.on(this._map, 'touchstart', this._onMapInteraction, this); L.DomEvent.disableClickPropagation(this._container); if (map.attributionControl) { map.attributionControl.addAttribution(this.options.attribution); } return container; }, _onMapInteraction: function (event) { this.blur(); // Only collapse if the input is clear, and is currently expanded. // Disabled if expanded is set to true if (!this.options.expanded) { if (!this._input.value && L.DomUtil.hasClass(this._container, 'leaflet-pelias-expanded')) { this.collapse(); } } }, _disableMapScrollWheelZoom: function (event) { // Prevent scrolling over results list from zooming the map, if enabled this._scrollWheelZoomEnabled = this._map.scrollWheelZoom.enabled(); if (this._scrollWheelZoomEnabled) { this._map.scrollWheelZoom.disable(); } }, _enableMapScrollWheelZoom: function (event) { // Re-enable scroll wheel zoom (if previously enabled) after // leaving the results box if (this._scrollWheelZoomEnabled) { this._map.scrollWheelZoom.enable(); } }, onRemove: function (map) { if (map.attributionControl) { map.attributionControl.removeAttribution(this.options.attribution); } } }); module.exports = Geocoder; },{"./utils/escapeRegExp":7,"./utils/throttle":8,"@mapbox/corslite":1,"console-polyfill":3,"leaflet":22}],7:[function(require,module,exports){ /* * escaping a string for regex Utility function * from https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex */ function escapeRegExp (str) { return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); } module.exports = escapeRegExp; },{}],8:[function(require,module,exports){ /* * throttle Utility function (borrowed from underscore) */ function throttle (func, wait, options) { var context, args, result; var timeout = null; var previous = 0; if (!options) options = {}; var later = function () { previous = options.leading === false ? 0 : new Date().getTime(); timeout = null; result = func.apply(context, args); if (!timeout) context = args = null; }; return function () { var now = new Date().getTime(); if (!previous && options.leading === false) previous = now; var remaining = wait - (now - previous); context = this; args = arguments; if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout)