fix(ui): consolidate POI info into single context dialog (remove duplicate native Leaflet popup)
All checks were successful
CI / test-and-build (push) Successful in 43s

This commit is contained in:
hermes-explorigin 2026-08-10 20:11:52 +00:00
parent b0147940af
commit a7955c0636

View File

@ -50,7 +50,6 @@
let loading = $state(true); let loading = $state(true);
let error = $state<string | null>(null); let error = $state<string | null>(null);
const layerGroups: Record<string, L.LayerGroup> = {}; const layerGroups: Record<string, L.LayerGroup> = {};
const featureLayers: Record<string, L.Layer & { bindPopup: (s: string) => void }> = {};
// Which layers are currently displayed on the map. // Which layers are currently displayed on the map.
let visible = $state<Record<string, boolean>>({}); let visible = $state<Record<string, boolean>>({});
let totalFeatures = $state(0); let totalFeatures = $state(0);
@ -96,19 +95,17 @@
const results = $derived(query.trim().length >= 2 ? search(query, searchables) : []); const results = $derived(query.trim().length >= 2 ? search(query, searchables) : []);
let showResults = $state(false); let showResults = $state(false);
function focusOn(item: Searchable, openPopup = true) { function focusOn(item: Searchable, openDialog = true) {
if (!map) return; if (!map) return;
if (item.kind === 'zone' && item.bounds) { if (item.kind === 'zone' && item.bounds) {
map.fitBounds(item.bounds as L.LatLngBoundsExpression, { padding: [60, 60] }); map.fitBounds(item.bounds as L.LatLngBoundsExpression, { padding: [60, 60] });
} else { } else {
map.setView([item.lat, item.lon], Math.max(map.getZoom(), 15)); map.setView([item.lat, item.lon], Math.max(map.getZoom(), 15));
} }
if (openPopup) { // Selecting a search result opens the same context dialog used for a
const fl = featureLayers[item.id]; // direct map click, so there is a single info/actions bubble everywhere.
if (fl && typeof fl.bindPopup === 'function') { if (openDialog) {
// openPopup is available on markers / path layers openContextAt({ lat: item.lat, lng: item.lon } as L.LatLng, item.feature ? featureInfoFor(item.feature) : null);
(fl as unknown as { openPopup: () => void }).openPopup?.();
}
} }
} }
@ -268,8 +265,6 @@
iconSize: [0, 0] iconSize: [0, 0]
}) })
}).addTo(savedPointLayer); }).addTo(savedPointLayer);
const nm = p.label || `${p.lat.toFixed(4)}, ${p.lon.toFixed(4)}`;
mk.bindPopup(`<strong>${nm.replace(/</g, '&lt;')}</strong><br><em class="popup-desc">Saved point</em>`);
mk.on('click', (e: L.LeafletMouseEvent) => { mk.on('click', (e: L.LeafletMouseEvent) => {
L.DomEvent.stopPropagation(e.originalEvent); L.DomEvent.stopPropagation(e.originalEvent);
openContextAt(e.latlng, null); openContextAt(e.latlng, null);
@ -392,37 +387,6 @@
return { color, weight: 2, fillColor: color, fillOpacity: 0.25 }; return { color, weight: 2, fillColor: color, fillOpacity: 0.25 };
} }
const COMPASS = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];
function compassPoint(deg: number): string {
const idx = Math.round((((deg % 360) + 360) % 360) / 22.5) % 16;
return COMPASS[idx];
}
function popupHtml(f: GeoJSON.Feature): string {
const props = (f.properties ?? {}) as Record<string, unknown>;
const name = (props.name as string | undefined) ?? String(f.id ?? '');
let extra = '';
if (props.address) extra += `<tr><th>Address</th><td>${String(props.address)}</td></tr>`;
if (props.bearing !== undefined) {
const b = Number(props.bearing);
extra += `<tr><th>Facing</th><td>${compassPoint(b)} (${b}&deg;)</td></tr>`;
}
if (props.fov !== undefined) {
extra += `<tr><th>Field of view</th><td>${String(props.fov)}&deg;</td></tr>`;
}
const rows = Object.entries(props)
.filter(([k]) => !['name', 'address', 'bearing', 'fov', 'description'].includes(k) && !k.startsWith('_'))
.map(([k, v]) => `<tr><th>${k}</th><td>${String(v)}</td></tr>`)
.join('');
let desc = '';
if (props.description) desc = `<div class="popup-desc">${String(props.description)}</div>`;
return `<strong>${name}</strong>${desc}<table class="popup">${extra}${rows}</table>`;
}
onMount(async () => { onMount(async () => {
let L; let L;
try { try {
@ -493,14 +457,12 @@
onEachFeature: (f, lay) => { onEachFeature: (f, lay) => {
totalFeatures++; totalFeatures++;
const props = (f.properties ?? {}) as Record<string, unknown>; const props = (f.properties ?? {}) as Record<string, unknown>;
lay.bindPopup(popupHtml(f));
// Clicking a feature opens the context dialog with its info and // Clicking a feature opens the context dialog with its info and
// stops propagation so the map's empty-click handler doesn't fire. // stops propagation so the map's empty-click handler doesn't fire.
lay.on('click', (e: L.LeafletMouseEvent) => { lay.on('click', (e: L.LeafletMouseEvent) => {
L.DomEvent.stopPropagation(e.originalEvent); L.DomEvent.stopPropagation(e.originalEvent);
openContextAt(e.latlng, featureInfoFor(f)); openContextAt(e.latlng, featureInfoFor(f));
}); });
featureLayers[f.id as string] = lay;
// Build a searchable record. A single malformed feature // Build a searchable record. A single malformed feature
// must not break the whole layer load. // must not break the whole layer load.
@ -783,10 +745,6 @@
.swatch { display: inline-block; width: 12px; height: 12px; border-radius: 3px; border: 1px solid #cbd5e1; } .swatch { display: inline-block; width: 12px; height: 12px; border-radius: 3px; border: 1px solid #cbd5e1; }
.lbl { flex: 1; } .lbl { flex: 1; }
.count { color: #94a3b8; font-size: 0.7rem; } .count { color: #94a3b8; font-size: 0.7rem; }
:global(.popup) { border-collapse: collapse; margin-top: 4px; font-size: 0.75rem; }
:global(.popup th) { text-align: left; padding-right: 10px; color: #64748b; font-weight: 600; }
:global(.popup td) { padding: 1px 0; }
:global(.popup-desc) { color: #475569; margin-top: 2px; font-size: 0.75rem; }
:global(.dir-marker-wrap) { background: none; border: none; } :global(.dir-marker-wrap) { background: none; border: none; }
:global(.dir-arrow) { :global(.dir-arrow) {
width: 0; height: 0; width: 0; height: 0;