Skip to content

Commit 1768fcc

Browse files
authored
refactor(map) :: import and use 3rd party Leaflet types (#1514)
* fix(chart) :: name refrence line feilds explictly `chart.handlebars` emits 6 keys for a reference line and stringifies so that unset fields come in as null. This is now explictly set in the type by making it a `Record`. * refactor(map) :: import and use 3rd party Leaflet types `declare const L: Untyped` meant every Leaflet value in sqlpage_map was typed `any`. Now we import `@types/leaflet` and use the types in SQLPage.
1 parent 8a0c488 commit 1768fcc

4 files changed

Lines changed: 99 additions & 61 deletions

File tree

‎frontend/src/globals.d.ts‎

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,4 @@
1-
// Names the browser bundle relies on at runtime rather than through an import:
2-
// libraries loaded on demand, and what a page may provide for our scripts.
3-
4-
/**
5-
* A library this project ships no type definitions for. Saying `unknown`
6-
* instead would only move the guesswork to a cast at every call site.
7-
*/
8-
// biome-ignore lint/suspicious/noExplicitAny: that is what an untyped library is
9-
type Untyped = any;
10-
11-
/** Leaflet, loaded from a CDN by sqlpage_map when a page holds a map. */
12-
declare const L: Untyped;
1+
// Names the browser bundle relies on at runtime rather than through an import.
132

143
interface Window {
154
/** Every chart rendered on the page, in the order they were built. */

‎frontend/src/sqlpage.ts‎

Lines changed: 79 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { bootstrap as bundled_bootstrap } from "@tabler/core";
2+
import type * as Leaflet from "leaflet";
23
import { add_init_fn } from "./init.ts";
34

45
// A page may load its own Bootstrap; prefer it over the bundled copy.
@@ -186,6 +187,15 @@ function sqlpage_table() {
186187
}
187188
}
188189

190+
// Leaflet is loaded from a CDN by sqlpage_map, so it is a global, not an import.
191+
declare const L: typeof Leaflet;
192+
193+
type MarkerStyle = Leaflet.MarkerOptions &
194+
Leaflet.PathOptions & {
195+
/** A GeoJSON feature may size its own icon, overriding the SVG's width. */
196+
size?: number | string;
197+
};
198+
189199
let is_leaflet_injected = false;
190200
let is_leaflet_loaded = false;
191201

@@ -214,11 +224,9 @@ function sqlpage_map() {
214224
if (first_map && is_leaflet_loaded) {
215225
onLeafletLoad();
216226
}
217-
/**
218-
*/
219227
function parseCoords(
220228
coords: string | undefined,
221-
): [number, number] | undefined {
229+
): Leaflet.LatLngTuple | undefined {
222230
if (!coords) return undefined;
223231
const parsed = coords.split(",", 2).map((c) => Number.parseFloat(c));
224232
if (parsed.length !== 2 || !parsed.every(Number.isFinite)) {
@@ -241,76 +249,98 @@ function sqlpage_map() {
241249
const center = parseCoords(m.dataset.center);
242250
if (tile_source)
243251
L.tileLayer(tile_source, { attribution, maxZoom }).addTo(map);
244-
map._sqlpage_markers = [];
245-
for (const marker_elem of m.getElementsByClassName("marker")) {
246-
setTimeout(addMarker, 0, marker_elem, map);
252+
const markers: (Leaflet.Marker | Leaflet.GeoJSON)[] = [];
253+
for (const marker_elem of m.querySelectorAll<HTMLElement>(".marker")) {
254+
setTimeout(() => {
255+
const marker = addMarker(marker_elem, map);
256+
if (marker) markers.push(marker);
257+
}, 0);
247258
}
248259
setTimeout(() => {
249260
if (center) map.setView(center, zoom);
250261
else {
251-
const markerBounds = (m) =>
252-
m.getLatLng ? m.getLatLng() : m.getBounds();
253-
const bounds = map._sqlpage_markers.map(markerBounds);
254-
if (bounds.length > 0) map.fitBounds(bounds);
262+
const bounds = L.latLngBounds([]);
263+
for (const marker of markers)
264+
bounds.extend(
265+
marker instanceof L.Marker
266+
? marker.getLatLng()
267+
: marker.getBounds(),
268+
);
269+
if (markers.length > 0) map.fitBounds(bounds);
255270
else map.setView([51.505, 10], zoom);
256271
if (!Number.isNaN(zoom)) map.setZoom(zoom);
257272
}
258273
}, 100);
259274
m.removeAttribute("data-pre-init");
260-
m.getElementsByClassName("spinner-border")[0]?.remove();
275+
m.querySelector(".spinner-border")?.remove();
261276
}
262277
}
263278

264-
function addMarker(marker_elem, map) {
265-
const { dataset } = marker_elem;
266-
const options = {
267-
color: marker_elem.dataset.color,
268-
title: marker_elem.getElementsByTagName("h3")[0].textContent.trim(),
279+
function addMarker(marker_elem: HTMLElement, map: Leaflet.Map) {
280+
const { color, coords, geojson, link } = marker_elem.dataset;
281+
const options: MarkerStyle = {
282+
color,
283+
title: marker_elem.querySelector("h3")?.textContent?.trim(),
269284
};
270-
const marker = dataset.coords
285+
const marker = coords
271286
? createMarker(marker_elem, options)
272-
: createGeoJSONMarker(marker_elem, options);
273-
if (!marker) return;
287+
: geojson && createGeoJSONMarker(marker_elem, geojson, options);
288+
if (!marker) return undefined;
274289
marker.addTo(map);
275-
map._sqlpage_markers.push(marker);
276-
if (marker_elem.textContent.trim()) marker.bindPopup(marker_elem);
277-
else if (marker_elem.dataset.link) {
290+
if (marker_elem.textContent?.trim()) marker.bindPopup(marker_elem);
291+
else if (link) {
278292
marker.on("click", () => {
279-
window.location.href = marker_elem.dataset.link;
293+
window.location.href = link;
280294
});
281295
}
296+
return marker;
282297
}
283-
function createMarker(marker_elem, options) {
298+
function createMarker(marker_elem: HTMLElement, options: MarkerStyle) {
284299
const coords = parseCoords(marker_elem.dataset.coords);
285-
if (!coords) return undefined;
286-
const icon_obj = marker_elem.getElementsByClassName("mapicon")[0];
287-
if (icon_obj) {
288-
const size =
289-
1.5 *
290-
+(options.size || icon_obj.firstChild?.getAttribute("width") || 24);
291-
options.icon = L.divIcon({
292-
html: icon_obj,
293-
className: `border-0 bg-${options.color || "primary"} bg-gradient text-white rounded-circle shadow d-flex justify-content-center align-items-center`,
294-
iconSize: [size, size],
295-
iconAnchor: [size / 2, size / 2],
296-
});
297-
}
298-
return L.marker(coords, options);
300+
return coords && createMarkerAt(marker_elem, coords, options);
299301
}
300-
function createGeoJSONMarker(marker_elem, options) {
301-
const geojson = JSON.parse(marker_elem.dataset.geojson);
302+
function createMarkerAt(
303+
marker_elem: HTMLElement,
304+
coords: Leaflet.LatLngTuple,
305+
options: MarkerStyle,
306+
) {
307+
const icon_obj = marker_elem.querySelector<HTMLElement>(".mapicon");
308+
if (!icon_obj) return L.marker(coords, options);
309+
const size =
310+
1.5 *
311+
+(
312+
options.size ||
313+
icon_obj.firstElementChild?.getAttribute("width") ||
314+
24
315+
);
316+
const icon = L.divIcon({
317+
html: icon_obj,
318+
className: `border-0 bg-${options.color || "primary"} bg-gradient text-white rounded-circle shadow d-flex justify-content-center align-items-center`,
319+
iconSize: [size, size],
320+
iconAnchor: [size / 2, size / 2],
321+
});
322+
return L.marker(coords, { ...options, icon });
323+
}
324+
function createGeoJSONMarker(
325+
marker_elem: HTMLElement,
326+
geojson: string,
327+
options: MarkerStyle,
328+
) {
302329
if (options.color) {
303330
options.color = get_tabler_color(options.color) || options.color;
304331
}
305-
function style({ properties }) {
306-
if (typeof properties !== "object") return options;
307-
return { ...options, ...properties };
308-
}
309-
function pointToLayer(feature, latlng) {
310-
marker_elem.dataset.coords = `${latlng.lat},${latlng.lng}`;
311-
return createMarker(marker_elem, { ...options, ...feature.properties });
312-
}
313-
return L.geoJSON(geojson, { style, pointToLayer });
332+
return L.geoJSON(JSON.parse(geojson), {
333+
style: (feature) => {
334+
const properties = feature?.properties;
335+
if (typeof properties !== "object") return options;
336+
return { ...options, ...properties };
337+
},
338+
pointToLayer: (feature, latlng) =>
339+
createMarkerAt(marker_elem, [latlng.lat, latlng.lng], {
340+
...options,
341+
...feature.properties,
342+
}),
343+
});
314344
}
315345
}
316346

‎package-lock.json‎

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
},
2323
"devDependencies": {
2424
"@biomejs/biome": "^2.5.11",
25+
"@types/leaflet": "^1.9.22",
2526
"@types/node": "^26.4.0",
2627
"rolldown": "^1.2.8",
2728
"typescript": "^7.0.2"

0 commit comments

Comments
 (0)