@rcpch/imd-map 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1571 @@
1
+ import { Map, AttributionControl, Popup, VectorTileSource, GeoJSONSource } from 'maplibre-gl';
2
+
3
+ // src/core/createImdMap.ts
4
+
5
+ // src/core/resolver.ts
6
+ function resolveEffectiveEra(nation, requestedEra) {
7
+ switch (nation) {
8
+ case "all":
9
+ return "2011";
10
+ case "england":
11
+ return requestedEra;
12
+ case "wales":
13
+ case "scotland":
14
+ case "northern_ireland":
15
+ return "2011";
16
+ default:
17
+ return "2011";
18
+ }
19
+ }
20
+ function willEraBeOverridden(nation, requestedEra) {
21
+ return resolveEffectiveEra(nation, requestedEra) !== requestedEra;
22
+ }
23
+ var ZOOM_TIERS = [
24
+ { tier: "z0_4", minzoom: 0, maxzoom: 5 },
25
+ { tier: "z5_7", minzoom: 5, maxzoom: 8 },
26
+ { tier: "z8_10", minzoom: 8, maxzoom: 24 }
27
+ ];
28
+ function resolveFullTableName(effectiveEra, tier) {
29
+ return `public.uk_master_${effectiveEra}_${tier}`;
30
+ }
31
+ function buildTileUrl(tilesBaseUrl, fullTableName) {
32
+ const base = tilesBaseUrl.replace(/\/$/, "");
33
+ return `${base}/${fullTableName}/{z}/{x}/{y}.pbf`;
34
+ }
35
+ function resolveNationFilter(nation) {
36
+ if (nation === "all") return null;
37
+ return ["==", ["get", "nation"], nation];
38
+ }
39
+
40
+ // src/core/state.ts
41
+ function createInitialState(nation = "all", era = "2021") {
42
+ return {
43
+ nation,
44
+ era,
45
+ effectiveEra: resolveEffectiveEra(nation, era),
46
+ hasPatients: false,
47
+ hasLeadCentre: false,
48
+ overlays: {
49
+ localAuthority: false,
50
+ nhser: false,
51
+ icb: false,
52
+ lhb: false
53
+ }
54
+ };
55
+ }
56
+ function choroplethSourceId(tier) {
57
+ return `rcpch-imd-${tier}`;
58
+ }
59
+ ZOOM_TIERS.map((t) => choroplethSourceId(t.tier));
60
+ var PATIENTS_SOURCE_ID = "rcpch-imd-patients";
61
+ var LEAD_CENTRE_SOURCE_ID = "rcpch-imd-lead-centre";
62
+ function addOrUpdateChoroplethSources(map, tilesBaseUrl, effectiveEra) {
63
+ for (const { tier } of ZOOM_TIERS) {
64
+ const sourceId = choroplethSourceId(tier);
65
+ const fullTableName = resolveFullTableName(effectiveEra, tier);
66
+ const tileUrl = buildTileUrl(tilesBaseUrl, fullTableName);
67
+ const existing = map.getSource(sourceId);
68
+ if (existing instanceof VectorTileSource) {
69
+ existing.setTiles([tileUrl]);
70
+ } else {
71
+ if (existing) map.removeSource(sourceId);
72
+ map.addSource(sourceId, {
73
+ type: "vector",
74
+ tiles: [tileUrl],
75
+ minzoom: 0,
76
+ maxzoom: 14
77
+ });
78
+ }
79
+ }
80
+ }
81
+ function addOrUpdatePatientsSource(map, features) {
82
+ const data = { type: "FeatureCollection", features };
83
+ const existing = map.getSource(PATIENTS_SOURCE_ID);
84
+ if (existing instanceof GeoJSONSource) {
85
+ existing.setData(data);
86
+ } else {
87
+ if (existing) map.removeSource(PATIENTS_SOURCE_ID);
88
+ map.addSource(PATIENTS_SOURCE_ID, { type: "geojson", data });
89
+ }
90
+ }
91
+ function addOrUpdateLeadCentreSource(map, feature) {
92
+ const data = {
93
+ type: "FeatureCollection",
94
+ features: feature ? [feature] : []
95
+ };
96
+ const existing = map.getSource(LEAD_CENTRE_SOURCE_ID);
97
+ if (existing instanceof GeoJSONSource) {
98
+ existing.setData(data);
99
+ } else {
100
+ if (existing) map.removeSource(LEAD_CENTRE_SOURCE_ID);
101
+ map.addSource(LEAD_CENTRE_SOURCE_ID, { type: "geojson", data });
102
+ }
103
+ }
104
+
105
+ // src/map/styles.ts
106
+ var RCPCH_DARK_BLUE = "#0d0d58";
107
+ var RCPCH_PINK = "#e00087";
108
+ var RCPCH_LIGHT_BLUE = "#41b6e6";
109
+ var RCPCH_CHARCOAL = "#3d3d3d";
110
+ var DEFAULT_BASE_COLORS_BY_NATION = {
111
+ england: "#d7191c",
112
+ wales: "#1a9641",
113
+ scotland: "#2b83ba",
114
+ northern_ireland: "#7f7f7f",
115
+ all: "#d7191c"
116
+ };
117
+ function clamp(value, min, max) {
118
+ return Math.min(max, Math.max(min, value));
119
+ }
120
+ function normalizeHex(hex) {
121
+ const clean = hex.trim();
122
+ if (/^#[0-9a-fA-F]{6}$/.test(clean)) return clean;
123
+ if (/^#[0-9a-fA-F]{3}$/.test(clean)) {
124
+ const r = clean[1];
125
+ const g = clean[2];
126
+ const b = clean[3];
127
+ return `#${r}${r}${g}${g}${b}${b}`;
128
+ }
129
+ return null;
130
+ }
131
+ function hexToRgb(hex) {
132
+ const normalized = normalizeHex(hex);
133
+ if (!normalized) return null;
134
+ const value = parseInt(normalized.slice(1), 16);
135
+ return {
136
+ r: value >> 16 & 255,
137
+ g: value >> 8 & 255,
138
+ b: value & 255
139
+ };
140
+ }
141
+ function rgbToHex(r, g, b) {
142
+ const rr = clamp(Math.round(r), 0, 255).toString(16).padStart(2, "0");
143
+ const gg = clamp(Math.round(g), 0, 255).toString(16).padStart(2, "0");
144
+ const bb = clamp(Math.round(b), 0, 255).toString(16).padStart(2, "0");
145
+ return `#${rr}${gg}${bb}`;
146
+ }
147
+ function rgbToHsl(r, g, b) {
148
+ const rn = r / 255;
149
+ const gn = g / 255;
150
+ const bn = b / 255;
151
+ const max = Math.max(rn, gn, bn);
152
+ const min = Math.min(rn, gn, bn);
153
+ let h = 0;
154
+ const l = (max + min) / 2;
155
+ const d = max - min;
156
+ if (d !== 0) {
157
+ switch (max) {
158
+ case rn:
159
+ h = ((gn - bn) / d + (gn < bn ? 6 : 0)) / 6;
160
+ break;
161
+ case gn:
162
+ h = ((bn - rn) / d + 2) / 6;
163
+ break;
164
+ default:
165
+ h = ((rn - gn) / d + 4) / 6;
166
+ break;
167
+ }
168
+ }
169
+ const s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
170
+ return { h, s, l };
171
+ }
172
+ function hslToRgb(h, s, l) {
173
+ const c = (1 - Math.abs(2 * l - 1)) * s;
174
+ const hp = h * 6;
175
+ const x = c * (1 - Math.abs(hp % 2 - 1));
176
+ let r1 = 0;
177
+ let g1 = 0;
178
+ let b1 = 0;
179
+ if (hp >= 0 && hp < 1) {
180
+ r1 = c;
181
+ g1 = x;
182
+ b1 = 0;
183
+ } else if (hp < 2) {
184
+ r1 = x;
185
+ g1 = c;
186
+ b1 = 0;
187
+ } else if (hp < 3) {
188
+ r1 = 0;
189
+ g1 = c;
190
+ b1 = x;
191
+ } else if (hp < 4) {
192
+ r1 = 0;
193
+ g1 = x;
194
+ b1 = c;
195
+ } else if (hp < 5) {
196
+ r1 = x;
197
+ g1 = 0;
198
+ b1 = c;
199
+ } else {
200
+ r1 = c;
201
+ g1 = 0;
202
+ b1 = x;
203
+ }
204
+ const m = l - c / 2;
205
+ return {
206
+ r: (r1 + m) * 255,
207
+ g: (g1 + m) * 255,
208
+ b: (b1 + m) * 255
209
+ };
210
+ }
211
+ function generateDecileRampFromBaseColor(baseHex) {
212
+ const rgb = hexToRgb(baseHex);
213
+ if (!rgb) return Array(10).fill("#cccccc");
214
+ const { h, s, l } = rgbToHsl(rgb.r, rgb.g, rgb.b);
215
+ const steps = 10;
216
+ const spread = 0.34;
217
+ const half = (steps - 1) / 2;
218
+ return Array.from({ length: steps }, (_, i) => {
219
+ const normalized = (i - half) / half;
220
+ const li = clamp(l + normalized * spread, 0.08, 0.92);
221
+ const { r, g, b } = hslToRgb(h, s, li);
222
+ return rgbToHex(r, g, b);
223
+ });
224
+ }
225
+ var DEFAULT_STYLE = {
226
+ choropleth: {
227
+ decileColorsByNation: {},
228
+ baseColorByNation: {
229
+ england: DEFAULT_BASE_COLORS_BY_NATION.england,
230
+ wales: DEFAULT_BASE_COLORS_BY_NATION.wales,
231
+ scotland: DEFAULT_BASE_COLORS_BY_NATION.scotland,
232
+ northern_ireland: DEFAULT_BASE_COLORS_BY_NATION.northern_ireland
233
+ },
234
+ fallbackDecileColors: generateDecileRampFromBaseColor(DEFAULT_BASE_COLORS_BY_NATION.england),
235
+ fillOpacity: 0.7,
236
+ borderColor: "#ffffff",
237
+ borderWidth: 0.5,
238
+ hoverBorderColor: RCPCH_DARK_BLUE,
239
+ hoverBorderWidth: 2
240
+ },
241
+ boundaries: {
242
+ localAuthorityColor: RCPCH_DARK_BLUE,
243
+ localAuthorityWidth: 1,
244
+ nhserColor: RCPCH_PINK,
245
+ nhserWidth: 1.5,
246
+ icbColor: RCPCH_CHARCOAL,
247
+ icbWidth: 1,
248
+ lhbColor: RCPCH_LIGHT_BLUE,
249
+ lhbWidth: 1
250
+ },
251
+ patients: {
252
+ circleColor: RCPCH_DARK_BLUE,
253
+ circleRadius: 5,
254
+ circleStrokeColor: "#ffffff",
255
+ circleStrokeWidth: 1,
256
+ circleOpacity: 0.8,
257
+ colorByGroup: {}
258
+ },
259
+ leadCentre: {
260
+ color: RCPCH_PINK,
261
+ radius: 10,
262
+ strokeColor: "#ffffff",
263
+ strokeWidth: 2
264
+ },
265
+ tooltip: {
266
+ backgroundColor: RCPCH_DARK_BLUE,
267
+ textColor: "#ffffff",
268
+ borderColor: RCPCH_DARK_BLUE,
269
+ borderRadius: 4,
270
+ areaLabel: "Area",
271
+ decileLabel: "IMD decile",
272
+ nationLabel: "Nation",
273
+ patientLabel: "Patient",
274
+ leadCentreLabel: "Lead centre",
275
+ patientTooltipText: "{{patientLabel}}",
276
+ leadCentreTooltipText: "{{leadCentreLabel}}: {{label}}"
277
+ },
278
+ legend: {
279
+ backgroundColor: "#ffffff",
280
+ textColor: RCPCH_DARK_BLUE,
281
+ borderColor: "#d8dde6",
282
+ borderRadius: 8,
283
+ fontSize: 13,
284
+ fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, sans-serif",
285
+ width: 220,
286
+ boxShadow: "0 6px 18px rgba(0, 0, 0, 0.12)",
287
+ toggleOnColor: RCPCH_DARK_BLUE,
288
+ toggleOffColor: "#6b7280"
289
+ }
290
+ };
291
+ function mergeStyle(defaults, overrides) {
292
+ if (!overrides) return defaults;
293
+ return {
294
+ choropleth: { ...defaults.choropleth, ...overrides.choropleth },
295
+ boundaries: { ...defaults.boundaries, ...overrides.boundaries },
296
+ patients: { ...defaults.patients, ...overrides.patients },
297
+ leadCentre: { ...defaults.leadCentre, ...overrides.leadCentre },
298
+ tooltip: { ...defaults.tooltip, ...overrides.tooltip },
299
+ legend: { ...defaults.legend, ...overrides.legend }
300
+ };
301
+ }
302
+ function getDecileColors(nation, style) {
303
+ const byNation = style.choropleth.decileColorsByNation ?? {};
304
+ const fromExplicitRamp = byNation[nation];
305
+ if (fromExplicitRamp && fromExplicitRamp.length === 10) return fromExplicitRamp;
306
+ const baseByNation = style.choropleth.baseColorByNation ?? {};
307
+ const configuredBase = baseByNation[nation];
308
+ if (configuredBase) return generateDecileRampFromBaseColor(configuredBase);
309
+ const defaultBase = DEFAULT_BASE_COLORS_BY_NATION[nation] ?? DEFAULT_BASE_COLORS_BY_NATION.england;
310
+ return generateDecileRampFromBaseColor(defaultBase) ?? style.choropleth.fallbackDecileColors ?? generateDecileRampFromBaseColor(DEFAULT_BASE_COLORS_BY_NATION.england);
311
+ }
312
+
313
+ // src/map/layers.ts
314
+ function choroplethFillLayerId(tier) {
315
+ return `rcpch-imd-fill-${tier}`;
316
+ }
317
+ function choroplethLineLayerId(tier) {
318
+ return `rcpch-imd-line-${tier}`;
319
+ }
320
+ var ALL_CHOROPLETH_LAYER_IDS = ZOOM_TIERS.flatMap((t) => [
321
+ choroplethFillLayerId(t.tier),
322
+ choroplethLineLayerId(t.tier)
323
+ ]);
324
+ var PATIENTS_LAYER_ID = "rcpch-imd-patients";
325
+ var LEAD_CENTRE_LAYER_ID = "rcpch-imd-lead-centre";
326
+ function buildDecileColorExpression(colors) {
327
+ const fallback = colors[0] ?? "#cccccc";
328
+ return [
329
+ "step",
330
+ ["get", "imd_decile"],
331
+ fallback,
332
+ 2,
333
+ colors[1] ?? fallback,
334
+ 3,
335
+ colors[2] ?? fallback,
336
+ 4,
337
+ colors[3] ?? fallback,
338
+ 5,
339
+ colors[4] ?? fallback,
340
+ 6,
341
+ colors[5] ?? fallback,
342
+ 7,
343
+ colors[6] ?? fallback,
344
+ 8,
345
+ colors[7] ?? fallback,
346
+ 9,
347
+ colors[8] ?? fallback,
348
+ 10,
349
+ colors[9] ?? fallback
350
+ ];
351
+ }
352
+ function buildColorExpression(nation, style) {
353
+ if (nation !== "all") {
354
+ return buildDecileColorExpression(getDecileColors(nation, style));
355
+ }
356
+ const perNation = ["england", "wales", "scotland", "northern_ireland"].flatMap((n) => [n, buildDecileColorExpression(getDecileColors(n, style))]);
357
+ return [
358
+ "match",
359
+ ["get", "nation"],
360
+ ...perNation,
361
+ // fallback for any unrecognised nation value
362
+ getDecileColors("england", style)[0] ?? "#cccccc"
363
+ ];
364
+ }
365
+ function firstSymbolLayerId(map) {
366
+ for (const layer of map.getStyle().layers ?? []) {
367
+ if (layer.type === "symbol") return layer.id;
368
+ }
369
+ return void 0;
370
+ }
371
+ function buildPatientCircleColorExpression(style) {
372
+ const fallback = style.patients.circleColor ?? "#0d0d58";
373
+ const groupMap = style.patients.colorByGroup ?? {};
374
+ const entries = Object.entries(groupMap).filter(([k, v]) => k && v);
375
+ if (!entries.length) return fallback;
376
+ return [
377
+ "match",
378
+ ["coalesce", ["to-string", ["get", "group"]], ""],
379
+ ...entries.flatMap(([group, color]) => [group, color]),
380
+ fallback
381
+ ];
382
+ }
383
+ function addChoroplethLayers(map, nation, effectiveEra, style) {
384
+ removeChoroplethLayers(map);
385
+ const nationFilter = resolveNationFilter(nation);
386
+ const filterProps = nationFilter ? { filter: nationFilter } : {};
387
+ const before = firstSymbolLayerId(map);
388
+ for (const { tier, minzoom, maxzoom } of ZOOM_TIERS) {
389
+ const sourceId = choroplethSourceId(tier);
390
+ const sourceLayer = resolveFullTableName(effectiveEra, tier);
391
+ const colorExpr = buildColorExpression(nation, style);
392
+ map.addLayer(
393
+ {
394
+ id: choroplethFillLayerId(tier),
395
+ type: "fill",
396
+ source: sourceId,
397
+ "source-layer": sourceLayer,
398
+ minzoom,
399
+ maxzoom,
400
+ ...filterProps,
401
+ paint: {
402
+ "fill-color": colorExpr,
403
+ "fill-opacity": style.choropleth.fillOpacity ?? 0.7
404
+ }
405
+ },
406
+ before
407
+ );
408
+ map.addLayer(
409
+ {
410
+ id: choroplethLineLayerId(tier),
411
+ type: "line",
412
+ source: sourceId,
413
+ "source-layer": sourceLayer,
414
+ minzoom,
415
+ maxzoom,
416
+ ...filterProps,
417
+ paint: {
418
+ "line-color": style.choropleth.borderColor ?? "#ffffff",
419
+ "line-width": style.choropleth.borderWidth ?? 0.5
420
+ }
421
+ },
422
+ before
423
+ );
424
+ }
425
+ }
426
+ function removeChoroplethLayers(map) {
427
+ for (const id of ALL_CHOROPLETH_LAYER_IDS) {
428
+ if (map.getLayer(id)) map.removeLayer(id);
429
+ }
430
+ }
431
+ function updateChoroplethStyle(map, nation, style) {
432
+ const colorExpr = buildColorExpression(nation, style);
433
+ for (const { tier } of ZOOM_TIERS) {
434
+ const fillId = choroplethFillLayerId(tier);
435
+ const lineId = choroplethLineLayerId(tier);
436
+ if (!map.getLayer(fillId)) continue;
437
+ map.setPaintProperty(fillId, "fill-color", colorExpr);
438
+ map.setPaintProperty(fillId, "fill-opacity", style.choropleth.fillOpacity ?? 0.7);
439
+ map.setPaintProperty(lineId, "line-color", style.choropleth.borderColor ?? "#ffffff");
440
+ map.setPaintProperty(lineId, "line-width", style.choropleth.borderWidth ?? 0.5);
441
+ }
442
+ }
443
+ function updateChoroplethNationFilter(map, nation, effectiveEra) {
444
+ const nationFilter = resolveNationFilter(nation);
445
+ for (const { tier } of ZOOM_TIERS) {
446
+ const fillId = choroplethFillLayerId(tier);
447
+ const lineId = choroplethLineLayerId(tier);
448
+ if (!map.getLayer(fillId)) continue;
449
+ map.setFilter(fillId, nationFilter);
450
+ map.setFilter(lineId, nationFilter);
451
+ }
452
+ }
453
+ function addOrUpdatePatientsLayer(map, style) {
454
+ const p = style.patients;
455
+ const color = buildPatientCircleColorExpression(style);
456
+ if (map.getLayer(PATIENTS_LAYER_ID)) {
457
+ map.setPaintProperty(PATIENTS_LAYER_ID, "circle-color", color);
458
+ map.setPaintProperty(PATIENTS_LAYER_ID, "circle-radius", p.circleRadius ?? 5);
459
+ map.setPaintProperty(PATIENTS_LAYER_ID, "circle-stroke-color", p.circleStrokeColor ?? "#ffffff");
460
+ map.setPaintProperty(PATIENTS_LAYER_ID, "circle-stroke-width", p.circleStrokeWidth ?? 1);
461
+ map.setPaintProperty(PATIENTS_LAYER_ID, "circle-opacity", p.circleOpacity ?? 0.8);
462
+ } else {
463
+ map.addLayer({
464
+ id: PATIENTS_LAYER_ID,
465
+ type: "circle",
466
+ source: PATIENTS_SOURCE_ID,
467
+ paint: {
468
+ "circle-color": color,
469
+ "circle-radius": p.circleRadius ?? 5,
470
+ "circle-stroke-color": p.circleStrokeColor ?? "#ffffff",
471
+ "circle-stroke-width": p.circleStrokeWidth ?? 1,
472
+ "circle-opacity": p.circleOpacity ?? 0.8
473
+ }
474
+ });
475
+ }
476
+ }
477
+ function removePatientsLayer(map) {
478
+ if (map.getLayer(PATIENTS_LAYER_ID)) map.removeLayer(PATIENTS_LAYER_ID);
479
+ }
480
+ function addOrUpdateLeadCentreLayer(map, style) {
481
+ const lc = style.leadCentre;
482
+ if (map.getLayer(LEAD_CENTRE_LAYER_ID)) {
483
+ map.setPaintProperty(LEAD_CENTRE_LAYER_ID, "circle-color", lc.color ?? "#e00087");
484
+ map.setPaintProperty(LEAD_CENTRE_LAYER_ID, "circle-radius", lc.radius ?? 10);
485
+ map.setPaintProperty(LEAD_CENTRE_LAYER_ID, "circle-stroke-color", lc.strokeColor ?? "#ffffff");
486
+ map.setPaintProperty(LEAD_CENTRE_LAYER_ID, "circle-stroke-width", lc.strokeWidth ?? 2);
487
+ } else {
488
+ map.addLayer({
489
+ id: LEAD_CENTRE_LAYER_ID,
490
+ type: "circle",
491
+ source: LEAD_CENTRE_SOURCE_ID,
492
+ paint: {
493
+ "circle-color": lc.color ?? "#e00087",
494
+ "circle-radius": lc.radius ?? 10,
495
+ "circle-stroke-color": lc.strokeColor ?? "#ffffff",
496
+ "circle-stroke-width": lc.strokeWidth ?? 2
497
+ }
498
+ });
499
+ }
500
+ }
501
+ function removeLeadCentreLayer(map) {
502
+ if (map.getLayer(LEAD_CENTRE_LAYER_ID)) map.removeLayer(LEAD_CENTRE_LAYER_ID);
503
+ }
504
+
505
+ // src/utils/properties.ts
506
+ var KEY_ALIASES = {
507
+ // Canonical: 'code' (uk_master_*). Also accept lsoa_code for lsoa_tiles_* family.
508
+ code: ["code", "lsoa_code", "LSOA11CD", "lsoa11cd", "LSOA21CD", "lsoa21cd"],
509
+ // Canonical: 'area_name' for both table families.
510
+ area_name: ["area_name", "LSOA11NM", "lsoa11nm", "LSOA21NM", "lsoa21nm", "name"],
511
+ imd_decile: ["imd_decile", "IMD_Decile", "IMDDecile", "decile", "imd_rank_decile"],
512
+ imd_rank: ["imd_rank", "IMD_Rank", "rank"],
513
+ imd_year: ["imd_year", "IMD_Year"],
514
+ nation: ["nation", "Nation", "country", "Country"],
515
+ year: ["year", "Year"],
516
+ // Health / administrative boundary codes
517
+ la_code: ["la_code", "LAD_code", "lad_code"],
518
+ la_name: ["la_name", "LAD_name", "lad_name"],
519
+ nhser_code: ["nhser_code", "NHSER_code"],
520
+ nhser_name: ["nhser_name", "NHSER_name"],
521
+ icb_code: ["icb_code", "ICB_code"],
522
+ icb_name: ["icb_name", "ICB_name"],
523
+ lhb_code: ["lhb_code", "LHB_code"],
524
+ lhb_name: ["lhb_name", "LHB_name"]
525
+ };
526
+ function getFeatureProperty(properties, canonicalKey) {
527
+ if (!properties) return void 0;
528
+ if (canonicalKey in properties) return properties[canonicalKey];
529
+ const aliases = KEY_ALIASES[canonicalKey];
530
+ if (aliases) {
531
+ for (const alias of aliases) {
532
+ if (alias in properties) return properties[alias];
533
+ }
534
+ }
535
+ const lowerKey = canonicalKey.toLowerCase();
536
+ for (const [k, v] of Object.entries(properties)) {
537
+ if (k.toLowerCase() === lowerKey) return v;
538
+ }
539
+ return void 0;
540
+ }
541
+
542
+ // src/map/popups.ts
543
+ function interpolateTemplate(template, tokens) {
544
+ return template.replace(/{{\s*([a-zA-Z0-9_]+)\s*}}/g, (_, key) => {
545
+ const value = tokens[key];
546
+ return value === null || value === void 0 ? "" : String(value);
547
+ });
548
+ }
549
+ function buildChoroplethTooltipHtml(properties, style) {
550
+ const t = style.tooltip;
551
+ const areaName = getFeatureProperty(properties, "area_name") ?? "Unknown area";
552
+ const decile = getFeatureProperty(properties, "imd_decile") ?? "\u2013";
553
+ const nation = getFeatureProperty(properties, "nation") ?? "\u2013";
554
+ const areaYear = getFeatureProperty(properties, "year") ?? "\u2013";
555
+ const imdYear = getFeatureProperty(properties, "imd_year") ?? "\u2013";
556
+ const bg = t.backgroundColor ?? "#0d0d58";
557
+ const color = t.textColor ?? "#ffffff";
558
+ const radius = t.borderRadius ?? 4;
559
+ return `<div style="background:${bg};color:${color};padding:8px 12px;border-radius:${radius}px;font-size:13px;line-height:1.6;font-family:sans-serif;">
560
+ <strong style="display:block;margin-bottom:2px;">${String(areaName)}</strong>
561
+ <span>LSOA year: ${String(areaYear)}</span><br/>
562
+ <span>${t.decileLabel ?? "IMD decile"}: <strong>${String(decile)}</strong></span><br/>
563
+ <span>IMD year: ${String(imdYear)}</span><br/>
564
+ <span>${t.nationLabel ?? "Nation"}: ${String(nation)}</span>
565
+ </div>`;
566
+ }
567
+ function buildPatientTooltipHtml(properties, style) {
568
+ const t = style.tooltip;
569
+ const id = getFeatureProperty(properties, "id") ?? "";
570
+ const group = getFeatureProperty(properties, "group") ?? "";
571
+ const bg = t.backgroundColor ?? "#0d0d58";
572
+ const color = t.textColor ?? "#ffffff";
573
+ const radius = t.borderRadius ?? 4;
574
+ const patientLabel = t.patientLabel ?? "Patient";
575
+ const template = t.patientTooltipText ?? "{{patientLabel}}";
576
+ const tokens = {
577
+ // Spread all feature properties first so named tokens (id, group, etc.)
578
+ // override any same-named extra field, and extra fields like nhs_number
579
+ // are available as {{nhs_number}} etc.
580
+ ...properties ?? {},
581
+ patientLabel,
582
+ id,
583
+ group
584
+ };
585
+ const text = interpolateTemplate(template, tokens);
586
+ return `<div style="background:${bg};color:${color};padding:8px 12px;border-radius:${radius}px;font-size:13px;line-height:1.6;font-family:sans-serif;">
587
+ <span>${text}</span>
588
+ </div>`;
589
+ }
590
+ function buildLeadCentreTooltipHtml(properties, style) {
591
+ const t = style.tooltip;
592
+ const label = getFeatureProperty(properties, "label") ?? "Lead centre";
593
+ const bg = t.backgroundColor ?? "#0d0d58";
594
+ const color = t.textColor ?? "#ffffff";
595
+ const radius = t.borderRadius ?? 4;
596
+ const leadCentreLabel = t.leadCentreLabel ?? "Lead centre";
597
+ const text = interpolateTemplate(t.leadCentreTooltipText ?? "{{leadCentreLabel}}: {{label}}", {
598
+ leadCentreLabel,
599
+ label
600
+ });
601
+ return `<div style="background:${bg};color:${color};padding:8px 12px;border-radius:${radius}px;font-size:13px;line-height:1.6;font-family:sans-serif;">
602
+ <span>${text}</span>
603
+ </div>`;
604
+ }
605
+ function featureToPayload(feature) {
606
+ const props = feature.properties ?? {};
607
+ const decileRaw = getFeatureProperty(props, "imd_decile");
608
+ return {
609
+ lsoaCode: String(getFeatureProperty(props, "code") ?? ""),
610
+ lsoaName: String(getFeatureProperty(props, "area_name") ?? ""),
611
+ imdDecile: typeof decileRaw === "number" ? decileRaw : void 0,
612
+ nation: String(getFeatureProperty(props, "nation") ?? ""),
613
+ rawProperties: props
614
+ };
615
+ }
616
+ function attachChoroplethInteraction(map, popup, style, options) {
617
+ const fillLayers = ALL_CHOROPLETH_LAYER_IDS.filter((id) => id.startsWith("rcpch-imd-fill-"));
618
+ for (const layerId of fillLayers) {
619
+ map.on("mousemove", layerId, (e) => {
620
+ const features = map.queryRenderedFeatures(e.point, { layers: [layerId] });
621
+ if (!features.length) return;
622
+ map.getCanvas().style.cursor = "pointer";
623
+ const feature = features[0];
624
+ popup.setLngLat(e.lngLat).setHTML(buildChoroplethTooltipHtml(feature.properties, style)).addTo(map);
625
+ options.onAreaHover?.(featureToPayload(feature));
626
+ });
627
+ map.on("mouseleave", layerId, () => {
628
+ map.getCanvas().style.cursor = "";
629
+ popup.remove();
630
+ });
631
+ map.on("click", layerId, (e) => {
632
+ const features = map.queryRenderedFeatures(e.point, { layers: [layerId] });
633
+ if (!features.length) return;
634
+ options.onAreaClick?.(featureToPayload(features[0]));
635
+ });
636
+ }
637
+ }
638
+ function attachPatientInteraction(map, popup, style) {
639
+ map.on("mousemove", PATIENTS_LAYER_ID, (e) => {
640
+ const features = map.queryRenderedFeatures(e.point, { layers: [PATIENTS_LAYER_ID] });
641
+ if (!features.length) return;
642
+ const feature = features[0];
643
+ map.getCanvas().style.cursor = "pointer";
644
+ popup.setLngLat(e.lngLat).setHTML(buildPatientTooltipHtml(feature.properties, style)).addTo(map);
645
+ });
646
+ map.on("mouseleave", PATIENTS_LAYER_ID, () => {
647
+ map.getCanvas().style.cursor = "";
648
+ popup.remove();
649
+ });
650
+ }
651
+ function attachLeadCentreInteraction(map, popup, style) {
652
+ map.on("mousemove", LEAD_CENTRE_LAYER_ID, (e) => {
653
+ const features = map.queryRenderedFeatures(e.point, { layers: [LEAD_CENTRE_LAYER_ID] });
654
+ if (!features.length) return;
655
+ const feature = features[0];
656
+ map.getCanvas().style.cursor = "pointer";
657
+ popup.setLngLat(e.lngLat).setHTML(buildLeadCentreTooltipHtml(feature.properties, style)).addTo(map);
658
+ });
659
+ map.on("mouseleave", LEAD_CENTRE_LAYER_ID, () => {
660
+ map.getCanvas().style.cursor = "";
661
+ popup.remove();
662
+ });
663
+ }
664
+
665
+ // src/utils/validation.ts
666
+ function validateLatLon(lat, lon) {
667
+ const errors = [];
668
+ if (typeof lat !== "number" || isNaN(lat) || lat < -90 || lat > 90) {
669
+ errors.push(`Invalid latitude: ${String(lat)}. Must be a number between -90 and 90.`);
670
+ }
671
+ if (typeof lon !== "number" || isNaN(lon) || lon < -180 || lon > 180) {
672
+ errors.push(`Invalid longitude: ${String(lon)}. Must be a number between -180 and 180.`);
673
+ }
674
+ return { valid: errors.length === 0, errors };
675
+ }
676
+ var UK_BOUNDS = { minLat: 49.5, maxLat: 61.5, minLon: -8.7, maxLon: 2.1 };
677
+ function isWithinUK(lat, lon) {
678
+ return lat >= UK_BOUNDS.minLat && lat <= UK_BOUNDS.maxLat && lon >= UK_BOUNDS.minLon && lon <= UK_BOUNDS.maxLon;
679
+ }
680
+ function validatePatientPoint(record) {
681
+ if (!record || typeof record !== "object") {
682
+ return { point: null, errors: ["Record is not an object."] };
683
+ }
684
+ const r = record;
685
+ const lat = r.lat ?? r.latitude ?? r.LAT;
686
+ const lon = r.lon ?? r.lng ?? r.longitude ?? r.LON;
687
+ const { valid, errors } = validateLatLon(lat, lon);
688
+ if (!valid) return { point: null, errors };
689
+ if (!isWithinUK(lat, lon)) {
690
+ const msg = `Point (${String(lat)}, ${String(lon)}) is outside UK bounds and will be skipped.`;
691
+ return { point: null, errors: [msg] };
692
+ }
693
+ const reservedKeys = /* @__PURE__ */ new Set(["lat", "lon", "id", "weight", "group", "latitude", "longitude", "lng", "LAT", "LON"]);
694
+ const point = {
695
+ lat,
696
+ lon,
697
+ id: r.id !== void 0 ? String(r.id) : void 0,
698
+ weight: typeof r.weight === "number" ? r.weight : void 0,
699
+ group: typeof r.group === "string" ? r.group : void 0,
700
+ properties: Object.fromEntries(
701
+ Object.entries(r).filter(([k]) => !reservedKeys.has(k))
702
+ )
703
+ };
704
+ return { point, errors: [] };
705
+ }
706
+
707
+ // src/adapters/patientInput.ts
708
+ function isGeoJsonFeatureCollection(data) {
709
+ return typeof data === "object" && data !== null && data.type === "FeatureCollection" && Array.isArray(data.features);
710
+ }
711
+ function isGeoJsonFeature(data) {
712
+ return typeof data === "object" && data !== null && data.type === "Feature";
713
+ }
714
+ function normalizePatientInput(data, options) {
715
+ const features = [];
716
+ const warnings = [];
717
+ const strict = options?.strict === true;
718
+ let records;
719
+ if (isGeoJsonFeatureCollection(data)) {
720
+ records = data.features.map((f) => ({
721
+ lat: f.geometry.coordinates[1],
722
+ lon: f.geometry.coordinates[0],
723
+ ...f.properties
724
+ }));
725
+ } else if (Array.isArray(data)) {
726
+ records = data.map(
727
+ (item) => isGeoJsonFeature(item) ? { lat: item.geometry.coordinates[1], lon: item.geometry.coordinates[0], ...item.properties } : item
728
+ );
729
+ } else {
730
+ if (strict) {
731
+ throw new Error("[rcpch-imd-map] PatientInput must be an array or GeoJSON FeatureCollection.");
732
+ }
733
+ warnings.push({ code: "INVALID_INPUT", message: "PatientInput must be an array or GeoJSON FeatureCollection." });
734
+ return { features, warnings };
735
+ }
736
+ for (let i = 0; i < records.length; i++) {
737
+ const { point, errors } = validatePatientPoint(records[i]);
738
+ if (!point) {
739
+ if (strict) {
740
+ throw new Error(
741
+ `[rcpch-imd-map] Patient record at index ${i} is invalid: ${errors.join("; ")}`
742
+ );
743
+ }
744
+ warnings.push({
745
+ code: "INVALID_PATIENT_POINT",
746
+ message: `Patient record at index ${i} is invalid and will be skipped: ${errors.join("; ")}`,
747
+ details: { index: i, errors }
748
+ });
749
+ continue;
750
+ }
751
+ for (const err of errors) {
752
+ warnings.push({ code: "PATIENT_POINT_WARNING", message: err, details: { index: i } });
753
+ }
754
+ features.push({
755
+ type: "Feature",
756
+ geometry: { type: "Point", coordinates: [point.lon, point.lat] },
757
+ properties: {
758
+ id: point.id ?? `patient-${i}`,
759
+ weight: point.weight ?? 1,
760
+ group: point.group ?? null,
761
+ ...point.properties
762
+ }
763
+ });
764
+ }
765
+ return { features, warnings };
766
+ }
767
+
768
+ // src/overlays/leadCentre.ts
769
+ function normalizeLeadCentreInput(data) {
770
+ if (!data || typeof data !== "object") return null;
771
+ let lat;
772
+ let lon;
773
+ let label = "Lead centre";
774
+ if (data.type === "Feature" && data.geometry?.type === "Point") {
775
+ [lon, lat] = data.geometry.coordinates;
776
+ label = data.properties?.label ?? label;
777
+ } else {
778
+ const d = data;
779
+ lat = d.lat ?? d.latitude;
780
+ lon = d.lon ?? d.lng ?? d.longitude;
781
+ label = d.label ?? label;
782
+ }
783
+ const { valid } = validateLatLon(lat, lon);
784
+ if (!valid) return null;
785
+ return {
786
+ type: "Feature",
787
+ geometry: { type: "Point", coordinates: [lon, lat] },
788
+ properties: { label }
789
+ };
790
+ }
791
+ var LOCAL_AUTHORITY_SOURCE_ID = "rcpch-imd-la-overlay";
792
+ var LOCAL_AUTHORITY_LAYER_ID = "rcpch-imd-la-overlay-line";
793
+ var LOCAL_AUTHORITY_FULL_TABLE_NAME = "public.la_tiles";
794
+ var LOCAL_AUTHORITY_SOURCE_LAYER = "public.la_tiles";
795
+ function addOrUpdateLocalAuthorityOverlay(map, tilesBaseUrl, style) {
796
+ const tileUrl = buildTileUrl(tilesBaseUrl, LOCAL_AUTHORITY_FULL_TABLE_NAME);
797
+ const existing = map.getSource(LOCAL_AUTHORITY_SOURCE_ID);
798
+ if (existing instanceof VectorTileSource) {
799
+ existing.setTiles([tileUrl]);
800
+ } else {
801
+ if (existing) map.removeSource(LOCAL_AUTHORITY_SOURCE_ID);
802
+ map.addSource(LOCAL_AUTHORITY_SOURCE_ID, {
803
+ type: "vector",
804
+ tiles: [tileUrl],
805
+ minzoom: 0,
806
+ maxzoom: 14
807
+ });
808
+ }
809
+ if (map.getLayer(LOCAL_AUTHORITY_LAYER_ID)) {
810
+ map.setPaintProperty(
811
+ LOCAL_AUTHORITY_LAYER_ID,
812
+ "line-color",
813
+ style.boundaries.localAuthorityColor ?? "#0d0d58"
814
+ );
815
+ map.setPaintProperty(
816
+ LOCAL_AUTHORITY_LAYER_ID,
817
+ "line-width",
818
+ style.boundaries.localAuthorityWidth ?? 1
819
+ );
820
+ map.setLayoutProperty(LOCAL_AUTHORITY_LAYER_ID, "visibility", "visible");
821
+ return;
822
+ }
823
+ map.addLayer({
824
+ id: LOCAL_AUTHORITY_LAYER_ID,
825
+ type: "line",
826
+ source: LOCAL_AUTHORITY_SOURCE_ID,
827
+ "source-layer": LOCAL_AUTHORITY_SOURCE_LAYER,
828
+ paint: {
829
+ "line-color": style.boundaries.localAuthorityColor ?? "#0d0d58",
830
+ "line-width": style.boundaries.localAuthorityWidth ?? 1
831
+ },
832
+ layout: {
833
+ visibility: "visible"
834
+ }
835
+ });
836
+ }
837
+ function hideLocalAuthorityOverlay(map) {
838
+ if (map.getLayer(LOCAL_AUTHORITY_LAYER_ID)) {
839
+ map.setLayoutProperty(LOCAL_AUTHORITY_LAYER_ID, "visibility", "none");
840
+ }
841
+ }
842
+ var NHSER_SOURCE_ID = "rcpch-imd-nhser-overlay";
843
+ var NHSER_LAYER_ID = "rcpch-imd-nhser-overlay-line";
844
+ var ICB_SOURCE_ID = "rcpch-imd-icb-overlay";
845
+ var ICB_LAYER_ID = "rcpch-imd-icb-overlay-line";
846
+ var LHB_SOURCE_ID = "rcpch-imd-lhb-overlay";
847
+ var LHB_LAYER_ID = "rcpch-imd-lhb-overlay-line";
848
+ var NHSER_FULL_TABLE_NAME = "public.nhser_tiles_2021";
849
+ var NHSER_SOURCE_LAYER = "public.nhser_tiles_2021";
850
+ var ICB_FULL_TABLE_NAME = "public.icb_tiles_2023";
851
+ var ICB_SOURCE_LAYER = "public.icb_tiles_2023";
852
+ var LHB_FULL_TABLE_NAME = "public.lhb_tiles_2022";
853
+ var LHB_SOURCE_LAYER = "public.lhb_tiles_2022";
854
+ function addOrUpdateBoundaryOverlay(map, tilesBaseUrl, input) {
855
+ const tileUrl = buildTileUrl(tilesBaseUrl, input.fullTableName);
856
+ const existing = map.getSource(input.sourceId);
857
+ if (existing instanceof VectorTileSource) {
858
+ existing.setTiles([tileUrl]);
859
+ } else {
860
+ if (existing) map.removeSource(input.sourceId);
861
+ map.addSource(input.sourceId, {
862
+ type: "vector",
863
+ tiles: [tileUrl],
864
+ minzoom: 0,
865
+ maxzoom: 14
866
+ });
867
+ }
868
+ if (map.getLayer(input.layerId)) {
869
+ map.setPaintProperty(input.layerId, "line-color", input.lineColor);
870
+ map.setPaintProperty(input.layerId, "line-width", input.lineWidth);
871
+ map.setLayoutProperty(input.layerId, "visibility", "visible");
872
+ return;
873
+ }
874
+ map.addLayer({
875
+ id: input.layerId,
876
+ type: "line",
877
+ source: input.sourceId,
878
+ "source-layer": input.sourceLayer,
879
+ paint: {
880
+ "line-color": input.lineColor,
881
+ "line-width": input.lineWidth
882
+ },
883
+ layout: {
884
+ visibility: "visible"
885
+ }
886
+ });
887
+ }
888
+ function addOrUpdateNhserOverlay(map, tilesBaseUrl, style) {
889
+ addOrUpdateBoundaryOverlay(map, tilesBaseUrl, {
890
+ sourceId: NHSER_SOURCE_ID,
891
+ layerId: NHSER_LAYER_ID,
892
+ fullTableName: NHSER_FULL_TABLE_NAME,
893
+ sourceLayer: NHSER_SOURCE_LAYER,
894
+ lineColor: style.boundaries.nhserColor ?? "#e00087",
895
+ lineWidth: style.boundaries.nhserWidth ?? 1.5
896
+ });
897
+ }
898
+ function addOrUpdateIcbOverlay(map, tilesBaseUrl, style) {
899
+ addOrUpdateBoundaryOverlay(map, tilesBaseUrl, {
900
+ sourceId: ICB_SOURCE_ID,
901
+ layerId: ICB_LAYER_ID,
902
+ fullTableName: ICB_FULL_TABLE_NAME,
903
+ sourceLayer: ICB_SOURCE_LAYER,
904
+ lineColor: style.boundaries.icbColor ?? "#57c7f2",
905
+ lineWidth: style.boundaries.icbWidth ?? 1
906
+ });
907
+ }
908
+ function addOrUpdateLhbOverlay(map, tilesBaseUrl, style) {
909
+ addOrUpdateBoundaryOverlay(map, tilesBaseUrl, {
910
+ sourceId: LHB_SOURCE_ID,
911
+ layerId: LHB_LAYER_ID,
912
+ fullTableName: LHB_FULL_TABLE_NAME,
913
+ sourceLayer: LHB_SOURCE_LAYER,
914
+ lineColor: style.boundaries.lhbColor ?? "#57c7f2",
915
+ lineWidth: style.boundaries.lhbWidth ?? 1
916
+ });
917
+ }
918
+ function hideOverlay(map, layerId) {
919
+ if (map.getLayer(layerId)) {
920
+ map.setLayoutProperty(layerId, "visibility", "none");
921
+ }
922
+ }
923
+ function hideNhserOverlay(map) {
924
+ hideOverlay(map, NHSER_LAYER_ID);
925
+ }
926
+ function hideIcbOverlay(map) {
927
+ hideOverlay(map, ICB_LAYER_ID);
928
+ }
929
+ function hideLhbOverlay(map) {
930
+ hideOverlay(map, LHB_LAYER_ID);
931
+ }
932
+
933
+ // src/map/legend.ts
934
+ function getLegendRows(state, visibility) {
935
+ const nation = state.nation;
936
+ const isNhserEligible = nation === "all" || nation === "england";
937
+ const isIcbEligible = nation === "all" || nation === "england";
938
+ const isLhbEligible = nation === "all" || nation === "wales";
939
+ const rows = [
940
+ {
941
+ key: "nhser",
942
+ label: "NHS England regions",
943
+ isVisible: visibility.nhser,
944
+ isEnabled: isNhserEligible,
945
+ isActive: state.overlays.nhser,
946
+ disabledNote: "England only"
947
+ },
948
+ {
949
+ key: "icb",
950
+ label: "ICBs",
951
+ isVisible: visibility.icb,
952
+ isEnabled: isIcbEligible,
953
+ isActive: state.overlays.icb,
954
+ disabledNote: "England only"
955
+ },
956
+ {
957
+ key: "localAuthority",
958
+ label: "Local authorities",
959
+ isVisible: visibility.localAuthority,
960
+ isEnabled: true,
961
+ isActive: state.overlays.localAuthority
962
+ },
963
+ {
964
+ key: "lhb",
965
+ label: "Local health boards",
966
+ isVisible: visibility.lhb,
967
+ isEnabled: isLhbEligible,
968
+ isActive: state.overlays.lhb,
969
+ disabledNote: "Wales only"
970
+ }
971
+ ];
972
+ return rows.filter((row) => row.isVisible);
973
+ }
974
+ function applyLegendPosition(el, position) {
975
+ el.style.top = "";
976
+ el.style.right = "";
977
+ el.style.bottom = "";
978
+ el.style.left = "";
979
+ switch (position) {
980
+ case "top-left":
981
+ el.style.top = "12px";
982
+ el.style.left = "12px";
983
+ break;
984
+ case "bottom-left":
985
+ el.style.bottom = "12px";
986
+ el.style.left = "12px";
987
+ break;
988
+ case "bottom-right":
989
+ el.style.bottom = "12px";
990
+ el.style.right = "12px";
991
+ break;
992
+ case "top-right":
993
+ default:
994
+ el.style.top = "12px";
995
+ el.style.right = "12px";
996
+ break;
997
+ }
998
+ }
999
+ function createLegendControl(input) {
1000
+ const containerStyle = window.getComputedStyle(input.container);
1001
+ if (containerStyle.position === "static") {
1002
+ input.container.style.position = "relative";
1003
+ }
1004
+ let currentState = input.state;
1005
+ let currentStyle = input.style;
1006
+ let collapsed = input.collapsed;
1007
+ const root = document.createElement("div");
1008
+ root.setAttribute("data-rcpch-legend", "true");
1009
+ root.style.position = "absolute";
1010
+ root.style.zIndex = "5";
1011
+ root.style.pointerEvents = "auto";
1012
+ applyLegendPosition(root, input.position);
1013
+ const panel = document.createElement("div");
1014
+ const headerBtn = document.createElement("button");
1015
+ const headerTitle = document.createElement("span");
1016
+ const headerIcon = document.createElement("span");
1017
+ const body = document.createElement("div");
1018
+ const keySection = document.createElement("div");
1019
+ headerBtn.type = "button";
1020
+ headerBtn.setAttribute("aria-expanded", String(!collapsed));
1021
+ headerTitle.textContent = input.title;
1022
+ headerIcon.textContent = collapsed ? "+" : "-";
1023
+ headerBtn.appendChild(headerTitle);
1024
+ headerBtn.appendChild(headerIcon);
1025
+ panel.appendChild(headerBtn);
1026
+ panel.appendChild(body);
1027
+ panel.appendChild(keySection);
1028
+ root.appendChild(panel);
1029
+ input.container.appendChild(root);
1030
+ function applyStyle() {
1031
+ const legend = currentStyle.legend;
1032
+ const backgroundColor = legend?.backgroundColor ?? "#ffffff";
1033
+ const textColor = legend?.textColor ?? "#0d0d58";
1034
+ const borderColor = legend?.borderColor ?? "#d8dde6";
1035
+ const borderRadius = legend?.borderRadius ?? 8;
1036
+ const fontSize = legend?.fontSize ?? 13;
1037
+ const fontFamily = legend?.fontFamily ?? "system-ui, -apple-system, Segoe UI, Roboto, sans-serif";
1038
+ const width = legend?.width ?? 220;
1039
+ const boxShadow = legend?.boxShadow ?? "0 6px 18px rgba(0, 0, 0, 0.12)";
1040
+ panel.style.background = backgroundColor;
1041
+ panel.style.color = textColor;
1042
+ panel.style.border = `1px solid ${borderColor}`;
1043
+ panel.style.borderRadius = `${borderRadius}px`;
1044
+ panel.style.width = `${width}px`;
1045
+ panel.style.boxShadow = boxShadow;
1046
+ panel.style.overflow = "hidden";
1047
+ headerBtn.style.width = "100%";
1048
+ headerBtn.style.border = "0";
1049
+ headerBtn.style.background = "transparent";
1050
+ headerBtn.style.color = textColor;
1051
+ headerBtn.style.display = "flex";
1052
+ headerBtn.style.alignItems = "center";
1053
+ headerBtn.style.justifyContent = "space-between";
1054
+ headerBtn.style.padding = "10px 12px";
1055
+ headerBtn.style.cursor = "pointer";
1056
+ headerBtn.style.fontWeight = "600";
1057
+ headerBtn.style.fontSize = `${fontSize}px`;
1058
+ headerBtn.style.fontFamily = fontFamily;
1059
+ headerBtn.style.textAlign = "left";
1060
+ body.style.padding = "0 12px 10px";
1061
+ body.style.display = collapsed ? "none" : "block";
1062
+ body.style.fontFamily = fontFamily;
1063
+ body.style.fontSize = `${fontSize}px`;
1064
+ keySection.style.padding = "0 12px 10px";
1065
+ keySection.style.display = collapsed ? "none" : "block";
1066
+ keySection.style.fontFamily = fontFamily;
1067
+ keySection.style.fontSize = `${Math.max(fontSize - 1, 11)}px`;
1068
+ keySection.style.borderTop = `1px solid ${borderColor}`;
1069
+ }
1070
+ function renderRows() {
1071
+ body.innerHTML = "";
1072
+ keySection.innerHTML = "";
1073
+ const rows = getLegendRows(currentState, input.visibility);
1074
+ if (!rows.length) {
1075
+ root.style.display = "none";
1076
+ return;
1077
+ }
1078
+ root.style.display = "";
1079
+ for (const row of rows) {
1080
+ const rowBtn = document.createElement("button");
1081
+ const dot = document.createElement("span");
1082
+ const label = document.createElement("span");
1083
+ const textWrap = document.createElement("span");
1084
+ rowBtn.type = "button";
1085
+ rowBtn.style.width = "100%";
1086
+ rowBtn.style.border = "0";
1087
+ rowBtn.style.background = "transparent";
1088
+ rowBtn.style.color = currentStyle.legend?.textColor ?? "#0d0d58";
1089
+ rowBtn.style.display = "flex";
1090
+ rowBtn.style.alignItems = "center";
1091
+ rowBtn.style.gap = "8px";
1092
+ rowBtn.style.padding = "6px 0";
1093
+ rowBtn.style.cursor = row.isEnabled ? "pointer" : "not-allowed";
1094
+ rowBtn.style.textAlign = "left";
1095
+ rowBtn.style.opacity = row.isEnabled ? row.isActive ? "1" : "0.75" : "0.5";
1096
+ dot.style.width = "10px";
1097
+ dot.style.height = "10px";
1098
+ dot.style.borderRadius = "999px";
1099
+ dot.style.flex = "0 0 10px";
1100
+ dot.style.background = row.isActive ? currentStyle.legend?.toggleOnColor ?? "#0d0d58" : currentStyle.legend?.toggleOffColor ?? "#6b7280";
1101
+ textWrap.style.display = "flex";
1102
+ textWrap.style.flexDirection = "column";
1103
+ textWrap.style.gap = "1px";
1104
+ label.textContent = row.label;
1105
+ textWrap.appendChild(label);
1106
+ if (!row.isEnabled && row.disabledNote) {
1107
+ const note = document.createElement("span");
1108
+ note.textContent = row.disabledNote;
1109
+ note.style.fontSize = "11px";
1110
+ note.style.opacity = "0.9";
1111
+ textWrap.appendChild(note);
1112
+ }
1113
+ rowBtn.setAttribute("aria-pressed", String(row.isActive));
1114
+ rowBtn.setAttribute("aria-disabled", String(!row.isEnabled));
1115
+ rowBtn.disabled = !row.isEnabled;
1116
+ rowBtn.setAttribute(
1117
+ "title",
1118
+ !row.isEnabled && row.disabledNote ? `${row.label} (${row.disabledNote})` : row.isActive ? `Hide ${row.label}` : `Show ${row.label}`
1119
+ );
1120
+ rowBtn.addEventListener("click", () => {
1121
+ if (!row.isEnabled) return;
1122
+ input.onToggle(row.key, !row.isActive);
1123
+ });
1124
+ rowBtn.appendChild(dot);
1125
+ rowBtn.appendChild(textWrap);
1126
+ body.appendChild(rowBtn);
1127
+ }
1128
+ renderKeySection(rows);
1129
+ }
1130
+ function renderKeySection(rows) {
1131
+ const keyTitle = document.createElement("div");
1132
+ keyTitle.textContent = "Key";
1133
+ keyTitle.style.paddingTop = "8px";
1134
+ keyTitle.style.paddingBottom = "6px";
1135
+ keyTitle.style.fontWeight = "600";
1136
+ keySection.appendChild(keyTitle);
1137
+ for (const row of rows) {
1138
+ const line = document.createElement("div");
1139
+ const swatch = document.createElement("span");
1140
+ const label = document.createElement("span");
1141
+ line.style.display = "flex";
1142
+ line.style.alignItems = "center";
1143
+ line.style.gap = "8px";
1144
+ line.style.padding = "2px 0";
1145
+ swatch.style.display = "inline-block";
1146
+ swatch.style.width = "18px";
1147
+ swatch.style.height = "0";
1148
+ swatch.style.borderTopWidth = `${Math.max(
1149
+ resolveBoundaryWidth(row.key, currentStyle),
1150
+ 2
1151
+ )}px`;
1152
+ swatch.style.borderTopStyle = "solid";
1153
+ swatch.style.borderTopColor = resolveBoundaryColor(row.key, currentStyle);
1154
+ label.textContent = row.label;
1155
+ line.appendChild(swatch);
1156
+ line.appendChild(label);
1157
+ keySection.appendChild(line);
1158
+ }
1159
+ const scale = document.createElement("div");
1160
+ scale.style.paddingTop = "8px";
1161
+ const scaleLabel = document.createElement("div");
1162
+ scaleLabel.textContent = "IMD decile (1 most deprived, 10 least deprived)";
1163
+ scaleLabel.style.paddingBottom = "4px";
1164
+ keySection.appendChild(scaleLabel);
1165
+ const ramp = document.createElement("div");
1166
+ ramp.style.display = "grid";
1167
+ ramp.style.gridTemplateColumns = "repeat(10, minmax(0, 1fr))";
1168
+ ramp.style.gap = "2px";
1169
+ const colors = getDecileColors(currentState.nation, currentStyle);
1170
+ for (let i = 0; i < 10; i++) {
1171
+ const chip = document.createElement("span");
1172
+ chip.style.height = "8px";
1173
+ chip.style.background = colors[i] ?? colors[0] ?? "#cccccc";
1174
+ chip.style.display = "inline-block";
1175
+ chip.title = `Decile ${i + 1}`;
1176
+ ramp.appendChild(chip);
1177
+ }
1178
+ const scaleTicks = document.createElement("div");
1179
+ scaleTicks.style.display = "flex";
1180
+ scaleTicks.style.justifyContent = "space-between";
1181
+ scaleTicks.style.paddingTop = "2px";
1182
+ scaleTicks.textContent = "1";
1183
+ const rightTick = document.createElement("span");
1184
+ rightTick.textContent = "10";
1185
+ scaleTicks.appendChild(rightTick);
1186
+ scale.appendChild(ramp);
1187
+ scale.appendChild(scaleTicks);
1188
+ keySection.appendChild(scale);
1189
+ }
1190
+ function resolveBoundaryColor(key, style) {
1191
+ if (key === "localAuthority") return style.boundaries.localAuthorityColor ?? "#0d0d58";
1192
+ if (key === "nhser") return style.boundaries.nhserColor ?? "#e00087";
1193
+ if (key === "icb") return style.boundaries.icbColor ?? "#57c7f2";
1194
+ return style.boundaries.lhbColor ?? "#57c7f2";
1195
+ }
1196
+ function resolveBoundaryWidth(key, style) {
1197
+ if (key === "localAuthority") return style.boundaries.localAuthorityWidth ?? 1;
1198
+ if (key === "nhser") return style.boundaries.nhserWidth ?? 1.5;
1199
+ if (key === "icb") return style.boundaries.icbWidth ?? 1;
1200
+ return style.boundaries.lhbWidth ?? 1;
1201
+ }
1202
+ headerBtn.addEventListener("click", () => {
1203
+ collapsed = !collapsed;
1204
+ headerBtn.setAttribute("aria-expanded", String(!collapsed));
1205
+ headerIcon.textContent = collapsed ? "+" : "-";
1206
+ body.style.display = collapsed ? "none" : "block";
1207
+ keySection.style.display = collapsed ? "none" : "block";
1208
+ });
1209
+ applyStyle();
1210
+ renderRows();
1211
+ return {
1212
+ update(nextState, nextStyle) {
1213
+ currentState = nextState;
1214
+ currentStyle = nextStyle;
1215
+ applyStyle();
1216
+ renderRows();
1217
+ },
1218
+ destroy() {
1219
+ root.remove();
1220
+ }
1221
+ };
1222
+ }
1223
+ var logger = {
1224
+ debug(message, ...args) {
1225
+ },
1226
+ info(message, ...args) {
1227
+ console.info(`[rcpch-imd-map] ${message}`, ...args);
1228
+ },
1229
+ warn(message, ...args) {
1230
+ console.warn(`[rcpch-imd-map] ${message}`, ...args);
1231
+ },
1232
+ error(message, ...args) {
1233
+ console.error(`[rcpch-imd-map] ${message}`, ...args);
1234
+ }
1235
+ };
1236
+
1237
+ // src/core/createImdMap.ts
1238
+ var UK_CENTER = [-2.5, 54];
1239
+ var UK_ZOOM = 5;
1240
+ function createImdMap(options) {
1241
+ const containerEl = typeof options.container === "string" ? document.getElementById(options.container) : options.container;
1242
+ if (!containerEl) {
1243
+ throw new Error(
1244
+ `[rcpch-imd-map] Container not found: "${String(options.container)}". Ensure the element exists in the DOM before calling createImdMap.`
1245
+ );
1246
+ }
1247
+ const tilesBaseUrl = options.tilesBaseUrl ?? "";
1248
+ if (!tilesBaseUrl) {
1249
+ logger.warn("No tilesBaseUrl provided. Choropleth tiles will not load.");
1250
+ }
1251
+ let resolvedStyle = mergeStyle(DEFAULT_STYLE, options.style);
1252
+ let state = createInitialState(options.initialNation ?? "all", options.initialEra ?? "2021");
1253
+ if (options.enableLocalAuthorityOverlay) {
1254
+ state = { ...state, overlays: { ...state.overlays, localAuthority: true } };
1255
+ }
1256
+ if (options.enableHealthOverlays) {
1257
+ state = {
1258
+ ...state,
1259
+ overlays: {
1260
+ ...state.overlays,
1261
+ nhser: true,
1262
+ icb: true,
1263
+ lhb: true
1264
+ }
1265
+ };
1266
+ }
1267
+ const showLegend = options.showLegend ?? true;
1268
+ const legendPosition = options.legendPosition ?? "top-right";
1269
+ const legendCollapsed = options.legendCollapsed ?? false;
1270
+ const legendTitle = options.legendTitle ?? "Map layers";
1271
+ const legendVisibility = {
1272
+ localAuthority: options.showLegendLocalAuthority ?? true,
1273
+ nhser: options.showLegendNhser ?? true,
1274
+ icb: options.showLegendIcb ?? true,
1275
+ lhb: options.showLegendLhb ?? true
1276
+ };
1277
+ if (options.initialNation && options.initialEra && willEraBeOverridden(options.initialNation, options.initialEra)) {
1278
+ const warning = {
1279
+ code: "ERA_OVERRIDE",
1280
+ message: `Era '${options.initialEra}' is not supported for nation '${options.initialNation}'. Effective era will be '${state.effectiveEra}'.`
1281
+ };
1282
+ logger.warn(warning.message);
1283
+ options.onWarning?.(warning);
1284
+ }
1285
+ const map = new Map({
1286
+ container: containerEl,
1287
+ style: options.mapStyleUrl ?? "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
1288
+ center: options.center ?? UK_CENTER,
1289
+ zoom: options.zoom ?? UK_ZOOM,
1290
+ attributionControl: false
1291
+ });
1292
+ map.addControl(new AttributionControl({ compact: true }));
1293
+ const popup = new Popup({ closeButton: false, closeOnClick: false });
1294
+ let mapLoaded = false;
1295
+ let pendingPatientFeatures = null;
1296
+ let pendingLeadCentreFeature = null;
1297
+ let pendingFitToData = null;
1298
+ let patientInteractionAttached = false;
1299
+ let leadCentreInteractionAttached = false;
1300
+ let storedLeadCentreCoord = null;
1301
+ let storedPatientCoords = [];
1302
+ function getFitCoords() {
1303
+ return [
1304
+ ...storedLeadCentreCoord ? [storedLeadCentreCoord] : [],
1305
+ ...storedPatientCoords
1306
+ ];
1307
+ }
1308
+ function applyFitToData(zoom, padding) {
1309
+ const coords = getFitCoords();
1310
+ if (!coords.length) {
1311
+ logger.warn("No patient or lead centre data to fit to.");
1312
+ return;
1313
+ }
1314
+ if (coords.length === 1) {
1315
+ const [lon, lat] = coords[0];
1316
+ map.flyTo({ center: [lon, lat], zoom: zoom ?? 6 });
1317
+ return;
1318
+ }
1319
+ let minLon = coords[0][0];
1320
+ let minLat = coords[0][1];
1321
+ let maxLon = coords[0][0];
1322
+ let maxLat = coords[0][1];
1323
+ for (const [lon, lat] of coords) {
1324
+ if (lon < minLon) minLon = lon;
1325
+ if (lat < minLat) minLat = lat;
1326
+ if (lon > maxLon) maxLon = lon;
1327
+ if (lat > maxLat) maxLat = lat;
1328
+ }
1329
+ map.fitBounds(
1330
+ [
1331
+ [minLon, minLat],
1332
+ [maxLon, maxLat]
1333
+ ],
1334
+ {
1335
+ padding: padding ?? 50,
1336
+ maxZoom: zoom
1337
+ }
1338
+ );
1339
+ }
1340
+ function applyOverlayVisibility() {
1341
+ if (!mapLoaded || !tilesBaseUrl) return;
1342
+ const nation = state.nation;
1343
+ const canShowNhser = nation === "all" || nation === "england";
1344
+ const canShowIcb = nation === "all" || nation === "england";
1345
+ const canShowLhb = nation === "all" || nation === "wales";
1346
+ if (state.overlays.localAuthority) {
1347
+ addOrUpdateLocalAuthorityOverlay(map, tilesBaseUrl, resolvedStyle);
1348
+ } else {
1349
+ hideLocalAuthorityOverlay(map);
1350
+ }
1351
+ if (state.overlays.nhser && canShowNhser) {
1352
+ addOrUpdateNhserOverlay(map, tilesBaseUrl, resolvedStyle);
1353
+ } else {
1354
+ hideNhserOverlay(map);
1355
+ }
1356
+ if (state.overlays.icb && canShowIcb) {
1357
+ addOrUpdateIcbOverlay(map, tilesBaseUrl, resolvedStyle);
1358
+ } else {
1359
+ hideIcbOverlay(map);
1360
+ }
1361
+ if (state.overlays.lhb && canShowLhb) {
1362
+ addOrUpdateLhbOverlay(map, tilesBaseUrl, resolvedStyle);
1363
+ } else {
1364
+ hideLhbOverlay(map);
1365
+ }
1366
+ }
1367
+ function setOverlayVisibilityState(input) {
1368
+ state = { ...state, overlays: { ...state.overlays, ...input } };
1369
+ applyOverlayVisibility();
1370
+ legendController?.update(state, resolvedStyle);
1371
+ }
1372
+ let legendController = null;
1373
+ if (showLegend) {
1374
+ legendController = createLegendControl({
1375
+ container: containerEl,
1376
+ position: legendPosition,
1377
+ title: legendTitle,
1378
+ collapsed: legendCollapsed,
1379
+ style: resolvedStyle,
1380
+ state,
1381
+ visibility: legendVisibility,
1382
+ onToggle: (key, nextValue) => {
1383
+ if (key === "localAuthority") {
1384
+ setOverlayVisibilityState({ localAuthority: nextValue });
1385
+ } else if (key === "nhser") {
1386
+ setOverlayVisibilityState({ nhser: nextValue });
1387
+ } else if (key === "icb") {
1388
+ setOverlayVisibilityState({ icb: nextValue });
1389
+ } else if (key === "lhb") {
1390
+ setOverlayVisibilityState({ lhb: nextValue });
1391
+ }
1392
+ }
1393
+ });
1394
+ }
1395
+ logger.debug(`tilesBaseUrl resolved to: "${tilesBaseUrl}"`);
1396
+ map.on("load", () => {
1397
+ mapLoaded = true;
1398
+ if (tilesBaseUrl) {
1399
+ addOrUpdateChoroplethSources(map, tilesBaseUrl, state.effectiveEra);
1400
+ addChoroplethLayers(map, state.nation, state.effectiveEra, resolvedStyle);
1401
+ }
1402
+ applyOverlayVisibility();
1403
+ attachChoroplethInteraction(map, popup, resolvedStyle, options);
1404
+ if (pendingPatientFeatures) {
1405
+ storedPatientCoords = pendingPatientFeatures.map((f) => f.geometry.coordinates);
1406
+ addOrUpdatePatientsSource(map, pendingPatientFeatures);
1407
+ addOrUpdatePatientsLayer(map, resolvedStyle);
1408
+ if (!patientInteractionAttached) {
1409
+ attachPatientInteraction(map, popup, resolvedStyle);
1410
+ patientInteractionAttached = true;
1411
+ }
1412
+ state = { ...state, hasPatients: true };
1413
+ pendingPatientFeatures = null;
1414
+ }
1415
+ if (pendingLeadCentreFeature) {
1416
+ storedLeadCentreCoord = pendingLeadCentreFeature.geometry.coordinates;
1417
+ addOrUpdateLeadCentreSource(map, pendingLeadCentreFeature);
1418
+ addOrUpdateLeadCentreLayer(map, resolvedStyle);
1419
+ if (!leadCentreInteractionAttached) {
1420
+ attachLeadCentreInteraction(map, popup, resolvedStyle);
1421
+ leadCentreInteractionAttached = true;
1422
+ }
1423
+ state = { ...state, hasLeadCentre: true };
1424
+ pendingLeadCentreFeature = null;
1425
+ }
1426
+ if (pendingFitToData) {
1427
+ const { zoom, padding } = pendingFitToData;
1428
+ map.once("idle", () => {
1429
+ applyFitToData(zoom, padding);
1430
+ });
1431
+ pendingFitToData = null;
1432
+ }
1433
+ });
1434
+ function applyViewChange(newNation, newEra) {
1435
+ const newEffectiveEra = resolveEffectiveEra(newNation, newEra);
1436
+ if (willEraBeOverridden(newNation, newEra)) {
1437
+ const warning = {
1438
+ code: "ERA_OVERRIDE",
1439
+ message: `Era '${newEra}' overridden to '${newEffectiveEra}' for nation '${newNation}'.`
1440
+ };
1441
+ logger.warn(warning.message);
1442
+ options.onWarning?.(warning);
1443
+ }
1444
+ const eraChanged = newEffectiveEra !== state.effectiveEra;
1445
+ const nationChanged = newNation !== state.nation;
1446
+ state = { ...state, nation: newNation, era: newEra, effectiveEra: newEffectiveEra };
1447
+ if (mapLoaded && tilesBaseUrl) {
1448
+ if (eraChanged) {
1449
+ removeChoroplethLayers(map);
1450
+ addOrUpdateChoroplethSources(map, tilesBaseUrl, newEffectiveEra);
1451
+ addChoroplethLayers(map, newNation, newEffectiveEra, resolvedStyle);
1452
+ } else if (nationChanged) {
1453
+ updateChoroplethNationFilter(map, newNation);
1454
+ }
1455
+ }
1456
+ options.onViewChange?.({ nation: newNation, era: newEra, effectiveEra: newEffectiveEra });
1457
+ }
1458
+ const instance = {
1459
+ setView({ nation, era } = {}) {
1460
+ applyViewChange(nation ?? state.nation, era ?? state.era);
1461
+ },
1462
+ setNation(nation) {
1463
+ applyViewChange(nation, state.era);
1464
+ },
1465
+ setEra(era) {
1466
+ applyViewChange(state.nation, era);
1467
+ },
1468
+ setStyle(newStyle) {
1469
+ resolvedStyle = mergeStyle(DEFAULT_STYLE, newStyle);
1470
+ if (mapLoaded) {
1471
+ updateChoroplethStyle(map, state.nation, resolvedStyle);
1472
+ applyOverlayVisibility();
1473
+ if (state.hasPatients) {
1474
+ addOrUpdatePatientsLayer(map, resolvedStyle);
1475
+ }
1476
+ if (state.hasLeadCentre) {
1477
+ addOrUpdateLeadCentreLayer(map, resolvedStyle);
1478
+ }
1479
+ legendController?.update(state, resolvedStyle);
1480
+ }
1481
+ },
1482
+ setOverlayVisibility(input) {
1483
+ setOverlayVisibilityState(input);
1484
+ },
1485
+ setPatients(data, patientOptions) {
1486
+ const { features, warnings } = normalizePatientInput(data, {
1487
+ strict: patientOptions?.strict
1488
+ });
1489
+ storedPatientCoords = features.map(
1490
+ (f) => f.geometry.coordinates
1491
+ );
1492
+ for (const w of warnings) {
1493
+ logger.warn(w.message);
1494
+ options.onWarning?.(w);
1495
+ }
1496
+ if (!mapLoaded) {
1497
+ pendingPatientFeatures = features;
1498
+ return;
1499
+ }
1500
+ addOrUpdatePatientsSource(map, features);
1501
+ addOrUpdatePatientsLayer(map, resolvedStyle);
1502
+ if (!patientInteractionAttached) {
1503
+ attachPatientInteraction(map, popup, resolvedStyle);
1504
+ patientInteractionAttached = true;
1505
+ }
1506
+ state = { ...state, hasPatients: true };
1507
+ },
1508
+ clearPatients() {
1509
+ pendingPatientFeatures = null;
1510
+ storedPatientCoords = [];
1511
+ if (!mapLoaded) return;
1512
+ removePatientsLayer(map);
1513
+ if (map.getSource(PATIENTS_SOURCE_ID)) map.removeSource(PATIENTS_SOURCE_ID);
1514
+ state = { ...state, hasPatients: false };
1515
+ },
1516
+ setLeadCentre(data, _options) {
1517
+ const feature = normalizeLeadCentreInput(data);
1518
+ if (!feature) {
1519
+ const warning = { code: "INVALID_LEAD_CENTRE", message: "Lead centre data could not be resolved to valid coordinates." };
1520
+ logger.warn(warning.message);
1521
+ options.onWarning?.(warning);
1522
+ return;
1523
+ }
1524
+ if (!mapLoaded) {
1525
+ pendingLeadCentreFeature = feature;
1526
+ return;
1527
+ }
1528
+ storedLeadCentreCoord = feature.geometry.coordinates;
1529
+ addOrUpdateLeadCentreSource(map, feature);
1530
+ addOrUpdateLeadCentreLayer(map, resolvedStyle);
1531
+ if (!leadCentreInteractionAttached) {
1532
+ attachLeadCentreInteraction(map, popup, resolvedStyle);
1533
+ leadCentreInteractionAttached = true;
1534
+ }
1535
+ state = { ...state, hasLeadCentre: true };
1536
+ },
1537
+ clearLeadCentre() {
1538
+ pendingLeadCentreFeature = null;
1539
+ storedLeadCentreCoord = null;
1540
+ if (!mapLoaded) return;
1541
+ removeLeadCentreLayer(map);
1542
+ if (map.getSource(LEAD_CENTRE_SOURCE_ID)) map.removeSource(LEAD_CENTRE_SOURCE_ID);
1543
+ state = { ...state, hasLeadCentre: false };
1544
+ },
1545
+ getState() {
1546
+ return { ...state };
1547
+ },
1548
+ resize() {
1549
+ map.resize();
1550
+ },
1551
+ fitToData(fitOptions) {
1552
+ const zoom = fitOptions?.zoom;
1553
+ const padding = fitOptions?.padding;
1554
+ if (!mapLoaded) {
1555
+ pendingFitToData = { zoom, padding };
1556
+ return;
1557
+ }
1558
+ applyFitToData(zoom, padding);
1559
+ },
1560
+ destroy() {
1561
+ popup.remove();
1562
+ legendController?.destroy();
1563
+ map.remove();
1564
+ }
1565
+ };
1566
+ return instance;
1567
+ }
1568
+
1569
+ export { createImdMap };
1570
+ //# sourceMappingURL=index.esm.js.map
1571
+ //# sourceMappingURL=index.esm.js.map