diff --git a/assets/css/Control.Geocoder.css b/assets/css/Control.Geocoder.css
new file mode 100644
index 00000000..00c32255
--- /dev/null
+++ b/assets/css/Control.Geocoder.css
@@ -0,0 +1,126 @@
+.leaflet-control-geocoder {
+ border-radius: 4px;
+ background: white;
+ min-width: 26px;
+ min-height: 26px;
+}
+
+.leaflet-touch .leaflet-control-geocoder {
+ min-width: 30px;
+ min-height: 30px;
+}
+
+.leaflet-control-geocoder a,
+.leaflet-control-geocoder .leaflet-control-geocoder-icon {
+ border-bottom: none;
+ display: inline-block;
+}
+
+.leaflet-control-geocoder .leaflet-control-geocoder-alternatives a {
+ width: inherit;
+ height: inherit;
+ line-height: inherit;
+}
+
+.leaflet-control-geocoder a:hover,
+.leaflet-control-geocoder .leaflet-control-geocoder-icon:hover {
+ border-bottom: none;
+ display: inline-block;
+}
+
+.leaflet-control-geocoder-form {
+ display: none;
+ vertical-align: middle;
+}
+.leaflet-control-geocoder-expanded .leaflet-control-geocoder-form {
+ display: inline-block;
+}
+.leaflet-control-geocoder-form input {
+ font-size: 120%;
+ border: 0;
+ background-color: transparent;
+ width: 246px;
+}
+
+.leaflet-control-geocoder-icon {
+ border-radius: 4px;
+ width: 26px;
+ height: 26px;
+ border: none;
+ background-color: white;
+ background-image: url(images/geocoder.png);
+ background-repeat: no-repeat;
+ background-position: center;
+ cursor: pointer;
+}
+
+.leaflet-touch .leaflet-control-geocoder-icon {
+ width: 30px;
+ height: 30px;
+}
+
+.leaflet-control-geocoder-throbber .leaflet-control-geocoder-icon {
+ background-image: url(images/throbber.gif);
+}
+
+.leaflet-control-geocoder-form-no-error {
+ display: none;
+}
+
+.leaflet-control-geocoder-form input:focus {
+ outline: none;
+}
+
+.leaflet-control-geocoder-form button {
+ display: none;
+}
+.leaflet-control-geocoder-error {
+ margin-top: 8px;
+ margin-left: 8px;
+ display: block;
+ color: #444;
+}
+.leaflet-control-geocoder-alternatives {
+ display: block;
+ width: 272px;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.leaflet-control-geocoder-alternatives-minimized {
+ display: none;
+ height: 0;
+}
+.leaflet-control-geocoder-alternatives li {
+ white-space: nowrap;
+ display: block;
+ overflow: hidden;
+ padding: 5px 8px;
+ text-overflow: ellipsis;
+ border-bottom: 1px solid #ccc;
+ cursor: pointer;
+}
+
+.leaflet-control-geocoder-alternatives li a,
+.leaflet-control-geocoder-alternatives li a:hover {
+ width: inherit;
+ height: inherit;
+ line-height: inherit;
+ background: inherit;
+ border-radius: inherit;
+ text-align: left;
+}
+
+.leaflet-control-geocoder-alternatives li:last-child {
+ border-bottom: none;
+}
+.leaflet-control-geocoder-alternatives li:hover,
+.leaflet-control-geocoder-selected {
+ background-color: #f5f5f5;
+}
+.leaflet-control-geocoder-address-detail {
+}
+.leaflet-control-geocoder-address-context {
+ color: #666;
+}
diff --git a/assets/css/images/geocoder.png b/assets/css/images/geocoder.png
new file mode 100644
index 00000000..d82a0170
Binary files /dev/null and b/assets/css/images/geocoder.png differ
diff --git a/assets/css/images/throbber.gif b/assets/css/images/throbber.gif
new file mode 100644
index 00000000..c0c52a06
Binary files /dev/null and b/assets/css/images/throbber.gif differ
diff --git a/assets/js/Control.Geocoder.js b/assets/js/Control.Geocoder.js
new file mode 100644
index 00000000..78408d40
--- /dev/null
+++ b/assets/js/Control.Geocoder.js
@@ -0,0 +1,1348 @@
+this.L = this.L || {};
+this.L.Control = this.L.Control || {};
+this.L.Control.Geocoder = (function (L) {
+'use strict';
+
+L = L && L.hasOwnProperty('default') ? L['default'] : L;
+
+var lastCallbackId = 0;
+
+// Adapted from handlebars.js
+// https://github.com/wycats/handlebars.js/
+var badChars = /[&<>"'`]/g;
+var possible = /[&<>"'`]/;
+var escape = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ "'": ''',
+ '`': '`'
+};
+
+function escapeChar(chr) {
+ return escape[chr];
+}
+
+function htmlEscape(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);
+}
+
+function jsonp(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);
+}
+
+function getJSON(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);
+}
+
+function template(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);
+ });
+}
+
+var Nominatim = {
+ class: L.Class.extend({
+ options: {
+ serviceUrl: 'https://nominatim.openstreetmap.org/',
+ geocodingQueryParams: {},
+ reverseQueryParams: {},
+ htmlTemplate: function(r) {
+ var a = r.address,
+ parts = [];
+ if (a.road || a.building) {
+ parts.push('{building} {road} {house_number}');
+ }
+
+ if (a.city || a.town || a.village || a.hamlet) {
+ parts.push(
+ '{postcode} {city} {town} {village} {hamlet}'
+ );
+ }
+
+ if (a.state || a.country) {
+ parts.push(
+ '{state} {country}'
+ );
+ }
+
+ return template(parts.join('
'), a, true);
+ }
+ },
+
+ initialize: function(options) {
+ L.Util.setOptions(this, options);
+ },
+
+ geocode: function(query, cb, context) {
+ getJSON(
+ this.options.serviceUrl + 'search',
+ L.extend(
+ {
+ q: query,
+ limit: 5,
+ format: 'json',
+ addressdetails: 1
+ },
+ this.options.geocodingQueryParams
+ ),
+ L.bind(function(data) {
+ var results = [];
+ for (var i = data.length - 1; i >= 0; i--) {
+ var bbox = data[i].boundingbox;
+ for (var j = 0; j < 4; j++) bbox[j] = parseFloat(bbox[j]);
+ results[i] = {
+ icon: data[i].icon,
+ name: data[i].display_name,
+ html: this.options.htmlTemplate ? this.options.htmlTemplate(data[i]) : undefined,
+ bbox: L.latLngBounds([bbox[0], bbox[2]], [bbox[1], bbox[3]]),
+ center: L.latLng(data[i].lat, data[i].lon),
+ properties: data[i]
+ };
+ }
+ cb.call(context, results);
+ }, this)
+ );
+ },
+
+ reverse: function(location, scale, cb, context) {
+ getJSON(
+ this.options.serviceUrl + 'reverse',
+ L.extend(
+ {
+ lat: location.lat,
+ lon: location.lng,
+ zoom: Math.round(Math.log(scale / 256) / Math.log(2)),
+ addressdetails: 1,
+ format: 'json'
+ },
+ this.options.reverseQueryParams
+ ),
+ L.bind(function(data) {
+ var result = [],
+ loc;
+
+ if (data && data.lat && data.lon) {
+ loc = L.latLng(data.lat, data.lon);
+ result.push({
+ name: data.display_name,
+ html: this.options.htmlTemplate ? this.options.htmlTemplate(data) : undefined,
+ center: loc,
+ bounds: L.latLngBounds(loc, loc),
+ properties: data
+ });
+ }
+
+ cb.call(context, result);
+ }, this)
+ );
+ }
+ }),
+
+ factory: function(options) {
+ return new L.Control.Geocoder.Nominatim(options);
+ }
+};
+
+var Control = {
+ class: L.Control.extend({
+ options: {
+ showResultIcons: false,
+ collapsed: true,
+ expand: 'touch', // options: touch, click, anythingelse
+ position: 'topright',
+ placeholder: 'Search...',
+ errorMessage: 'Nothing found.',
+ suggestMinLength: 3,
+ suggestTimeout: 250,
+ defaultMarkGeocode: true
+ },
+
+ includes: L.Evented.prototype || L.Mixin.Events,
+
+ initialize: function(options) {
+ L.Util.setOptions(this, options);
+ if (!this.options.geocoder) {
+ this.options.geocoder = new Nominatim.class();
+ }
+
+ this._requestCount = 0;
+ },
+
+ onAdd: function(map) {
+ var className = 'leaflet-control-geocoder',
+ container = L.DomUtil.create('div', className + ' leaflet-bar'),
+ icon = L.DomUtil.create('button', className + '-icon', container),
+ form = (this._form = L.DomUtil.create('div', className + '-form', container)),
+ input;
+
+ this._map = map;
+ this._container = container;
+
+ icon.innerHTML = ' ';
+ icon.type = 'button';
+
+ input = this._input = L.DomUtil.create('input', '', form);
+ input.type = 'text';
+ input.placeholder = this.options.placeholder;
+
+ this._errorElement = L.DomUtil.create('div', className + '-form-no-error', container);
+ this._errorElement.innerHTML = this.options.errorMessage;
+
+ this._alts = L.DomUtil.create(
+ 'ul',
+ className + '-alternatives leaflet-control-geocoder-alternatives-minimized',
+ container
+ );
+ L.DomEvent.disableClickPropagation(this._alts);
+
+ L.DomEvent.addListener(input, 'keydown', this._keydown, this);
+ if (this.options.geocoder.suggest) {
+ L.DomEvent.addListener(input, 'input', this._change, this);
+ }
+ L.DomEvent.addListener(
+ input,
+ 'blur',
+ function() {
+ if (this.options.collapsed && !this._preventBlurCollapse) {
+ this._collapse();
+ }
+ this._preventBlurCollapse = false;
+ },
+ this
+ );
+
+ if (this.options.collapsed) {
+ if (this.options.expand === 'click') {
+ L.DomEvent.addListener(
+ container,
+ 'click',
+ function(e) {
+ if (e.button === 0 && e.detail !== 2) {
+ this._toggle();
+ }
+ },
+ this
+ );
+ } else if (L.Browser.touch && this.options.expand === 'touch') {
+ L.DomEvent.addListener(
+ container,
+ 'touchstart mousedown',
+ function(e) {
+ this._toggle();
+ e.preventDefault(); // mobile: clicking focuses the icon, so UI expands and immediately collapses
+ e.stopPropagation();
+ },
+ this
+ );
+ } else {
+ L.DomEvent.addListener(container, 'mouseover', this._expand, this);
+ L.DomEvent.addListener(container, 'mouseout', this._collapse, this);
+ this._map.on('movestart', this._collapse, this);
+ }
+ } else {
+ this._expand();
+ if (L.Browser.touch) {
+ L.DomEvent.addListener(
+ container,
+ 'touchstart',
+ function() {
+ this._geocode();
+ },
+ this
+ );
+ } else {
+ L.DomEvent.addListener(
+ container,
+ 'click',
+ function() {
+ this._geocode();
+ },
+ this
+ );
+ }
+ }
+
+ if (this.options.defaultMarkGeocode) {
+ this.on('markgeocode', this.markGeocode, this);
+ }
+
+ this.on(
+ 'startgeocode',
+ function() {
+ L.DomUtil.addClass(this._container, 'leaflet-control-geocoder-throbber');
+ },
+ this
+ );
+ this.on(
+ 'finishgeocode',
+ function() {
+ L.DomUtil.removeClass(this._container, 'leaflet-control-geocoder-throbber');
+ },
+ this
+ );
+
+ L.DomEvent.disableClickPropagation(container);
+
+ return container;
+ },
+
+ _geocodeResult: function(results, suggest) {
+ if (!suggest && results.length === 1) {
+ this._geocodeResultSelected(results[0]);
+ } else if (results.length > 0) {
+ this._alts.innerHTML = '';
+ this._results = results;
+ L.DomUtil.removeClass(this._alts, 'leaflet-control-geocoder-alternatives-minimized');
+ for (var i = 0; i < results.length; i++) {
+ this._alts.appendChild(this._createAlt(results[i], i));
+ }
+ } else {
+ L.DomUtil.addClass(this._errorElement, 'leaflet-control-geocoder-error');
+ }
+ },
+
+ markGeocode: function(result) {
+ result = result.geocode || result;
+
+ this._map.fitBounds(result.bbox);
+
+ if (this._geocodeMarker) {
+ this._map.removeLayer(this._geocodeMarker);
+ }
+
+ this._geocodeMarker = new L.Marker(result.center)
+ .bindPopup(result.html || result.name)
+ .addTo(this._map)
+ .openPopup();
+
+ return this;
+ },
+
+ _geocode: function(suggest) {
+ var requestCount = ++this._requestCount,
+ mode = suggest ? 'suggest' : 'geocode',
+ eventData = { input: this._input.value };
+
+ this._lastGeocode = this._input.value;
+ if (!suggest) {
+ this._clearResults();
+ }
+
+ this.fire('start' + mode, eventData);
+ this.options.geocoder[mode](
+ this._input.value,
+ function(results) {
+ if (requestCount === this._requestCount) {
+ eventData.results = results;
+ this.fire('finish' + mode, eventData);
+ this._geocodeResult(results, suggest);
+ }
+ },
+ this
+ );
+ },
+
+ _geocodeResultSelected: function(result) {
+ this.fire('markgeocode', { geocode: result });
+ },
+
+ _toggle: function() {
+ if (L.DomUtil.hasClass(this._container, 'leaflet-control-geocoder-expanded')) {
+ this._collapse();
+ } else {
+ this._expand();
+ }
+ },
+
+ _expand: function() {
+ L.DomUtil.addClass(this._container, 'leaflet-control-geocoder-expanded');
+ this._input.select();
+ this.fire('expand');
+ },
+
+ _collapse: function() {
+ L.DomUtil.removeClass(this._container, 'leaflet-control-geocoder-expanded');
+ L.DomUtil.addClass(this._alts, 'leaflet-control-geocoder-alternatives-minimized');
+ L.DomUtil.removeClass(this._errorElement, 'leaflet-control-geocoder-error');
+ this._input.blur(); // mobile: keyboard shouldn't stay expanded
+ this.fire('collapse');
+ },
+
+ _clearResults: function() {
+ L.DomUtil.addClass(this._alts, 'leaflet-control-geocoder-alternatives-minimized');
+ this._selection = null;
+ L.DomUtil.removeClass(this._errorElement, 'leaflet-control-geocoder-error');
+ },
+
+ _createAlt: function(result, index) {
+ var li = L.DomUtil.create('li', ''),
+ a = L.DomUtil.create('a', '', li),
+ icon = this.options.showResultIcons && result.icon ? L.DomUtil.create('img', '', a) : null,
+ text = result.html ? undefined : document.createTextNode(result.name),
+ mouseDownHandler = function mouseDownHandler(e) {
+ // In some browsers, a click will fire on the map if the control is
+ // collapsed directly after mousedown. To work around this, we
+ // wait until the click is completed, and _then_ collapse the
+ // control. Messy, but this is the workaround I could come up with
+ // for #142.
+ this._preventBlurCollapse = true;
+ L.DomEvent.stop(e);
+ this._geocodeResultSelected(result);
+ L.DomEvent.on(
+ li,
+ 'click',
+ function() {
+ if (this.options.collapsed) {
+ this._collapse();
+ } else {
+ this._clearResults();
+ }
+ },
+ this
+ );
+ };
+
+ if (icon) {
+ icon.src = result.icon;
+ }
+
+ li.setAttribute('data-result-index', index);
+
+ if (result.html) {
+ a.innerHTML = a.innerHTML + result.html;
+ } else {
+ a.appendChild(text);
+ }
+
+ // Use mousedown and not click, since click will fire _after_ blur,
+ // causing the control to have collapsed and removed the items
+ // before the click can fire.
+ L.DomEvent.addListener(li, 'mousedown touchstart', mouseDownHandler, this);
+
+ return li;
+ },
+
+ _keydown: function(e) {
+ var _this = this,
+ select = function select(dir) {
+ if (_this._selection) {
+ L.DomUtil.removeClass(_this._selection, 'leaflet-control-geocoder-selected');
+ _this._selection = _this._selection[dir > 0 ? 'nextSibling' : 'previousSibling'];
+ }
+ if (!_this._selection) {
+ _this._selection = _this._alts[dir > 0 ? 'firstChild' : 'lastChild'];
+ }
+
+ if (_this._selection) {
+ L.DomUtil.addClass(_this._selection, 'leaflet-control-geocoder-selected');
+ }
+ };
+
+ switch (e.keyCode) {
+ // Escape
+ case 27:
+ if (this.options.collapsed) {
+ this._collapse();
+ }
+ break;
+ // Up
+ case 38:
+ select(-1);
+ break;
+ // Up
+ case 40:
+ select(1);
+ break;
+ // Enter
+ case 13:
+ if (this._selection) {
+ var index = parseInt(this._selection.getAttribute('data-result-index'), 10);
+ this._geocodeResultSelected(this._results[index]);
+ this._clearResults();
+ } else {
+ this._geocode();
+ }
+ break;
+ }
+ },
+ _change: function() {
+ var v = this._input.value;
+ if (v !== this._lastGeocode) {
+ clearTimeout(this._suggestTimeout);
+ if (v.length >= this.options.suggestMinLength) {
+ this._suggestTimeout = setTimeout(
+ L.bind(function() {
+ this._geocode(true);
+ }, this),
+ this.options.suggestTimeout
+ );
+ } else {
+ this._clearResults();
+ }
+ }
+ }
+ }),
+ factory: function(options) {
+ return new L.Control.Geocoder(options);
+ }
+};
+
+var Bing = {
+ class: L.Class.extend({
+ initialize: function(key) {
+ this.key = key;
+ },
+
+ geocode: function(query, cb, context) {
+ jsonp(
+ 'https://dev.virtualearth.net/REST/v1/Locations',
+ {
+ query: query,
+ key: this.key
+ },
+ function(data) {
+ var results = [];
+ if (data.resourceSets.length > 0) {
+ for (var i = data.resourceSets[0].resources.length - 1; i >= 0; i--) {
+ var resource = data.resourceSets[0].resources[i],
+ bbox = resource.bbox;
+ results[i] = {
+ name: resource.name,
+ bbox: L.latLngBounds([bbox[0], bbox[1]], [bbox[2], bbox[3]]),
+ center: L.latLng(resource.point.coordinates)
+ };
+ }
+ }
+ cb.call(context, results);
+ },
+ this,
+ 'jsonp'
+ );
+ },
+
+ reverse: function(location, scale, cb, context) {
+ jsonp(
+ '//dev.virtualearth.net/REST/v1/Locations/' + location.lat + ',' + location.lng,
+ {
+ key: this.key
+ },
+ function(data) {
+ var results = [];
+ for (var i = data.resourceSets[0].resources.length - 1; i >= 0; i--) {
+ var resource = data.resourceSets[0].resources[i],
+ bbox = resource.bbox;
+ results[i] = {
+ name: resource.name,
+ bbox: L.latLngBounds([bbox[0], bbox[1]], [bbox[2], bbox[3]]),
+ center: L.latLng(resource.point.coordinates)
+ };
+ }
+ cb.call(context, results);
+ },
+ this,
+ 'jsonp'
+ );
+ }
+ }),
+
+ factory: function(key) {
+ return new L.Control.Geocoder.Bing(key);
+ }
+};
+
+var MapQuest = {
+ class: L.Class.extend({
+ options: {
+ serviceUrl: 'https://www.mapquestapi.com/geocoding/v1'
+ },
+
+ initialize: function(key, options) {
+ // MapQuest seems to provide URI encoded API keys,
+ // so to avoid encoding them twice, we decode them here
+ this._key = decodeURIComponent(key);
+
+ L.Util.setOptions(this, options);
+ },
+
+ _formatName: function() {
+ var r = [],
+ i;
+ for (i = 0; i < arguments.length; i++) {
+ if (arguments[i]) {
+ r.push(arguments[i]);
+ }
+ }
+
+ return r.join(', ');
+ },
+
+ geocode: function(query, cb, context) {
+ getJSON(
+ this.options.serviceUrl + '/address',
+ {
+ key: this._key,
+ location: query,
+ limit: 5,
+ outFormat: 'json'
+ },
+ L.bind(function(data) {
+ var results = [],
+ loc,
+ latLng;
+ if (data.results && data.results[0].locations) {
+ for (var i = data.results[0].locations.length - 1; i >= 0; i--) {
+ loc = data.results[0].locations[i];
+ latLng = L.latLng(loc.latLng);
+ results[i] = {
+ name: this._formatName(loc.street, loc.adminArea4, loc.adminArea3, loc.adminArea1),
+ bbox: L.latLngBounds(latLng, latLng),
+ center: latLng
+ };
+ }
+ }
+
+ cb.call(context, results);
+ }, this)
+ );
+ },
+
+ reverse: function(location, scale, cb, context) {
+ getJSON(
+ this.options.serviceUrl + '/reverse',
+ {
+ key: this._key,
+ location: location.lat + ',' + location.lng,
+ outputFormat: 'json'
+ },
+ L.bind(function(data) {
+ var results = [],
+ loc,
+ latLng;
+ if (data.results && data.results[0].locations) {
+ for (var i = data.results[0].locations.length - 1; i >= 0; i--) {
+ loc = data.results[0].locations[i];
+ latLng = L.latLng(loc.latLng);
+ results[i] = {
+ name: this._formatName(loc.street, loc.adminArea4, loc.adminArea3, loc.adminArea1),
+ bbox: L.latLngBounds(latLng, latLng),
+ center: latLng
+ };
+ }
+ }
+
+ cb.call(context, results);
+ }, this)
+ );
+ }
+ }),
+
+ factory: function(key, options) {
+ return new L.Control.Geocoder.MapQuest(key, options);
+ }
+};
+
+var Mapbox = {
+ class: L.Class.extend({
+ options: {
+ serviceUrl: 'https://api.tiles.mapbox.com/v4/geocode/mapbox.places-v1/',
+ geocodingQueryParams: {},
+ reverseQueryParams: {}
+ },
+
+ initialize: function(accessToken, options) {
+ L.setOptions(this, options);
+ this.options.geocodingQueryParams.access_token = accessToken;
+ this.options.reverseQueryParams.access_token = accessToken;
+ },
+
+ geocode: function(query, cb, context) {
+ var params = this.options.geocodingQueryParams;
+ if (
+ typeof params.proximity !== 'undefined' &&
+ params.proximity.hasOwnProperty('lat') &&
+ params.proximity.hasOwnProperty('lng')
+ ) {
+ params.proximity = params.proximity.lng + ',' + params.proximity.lat;
+ }
+ getJSON(this.options.serviceUrl + encodeURIComponent(query) + '.json', params, function(
+ data
+ ) {
+ var results = [],
+ loc,
+ latLng,
+ latLngBounds;
+ if (data.features && data.features.length) {
+ for (var i = 0; i <= data.features.length - 1; i++) {
+ loc = data.features[i];
+ latLng = L.latLng(loc.center.reverse());
+ if (loc.hasOwnProperty('bbox')) {
+ latLngBounds = L.latLngBounds(
+ L.latLng(loc.bbox.slice(0, 2).reverse()),
+ L.latLng(loc.bbox.slice(2, 4).reverse())
+ );
+ } else {
+ latLngBounds = L.latLngBounds(latLng, latLng);
+ }
+ results[i] = {
+ name: loc.place_name,
+ bbox: latLngBounds,
+ center: latLng
+ };
+ }
+ }
+
+ cb.call(context, results);
+ });
+ },
+
+ suggest: function(query, cb, context) {
+ return this.geocode(query, cb, context);
+ },
+
+ reverse: function(location, scale, cb, context) {
+ getJSON(
+ this.options.serviceUrl +
+ encodeURIComponent(location.lng) +
+ ',' +
+ encodeURIComponent(location.lat) +
+ '.json',
+ this.options.reverseQueryParams,
+ function(data) {
+ var results = [],
+ loc,
+ latLng,
+ latLngBounds;
+ if (data.features && data.features.length) {
+ for (var i = 0; i <= data.features.length - 1; i++) {
+ loc = data.features[i];
+ latLng = L.latLng(loc.center.reverse());
+ if (loc.hasOwnProperty('bbox')) {
+ latLngBounds = L.latLngBounds(
+ L.latLng(loc.bbox.slice(0, 2).reverse()),
+ L.latLng(loc.bbox.slice(2, 4).reverse())
+ );
+ } else {
+ latLngBounds = L.latLngBounds(latLng, latLng);
+ }
+ results[i] = {
+ name: loc.place_name,
+ bbox: latLngBounds,
+ center: latLng
+ };
+ }
+ }
+
+ cb.call(context, results);
+ }
+ );
+ }
+ }),
+
+ factory: function(accessToken, options) {
+ return new L.Control.Geocoder.Mapbox(accessToken, options);
+ }
+};
+
+var What3Words = {
+ class: L.Class.extend({
+ options: {
+ serviceUrl: 'https://api.what3words.com/v2/'
+ },
+
+ initialize: function(accessToken) {
+ this._accessToken = accessToken;
+ },
+
+ geocode: function(query, cb, context) {
+ //get three words and make a dot based string
+ getJSON(
+ this.options.serviceUrl + 'forward',
+ {
+ key: this._accessToken,
+ addr: query.split(/\s+/).join('.')
+ },
+ function(data) {
+ var results = [],
+ latLng,
+ latLngBounds;
+ if (data.hasOwnProperty('geometry')) {
+ latLng = L.latLng(data.geometry['lat'], data.geometry['lng']);
+ latLngBounds = L.latLngBounds(latLng, latLng);
+ results[0] = {
+ name: data.words,
+ bbox: latLngBounds,
+ center: latLng
+ };
+ }
+
+ cb.call(context, results);
+ }
+ );
+ },
+
+ suggest: function(query, cb, context) {
+ return this.geocode(query, cb, context);
+ },
+
+ reverse: function(location, scale, cb, context) {
+ getJSON(
+ this.options.serviceUrl + 'reverse',
+ {
+ key: this._accessToken,
+ coords: [location.lat, location.lng].join(',')
+ },
+ function(data) {
+ var results = [],
+ latLng,
+ latLngBounds;
+ if (data.status.status == 200) {
+ latLng = L.latLng(data.geometry['lat'], data.geometry['lng']);
+ latLngBounds = L.latLngBounds(latLng, latLng);
+ results[0] = {
+ name: data.words,
+ bbox: latLngBounds,
+ center: latLng
+ };
+ }
+ cb.call(context, results);
+ }
+ );
+ }
+ }),
+
+ factory: function(accessToken) {
+ return new L.Control.Geocoder.What3Words(accessToken);
+ }
+};
+
+var Google = {
+ class: L.Class.extend({
+ options: {
+ serviceUrl: 'https://maps.googleapis.com/maps/api/geocode/json',
+ geocodingQueryParams: {},
+ reverseQueryParams: {}
+ },
+
+ initialize: function(key, options) {
+ this._key = key;
+ L.setOptions(this, options);
+ // Backwards compatibility
+ this.options.serviceUrl = this.options.service_url || this.options.serviceUrl;
+ },
+
+ geocode: function(query, cb, context) {
+ var params = {
+ address: query
+ };
+
+ if (this._key && this._key.length) {
+ params.key = this._key;
+ }
+
+ params = L.Util.extend(params, this.options.geocodingQueryParams);
+
+ getJSON(this.options.serviceUrl, params, function(data) {
+ var results = [],
+ loc,
+ latLng,
+ latLngBounds;
+ if (data.results && data.results.length) {
+ for (var i = 0; i <= data.results.length - 1; i++) {
+ loc = data.results[i];
+ latLng = L.latLng(loc.geometry.location);
+ latLngBounds = L.latLngBounds(
+ L.latLng(loc.geometry.viewport.northeast),
+ L.latLng(loc.geometry.viewport.southwest)
+ );
+ results[i] = {
+ name: loc.formatted_address,
+ bbox: latLngBounds,
+ center: latLng,
+ properties: loc.address_components
+ };
+ }
+ }
+
+ cb.call(context, results);
+ });
+ },
+
+ reverse: function(location, scale, cb, context) {
+ var params = {
+ latlng: encodeURIComponent(location.lat) + ',' + encodeURIComponent(location.lng)
+ };
+ params = L.Util.extend(params, this.options.reverseQueryParams);
+ if (this._key && this._key.length) {
+ params.key = this._key;
+ }
+
+ getJSON(this.options.serviceUrl, params, function(data) {
+ var results = [],
+ loc,
+ latLng,
+ latLngBounds;
+ if (data.results && data.results.length) {
+ for (var i = 0; i <= data.results.length - 1; i++) {
+ loc = data.results[i];
+ latLng = L.latLng(loc.geometry.location);
+ latLngBounds = L.latLngBounds(
+ L.latLng(loc.geometry.viewport.northeast),
+ L.latLng(loc.geometry.viewport.southwest)
+ );
+ results[i] = {
+ name: loc.formatted_address,
+ bbox: latLngBounds,
+ center: latLng,
+ properties: loc.address_components
+ };
+ }
+ }
+
+ cb.call(context, results);
+ });
+ }
+ }),
+
+ factory: function(key, options) {
+ return new L.Control.Geocoder.Google(key, options);
+ }
+};
+
+var Photon = {
+ class: L.Class.extend({
+ options: {
+ serviceUrl: 'https://photon.komoot.de/api/',
+ reverseUrl: 'https://photon.komoot.de/reverse/',
+ nameProperties: ['name', 'street', 'suburb', 'hamlet', 'town', 'city', 'state', 'country']
+ },
+
+ initialize: function(options) {
+ L.setOptions(this, options);
+ },
+
+ geocode: function(query, cb, context) {
+ var params = L.extend(
+ {
+ q: query
+ },
+ this.options.geocodingQueryParams
+ );
+
+ getJSON(
+ this.options.serviceUrl,
+ params,
+ L.bind(function(data) {
+ cb.call(context, this._decodeFeatures(data));
+ }, this)
+ );
+ },
+
+ suggest: function(query, cb, context) {
+ return this.geocode(query, cb, context);
+ },
+
+ reverse: function(latLng, scale, cb, context) {
+ var params = L.extend(
+ {
+ lat: latLng.lat,
+ lon: latLng.lng
+ },
+ this.options.reverseQueryParams
+ );
+
+ getJSON(
+ this.options.reverseUrl,
+ params,
+ L.bind(function(data) {
+ cb.call(context, this._decodeFeatures(data));
+ }, this)
+ );
+ },
+
+ _decodeFeatures: function(data) {
+ var results = [],
+ i,
+ f,
+ c,
+ latLng,
+ extent,
+ bbox;
+
+ if (data && data.features) {
+ for (i = 0; i < data.features.length; i++) {
+ f = data.features[i];
+ c = f.geometry.coordinates;
+ latLng = L.latLng(c[1], c[0]);
+ extent = f.properties.extent;
+
+ if (extent) {
+ bbox = L.latLngBounds([extent[1], extent[0]], [extent[3], extent[2]]);
+ } else {
+ bbox = L.latLngBounds(latLng, latLng);
+ }
+
+ results.push({
+ name: this._deocodeFeatureName(f),
+ html: this.options.htmlTemplate ? this.options.htmlTemplate(f) : undefined,
+ center: latLng,
+ bbox: bbox,
+ properties: f.properties
+ });
+ }
+ }
+
+ return results;
+ },
+
+ _deocodeFeatureName: function(f) {
+ var j, name;
+ for (j = 0; !name && j < this.options.nameProperties.length; j++) {
+ name = f.properties[this.options.nameProperties[j]];
+ }
+
+ return name;
+ }
+ }),
+
+ factory: function(options) {
+ return new L.Control.Geocoder.Photon(options);
+ }
+};
+
+var Mapzen = {
+ 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;
+ 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;
+ 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;
+ 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);
+ }
+};
+
+var ArcGis = {
+ class: L.Class.extend({
+ options: {
+ service_url: 'http://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer'
+ },
+
+ initialize: function(accessToken, options) {
+ L.setOptions(this, options);
+ this._accessToken = accessToken;
+ },
+
+ geocode: function(query, cb, context) {
+ var params = {
+ SingleLine: query,
+ outFields: 'Addr_Type',
+ forStorage: false,
+ maxLocations: 10,
+ f: 'json'
+ };
+
+ if (this._key && this._key.length) {
+ params.token = this._key;
+ }
+
+ getJSON(this.options.service_url + '/findAddressCandidates', params, function(data) {
+ var results = [],
+ loc,
+ latLng,
+ latLngBounds;
+
+ if (data.candidates && data.candidates.length) {
+ for (var i = 0; i <= data.candidates.length - 1; i++) {
+ loc = data.candidates[i];
+ latLng = L.latLng(loc.location.y, loc.location.x);
+ latLngBounds = L.latLngBounds(
+ L.latLng(loc.extent.ymax, loc.extent.xmax),
+ L.latLng(loc.extent.ymin, loc.extent.xmin)
+ );
+ results[i] = {
+ name: loc.address,
+ bbox: latLngBounds,
+ center: latLng
+ };
+ }
+ }
+
+ cb.call(context, results);
+ });
+ },
+
+ suggest: function(query, cb, context) {
+ return this.geocode(query, cb, context);
+ },
+
+ reverse: function(location, scale, cb, context) {
+ var params = {
+ location: encodeURIComponent(location.lng) + ',' + encodeURIComponent(location.lat),
+ distance: 100,
+ f: 'json'
+ };
+
+ getJSON(this.options.service_url + '/reverseGeocode', params, function(data) {
+ var result = [],
+ loc;
+
+ if (data && !data.error) {
+ loc = L.latLng(data.location.y, data.location.x);
+ result.push({
+ name: data.address.Match_addr,
+ center: loc,
+ bounds: L.latLngBounds(loc, loc)
+ });
+ }
+
+ cb.call(context, result);
+ });
+ }
+ }),
+
+ factory: function(accessToken, options) {
+ return new L.Control.Geocoder.ArcGis(accessToken, options);
+ }
+};
+
+var HERE = {
+ class: L.Class.extend({
+ options: {
+ geocodeUrl: 'http://geocoder.api.here.com/6.2/geocode.json',
+ reverseGeocodeUrl: 'http://reverse.geocoder.api.here.com/6.2/reversegeocode.json',
+ app_id: '',
+ app_code: '',
+ geocodingQueryParams: {},
+ reverseQueryParams: {}
+ },
+
+ initialize: function(options) {
+ L.setOptions(this, options);
+ },
+
+ geocode: function(query, cb, context) {
+ var params = {
+ searchtext: query,
+ gen: 9,
+ app_id: this.options.app_id,
+ app_code: this.options.app_code,
+ jsonattributes: 1
+ };
+ params = L.Util.extend(params, this.options.geocodingQueryParams);
+ this.getJSON(this.options.geocodeUrl, params, cb, context);
+ },
+
+ reverse: function(location, scale, cb, context) {
+ var params = {
+ prox: encodeURIComponent(location.lat) + ',' + encodeURIComponent(location.lng),
+ mode: 'retrieveAddresses',
+ app_id: this.options.app_id,
+ app_code: this.options.app_code,
+ gen: 9,
+ jsonattributes: 1
+ };
+ params = L.Util.extend(params, this.options.reverseQueryParams);
+ this.getJSON(this.options.reverseGeocodeUrl, params, cb, context);
+ },
+
+ getJSON: function(url, params, cb, context) {
+ getJSON(url, params, function(data) {
+ var results = [],
+ loc,
+ latLng,
+ latLngBounds;
+ if (data.response.view && data.response.view.length) {
+ for (var i = 0; i <= data.response.view[0].result.length - 1; i++) {
+ loc = data.response.view[0].result[i].location;
+ latLng = L.latLng(loc.displayPosition.latitude, loc.displayPosition.longitude);
+ latLngBounds = L.latLngBounds(
+ L.latLng(loc.mapView.topLeft.latitude, loc.mapView.topLeft.longitude),
+ L.latLng(loc.mapView.bottomRight.latitude, loc.mapView.bottomRight.longitude)
+ );
+ results[i] = {
+ name: loc.address.label,
+ bbox: latLngBounds,
+ center: latLng
+ };
+ }
+ }
+ cb.call(context, results);
+ });
+ }
+ }),
+
+ factory: function(options) {
+ return new L.Control.Geocoder.HERE(options);
+ }
+};
+
+var Geocoder = L.Util.extend(Control.class, {
+ Nominatim: Nominatim.class,
+ nominatim: Nominatim.factory,
+ Bing: Bing.class,
+ bing: Bing.factory,
+ MapQuest: MapQuest.class,
+ mapQuest: MapQuest.factory,
+ Mapbox: Mapbox.class,
+ mapbox: Mapbox.factory,
+ What3Words: What3Words.class,
+ what3words: What3Words.factory,
+ Google: Google.class,
+ google: Google.factory,
+ Photon: Photon.class,
+ photon: Photon.factory,
+ Mapzen: Mapzen.class,
+ mapzen: Mapzen.factory,
+ ArcGis: ArcGis.class,
+ arcgis: ArcGis.factory,
+ HERE: HERE.class,
+ here: HERE.factory
+});
+
+L.Util.extend(L.Control, {
+ Geocoder: Geocoder,
+ geocoder: Control.factory
+});
+
+return Geocoder;
+
+}(L));
+//# sourceMappingURL=Control.Geocoder.js.map
diff --git a/includes/admin/class-sp-admin-assets.php b/includes/admin/class-sp-admin-assets.php
index 86129ced..e40d0baa 100755
--- a/includes/admin/class-sp-admin-assets.php
+++ b/includes/admin/class-sp-admin-assets.php
@@ -37,7 +37,9 @@ class SP_Admin_Assets {
if ( in_array( $screen->id, sp_get_screen_ids() ) ) {
// Admin styles for SP pages only
wp_enqueue_style( 'jquery-chosen', SP()->plugin_url() . '/assets/css/chosen.css', array(), '1.1.0' );
+ //OpenStreetMaps
wp_enqueue_style( 'leaflet_stylesheet', SP()->plugin_url() . '/assets/css/leaflet.css', array(), '1.4.0' );
+ wp_enqueue_style( 'control-geocoder', SP()->plugin_url() . '/assets/css/Control.Geocoder.css', array() );
wp_enqueue_style( 'wp-color-picker' );
wp_enqueue_style( 'sportspress-admin', SP()->plugin_url() . '/assets/css/admin.css', array(), SP_VERSION );
} elseif ( strpos( $screen->id, 'sportspress-config' ) !== false ) {
@@ -85,12 +87,15 @@ class SP_Admin_Assets {
wp_register_script( 'jquery-fitvids', SP()->plugin_url() . '/assets/js/jquery.fitvids.js', array( 'jquery' ), '1.1', true );
- wp_register_script( 'google-maps', '//tboy.co/maps_js' );
+ //wp_register_script( 'google-maps', '//tboy.co/maps_js' );
+
+ //OpenStreetMaps
wp_register_script( 'leaflet_js', SP()->plugin_url() . '/assets/js/leaflet.js', array(), '1.4.0' );
+ wp_register_script( 'control-geocoder', SP()->plugin_url() . '/assets/js/Control.Geocoder.js', array( 'leaflet_js' ) );
- wp_register_script( 'jquery-locationpicker', SP()->plugin_url() . '/assets/js/locationpicker.jquery.js', array( 'jquery', 'google-maps' ), '0.1.6', true );
+ //wp_register_script( 'jquery-locationpicker', SP()->plugin_url() . '/assets/js/locationpicker.jquery.js', array( 'jquery', 'google-maps' ), '0.1.6', true );
- wp_register_script( 'sportspress-admin-locationpicker', SP()->plugin_url() . '/assets/js/admin/locationpicker.js', array( 'jquery', 'google-maps', 'jquery-locationpicker' ), SP_VERSION, true );
+ //wp_register_script( 'sportspress-admin-locationpicker', SP()->plugin_url() . '/assets/js/admin/locationpicker.js', array( 'jquery', 'google-maps', 'jquery-locationpicker' ), SP_VERSION, true );
wp_register_script( 'sportspress-admin-equationbuilder', SP()->plugin_url() . '/assets/js/admin/equationbuilder.js', array( 'jquery', 'jquery-ui-core', 'jquery-ui-draggable', 'jquery-ui-droppable' ), SP_VERSION, true );
@@ -134,8 +139,9 @@ class SP_Admin_Assets {
// Edit venue pages
if ( in_array( $screen->id, array( 'edit-sp_venue' ) ) ) {
- wp_enqueue_script( 'google-maps' );
+ //wp_enqueue_script( 'google-maps' );
wp_enqueue_script( 'leaflet_js' );
+ wp_enqueue_script( 'control-geocoder' );
wp_enqueue_script( 'jquery-locationpicker' );
wp_enqueue_script( 'sportspress-admin-locationpicker' );
}
diff --git a/includes/admin/class-sp-admin-taxonomies.php b/includes/admin/class-sp-admin-taxonomies.php
index 3e30360e..fdcd1f2c 100644
--- a/includes/admin/class-sp-admin-taxonomies.php
+++ b/includes/admin/class-sp-admin-taxonomies.php
@@ -113,9 +113,9 @@ class SP_Admin_Taxonomies {
endif;
?>
@@ -124,15 +124,62 @@ class SP_Admin_Taxonomies {
var lat = ;
var lon = ;
// initialize map
- map = L.map('mapDiv').setView([lat, lon], 15);
+
+ var map = L.map('mapDiv').setView([lat, lon], 15);
// set map tiles source
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: 'Map data © OpenStreetMap contributors',
maxZoom: 15,
}).addTo(map);
+
+ //L.Control.geocoder().addTo(map);
+
+ var geocoder = L.Control.geocoder({
+ defaultMarkGeocode: true
+ })
+ .on('markgeocode', function(e) {
+ var center = e.geocode.center;
+ document.getElementById('term_meta[sp_latitude]').value = center.lat;
+ document.getElementById('term_meta[sp_longitude]').value = center.lng;
+ })
+ .addTo(map);
+
// add marker to the map
marker = L.marker([lat, lon],{draggable: true, autoPan: true}).addTo(map);
+ //get new coordinates and pass them to the fields
+ marker.on('dragend', function (e) {
+ document.getElementById('term_meta[sp_latitude]').value = marker.getLatLng().lat;
+ document.getElementById('term_meta[sp_longitude]').value = marker.getLatLng().lng;
+ });
+