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
All checks were successful
CI / test-and-build (push) Successful in 43s
This commit is contained in:
parent
b0147940af
commit
a7955c0636
@ -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, '<')}</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}°)</td></tr>`;
|
|
||||||
}
|
|
||||||
if (props.fov !== undefined) {
|
|
||||||
extra += `<tr><th>Field of view</th><td>${String(props.fov)}°</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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user