@rcpch/imd-map 0.2.1 → 0.3.1

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.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,28 @@ This project adheres to [Semantic Versioning](https://semver.org/).
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.3.1] — 2026-04-28
12
+
13
+ ### Added
14
+
15
+ - Optional tile API key support via `tilesApiKey` and `tilesApiKeyParam` options in `CreateImdMapOptions`.
16
+ - Tile API key query parameters are appended to all choropleth and boundary overlay tile requests.
17
+ - Customizable query parameter name for tile auth (defaults to `api_key`).
18
+ - Unit test coverage for tile URL builder with optional auth and auth propagation across overlays.
19
+
20
+ ### Documentation
21
+
22
+ - Updated README runtime tile configuration section with tile auth options and example usage.
23
+ - Added note that browser-delivered keys are non-secret and primarily useful for operational traffic control (rate limiting, revocation, origin restrictions).
24
+
25
+ ## [0.3.0] — 2026-04-27
26
+
27
+ ### Changed
28
+
29
+ - Updated all-UK era resolution so `initialNation: 'all'` honours the requested era instead of always forcing `2011`.
30
+ - Documented the mixed-vintage `uk_master_2021_*` UK tiles, where England uses 2021 LSOA boundaries with 2025 IMD data while Wales, Scotland, and Northern Ireland remain on their current older datasets.
31
+ - Clarified in the public docs that `era` refers to the LSOA boundary year, not the IMD publication year, and documented the England pairings of `2011` → 2011 LSOAs + 2019 IMD and `2021` → 2021 LSOAs + 2025 IMD.
32
+
11
33
  ## [0.2.0] — 2026-04-26
12
34
 
13
35
  ### Changed
package/README.md CHANGED
@@ -69,7 +69,7 @@ The UMD bundle includes MapLibre GL. No separate script tag required.
69
69
  ```html
70
70
  <div id="map" style="height: 600px"></div>
71
71
 
72
- <script src="https://cdn.jsdelivr.net/npm/@rcpch/imd-map@0.1.0/dist/umd/rcpch-imd-map.min.js"></script>
72
+ <script src="https://cdn.jsdelivr.net/npm/@rcpch/imd-map@0.3.0/dist/umd/rcpch-imd-map.min.js"></script>
73
73
  <script>
74
74
  const map = RcpchImdMap.createImdMap({
75
75
  container: 'map',
@@ -201,6 +201,24 @@ Tile URL resolution precedence:
201
201
 
202
202
  The library source contains **no hardcoded tile URLs**.
203
203
 
204
+ Optional tile auth query options:
205
+
206
+ - `tilesApiKey`: appends a query value to all choropleth and overlay tile requests
207
+ - `tilesApiKeyParam`: query parameter name for `tilesApiKey` (default: `api_key`)
208
+
209
+ Example:
210
+
211
+ ```js
212
+ createImdMap({
213
+ container: 'map',
214
+ tilesBaseUrl: 'https://your-tile-server.example.com',
215
+ tilesApiKey: 'switchable-browser-token',
216
+ tilesApiKeyParam: 'key',
217
+ });
218
+ ```
219
+
220
+ This is useful for revoking abusive traffic quickly, but browser-delivered keys must still be treated as non-secret.
221
+
204
222
  ### Overlay boundary tile contract
205
223
 
206
224
  Boundary overlays (local authority, NHSER, ICB, LHB) are requested from schema-qualified table ids and rendered with the same schema-qualified `source-layer` name. Example:
@@ -216,9 +234,16 @@ Bring your own overlay configuration (custom overlay table/layer names via libra
216
234
 
217
235
  ## Nation and era rules
218
236
 
237
+ The `era` option refers to the boundary year used for the LSOA geography, not the IMD publication year.
238
+
239
+ For England, the supported pairings are:
240
+
241
+ - `2011` era = 2011 LSOA boundaries + 2019 IMD data
242
+ - `2021` era = 2021 LSOA boundaries + 2025 IMD data
243
+
219
244
  | Nation | Requested era | Effective era |
220
245
  |---|---|---|
221
- | `all` | any | always `2011` |
246
+ | `all` | `2011` or `2021` | as requested |
222
247
  | `england` | `2011` or `2021` | as requested |
223
248
  | `wales` | any | always `2011` |
224
249
  | `scotland` | any | always `2011` |
@@ -226,6 +251,31 @@ Bring your own overlay configuration (custom overlay table/layer names via libra
226
251
 
227
252
  When the effective era differs from the requested era, `onWarning` is called with code `ERA_OVERRIDE`.
228
253
 
254
+ For all-UK maps, `initialEra: '2021'` now uses the mixed-vintage `uk_master_2021_*` tables: England renders with 2021 LSOA boundaries and 2025 IMD data, while Wales, Scotland, and Northern Ireland continue to render from their existing older datasets within the same UK tile family. Use `initialEra: '2011'` when you want the older England 2011 LSOA + 2019 IMD view alongside the existing Welsh and other nation data.
255
+
256
+ This means you can instantiate two separate UK maps in the same application, choosing the England boundary/IMD pairing by era:
257
+
258
+ ```js
259
+ const historicalMap = createImdMap({
260
+ container: 'map-2011',
261
+ tilesBaseUrl: 'https://your-tile-server.example.com',
262
+ initialNation: 'all',
263
+ initialEra: '2011',
264
+ });
265
+
266
+ const currentMap = createImdMap({
267
+ container: 'map-2021',
268
+ tilesBaseUrl: 'https://your-tile-server.example.com',
269
+ initialNation: 'all',
270
+ initialEra: '2021',
271
+ });
272
+ ```
273
+
274
+ In a patient-facing application, a common pattern would be:
275
+
276
+ - patients before 2025 or 2026 cutoff: `initialEra: '2011'`
277
+ - patients in the newer cohort: `initialEra: '2021'`
278
+
229
279
  ---
230
280
 
231
281
  ## Styling
@@ -375,6 +425,8 @@ map.setStyle({ tooltip: { areaLabel: 'Local area' } });
375
425
  |---|---|---|---|
376
426
  | `container` | `string \| HTMLElement` | — | DOM element ID or element reference |
377
427
  | `tilesBaseUrl` | `string` | — | Base URL of the tile server |
428
+ | `tilesApiKey` | `string` | — | Optional API key appended to tile URLs as a query parameter |
429
+ | `tilesApiKeyParam` | `string` | `'api_key'` | Query parameter name used for `tilesApiKey` |
378
430
  | `initialNation` | `Nation` | `'all'` | Starting nation filter |
379
431
  | `initialEra` | `Era` | `'2021'` | Requested era (may be overridden) |
380
432
  | `enableLocalAuthorityOverlay` | `boolean` | `false` | Show local authority boundary overlay at startup |
package/dist/index.d.ts CHANGED
@@ -183,6 +183,10 @@ interface CreateImdMapOptions {
183
183
  container: string | HTMLElement;
184
184
  /** Base URL of the tile server. Required for choropleth rendering. */
185
185
  tilesBaseUrl?: string;
186
+ /** Optional API key appended to tile URLs as a query string parameter. */
187
+ tilesApiKey?: string;
188
+ /** Query parameter name used for tilesApiKey. Default: api_key. */
189
+ tilesApiKeyParam?: string;
186
190
  initialNation?: Nation;
187
191
  initialEra?: Era;
188
192
  showDefaultControls?: boolean;
package/dist/index.esm.js CHANGED
@@ -6,7 +6,7 @@ import { Map, AttributionControl, Popup, VectorTileSource, GeoJSONSource } from
6
6
  function resolveEffectiveEra(nation, requestedEra) {
7
7
  switch (nation) {
8
8
  case "all":
9
- return "2011";
9
+ return requestedEra;
10
10
  case "england":
11
11
  return requestedEra;
12
12
  case "wales":
@@ -29,9 +29,13 @@ var ZOOM_TIERS = [
29
29
  function resolveFullTableName(effectiveEra, tier) {
30
30
  return `public.uk_master_${effectiveEra}_${tier}`;
31
31
  }
32
- function buildTileUrl(tilesBaseUrl, fullTableName) {
32
+ function buildTileUrl(tilesBaseUrl, fullTableName, auth) {
33
33
  const base = tilesBaseUrl.replace(/\/$/, "");
34
- return `${base}/${fullTableName}/{z}/{x}/{y}.pbf`;
34
+ const tileUrl = `${base}/${fullTableName}/{z}/{x}/{y}.pbf`;
35
+ if (!auth?.apiKey) return tileUrl;
36
+ const paramName = auth.apiKeyParam?.trim() || "api_key";
37
+ const separator = tileUrl.includes("?") ? "&" : "?";
38
+ return `${tileUrl}${separator}${encodeURIComponent(paramName)}=${encodeURIComponent(auth.apiKey)}`;
35
39
  }
36
40
  function resolveNationFilter(nation) {
37
41
  if (nation === "all") return null;
@@ -60,11 +64,11 @@ function choroplethSourceId(tier) {
60
64
  ZOOM_TIERS.map((t) => choroplethSourceId(t.tier));
61
65
  var PATIENTS_SOURCE_ID = "rcpch-imd-patients";
62
66
  var LEAD_CENTRE_SOURCE_ID = "rcpch-imd-lead-centre";
63
- function addOrUpdateChoroplethSources(map, tilesBaseUrl, effectiveEra) {
67
+ function addOrUpdateChoroplethSources(map, tilesBaseUrl, effectiveEra, tileAuth) {
64
68
  for (const { tier } of ZOOM_TIERS) {
65
69
  const sourceId = choroplethSourceId(tier);
66
70
  const fullTableName = resolveFullTableName(effectiveEra, tier);
67
- const tileUrl = buildTileUrl(tilesBaseUrl, fullTableName);
71
+ const tileUrl = buildTileUrl(tilesBaseUrl, fullTableName, tileAuth);
68
72
  const existing = map.getSource(sourceId);
69
73
  if (existing instanceof VectorTileSource) {
70
74
  existing.setTiles([tileUrl]);
@@ -899,13 +903,13 @@ function localAuthoritySourceId(tier) {
899
903
  function localAuthorityLayerId(tier) {
900
904
  return `${LOCAL_AUTHORITY_LAYER_ID}-${tier}`;
901
905
  }
902
- function addOrUpdateLocalAuthorityOverlay(map, tilesBaseUrl, style) {
906
+ function addOrUpdateLocalAuthorityOverlay(map, tilesBaseUrl, style, tileAuth) {
903
907
  for (const { tier, minzoom, maxzoom } of ZOOM_TIERS) {
904
908
  const sourceId = localAuthoritySourceId(tier);
905
909
  const layerId = localAuthorityLayerId(tier);
906
910
  const fullTableName = buildMvtLayerName(LOCAL_AUTHORITY_TABLE_PREFIX, tier);
907
911
  const sourceLayer = fullTableName;
908
- const tileUrl = buildTileUrl(tilesBaseUrl, fullTableName);
912
+ const tileUrl = buildTileUrl(tilesBaseUrl, fullTableName, tileAuth);
909
913
  const existing = map.getSource(sourceId);
910
914
  if (existing instanceof VectorTileSource) {
911
915
  existing.setTiles([tileUrl]);
@@ -972,13 +976,13 @@ function overlaySourceId(baseSourceId, tier) {
972
976
  function overlayLayerId(baseLayerId, tier) {
973
977
  return `${baseLayerId}-${tier}`;
974
978
  }
975
- function addOrUpdateBoundaryOverlay(map, tilesBaseUrl, input) {
979
+ function addOrUpdateBoundaryOverlay(map, tilesBaseUrl, tileAuth, input) {
976
980
  for (const { tier, minzoom, maxzoom } of ZOOM_TIERS) {
977
981
  const sourceId = overlaySourceId(input.sourceId, tier);
978
982
  const layerId = overlayLayerId(input.layerId, tier);
979
983
  const fullTableName = buildMvtLayerName(input.tablePrefix, tier);
980
984
  const sourceLayer = fullTableName;
981
- const tileUrl = buildTileUrl(tilesBaseUrl, fullTableName);
985
+ const tileUrl = buildTileUrl(tilesBaseUrl, fullTableName, tileAuth);
982
986
  const existing = map.getSource(sourceId);
983
987
  if (existing instanceof VectorTileSource) {
984
988
  existing.setTiles([tileUrl]);
@@ -1014,8 +1018,8 @@ function addOrUpdateBoundaryOverlay(map, tilesBaseUrl, input) {
1014
1018
  });
1015
1019
  }
1016
1020
  }
1017
- function addOrUpdateNhserOverlay(map, tilesBaseUrl, style) {
1018
- addOrUpdateBoundaryOverlay(map, tilesBaseUrl, {
1021
+ function addOrUpdateNhserOverlay(map, tilesBaseUrl, style, tileAuth) {
1022
+ addOrUpdateBoundaryOverlay(map, tilesBaseUrl, tileAuth, {
1019
1023
  sourceId: NHSER_SOURCE_ID,
1020
1024
  layerId: NHSER_LAYER_ID,
1021
1025
  tablePrefix: NHSER_TABLE_PREFIX,
@@ -1023,8 +1027,8 @@ function addOrUpdateNhserOverlay(map, tilesBaseUrl, style) {
1023
1027
  lineWidth: style.boundaries.nhserWidth ?? 1.5
1024
1028
  });
1025
1029
  }
1026
- function addOrUpdateIcbOverlay(map, tilesBaseUrl, style) {
1027
- addOrUpdateBoundaryOverlay(map, tilesBaseUrl, {
1030
+ function addOrUpdateIcbOverlay(map, tilesBaseUrl, style, tileAuth) {
1031
+ addOrUpdateBoundaryOverlay(map, tilesBaseUrl, tileAuth, {
1028
1032
  sourceId: ICB_SOURCE_ID,
1029
1033
  layerId: ICB_LAYER_ID,
1030
1034
  tablePrefix: ICB_TABLE_PREFIX,
@@ -1032,8 +1036,8 @@ function addOrUpdateIcbOverlay(map, tilesBaseUrl, style) {
1032
1036
  lineWidth: style.boundaries.icbWidth ?? 1
1033
1037
  });
1034
1038
  }
1035
- function addOrUpdateLhbOverlay(map, tilesBaseUrl, style) {
1036
- addOrUpdateBoundaryOverlay(map, tilesBaseUrl, {
1039
+ function addOrUpdateLhbOverlay(map, tilesBaseUrl, style, tileAuth) {
1040
+ addOrUpdateBoundaryOverlay(map, tilesBaseUrl, tileAuth, {
1037
1041
  sourceId: LHB_SOURCE_ID,
1038
1042
  layerId: LHB_LAYER_ID,
1039
1043
  tablePrefix: LHB_TABLE_PREFIX,
@@ -1377,6 +1381,10 @@ function createImdMap(options) {
1377
1381
  if (!tilesBaseUrl) {
1378
1382
  logger.warn("No tilesBaseUrl provided. Choropleth tiles will not load.");
1379
1383
  }
1384
+ const tileAuth = {
1385
+ apiKey: options.tilesApiKey,
1386
+ apiKeyParam: options.tilesApiKeyParam
1387
+ };
1380
1388
  let resolvedStyle = mergeStyle(DEFAULT_STYLE, options.style);
1381
1389
  let state = createInitialState(options.initialNation ?? "all", options.initialEra ?? "2021");
1382
1390
  if (options.enableLocalAuthorityOverlay) {
@@ -1473,22 +1481,22 @@ function createImdMap(options) {
1473
1481
  const canShowIcb = nation === "all" || nation === "england";
1474
1482
  const canShowLhb = nation === "all" || nation === "wales";
1475
1483
  if (state.overlays.localAuthority) {
1476
- addOrUpdateLocalAuthorityOverlay(map, tilesBaseUrl, resolvedStyle);
1484
+ addOrUpdateLocalAuthorityOverlay(map, tilesBaseUrl, resolvedStyle, tileAuth);
1477
1485
  } else {
1478
1486
  hideLocalAuthorityOverlay(map);
1479
1487
  }
1480
1488
  if (state.overlays.nhser && canShowNhser) {
1481
- addOrUpdateNhserOverlay(map, tilesBaseUrl, resolvedStyle);
1489
+ addOrUpdateNhserOverlay(map, tilesBaseUrl, resolvedStyle, tileAuth);
1482
1490
  } else {
1483
1491
  hideNhserOverlay(map);
1484
1492
  }
1485
1493
  if (state.overlays.icb && canShowIcb) {
1486
- addOrUpdateIcbOverlay(map, tilesBaseUrl, resolvedStyle);
1494
+ addOrUpdateIcbOverlay(map, tilesBaseUrl, resolvedStyle, tileAuth);
1487
1495
  } else {
1488
1496
  hideIcbOverlay(map);
1489
1497
  }
1490
1498
  if (state.overlays.lhb && canShowLhb) {
1491
- addOrUpdateLhbOverlay(map, tilesBaseUrl, resolvedStyle);
1499
+ addOrUpdateLhbOverlay(map, tilesBaseUrl, resolvedStyle, tileAuth);
1492
1500
  } else {
1493
1501
  hideLhbOverlay(map);
1494
1502
  }
@@ -1522,10 +1530,13 @@ function createImdMap(options) {
1522
1530
  });
1523
1531
  }
1524
1532
  logger.debug(`tilesBaseUrl resolved to: "${tilesBaseUrl}"`);
1533
+ if (tileAuth.apiKey) {
1534
+ logger.debug("Tile API key is configured for tile URL requests.");
1535
+ }
1525
1536
  map.on("load", () => {
1526
1537
  mapLoaded = true;
1527
1538
  if (tilesBaseUrl) {
1528
- addOrUpdateChoroplethSources(map, tilesBaseUrl, state.effectiveEra);
1539
+ addOrUpdateChoroplethSources(map, tilesBaseUrl, state.effectiveEra, tileAuth);
1529
1540
  addChoroplethLayers(map, state.nation, state.effectiveEra, resolvedStyle);
1530
1541
  }
1531
1542
  applyOverlayVisibility();
@@ -1576,7 +1587,7 @@ function createImdMap(options) {
1576
1587
  if (mapLoaded && tilesBaseUrl) {
1577
1588
  if (eraChanged) {
1578
1589
  removeChoroplethLayers(map);
1579
- addOrUpdateChoroplethSources(map, tilesBaseUrl, newEffectiveEra);
1590
+ addOrUpdateChoroplethSources(map, tilesBaseUrl, newEffectiveEra, tileAuth);
1580
1591
  addChoroplethLayers(map, newNation, newEffectiveEra, resolvedStyle);
1581
1592
  } else if (nationChanged) {
1582
1593
  updateChoroplethNationFilter(map, newNation);