├── 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 .= "" . 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