]> git.openstreetmap.org Git - rails.git/blob - app/assets/javascripts/index_modules/directions/endpoint.js
Merge pull request #7191 from CommanderStorm/fix-attribution-listener-leak
[rails.git] / app / assets / javascripts / index_modules / directions / endpoint.js
1 /* exported Endpoint */
2 function Endpoint(map, input, marker, dragCallback, changeCallback) {
3   const endpoint = {};
4
5   endpoint.marker = L.marker([0, 0], {
6     icon: OSM.getMarker(marker),
7     draggable: true,
8     autoPan: true
9   });
10
11   endpoint.enableListeners = function () {
12     endpoint.marker.on("drag dragend", markerDragListener);
13     input.on("keydown", inputKeydownListener);
14     input.on("change", inputChangeListener);
15   };
16
17   endpoint.disableListeners = function () {
18     endpoint.marker.off("drag dragend", markerDragListener);
19     input.off("keydown", inputKeydownListener);
20     input.off("change", inputChangeListener);
21   };
22
23   function markerDragListener(e) {
24     const { lat, lng } = OSM.cropLocation(e.target.getLatLng(), map.getZoom());
25     const latlng = L.latLng(lat, lng);
26
27     if (endpoint.geocodeRequest) endpoint.geocodeRequest.abort();
28     delete endpoint.geocodeRequest;
29
30     setLatLng(latlng);
31     setInputValueFromLatLng(latlng);
32     endpoint.value = input.val();
33     if (e.type === "dragend") getReverseGeocode();
34     dragCallback(e.type === "drag");
35   }
36
37   function inputKeydownListener() {
38     input.removeClass("is-invalid");
39   }
40
41   function inputChangeListener(e) {
42     // make text the same in both text boxes
43     const value = e.target.value;
44     endpoint.setValue(value);
45   }
46
47   endpoint.setValue = function (value) {
48     if (endpoint.geocodeRequest) endpoint.geocodeRequest.abort();
49     delete endpoint.geocodeRequest;
50     input.removeClass("is-invalid");
51
52     const coordinatesMatch = value.match(/^\s*([+-]?\d+(?:\.\d*)?)(?:\s+|\s*[/,]\s*)([+-]?\d+(?:\.\d*)?)\s*$/);
53     const latlng = coordinatesMatch && L.latLng(coordinatesMatch[1], coordinatesMatch[2]);
54
55     if (latlng && endpoint.cachedReverseGeocode && endpoint.cachedReverseGeocode.latlng.equals(latlng)) {
56       setLatLng(latlng);
57       if (endpoint.cachedReverseGeocode.notFound) {
58         endpoint.value = value;
59         input.addClass("is-invalid");
60       } else {
61         endpoint.value = endpoint.cachedReverseGeocode.value;
62       }
63       input.val(endpoint.value);
64       changeCallback();
65       return;
66     }
67
68     endpoint.value = value;
69     removeLatLng();
70     input.val(value);
71
72     if (latlng) {
73       setLatLng(latlng);
74       setInputValueFromLatLng(latlng);
75       getReverseGeocode();
76       changeCallback();
77     } else if (endpoint.value) {
78       getGeocode();
79     }
80   };
81
82   endpoint.clearValue = function () {
83     if (endpoint.geocodeRequest) endpoint.geocodeRequest.abort();
84     delete endpoint.geocodeRequest;
85     removeLatLng();
86     delete endpoint.value;
87     input.val("");
88     map.removeLayer(endpoint.marker);
89   };
90
91   endpoint.swapCachedReverseGeocodes = function (otherEndpoint) {
92     const g0 = endpoint.cachedReverseGeocode;
93     const g1 = otherEndpoint.cachedReverseGeocode;
94     delete endpoint.cachedReverseGeocode;
95     delete otherEndpoint.cachedReverseGeocode;
96     if (g0) otherEndpoint.cachedReverseGeocode = g0;
97     if (g1) endpoint.cachedReverseGeocode = g1;
98   };
99
100   function findPreferredEntrance(entrances, types) {
101     if (!entrances) return null;
102
103     for (const t of types) {
104       const matchId = entrances.find(e => e.type === t);
105       if (matchId) return matchId;
106     }
107
108     return null;
109   }
110
111   function getGeocode() {
112     const viewbox = map.getBounds().toBBoxString(), // <sw lon>,<sw lat>,<ne lon>,<ne lat>
113           geocodeUrl = OSM.NOMINATIM_URL + "search?" + new URLSearchParams({ q: endpoint.value, format: "json", viewbox, limit: 1, entrances: 1 });
114
115     endpoint.geocodeRequest = new AbortController();
116     fetch(geocodeUrl, { signal: endpoint.geocodeRequest.signal })
117       .then(r => r.json())
118       .then(success)
119       .catch(() => {});
120
121     function success(json) {
122       delete endpoint.geocodeRequest;
123       if (json.length === 0) {
124         input.addClass("is-invalid");
125         OSM.showAlert(OSM.i18n.t("javascripts.directions.errors.no_place.title"),
126                       OSM.i18n.t("javascripts.directions.errors.no_place.body", { place: endpoint.value }));
127         return;
128       }
129
130       setLatLng(L.latLng(findPreferredEntrance(json[0].entrances, ["main", "yes"]) || json[0]));
131
132       endpoint.value = json[0].display_name;
133       input.val(json[0].display_name);
134
135       changeCallback();
136     }
137   }
138
139   function getReverseGeocode() {
140     const latlng = endpoint.latlng.clone(),
141           { lat, lng } = latlng,
142           reverseGeocodeUrl = OSM.NOMINATIM_URL + "reverse?" + new URLSearchParams({ lat, lon: lng, format: "json" });
143
144     endpoint.geocodeRequest = new AbortController();
145     fetch(reverseGeocodeUrl, { signal: endpoint.geocodeRequest.signal })
146       .then(r => r.json())
147       .then(success)
148       .catch(() => {});
149
150     function success(json) {
151       delete endpoint.geocodeRequest;
152       if (!json || !json.display_name) {
153         endpoint.cachedReverseGeocode = { latlng: latlng, notFound: true };
154         return;
155       }
156
157       endpoint.value = json.display_name;
158       input.val(json.display_name);
159       endpoint.cachedReverseGeocode = { latlng: latlng, value: endpoint.value };
160     }
161   }
162
163   function setLatLng(ll) {
164     input
165       .attr("data-lat", ll.lat)
166       .attr("data-lon", ll.lng);
167     endpoint.latlng = ll;
168     endpoint.marker
169       .setLatLng(ll)
170       .addTo(map);
171   }
172
173   function removeLatLng() {
174     input
175       .removeAttr("data-lat")
176       .removeAttr("data-lon");
177     delete endpoint.latlng;
178   }
179
180   function setInputValueFromLatLng(latlng) {
181     input.val(latlng.lat + ", " + latlng.lng);
182   }
183
184   return endpoint;
185 };