Skip to content

Commit 3bce6f1

Browse files
authored
Merge pull request #24 from hothsys/features/3d-satellite
Feature: 3D satellite, category search and service worker updates
2 parents 5374476 + 481c2c4 commit 3bce6f1

8 files changed

Lines changed: 170 additions & 39 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
| 🗓 Timeline | Chronological photo browser |
5858
| 🖼 Lightbox | Full-size viewer with navigation and camera info |
5959
| 📝 Notes | Add notes to any pin |
60-
| 🛰 Map styles | Light, Bright, Dark, Terrain, 3D Terrain, Satellite, Globe |
60+
| 🛰 Map styles | Light, Bright, Dark, Terrain, 3D Terrain, Satellite, 3D Satellite, Globe |
6161
| 🔄 Clustering | Pins cluster by zoom, expand on click |
6262
| 💾 Auto-save | Background backup to disk via `serve.py` |
6363
| 📦 Export / Import | Full dataset as compressed `.json.gz` |

css/styles.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,15 @@ input,textarea,select{font-family:var(--font)}
225225
.dest-item-name{font-weight:500;color:var(--text)}
226226
.dest-item-detail{font-size:.66rem;color:var(--muted);margin-top:1px}
227227
#dest-loading{padding:10px 12px;font-size:.73rem;color:var(--muted);display:none}
228+
#search-cat-wrap{position:relative}
229+
#search-cat-btn{background:var(--surface2);border:1px solid var(--border);color:var(--muted);font-size:.73rem;font-weight:500;padding:5px 10px;border-radius:20px;cursor:pointer;white-space:nowrap;transition:all .15s;line-height:1}
230+
#search-cat-btn:hover{background:var(--surface3);color:var(--text)}
231+
#search-cat-btn.active{border-color:var(--accent);color:var(--accent)}
232+
#search-cat-menu{position:absolute;top:calc(100% + 6px);right:0;background:var(--surface);border:1px solid var(--border);border-radius:10px;box-shadow:var(--shadow);display:none;z-index:100;min-width:130px;overflow:hidden}
233+
#search-cat-menu.open{display:block}
234+
.search-cat-item{padding:8px 14px;font-size:.73rem;cursor:pointer;transition:background .12s;color:var(--text)}
235+
.search-cat-item:hover{background:var(--surface2)}
236+
.search-cat-item.active{color:var(--accent);font-weight:500}
228237

229238
/* PIN MARKERS (canvas-rendered via symbol layer — no DOM positioning) */
230239
/* Cluster markers still use DOM for click expand */

index.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,15 @@
120120
<button id="dest-clear" onclick="clearDestSearch()"></button>
121121
<div id="dest-results"><div id="dest-loading">Searching…</div></div>
122122
</div>
123+
<div id="search-cat-wrap">
124+
<button id="search-cat-btn" onclick="toggleSearchCatMenu()"><span id="search-cat-label">All</span></button>
125+
<div id="search-cat-menu">
126+
<div class="search-cat-item active" data-cat="all" onclick="setSearchCat('all')">All</div>
127+
<div class="search-cat-item" data-cat="hotel" onclick="setSearchCat('hotel')">🏨 Hotel</div>
128+
<div class="search-cat-item" data-cat="restaurant" onclick="setSearchCat('restaurant')">🍽 Restaurant</div>
129+
<div class="search-cat-item" data-cat="attraction" onclick="setSearchCat('attraction')">🏛 Attraction</div>
130+
</div>
131+
</div>
123132
<div id="tile-spinner" class="tile-spinner"></div>
124133
<div class="tb-sep"></div>
125134
<div id="map-style-wrap" style="position:relative">
@@ -131,6 +140,7 @@
131140
<div class="style-menu-item" data-style="enriched" onclick="setMapStyle('enriched')">Terrain</div>
132141
<div class="style-menu-item" data-style="terrain3d" onclick="setMapStyle('terrain3d')">3D Terrain</div>
133142
<div class="style-menu-item" data-style="satellite" onclick="setMapStyle('satellite')">Satellite</div>
143+
<div class="style-menu-item" data-style="satellite3d" onclick="setMapStyle('satellite3d')">3D Satellite</div>
134144
<div class="style-menu-item" data-style="globe" onclick="setMapStyle('globe')">Globe</div>
135145
</div>
136146
</div>

js/data.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ async function autoSave() {
405405
if (!_autoSaveAvailable) return;
406406
try {
407407
// Upload new photo files that haven't been saved yet
408-
const uploads = photos.filter(p => !_savedPhotoDisk.has(p.id) && p.dataUrl && p.dataUrl.startsWith('data:'));
408+
const uploads = photos.filter(p => !_savedPhotoDisk.has(p.id) && !p.isEmptyPin);
409409
await Promise.all(uploads.map(p => uploadPhotoFile(p)));
410410

411411
// Build metadata-only payload with file paths instead of base64
@@ -704,9 +704,6 @@ window.addEventListener('offline', () => updateOfflineState(true));
704704
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
705705
if ('serviceWorker' in navigator && !isSafari) {
706706
navigator.serviceWorker.register('/sw.js', { updateViaCache: 'none' }).catch(err => console.warn('SW registration failed:', err));
707-
navigator.serviceWorker.ready.then(reg => {
708-
if (reg.active) reg.active.postMessage({ type: 'set-port', port: location.port || '8765' });
709-
});
710707
} else if (isSafari && navigator.serviceWorker) {
711708
// Unregister any existing SW in Safari to clean up stale state
712709
navigator.serviceWorker.getRegistrations().then(regs => regs.forEach(r => r.unregister()));

js/map.js

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// MAP
33
// ═══════════════════════════════════════
44
function _styleUrl() {
5-
if (_mapStyle === 'satellite') return STYLE_SAT;
5+
if (_mapStyle === 'satellite' || _mapStyle === 'satellite3d') return STYLE_SAT;
66
if (_mapStyle === 'dark') return STYLE_DARK;
77
if (_mapStyle === 'bright') return STYLE_BRIGHT;
88
return STYLE_STREET; // light, enriched, terrain3d, globe all use Liberty as base
@@ -236,7 +236,7 @@ function _scaledTextSize(orig) {
236236
}
237237

238238
function applyLabelScale() {
239-
if (_mapStyle === 'satellite') return;
239+
if (_mapStyle === 'satellite' || _mapStyle === 'satellite3d') return;
240240
const style = map.getStyle();
241241
if (!style || !style.layers) return;
242242
for (const layer of style.layers) {
@@ -251,7 +251,7 @@ function applyLabelScale() {
251251
}
252252

253253
function applyLabelVisibility() {
254-
if (_mapStyle === 'satellite') return;
254+
if (_mapStyle === 'satellite' || _mapStyle === 'satellite3d') return;
255255
const vis = labelsVisible ? 'visible' : 'none';
256256
const style = map.getStyle();
257257
if (!style || !style.layers) return;
@@ -276,7 +276,7 @@ function toggleLabels() {
276276
// Apply vivid parks, airport runways, mountain peaks, and optional 3D buildings
277277
function applyExtraLayers() {
278278
const hasOmt = map.getSource && map.getSource('openmaptiles');
279-
if (!hasOmt || ['satellite', 'globe'].includes(_mapStyle)) return;
279+
if (!hasOmt || ['satellite', 'satellite3d', 'globe'].includes(_mapStyle)) return;
280280

281281
// Vivid park fill — more saturated green over the base park layer
282282
if (!map.getLayer('matrix-park-vivid')) {
@@ -357,7 +357,7 @@ function applyExtraLayers() {
357357

358358
function apply3DBuildings() {
359359
if (!map.getSource || !map.getSource('openmaptiles')) return;
360-
const shouldShow = buildings3DVisible && !['satellite', 'terrain3d', 'globe'].includes(_mapStyle);
360+
const shouldShow = buildings3DVisible && !['satellite', 'satellite3d', 'terrain3d', 'globe'].includes(_mapStyle);
361361
if (shouldShow && !map.getLayer('matrix-buildings-3d')) {
362362
// Insert below the first symbol layer so road/POI labels render on top of buildings
363363
const firstSymbol = map.getStyle()?.layers?.find(l => l.type === 'symbol');
@@ -418,7 +418,7 @@ function addPinLayers() {
418418
if (!alreadyThere) {
419419
// In 3D Terrain (pitched view), offset the focal point downward in screen space
420420
// so the pin appears in the visible ground area rather than drifting off-screen
421-
const pitchOffset = _mapStyle === 'terrain3d' ? [0, Math.round(map.transform?.height * 0.15 || 100)] : [0, 0];
421+
const pitchOffset = (_mapStyle === 'terrain3d' || _mapStyle === 'satellite3d') ? [0, Math.round(map.transform?.height * 0.15 || 100)] : [0, 0];
422422
map.flyTo({ center: [lng, lat], zoom: targetZoom, speed: 0.8, curve: 1.0, essential: true,
423423
offset: pitchOffset,
424424
easing: t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2 });
@@ -446,7 +446,7 @@ function applyTheme() {
446446
_tileTemplatesCache = null;
447447
// Set initial button label
448448
const btn = document.getElementById('tb-style-btn');
449-
const labels = { light: 'Light Map', bright: 'Bright Map', enriched: 'Terrain', dark: 'Dark Map', satellite: 'Satellite', terrain3d: '3D Terrain', globe: 'Globe' };
449+
const labels = { light: 'Light Map', bright: 'Bright Map', enriched: 'Terrain', dark: 'Dark Map', satellite: 'Satellite', satellite3d: '3D Satellite', terrain3d: '3D Terrain', globe: 'Globe' };
450450
if (btn) btn.textContent = (labels[_mapStyle] || _mapStyle) + ' ▾';
451451

452452
// Set initial active state in menu
@@ -624,7 +624,7 @@ async function initMap() {
624624
map.getCanvas().addEventListener('mouseout', () => { coordsEl.style.display = 'none'; });
625625
const updateZoom = () => {
626626
zoomEl.textContent = 'z' + map.getZoom().toFixed(2);
627-
const is3D = _mapStyle === 'terrain3d';
627+
const is3D = _mapStyle === 'terrain3d' || _mapStyle === 'satellite3d';
628628
if (is3D) {
629629
pitchWrap.style.display = 'flex';
630630
pitchEl.textContent = `p${map.getPitch().toFixed(0)}° b${map.getBearing().toFixed(0)}°`;
@@ -676,7 +676,7 @@ async function initMap() {
676676
bldgBtn.innerHTML = '<span style="font-size:11px;font-weight:700;line-height:29px;display:block;color:var(--text);opacity:.7;font-family:var(--font)">3D</span>';
677677
bldgBtn.addEventListener('click', toggle3DBuildings);
678678
bldgWrap.appendChild(bldgBtn);
679-
if (['satellite', 'terrain3d', 'globe'].includes(_mapStyle)) bldgWrap.style.display = 'none';
679+
if (['satellite', 'satellite3d', 'terrain3d', 'globe'].includes(_mapStyle)) bldgWrap.style.display = 'none';
680680
navGroup.before(bldgWrap);
681681
}
682682
applyLabelScale();
@@ -701,7 +701,7 @@ async function initMap() {
701701
// Water clicks allowed at any zoom; land clicks require zoom >= 5.
702702
// Satellite/terrain3d/globe have no vector layers for water detection.
703703
const allHits = map.queryRenderedFeatures(e.point);
704-
const noVectorLayers = ['satellite', 'terrain3d', 'globe'].includes(_mapStyle);
704+
const noVectorLayers = ['satellite', 'satellite3d', 'terrain3d', 'globe'].includes(_mapStyle);
705705
const isWater = !noVectorLayers && allHits.some(f => f.layer.type === 'fill' && /^(water|ocean)/.test(f.layer.id));
706706
// If style is mid-transition (queryRenderedFeatures returns nothing), treat as land
707707
if (!isWater && map.getZoom() < 5) return;
@@ -765,7 +765,7 @@ async function initMap() {
765765

766766
// Elevation — only in 3D Terrain mode
767767
let elevationStr = '';
768-
if (_mapStyle === 'terrain3d') {
768+
if (_mapStyle === 'terrain3d' || _mapStyle === 'satellite3d') {
769769
try {
770770
const elev = map.queryTerrainElevation([lng, lat]);
771771
if (elev !== null && elev !== undefined) {
@@ -871,21 +871,21 @@ function toggleStyleMenu(e) {
871871
function setMapStyle(mode) {
872872
const wasDark = _mapStyle === 'dark';
873873
_mapStyle = mode;
874-
// Persist style preference (satellite resets to previous on reload)
875-
if (mode !== 'satellite') localStorage.setItem('matrix-theme', mode);
874+
// Persist style preference (satellite/satellite3d reset to previous on reload)
875+
if (mode !== 'satellite' && mode !== 'satellite3d') localStorage.setItem('matrix-theme', mode);
876876

877877
// Defer dark-map CSS class removal until pin icons are re-added with correct
878878
// compensation (otherwise pre-darkened images render without the CSS filter)
879879
const mapEl = document.getElementById('map');
880880
if (_mapStyle === 'dark') mapEl.classList.add('dark-map');
881-
mapEl.classList.toggle('sat-mode', ['satellite', 'terrain3d', 'globe'].includes(_mapStyle));
881+
mapEl.classList.toggle('sat-mode', ['satellite', 'satellite3d', 'terrain3d', 'globe'].includes(_mapStyle));
882882

883883
// Labels toggle visibility
884884
const labelsWrap = document.getElementById('labels-toggle-wrap');
885-
if (labelsWrap) labelsWrap.style.visibility = _mapStyle === 'satellite' ? 'hidden' : 'visible';
885+
if (labelsWrap) labelsWrap.style.visibility = (_mapStyle === 'satellite' || _mapStyle === 'satellite3d') ? 'hidden' : 'visible';
886886
// Hide 3D buildings toggle in modes where buildings don't make sense
887887
const bldgWrap = document.getElementById('buildings-toggle-wrap');
888-
if (bldgWrap) bldgWrap.style.display = ['satellite', 'terrain3d', 'globe'].includes(_mapStyle) ? 'none' : '';
888+
if (bldgWrap) bldgWrap.style.display = ['satellite', 'satellite3d', 'terrain3d', 'globe'].includes(_mapStyle) ? 'none' : '';
889889

890890
// Disable Export Video in Globe mode (flyTo animation doesn't translate to globe projection)
891891
const exportBtn = document.getElementById('tb-export-video');
@@ -895,7 +895,7 @@ function setMapStyle(mode) {
895895
}
896896

897897
// Update button label
898-
const labels = { light: 'Light Map', bright: 'Bright Map', enriched: 'Terrain', dark: 'Dark Map', satellite: 'Satellite', terrain3d: '3D Terrain', globe: 'Globe' };
898+
const labels = { light: 'Light Map', bright: 'Bright Map', enriched: 'Terrain', dark: 'Dark Map', satellite: 'Satellite', satellite3d: '3D Satellite', terrain3d: '3D Terrain', globe: 'Globe' };
899899
const btn = document.getElementById('tb-style-btn');
900900
if (btn) btn.textContent = (labels[mode] || mode) + ' ▾';
901901

@@ -914,7 +914,7 @@ function setMapStyle(mode) {
914914
}
915915

916916
function _applyTerrainAndProjection() {
917-
const is3D = _mapStyle === 'terrain3d';
917+
const is3D = _mapStyle === 'terrain3d' || _mapStyle === 'satellite3d';
918918
const isGlobe = _mapStyle === 'globe';
919919

920920
// Set projection FIRST — before any camera moves — so easeTo never runs under the wrong projection
@@ -956,7 +956,7 @@ function _applyTerrainAndProjection() {
956956
if (map.getLayer('sky') === undefined) {
957957
map.addLayer({ id: 'sky', type: 'sky', paint: { 'sky-type': 'gradient', 'sky-gradient': ['interpolate', ['linear'], ['sky-radial-progress'], 0, 'rgba(255,255,255,0)', 0.5, '#87cefa', 1, '#4682b4'] } });
958958
}
959-
if (!map.getLayer('terrain-hillshade')) {
959+
if (_mapStyle !== 'satellite3d' && !map.getLayer('terrain-hillshade')) {
960960
// Insert below the first road/label layer so hillshading shows through
961961
const firstSymbol = map.getStyle().layers.find(l => l.type === 'line' || l.type === 'symbol');
962962
map.addLayer({

js/search.js

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,55 @@ const dClear = document.getElementById('dest-clear');
99

1010
const _searchCache = {};
1111
let _searchMoveTimer = null;
12+
let _searchCategory = 'all';
13+
14+
const POI_CATEGORIES = {
15+
hotel: { label: '🏨 Hotel', tags: [['amenity','hotel'],['tourism','hotel']] },
16+
restaurant: { label: '🍽 Restaurant', tags: [['amenity','restaurant'],['amenity','cafe'],['amenity','bar'],['amenity','pub'],['amenity','fast_food']] },
17+
attraction: { label: '🏛 Attraction', tags: [['tourism','attraction'],['tourism','museum'],['tourism','viewpoint']] },
18+
};
19+
20+
function toggleSearchCatMenu() {
21+
document.getElementById('search-cat-menu').classList.toggle('open');
22+
}
23+
24+
function setSearchCat(cat) {
25+
_searchCategory = cat;
26+
document.getElementById('search-cat-label').textContent = cat === 'all' ? 'All' : POI_CATEGORIES[cat].label;
27+
document.getElementById('search-cat-btn').classList.toggle('active', cat !== 'all');
28+
document.querySelectorAll('.search-cat-item').forEach(el => el.classList.toggle('active', el.dataset.cat === cat));
29+
document.getElementById('search-cat-menu').classList.remove('open');
30+
Object.keys(_searchCache).forEach(k => delete _searchCache[k]);
31+
const q = dInput.value.trim();
32+
if (q.length >= 2) {
33+
dResults.style.display = 'block';
34+
runDestSearch(q);
35+
} else if (cat !== 'all') {
36+
dResults.style.display = 'block';
37+
dLoading.style.display = 'none';
38+
dResults.innerHTML = '<div style="padding:9px 12px;font-size:.73rem;color:var(--muted)">Type to search nearby</div>';
39+
}
40+
}
1241

1342
// Re-run visible search when the map viewport changes (pan/zoom)
1443
function _onMapMoveForSearch() {
1544
clearTimeout(_searchMoveTimer);
1645
const q = dInput.value.trim();
17-
if (q.length < 2 || dResults.style.display === 'none' || destMarkerObj) return;
46+
if ((q.length < 2 && _searchCategory === 'all') || dResults.style.display === 'none' || destMarkerObj) return;
1847
_searchMoveTimer = setTimeout(() => runDestSearch(q), 600);
1948
}
2049

2150
dInput.addEventListener('input', () => {
2251
const q = dInput.value.trim();
2352
dClear.style.display = q ? 'block' : 'none';
2453
clearTimeout(searchTimer);
25-
if (q.length < 2) { dResults.style.display='none'; return; }
54+
if (q.length < 2 && _searchCategory === 'all') { dResults.style.display='none'; return; }
2655
searchTimer = setTimeout(() => runDestSearch(q), 380);
2756
});
2857
dInput.addEventListener('keydown', e => { if(e.key==='Escape'){clearDestSearch();dInput.blur();} });
2958
document.addEventListener('click', e => {
3059
if (!document.getElementById('dest-search-wrap').contains(e.target)) dResults.style.display='none';
60+
if (!document.getElementById('search-cat-wrap').contains(e.target)) document.getElementById('search-cat-menu').classList.remove('open');
3161
});
3262

3363

@@ -130,6 +160,69 @@ async function runReverseGeoSearch(lat, lng) {
130160
}
131161
}
132162

163+
async function runCategorySearch(q, cat, cacheKey) {
164+
if (!map || q.length < 2) {
165+
dLoading.style.display = 'none';
166+
dResults.innerHTML = '<div style="padding:9px 12px;font-size:.73rem;color:var(--muted)">Type to search nearby</div>';
167+
return;
168+
}
169+
const b = map.getBounds();
170+
171+
// --- Attempt 1: Overpass via local proxy (precise tag-based POI search) ---
172+
const bbox = `(${b.getSouth().toFixed(5)},${b.getWest().toFixed(5)},${b.getNorth().toFixed(5)},${b.getEast().toFixed(5)})`;
173+
const safe = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
174+
const nameFilter = `["name"~"${safe}",i]`;
175+
const stmts = POI_CATEGORIES[cat].tags.flatMap(([k, v]) => [
176+
`node["${k}"="${v}"]${nameFilter}${bbox};`,
177+
`way["${k}"="${v}"]${nameFilter}${bbox};`,
178+
`relation["${k}"="${v}"]${nameFilter}${bbox};`,
179+
]).join('\n ');
180+
const query = `[out:json][timeout:10];\n(\n ${stmts}\n);\nout center 20;`;
181+
try {
182+
const r = await fetch('/api/overpass', { method: 'POST', body: new URLSearchParams({ data: query }) });
183+
if (r.ok) {
184+
const data = await r.json();
185+
const results = (data.elements || [])
186+
.filter(el => el.tags?.name)
187+
.map(el => {
188+
const t = el.tags, lat = el.lat ?? el.center?.lat, lon = el.lon ?? el.center?.lon;
189+
if (lat == null || lon == null) return null;
190+
const city = t['addr:city'] || t['addr:town'] || t['addr:village'] || '';
191+
const country = t['addr:country'] || '';
192+
return {
193+
display_name: [t.name, city, country].filter(Boolean).join(', '),
194+
lat: String(lat), lon: String(lon),
195+
address: { city, country }
196+
};
197+
})
198+
.filter(Boolean);
199+
_searchCache[cacheKey] = results;
200+
renderSearchResults(results);
201+
return;
202+
}
203+
} catch(_) { /* proxy unreachable — fall through to Nominatim */ }
204+
205+
// --- Fallback: Nominatim bounded=1 + layer=poi (works everywhere, less precise) ---
206+
const viewbox = `&viewbox=${b.getWest()},${b.getNorth()},${b.getEast()},${b.getSouth()}&bounded=1&layer=poi`;
207+
const searchWait = Math.max(0, 1100 - (Date.now() - _lastNominatimCall));
208+
if (searchWait > 0) await new Promise(r => setTimeout(r, searchWait));
209+
_lastNominatimCall = Date.now();
210+
try {
211+
const r = await fetch(
212+
`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(q)}&format=json&limit=20&addressdetails=1${viewbox}`,
213+
{ headers: { 'Accept-Language': 'en' } }
214+
);
215+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
216+
const data = await r.json();
217+
_searchCache[cacheKey] = data;
218+
renderSearchResults(data);
219+
} catch(err) {
220+
console.warn('Category search failed:', err);
221+
dLoading.style.display = 'none';
222+
dResults.innerHTML = `<div style="padding:9px 12px;font-size:.73rem;color:var(--accent2)">Search failed — try again</div>`;
223+
}
224+
}
225+
133226
async function runDestSearch(q) {
134227
if (_isOffline) {
135228
dResults.style.display='block'; dLoading.style.display='none';
@@ -153,8 +246,13 @@ async function runDestSearch(q) {
153246
}
154247

155248
const zoom = map ? map.getZoom() : 0;
156-
const cacheKey = q + (zoom >= 3 ? `@${map.getCenter().lng.toFixed(1)},${map.getCenter().lat.toFixed(1)}` : '');
249+
const cacheKey = q + (zoom >= 3 ? `@${map.getCenter().lng.toFixed(1)},${map.getCenter().lat.toFixed(1)}` : '') + (_searchCategory !== 'all' ? `#${_searchCategory}` : '');
157250
if (_searchCache[cacheKey]) { renderSearchResults(_searchCache[cacheKey]); return; }
251+
252+
if (_searchCategory !== 'all') {
253+
await runCategorySearch(q, _searchCategory, cacheKey);
254+
return;
255+
}
158256
try {
159257
const searchWait = Math.max(0, 1100 - (Date.now() - _lastNominatimCall));
160258
if (searchWait > 0) await new Promise(r => setTimeout(r, searchWait));

0 commit comments

Comments
 (0)