@rcpch/imd-map 0.3.0 → 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,20 @@ 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
+
11
25
  ## [0.3.0] — 2026-04-27
12
26
 
13
27
  ### Changed
package/README.md CHANGED
@@ -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:
@@ -407,6 +425,8 @@ map.setStyle({ tooltip: { areaLabel: 'Local area' } });
407
425
  |---|---|---|---|
408
426
  | `container` | `string \| HTMLElement` | — | DOM element ID or element reference |
409
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` |
410
430
  | `initialNation` | `Nation` | `'all'` | Starting nation filter |
411
431
  | `initialEra` | `Era` | `'2021'` | Requested era (may be overridden) |
412
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
@@ -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);