]> git.openstreetmap.org Git - rails.git/blob - app/assets/javascripts/leaflet.map.js
d053a81ce4d873a44c0e9d7e8ce0834668193beb
[rails.git] / app / assets / javascripts / leaflet.map.js
1 //= require qs/dist/qs
2
3 L.extend(L.LatLngBounds.prototype, {
4   getSize: function () {
5     return (this._northEast.lat - this._southWest.lat) *
6            (this._northEast.lng - this._southWest.lng);
7   },
8
9   wrap: function () {
10     return new L.LatLngBounds(this._southWest.wrap(), this._northEast.wrap());
11   }
12 });
13
14 L.OSM.Map = L.Map.extend({
15   initialize: function (id, options) {
16     L.Map.prototype.initialize.call(this, id, options);
17
18     const layerDefinitions = [
19       {
20         leafletOsmId: "Mapnik",
21         code: "M",
22         keyId: "mapnik",
23         nameId: "standard",
24         credit: {
25           id: "make_a_donation",
26           href: "https://supporting.openstreetmap.org",
27           donate: true
28         }
29       },
30       {
31         leafletOsmId: "CyclOSM",
32         code: "Y",
33         keyId: "cyclosm",
34         nameId: "cyclosm",
35         credit: {
36           id: "cyclosm_credit",
37           children: {
38             cyclosm_link: {
39               id: "cyclosm_name",
40               href: "https://www.cyclosm.org"
41             },
42             osm_france_link: {
43               id: "osm_france",
44               href: "https://openstreetmap.fr/"
45             }
46           }
47         }
48       },
49       {
50         leafletOsmId: "CycleMap",
51         code: "C",
52         keyId: "cyclemap",
53         nameId: "cycle_map",
54         apiKeyId: "THUNDERFOREST_KEY",
55         credit: {
56           id: "thunderforest_credit",
57           children: {
58             thunderforest_link: {
59               id: "andy_allan",
60               href: "https://www.thunderforest.com/"
61             }
62           }
63         }
64       },
65       {
66         leafletOsmId: "TransportMap",
67         code: "T",
68         keyId: "transportmap",
69         nameId: "transport_map",
70         apiKeyId: "THUNDERFOREST_KEY",
71         credit: {
72           id: "thunderforest_credit",
73           children: {
74             thunderforest_link: {
75               id: "andy_allan",
76               href: "https://www.thunderforest.com/"
77             }
78           }
79         }
80       },
81       {
82         leafletOsmId: "TracestrackTopo",
83         code: "P",
84         keyId: "tracestracktopo",
85         nameId: "tracestracktop_topo",
86         apiKeyId: "TRACESTRACK_KEY",
87         credit: {
88           id: "tracestrack_credit",
89           children: {
90             tracestrack_link: {
91               id: "tracestrack",
92               href: "https://www.tracestrack.com/"
93             }
94           }
95         }
96       },
97       {
98         leafletOsmId: "HOT",
99         code: "H",
100         keyId: "hot",
101         nameId: "hot",
102         credit: {
103           id: "hotosm_credit",
104           children: {
105             hotosm_link: {
106               id: "hotosm_name",
107               href: "https://www.hotosm.org/"
108             },
109             osm_france_link: {
110               id: "osm_france",
111               href: "https://openstreetmap.fr/"
112             }
113           }
114         }
115       }
116     ];
117
118     this.baseLayers = [];
119
120     for (const layerDefinition of layerDefinitions) {
121       if (layerDefinition.apiKeyId && !OSM[layerDefinition.apiKeyId]) continue;
122
123       const layerOptions = {
124         attribution: makeAttribution(layerDefinition.credit),
125         code: layerDefinition.code,
126         keyid: layerDefinition.keyId,
127         name: I18n.t(`javascripts.map.base.${layerDefinition.nameId}`)
128       };
129       if (layerDefinition.apiKeyId) {
130         layerOptions.apikey = OSM[layerDefinition.apiKeyId];
131       }
132
133       const layer = new L.OSM[layerDefinition.leafletOsmId](layerOptions);
134       this.baseLayers.push(layer);
135     }
136
137     this.noteLayer = new L.FeatureGroup();
138     this.noteLayer.options = { code: "N" };
139
140     this.dataLayer = new L.OSM.DataLayer(null);
141     this.dataLayer.options.code = "D";
142
143     this.gpsLayer = new L.OSM.GPS({
144       pane: "overlayPane",
145       code: "G"
146     });
147
148     this.on("layeradd", function (event) {
149       if (this.baseLayers.indexOf(event.layer) >= 0) {
150         this.setMaxZoom(event.layer.options.maxZoom);
151       }
152     });
153
154     function makeAttribution(credit) {
155       let attribution = "";
156
157       attribution += I18n.t("javascripts.map.copyright_text", {
158         copyright_link: $("<a>", {
159           href: "/copyright",
160           text: I18n.t("javascripts.map.openstreetmap_contributors")
161         }).prop("outerHTML")
162       });
163
164       attribution += credit.donate ? " &hearts; " : ". ";
165       attribution += makeCredit(credit);
166       attribution += ". ";
167
168       attribution += $("<a>", {
169         href: "https://wiki.osmfoundation.org/wiki/Terms_of_Use",
170         text: I18n.t("javascripts.map.website_and_api_terms")
171       }).prop("outerHTML");
172
173       return attribution;
174     }
175
176     function makeCredit(credit) {
177       const children = {};
178       for (const childId in credit.children) {
179         children[childId] = makeCredit(credit.children[childId]);
180       }
181       const text = I18n.t(`javascripts.map.${credit.id}`, children);
182       if (credit.href) {
183         const link = $("<a>", {
184           href: credit.href,
185           text: text
186         });
187         if (credit.donate) {
188           link.addClass("donate-attr");
189         } else {
190           link.attr("target", "_blank");
191         }
192         return link.prop("outerHTML");
193       } else {
194         return text;
195       }
196     }
197   },
198
199   updateLayers: function (layerParam) {
200     var layers = layerParam || "M",
201         layersAdded = "";
202
203     for (var i = this.baseLayers.length - 1; i >= 0; i--) {
204       if (layers.indexOf(this.baseLayers[i].options.code) >= 0) {
205         this.addLayer(this.baseLayers[i]);
206         layersAdded = layersAdded + this.baseLayers[i].options.code;
207       } else if (i === 0 && layersAdded === "") {
208         this.addLayer(this.baseLayers[i]);
209       } else {
210         this.removeLayer(this.baseLayers[i]);
211       }
212     }
213   },
214
215   getLayersCode: function () {
216     var layerConfig = "";
217     this.eachLayer(function (layer) {
218       if (layer.options && layer.options.code) {
219         layerConfig += layer.options.code;
220       }
221     });
222     return layerConfig;
223   },
224
225   getMapBaseLayerId: function () {
226     var baseLayerId;
227     this.eachLayer(function (layer) {
228       if (layer.options && layer.options.keyid) baseLayerId = layer.options.keyid;
229     });
230     return baseLayerId;
231   },
232
233   getUrl: function (marker) {
234     var precision = OSM.zoomPrecision(this.getZoom()),
235         params = {};
236
237     if (marker && this.hasLayer(marker)) {
238       var latLng = marker.getLatLng().wrap();
239       params.mlat = latLng.lat.toFixed(precision);
240       params.mlon = latLng.lng.toFixed(precision);
241     }
242
243     var url = window.location.protocol + "//" + OSM.SERVER_URL + "/",
244         query = Qs.stringify(params),
245         hash = OSM.formatHash(this);
246
247     if (query) url += "?" + query;
248     if (hash) url += hash;
249
250     return url;
251   },
252
253   getShortUrl: function (marker) {
254     var zoom = this.getZoom(),
255         latLng = marker && this.hasLayer(marker) ? marker.getLatLng().wrap() : this.getCenter().wrap(),
256         str = window.location.hostname.match(/^www\.openstreetmap\.org/i) ?
257           window.location.protocol + "//osm.org/go/" :
258           window.location.protocol + "//" + window.location.hostname + "/go/",
259         char_array = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_~",
260         x = Math.round((latLng.lng + 180.0) * ((1 << 30) / 90.0)),
261         y = Math.round((latLng.lat + 90.0) * ((1 << 30) / 45.0)),
262         // JavaScript only has to keep 32 bits of bitwise operators, so this has to be
263         // done in two parts. each of the parts c1/c2 has 30 bits of the total in it
264         // and drops the last 4 bits of the full 64 bit Morton code.
265         c1 = interlace(x >>> 17, y >>> 17), c2 = interlace((x >>> 2) & 0x7fff, (y >>> 2) & 0x7fff),
266         digit,
267         i;
268
269     for (i = 0; i < Math.ceil((zoom + 8) / 3.0) && i < 5; ++i) {
270       digit = (c1 >> (24 - (6 * i))) & 0x3f;
271       str += char_array.charAt(digit);
272     }
273     for (i = 5; i < Math.ceil((zoom + 8) / 3.0); ++i) {
274       digit = (c2 >> (24 - (6 * (i - 5)))) & 0x3f;
275       str += char_array.charAt(digit);
276     }
277     for (i = 0; i < ((zoom + 8) % 3); ++i) str += "-";
278
279     // Called to interlace the bits in x and y, making a Morton code.
280     function interlace(x, y) {
281       var interlaced_x = x,
282           interlaced_y = y;
283       interlaced_x = (interlaced_x | (interlaced_x << 8)) & 0x00ff00ff;
284       interlaced_x = (interlaced_x | (interlaced_x << 4)) & 0x0f0f0f0f;
285       interlaced_x = (interlaced_x | (interlaced_x << 2)) & 0x33333333;
286       interlaced_x = (interlaced_x | (interlaced_x << 1)) & 0x55555555;
287       interlaced_y = (interlaced_y | (interlaced_y << 8)) & 0x00ff00ff;
288       interlaced_y = (interlaced_y | (interlaced_y << 4)) & 0x0f0f0f0f;
289       interlaced_y = (interlaced_y | (interlaced_y << 2)) & 0x33333333;
290       interlaced_y = (interlaced_y | (interlaced_y << 1)) & 0x55555555;
291       return (interlaced_x << 1) | interlaced_y;
292     }
293
294     var params = {};
295     var layers = this.getLayersCode().replace("M", "");
296
297     if (layers) {
298       params.layers = layers;
299     }
300
301     if (marker && this.hasLayer(marker)) {
302       params.m = "";
303     }
304
305     if (this._object) {
306       params[this._object.type] = this._object.id;
307     }
308
309     var query = Qs.stringify(params);
310     if (query) {
311       str += "?" + query;
312     }
313
314     return str;
315   },
316
317   getGeoUri: function (marker) {
318     var precision = OSM.zoomPrecision(this.getZoom()),
319         latLng,
320         params = {};
321
322     if (marker && this.hasLayer(marker)) {
323       latLng = marker.getLatLng().wrap();
324     } else {
325       latLng = this.getCenter();
326     }
327
328     params.lat = latLng.lat.toFixed(precision);
329     params.lon = latLng.lng.toFixed(precision);
330     params.zoom = this.getZoom();
331
332     return "geo:" + params.lat + "," + params.lon + "?z=" + params.zoom;
333   },
334
335   addObject: function (object, callback) {
336     var objectStyle = {
337       color: "#FF6200",
338       weight: 4,
339       opacity: 1,
340       fillOpacity: 0.5
341     };
342
343     var changesetStyle = {
344       weight: 4,
345       color: "#FF9500",
346       opacity: 1,
347       fillOpacity: 0,
348       interactive: false
349     };
350
351     var haloStyle = {
352       weight: 2.5,
353       radius: 20,
354       fillOpacity: 0.5,
355       color: "#FF6200"
356     };
357
358     this.removeObject();
359
360     if (object.type === "note") {
361       this._objectLoader = {
362         abort: function () {}
363       };
364
365       this._object = object;
366       this._objectLayer = L.featureGroup().addTo(this);
367
368       L.circleMarker(object.latLng, haloStyle).addTo(this._objectLayer);
369
370       if (object.icon) {
371         L.marker(object.latLng, {
372           icon: object.icon,
373           opacity: 1,
374           interactive: true
375         }).addTo(this._objectLayer);
376       }
377
378       if (callback) callback(this._objectLayer.getBounds());
379     } else { // element or changeset handled by L.OSM.DataLayer
380       var map = this;
381       this._objectLoader = $.ajax({
382         url: OSM.apiUrl(object),
383         dataType: "xml",
384         success: function (xml) {
385           map._object = object;
386
387           map._objectLayer = new L.OSM.DataLayer(null, {
388             styles: {
389               node: objectStyle,
390               way: objectStyle,
391               area: objectStyle,
392               changeset: changesetStyle
393             }
394           });
395
396           map._objectLayer.interestingNode = function (node, ways, relations) {
397             if (object.type === "node") {
398               return true;
399             } else if (object.type === "relation") {
400               for (var i = 0; i < relations.length; i++) {
401                 if (relations[i].members.indexOf(node) !== -1) return true;
402               }
403             } else {
404               return false;
405             }
406           };
407
408           map._objectLayer.addData(xml);
409           map._objectLayer.addTo(map);
410
411           if (callback) callback(map._objectLayer.getBounds());
412         }
413       });
414     }
415   },
416
417   removeObject: function () {
418     this._object = null;
419     if (this._objectLoader) this._objectLoader.abort();
420     if (this._objectLayer) this.removeLayer(this._objectLayer);
421   },
422
423   getState: function () {
424     return {
425       center: this.getCenter().wrap(),
426       zoom: this.getZoom(),
427       layers: this.getLayersCode()
428     };
429   },
430
431   setState: function (state, options) {
432     if (state.center) this.setView(state.center, state.zoom, options);
433     if (state.layers) this.updateLayers(state.layers);
434   },
435
436   setSidebarOverlaid: function (overlaid) {
437     var sidebarWidth = 350;
438     if (overlaid && !$("#content").hasClass("overlay-sidebar")) {
439       $("#content").addClass("overlay-sidebar");
440       this.invalidateSize({ pan: false });
441       if ($("html").attr("dir") !== "rtl") {
442         this.panBy([-sidebarWidth, 0], { animate: false });
443       }
444     } else if (!overlaid && $("#content").hasClass("overlay-sidebar")) {
445       if ($("html").attr("dir") !== "rtl") {
446         this.panBy([sidebarWidth, 0], { animate: false });
447       }
448       $("#content").removeClass("overlay-sidebar");
449       this.invalidateSize({ pan: false });
450     }
451     return this;
452   }
453 });
454
455 L.Icon.Default.imagePath = "/images/";
456
457 L.Icon.Default.imageUrls = {
458   "/images/marker-icon.png": OSM.MARKER_ICON,
459   "/images/marker-icon-2x.png": OSM.MARKER_ICON_2X,
460   "/images/marker-shadow.png": OSM.MARKER_SHADOW
461 };
462
463 L.extend(L.Icon.Default.prototype, {
464   _oldGetIconUrl: L.Icon.Default.prototype._getIconUrl,
465
466   _getIconUrl: function (name) {
467     var url = this._oldGetIconUrl(name);
468     return L.Icon.Default.imageUrls[url];
469   }
470 });
471
472 OSM.getUserIcon = function (url) {
473   return L.icon({
474     iconUrl: url || OSM.MARKER_RED,
475     iconSize: [25, 41],
476     iconAnchor: [12, 41],
477     popupAnchor: [1, -34],
478     shadowUrl: OSM.MARKER_SHADOW,
479     shadowSize: [41, 41]
480   });
481 };