├── Control.Geocoder.css ├── Control.Geocoder.js ├── FieldtypeLeafletMapMarker.module ├── InputfieldLeafletMapMarker.css ├── InputfieldLeafletMapMarker.js ├── InputfieldLeafletMapMarker.module ├── LeafletMapMarker.php ├── MarkupAddInlineScript.module ├── MarkupLeafletMap.js ├── MarkupLeafletMap.module ├── README.md ├── assets ├── font-awesome-4.6.3 │ ├── css │ │ └── font-awesome.min.css │ └── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 ├── leaflet-awesome-markers │ ├── images │ │ ├── markers-matte.png │ │ ├── markers-matte@2x.png │ │ ├── markers-plain.png │ │ ├── markers-shadow.png │ │ ├── markers-shadow@2x.png │ │ ├── markers-soft.png │ │ └── markers-soft@2x.png │ ├── leaflet.awesome-markers.css │ └── leaflet.awesome-markers.min.js ├── leaflet-markercluster │ ├── MarkerCluster.Default.css │ ├── MarkerCluster.css │ └── leaflet.markercluster.js └── leaflet-providers │ └── leaflet-providers.js ├── examples └── .Contacts.php ├── images ├── 005.png ├── geocoder.png └── throbber.gif └── inc ├── providers.inc └── providers.json /Control.Geocoder.css: -------------------------------------------------------------------------------- 1 | .InputfieldLeafletMapMarker .leaflet-control-geocoder-expanded .leaflet-control-geocoder-form input[type=text] { 2 | width: 256px; 3 | } 4 | 5 | .InputfieldLeafletMapMarker .leaflet-control-geocoder-form input[type=text] { 6 | width: 0; 7 | padding: 0 !important; 8 | border: none !important; 9 | background: none !important; 10 | } 11 | 12 | .leaflet-control-geocoder { 13 | background: white; 14 | box-shadow: 0 1px 7px rgba(0,0,0,0.65); 15 | -webkit-border-radius: 4px; 16 | border-radius: 4px; 17 | line-height: 26px; 18 | overflow: hidden; 19 | } 20 | 21 | .leaflet-touch .leaflet-control-geocoder { 22 | box-shadow: none; 23 | border: 2px solid rgba(0,0,0,0.2); 24 | background-clip: padding-box; 25 | line-height: 30px; 26 | } 27 | 28 | .leaflet-control-geocoder-form { 29 | display: inline; 30 | } 31 | 32 | .leaflet-control-geocoder-form input, .leaflet-control-geocoder-form ul, .leaflet-control-geocoder-error { 33 | border: 0; 34 | color: transparent; 35 | background: white; 36 | } 37 | 38 | .leaflet-control-geocoder-form input { 39 | font-size: 16px; 40 | width: 0; 41 | transition: width 0.125s ease-in; 42 | } 43 | 44 | .leaflet-touch .leaflet-control-geocoder-form input { 45 | font-size: 22px; 46 | } 47 | 48 | .leaflet-control-geocoder-icon { 49 | width: 26px; 50 | height: 26px; 51 | background-image: url(./images/geocoder.png); 52 | background-repeat: no-repeat; 53 | background-position: center; 54 | float: right; 55 | cursor: pointer; 56 | } 57 | 58 | .leaflet-touch .leaflet-control-geocoder-icon { 59 | margin-top: 2px; 60 | width: 30px; 61 | } 62 | 63 | .leaflet-control-geocoder-throbber .leaflet-control-geocoder-icon { 64 | background-image: url(images/throbber.gif); 65 | } 66 | 67 | .leaflet-control-geocoder-expanded input, .leaflet-control-geocoder-error { 68 | width: 226px; 69 | margin: 0 0 0 4px; 70 | padding: 0 0 0 4px; 71 | vertical-align: middle; 72 | color: #000; 73 | } 74 | 75 | .leaflet-control-geocoder-form input:focus { 76 | outline: none; 77 | } 78 | 79 | .leaflet-control-geocoder-form button { 80 | display: none; 81 | } 82 | 83 | .leaflet-control-geocoder-form-no-error { 84 | display: none; 85 | } 86 | 87 | .leaflet-control-geocoder-error { 88 | margin-top: 8px; 89 | display: block; 90 | color: #444; 91 | } 92 | 93 | ul.leaflet-control-geocoder-alternatives { 94 | width: 260px; 95 | overflow: hidden; 96 | text-overflow: ellipsis; 97 | white-space: nowrap; 98 | list-style: none; 99 | padding: 0; 100 | transition: height 0.125s ease-in; 101 | } 102 | 103 | .leaflet-control-geocoder-alternatives-minimized { 104 | width: 0 !important; 105 | height: 0; 106 | overflow: hidden; 107 | margin: 0; 108 | padding: 0; 109 | } 110 | 111 | .leaflet-control-geocoder-alternatives li { 112 | width: 100%; 113 | overflow: hidden; 114 | text-overflow: ellipsis; 115 | border-bottom: 1px solid #eee; 116 | padding: 0; 117 | } 118 | 119 | 120 | .leaflet-control-geocoder-alternatives li:last-child { 121 | border-bottom: none; 122 | } 123 | 124 | .leaflet-control-geocoder-alternatives a { 125 | display: block; 126 | text-decoration: none; 127 | color: black; 128 | padding: 6px 8px 16px 6px; 129 | font-size: 14px; 130 | line-height: 1; 131 | font-weight: bold; 132 | } 133 | 134 | .leaflet-touch .leaflet-control-geocoder-alternatives a { 135 | font-size: 18px; 136 | } 137 | 138 | .leaflet-control-geocoder-alternatives a:hover, .leaflet-control-geocoder-selected { 139 | background-color: #ddd; 140 | } 141 | 142 | .leaflet-control-geocoder-address-detail { 143 | font-size: 12px; 144 | font-weight: normal; 145 | } 146 | 147 | .leaflet-control-geocoder-address-context { 148 | color: #666; 149 | font-size: 12px; 150 | font-weight: lighter; 151 | } 152 | -------------------------------------------------------------------------------- /Control.Geocoder.js: -------------------------------------------------------------------------------- 1 | (function (factory) { 2 | // Packaging/modules magic dance 3 | var L; 4 | if (typeof define === 'function' && define.amd) { 5 | // AMD 6 | define(['leaflet'], factory); 7 | } else if (typeof module !== 'undefined') { 8 | // Node/CommonJS 9 | L = require('leaflet'); 10 | module.exports = factory(L); 11 | } else { 12 | // Browser globals 13 | if (typeof window.L === 'undefined') 14 | throw 'Leaflet must be loaded first'; 15 | factory(window.L); 16 | } 17 | }(function (L) { 18 | 'use strict'; 19 | L.Control.Geocoder = L.Control.extend({ 20 | options: { 21 | showResultIcons: false, 22 | collapsed: true, 23 | expand: 'click', 24 | position: 'topright', 25 | placeholder: 'Search...', 26 | errorMessage: 'Nothing found.' 27 | }, 28 | 29 | _callbackId: 0, 30 | 31 | initialize: function (options) { 32 | L.Util.setOptions(this, options); 33 | if (!this.options.geocoder) { 34 | this.options.geocoder = new L.Control.Geocoder.Nominatim(); 35 | } 36 | }, 37 | 38 | onAdd: function (map) { 39 | var className = 'leaflet-control-geocoder', 40 | container = L.DomUtil.create('div', className), 41 | icon = L.DomUtil.create('div', 'leaflet-control-geocoder-icon', container), 42 | form = this._form = L.DomUtil.create('form', className + '-form', container), 43 | input; 44 | 45 | this._map = map; 46 | this._container = container; 47 | input = this._input = L.DomUtil.create('input'); 48 | input.type = 'text'; 49 | input.placeholder = this.options.placeholder; 50 | 51 | L.DomEvent.addListener(input, 'keydown', this._keydown, this); 52 | //L.DomEvent.addListener(input, 'onpaste', this._clearResults, this); 53 | //L.DomEvent.addListener(input, 'oninput', this._clearResults, this); 54 | 55 | this._errorElement = document.createElement('div'); 56 | this._errorElement.className = className + '-form-no-error'; 57 | this._errorElement.innerHTML = this.options.errorMessage; 58 | 59 | this._alts = L.DomUtil.create('ul', className + '-alternatives leaflet-control-geocoder-alternatives-minimized'); 60 | 61 | form.appendChild(input); 62 | form.appendChild(this._errorElement); 63 | container.appendChild(this._alts); 64 | 65 | L.DomEvent.addListener(form, 'submit', this._geocode, this); 66 | 67 | if (this.options.collapsed) { 68 | if (this.options.expand === 'click') { 69 | L.DomEvent.addListener(icon, 'click', function(e) { 70 | // TODO: touch 71 | if (e.button === 0 && e.detail !== 2) { 72 | this._toggle(); 73 | } 74 | }, this); 75 | } else { 76 | L.DomEvent.addListener(icon, 'mouseover', this._expand, this); 77 | L.DomEvent.addListener(icon, 'mouseout', this._collapse, this); 78 | this._map.on('movestart', this._collapse, this); 79 | } 80 | } else { 81 | this._expand(); 82 | } 83 | 84 | L.DomEvent.disableClickPropagation(container); 85 | 86 | return container; 87 | }, 88 | 89 | _geocodeResult: function (results) { 90 | L.DomUtil.removeClass(this._container, 'leaflet-control-geocoder-throbber'); 91 | if (results.length === 1) { 92 | this._geocodeResultSelected(results[0]); 93 | } else if (results.length > 0) { 94 | this._alts.innerHTML = ''; 95 | this._results = results; 96 | L.DomUtil.removeClass(this._alts, 'leaflet-control-geocoder-alternatives-minimized'); 97 | for (var i = 0; i < results.length; i++) { 98 | this._alts.appendChild(this._createAlt(results[i], i)); 99 | } 100 | } else { 101 | L.DomUtil.addClass(this._errorElement, 'leaflet-control-geocoder-error'); 102 | } 103 | }, 104 | 105 | markGeocode: function(result) { 106 | this._map.fitBounds(result.bbox); 107 | 108 | if (this._geocodeMarker) { 109 | this._map.removeLayer(this._geocodeMarker); 110 | } 111 | 112 | this._geocodeMarker = new L.Marker(result.center) 113 | .bindPopup(result.html || result.name) 114 | .addTo(this._map) 115 | .openPopup(); 116 | 117 | return this; 118 | }, 119 | 120 | _geocode: function(event) { 121 | L.DomEvent.preventDefault(event); 122 | 123 | L.DomUtil.addClass(this._container, 'leaflet-control-geocoder-throbber'); 124 | this._clearResults(); 125 | this.options.geocoder.geocode(this._input.value, this._geocodeResult, this); 126 | 127 | return false; 128 | }, 129 | 130 | _geocodeResultSelected: function(result) { 131 | if (this.options.collapsed) { 132 | this._collapse(); 133 | } else { 134 | this._clearResults(); 135 | } 136 | this.markGeocode(result); 137 | }, 138 | 139 | _toggle: function() { 140 | if (this._container.className.indexOf('leaflet-control-geocoder-expanded') >= 0) { 141 | this._collapse(); 142 | } else { 143 | this._expand(); 144 | } 145 | }, 146 | 147 | _expand: function () { 148 | L.DomUtil.addClass(this._container, 'leaflet-control-geocoder-expanded'); 149 | this._input.select(); 150 | }, 151 | 152 | _collapse: function () { 153 | this._container.className = this._container.className.replace(' leaflet-control-geocoder-expanded', ''); 154 | L.DomUtil.addClass(this._alts, 'leaflet-control-geocoder-alternatives-minimized'); 155 | L.DomUtil.removeClass(this._errorElement, 'leaflet-control-geocoder-error'); 156 | }, 157 | 158 | _clearResults: function () { 159 | L.DomUtil.addClass(this._alts, 'leaflet-control-geocoder-alternatives-minimized'); 160 | this._selection = null; 161 | L.DomUtil.removeClass(this._errorElement, 'leaflet-control-geocoder-error'); 162 | }, 163 | 164 | _createAlt: function(result, index) { 165 | var li = document.createElement('li'), 166 | a = L.DomUtil.create('a', '', li), 167 | icon = this.options.showResultIcons && result.icon ? L.DomUtil.create('img', '', a) : null, 168 | text = result.html ? undefined : document.createTextNode(result.name); 169 | 170 | if (icon) { 171 | icon.src = result.icon; 172 | } 173 | 174 | a.href = '#'; 175 | a.setAttribute('data-result-index', index); 176 | 177 | if (result.html) { 178 | a.innerHTML = result.html; 179 | } else { 180 | a.appendChild(text); 181 | } 182 | 183 | L.DomEvent.addListener(li, 'click', function clickHandler(e) { 184 | L.DomEvent.preventDefault(e); 185 | this._geocodeResultSelected(result); 186 | }, this); 187 | 188 | return li; 189 | }, 190 | 191 | _keydown: function(e) { 192 | var _this = this, 193 | select = function select(dir) { 194 | if (_this._selection) { 195 | L.DomUtil.removeClass(_this._selection.firstChild, 'leaflet-control-geocoder-selected'); 196 | _this._selection = _this._selection[dir > 0 ? 'nextSibling' : 'previousSibling']; 197 | } 198 | if (!_this._selection) { 199 | _this._selection = _this._alts[dir > 0 ? 'firstChild' : 'lastChild']; 200 | } 201 | 202 | if (_this._selection) { 203 | L.DomUtil.addClass(_this._selection.firstChild, 'leaflet-control-geocoder-selected'); 204 | } 205 | }; 206 | 207 | switch (e.keyCode) { 208 | // Escape 209 | case 27: 210 | if (this.options.collapsed) { 211 | this._collapse(); 212 | } 213 | break; 214 | // Up 215 | case 38: 216 | select(-1); 217 | L.DomEvent.preventDefault(e); 218 | break; 219 | // Up 220 | case 40: 221 | select(1); 222 | L.DomEvent.preventDefault(e); 223 | break; 224 | // Enter 225 | case 13: 226 | if (this._selection) { 227 | var index = parseInt(this._selection.firstChild.getAttribute('data-result-index'), 10); 228 | this._geocodeResultSelected(this._results[index]); 229 | this._clearResults(); 230 | L.DomEvent.preventDefault(e); 231 | } 232 | } 233 | return true; 234 | } 235 | }); 236 | 237 | L.Control.geocoder = function(id, options) { 238 | return new L.Control.Geocoder(id, options); 239 | }; 240 | 241 | L.Control.Geocoder.callbackId = 0; 242 | L.Control.Geocoder.jsonp = function(url, params, callback, context, jsonpParam) { 243 | var callbackId = '_l_geocoder_' + (L.Control.Geocoder.callbackId++); 244 | params[jsonpParam || 'callback'] = callbackId; 245 | window[callbackId] = L.Util.bind(callback, context); 246 | var script = document.createElement('script'); 247 | script.type = 'text/javascript'; 248 | script.src = url + L.Util.getParamString(params); 249 | script.id = callbackId; 250 | document.getElementsByTagName('head')[0].appendChild(script); 251 | }; 252 | L.Control.Geocoder.getJSON = function(url, params, callback) { 253 | var xmlHttp = new XMLHttpRequest(); 254 | xmlHttp.open( "GET", url + L.Util.getParamString(params), true); 255 | xmlHttp.send(null); 256 | xmlHttp.onreadystatechange = function () { 257 | if (xmlHttp.readyState != 4) return; 258 | if (xmlHttp.status != 200 && req.status != 304) return; 259 | callback(JSON.parse(xmlHttp.response)); 260 | }; 261 | }; 262 | 263 | L.Control.Geocoder.template = function (str, data, htmlEscape) { 264 | return str.replace(/\{ *([\w_]+) *\}/g, function (str, key) { 265 | var value = data[key]; 266 | if (value === undefined) { 267 | value = ''; 268 | } else if (typeof value === 'function') { 269 | value = value(data); 270 | } 271 | return L.Control.Geocoder.htmlEscape(value); 272 | }); 273 | }; 274 | 275 | // Adapted from handlebars.js 276 | // https://github.com/wycats/handlebars.js/ 277 | L.Control.Geocoder.htmlEscape = (function() { 278 | var badChars = /[&<>"'`]/g; 279 | var possible = /[&<>"'`]/; 280 | var escape = { 281 | '&': '&', 282 | '<': '<', 283 | '>': '>', 284 | '"': '"', 285 | '\'': ''', 286 | '`': '`' 287 | }; 288 | 289 | function escapeChar(chr) { 290 | return escape[chr]; 291 | } 292 | 293 | return function(string) { 294 | if (string == null) { 295 | return ''; 296 | } else if (!string) { 297 | return string + ''; 298 | } 299 | 300 | // Force a string conversion as this will be done by the append regardless and 301 | // the regex test will do this transparently behind the scenes, causing issues if 302 | // an object's to string has escaped characters in it. 303 | string = '' + string; 304 | 305 | if (!possible.test(string)) { 306 | return string; 307 | } 308 | return string.replace(badChars, escapeChar); 309 | }; 310 | })(); 311 | 312 | L.Control.Geocoder.Nominatim = L.Class.extend({ 313 | options: { 314 | serviceUrl: '//nominatim.openstreetmap.org/', 315 | geocodingQueryParams: {}, 316 | reverseQueryParams: {}, 317 | htmlTemplate: function(r) { 318 | var a = r.address, 319 | parts = []; 320 | if (a.road || a.building) { 321 | parts.push('{building} {road} {house_number}'); 322 | } 323 | 324 | if (a.city || a.town || a.village) { 325 | parts.push('{postcode} {city} {town} {village}'); 327 | } 328 | 329 | if (a.state || a.country) { 330 | parts.push('{state} {country}'); 332 | } 333 | 334 | return L.Control.Geocoder.template(parts.join('
'), a, true); 335 | } 336 | }, 337 | 338 | initialize: function(options) { 339 | L.Util.setOptions(this, options); 340 | }, 341 | 342 | geocode: function(query, cb, context) { 343 | L.Control.Geocoder.jsonp(this.options.serviceUrl + 'search/', L.extend({ 344 | q: query, 345 | limit: 5, 346 | format: 'json', 347 | addressdetails: 1 348 | }, this.options.geocodingQueryParams), 349 | function(data) { 350 | var results = []; 351 | for (var i = data.length - 1; i >= 0; i--) { 352 | var bbox = data[i].boundingbox; 353 | for (var j = 0; j < 4; j++) bbox[j] = parseFloat(bbox[j]); 354 | results[i] = { 355 | icon: data[i].icon, 356 | name: data[i].display_name, 357 | html: this.options.htmlTemplate ? 358 | this.options.htmlTemplate(data[i]) 359 | : undefined, 360 | bbox: L.latLngBounds([bbox[0], bbox[2]], [bbox[1], bbox[3]]), 361 | center: L.latLng(data[i].lat, data[i].lon), 362 | properties: data[i] 363 | }; 364 | } 365 | cb.call(context, results); 366 | }, this, 'json_callback'); 367 | }, 368 | 369 | reverse: function(location, scale, cb, context) { 370 | L.Control.Geocoder.jsonp(this.options.serviceUrl + 'reverse/', L.extend({ 371 | lat: location.lat, 372 | lon: location.lng, 373 | zoom: Math.round(Math.log(scale / 256) / Math.log(2)), 374 | addressdetails: 1, 375 | format: 'json' 376 | }, this.options.reverseQueryParams), function(data) { 377 | var result = [], 378 | loc; 379 | 380 | if (data && data.lat && data.lon) { 381 | loc = L.latLng(data.lat, data.lon); 382 | result.push({ 383 | name: data.display_name, 384 | html: this.options.htmlTemplate ? 385 | this.options.htmlTemplate(data) 386 | : undefined, 387 | center: loc, 388 | bounds: L.latLngBounds(loc, loc), 389 | properties: data 390 | }); 391 | } 392 | 393 | cb.call(context, result); 394 | }, this, 'json_callback'); 395 | } 396 | }); 397 | 398 | L.Control.Geocoder.nominatim = function(options) { 399 | return new L.Control.Geocoder.Nominatim(options); 400 | }; 401 | 402 | L.Control.Geocoder.Bing = L.Class.extend({ 403 | initialize: function(key) { 404 | this.key = key; 405 | }, 406 | 407 | geocode : function (query, cb, context) { 408 | L.Control.Geocoder.jsonp('//dev.virtualearth.net/REST/v1/Locations', { 409 | query: query, 410 | key : this.key 411 | }, function(data) { 412 | var results = []; 413 | for (var i = data.resourceSets[0].resources.length - 1; i >= 0; i--) { 414 | var resource = data.resourceSets[0].resources[i], 415 | bbox = resource.bbox; 416 | results[i] = { 417 | name: resource.name, 418 | bbox: L.latLngBounds([bbox[0], bbox[1]], [bbox[2], bbox[3]]), 419 | center: L.latLng(resource.point.coordinates) 420 | }; 421 | } 422 | cb.call(context, results); 423 | }, this, 'jsonp'); 424 | }, 425 | 426 | reverse: function(location, scale, cb, context) { 427 | L.Control.Geocoder.jsonp('//dev.virtualearth.net/REST/v1/Locations/' + location.lat + ',' + location.lng, { 428 | key : this.key 429 | }, function(data) { 430 | var results = []; 431 | for (var i = data.resourceSets[0].resources.length - 1; i >= 0; i--) { 432 | var resource = data.resourceSets[0].resources[i], 433 | bbox = resource.bbox; 434 | results[i] = { 435 | name: resource.name, 436 | bbox: L.latLngBounds([bbox[0], bbox[1]], [bbox[2], bbox[3]]), 437 | center: L.latLng(resource.point.coordinates) 438 | }; 439 | } 440 | cb.call(context, results); 441 | }, this, 'jsonp'); 442 | } 443 | }); 444 | 445 | L.Control.Geocoder.bing = function(key) { 446 | return new L.Control.Geocoder.Bing(key); 447 | }; 448 | 449 | L.Control.Geocoder.RaveGeo = L.Class.extend({ 450 | options: { 451 | querySuffix: '', 452 | deepSearch: true, 453 | wordBased: false 454 | }, 455 | 456 | jsonp: function(params, callback, context) { 457 | var callbackId = '_l_geocoder_' + (L.Control.Geocoder.callbackId++), 458 | paramParts = []; 459 | params.prepend = callbackId + '('; 460 | params.append = ')'; 461 | for (var p in params) { 462 | paramParts.push(p + '=' + escape(params[p])); 463 | } 464 | 465 | window[callbackId] = L.Util.bind(callback, context); 466 | var script = document.createElement('script'); 467 | script.type = 'text/javascript'; 468 | script.src = this._serviceUrl + '?' + paramParts.join('&'); 469 | script.id = callbackId; 470 | document.getElementsByTagName('head')[0].appendChild(script); 471 | }, 472 | 473 | initialize: function(serviceUrl, scheme, options) { 474 | L.Util.setOptions(this, options); 475 | 476 | this._serviceUrl = serviceUrl; 477 | this._scheme = scheme; 478 | }, 479 | 480 | geocode: function(query, cb, context) { 481 | L.Control.Geocoder.jsonp(this._serviceUrl, { 482 | address: query + this.options.querySuffix, 483 | scheme: this._scheme, 484 | outputFormat: 'jsonp', 485 | deepSearch: this.options.deepSearch, 486 | wordBased: this.options.wordBased 487 | }, function(data) { 488 | var results = []; 489 | for (var i = data.length - 1; i >= 0; i--) { 490 | var r = data[i], 491 | c = L.latLng(r.y, r.x); 492 | results[i] = { 493 | name: r.address, 494 | bbox: L.latLngBounds([c]), 495 | center: c 496 | }; 497 | } 498 | cb.call(context, results); 499 | }, this); 500 | } 501 | }); 502 | 503 | L.Control.Geocoder.raveGeo = function(serviceUrl, scheme, options) { 504 | return new L.Control.Geocoder.RaveGeo(serviceUrl, scheme, options); 505 | }; 506 | 507 | L.Control.Geocoder.MapQuest = L.Class.extend({ 508 | initialize: function(key) { 509 | // MapQuest seems to provide URI encoded API keys, 510 | // so to avoid encoding them twice, we decode them here 511 | this._key = decodeURIComponent(key); 512 | }, 513 | 514 | _formatName: function() { 515 | var r = [], 516 | i; 517 | for (i = 0; i < arguments.length; i++) { 518 | if (arguments[i]) { 519 | r.push(arguments[i]); 520 | } 521 | } 522 | 523 | return r.join(', '); 524 | }, 525 | 526 | geocode: function(query, cb, context) { 527 | L.Control.Geocoder.jsonp('//www.mapquestapi.com/geocoding/v1/address', { 528 | key: this._key, 529 | location: query, 530 | limit: 5, 531 | outFormat: 'json' 532 | }, function(data) { 533 | var results = [], 534 | loc, 535 | latLng; 536 | if (data.results && data.results[0].locations) { 537 | for (var i = data.results[0].locations.length - 1; i >= 0; i--) { 538 | loc = data.results[0].locations[i]; 539 | latLng = L.latLng(loc.latLng); 540 | results[i] = { 541 | name: this._formatName(loc.street, loc.adminArea4, loc.adminArea3, loc.adminArea1), 542 | bbox: L.latLngBounds(latLng, latLng), 543 | center: latLng 544 | }; 545 | } 546 | } 547 | 548 | cb.call(context, results); 549 | }, this); 550 | }, 551 | 552 | reverse: function(location, scale, cb, context) { 553 | L.Control.Geocoder.jsonp('//www.mapquestapi.com/geocoding/v1/reverse', { 554 | key: this._key, 555 | location: location.lat + ',' + location.lng, 556 | outputFormat: 'json' 557 | }, function(data) { 558 | var results = [], 559 | loc, 560 | latLng; 561 | if (data.results && data.results[0].locations) { 562 | for (var i = data.results[0].locations.length - 1; i >= 0; i--) { 563 | loc = data.results[0].locations[i]; 564 | latLng = L.latLng(loc.latLng); 565 | results[i] = { 566 | name: this._formatName(loc.street, loc.adminArea4, loc.adminArea3, loc.adminArea1), 567 | bbox: L.latLngBounds(latLng, latLng), 568 | center: latLng 569 | }; 570 | } 571 | } 572 | 573 | cb.call(context, results); 574 | }, this); 575 | } 576 | }); 577 | 578 | L.Control.Geocoder.mapQuest = function(key) { 579 | return new L.Control.Geocoder.MapQuest(key); 580 | }; 581 | 582 | L.Control.Geocoder.Mapbox = L.Class.extend({ 583 | options: { 584 | service_url: 'https://api.tiles.mapbox.com/v4/geocode/mapbox.places-v1/' 585 | }, 586 | 587 | initialize: function(access_token) { 588 | this._access_token = access_token; 589 | }, 590 | 591 | geocode: function(query, cb, context) { 592 | L.Control.Geocoder.getJSON(this.options.service_url + encodeURIComponent(query) + '.json', { 593 | access_token: this._access_token, 594 | }, function(data) { 595 | var results = [], 596 | loc, 597 | latLng, 598 | latLngBounds; 599 | if (data.features && data.features.length) { 600 | for (var i = 0; i <= data.features.length - 1; i++) { 601 | loc = data.features[i]; 602 | latLng = L.latLng(loc.center.reverse()); 603 | if(loc.hasOwnProperty('bbox')) 604 | { 605 | latLngBounds = L.latLngBounds(L.latLng(loc.bbox.slice(0, 2).reverse()), L.latLng(loc.bbox.slice(2, 4).reverse())); 606 | } 607 | else 608 | { 609 | latLngBounds = L.latLngBounds(latLng, latLng); 610 | } 611 | results[i] = { 612 | name: loc.place_name, 613 | bbox: latLngBounds, 614 | center: latLng 615 | }; 616 | } 617 | } 618 | 619 | cb.call(context, results); 620 | }); 621 | }, 622 | 623 | reverse: function(location, scale, cb, context) { 624 | L.Control.Geocoder.getJSON(this.options.service_url + encodeURIComponent(location.lng) + ',' + encodeURIComponent(location.lat) + '.json', { 625 | access_token: this._access_token, 626 | }, function(data) { 627 | var results = [], 628 | loc, 629 | latLng, 630 | latLngBounds; 631 | if (data.features && data.features.length) { 632 | for (var i = 0; i <= data.features.length - 1; i++) { 633 | loc = data.features[i]; 634 | latLng = L.latLng(loc.center.reverse()); 635 | if(loc.hasOwnProperty('bbox')) 636 | { 637 | latLngBounds = L.latLngBounds(L.latLng(loc.bbox.slice(0, 2).reverse()), L.latLng(loc.bbox.slice(2, 4).reverse())); 638 | } 639 | else 640 | { 641 | latLngBounds = L.latLngBounds(latLng, latLng); 642 | } 643 | results[i] = { 644 | name: loc.place_name, 645 | bbox: latLngBounds, 646 | center: latLng 647 | }; 648 | } 649 | } 650 | 651 | cb.call(context, results); 652 | }); 653 | } 654 | }); 655 | 656 | L.Control.Geocoder.mapbox = function(access_token) { 657 | return new L.Control.Geocoder.Mapbox(access_token); 658 | }; 659 | 660 | L.Control.Geocoder.Google = L.Class.extend({ 661 | options: { 662 | service_url: 'https://maps.googleapis.com/maps/api/geocode/json' 663 | }, 664 | 665 | initialize: function(key) { 666 | this._key = key; 667 | }, 668 | 669 | geocode: function(query, cb, context) { 670 | var params = { 671 | address: query, 672 | }; 673 | if(this._key && this._key.length) 674 | { 675 | params['key'] = this._key 676 | } 677 | 678 | L.Control.Geocoder.getJSON(this.options.service_url, params, function(data) { 679 | var results = [], 680 | loc, 681 | latLng, 682 | latLngBounds; 683 | if (data.results && data.results.length) { 684 | for (var i = 0; i <= data.results.length - 1; i++) { 685 | loc = data.results[i]; 686 | latLng = L.latLng(loc.geometry.location); 687 | latLngBounds = L.latLngBounds(L.latLng(loc.geometry.viewport.northeast), L.latLng(loc.geometry.viewport.southwest)); 688 | results[i] = { 689 | name: loc.formatted_address, 690 | bbox: latLngBounds, 691 | center: latLng 692 | }; 693 | } 694 | } 695 | 696 | cb.call(context, results); 697 | }); 698 | }, 699 | 700 | reverse: function(location, scale, cb, context) { 701 | var params = { 702 | latlng: encodeURIComponent(location.lat) + ',' + encodeURIComponent(location.lng) 703 | }; 704 | if(this._key && this._key.length) 705 | { 706 | params['key'] = this._key 707 | } 708 | L.Control.Geocoder.getJSON(this.options.service_url, params, function(data) { 709 | var results = [], 710 | loc, 711 | latLng, 712 | latLngBounds; 713 | if (data.results && data.results.length) { 714 | for (var i = 0; i <= data.results.length - 1; i++) { 715 | loc = data.results[i]; 716 | latLng = L.latLng(loc.geometry.location); 717 | latLngBounds = L.latLngBounds(L.latLng(loc.geometry.viewport.northeast), L.latLng(loc.geometry.viewport.southwest)); 718 | results[i] = { 719 | name: loc.formatted_address, 720 | bbox: latLngBounds, 721 | center: latLng 722 | }; 723 | } 724 | } 725 | 726 | cb.call(context, results); 727 | }); 728 | } 729 | }); 730 | 731 | L.Control.Geocoder.google = function(key) { 732 | return new L.Control.Geocoder.Google(key); 733 | }; 734 | return L.Control.Geocoder; 735 | })); 736 | -------------------------------------------------------------------------------- /FieldtypeLeafletMapMarker.module: -------------------------------------------------------------------------------- 1 | 'Leaflet Map Marker', 26 | 'version' => '3.0.4', 27 | 'summary' => 'Field that stores an address with latitude and longitude coordinates and has built-in geocoding capability with Leaflet Maps API.', 28 | 'installs' => 'InputfieldLeafletMapMarker', 29 | 'icon' => 'map-marker', 30 | 'requires' => 'ProcessWire>=3.0.0', 31 | ); 32 | } 33 | 34 | /** 35 | * Include our LeafletMapMarker class, which serves as the value for fields of type FieldtypeLeafletMapMarker 36 | * 37 | */ 38 | public function __construct() { 39 | require_once(dirname(__FILE__) . '/LeafletMapMarker.php'); 40 | } 41 | 42 | /** 43 | * Return the Inputfield required by this Fieldtype 44 | * 45 | */ 46 | public function getInputfield(Page $page, Field $field) { 47 | $inputfield = $this->modules->get('InputfieldLeafletMapMarker'); 48 | return $inputfield; 49 | } 50 | 51 | /** 52 | * Return all compatible Fieldtypes 53 | * 54 | */ 55 | public function ___getCompatibleFieldtypes(Field $field) { 56 | // there are no other fieldtypes compatible with this one 57 | return null; 58 | } 59 | 60 | /** 61 | * Sanitize value for runtime 62 | * 63 | */ 64 | public function sanitizeValue(Page $page, Field $field, $value) { 65 | 66 | // if it's not a LeafletMapMarker, then just return a blank LeafletMapMarker 67 | if(!$value instanceof LeafletMapMarker) $value = $this->getBlankValue($page, $field); 68 | 69 | // if the address changed, tell the $page that this field changed 70 | if($value->isChanged('address')) $page->trackChange($field->name); 71 | 72 | return $value; 73 | } 74 | 75 | /** 76 | * Get a blank value used by this fieldtype 77 | * 78 | */ 79 | public function getBlankValue(Page $page, Field $field) { 80 | return new LeafletMapMarker(); 81 | } 82 | 83 | /** 84 | * Given a raw value (value as stored in DB), return the value as it would appear in a Page object 85 | * 86 | * @param Page $page 87 | * @param Field $field 88 | * @param string|int|array $value 89 | * @return string|int|array|object $value 90 | * 91 | */ 92 | public function ___wakeupValue(Page $page, Field $field, $value) { 93 | 94 | // get a blank LeafletMapMarker instance 95 | $marker = $this->getBlankValue($page, $field); 96 | 97 | if("$value[lat]" === "0") $value['lat'] = ''; 98 | if("$value[lng]" === "0") $value['lng'] = ''; 99 | 100 | // populate the marker 101 | $marker->address = $value['data']; 102 | $marker->lat = $value['lat']; 103 | $marker->lng = $value['lng']; 104 | $marker->status = $value['status']; 105 | $marker->zoom = $value['zoom']; 106 | $marker->raw = $value['raw']; 107 | // $marker->provider = $value['provider']; 108 | $marker->setTrackChanges(true); 109 | 110 | return $marker; 111 | } 112 | 113 | /** 114 | * Given an 'awake' value, as set by wakeupValue, convert the value back to a basic type for storage in DB. 115 | * 116 | * @param Page $page 117 | * @param Field $field 118 | * @param string|int|array|object $value 119 | * @return string|int 120 | * 121 | */ 122 | public function ___sleepValue(Page $page, Field $field, $value) { 123 | 124 | $marker = $value; 125 | 126 | if(!$marker instanceof LeafletMapMarker) 127 | throw new WireException("Expecting an instance of LeafletMapMarker"); 128 | 129 | // if the address was changed, then force it to geocode the new address 130 | if($marker->isChanged('address') && $marker->address && $marker->status != LeafletMapMarker::statusNoGeocode) $marker->geocode(); 131 | 132 | $sleepValue = array( 133 | 'data' => $marker->address, 134 | 'lat' => strlen($marker->lat) ? $marker->lat : 0, 135 | 'lng' => strlen($marker->lng) ? $marker->lng : 0, 136 | 'status' => $marker->status, 137 | 'zoom' => $marker->zoom, 138 | 'raw' => $marker->raw/*, 139 | 'provider' => $marker->provider*/ 140 | ); 141 | 142 | return $sleepValue; 143 | } 144 | 145 | 146 | /** 147 | * Return the database schema in specified format 148 | * 149 | */ 150 | public function getDatabaseSchema(Field $field) { 151 | 152 | // get the default schema 153 | $schema = parent::getDatabaseSchema($field); 154 | 155 | $schema['data'] = "VARCHAR(255) NOT NULL DEFAULT ''"; // address (reusing the 'data' field from default schema) 156 | $schema['lat'] = "FLOAT(10,6) NOT NULL DEFAULT 0"; // latitude 157 | $schema['lng'] = "FLOAT(10,6) NOT NULL DEFAULT 0"; // longitude 158 | $schema['status'] = "TINYINT NOT NULL DEFAULT 0"; // geocode status 159 | $schema['zoom'] = "TINYINT NOT NULL DEFAULT 0"; // zoom level (schema v1) 160 | $schema['raw'] = "TEXT NOT NULL DEFAULT ''"; // raw google geocode data 161 | // $schema['provider'] = "VARCHAR(255) NOT NULL DEFAULT ''"; 162 | 163 | $schema['keys']['latlng'] = "KEY latlng (lat, lng)"; // keep an index of lat/lng 164 | $schema['keys']['data'] = 'FULLTEXT KEY `data` (`data`)'; 165 | $schema['keys']['zoom'] = "KEY zoom (zoom)"; 166 | 167 | if($field->id) $this->updateDatabaseSchema($field, $schema); 168 | 169 | return $schema; 170 | } 171 | 172 | /** 173 | * Update the DB schema, if necessary 174 | * 175 | */ 176 | protected function updateDatabaseSchema(Field $field, array $schema) { 177 | // increment the requiredVersion number with each new database schema change 178 | $requiredVersion = 2; 179 | $schemaVersion = (int) $field->schemaVersion; 180 | 181 | if($schemaVersion >= $requiredVersion) { 182 | // already up-to-date 183 | return; 184 | } 185 | 186 | //PDO update by Ryan 187 | if($schemaVersion == 0) { 188 | // update schema to v1: add 'zoom' column 189 | $schemaVersion = 1; 190 | $database = $this->wire('database'); 191 | $table = $database->escapeTable($field->getTable()); 192 | $query = $database->prepare("SHOW TABLES LIKE '$table'"); 193 | $query->execute(); 194 | $row = $query->fetch(\PDO::FETCH_NUM); 195 | $query->closeCursor(); 196 | if(!empty($row)) { 197 | $query = $database->prepare("SHOW COLUMNS FROM `$table` WHERE field='zoom'"); 198 | $query->execute(); 199 | if(!$query->rowCount()) try { 200 | $database->exec("ALTER TABLE `$table` ADD zoom $schema[zoom] AFTER status"); 201 | $this->message("Added 'zoom' column to '$field->table'"); 202 | } catch(Exception $e) { 203 | $this->error($e->getMessage()); 204 | } 205 | } 206 | } 207 | 208 | //PDO update by Patman15 209 | if($schemaVersion == 1) { 210 | // update schema to v2: add 'raw' column 211 | $schemaVersion = 2; 212 | $database = $this->wire('database'); 213 | $table = $database->escapeTable($field->getTable()); 214 | $query = $database->prepare("SHOW TABLES LIKE '$table'"); 215 | $query->execute(); 216 | $row = $query->fetch(\PDO::FETCH_NUM); 217 | $query->closeCursor(); 218 | if(!empty($row)) { 219 | $query = $database->prepare("SHOW COLUMNS FROM `$table` WHERE field='raw'"); 220 | $query->execute(); 221 | if(!$query->rowCount()) try { 222 | $database->exec("ALTER TABLE `$table` ADD raw $schema[raw] AFTER zoom"); 223 | $this->message("Added 'raw' column to '$field->table'"); 224 | } catch(Exception $e) { 225 | $this->error($e->getMessage()); 226 | } 227 | } 228 | } 229 | 230 | $field->set('schemaVersion', $schemaVersion); 231 | $field->save(); 232 | 233 | } 234 | 235 | /** 236 | * Match values for PageFinder 237 | * 238 | */ 239 | public function getMatchQuery($query, $table, $subfield, $operator, $value) { 240 | if(!$subfield || $subfield == 'address') $subfield = 'data'; 241 | if($subfield != 'data' || wire('database')->isOperator($operator)) { 242 | // if dealing with something other than address, or operator is native to SQL, 243 | // then let Fieldtype::getMatchQuery handle it instead 244 | return parent::getMatchQuery($query, $table, $subfield, $operator, $value); 245 | } 246 | // if we get here, then we're performing either %= (LIKE and variations) or *= (FULLTEXT and variations) 247 | $ft = new DatabaseQuerySelectFulltext($query); 248 | $ft->match($table, $subfield, $operator, $value); 249 | return $query; 250 | } 251 | 252 | /** 253 | * Perform installation: check that this fieldtype can be used with geocoding and warn them if not. 254 | * 255 | */ 256 | public function ___install() { 257 | if(!ini_get('allow_url_fopen')) { 258 | $this->error("Some parts of LeafletMapMarker geocoding will not work because 'allow_url_fopen' is denied in your PHP settings."); 259 | } 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /InputfieldLeafletMapMarker.css: -------------------------------------------------------------------------------- 1 | .InputfieldLeafletMapMarker input[type=number], 2 | .InputfieldLeafletMapMarker input[type=text] { 3 | width: 99.5%; 4 | } 5 | 6 | input[readonly="readonly"], input[readonly] { 7 | color: grey !important; 8 | } 9 | 10 | .InputfieldLeafletMapMarkerToggle span { 11 | display: none; 12 | } 13 | 14 | .InputfieldLeafletMapMarkerAddress { 15 | float: left; 16 | width: 70%; 17 | padding-right: 2%; 18 | } 19 | 20 | .InputfieldLeafletMapMarkerToggle { 21 | float: left; 22 | width: 28%; 23 | } 24 | 25 | .InputfieldLeafletMapMarkerLat, 26 | .InputfieldLeafletMapMarkerLng { 27 | width: 42%; 28 | float: left; 29 | padding-right: 2%; 30 | } 31 | 32 | .InputfieldLeafletMapMarkerZoom { 33 | float: left; 34 | width: 10%; 35 | } 36 | 37 | .InputfieldLeafletMapMarker .notes { 38 | clear: both; 39 | } 40 | 41 | .InputfieldLeafletMapMarkerMap { 42 | width: 100%; 43 | height: 300px; 44 | } 45 | 46 | @media only screen and (min-width: 768px) { 47 | 48 | .InputfieldLeafletMapMarkerAddress { 49 | width: 38%; 50 | padding-right: 1%; 51 | } 52 | 53 | .InputfieldLeafletMapMarkerToggle { 54 | width: 2%; 55 | padding-right: 0.5%; 56 | position: relative; 57 | } 58 | 59 | .InputfieldLeafletMapMarkerToggle strong { 60 | /* hide geocode label */ 61 | display: none; 62 | } 63 | 64 | .InputfieldLeafletMapMarkerLat, 65 | .InputfieldLeafletMapMarkerLng { 66 | width: 23%; 67 | padding-right: 1%; 68 | } 69 | 70 | .InputfieldLeafletMapMarkerZoom { 71 | float: left; 72 | width: 9.5%; 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /InputfieldLeafletMapMarker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Display a Leaflet Map and pinpoint a location for InputfieldLeafletMapMarker 3 | * 4 | */ 5 | 6 | var InputfieldLeafletMapMarker = { 7 | 8 | options: { 9 | zoom: 9, 10 | draggable: true, 11 | center: null 12 | }, 13 | 14 | 15 | init: function(mapId, lat, lng, zoom, mapType, provider) { 16 | 17 | var mapElement = document.getElementById(mapId); 18 | var options = InputfieldLeafletMapMarker.options; 19 | 20 | if(zoom < 1) zoom = 9; 21 | //options.center = new google.maps.LatLng(lat, lng); 22 | options.zoom = parseInt(zoom); 23 | 24 | var map = L.map(mapElement).setView([lat, lng], options.zoom); 25 | L.tileLayer.provider(provider).addTo(map); 26 | 27 | var coder = L.Control.Geocoder.nominatim(), 28 | geocoder = L.Control.geocoder({ 29 | geocoder: geocoder, placeholder: '' 30 | }).addTo(map); 31 | 32 | var marker = L.marker( 33 | [lat,lng], 34 | {draggable: options.draggable} 35 | ).addTo(map); 36 | 37 | var $map = $('#' + mapId); 38 | //var $latlng = $map.siblings(".InputfieldLeafletMapMarkerLatLng").find("input[type=text]"); 39 | var $lat = $map.siblings(".InputfieldLeafletMapMarkerLat").find("input[type=text]"); 40 | var $lng = $map.siblings(".InputfieldLeafletMapMarkerLng").find("input[type=text]"); 41 | var $addr = $map.siblings(".InputfieldLeafletMapMarkerAddress").find("input[type=text]"); 42 | var $addrJS = $map.siblings(".InputfieldLeafletMapMarkerAddress").find("input[type=hidden]"); 43 | var $raw = $map.siblings(".InputfieldLeafletMapMarkerAddress").find("input[name$=_raw]"); 44 | var $toggle = $map.siblings(".InputfieldLeafletMapMarkerToggle").find("input[type=checkbox]"); 45 | var $zoom = $map.siblings(".InputfieldLeafletMapMarkerZoom").find("input[type=number]"); 46 | var $notes = $map.siblings(".notes"); 47 | var $latlng = ''; 48 | 49 | $( ".InputfieldLeafletMapMarkerAddress" ).on( "click", function() { 50 | $(".leaflet-control-geocoder.leaflet-control").toggleClass("leaflet-control-geocoder-expanded"); 51 | setTimeout(function() { $('input.undefined').focus() }, 300); 52 | }); 53 | 54 | $lat.val(marker.getLatLng().lat); 55 | $lng.val(marker.getLatLng().lng); 56 | $zoom.val(map.getZoom()); 57 | 58 | $zoom.change(function(event) { 59 | map.setZoom($zoom.val()); 60 | map.setView(marker.getLatLng()); 61 | }); 62 | 63 | $lat.change(function(event) { 64 | marker.setLatLng([$lat.val(),$lng.val()]); 65 | map.setView(marker.getLatLng(), 9); 66 | 67 | coder.reverse(marker.getLatLng(), map.options.crs.scale(map.getZoom()), function(results) { 68 | var r = results[0]; 69 | if (r) { 70 | $addr.val(r.name); 71 | $raw.val(JSON.stringify(r.properties)); 72 | } 73 | }) 74 | }); 75 | 76 | 77 | $lng.change(function(event) { 78 | marker.setLatLng([$lat.val(),$lng.val()]); 79 | map.setView(marker.getLatLng(), 9); 80 | coder.reverse(marker.getLatLng(), map.options.crs.scale(map.getZoom()), function(results) { 81 | var r = results[0]; 82 | if (r) { 83 | $addr.val(r.name); 84 | $raw.val(JSON.stringify(r.properties)); 85 | } 86 | }) 87 | }); 88 | 89 | 90 | geocoder.markGeocode = function(result) { 91 | marker.setLatLng(result.center); 92 | map.fitBounds(result.bbox); 93 | $lat.val(result.center.lat); 94 | $lng.val(result.center.lng); 95 | $addr.val(result.name); 96 | $raw.val(JSON.stringify(result.properties)); 97 | }; 98 | 99 | 100 | marker.on('dragend', function(event) { 101 | var result = marker.getLatLng(); 102 | $lat.val(result.lat).trigger('change.custom'); 103 | $lng.val(result.lng).trigger('change.custom'); 104 | 105 | //reverse geocoding displays in the adress field 106 | coder.reverse(marker.getLatLng(), map.options.crs.scale(map.getZoom()), function(results) { 107 | var r = results[0]; 108 | if (r) { 109 | $addr.val(r.name); 110 | $raw.val(JSON.stringify(r.properties)); 111 | } 112 | }) 113 | }); 114 | 115 | map.on('zoomend', function(event){ 116 | $zoom.val(map.getZoom()); 117 | }); 118 | 119 | // get the tab element where this map is integrated 120 | var $map = $('#' + mapId); 121 | 122 | // This seem to no longer work with Uikit theme. 123 | // var $tab = $('#_' + $map.closest('.InputfieldFieldsetTabOpen').attr('id')); 124 | // // get the inputfield where this map is integrated and add the tab to the stack 125 | // var $inputFields = $map.closest('.Inputfield').find('.InputfieldStateToggle').add($tab); 126 | 127 | // Get closed wrappers around the map. 128 | var $inputFields = $map.parents('.Inputfield.InputfieldStateCollapsed'); 129 | 130 | // Refresh the map when any of the wrappers open. 131 | $inputFields.on('opened',function(){ 132 | window.setTimeout(function(){ 133 | map.invalidateSize(); 134 | }, 200); 135 | }); 136 | } 137 | }; 138 | 139 | function initializeLeafletMap() { 140 | $(".InputfieldLeafletMapMarkerMap").each(function(item) { 141 | var $t = $(this); 142 | if (!$t.children().length) { 143 | InputfieldLeafletMapMarker.init($t.attr('id'), $t.attr('data-lat'), $t.attr('data-lng'), $t.attr('data-zoom'), $t.attr('data-type'), $t.attr('data-provider')); 144 | } 145 | }); 146 | } 147 | 148 | $(document).ready(function() { 149 | initializeLeafletMap(); 150 | $(document).on('reloaded', '.InputfieldLeafletMapMarker', initializeLeafletMap); 151 | }); 152 | -------------------------------------------------------------------------------- /InputfieldLeafletMapMarker.module: -------------------------------------------------------------------------------- 1 | 'Leaflet Map Marker', 27 | 'version' => '3.0.4', 28 | 'summary' => "Provides input for the LeafletMapMarker Fieldtype", 29 | 'requires' => 'ProcessWire>=3.0.0, FieldtypeLeafletMapMarker', 30 | 'icon' => 'map-marker', 31 | ); 32 | } 33 | 34 | const defaultAddr = 'Decatur, Georgia'; 35 | 36 | /** 37 | * Just in case this Inputfield is being used separately from FieldtypeLeafletMapMarker, we include the LeafletMapMarker class 38 | * 39 | */ 40 | public function __construct() { 41 | require_once(dirname(__FILE__) . '/LeafletMapMarker.php'); 42 | $this->set('defaultAddr', self::defaultAddr); 43 | $this->set('defaultZoom', 12); 44 | $this->set('defaultLat', ''); 45 | $this->set('defaultLng', ''); 46 | $this->set('height', 500); 47 | parent::__construct(); 48 | } 49 | 50 | /** 51 | * Initialize the LeafletLeafletMapMarker Inputfield 52 | * 53 | * We require Leaflet Maps API for map display, so we add it the scripts that will be loaded in PW admin 54 | * 55 | */ 56 | public function init() { 57 | 58 | $class = $this->className(); 59 | $assetPath = $this->config->urls->$class; 60 | 61 | $this->config->styles->add(($this->config->https ? 'https' : 'http') . '://unpkg.com/leaflet@0.7.7/dist/leaflet.css'); 62 | $this->config->styles->add($assetPath . 'Control.Geocoder.css'); 63 | //$this->config->styles->add($this->config->urls->$class . 'InputfieldLeafletMapMarker.css'); 64 | $this->config->scripts->add(($this->config->https ? 'https' : 'http') . '://unpkg.com/leaflet@0.7.7/dist/leaflet.js'); 65 | $this->config->scripts->add($assetPath . 'assets/leaflet-providers/leaflet-providers.js'); 66 | $this->config->scripts->add($assetPath . 'Control.Geocoder.js'); 67 | 68 | $this->set('defaultProvider', 'OpenStreetMap.Mapnik'); 69 | 70 | return parent::init(); 71 | } 72 | 73 | 74 | /** 75 | * Set an attribute to this Inputfield 76 | * 77 | * In this case, we just capture the 'value' attribute and make sure it's something valid 78 | * 79 | */ 80 | public function setAttribute($key, $value) { 81 | 82 | if($key == 'value' && !$value instanceof LeafletMapMarker && !is_null($value)) { 83 | throw new WireException("This input only accepts a LeafletLeafletMapMarker for it's value"); 84 | } 85 | 86 | return parent::setAttribute($key, $value); 87 | } 88 | 89 | public function isEmpty() { 90 | return (!$this->value || ((float) $this->value->lat) === 0.0); 91 | } 92 | 93 | /** 94 | * Render the markup needed to draw the Inputfield 95 | * 96 | */ 97 | public function ___render() { 98 | 99 | $name = $this->attr('name'); 100 | $id = $this->attr('id'); 101 | $marker = $this->attr('value'); 102 | if(!$marker->lat || $marker->lat == 0.0) $marker->lat = $this->defaultLat; 103 | if(!$marker->lng || $marker->lng == 0.0) $marker->lng = $this->defaultLng; 104 | if(!$marker->zoom) $marker->zoom = $this->defaultZoom; 105 | $address = htmlentities($marker->address, ENT_QUOTES, "UTF-8"); 106 | $toggleChecked = $marker->status != LeafletMapMarker::statusNoGeocode ? " checked='checked'" : ''; 107 | $status = $marker->status == LeafletMapMarker::statusNoGeocode ? 0 : $marker->status; 108 | $mapType = $this->defaultType; 109 | $provider = $this->defaultProvider; 110 | $height = $this->height ? (int) $this->height : 300; 111 | 112 | $labels = array( 113 | 'addr' => $this->_('Address'), 114 | 'lat' => $this->_('Latitude'), 115 | 'lng' => $this->_('Longitude'), 116 | 'zoom' => $this->_('Zoom') 117 | ); 118 | 119 | $out = <<< _OUT 120 | 121 | 122 | 123 |

124 | 129 | 130 | 131 |

132 | 133 | 134 |

135 | 139 |

140 | 141 |

142 | 146 |

147 | 148 |

149 | 153 |

154 | 155 | _OUT; 156 | 157 | $out .= "
" . 165 | "
"; 166 | 167 | //$this->notes = $marker->statusString; 168 | 169 | return $out; 170 | } 171 | 172 | /** 173 | * Process the input after a form submission 174 | * 175 | */ 176 | public function ___processInput(WireInputData $input) { 177 | 178 | $name = $this->attr('name'); 179 | $marker = $this->attr('value'); 180 | 181 | if(!isset($input->$name)) return $this; 182 | 183 | if($input->$name == $this->defaultAddr) { 184 | $marker->set('address', ''); 185 | } else { 186 | $marker->set('address', $input->$name); 187 | } 188 | 189 | $lat = $input["_{$name}_lat"]; 190 | $lng = $input["_{$name}_lng"]; 191 | 192 | $precision = 4; 193 | if( ((string) round($lat, $precision)) != ((string) round($this->defaultLat, $precision)) || 194 | ((string) round($lng, $precision)) != ((string) round($this->defaultLng, $precision))) { 195 | 196 | $marker->set('lat', $lat); 197 | $marker->set('lng', $lng); 198 | } else { 199 | // $this->message("Kept lat/lng at unset value", Notice::debug); 200 | } 201 | 202 | $zoom = $input["_{$name}_zoom"]; 203 | if($zoom > -1 && $zoom < 30) $marker->zoom = (int) $zoom; 204 | 205 | $status = $input["_{$name}_status"]; 206 | if(is_null($status)) $marker->set('status', LeafletMapMarker::statusNoGeocode); // disable geocode 207 | else $marker->set('status', (int) $status); 208 | 209 | $raw = $input["_{$name}_raw"]; 210 | $marker->set('raw', $raw); 211 | // $provider = $input["_{$name}_provider"]; 212 | // $marker->set('provider', $provider); 213 | 214 | // if the address changed, then redo the geocoding. 215 | // while we do this in the Fieldtype, we also do it here in case this Inputfield is used on it's own. 216 | // the LeafletMapMarker class checks to make sure it doesn't do the same geocode twice. 217 | if($marker->isChanged('address') && $marker->address && $marker->status != LeafletMapMarker::statusNoGeocode) { 218 | // double check that the address wasn't already populated by the JS geocoder 219 | // this prevents user-dragged markers that don't geocode to an exact location from getting 220 | // unintentionally moved by the PHP-side geocoder 221 | if($input["_{$name}_js_geocode_address"] == $marker->address) { 222 | // prevent the geocoder from running in the fieldtype 223 | $marker->skipGeocode = true; 224 | $this->message('Skipping geocode (already done by JS geocoder)', Notice::debug); 225 | } else { 226 | $marker->geocode(); 227 | } 228 | } 229 | 230 | return $this; 231 | } 232 | 233 | public function ___getConfigInputfields() { 234 | $inputfields = parent::___getConfigInputfields(); 235 | 236 | $field = $this->modules->get('InputfieldText'); 237 | $field->attr('name', 'defaultAddr'); 238 | $field->label = $this->_('Default Address'); 239 | $field->description = $this->_('This will be geocoded to become the starting point of the map.'); 240 | $field->attr('value', $this->defaultAddr); 241 | $field->notes = $this->_('When modifying the default address, please make the Latitude and Longitude fields below blank, which will force the system to geocode your new address.'); 242 | $inputfields->add($field); 243 | 244 | if(!$this->defaultLat && !$this->defaultLng) { 245 | $m = new LeafletMapMarker(); 246 | $m->address = $this->defaultAddr; 247 | $status = $m->geocode(); 248 | if($status > 0) { 249 | $this->defaultLat = $m->lat; 250 | $this->defaultLng = $m->lng; 251 | $this->message($this->_('Geocoded your default address. Please hit save once again to commit the new default latitude and longitude.')); 252 | } 253 | } 254 | 255 | $field = $this->modules->get('InputfieldText'); 256 | $field->attr('name', 'defaultLat'); 257 | $field->label = $this->_('Default Latitude'); 258 | $field->attr('value', $this->defaultLat); 259 | $field->columnWidth = 50; 260 | $inputfields->add($field); 261 | 262 | $field = $this->modules->get('InputfieldText'); 263 | $field->attr('name', 'defaultLng'); 264 | $field->label = $this->_('Default Longitude'); 265 | $field->attr('value', $this->defaultLng); 266 | $field->columnWidth = 50; 267 | $inputfields->add($field); 268 | /* 269 | $field = $this->modules->get('InputfieldRadios'); 270 | $field->attr('name', 'defaultType'); 271 | $field->label = $this->_('Default Map Type'); 272 | $field->addOption('HYBRID', $this->_('Hybrid')); 273 | $field->addOption('ROADMAP', $this->_('Road Map')); 274 | $field->addOption('SATELLITE', $this->_('Satellite')); 275 | $field->attr('value', $this->defaultType); 276 | $field->optionColumns = 1; 277 | $field->columnWidth = 50; 278 | $inputfields->add($field); 279 | */ 280 | $field = $this->modules->get('InputfieldInteger'); 281 | $field->attr('name', 'height'); 282 | $field->label = $this->_('Map Height (in pixels)'); 283 | $field->attr('value', $this->height); 284 | $field->attr('type', 'number'); 285 | $field->columnWidth = 50; 286 | $inputfields->add($field); 287 | 288 | $field = $this->modules->get('InputfieldInteger'); 289 | $field->attr('name', 'defaultZoom'); 290 | $field->label = $this->_('Default Zoom'); 291 | $field->description = $this->_('Enter a value between 1 and 18.'); // Zoom level description 292 | $field->attr('value', $this->defaultZoom); 293 | $field->attr('type', 'number'); 294 | $inputfields->add($field); 295 | 296 | $field = $this->modules->get('InputfieldSelect'); 297 | $field->attr('name', 'defaultProvider'); 298 | $field->label = $this->_('Default Map Tile Provider'); 299 | $field->description = $this->_('Select the provider name, standard is \'OpenStreetMap.Mapnik\''); 300 | $field->notes = $this->_('You can see the tiles for different providers at [http://leaflet-extras.github.io/leaflet-providers/preview/](http://leaflet-extras.github.io/leaflet-providers/preview/)'); 301 | $field->addOptionsString(file_get_contents(dirname(__FILE__) . '/inc/providers.inc')); 302 | $field->attr('value', $this->defaultProvider); 303 | $inputfields->add($field); 304 | 305 | $field = $this->modules->get('InputfieldMarkup'); 306 | $field->label = $this->_('API Notes'); 307 | $field->description = $this->_('You can access individual values from this field using the following from your template files:'); 308 | $field->value = 309 | "
" .
310 |             "\$page->{$this->name}->address\n" .
311 |             "\$page->{$this->name}->lat\n" .
312 |             "\$page->{$this->name}->lng\n" .
313 |             "\$page->{$this->name}->zoom\n" .
314 |             "\$page->{$this->name}->raw" .            
315 |             "
"; 316 | 317 | $inputfields->add($field); 318 | 319 | return $inputfields; 320 | } 321 | 322 | } 323 | -------------------------------------------------------------------------------- /LeafletMapMarker.php: -------------------------------------------------------------------------------- 1 | 'N/A', 14 | 1 => 'OK', 15 | 2 => 'OK_ROOFTOP', 16 | 3 => 'OK_RANGE_INTERPOLATED', 17 | 4 => 'OK_GEOMETRIC_CENTER', 18 | 5 => 'OK_APPROXIMATE', 19 | 20 | -1 => 'UNKNOWN', 21 | -2 => 'ZERO_RESULTS', 22 | -3 => 'OVER_QUERY_LIMIT', 23 | -4 => 'REQUEST_DENIED', 24 | -5 => 'INVALID_REQUEST', 25 | 26 | -100 => 'Geocode OFF', // RCD 27 | 28 | ); 29 | 30 | protected $geocodedAddress = ''; 31 | 32 | public function __construct() { 33 | $this->set('lat', ''); 34 | $this->set('lng', ''); 35 | $this->set('address', ''); 36 | $this->set('status', 0); 37 | $this->set('zoom', 0); 38 | $this->set('provider', ''); 39 | $this->set('raw', ''); 40 | // temporary runtime property to indicate the geocode should be skipped 41 | $this->set('skipGeocode', false); 42 | } 43 | 44 | public function set($key, $value) { 45 | 46 | if($key == 'lat' || $key == 'lng') { 47 | // if value isn't numeric, then it's not valid: make it blank 48 | if(strpos($value, ',') !== false) $value = str_replace(',', '.', $value); // convert 123,456 to 123.456 49 | if(!is_numeric($value)) $value = ''; 50 | 51 | } else if($key == 'address') { 52 | $value = wire('sanitizer')->text($value); 53 | 54 | } else if($key == 'status') { 55 | $value = (int) $value; 56 | if(!isset($this->geocodeStatuses[$value])) $value = -1; // -1 = unknown 57 | } else if($key == 'zoom') { 58 | $value = (int) $value; 59 | } else if($key == 'provider') { 60 | $value = $value; 61 | } 62 | 63 | return parent::set($key, $value); 64 | } 65 | 66 | public function get($key) { 67 | if($key == 'statusString') return str_replace('_', ' ', $this->geocodeStatuses[$this->status]); 68 | return parent::get($key); 69 | } 70 | 71 | public function geocode() { 72 | if($this->skipGeocode) return -100; 73 | 74 | // check if address was already geocoded 75 | if($this->geocodedAddress == $this->address) return $this->status; 76 | $this->geocodedAddress = $this->address; 77 | 78 | if(!ini_get('allow_url_fopen')) { 79 | $this->error("Geocode is not supported because 'allow_url_fopen' is disabled in PHP"); 80 | return 0; 81 | } 82 | 83 | // use openstreetmap api for get geocode 84 | $url = 'https://nominatim.openstreetmap.org/search?limit=1&format=json&q=' . urlencode($this->address); 85 | $http = new WireHttp(); 86 | $response = $http->get($url); 87 | if ($response !== false) { 88 | $result = json_decode($response, true); 89 | } else { 90 | $this->error("Error geocoding address"); 91 | if(isset($json['status'])) $this->status = (int) array_search($json['status'], $this->geocodeStatuses); 92 | else $this->status = -1; 93 | $this->lat = 0; 94 | $this->lng = 0; 95 | $this->raw = ''; 96 | return $this->status; 97 | } 98 | 99 | $this->lat = $result[0]['lat']; 100 | $this->lng = $result[0]['lon']; 101 | $this->raw = $response; 102 | 103 | $this->message("Geocode OK: '{$this->address}'"); 104 | $this->status = 1; 105 | return $this->status; 106 | } 107 | 108 | /** 109 | * If accessed as a string, then just output the lat, lng coordinates 110 | * 111 | */ 112 | public function __toString() { 113 | return "$this->address ($this->lat, $this->lng, $this->zoom) [$this->statusString]"; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /MarkupAddInlineScript.module: -------------------------------------------------------------------------------- 1 | 'Inline Scripts', 14 | 'version' => '3.0.2', 15 | 'summary' => 'adds inline script before ', 16 | 'singular' => true, 17 | 'autoload' => true, 18 | 'requires' => 'ProcessWire>=3.0.0, MarkupLeafletMap', 19 | 'Author' => 'gebeer', 20 | ); 21 | } 22 | 23 | /** 24 | * Initialize the module 25 | * 26 | * ProcessWire calls this when the module is loaded. For 'autoload' modules, this will be called 27 | * when ProcessWire's API is ready. As a result, this is a good place to attach hooks. 28 | * 29 | */ 30 | public function init() { 31 | $this->addHookAfter('Page::render', $this, 'addInlineScript'); 32 | } 33 | 34 | public function addInlineScript($event) { 35 | $page = $event->object; 36 | if ($page->template->name !== "admin" && $page->inlineScript) { 37 | $event->return = str_replace("", $page->inlineScript . "", $event->return); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /MarkupLeafletMap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ProcessWire Leaflet Map Markup (JS) 3 | * 4 | * Renders maps for the FieldtypeLeafletMapMarker module 5 | * 6 | * ProcessWire 2.x 7 | * Port of Google Map Markup by Ryan Cramer 8 | * Copyright (C) 2013 by Ryan Cramer 9 | 10 | * Copyright (C) 2015 by Mats Neander 11 | * Licensed under GNU/GPL v2, see LICENSE.TXT 12 | * 13 | * http://processwire.com 14 | * 15 | * Javascript Usage: 16 | * ================= 17 | * var map = new jsMarkupLeafletMap(); 18 | * map.setOption('any-leaflet-maps-option', 'value'); 19 | * map.setOption('zoom', 12); // example 20 | * 21 | * // init(container ID, latitude, longitude): 22 | * map.init('#map-div', 26.0936823, -77.5332796); 23 | * 24 | * // addMarker(latitude, longitude, optional URL, optional URL to icon file): 25 | * map.addMarker(26.0936823, -77.5332796, 'en.wikipedia.org/wiki/Castaway_Cay', ''); 26 | * map.addMarker(...you may have as many of these as you want...); 27 | * 28 | * // optionally fit the map to the bounds of the markers you added 29 | * map.fitToMarkers(); 30 | * 31 | */ 32 | 33 | function jsMarkupLeafletMap() { 34 | 35 | this.map = null; 36 | this.markers = []; 37 | this.numMarkers = 0; 38 | 39 | this.options = { 40 | zoom: 10, 41 | center: null, 42 | }; 43 | 44 | this._currentURL = ''; 45 | this.init = function(mapID, lat, lng, provider) { 46 | if(lat != 0) this.map = L.map(mapID, {center: [lat, lng], zoom: this.options.zoom, scrollWheelZoom: this.options.scrollWheelZoom,} ); 47 | L.tileLayer.provider(provider).addTo(this.map); 48 | } 49 | 50 | this.setOption = function(key, value) { 51 | this.options[key] = value; 52 | } 53 | 54 | var markers = new L.MarkerClusterGroup({polygonOptions: {color: 'teal', weight: 1, opacity: .39, lineJoin: 'round'}}); 55 | var marker = ''; 56 | 57 | this.addMarker = function(lat, lng, url, title, extra) { 58 | if(lat == 0.0) return; 59 | 60 | var latLng = L.latLng(lat, lng); 61 | 62 | var markerOptions = { 63 | linkURL: '', 64 | title: title 65 | }; 66 | 67 | marker = L.marker(latLng, markerOptions); 68 | markers.addLayer(marker); 69 | this.map.addLayer(markers); 70 | this.markers[this.numMarkers] = marker; 71 | this.numMarkers++; 72 | 73 | if(url.length > 0) { 74 | marker.linkURL = url; 75 | if (extra.length > 0) { 76 | extra = '
' + extra; 77 | } 78 | marker.bindPopup("" + title + "" + extra); 79 | } 80 | } 81 | 82 | 83 | this.addMarkerIcon = function(icon, lat, lng, url, title, extra) { 84 | if(lat == 0.0) return; 85 | 86 | var latLng = L.latLng(lat, lng); 87 | 88 | marker = L.marker(latLng, {icon: icon, linkURL: '', title: title}); 89 | markers.addLayer(marker); 90 | this.map.addLayer(markers); 91 | this.markers[this.numMarkers] = marker; 92 | this.numMarkers++; 93 | 94 | if(url.length > 0) { 95 | marker.linkURL = url; 96 | 97 | if (extra.length > 0) { 98 | extra = '
' + extra; 99 | } 100 | marker.bindPopup("" + title + "" + extra); 101 | } 102 | } 103 | 104 | 105 | this.fitToMarkers = function() { 106 | var map = this.map; 107 | var mg = []; 108 | for(var i = 0; i < this.numMarkers; i++) { 109 | mg.push(this.markers[i].getLatLng()); 110 | } 111 | 112 | map.fitBounds(mg); 113 | } 114 | 115 | 116 | this.setMaxZoom = function() { 117 | var map = this.map; 118 | map.setZoom(this.options['zoom']); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /MarkupLeafletMap.module: -------------------------------------------------------------------------------- 1 | tag: 19 | * 20 | * 21 | * 22 | * 23 | * In the location where you want to output your map, do the following in your template file: 24 | * 25 | * $map = $modules->get('MarkupLeafletMap'); 26 | * echo $map->render($page, 'map'); // replace 'map' with the name of your LeafletFieldtypeMap field 27 | * 28 | * To render a map with multiple markers on it, specify a PageArray rather than a single $page: 29 | * 30 | * $items = $pages->find("template=something, map!='', sort=title"); 31 | * $map = $modules->get('MarkupLeafletMap'); 32 | * echo $map->render($items, 'map'); 33 | * 34 | * To specify options, provide a 3rd argument with an options array: 35 | * 36 | * $map = $modules->get('MarkupLeafletMap'); 37 | * echo $map->render($items, 'map', array('height' => '500px')); 38 | * 39 | * 40 | * OPTIONS 41 | * ======= 42 | * Here is a list of all possible options (with defaults shown): 43 | * 44 | * // default width of the map 45 | * 'width' => '100%' 46 | * 47 | * // default height of the map 48 | * 'height' => '300px' 49 | * 50 | * // zoom level 51 | * 'zoom' => 12 (or $field->defaultZoom) 52 | * 53 | * // set to true to automatically adjust maximum zoom level to default zoom 54 | * 'maxZoom' => true 55 | * 56 | * // set to false to disable the scroll wheel zoom 57 | * 'scrollWheelZoom' => true 58 | * 59 | * // map ID attribute 60 | * 'id' => "mgmap" 61 | * 62 | * // map class attribute 63 | * 'class' => "MarkupGoogleMap" 64 | * 65 | * // center latitude 66 | * 'lat' => $field->defaultLat 67 | * 68 | * // center longitude 69 | * 'lng' => $field->defaultLng 70 | * 71 | * // set to false only if you will style the map
yourself 72 | * 'useStyles' => true 73 | * 74 | * // field to use for the marker link, or blank to not link 75 | * 'markerLinkField' => 'url' 76 | * 77 | * // field to use for the marker title 78 | * 'markerTitleField' => 'title' 79 | * 80 | * // map will automatically adjust to fit to the given markers (when multiple markers) 81 | * 'fitToMarkers' => true 82 | * 83 | * 84 | * // Any extra javascript initialization code you want to occur before the map itself is drawn 85 | * 'init' => '', 86 | * 87 | */ 88 | 89 | class MarkupLeafletMap extends WireData implements Module { 90 | 91 | public static function getModuleInfo() { 92 | return array( 93 | 'title' => 'LeafletMap Markup', 94 | 'version' => '3.0.2', 95 | 'summary' => 'Renders Leaflet Maps for the LealetMapMarker Fieldtype', 96 | 'requires' => 'ProcessWire>=3.0.0, FieldtypeLeafletMapMarker', 97 | 'installs' => 'MarkupAddInlineScript', 98 | ); 99 | } 100 | 101 | /** 102 | * Include our MapMarker class, which serves as the value for fields of type FieldtypeMapMarker 103 | * 104 | */ 105 | public function init() { 106 | $class = $this->classname(); 107 | $assetPath = $this->config->urls->$class; 108 | 109 | $this->config->styles->add('https://unpkg.com/leaflet@0.7.3/dist/leaflet.css'); 110 | $this->config->styles->add($assetPath . "assets/leaflet-markercluster/MarkerCluster.css"); 111 | $this->config->styles->add($assetPath . "assets/leaflet-markercluster/MarkerCluster.Default.css"); 112 | $this->config->scripts->add('https://unpkg.com/leaflet@0.7.3/dist/leaflet.js'); 113 | $this->config->scripts->add($assetPath . 'assets/leaflet-markercluster/leaflet.markercluster.js'); 114 | $this->config->scripts->add($assetPath . 'assets/leaflet-providers/leaflet-providers.js'); 115 | $this->config->scripts->add($assetPath . 'MarkupLeafletMap.js'); 116 | 117 | require_once(dirname(__FILE__) . '/LeafletMapMarker.php'); 118 | } 119 | 120 | public function getOptions($fieldName) { 121 | static $n = 0; 122 | $field = $this->fields->get($fieldName); 123 | if(!$field) throw new WireException("Unknown field: $fieldName"); 124 | 125 | return array( 126 | 'useStyles' => true, 127 | 'fitToMarkers' => true, 128 | 'useMarkerSettings' => true, 129 | 'useHoverBox' => false, 130 | 'hoverBoxMarkup' => "
", 131 | 'markerLinkField' => 'url', 132 | 'markerTitleField' => 'title', 133 | 'width' => '100%', 134 | 'height' => $field->height, 135 | 'zoom' => $field->defaultZoom ? (int) $field->defaultZoom : 12, 136 | 'type' => $field->defaultType ? $field->defaultType : 'HYBRID', 137 | 'id' => "mleafletmap" . (++$n), 138 | 'class' => "MarkupLeafletMap", 139 | 'lat' => $field->defaultLat, 140 | 'lng' => $field->defaultLng, 141 | 'provider' => $field->defaultProvider, 142 | 'maxZoom' => false, 143 | 'scrollWheelZoom' => true, 144 | 'icon' => '', // url to icon (blank=use default) 145 | 'iconHover' => '', // url to icon when hovered (default=none) 146 | 'shadow' => '', // url to icon shadow (blank=use default) 147 | 'init' => '', // additional javascript code to insert in map initialization 148 | 'n' => $n, 149 | 'popupFormatter' => '', 150 | 151 | // See https://github.com/lvoogdt/Leaflet.awesome-markers#properties 152 | // for documentation on what values can go in the two lines above (look at the icon and markerColor setting in the docs) 153 | 'markerIcon' => 'home', 154 | 'markerColour' => 'darkblue', 155 | 'markerIconColour' => 'white', 156 | 'markerFormatter' => '', 157 | ); 158 | } 159 | 160 | 161 | /** 162 | * Call this method when constucting the HTML header, to inject all the script and style files needed for Leaflet maps. 163 | */ 164 | public function getLeafletMapHeaderLines($fontawesome = true) { 165 | $url = $this->config->urls->siteModules. 'FieldtypeLeafletMapMarker/'; 166 | $lines = ''; 167 | if ($fontawesome) { 168 | $lines .= ""; 169 | } 170 | $lines .= << 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | LINES; 186 | 187 | return $lines; 188 | } 189 | 190 | 191 | public function render($pageArray, $fieldName, array $options = array()) { 192 | static $n = 0; 193 | $n++; 194 | 195 | $defaultOptions = $this->getOptions($fieldName); 196 | $options = array_merge($defaultOptions, $options); 197 | if($pageArray instanceof Page) { 198 | $page = $pageArray; 199 | $pageArray = new PageArray(); 200 | $pageArray->add($page); 201 | } 202 | 203 | $height = $options['height']; 204 | $width = $options['width']; 205 | if(empty($height)) $height = 300; 206 | if(ctype_digit("$height")) $height .= "px"; 207 | if(ctype_digit("$width")) $width .= "px"; 208 | 209 | $style = ''; 210 | if($options['useStyles'] && !empty($height) && !empty($width)) { 211 | $style = " style='width: $width; height: $height;'"; 212 | } 213 | 214 | $lat = $options['lat']; 215 | $lng = $options['lng']; 216 | $zoom = $options['zoom'] > 0 ? (int) $options['zoom'] : $defaultOptions['zoom']; 217 | //$type = in_array($options['type'], array('ROADMAP', 'SATELLITE', 'HYBRID')) ? $options['type'] : 'HYBRID'; 218 | $provider = ($options['provider'] == "") ? 'OpenStreetMap.Mapnik' : $options['provider']; 219 | 220 | if($options['useMarkerSettings'] && (count($pageArray) == 1 || !$lat)) { 221 | // single marker overrides lat, lng and zoom settings 222 | $marker = $pageArray->first()->get($fieldName); 223 | $lat = $marker->lat; 224 | $lng = $marker->lng; 225 | if($marker->zoom > 0) $zoom = (int) $marker->zoom; 226 | } 227 | 228 | $id = $options['id']; 229 | $out = ''; 230 | 231 | $out .= "
"; 232 | 233 | $inlineScript = ""; 282 | 283 | $this->page->inlineScript = $inlineScript; 284 | 285 | return $out; 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FieldtypeLeafletMapMarker Module for ProcessWire 2 | 3 | This is a port of the Map Marker Fieldtype by Ryan Cramer. Instead of Google maps it uses Leaflet maps. 4 | 5 | Google maps geocoding is still used for geocoding default lat/lng values under *field* settings but the geocoding on page 6 | editing uses Per Liedmans [leaflet-control-geocoder] (https://github.com/perliedman/leaflet-control-geocoder) 7 | 8 | This Fieldtype for ProcessWire holds an address or location name, and automatically geocodes the address to latitude/longitude using leaflet-control-geocoder. The resulting values may be used to populate any kind of map (whether Leaflet Maps or another). 9 | 10 | Support for per-marker customisation of visuals and popup content is included. 11 | 12 | ![Screenshot of map with customised markers and popup text](/images/005.png) 13 | 14 | ---------- 15 | 16 | ## Installation 17 | 18 | 1. Copy all of the files for this module into /site/modules/FieldtypeLeafletMapMarker/ 19 | 20 | 2. In your admin, go to the Modules screen and "check for new modules." Under the 'Fieldtype' section, install the 'Leaflet Map Marker' module (FieldtypeLeadletMapMarker) 21 | 22 | 3. Under the 'Inputfield' section, install the 'Leaflet Map Marker' module (InputfieldLeafletMapMarker) if it is not 23 | already installed. 24 | 25 | 4. Under the 'Markup' section, install the 'Leaflet Map' module (MarkupLeafletMap) **and** the 'Inline Scripts' module (MarkupAddInlineScript) 26 | 27 | 5. In your admin, go to Setup > Fields > Add New Field. Choose LeafletMapMarker as the type. 28 | If you are not sure what to name your field, simply "map" is a good one! Once created, configure the settings on the *input* tab. 29 | 30 | 6. Add your new "map" field to one or more templates, as you would any other field. 31 | 32 | 33 | ### How to use from the page editor 34 | 35 | 1. Create or edit a page using one of the templates you added the "map" field to. 36 | 37 | 2. Type in a location or address into the "address" box for the map field. Then click 38 | outside of the address, and the Javascript geocoder should automatically populate the 39 | latitude, longitude and map location. The Leaflet geocoder will accept full addresses 40 | or known location names. For instance, you could type in "Disney Land" and it knows 41 | how to find locations like that. 42 | 43 | 3. The geocoding also works in reverse. You may drag the map marker wherever you want 44 | and it will populate the address field for you. You may also populate the latitude, 45 | longitude and zoom fields manually if you like. Unchecking the box between address 46 | and latitude disables the geocoder. 47 | 48 | _If the geocoding does not work, please ensure that your browser is not blocking the execution of any scripts._ 49 | 50 | 51 | ### How to use from the API, in your template files 52 | 53 | In your template files, you can utilize this data for your own Leaflet Maps (or anything else that you might need latitude/longitude for). 54 | 55 | Lets assume that your field is called 'map'. Here is how you would access the components of it from the API: 56 | 57 | ```php 58 | echo $page->map->address; // outputs the address you entered 59 | echo $page->map->lat; // outputs the latitude 60 | echo $page->map->lng; // outputs the longitude 61 | echo $page->map->zoom; // outputs the zoom level 62 | ``` 63 | 64 | --- 65 | 66 | ## Markup Leaflet Map 67 | 68 | This package also comes with a markup helper module called MarkupLeafletMap. It provides a simple means of outputting a Leaflet Map based on the data managed by FieldtypeLeafletMapMarker. 69 | 70 | ### Basic Usage 71 | 72 | Seeing as we are going to need access to the module as we generate the HTML header block (to add script and style 73 | includes), we first load and gain access to the Markup module... 74 | 75 | ```php 76 | get('MarkupLeafletMap'); ?> 77 | ``` 78 | 79 | _Leaflet's js code requires jquery - so make sure you load that in HTML header section, the markup module will not do this for you._ 80 | 81 | MarkupLeafletMap includes the _getLeafletMapHeaderLines()_ method that generates all the needed script and style includes for your HTML header 82 | section so simply add this somewhere before your closing `` tag: 83 | 84 | ```php 85 | getLeafletMapHeaderLines(); ?> 86 | ``` 87 | 88 | If you already load FontAwesome elsewhere in your header and you do not want it included as part of the Leaflet Map header lines, use the following version: 89 | 90 | ```php 91 | getLeafletMapHeaderLines(false); ?> 92 | ``` 93 | 94 | 95 | In the location within the body of your HTML where you want your map to appear, place the following: 96 | 97 | ```php 98 | render($page, 'YOUR MARKER FIELD'); ?> 99 | ``` 100 | 101 | To render a map with multiple markers on it, specify a PageArray rather than a single $page: 102 | 103 | ```php 104 | $items = $pages->find("A SELECTOR THAT GETS YOUR PAGES WITH MARKER FIELDS"); 105 | echo $map->render($items, 'YOUR MARKER FIELD'); 106 | ``` 107 | 108 | To specify options, provide a 3rd argument with an options array: 109 | 110 | ```php 111 | echo $map->render($items, 'YOUR MARKER FIELD', array('height' => '500px')); 112 | ``` 113 | 114 | ### Options 115 | 116 | Consult the following table for more options for customising your Leaflet map. 117 | 118 | Option | Notes 119 | ------ | ----- 120 | `width` | Width of the map (type: string; default: 100%) 121 | `height` | Height of the map (type: string; default: 300px) 122 | `zoom` | Zoom level 1-25 (type: integer; default: from your field settings) 123 | `id` | Map ID attribute (type: string; default: mgmap) 124 | `class` | Map class attribute (type: string; default: MarkupLeafletMap) 125 | `lat` | Map center latitude (type: string/float; default: from your field settings) 126 | `lng` | Map center longitude (type: string/float; default: from your field settings) 127 | `useStyles` | Whether to populate inline styles to the map div for width/height (type: boolean; default: true). Set to false only if you will style the map div yourself 128 | `useMarkerSettings` | Makes single-marker map use marker settings rather than map settings (type: boolean; default: true) 129 | `markerLinkField` | Page field to use for the marker link, or blank to not link (type: string; default: url) 130 | `markerTitleField` | Page field to use for the marker title, or blank not to use a marker title (type: string; default: title) 131 | `fitToMarkers` | When multiple markers are present, set map to automatically adjust to fit to the given markers (type: boolean; default: true) 132 | `popupFormatter` | A PHP callback function, taking a `$page` as an argument, for generating additional content of a marker's popup box 133 | `markerIcon` | The default name of the FontAwesome icon to use in the marker - without the prefix 'fa-'. (type: string; default: 'home') 134 | `markerIconColour` | The default colour the of the FontAwesome icon (type: string; default 'white') 135 | `markerColour` | The default colour of the marker body that surrounds the icon. (type: string; default 'darkblue'.) See Leaflet.AwesomeMarker's [markerColor](https://github.com/lvoogdt/Leaflet.awesome-markers#properties) entry for the available colours - they are limited. 136 | `markerFormatter` | A PHP callback function (taking a PW `$page` and AwesomeMarker `$marker_options` as arguments) for customising the look of any marker on the map. This is called once for each marker being placed on the map and allows the defaults to be overridden for each marker. 137 | `provider` | Defines which tile layer provider to use. (type: string; default: OpenStreetMap.Mapnik) 138 | `scrollWheelZoom` | Whether to allow zooming with the mouse wheel. (type: boolean; default: true) 139 | 140 | ---------- 141 | 142 | ## Customising A Map's Tile Layer 143 | 144 | You can use the `provider` option to tell the map which tile layer to use. Available layers can be previewed [here](https://leaflet-extras.github.io/leaflet-providers/preview/index.html). 145 | 146 | 147 | ## Customising A Marker's Visuals and Popup Contents 148 | 149 | [Leaflet.AwesomeMarkers](https://github.com/lvoogdt/Leaflet.awesome-markers) and [FontAwesome](http://http://fontawesome.io/) are included in the module's assets and allow you to customise the look 150 | of your markers very nicely. 151 | 152 | ### Changing the Default Marker Appearance 153 | 154 | You can change the default appearance of your map markers by supplying values for any or all of the `markerIcon`, 155 | `markerIconColour` and `markerColour` options that you pass into the `$map->render()` method. For instance, to make all 156 | the markers Red, with a white flag icon, use this code... 157 | 158 | ```php 159 | 'flag', 'markerColour' => 'red'); 161 | echo $map->render($items, 'YOUR MARKER FIELD', $options); 162 | ?> 163 | ``` 164 | 165 | ### Changing Per-Marker Appearance 166 | 167 | As part of the options array, you can specify a callback that can override the values used to generate each marker's 168 | appearance. The callback function takes a PW `$page` and some `$marker_options` as arguments and can use any fields from 169 | the page to customise the visuals for the marker generated for that page. 170 | 171 | If you are using a PHP5.4 or above, anonymous functions make this very easy. If you are stuck with an older version of 172 | PHP, you can use a named function or method. 173 | 174 | To support this, let's add a new field called 'marker_icon', of type text, to the template of the pages that hold the LeafletMapMarker 175 | field. 176 | 177 | ```php 178 | function($page, $marker_options) { 181 | $marker_options['icon'] = $page->marker_icon; // Override the default icon for this marker. 182 | return $marker_options; 183 | } 184 | ); 185 | echo $map->render($items, 'YOUR MARKER FIELD', $options); 186 | ?> 187 | ``` 188 | 189 | `$marker_options` is the array of [Leaflet.AwesomeMarker properties](https://github.com/lvoogdt/Leaflet.awesome-markers#properties) that will be used to generate this marker. 190 | 191 | In this way, you can let the pages holding your LeafletMarkers also define their visuals. 192 | 193 | ### Customising Popup Content 194 | 195 | You can similarly use a callback method to customise the content that appears within a marker's popup box. The return 196 | value from this method should be an HTML snippet that will be placed into the popup box _by Javascript_. 197 | 198 | ```php 199 | function($page) { 202 | $out[] = "Contact: $page->phone_number"; 203 | $out[] = "image->url}\" width=\"100\" height=\"100\" />"; // ** NB: Use escaped double quotes if HTML attributes needed ** 204 | return implode('
', $out); 205 | } 206 | ); 207 | 208 | echo $map->render($items, 'YOUR MARKER FIELD', $options); 209 | ?> 210 | ``` 211 | 212 | _NB._ The example above uses a PW image field called 'image' that is configured as a single instance field. 213 | 214 | --- 215 | 216 | ### Contributors 217 | 218 | * [Ryan Cramer](https://processwire.com/talk/profile/2-ryan/) provided the original Google Maps module. 219 | * [Mats](https://processwire.com/talk/profile/67-mats/) produces the original Leaflet version. 220 | * [gebeer](https://processwire.com/talk/profile/1920-gebeer/) extended Mats' work, adding the Inline Scripts module. 221 | * [netcarver](https://processwire.com/talk/profile/465-netcarver/) added callback formatters for marker and popover content generation. He also added AwesomeMarker 222 | support. 223 | * [Glenn McLelland](https://github.com/gmclelland) fixes for PW3 branch and provider documentation. 224 | -------------------------------------------------------------------------------- /assets/font-awesome-4.6.3/css/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.6.3 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.6.3');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.6.3') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.6.3') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.6.3') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.6.3') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.6.3#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} -------------------------------------------------------------------------------- /assets/font-awesome-4.6.3/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfProcessWire/FieldtypeLeafletMapMarker/488e840bc6c262f655793b93f324e35f0e2c74c0/assets/font-awesome-4.6.3/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /assets/font-awesome-4.6.3/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfProcessWire/FieldtypeLeafletMapMarker/488e840bc6c262f655793b93f324e35f0e2c74c0/assets/font-awesome-4.6.3/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /assets/font-awesome-4.6.3/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfProcessWire/FieldtypeLeafletMapMarker/488e840bc6c262f655793b93f324e35f0e2c74c0/assets/font-awesome-4.6.3/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /assets/font-awesome-4.6.3/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfProcessWire/FieldtypeLeafletMapMarker/488e840bc6c262f655793b93f324e35f0e2c74c0/assets/font-awesome-4.6.3/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /assets/font-awesome-4.6.3/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfProcessWire/FieldtypeLeafletMapMarker/488e840bc6c262f655793b93f324e35f0e2c74c0/assets/font-awesome-4.6.3/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /assets/leaflet-awesome-markers/images/markers-matte.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfProcessWire/FieldtypeLeafletMapMarker/488e840bc6c262f655793b93f324e35f0e2c74c0/assets/leaflet-awesome-markers/images/markers-matte.png -------------------------------------------------------------------------------- /assets/leaflet-awesome-markers/images/markers-matte@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfProcessWire/FieldtypeLeafletMapMarker/488e840bc6c262f655793b93f324e35f0e2c74c0/assets/leaflet-awesome-markers/images/markers-matte@2x.png -------------------------------------------------------------------------------- /assets/leaflet-awesome-markers/images/markers-plain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfProcessWire/FieldtypeLeafletMapMarker/488e840bc6c262f655793b93f324e35f0e2c74c0/assets/leaflet-awesome-markers/images/markers-plain.png -------------------------------------------------------------------------------- /assets/leaflet-awesome-markers/images/markers-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfProcessWire/FieldtypeLeafletMapMarker/488e840bc6c262f655793b93f324e35f0e2c74c0/assets/leaflet-awesome-markers/images/markers-shadow.png -------------------------------------------------------------------------------- /assets/leaflet-awesome-markers/images/markers-shadow@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfProcessWire/FieldtypeLeafletMapMarker/488e840bc6c262f655793b93f324e35f0e2c74c0/assets/leaflet-awesome-markers/images/markers-shadow@2x.png -------------------------------------------------------------------------------- /assets/leaflet-awesome-markers/images/markers-soft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfProcessWire/FieldtypeLeafletMapMarker/488e840bc6c262f655793b93f324e35f0e2c74c0/assets/leaflet-awesome-markers/images/markers-soft.png -------------------------------------------------------------------------------- /assets/leaflet-awesome-markers/images/markers-soft@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfProcessWire/FieldtypeLeafletMapMarker/488e840bc6c262f655793b93f324e35f0e2c74c0/assets/leaflet-awesome-markers/images/markers-soft@2x.png -------------------------------------------------------------------------------- /assets/leaflet-awesome-markers/leaflet.awesome-markers.css: -------------------------------------------------------------------------------- 1 | /* 2 | Author: L. Voogdt 3 | License: MIT 4 | Version: 1.0 5 | */ 6 | 7 | /* Marker setup */ 8 | .awesome-marker { 9 | background: url('images/markers-soft.png') no-repeat 0 0; 10 | width: 35px; 11 | height: 46px; 12 | position:absolute; 13 | left:0; 14 | top:0; 15 | display: block; 16 | text-align: center; 17 | } 18 | 19 | .awesome-marker-shadow { 20 | background: url('images/markers-shadow.png') no-repeat 0 0; 21 | width: 36px; 22 | height: 16px; 23 | } 24 | 25 | /* Retina displays */ 26 | @media (min--moz-device-pixel-ratio: 1.5),(-o-min-device-pixel-ratio: 3/2), 27 | (-webkit-min-device-pixel-ratio: 1.5),(min-device-pixel-ratio: 1.5),(min-resolution: 1.5dppx) { 28 | .awesome-marker { 29 | background-image: url('images/markers-soft@2x.png'); 30 | background-size: 720px 46px; 31 | } 32 | .awesome-marker-shadow { 33 | background-image: url('images/markers-shadow@2x.png'); 34 | background-size: 35px 16px; 35 | } 36 | } 37 | 38 | .awesome-marker i { 39 | color: #333; 40 | margin-top: 10px; 41 | display: inline-block; 42 | font-size: 14px; 43 | } 44 | 45 | .awesome-marker .icon-white { 46 | color: #fff; 47 | } 48 | 49 | /* Colors */ 50 | .awesome-marker-icon-red { 51 | background-position: 0 0; 52 | } 53 | 54 | .awesome-marker-icon-darkred { 55 | background-position: -180px 0; 56 | } 57 | 58 | .awesome-marker-icon-lightred { 59 | background-position: -360px 0; 60 | } 61 | 62 | .awesome-marker-icon-orange { 63 | background-position: -36px 0; 64 | } 65 | 66 | .awesome-marker-icon-beige { 67 | background-position: -396px 0; 68 | } 69 | 70 | .awesome-marker-icon-green { 71 | background-position: -72px 0; 72 | } 73 | 74 | .awesome-marker-icon-darkgreen { 75 | background-position: -252px 0; 76 | } 77 | 78 | .awesome-marker-icon-lightgreen { 79 | background-position: -432px 0; 80 | } 81 | 82 | .awesome-marker-icon-blue { 83 | background-position: -108px 0; 84 | } 85 | 86 | .awesome-marker-icon-darkblue { 87 | background-position: -216px 0; 88 | } 89 | 90 | .awesome-marker-icon-lightblue { 91 | background-position: -468px 0; 92 | } 93 | 94 | .awesome-marker-icon-purple { 95 | background-position: -144px 0; 96 | } 97 | 98 | .awesome-marker-icon-darkpurple { 99 | background-position: -288px 0; 100 | } 101 | 102 | .awesome-marker-icon-pink { 103 | background-position: -504px 0; 104 | } 105 | 106 | .awesome-marker-icon-cadetblue { 107 | background-position: -324px 0; 108 | } 109 | 110 | .awesome-marker-icon-white { 111 | background-position: -574px 0; 112 | } 113 | 114 | .awesome-marker-icon-gray { 115 | background-position: -648px 0; 116 | } 117 | 118 | .awesome-marker-icon-lightgray { 119 | background-position: -612px 0; 120 | } 121 | 122 | .awesome-marker-icon-black { 123 | background-position: -682px 0; 124 | } 125 | -------------------------------------------------------------------------------- /assets/leaflet-awesome-markers/leaflet.awesome-markers.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | Leaflet.AwesomeMarkers, a plugin that adds colorful iconic markers for Leaflet, based on the Font Awesome icons 3 | (c) 2012-2013, Lennard Voogdt 4 | 5 | http://leafletjs.com 6 | https://github.com/lvoogdt 7 | *//*global L*/(function(e,t,n){"use strict";L.AwesomeMarkers={};L.AwesomeMarkers.version="2.0.1";L.AwesomeMarkers.Icon=L.Icon.extend({options:{iconSize:[35,45],iconAnchor:[17,42],popupAnchor:[1,-32],shadowAnchor:[10,12],shadowSize:[36,16],className:"awesome-marker",prefix:"glyphicon",spinClass:"fa-spin",icon:"home",markerColor:"blue",iconColor:"white"},initialize:function(e){e=L.Util.setOptions(this,e)},createIcon:function(){var e=t.createElement("div"),n=this.options;n.icon&&(e.innerHTML=this._createInner());n.bgPos&&(e.style.backgroundPosition=-n.bgPos.x+"px "+ -n.bgPos.y+"px");this._setIconStyles(e,"icon-"+n.markerColor);return e},_createInner:function(){var e,t="",n="",r="",i=this.options;i.icon.slice(0,i.prefix.length+1)===i.prefix+"-"?e=i.icon:e=i.prefix+"-"+i.icon;i.spin&&typeof i.spinClass=="string"&&(t=i.spinClass);i.iconColor&&(i.iconColor==="white"||i.iconColor==="black"?n="icon-"+i.iconColor:r="style='color: "+i.iconColor+"' ");return""},_setIconStyles:function(e,t){var n=this.options,r=L.point(n[t==="shadow"?"shadowSize":"iconSize"]),i;t==="shadow"?i=L.point(n.shadowAnchor||n.iconAnchor):i=L.point(n.iconAnchor);!i&&r&&(i=r.divideBy(2,!0));e.className="awesome-marker-"+t+" "+n.className;if(i){e.style.marginLeft=-i.x+"px";e.style.marginTop=-i.y+"px"}if(r){e.style.width=r.x+"px";e.style.height=r.y+"px"}},createShadow:function(){var e=t.createElement("div");this._setIconStyles(e,"shadow");return e}});L.AwesomeMarkers.icon=function(e){return new L.AwesomeMarkers.Icon(e)}})(this,document); 8 | -------------------------------------------------------------------------------- /assets/leaflet-markercluster/MarkerCluster.Default.css: -------------------------------------------------------------------------------- 1 | .marker-cluster-small { 2 | background-color: rgba(181, 226, 140, 0.6); 3 | } 4 | .marker-cluster-small div { 5 | background-color: rgba(110, 204, 57, 0.6); 6 | } 7 | 8 | .marker-cluster-medium { 9 | background-color: rgba(241, 211, 87, 0.6); 10 | } 11 | .marker-cluster-medium div { 12 | background-color: rgba(240, 194, 12, 0.6); 13 | } 14 | 15 | .marker-cluster-large { 16 | background-color: rgba(253, 156, 115, 0.6); 17 | } 18 | .marker-cluster-large div { 19 | background-color: rgba(241, 128, 23, 0.6); 20 | } 21 | 22 | /* IE 6-8 fallback colors */ 23 | .leaflet-oldie .marker-cluster-small { 24 | background-color: rgb(181, 226, 140); 25 | } 26 | .leaflet-oldie .marker-cluster-small div { 27 | background-color: rgb(110, 204, 57); 28 | } 29 | 30 | .leaflet-oldie .marker-cluster-medium { 31 | background-color: rgb(241, 211, 87); 32 | } 33 | .leaflet-oldie .marker-cluster-medium div { 34 | background-color: rgb(240, 194, 12); 35 | } 36 | 37 | .leaflet-oldie .marker-cluster-large { 38 | background-color: rgb(253, 156, 115); 39 | } 40 | .leaflet-oldie .marker-cluster-large div { 41 | background-color: rgb(241, 128, 23); 42 | } 43 | 44 | .marker-cluster { 45 | background-clip: padding-box; 46 | border-radius: 20px; 47 | } 48 | .marker-cluster div { 49 | width: 30px; 50 | height: 30px; 51 | margin-left: 5px; 52 | margin-top: 5px; 53 | 54 | text-align: center; 55 | border-radius: 15px; 56 | font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; 57 | } 58 | .marker-cluster span { 59 | line-height: 30px; 60 | } -------------------------------------------------------------------------------- /assets/leaflet-markercluster/MarkerCluster.css: -------------------------------------------------------------------------------- 1 | .leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow { 2 | -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in; 3 | -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in; 4 | -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in; 5 | transition: transform 0.3s ease-out, opacity 0.3s ease-in; 6 | } 7 | -------------------------------------------------------------------------------- /assets/leaflet-markercluster/leaflet.markercluster.js: -------------------------------------------------------------------------------- 1 | /* 2 | Leaflet.markercluster, Provides Beautiful Animated Marker Clustering functionality for Leaflet, a JS library for interactive maps. 3 | https://github.com/Leaflet/Leaflet.markercluster 4 | (c) 2012-2013, Dave Leaver, smartrak 5 | */ 6 | !function(t,e){L.MarkerClusterGroup=L.FeatureGroup.extend({options:{maxClusterRadius:80,iconCreateFunction:null,spiderfyOnMaxZoom:!0,showCoverageOnHover:!0,zoomToBoundsOnClick:!0,singleMarkerMode:!1,disableClusteringAtZoom:null,removeOutsideVisibleBounds:!0,animateAddingMarkers:!1,spiderfyDistanceMultiplier:1,polygonOptions:{}},initialize:function(t){L.Util.setOptions(this,t),this.options.iconCreateFunction||(this.options.iconCreateFunction=this._defaultIconCreateFunction),this._featureGroup=L.featureGroup(),this._featureGroup.on(L.FeatureGroup.EVENTS,this._propagateEvent,this),this._nonPointGroup=L.featureGroup(),this._nonPointGroup.on(L.FeatureGroup.EVENTS,this._propagateEvent,this),this._inZoomAnimation=0,this._needsClustering=[],this._needsRemoving=[],this._currentShownBounds=null,this._queue=[]},addLayer:function(t){if(t instanceof L.LayerGroup){var e=[];for(var i in t._layers)e.push(t._layers[i]);return this.addLayers(e)}if(!t.getLatLng)return this._nonPointGroup.addLayer(t),this;if(!this._map)return this._needsClustering.push(t),this;if(this.hasLayer(t))return this;this._unspiderfy&&this._unspiderfy(),this._addLayer(t,this._maxZoom);var n=t,s=this._map.getZoom();if(t.__parent)for(;n.__parent._zoom>=s;)n=n.__parent;return this._currentShownBounds.contains(n.getLatLng())&&(this.options.animateAddingMarkers?this._animationAddLayer(t,n):this._animationAddLayerNonAnimated(t,n)),this},removeLayer:function(t){if(t instanceof L.LayerGroup){var e=[];for(var i in t._layers)e.push(t._layers[i]);return this.removeLayers(e)}return t.getLatLng?this._map?t.__parent?(this._unspiderfy&&(this._unspiderfy(),this._unspiderfyLayer(t)),this._removeLayer(t,!0),this._featureGroup.hasLayer(t)&&(this._featureGroup.removeLayer(t),t.setOpacity&&t.setOpacity(1)),this):this:(!this._arraySplice(this._needsClustering,t)&&this.hasLayer(t)&&this._needsRemoving.push(t),this):(this._nonPointGroup.removeLayer(t),this)},addLayers:function(t){var e,i,n,s=this._map,r=this._featureGroup,o=this._nonPointGroup;for(e=0,i=t.length;i>e;e++)if(n=t[e],n.getLatLng){if(!this.hasLayer(n))if(s){if(this._addLayer(n,this._maxZoom),n.__parent&&2===n.__parent.getChildCount()){var a=n.__parent.getAllChildMarkers(),h=a[0]===n?a[1]:a[0];r.removeLayer(h)}}else this._needsClustering.push(n)}else o.addLayer(n);return s&&(r.eachLayer(function(t){t instanceof L.MarkerCluster&&t._iconNeedsUpdate&&t._updateIcon()}),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds)),this},removeLayers:function(t){var e,i,n,s=this._featureGroup,r=this._nonPointGroup;if(!this._map){for(e=0,i=t.length;i>e;e++)n=t[e],this._arraySplice(this._needsClustering,n),r.removeLayer(n);return this}for(e=0,i=t.length;i>e;e++)n=t[e],n.__parent?(this._removeLayer(n,!0,!0),s.hasLayer(n)&&(s.removeLayer(n),n.setOpacity&&n.setOpacity(1))):r.removeLayer(n);return this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds),s.eachLayer(function(t){t instanceof L.MarkerCluster&&t._updateIcon()}),this},clearLayers:function(){return this._map||(this._needsClustering=[],delete this._gridClusters,delete this._gridUnclustered),this._noanimationUnspiderfy&&this._noanimationUnspiderfy(),this._featureGroup.clearLayers(),this._nonPointGroup.clearLayers(),this.eachLayer(function(t){delete t.__parent}),this._map&&this._generateInitialClusters(),this},getBounds:function(){var t=new L.LatLngBounds;if(this._topClusterLevel)t.extend(this._topClusterLevel._bounds);else for(var e=this._needsClustering.length-1;e>=0;e--)t.extend(this._needsClustering[e].getLatLng());return t.extend(this._nonPointGroup.getBounds()),t},eachLayer:function(t,e){var i,n=this._needsClustering.slice();for(this._topClusterLevel&&this._topClusterLevel.getAllChildMarkers(n),i=n.length-1;i>=0;i--)t.call(e,n[i]);this._nonPointGroup.eachLayer(t,e)},getLayers:function(){var t=[];return this.eachLayer(function(e){t.push(e)}),t},getLayer:function(t){var e=null;return this.eachLayer(function(i){L.stamp(i)===t&&(e=i)}),e},hasLayer:function(t){if(!t)return!1;var e,i=this._needsClustering;for(e=i.length-1;e>=0;e--)if(i[e]===t)return!0;for(i=this._needsRemoving,e=i.length-1;e>=0;e--)if(i[e]===t)return!1;return!(!t.__parent||t.__parent._group!==this)||this._nonPointGroup.hasLayer(t)},zoomToShowLayer:function(t,e){var i=function(){if((t._icon||t.__parent._icon)&&!this._inZoomAnimation)if(this._map.off("moveend",i,this),this.off("animationend",i,this),t._icon)e();else if(t.__parent._icon){var n=function(){this.off("spiderfied",n,this),e()};this.on("spiderfied",n,this),t.__parent.spiderfy()}};t._icon&&this._map.getBounds().contains(t.getLatLng())?e():t.__parent._zoome;e++)n=this._needsRemoving[e],this._removeLayer(n,!0);for(this._needsRemoving=[],e=0,i=this._needsClustering.length;i>e;e++)n=this._needsClustering[e],n.getLatLng?n.__parent||this._addLayer(n,this._maxZoom):this._featureGroup.addLayer(n);this._needsClustering=[],this._map.on("zoomend",this._zoomEnd,this),this._map.on("moveend",this._moveEnd,this),this._spiderfierOnAdd&&this._spiderfierOnAdd(),this._bindEvents(),this._zoom=this._map.getZoom(),this._currentShownBounds=this._getExpandedVisibleBounds(),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds)},onRemove:function(t){t.off("zoomend",this._zoomEnd,this),t.off("moveend",this._moveEnd,this),this._unbindEvents(),this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim",""),this._spiderfierOnRemove&&this._spiderfierOnRemove(),this._hideCoverage(),this._featureGroup.onRemove(t),this._nonPointGroup.onRemove(t),this._featureGroup.clearLayers(),this._map=null},getVisibleParent:function(t){for(var e=t;e&&!e._icon;)e=e.__parent;return e||null},_arraySplice:function(t,e){for(var i=t.length-1;i>=0;i--)if(t[i]===e)return t.splice(i,1),!0},_removeLayer:function(t,e,i){var n=this._gridClusters,s=this._gridUnclustered,r=this._featureGroup,o=this._map;if(e)for(var a=this._maxZoom;a>=0&&s[a].removeObject(t,o.project(t.getLatLng(),a));a--);var h,_=t.__parent,u=_._markers;for(this._arraySplice(u,t);_&&(_._childCount--,!(_._zoom<0));)e&&_._childCount<=1?(h=_._markers[0]===t?_._markers[1]:_._markers[0],n[_._zoom].removeObject(_,o.project(_._cLatLng,_._zoom)),s[_._zoom].addObject(h,o.project(h.getLatLng(),_._zoom)),this._arraySplice(_.__parent._childClusters,_),_.__parent._markers.push(h),h.__parent=_.__parent,_._icon&&(r.removeLayer(_),i||r.addLayer(h))):(_._recalculateBounds(),i&&_._icon||_._updateIcon()),_=_.__parent;delete t.__parent},_isOrIsParent:function(t,e){for(;e;){if(t===e)return!0;e=e.parentNode}return!1},_propagateEvent:function(t){if(t.layer instanceof L.MarkerCluster){if(t.originalEvent&&this._isOrIsParent(t.layer._icon,t.originalEvent.relatedTarget))return;t.type="cluster"+t.type}this.fire(t.type,t)},_defaultIconCreateFunction:function(t){var e=t.getChildCount(),i=" marker-cluster-";return i+=10>e?"small":100>e?"medium":"large",new L.DivIcon({html:"
"+e+"
",className:"marker-cluster"+i,iconSize:new L.Point(40,40)})},_bindEvents:function(){var t=this._map,e=this.options.spiderfyOnMaxZoom,i=this.options.showCoverageOnHover,n=this.options.zoomToBoundsOnClick;(e||n)&&this.on("clusterclick",this._zoomOrSpiderfy,this),i&&(this.on("clustermouseover",this._showCoverage,this),this.on("clustermouseout",this._hideCoverage,this),t.on("zoomend",this._hideCoverage,this))},_zoomOrSpiderfy:function(t){var e=this._map;e.getMaxZoom()===e.getZoom()?this.options.spiderfyOnMaxZoom&&t.layer.spiderfy():this.options.zoomToBoundsOnClick&&t.layer.zoomToBounds(),t.originalEvent&&13===t.originalEvent.keyCode&&e._container.focus()},_showCoverage:function(t){var e=this._map;this._inZoomAnimation||(this._shownPolygon&&e.removeLayer(this._shownPolygon),t.layer.getChildCount()>2&&t.layer!==this._spiderfied&&(this._shownPolygon=new L.Polygon(t.layer.getConvexHull(),this.options.polygonOptions),e.addLayer(this._shownPolygon)))},_hideCoverage:function(){this._shownPolygon&&(this._map.removeLayer(this._shownPolygon),this._shownPolygon=null)},_unbindEvents:function(){var t=this.options.spiderfyOnMaxZoom,e=this.options.showCoverageOnHover,i=this.options.zoomToBoundsOnClick,n=this._map;(t||i)&&this.off("clusterclick",this._zoomOrSpiderfy,this),e&&(this.off("clustermouseover",this._showCoverage,this),this.off("clustermouseout",this._hideCoverage,this),n.off("zoomend",this._hideCoverage,this))},_zoomEnd:function(){this._map&&(this._mergeSplitClusters(),this._zoom=this._map._zoom,this._currentShownBounds=this._getExpandedVisibleBounds())},_moveEnd:function(){if(!this._inZoomAnimation){var t=this._getExpandedVisibleBounds();this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,this._zoom,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._map._zoom,t),this._currentShownBounds=t}},_generateInitialClusters:function(){var t=this._map.getMaxZoom(),e=this.options.maxClusterRadius;this.options.disableClusteringAtZoom&&(t=this.options.disableClusteringAtZoom-1),this._maxZoom=t,this._gridClusters={},this._gridUnclustered={};for(var i=t;i>=0;i--)this._gridClusters[i]=new L.DistanceGrid(e),this._gridUnclustered[i]=new L.DistanceGrid(e);this._topClusterLevel=new L.MarkerCluster(this,-1)},_addLayer:function(t,e){var i,n,s=this._gridClusters,r=this._gridUnclustered;for(this.options.singleMarkerMode&&(t.options.icon=this.options.iconCreateFunction({getChildCount:function(){return 1},getAllChildMarkers:function(){return[t]}}));e>=0;e--){i=this._map.project(t.getLatLng(),e);var o=s[e].getNearObject(i);if(o)return o._addChild(t),t.__parent=o,void 0;if(o=r[e].getNearObject(i)){var a=o.__parent;a&&this._removeLayer(o,!1);var h=new L.MarkerCluster(this,e,o,t);s[e].addObject(h,this._map.project(h._cLatLng,e)),o.__parent=h,t.__parent=h;var _=h;for(n=e-1;n>a._zoom;n--)_=new L.MarkerCluster(this,n,_),s[n].addObject(_,this._map.project(o.getLatLng(),n));for(a._addChild(_),n=e;n>=0&&r[n].removeObject(o,this._map.project(o.getLatLng(),n));n--);return}r[e].addObject(t,i)}this._topClusterLevel._addChild(t),t.__parent=this._topClusterLevel},_enqueue:function(t){this._queue.push(t),this._queueTimeout||(this._queueTimeout=setTimeout(L.bind(this._processQueue,this),300))},_processQueue:function(){for(var t=0;tthis._map._zoom?(this._animationStart(),this._animationZoomOut(this._zoom,this._map._zoom)):this._moveEnd()},_getExpandedVisibleBounds:function(){if(!this.options.removeOutsideVisibleBounds)return this.getBounds();var t=this._map,e=t.getBounds(),i=e._southWest,n=e._northEast,s=L.Browser.mobile?0:Math.abs(i.lat-n.lat),r=L.Browser.mobile?0:Math.abs(i.lng-n.lng);return new L.LatLngBounds(new L.LatLng(i.lat-s,i.lng-r,!0),new L.LatLng(n.lat+s,n.lng+r,!0))},_animationAddLayerNonAnimated:function(t,e){if(e===t)this._featureGroup.addLayer(t);else if(2===e._childCount){e._addToMap();var i=e.getAllChildMarkers();this._featureGroup.removeLayer(i[0]),this._featureGroup.removeLayer(i[1])}else e._updateIcon()}}),L.MarkerClusterGroup.include(L.DomUtil.TRANSITION?{_animationStart:function(){this._map._mapPane.className+=" leaflet-cluster-anim",this._inZoomAnimation++},_animationEnd:function(){this._map&&(this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim","")),this._inZoomAnimation--,this.fire("animationend")},_animationZoomIn:function(t,e){var i,n=this._getExpandedVisibleBounds(),s=this._featureGroup;this._topClusterLevel._recursively(n,t,0,function(r){var o,a=r._latlng,h=r._markers;for(n.contains(a)||(a=null),r._isSingleParent()&&t+1===e?(s.removeLayer(r),r._recursivelyAddChildrenToMap(null,e,n)):(r.setOpacity(0),r._recursivelyAddChildrenToMap(a,e,n)),i=h.length-1;i>=0;i--)o=h[i],n.contains(o._latlng)||s.removeLayer(o)}),this._forceLayout(),this._topClusterLevel._recursivelyBecomeVisible(n,e),s.eachLayer(function(t){t instanceof L.MarkerCluster||!t._icon||t.setOpacity(1)}),this._topClusterLevel._recursively(n,t,e,function(t){t._recursivelyRestoreChildPositions(e)}),this._enqueue(function(){this._topClusterLevel._recursively(n,t,0,function(t){s.removeLayer(t),t.setOpacity(1)}),this._animationEnd()})},_animationZoomOut:function(t,e){this._animationZoomOutSingle(this._topClusterLevel,t-1,e),this._topClusterLevel._recursivelyAddChildrenToMap(null,e,this._getExpandedVisibleBounds()),this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,t,this._getExpandedVisibleBounds())},_animationZoomOutSingle:function(t,e,i){var n=this._getExpandedVisibleBounds();t._recursivelyAnimateChildrenInAndAddSelfToMap(n,e+1,i);var s=this;this._forceLayout(),t._recursivelyBecomeVisible(n,i),this._enqueue(function(){if(1===t._childCount){var r=t._markers[0];r.setLatLng(r.getLatLng()),r.setOpacity(1)}else t._recursively(n,i,0,function(t){t._recursivelyRemoveChildrenFromMap(n,e+1)});s._animationEnd()})},_animationAddLayer:function(t,e){var i=this,n=this._featureGroup;n.addLayer(t),e!==t&&(e._childCount>2?(e._updateIcon(),this._forceLayout(),this._animationStart(),t._setPos(this._map.latLngToLayerPoint(e.getLatLng())),t.setOpacity(0),this._enqueue(function(){n.removeLayer(t),t.setOpacity(1),i._animationEnd()})):(this._forceLayout(),i._animationStart(),i._animationZoomOutSingle(e,this._map.getMaxZoom(),this._map.getZoom())))},_forceLayout:function(){L.Util.falseFn(e.body.offsetWidth)}}:{_animationStart:function(){},_animationZoomIn:function(t,e){this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,e,this._getExpandedVisibleBounds())},_animationZoomOut:function(t,e){this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,e,this._getExpandedVisibleBounds())},_animationAddLayer:function(t,e){this._animationAddLayerNonAnimated(t,e)}}),L.markerClusterGroup=function(t){return new L.MarkerClusterGroup(t)},L.MarkerCluster=L.Marker.extend({initialize:function(t,e,i,n){L.Marker.prototype.initialize.call(this,i?i._cLatLng||i.getLatLng():new L.LatLng(0,0),{icon:this}),this._group=t,this._zoom=e,this._markers=[],this._childClusters=[],this._childCount=0,this._iconNeedsUpdate=!0,this._bounds=new L.LatLngBounds,i&&this._addChild(i),n&&this._addChild(n)},getAllChildMarkers:function(t){t=t||[];for(var e=this._childClusters.length-1;e>=0;e--)this._childClusters[e].getAllChildMarkers(t);for(var i=this._markers.length-1;i>=0;i--)t.push(this._markers[i]);return t},getChildCount:function(){return this._childCount},zoomToBounds:function(){for(var t,e=this._childClusters.slice(),i=this._group._map,n=i.getBoundsZoom(this._bounds),s=this._zoom+1,r=i.getZoom();e.length>0&&n>s;){s++;var o=[];for(t=0;ts?this._group._map.setView(this._latlng,s):r>=n?this._group._map.setView(this._latlng,r+1):this._group._map.fitBounds(this._bounds)},getBounds:function(){var t=new L.LatLngBounds;return t.extend(this._bounds),t},_updateIcon:function(){this._iconNeedsUpdate=!0,this._icon&&this.setIcon(this)},createIcon:function(){return this._iconNeedsUpdate&&(this._iconObj=this._group.options.iconCreateFunction(this),this._iconNeedsUpdate=!1),this._iconObj.createIcon()},createShadow:function(){return this._iconObj.createShadow()},_addChild:function(t,e){this._iconNeedsUpdate=!0,this._expandBounds(t),t instanceof L.MarkerCluster?(e||(this._childClusters.push(t),t.__parent=this),this._childCount+=t._childCount):(e||this._markers.push(t),this._childCount++),this.__parent&&this.__parent._addChild(t,!0)},_expandBounds:function(t){var e,i=t._wLatLng||t._latlng;t instanceof L.MarkerCluster?(this._bounds.extend(t._bounds),e=t._childCount):(this._bounds.extend(i),e=1),this._cLatLng||(this._cLatLng=t._cLatLng||i);var n=this._childCount+e;this._wLatLng?(this._wLatLng.lat=(i.lat*e+this._wLatLng.lat*this._childCount)/n,this._wLatLng.lng=(i.lng*e+this._wLatLng.lng*this._childCount)/n):this._latlng=this._wLatLng=new L.LatLng(i.lat,i.lng)},_addToMap:function(t){t&&(this._backupLatlng=this._latlng,this.setLatLng(t)),this._group._featureGroup.addLayer(this)},_recursivelyAnimateChildrenIn:function(t,e,i){this._recursively(t,0,i-1,function(t){var i,n,s=t._markers;for(i=s.length-1;i>=0;i--)n=s[i],n._icon&&(n._setPos(e),n.setOpacity(0))},function(t){var i,n,s=t._childClusters;for(i=s.length-1;i>=0;i--)n=s[i],n._icon&&(n._setPos(e),n.setOpacity(0))})},_recursivelyAnimateChildrenInAndAddSelfToMap:function(t,e,i){this._recursively(t,i,0,function(n){n._recursivelyAnimateChildrenIn(t,n._group._map.latLngToLayerPoint(n.getLatLng()).round(),e),n._isSingleParent()&&e-1===i?(n.setOpacity(1),n._recursivelyRemoveChildrenFromMap(t,e)):n.setOpacity(0),n._addToMap()})},_recursivelyBecomeVisible:function(t,e){this._recursively(t,0,e,null,function(t){t.setOpacity(1)})},_recursivelyAddChildrenToMap:function(t,e,i){this._recursively(i,-1,e,function(n){if(e!==n._zoom)for(var s=n._markers.length-1;s>=0;s--){var r=n._markers[s];i.contains(r._latlng)&&(t&&(r._backupLatlng=r.getLatLng(),r.setLatLng(t),r.setOpacity&&r.setOpacity(0)),n._group._featureGroup.addLayer(r))}},function(e){e._addToMap(t)})},_recursivelyRestoreChildPositions:function(t){for(var e=this._markers.length-1;e>=0;e--){var i=this._markers[e];i._backupLatlng&&(i.setLatLng(i._backupLatlng),delete i._backupLatlng)}if(t-1===this._zoom)for(var n=this._childClusters.length-1;n>=0;n--)this._childClusters[n]._restorePosition();else for(var s=this._childClusters.length-1;s>=0;s--)this._childClusters[s]._recursivelyRestoreChildPositions(t)},_restorePosition:function(){this._backupLatlng&&(this.setLatLng(this._backupLatlng),delete this._backupLatlng)},_recursivelyRemoveChildrenFromMap:function(t,e,i){var n,s;this._recursively(t,-1,e-1,function(t){for(s=t._markers.length-1;s>=0;s--)n=t._markers[s],i&&i.contains(n._latlng)||(t._group._featureGroup.removeLayer(n),n.setOpacity&&n.setOpacity(1))},function(t){for(s=t._childClusters.length-1;s>=0;s--)n=t._childClusters[s],i&&i.contains(n._latlng)||(t._group._featureGroup.removeLayer(n),n.setOpacity&&n.setOpacity(1))})},_recursively:function(t,e,i,n,s){var r,o,a=this._childClusters,h=this._zoom;if(e>h)for(r=a.length-1;r>=0;r--)o=a[r],t.intersects(o._bounds)&&o._recursively(t,e,i,n,s);else if(n&&n(this),s&&this._zoom===i&&s(this),i>h)for(r=a.length-1;r>=0;r--)o=a[r],t.intersects(o._bounds)&&o._recursively(t,e,i,n,s)},_recalculateBounds:function(){var t,e=this._markers,i=this._childClusters;for(this._bounds=new L.LatLngBounds,delete this._wLatLng,t=e.length-1;t>=0;t--)this._expandBounds(e[t]);for(t=i.length-1;t>=0;t--)this._expandBounds(i[t])},_isSingleParent:function(){return this._childClusters.length>0&&this._childClusters[0]._childCount===this._childCount}}),L.DistanceGrid=function(t){this._cellSize=t,this._sqCellSize=t*t,this._grid={},this._objectPoint={}},L.DistanceGrid.prototype={addObject:function(t,e){var i=this._getCoord(e.x),n=this._getCoord(e.y),s=this._grid,r=s[n]=s[n]||{},o=r[i]=r[i]||[],a=L.Util.stamp(t);this._objectPoint[a]=e,o.push(t)},updateObject:function(t,e){this.removeObject(t),this.addObject(t,e)},removeObject:function(t,e){var i,n,s=this._getCoord(e.x),r=this._getCoord(e.y),o=this._grid,a=o[r]=o[r]||{},h=a[s]=a[s]||[];for(delete this._objectPoint[L.Util.stamp(t)],i=0,n=h.length;n>i;i++)if(h[i]===t)return h.splice(i,1),1===n&&delete a[s],!0},eachObject:function(t,e){var i,n,s,r,o,a,h,_=this._grid;for(i in _){o=_[i];for(n in o)for(a=o[n],s=0,r=a.length;r>s;s++)h=t.call(e,a[s]),h&&(s--,r--)}},getNearObject:function(t){var e,i,n,s,r,o,a,h,_=this._getCoord(t.x),u=this._getCoord(t.y),l=this._objectPoint,d=this._sqCellSize,p=null;for(e=u-1;u+1>=e;e++)if(s=this._grid[e])for(i=_-1;_+1>=i;i++)if(r=s[i])for(n=0,o=r.length;o>n;n++)a=r[n],h=this._sqDist(l[L.Util.stamp(a)],t),d>h&&(d=h,p=a);return p},_getCoord:function(t){return Math.floor(t/this._cellSize)},_sqDist:function(t,e){var i=e.x-t.x,n=e.y-t.y;return i*i+n*n}},function(){L.QuickHull={getDistant:function(t,e){var i=e[1].lat-e[0].lat,n=e[0].lng-e[1].lng;return n*(t.lat-e[0].lat)+i*(t.lng-e[0].lng)},findMostDistantPointFromBaseLine:function(t,e){var i,n,s,r=0,o=null,a=[];for(i=e.length-1;i>=0;i--)n=e[i],s=this.getDistant(n,t),s>0&&(a.push(n),s>r&&(r=s,o=n));return{maxPoint:o,newPoints:a}},buildConvexHull:function(t,e){var i=[],n=this.findMostDistantPointFromBaseLine(t,e);return n.maxPoint?(i=i.concat(this.buildConvexHull([t[0],n.maxPoint],n.newPoints)),i=i.concat(this.buildConvexHull([n.maxPoint,t[1]],n.newPoints))):[t[0]]},getConvexHull:function(t){var e,i=!1,n=!1,s=null,r=null;for(e=t.length-1;e>=0;e--){var o=t[e];(i===!1||o.lat>i)&&(s=o,i=o.lat),(n===!1||o.lat=0;e--)t=i[e].getLatLng(),n.push(t);return L.QuickHull.getConvexHull(n)}}),L.MarkerCluster.include({_2PI:2*Math.PI,_circleFootSeparation:25,_circleStartAngle:Math.PI/6,_spiralFootSeparation:28,_spiralLengthStart:11,_spiralLengthFactor:5,_circleSpiralSwitchover:9,spiderfy:function(){if(this._group._spiderfied!==this&&!this._group._inZoomAnimation){var t,e=this.getAllChildMarkers(),i=this._group,n=i._map,s=n.latLngToLayerPoint(this._latlng);this._group._unspiderfy(),this._group._spiderfied=this,e.length>=this._circleSpiralSwitchover?t=this._generatePointsSpiral(e.length,s):(s.y+=10,t=this._generatePointsCircle(e.length,s)),this._animationSpiderfy(e,t)}},unspiderfy:function(t){this._group._inZoomAnimation||(this._animationUnspiderfy(t),this._group._spiderfied=null)},_generatePointsCircle:function(t,e){var i,n,s=this._group.options.spiderfyDistanceMultiplier*this._circleFootSeparation*(2+t),r=s/this._2PI,o=this._2PI/t,a=[];for(a.length=t,i=t-1;i>=0;i--)n=this._circleStartAngle+i*o,a[i]=new L.Point(e.x+r*Math.cos(n),e.y+r*Math.sin(n))._round();return a},_generatePointsSpiral:function(t,e){var i,n=this._group.options.spiderfyDistanceMultiplier*this._spiralLengthStart,s=this._group.options.spiderfyDistanceMultiplier*this._spiralFootSeparation,r=this._group.options.spiderfyDistanceMultiplier*this._spiralLengthFactor,o=0,a=[];for(a.length=t,i=t-1;i>=0;i--)o+=s/n+5e-4*i,a[i]=new L.Point(e.x+n*Math.cos(o),e.y+n*Math.sin(o))._round(),n+=this._2PI*r/o;return a},_noanimationUnspiderfy:function(){var t,e,i=this._group,n=i._map,s=i._featureGroup,r=this.getAllChildMarkers();for(this.setOpacity(1),e=r.length-1;e>=0;e--)t=r[e],s.removeLayer(t),t._preSpiderfyLatlng&&(t.setLatLng(t._preSpiderfyLatlng),delete t._preSpiderfyLatlng),t.setZIndexOffset&&t.setZIndexOffset(0),t._spiderLeg&&(n.removeLayer(t._spiderLeg),delete t._spiderLeg);i._spiderfied=null}}),L.MarkerCluster.include(L.DomUtil.TRANSITION?{SVG_ANIMATION:function(){return e.createElementNS("http://www.w3.org/2000/svg","animate").toString().indexOf("SVGAnimate")>-1}(),_animationSpiderfy:function(t,i){var n,s,r,o,a=this,h=this._group,_=h._map,u=h._featureGroup,l=_.latLngToLayerPoint(this._latlng);for(n=t.length-1;n>=0;n--)s=t[n],s.setOpacity?(s.setZIndexOffset(1e6),s.setOpacity(0),u.addLayer(s),s._setPos(l)):u.addLayer(s);h._forceLayout(),h._animationStart();var d=L.Path.SVG?0:.3,p=L.Path.SVG_NS;for(n=t.length-1;n>=0;n--)if(o=_.layerPointToLatLng(i[n]),s=t[n],s._preSpiderfyLatlng=s._latlng,s.setLatLng(o),s.setOpacity&&s.setOpacity(1),r=new L.Polyline([a._latlng,o],{weight:1.5,color:"#222",opacity:d}),_.addLayer(r),s._spiderLeg=r,L.Path.SVG&&this.SVG_ANIMATION){var c=r._path.getTotalLength();r._path.setAttribute("stroke-dasharray",c+","+c);var m=e.createElementNS(p,"animate");m.setAttribute("attributeName","stroke-dashoffset"),m.setAttribute("begin","indefinite"),m.setAttribute("from",c),m.setAttribute("to",0),m.setAttribute("dur",.25),r._path.appendChild(m),m.beginElement(),m=e.createElementNS(p,"animate"),m.setAttribute("attributeName","stroke-opacity"),m.setAttribute("attributeName","stroke-opacity"),m.setAttribute("begin","indefinite"),m.setAttribute("from",0),m.setAttribute("to",.5),m.setAttribute("dur",.25),r._path.appendChild(m),m.beginElement()}if(a.setOpacity(.3),L.Path.SVG)for(this._group._forceLayout(),n=t.length-1;n>=0;n--)s=t[n]._spiderLeg,s.options.opacity=.5,s._path.setAttribute("stroke-opacity",.5);setTimeout(function(){h._animationEnd(),h.fire("spiderfied")},200)},_animationUnspiderfy:function(t){var e,i,n,s=this._group,r=s._map,o=s._featureGroup,a=t?r._latLngToNewLayerPoint(this._latlng,t.zoom,t.center):r.latLngToLayerPoint(this._latlng),h=this.getAllChildMarkers(),_=L.Path.SVG&&this.SVG_ANIMATION;for(s._animationStart(),this.setOpacity(1),i=h.length-1;i>=0;i--)e=h[i],e._preSpiderfyLatlng&&(e.setLatLng(e._preSpiderfyLatlng),delete e._preSpiderfyLatlng,e.setOpacity?(e._setPos(a),e.setOpacity(0)):o.removeLayer(e),_&&(n=e._spiderLeg._path.childNodes[0],n.setAttribute("to",n.getAttribute("from")),n.setAttribute("from",0),n.beginElement(),n=e._spiderLeg._path.childNodes[1],n.setAttribute("from",.5),n.setAttribute("to",0),n.setAttribute("stroke-opacity",0),n.beginElement(),e._spiderLeg._path.setAttribute("stroke-opacity",0)));setTimeout(function(){var t=0;for(i=h.length-1;i>=0;i--)e=h[i],e._spiderLeg&&t++;for(i=h.length-1;i>=0;i--)e=h[i],e._spiderLeg&&(e.setOpacity&&(e.setOpacity(1),e.setZIndexOffset(0)),t>1&&o.removeLayer(e),r.removeLayer(e._spiderLeg),delete e._spiderLeg);s._animationEnd()},200)}}:{_animationSpiderfy:function(t,e){var i,n,s,r,o=this._group,a=o._map,h=o._featureGroup;for(i=t.length-1;i>=0;i--)r=a.layerPointToLatLng(e[i]),n=t[i],n._preSpiderfyLatlng=n._latlng,n.setLatLng(r),n.setZIndexOffset&&n.setZIndexOffset(1e6),h.addLayer(n),s=new L.Polyline([this._latlng,r],{weight:1.5,color:"#222"}),a.addLayer(s),n._spiderLeg=s;this.setOpacity(.3),o.fire("spiderfied")},_animationUnspiderfy:function(){this._noanimationUnspiderfy()}}),L.MarkerClusterGroup.include({_spiderfied:null,_spiderfierOnAdd:function(){this._map.on("click",this._unspiderfyWrapper,this),this._map.options.zoomAnimation&&this._map.on("zoomstart",this._unspiderfyZoomStart,this),this._map.on("zoomend",this._noanimationUnspiderfy,this),L.Path.SVG&&!L.Browser.touch&&this._map._initPathRoot()},_spiderfierOnRemove:function(){this._map.off("click",this._unspiderfyWrapper,this),this._map.off("zoomstart",this._unspiderfyZoomStart,this),this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy()},_unspiderfyZoomStart:function(){this._map&&this._map.on("zoomanim",this._unspiderfyZoomAnim,this)},_unspiderfyZoomAnim:function(t){L.DomUtil.hasClass(this._map._mapPane,"leaflet-touching")||(this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy(t))},_unspiderfyWrapper:function(){this._unspiderfy()},_unspiderfy:function(t){this._spiderfied&&this._spiderfied.unspiderfy(t)},_noanimationUnspiderfy:function(){this._spiderfied&&this._spiderfied._noanimationUnspiderfy()},_unspiderfyLayer:function(t){t._spiderLeg&&(this._featureGroup.removeLayer(t),t.setOpacity(1),t.setZIndexOffset(0),this._map.removeLayer(t._spiderLeg),delete t._spiderLeg)}})}(window,document); -------------------------------------------------------------------------------- /assets/leaflet-providers/leaflet-providers.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | L.TileLayer.Provider = L.TileLayer.extend({ 5 | initialize: function (arg, options) { 6 | var providers = L.TileLayer.Provider.providers; 7 | 8 | var parts = arg.split('.'); 9 | 10 | var providerName = parts[0]; 11 | var variantName = parts[1]; 12 | 13 | if (!providers[providerName]) { 14 | throw 'No such provider (' + providerName + ')'; 15 | } 16 | 17 | var provider = { 18 | url: providers[providerName].url, 19 | options: providers[providerName].options 20 | }; 21 | 22 | // overwrite values in provider from variant. 23 | if (variantName && 'variants' in providers[providerName]) { 24 | if (!(variantName in providers[providerName].variants)) { 25 | throw 'No such variant of ' + providerName + ' (' + variantName + ')'; 26 | } 27 | var variant = providers[providerName].variants[variantName]; 28 | var variantOptions; 29 | if (typeof variant === 'string') { 30 | variantOptions = { 31 | variant: variant 32 | }; 33 | } else { 34 | variantOptions = variant.options; 35 | } 36 | provider = { 37 | url: variant.url || provider.url, 38 | options: L.Util.extend({}, provider.options, variantOptions) 39 | }; 40 | } else if (typeof provider.url === 'function') { 41 | provider.url = provider.url(parts.splice(1, parts.length - 1).join('.')); 42 | } 43 | 44 | var forceHTTP = window.location.protocol === 'file:' || provider.options.forceHTTP; 45 | if (provider.url.indexOf('//') === 0 && forceHTTP) { 46 | provider.url = 'http:' + provider.url; 47 | } 48 | 49 | // replace attribution placeholders with their values from toplevel provider attribution, 50 | // recursively 51 | var attributionReplacer = function (attr) { 52 | if (attr.indexOf('{attribution.') === -1) { 53 | return attr; 54 | } 55 | return attr.replace(/\{attribution.(\w*)\}/, 56 | function (match, attributionName) { 57 | return attributionReplacer(providers[attributionName].options.attribution); 58 | } 59 | ); 60 | }; 61 | provider.options.attribution = attributionReplacer(provider.options.attribution); 62 | 63 | // Compute final options combining provider options with any user overrides 64 | var layerOpts = L.Util.extend({}, provider.options, options); 65 | L.TileLayer.prototype.initialize.call(this, provider.url, layerOpts); 66 | } 67 | }); 68 | 69 | /** 70 | * Definition of providers. 71 | * see http://leafletjs.com/reference.html#tilelayer for options in the options map. 72 | */ 73 | 74 | L.TileLayer.Provider.providers = { 75 | OpenStreetMap: { 76 | url: '//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 77 | options: { 78 | attribution: 79 | '© OpenStreetMap' 80 | }, 81 | variants: { 82 | Mapnik: {}, 83 | BlackAndWhite: { 84 | url: 'http://{s}.tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png' 85 | }, 86 | DE: { 87 | url: 'http://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png' 88 | }, 89 | HOT: { 90 | url: 'http://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', 91 | options: { 92 | attribution: '{attribution.OpenStreetMap}, Tiles courtesy of Humanitarian OpenStreetMap Team' 93 | } 94 | } 95 | } 96 | }, 97 | OpenSeaMap: { 98 | url: 'http://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', 99 | options: { 100 | attribution: 'Map data: © OpenSeaMap contributors' 101 | } 102 | }, 103 | OpenTopoMap: { 104 | url: '//{s}.tile.opentopomap.org/{z}/{x}/{y}.png', 105 | options: { 106 | maxZoom: 16, 107 | attribution: 'Map data: {attribution.OpenStreetMap}, SRTM | Map style: © OpenTopoMap (CC-BY-SA)' 108 | } 109 | }, 110 | Thunderforest: { 111 | url: '//{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png', 112 | options: { 113 | attribution: 114 | '© OpenCycleMap, {attribution.OpenStreetMap}', 115 | variant: 'cycle' 116 | }, 117 | variants: { 118 | OpenCycleMap: 'cycle', 119 | Transport: 'transport', 120 | TransportDark: 'transport-dark', 121 | Landscape: 'landscape', 122 | Outdoors: 'outdoors' 123 | } 124 | }, 125 | OpenMapSurfer: { 126 | url: 'http://openmapsurfer.uni-hd.de/tiles/{variant}/x={x}&y={y}&z={z}', 127 | options: { 128 | minZoom: 0, 129 | maxZoom: 20, 130 | variant: 'roads', 131 | attribution: 'Imagery from GIScience Research Group @ University of Heidelberg — Map data {attribution.OpenStreetMap}' 132 | }, 133 | variants: { 134 | Roads: 'roads', 135 | AdminBounds: { 136 | options: { 137 | variant: 'adminb', 138 | maxZoom: 19 139 | } 140 | }, 141 | Grayscale: { 142 | options: { 143 | variant: 'roadsg', 144 | maxZoom: 19 145 | } 146 | } 147 | } 148 | }, 149 | Hydda: { 150 | url: 'http://{s}.tile.openstreetmap.se/hydda/{variant}/{z}/{x}/{y}.png', 151 | options: { 152 | minZoom: 0, 153 | maxZoom: 18, 154 | variant: 'full', 155 | attribution: 'Tiles courtesy of OpenStreetMap Sweden — Map data {attribution.OpenStreetMap}' 156 | }, 157 | variants: { 158 | Full: 'full', 159 | Base: 'base', 160 | RoadsAndLabels: 'roads_and_labels' 161 | } 162 | }, 163 | MapQuestOpen: { 164 | /* Mapquest does support https, but with a different subdomain: 165 | * https://otile{s}-s.mqcdn.com/tiles/1.0.0/{type}/{z}/{x}/{y}.{ext} 166 | * which makes implementing protocol relativity impossible. 167 | */ 168 | url: 'http://otile{s}.mqcdn.com/tiles/1.0.0/{type}/{z}/{x}/{y}.{ext}', 169 | options: { 170 | type: 'map', 171 | ext: 'jpg', 172 | attribution: 173 | 'Tiles Courtesy of MapQuest — ' + 174 | 'Map data {attribution.OpenStreetMap}', 175 | subdomains: '1234' 176 | }, 177 | variants: { 178 | OSM: {}, 179 | Aerial: { 180 | options: { 181 | type: 'sat', 182 | attribution: 183 | 'Tiles Courtesy of MapQuest — ' + 184 | 'Portions Courtesy NASA/JPL-Caltech and U.S. Depart. of Agriculture, Farm Service Agency' 185 | } 186 | }, 187 | HybridOverlay: { 188 | options: { 189 | type: 'hyb', 190 | ext: 'png', 191 | opacity: 0.9 192 | } 193 | } 194 | } 195 | }, 196 | MapBox: { 197 | url: function (id) { 198 | return '//{s}.tiles.mapbox.com/v3/' + id + '/{z}/{x}/{y}.png'; 199 | }, 200 | options: { 201 | attribution: 202 | 'Imagery from MapBox — ' + 203 | 'Map data {attribution.OpenStreetMap}', 204 | subdomains: 'abcd' 205 | } 206 | }, 207 | Stamen: { 208 | url: 'http://{s}.tile.stamen.com/{variant}/{z}/{x}/{y}.{ext}', 209 | options: { 210 | attribution: 211 | 'Map tiles by Stamen Design, ' + 212 | 'CC BY 3.0 — ' + 213 | 'Map data {attribution.OpenStreetMap}', 214 | subdomains: 'abcd', 215 | minZoom: 0, 216 | maxZoom: 20, 217 | variant: 'toner', 218 | ext: 'png' 219 | }, 220 | variants: { 221 | Toner: 'toner', 222 | TonerBackground: 'toner-background', 223 | TonerHybrid: 'toner-hybrid', 224 | TonerLines: 'toner-lines', 225 | TonerLabels: 'toner-labels', 226 | TonerLite: 'toner-lite', 227 | Watercolor: { 228 | options: { 229 | variant: 'watercolor', 230 | minZoom: 1, 231 | maxZoom: 16 232 | } 233 | }, 234 | Terrain: { 235 | options: { 236 | variant: 'terrain', 237 | minZoom: 4, 238 | maxZoom: 18, 239 | bounds: [[22, -132], [70, -56]] 240 | } 241 | }, 242 | TerrainBackground: { 243 | options: { 244 | variant: 'terrain-background', 245 | minZoom: 4, 246 | maxZoom: 18, 247 | bounds: [[22, -132], [70, -56]] 248 | } 249 | }, 250 | TopOSMRelief: { 251 | options: { 252 | variant: 'toposm-color-relief', 253 | ext: 'jpg', 254 | bounds: [[22, -132], [51, -56]] 255 | } 256 | }, 257 | TopOSMFeatures: { 258 | options: { 259 | variant: 'toposm-features', 260 | bounds: [[22, -132], [51, -56]], 261 | opacity: 0.9 262 | } 263 | } 264 | } 265 | }, 266 | Esri: { 267 | url: '//server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}', 268 | options: { 269 | variant: 'World_Street_Map', 270 | attribution: 'Tiles © Esri' 271 | }, 272 | variants: { 273 | WorldStreetMap: { 274 | options: { 275 | attribution: 276 | '{attribution.Esri} — ' + 277 | 'Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, Esri China (Hong Kong), Esri (Thailand), TomTom, 2012' 278 | } 279 | }, 280 | DeLorme: { 281 | options: { 282 | variant: 'Specialty/DeLorme_World_Base_Map', 283 | minZoom: 1, 284 | maxZoom: 11, 285 | attribution: '{attribution.Esri} — Copyright: ©2012 DeLorme' 286 | } 287 | }, 288 | WorldTopoMap: { 289 | options: { 290 | variant: 'World_Topo_Map', 291 | attribution: 292 | '{attribution.Esri} — ' + 293 | 'Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community' 294 | } 295 | }, 296 | WorldImagery: { 297 | options: { 298 | variant: 'World_Imagery', 299 | attribution: 300 | '{attribution.Esri} — ' + 301 | 'Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community' 302 | } 303 | }, 304 | WorldTerrain: { 305 | options: { 306 | variant: 'World_Terrain_Base', 307 | maxZoom: 13, 308 | attribution: 309 | '{attribution.Esri} — ' + 310 | 'Source: USGS, Esri, TANA, DeLorme, and NPS' 311 | } 312 | }, 313 | WorldShadedRelief: { 314 | options: { 315 | variant: 'World_Shaded_Relief', 316 | maxZoom: 13, 317 | attribution: '{attribution.Esri} — Source: Esri' 318 | } 319 | }, 320 | WorldPhysical: { 321 | options: { 322 | variant: 'World_Physical_Map', 323 | maxZoom: 8, 324 | attribution: '{attribution.Esri} — Source: US National Park Service' 325 | } 326 | }, 327 | OceanBasemap: { 328 | options: { 329 | variant: 'Ocean_Basemap', 330 | maxZoom: 13, 331 | attribution: '{attribution.Esri} — Sources: GEBCO, NOAA, CHS, OSU, UNH, CSUMB, National Geographic, DeLorme, NAVTEQ, and Esri' 332 | } 333 | }, 334 | NatGeoWorldMap: { 335 | options: { 336 | variant: 'NatGeo_World_Map', 337 | maxZoom: 16, 338 | attribution: '{attribution.Esri} — National Geographic, Esri, DeLorme, NAVTEQ, UNEP-WCMC, USGS, NASA, ESA, METI, NRCAN, GEBCO, NOAA, iPC' 339 | } 340 | }, 341 | WorldGrayCanvas: { 342 | options: { 343 | variant: 'Canvas/World_Light_Gray_Base', 344 | maxZoom: 16, 345 | attribution: '{attribution.Esri} — Esri, DeLorme, NAVTEQ' 346 | } 347 | } 348 | } 349 | }, 350 | OpenWeatherMap: { 351 | url: 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png', 352 | options: { 353 | attribution: 'Map data © OpenWeatherMap', 354 | opacity: 0.5 355 | }, 356 | variants: { 357 | Clouds: 'clouds', 358 | CloudsClassic: 'clouds_cls', 359 | Precipitation: 'precipitation', 360 | PrecipitationClassic: 'precipitation_cls', 361 | Rain: 'rain', 362 | RainClassic: 'rain_cls', 363 | Pressure: 'pressure', 364 | PressureContour: 'pressure_cntr', 365 | Wind: 'wind', 366 | Temperature: 'temp', 367 | Snow: 'snow' 368 | } 369 | }, 370 | HERE: { 371 | /* 372 | * HERE maps, formerly Nokia maps. 373 | * These basemaps are free, but you need an API key. Please sign up at 374 | * http://developer.here.com/getting-started 375 | * 376 | * Note that the base urls contain '.cit' whichs is HERE's 377 | * 'Customer Integration Testing' environment. Please remove for production 378 | * envirionments. 379 | */ 380 | url: 381 | '//{s}.{base}.maps.cit.api.here.com/maptile/2.1/' + 382 | 'maptile/{mapID}/{variant}/{z}/{x}/{y}/256/png8?' + 383 | 'app_id={app_id}&app_code={app_code}', 384 | options: { 385 | attribution: 386 | 'Map © 1987-2014 HERE', 387 | subdomains: '1234', 388 | mapID: 'newest', 389 | 'app_id': '', 390 | 'app_code': '', 391 | base: 'base', 392 | variant: 'normal.day', 393 | minZoom: 0, 394 | maxZoom: 20 395 | }, 396 | variants: { 397 | normalDay: 'normal.day', 398 | normalDayCustom: 'normal.day.custom', 399 | normalDayGrey: 'normal.day.grey', 400 | normalDayMobile: 'normal.day.mobile', 401 | normalDayGreyMobile: 'normal.day.grey.mobile', 402 | normalDayTransit: 'normal.day.transit', 403 | normalDayTransitMobile: 'normal.day.transit.mobile', 404 | normalNight: 'normal.night', 405 | normalNightMobile: 'normal.night.mobile', 406 | normalNightGrey: 'normal.night.grey', 407 | normalNightGreyMobile: 'normal.night.grey.mobile', 408 | 409 | carnavDayGrey: 'carnav.day.grey', 410 | hybridDay: { 411 | options: { 412 | base: 'aerial', 413 | variant: 'hybrid.day' 414 | } 415 | }, 416 | hybridDayMobile: { 417 | options: { 418 | base: 'aerial', 419 | variant: 'hybrid.day.mobile' 420 | } 421 | }, 422 | pedestrianDay: 'pedestrian.day', 423 | pedestrianNight: 'pedestrian.night', 424 | satelliteDay: { 425 | options: { 426 | base: 'aerial', 427 | variant: 'satellite.day' 428 | } 429 | }, 430 | terrainDay: { 431 | options: { 432 | base: 'aerial', 433 | variant: 'terrain.day' 434 | } 435 | }, 436 | terrainDayMobile: { 437 | options: { 438 | base: 'aerial', 439 | variant: 'terrain.day.mobile' 440 | } 441 | } 442 | } 443 | }, 444 | Acetate: { 445 | url: 'http://a{s}.acetate.geoiq.com/tiles/{variant}/{z}/{x}/{y}.png', 446 | options: { 447 | attribution: 448 | '©2012 Esri & Stamen, Data from OSM and Natural Earth', 449 | subdomains: '0123', 450 | minZoom: 2, 451 | maxZoom: 18, 452 | variant: 'acetate-base' 453 | }, 454 | variants: { 455 | basemap: 'acetate-base', 456 | terrain: 'terrain', 457 | all: 'acetate-hillshading', 458 | foreground: 'acetate-fg', 459 | roads: 'acetate-roads', 460 | labels: 'acetate-labels', 461 | hillshading: 'hillshading' 462 | } 463 | }, 464 | FreeMapSK: { 465 | url: 'http://{s}.freemap.sk/T/{z}/{x}/{y}.jpeg', 466 | options: { 467 | minZoom: 8, 468 | maxZoom: 16, 469 | subdomains: ['t1', 't2', 't3', 't4'], 470 | attribution: 471 | '{attribution.OpenStreetMap}, vizualization CC-By-SA 2.0 Freemap.sk' 472 | } 473 | }, 474 | MtbMap: { 475 | url: 'http://tile.mtbmap.cz/mtbmap_tiles/{z}/{x}/{y}.png', 476 | options: { 477 | attribution: 478 | '{attribution.OpenStreetMap} & USGS' 479 | } 480 | }, 481 | CartoDB: { 482 | url: 'http://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}.png', 483 | options: { 484 | attribution: '{attribution.OpenStreetMap} © CartoDB', 485 | subdomains: 'abcd', 486 | minZoom: 0, 487 | maxZoom: 18, 488 | variant: 'light_all' 489 | }, 490 | variants: { 491 | Positron: 'light_all', 492 | PositronNoLabels: 'light_nolabels', 493 | DarkMatter: 'dark_all', 494 | DarkMatterNoLabels: 'dark_nolabels' 495 | } 496 | }, 497 | HikeBike: { 498 | url: 'http://{s}.tiles.wmflabs.org/hikebike/{z}/{x}/{y}.png', 499 | options: { 500 | attribution: '{attribution.OpenStreetMap}' 501 | } 502 | }, 503 | BasemapAT: { 504 | url: '//maps{s}.wien.gv.at/basemap/{variant}/normal/google3857/{z}/{y}/{x}.{format}', 505 | options: { 506 | attribution: 'Datenquelle: basemap.at', 507 | subdomains: ['', '1', '2', '3', '4'], 508 | bounds: [[46.358770, 8.782379], [49.037872, 17.189532]] 509 | }, 510 | variants: { 511 | basemap: { 512 | options: { 513 | variant: 'geolandbasemap', 514 | format: 'jpeg' 515 | } 516 | }, 517 | highdpi: { 518 | options: { 519 | variant: 'bmaphidpi', 520 | format: 'jpeg' 521 | } 522 | }, 523 | grau: { 524 | options: { 525 | variant: 'bmapgrau', 526 | format: 'png' 527 | } 528 | }, 529 | overlay: { 530 | options: { 531 | variant: 'bmapoverlay', 532 | format: 'png' 533 | } 534 | }, 535 | orthofoto: { 536 | options: { 537 | variant: 'bmaporthofoto30cm', 538 | format: 'jpeg' 539 | } 540 | } 541 | } 542 | }, 543 | NASAGIBS: { 544 | url: '//map1.vis.earthdata.nasa.gov/wmts-webmerc/{variant}/default/{time}/{tilematrixset}{maxZoom}/{z}/{y}/{x}.{format}', 545 | options: { 546 | attribution: 547 | 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System ' + 548 | '(ESDIS) with funding provided by NASA/HQ.', 549 | bounds: [[-85.0511287776, -179.999999975], [85.0511287776, 179.999999975]], 550 | minZoom: 1, 551 | maxZoom: 9, 552 | format: 'jpg', 553 | time: '', 554 | tilematrixset: 'GoogleMapsCompatible_Level' 555 | }, 556 | variants: { 557 | ModisTerraTrueColorCR: 'MODIS_Terra_CorrectedReflectance_TrueColor', 558 | ModisTerraBands367CR: 'MODIS_Terra_CorrectedReflectance_Bands367', 559 | ViirsEarthAtNight2012: { 560 | options: { 561 | variant: 'VIIRS_CityLights_2012', 562 | maxZoom: 8 563 | } 564 | }, 565 | ModisTerraLSTDay: { 566 | options: { 567 | variant: 'MODIS_Terra_Land_Surface_Temp_Day', 568 | format: 'png', 569 | maxZoom: 7, 570 | opacity: 0.75 571 | } 572 | }, 573 | ModisTerraSnowCover: { 574 | options: { 575 | variant: 'MODIS_Terra_Snow_Cover', 576 | format: 'png', 577 | maxZoom: 8, 578 | opacity: 0.75 579 | } 580 | }, 581 | ModisTerraAOD: { 582 | options: { 583 | variant: 'MODIS_Terra_Aerosol', 584 | format: 'png', 585 | maxZoom: 6, 586 | opacity: 0.75 587 | } 588 | }, 589 | ModisTerraChlorophyll: { 590 | options: { 591 | variant: 'MODIS_Terra_Chlorophyll_A', 592 | format: 'png', 593 | maxZoom: 7, 594 | opacity: 0.75 595 | } 596 | } 597 | } 598 | } 599 | }; 600 | 601 | L.tileLayer.provider = function (provider, options) { 602 | return new L.TileLayer.Provider(provider, options); 603 | }; 604 | }()); 605 | -------------------------------------------------------------------------------- /examples/.Contacts.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | get('MarkupLeafletMap'); ?> 4 | 5 | 6 | <?php echo $page->title; ?> 7 | 8 | 9 | 10 | 11 | getLeafletMapHeaderLines(); ?> 12 | 13 | 14 |

title; ?>

15 | find("template=Contact"); // Update the selector to choose the pages you want to use 19 | 20 | 21 | $map_options = array( 22 | // Define a callback function that gets called once for each page passed to the map render function 23 | // This will be called to prepare the content of the marker's popup dialog and is given access 24 | // to the page it needs to generate its content. Returns a string that will be included in the JS 25 | // inserted into the page hosting the map. 26 | 'popupFormatter' => function($page) { 27 | $lines = array(); 28 | 29 | // Here's the phone number 30 | if ($page->phone) { 31 | $lines[] = "Phone: {$page->phone}"; 32 | } 33 | 34 | // Here's the geocoded address for this location... 35 | if ($page->location->address) { 36 | $lines[] = "Address: {$page->location->address}"; 37 | } 38 | 39 | // NOTE: If your images are setup to always supply an array, please use this code... 40 | if ($page->image->first()) { 41 | // NB: THIS IS TRICKY. We must not use single quotes in any generated HTML as this is being 42 | // inserted into the Javascript that drives leaflet, so we must escape double quotes within this 43 | // string. 44 | $lines[] = "image->first()->url}\" height=\"100\" width=\"100\" />"; // change 'image' if needed 45 | } 46 | // ELSE: If your images are setup to a single image or null, please comment out the above code and uncomment the following... 47 | // if ($page->image)) { 48 | // $lines[] = "image->url}\" height=\"100\" width=\"100\" />"; // change 'image' if needed 49 | // } 50 | 51 | return implode("
", $lines); 52 | }, 53 | 54 | 55 | // This routine formats the marker icon that gets used on the map. 56 | 'markerFormatter' => function ($page, $marker_options) { 57 | // Pull override values from the $page. 58 | if ($page->marker_icon) { 59 | $marker_options['icon'] = $page->marker_icon; 60 | } 61 | 62 | if ($page->marker_colour->title) { 63 | $marker_options['markerColor'] = $page->marker_colour->title; 64 | } 65 | 66 | // And the icon colour. This is another text field. Colour values like White, Black or an RGB value are ok here. 67 | if ($page->marker_icon_colour) { 68 | $marker_options['iconColor'] = $page->marker_icon_colour; 69 | } 70 | 71 | return $marker_options; 72 | } 73 | ); 74 | 75 | // Generate the HTML and JS that will render the map with a marker for each page in the $items page array 76 | echo $map->render($items, 'location', $map_options); 77 | ?> 78 | 79 | 80 | -------------------------------------------------------------------------------- /images/005.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfProcessWire/FieldtypeLeafletMapMarker/488e840bc6c262f655793b93f324e35f0e2c74c0/images/005.png -------------------------------------------------------------------------------- /images/geocoder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfProcessWire/FieldtypeLeafletMapMarker/488e840bc6c262f655793b93f324e35f0e2c74c0/images/geocoder.png -------------------------------------------------------------------------------- /images/throbber.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfProcessWire/FieldtypeLeafletMapMarker/488e840bc6c262f655793b93f324e35f0e2c74c0/images/throbber.gif -------------------------------------------------------------------------------- /inc/providers.inc: -------------------------------------------------------------------------------- 1 | +OpenStreetMap.Mapnik 2 | OpenStreetMap.BlackAndWhite 3 | OpenStreetMap.DE 4 | OpenStreetMap.HOT 5 | OpenTopoMap 6 | Thunderforest.OpenCycleMap 7 | Thunderforest.Transport 8 | Thunderforest.TransportDark 9 | Thunderforest.Landscape 10 | Thunderforest.Outdoors 11 | OpenMapSurfer.Roads 12 | OpenMapSurfer.Grayscale 13 | Hydda.Full 14 | Hydda.Base 15 | MapQuestOpen.OSM 16 | MapQuestOpen.Aerial 17 | MapBox 18 | Stamen.Toner 19 | Stamen.TonerBackground 20 | Stamen.TonerLite 21 | Stamen.Watercolor 22 | Stamen.Terrain 23 | Stamen.TerrainBackground 24 | Stamen.TopOSMRelief 25 | Esri.WorldStreetMap 26 | Esri.DeLorme 27 | Esri.WorldTopoMap 28 | Esri.WorldImagery 29 | Esri.WorldTerrain 30 | Esri.WorldShadedRelief 31 | Esri.WorldPhysical 32 | Esri.OceanBasemap 33 | Esri.NatGeoWorldMap 34 | Esri.WorldGrayCanvas 35 | HERE.normalDay 36 | HERE.normalDayCustom 37 | HERE.normalDayGrey 38 | HERE.normalDayMobile 39 | HERE.normalDayGreyMobile 40 | HERE.normalDayTransit 41 | HERE.normalDayTransitMobile 42 | HERE.normalNight 43 | HERE.normalNightMobile 44 | HERE.normalNightGrey 45 | HERE.normalNightGreyMobile 46 | HERE.carnavDayGrey 47 | HERE.hybridDay 48 | HERE.hybridDayMobile 49 | HERE.pedestrianDay 50 | HERE.pedestrianNight 51 | HERE.satelliteDay 52 | HERE.terrainDay 53 | HERE.terrainDayMobile 54 | Acetate.basemap 55 | Acetate.terrain 56 | Acetate.all 57 | Acetate.hillshading 58 | FreeMapSK 59 | MtbMap 60 | CartoDB.Positron 61 | CartoDB.PositronNoLabels 62 | CartoDB.DarkMatter 63 | CartoDB.DarkMatterNoLabels 64 | HikeBike 65 | BasemapAT.basemap 66 | BasemapAT.highdpi 67 | BasemapAT.grau 68 | BasemapAT.overlay 69 | BasemapAT.orthofoto 70 | NASAGIBS.ModisTerraTrueColorCR 71 | NASAGIBS.ModisTerraBands367CR 72 | NASAGIBS.ViirsEarthAtNight2012 -------------------------------------------------------------------------------- /inc/providers.json: -------------------------------------------------------------------------------- 1 | { 2 | "providers": [ 3 | "OpenStreetMap.Mapnik", 4 | "OpenStreetMap.BlackAndWhite", 5 | "OpenStreetMap.DE", 6 | "OpenStreetMap.HOT", 7 | "OpenTopoMap", 8 | "Thunderforest.OpenCycleMap", 9 | "Thunderforest.Transport", 10 | "Thunderforest.TransportDark", 11 | "Thunderforest.Landscape", 12 | "Thunderforest.Outdoors", 13 | "OpenMapSurfer.Roads", 14 | "OpenMapSurfer.Grayscale", 15 | "Hydda.Full", 16 | "Hydda.Base", 17 | "MapQuestOpen.OSM", 18 | "MapQuestOpen.Aerial", 19 | "MapBox", 20 | "Stamen.Toner", 21 | "Stamen.TonerBackground", 22 | "Stamen.TonerLite", 23 | "Stamen.Watercolor", 24 | "Stamen.Terrain", 25 | "Stamen.TerrainBackground", 26 | "Stamen.TopOSMRelief", 27 | "Esri.WorldStreetMap", 28 | "Esri.DeLorme", 29 | "Esri.WorldTopoMap", 30 | "Esri.WorldImagery", 31 | "Esri.WorldTerrain", 32 | "Esri.WorldShadedRelief", 33 | "Esri.WorldPhysical", 34 | "Esri.OceanBasemap", 35 | "Esri.NatGeoWorldMap", 36 | "Esri.WorldGrayCanvas", 37 | "HERE.normalDay", 38 | "HERE.normalDayCustom", 39 | "HERE.normalDayGrey", 40 | "HERE.normalDayMobile", 41 | "HERE.normalDayGreyMobile", 42 | "HERE.normalDayTransit", 43 | "HERE.normalDayTransitMobile", 44 | "HERE.normalNight", 45 | "HERE.normalNightMobile", 46 | "HERE.normalNightGrey", 47 | "HERE.normalNightGreyMobile", 48 | "HERE.carnavDayGrey", 49 | "HERE.hybridDay", 50 | "HERE.hybridDayMobile", 51 | "HERE.pedestrianDay", 52 | "HERE.pedestrianNight", 53 | "HERE.satelliteDay", 54 | "HERE.terrainDay", 55 | "HERE.terrainDayMobile", 56 | "Acetate.basemap", 57 | "Acetate.terrain", 58 | "Acetate.all", 59 | "Acetate.hillshading", 60 | "FreeMapSK", 61 | "MtbMap", 62 | "CartoDB.Positron", 63 | "CartoDB.PositronNoLabels", 64 | "CartoDB.DarkMatter", 65 | "CartoDB.DarkMatterNoLabels", 66 | "HikeBike", 67 | "BasemapAT.basemap", 68 | "BasemapAT.highdpi", 69 | "BasemapAT.grau", 70 | "BasemapAT.overlay", 71 | "BasemapAT.orthofoto", 72 | "NASAGIBS.ModisTerraTrueColorCR", 73 | "NASAGIBS.ModisTerraBands367CR", 74 | "NASAGIBS.ViirsEarthAtNight2012" 75 | ] 76 | } --------------------------------------------------------------------------------