"use strict"; // Shared modal "location picker". Every view's Find button opens this overlay: // a search box + a Leaflet map. Searching a place jumps the map (and a matching // result selects it immediately); tapping anywhere on the map drops a pin the user // can fine-tune before confirming. Picking closes the overlay and hands the chosen // lat/lon back to the page via the onPick callback. // // Loaded on every page AFTER leaflet.js and BEFORE that page's own script. The map // itself is created lazily the first time the picker opens (and reused after), so // pages that never open it pay nothing. (function () { let overlay, mapEl, map, marker, pin = null, onPickCb = null, pinIcon = null; let searchForm, searchInput, sugEl, confirmBtn, hintEl; const PIN_ICON = ``; function build() { overlay = document.createElement("div"); overlay.className = "mp-overlay"; overlay.hidden = true; overlay.innerHTML = ` `; document.body.appendChild(overlay); mapEl = overlay.querySelector(".mp-map"); searchForm = overlay.querySelector(".mp-search"); searchInput = searchForm.querySelector("input"); sugEl = overlay.querySelector(".mp-sug"); confirmBtn = overlay.querySelector(".mp-confirm"); hintEl = overlay.querySelector(".mp-hint"); overlay.querySelector(".mp-close").onclick = close; // Tapping the dimmed backdrop (outside the modal) closes the picker. overlay.addEventListener("pointerdown", (e) => { if (e.target === overlay) close(); }); confirmBtn.onclick = () => finish(pin); searchForm.addEventListener("submit", async (e) => { e.preventDefault(); const q = searchInput.value.trim(); if (!q) return; try { const res = await fetch(`api/v2/geocode?q=${encodeURIComponent(q)}`); const d = await res.json(); renderSug(d.results || []); } catch (err) { /* leave the map as the fallback way to pick */ } }); // Hide the suggestions when tapping elsewhere in the modal (but not the map, // which has its own tap handler). overlay.addEventListener("pointerdown", (e) => { if (!e.target.closest(".mp-search")) sugEl.hidden = true; }, true); document.addEventListener("keydown", (e) => { if (overlay && !overlay.hidden && e.key === "Escape") close(); }); } function renderSug(list) { const usca = list.filter((r) => ["US", "CA"].includes(r.country_code)); const shown = usca.length ? usca : list; sugEl.innerHTML = ""; if (!shown.length) { sugEl.hidden = true; return; } shown.forEach((r) => { const li = document.createElement("li"); li.innerHTML = `${r.name} — ${[r.admin1, r.country].filter(Boolean).join(", ")}`; // A picked search result is an unambiguous choice — drop the pin, recenter, // and select it right away (the map is there for manual/fine picks). li.onclick = () => { sugEl.hidden = true; searchInput.value = r.name; setPin(r.lat, r.lon, 10); finish({ lat: r.lat, lon: r.lon }); }; sugEl.appendChild(li); }); sugEl.hidden = false; } function setPin(lat, lon, zoom) { pin = { lat, lon }; if (marker) marker.setLatLng([lat, lon]); else marker = L.marker([lat, lon], pinIcon ? { icon: pinIcon } : undefined).addTo(map); map.setView([lat, lon], zoom || map.getZoom()); confirmBtn.disabled = false; hintEl.textContent = "Pin set — confirm below, or tap elsewhere to move it."; } function finish(p) { if (!p) return; const cb = onPickCb; close(); if (cb) cb(p.lat, p.lon); } function open(cur, onPick) { if (!overlay) build(); onPickCb = onPick; pin = null; confirmBtn.disabled = true; sugEl.hidden = true; searchInput.value = ""; hintEl.textContent = "Search above, or tap the map to drop a pin."; overlay.hidden = false; if (!map) { map = L.map(mapEl, { worldCopyJump: true, zoomControl: true }).setView([44.5, -95], 4); // Colorful CARTO "Voyager" basemap (no key needed) instead of the plain gray // OSM raster — used in both themes for its richer road/land/water colors. // Requesting tiles one zoom level lower and rendering them at 512px // (tileSize 512 + zoomOffset -1) makes roads and city labels noticeably // larger; @2x keeps them crisp while enlarged. A saturation boost (style.css) // pushes the colors further. L.tileLayer("https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}@2x.png", { subdomains: "abcd", maxZoom: 20, tileSize: 512, zoomOffset: -1, attribution: '© OpenStreetMap © CARTO', }).addTo(map); // Branded accent teardrop pin (the same glyph the Find button uses), styled // in style.css — replaces Leaflet's default blue raster marker. pinIcon = L.divIcon({ className: "mp-pin", html: PIN_ICON, iconSize: [32, 32], iconAnchor: [16, 30], }); map.on("click", (e) => setPin(e.latlng.lat, e.latlng.lng)); } // Leaflet measures the container on creation; it's zero-sized while hidden, so // recalc once the modal is actually visible and (if reopening on a known spot) // drop a pin there as the starting point. setTimeout(() => { map.invalidateSize(); if (cur && cur.lat != null && cur.lon != null) setPin(cur.lat, cur.lon, 10); searchInput.focus(); }, 60); } function close() { if (overlay) overlay.hidden = true; } window.LocationPicker = { open, PIN_ICON }; })();