@india-boundary-corrector/leaflet-layer 0.0.2 → 0.0.4

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/README.md CHANGED
@@ -19,9 +19,9 @@ npm install @india-boundary-corrector/leaflet-layer leaflet
19
19
  No bundler required! Just include the script and use the global `IndiaBoundaryCorrector`:
20
20
 
21
21
  ```html
22
- <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
23
- <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
24
- <script src="https://unpkg.com/@india-boundary-corrector/leaflet-layer/dist/index.global.js"></script>
22
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css" />
23
+ <script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script>
24
+ <script src="https://cdn.jsdelivr.net/npm/@india-boundary-corrector/leaflet-layer/dist/index.global.js"></script>
25
25
 
26
26
  <div id="map" style="height: 400px;"></div>
27
27
 
@@ -117,6 +117,7 @@ All standard `L.TileLayer` options are supported, plus:
117
117
  | `pmtilesUrl` | string | URL to PMTiles file (auto-detected if not provided) |
118
118
  | `layerConfig` | LayerConfig \| string | Layer config object or config ID |
119
119
  | `extraLayerConfigs` | LayerConfig[] | Additional configs for auto-detection |
120
+ | `fallbackOnCorrectionFailure` | boolean | Return original tile if corrections fail (default: true) |
120
121
 
121
122
  ## Events
122
123
 
@@ -138,6 +139,10 @@ layer.on('correctionerror', (e) => {
138
139
  | `coords` | object | Tile coordinates `{ z, x, y }` |
139
140
  | `tileUrl` | string | URL of the tile being loaded |
140
141
 
142
+ ## Bundling
143
+
144
+ If you're bundling your application (Rollup, Webpack, Vite, etc.), you may need to copy the PMTiles data file to your output directory. See **[Bundling the PMTiles Asset](../data/bundling-pmtiles.md)** for instructions.
145
+
141
146
  ## License
142
147
 
143
148
  Unlicense
package/dist/index.cjs CHANGED
@@ -139,10 +139,10 @@ function templateToTemplateRegex(template) {
139
139
  let pattern = template.replace(/[.*+?^${}()|[\]\\]/g, (char) => {
140
140
  if (char === "{" || char === "}") return char;
141
141
  return "\\" + char;
142
- }).replace(/^https:\/\//, "https?://").replace(/^http:\/\//, "https?://").replace(/\{([a-z0-9])-([a-z0-9])\}/gi, (_2, start, end) => `(\\{${start}-${end}\\}|[a-z0-9]+)`).replace(/\{(z|x|y|s|r)\}/gi, (_2, name) => {
142
+ }).replace(/^https:\/\//, "https?://").replace(/^http:\/\//, "https?://").replace(/\{([a-z0-9])-([a-z0-9])\}/gi, (_2, start, end) => `(\\{${start}-${end}\\}|\\{s\\}|[a-z0-9]+)`).replace(/\{(z|x|y|s|r)\}/gi, (_2, name) => {
143
143
  const lowerName = name.toLowerCase();
144
144
  if (lowerName === "s") {
145
- return "(\\{s\\}|[a-z0-9]+)";
145
+ return "(\\{s\\}|\\{[a-z0-9]-[a-z0-9]\\}|[a-z0-9]+)";
146
146
  }
147
147
  if (lowerName === "r") {
148
148
  return "(\\{r\\}|@\\d+x)?";
@@ -159,6 +159,7 @@ var LayerConfig = class _LayerConfig {
159
159
  // Tile URL templates for matching (e.g., "https://{s}.tile.example.com/{z}/{x}/{y}.png")
160
160
  tileUrlTemplates = [],
161
161
  // Line width stops: map of zoom level to line width (at least 2 entries)
162
+ // Note: interpolated/extrapolated line width is capped at a minimum of 0.5
162
163
  lineWidthStops = { 1: 0.5, 10: 2.5 },
163
164
  // Line styles array - each element describes a line to draw
164
165
  // { color: string, widthFraction?: number, dashArray?: number[], startZoom?: number, endZoom?: number }
@@ -176,6 +177,9 @@ var LayerConfig = class _LayerConfig {
176
177
  if (!id || typeof id !== "string") {
177
178
  throw new Error("LayerConfig requires a non-empty string id");
178
179
  }
180
+ if (id.includes("/")) {
181
+ throw new Error(`LayerConfig id cannot contain slashes: "${id}"`);
182
+ }
179
183
  this.id = id;
180
184
  this.startZoom = startZoom;
181
185
  this.zoomThreshold = zoomThreshold;
@@ -398,6 +402,9 @@ for (const configData of configs_default) {
398
402
  }
399
403
 
400
404
  // ../tilefixer/dist/index.js
405
+ var __defProp2 = Object.defineProperty;
406
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
407
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
401
408
  var u8 = Uint8Array;
402
409
  var u16 = Uint16Array;
403
410
  var i32 = Int32Array;
@@ -2413,10 +2420,16 @@ function writeUtf8(buf, str, pos) {
2413
2420
  }
2414
2421
  return pos;
2415
2422
  }
2416
- var DEFAULT_CACHE_SIZE = 64;
2417
2423
  function toIndex(z2, x22, y) {
2418
2424
  return `${x22}:${y}:${z2}`;
2419
2425
  }
2426
+ function countFeatures(corrections) {
2427
+ let count = 0;
2428
+ for (const features of Object.values(corrections)) {
2429
+ count += features.length;
2430
+ }
2431
+ return count;
2432
+ }
2420
2433
  function parseTile(buffer) {
2421
2434
  const tile = new VectorTile(new Pbf(buffer));
2422
2435
  const result = {};
@@ -2465,16 +2478,19 @@ var CorrectionsSource = class {
2465
2478
  /**
2466
2479
  * @param {string} pmtilesUrl - URL to the PMTiles file
2467
2480
  * @param {Object} [options] - Options
2468
- * @param {number} [options.cacheSize=64] - Maximum number of tiles to cache
2481
+ * @param {number} [options.cacheMaxFeatures=10000] - Maximum number of features to cache
2469
2482
  * @param {number} [options.maxDataZoom] - Maximum zoom level in PMTiles (auto-detected if not provided)
2470
2483
  */
2471
2484
  constructor(pmtilesUrl, options = {}) {
2472
2485
  this.pmtilesUrl = pmtilesUrl;
2473
2486
  this.pmtiles = new x(pmtilesUrl);
2474
- this.cacheSize = options.cacheSize ?? DEFAULT_CACHE_SIZE;
2487
+ this.cacheMaxFeatures = options.cacheMaxFeatures;
2475
2488
  this.maxDataZoom = options.maxDataZoom;
2476
2489
  this.cache = /* @__PURE__ */ new Map();
2477
2490
  this.inflight = /* @__PURE__ */ new Map();
2491
+ this.cachedFeatureCount = 0;
2492
+ this.emptyKeys = /* @__PURE__ */ new Set();
2493
+ this.nonEmptyKeys = /* @__PURE__ */ new Set();
2478
2494
  }
2479
2495
  /**
2480
2496
  * Get the PMTiles source object.
@@ -2489,6 +2505,35 @@ var CorrectionsSource = class {
2489
2505
  clearCache() {
2490
2506
  this.cache.clear();
2491
2507
  this.inflight.clear();
2508
+ this.cachedFeatureCount = 0;
2509
+ this.emptyKeys.clear();
2510
+ this.nonEmptyKeys.clear();
2511
+ }
2512
+ /**
2513
+ * Evict cache entries to stay under the feature limit.
2514
+ * Empty entries are evicted first (cheap to re-fetch from PMTiles directory cache).
2515
+ * Within each category, evicts LRU (least recently used) entries.
2516
+ * @private
2517
+ */
2518
+ _evictIfNeeded() {
2519
+ while (this.cachedFeatureCount > this.cacheMaxFeatures && this.cache.size > 1) {
2520
+ const targetSet = this.emptyKeys.size > 0 ? this.emptyKeys : this.nonEmptyKeys;
2521
+ if (targetSet.size === 0) break;
2522
+ let evictKey = void 0;
2523
+ let minUsed = Infinity;
2524
+ for (const key of targetSet) {
2525
+ const entry = this.cache.get(key);
2526
+ if (entry && entry.used < minUsed) {
2527
+ minUsed = entry.used;
2528
+ evictKey = key;
2529
+ }
2530
+ }
2531
+ if (!evictKey) break;
2532
+ const evicted = this.cache.get(evictKey);
2533
+ this.cachedFeatureCount -= evicted.featureCount;
2534
+ targetSet.delete(evictKey);
2535
+ this.cache.delete(evictKey);
2536
+ }
2492
2537
  }
2493
2538
  /**
2494
2539
  * Auto-detect max zoom from PMTiles metadata.
@@ -2534,7 +2579,11 @@ var CorrectionsSource = class {
2534
2579
  } else {
2535
2580
  data = {};
2536
2581
  }
2537
- this.cache.set(idx, { used: performance.now(), data });
2582
+ const featureCount = countFeatures(data);
2583
+ const empty = featureCount === 0;
2584
+ this.cache.set(idx, { used: performance.now(), data, featureCount, empty });
2585
+ this.cachedFeatureCount += featureCount;
2586
+ (empty ? this.emptyKeys : this.nonEmptyKeys).add(idx);
2538
2587
  const ifentry2 = this.inflight.get(idx);
2539
2588
  if (ifentry2) {
2540
2589
  for (const waiter of ifentry2) {
@@ -2543,19 +2592,7 @@ var CorrectionsSource = class {
2543
2592
  }
2544
2593
  this.inflight.delete(idx);
2545
2594
  resolve(data);
2546
- if (this.cache.size > this.cacheSize) {
2547
- let minUsed = Infinity;
2548
- let minKey = void 0;
2549
- this.cache.forEach((value, key) => {
2550
- if (value.used < minUsed) {
2551
- minUsed = value.used;
2552
- minKey = key;
2553
- }
2554
- });
2555
- if (minKey) {
2556
- this.cache.delete(minKey);
2557
- }
2558
- }
2595
+ this._evictIfNeeded();
2559
2596
  }).catch((e) => {
2560
2597
  const ifentry2 = this.inflight.get(idx);
2561
2598
  if (ifentry2) {
@@ -2594,6 +2631,33 @@ var CorrectionsSource = class {
2594
2631
  return await this._fetchTile(z2, x22, y);
2595
2632
  }
2596
2633
  };
2634
+ var TileFetchError = class _TileFetchError extends Error {
2635
+ /**
2636
+ * @param {number} status - HTTP status code
2637
+ * @param {string} [url] - The URL that failed
2638
+ * @param {string} [body] - Response body text
2639
+ */
2640
+ constructor(status, url, body) {
2641
+ super(`Tile fetch failed: ${status}`);
2642
+ this.name = "TileFetchError";
2643
+ this.status = status;
2644
+ this.url = url;
2645
+ this.body = body;
2646
+ }
2647
+ /**
2648
+ * Create a TileFetchError from a failed Response.
2649
+ * @param {Response} response - The failed fetch response
2650
+ * @returns {Promise<TileFetchError>}
2651
+ */
2652
+ static async fromResponse(response) {
2653
+ let body;
2654
+ try {
2655
+ body = await response.text();
2656
+ } catch {
2657
+ }
2658
+ return new _TileFetchError(response.status, response.url, body);
2659
+ }
2660
+ };
2597
2661
  var MIN_LINE_WIDTH = 0.5;
2598
2662
  function getLineWidth(zoom, lineWidthStops) {
2599
2663
  const zooms = Object.keys(lineWidthStops).map(Number).sort((a, b2) => a - b2);
@@ -2811,11 +2875,34 @@ function drawFeatures(ctx, features, color, lineWidth, tileSize, dashArray, alph
2811
2875
  ctx.globalAlpha = prevAlpha;
2812
2876
  }
2813
2877
  }
2814
- var BoundaryCorrector = class {
2878
+ var _TileFixer = class _TileFixer2 {
2879
+ /**
2880
+ * Set the default maximum features to cache for new TileFixer instances.
2881
+ * @param {number} maxFeatures - Maximum features to cache
2882
+ */
2883
+ static setDefaultCacheMaxFeatures(maxFeatures) {
2884
+ _TileFixer2._defaultCacheMaxFeatures = maxFeatures;
2885
+ }
2886
+ /**
2887
+ * Get or create a TileFixer instance for a given PMTiles URL.
2888
+ * Reuses existing instances for the same URL.
2889
+ * @param {string} pmtilesUrl - URL to the PMTiles file
2890
+ * @returns {TileFixer}
2891
+ */
2892
+ static getOrCreate(pmtilesUrl) {
2893
+ let instance = _TileFixer2._instances.get(pmtilesUrl);
2894
+ if (!instance) {
2895
+ instance = new _TileFixer2(pmtilesUrl, {
2896
+ cacheMaxFeatures: _TileFixer2._defaultCacheMaxFeatures
2897
+ });
2898
+ _TileFixer2._instances.set(pmtilesUrl, instance);
2899
+ }
2900
+ return instance;
2901
+ }
2815
2902
  /**
2816
2903
  * @param {string} pmtilesUrl - URL to the PMTiles file
2817
2904
  * @param {Object} [options] - Options
2818
- * @param {number} [options.cacheSize=64] - Maximum number of tiles to cache
2905
+ * @param {number} [options.cacheMaxFeatures] - Maximum number of features to cache
2819
2906
  * @param {number} [options.maxDataZoom] - Maximum zoom level in PMTiles (auto-detected if not provided)
2820
2907
  */
2821
2908
  constructor(pmtilesUrl, options = {}) {
@@ -2852,10 +2939,9 @@ var BoundaryCorrector = class {
2852
2939
  * @param {ArrayBuffer} rasterTile - The original raster tile as ArrayBuffer
2853
2940
  * @param {Object} layerConfig - Layer configuration with colors and styles
2854
2941
  * @param {number} zoom - Current zoom level
2855
- * @param {number} [tileSize=256] - Size of the tile in pixels
2856
2942
  * @returns {Promise<ArrayBuffer>} The corrected tile as ArrayBuffer (PNG)
2857
2943
  */
2858
- async fixTile(corrections, rasterTile, layerConfig, zoom, tileSize = 256) {
2944
+ async fixTile(corrections, rasterTile, layerConfig, zoom) {
2859
2945
  const {
2860
2946
  startZoom = 0,
2861
2947
  zoomThreshold,
@@ -2879,13 +2965,14 @@ var BoundaryCorrector = class {
2879
2965
  const useOsm = zoom >= zoomThreshold;
2880
2966
  const addLayerName = useOsm ? "to-add-osm" : "to-add-ne";
2881
2967
  const delLayerName = useOsm ? "to-del-osm" : "to-del-ne";
2968
+ const blob = new Blob([rasterTile]);
2969
+ const imageBitmap = await createImageBitmap(blob);
2970
+ const tileSize = imageBitmap.width;
2882
2971
  if (!this._canvas || this._canvas.width !== tileSize) {
2883
2972
  this._canvas = new OffscreenCanvas(tileSize, tileSize);
2884
2973
  }
2885
2974
  const canvas = this._canvas;
2886
2975
  const ctx = canvas.getContext("2d", { willReadFrequently: true });
2887
- const blob = new Blob([rasterTile]);
2888
- const imageBitmap = await createImageBitmap(blob);
2889
2976
  ctx.drawImage(imageBitmap, 0, 0, tileSize, tileSize);
2890
2977
  const baseLineWidth = getLineWidth(zoom, lineWidthStops);
2891
2978
  const maxWidthFraction = activeLineStyles.length > 0 ? Math.max(...activeLineStyles.map((s) => s.widthFraction ?? 1)) : 1;
@@ -2922,24 +3009,24 @@ var BoundaryCorrector = class {
2922
3009
  * @param {number} y - Tile Y coordinate
2923
3010
  * @param {Object} layerConfig - Layer configuration with colors and styles
2924
3011
  * @param {Object} [options] - Fetch options
2925
- * @param {number} [options.tileSize=256] - Tile size in pixels
2926
3012
  * @param {AbortSignal} [options.signal] - Abort signal for fetch
2927
3013
  * @param {RequestMode} [options.mode] - Fetch mode (e.g., 'cors')
3014
+ * @param {boolean} [options.fallbackOnCorrectionFailure=true] - Return original tile if corrections fail
2928
3015
  * @returns {Promise<{data: ArrayBuffer, wasFixed: boolean}>}
2929
3016
  */
2930
3017
  async fetchAndFixTile(tileUrl, z2, x22, y, layerConfig, options = {}) {
2931
- const { tileSize = 256, signal, mode } = options;
3018
+ const { signal, mode, fallbackOnCorrectionFailure = true } = options;
2932
3019
  const fetchOptions = {};
2933
3020
  if (signal) fetchOptions.signal = signal;
2934
3021
  if (mode) fetchOptions.mode = mode;
2935
3022
  if (!layerConfig) {
2936
3023
  const response = await fetch(tileUrl, fetchOptions);
2937
- if (!response.ok) throw new Error(`Tile fetch failed: ${response.status}`);
3024
+ if (!response.ok) throw await TileFetchError.fromResponse(response);
2938
3025
  return { data: await response.arrayBuffer(), wasFixed: false };
2939
3026
  }
2940
3027
  const [tileResult, correctionsResult] = await Promise.allSettled([
2941
- fetch(tileUrl, fetchOptions).then((r) => {
2942
- if (!r.ok) throw new Error(`Tile fetch failed: ${r.status}`);
3028
+ fetch(tileUrl, fetchOptions).then(async (r) => {
3029
+ if (!r.ok) throw await TileFetchError.fromResponse(r);
2943
3030
  return r.arrayBuffer();
2944
3031
  }),
2945
3032
  this.getCorrections(z2, x22, y)
@@ -2953,15 +3040,21 @@ var BoundaryCorrector = class {
2953
3040
  const tileData = tileResult.value;
2954
3041
  const correctionsFailed = correctionsResult.status === "rejected";
2955
3042
  const correctionsError = correctionsFailed ? correctionsResult.reason : null;
3043
+ if (correctionsFailed && !fallbackOnCorrectionFailure) {
3044
+ throw correctionsError;
3045
+ }
2956
3046
  const corrections = correctionsResult.status === "fulfilled" ? correctionsResult.value : {};
2957
3047
  const hasCorrections = Object.values(corrections).some((arr) => arr && arr.length > 0);
2958
3048
  if (!hasCorrections) {
2959
3049
  return { data: tileData, wasFixed: false, correctionsFailed, correctionsError };
2960
3050
  }
2961
- const fixedData = await this.fixTile(corrections, tileData, layerConfig, z2, tileSize);
3051
+ const fixedData = await this.fixTile(corrections, tileData, layerConfig, z2);
2962
3052
  return { data: fixedData, wasFixed: true, correctionsFailed: false, correctionsError: null };
2963
3053
  }
2964
3054
  };
3055
+ __publicField(_TileFixer, "_instances", /* @__PURE__ */ new Map());
3056
+ __publicField(_TileFixer, "_defaultCacheMaxFeatures", 25e3);
3057
+ var TileFixer = _TileFixer;
2965
3058
 
2966
3059
  // src/index.js
2967
3060
  var import_data2 = require("@india-boundary-corrector/data");
@@ -2973,12 +3066,13 @@ function extendLeaflet(L2) {
2973
3066
  options: {
2974
3067
  pmtilesUrl: null,
2975
3068
  layerConfig: null,
2976
- extraLayerConfigs: null
3069
+ extraLayerConfigs: null,
3070
+ fallbackOnCorrectionFailure: true
2977
3071
  },
2978
3072
  initialize: function(url, options) {
2979
3073
  L2.TileLayer.prototype.initialize.call(this, url, options);
2980
3074
  this._pmtilesUrl = this.options.pmtilesUrl ?? (0, import_data.getPmtilesUrl)();
2981
- this._tileFixer = new BoundaryCorrector(this._pmtilesUrl);
3075
+ this._tileFixer = TileFixer.getOrCreate(this._pmtilesUrl);
2982
3076
  this._registry = layerConfigs.createMergedRegistry(this.options.extraLayerConfigs);
2983
3077
  if (typeof this.options.layerConfig === "string") {
2984
3078
  this._layerConfig = this._registry.get(this.options.layerConfig);
@@ -2991,29 +3085,6 @@ function extendLeaflet(L2) {
2991
3085
  console.warn("[L.TileLayer.IndiaBoundaryCorrected] Could not detect layer config from URL. Corrections will not be applied.");
2992
3086
  }
2993
3087
  },
2994
- /**
2995
- * Handle tile fetching and correction application logic.
2996
- * This method is extracted for testability.
2997
- * @param {string} tileUrl - URL of the raster tile
2998
- * @param {number} z - Zoom level
2999
- * @param {number} x - Tile X coordinate
3000
- * @param {number} y - Tile Y coordinate
3001
- * @param {number} tileSize - Tile size in pixels
3002
- * @returns {Promise<{blob: Blob, wasFixed: boolean, correctionsFailed: boolean, correctionsError: Error|null}>}
3003
- * @private
3004
- */
3005
- _fetchAndFixTile: async function(tileUrl, z2, x3, y, tileSize) {
3006
- const { data, wasFixed, correctionsFailed, correctionsError } = await this._tileFixer.fetchAndFixTile(
3007
- tileUrl,
3008
- z2,
3009
- x3,
3010
- y,
3011
- this._layerConfig,
3012
- { tileSize }
3013
- );
3014
- const blob = new Blob([data], { type: wasFixed ? "image/png" : void 0 });
3015
- return { blob, wasFixed, correctionsFailed, correctionsError };
3016
- },
3017
3088
  createTile: function(coords, done) {
3018
3089
  const tile = document.createElement("img");
3019
3090
  tile.alt = "";
@@ -3033,12 +3104,13 @@ function extendLeaflet(L2) {
3033
3104
  const z2 = coords.z;
3034
3105
  const x3 = coords.x;
3035
3106
  const y = coords.y;
3036
- const tileSize = this.options.tileSize || 256;
3037
- this._fetchAndFixTile(tileUrl, z2, x3, y, tileSize).then(({ blob, wasFixed, correctionsFailed, correctionsError }) => {
3107
+ const fallbackOnCorrectionFailure = this.options.fallbackOnCorrectionFailure;
3108
+ this._tileFixer.fetchAndFixTile(tileUrl, z2, x3, y, this._layerConfig, { fallbackOnCorrectionFailure }).then(({ data, correctionsFailed, correctionsError }) => {
3038
3109
  if (correctionsFailed) {
3039
3110
  console.warn("[L.TileLayer.IndiaBoundaryCorrected] Corrections fetch failed:", correctionsError);
3040
3111
  this.fire("correctionerror", { error: correctionsError, coords: { z: z2, x: x3, y }, tileUrl });
3041
3112
  }
3113
+ const blob = new Blob([data]);
3042
3114
  tile.src = URL.createObjectURL(blob);
3043
3115
  tile.onload = () => {
3044
3116
  URL.revokeObjectURL(tile.src);