@india-boundary-corrector/tilefixer 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
@@ -13,12 +13,12 @@ npm install @india-boundary-corrector/tilefixer
13
13
  ## Usage
14
14
 
15
15
  ```javascript
16
- import { BoundaryCorrector } from '@india-boundary-corrector/tilefixer';
16
+ import { TileFixer } from '@india-boundary-corrector/tilefixer';
17
17
  import { getPmtilesUrl } from '@india-boundary-corrector/data';
18
18
  import { layerConfigs } from '@india-boundary-corrector/layer-configs';
19
19
 
20
20
  // Create a boundary corrector
21
- const corrector = new BoundaryCorrector(getPmtilesUrl());
21
+ const corrector = new TileFixer(getPmtilesUrl());
22
22
 
23
23
  // Get corrections for a tile
24
24
  const corrections = await corrector.getCorrections(z, x, y);
@@ -30,32 +30,38 @@ const fixedTileData = await corrector.fixTile(
30
30
  corrections,
31
31
  originalTileArrayBuffer,
32
32
  layerConfig,
33
- z, // zoom level
34
- 256 // tile size (optional, defaults to 256)
33
+ z // zoom level (tile size is derived from image)
35
34
  );
36
35
  ```
37
36
 
38
37
  ## API
39
38
 
40
- ### `BoundaryCorrector`
39
+ ### `TileFixer`
40
+
41
+ #### Static Methods
42
+
43
+ | Method | Returns | Description |
44
+ |--------|---------|-------------|
45
+ | `TileFixer.getOrCreate(pmtilesUrl)` | `TileFixer` | Get or create a TileFixer for a URL (reuses existing instances) |
46
+ | `TileFixer.setDefaultCacheMaxFeatures(count)` | `void` | Set default max features to cache for new instances (default: 25000) |
41
47
 
42
48
  #### Constructor
43
49
 
44
50
  ```javascript
45
- new BoundaryCorrector(pmtilesUrl, options?)
51
+ new TileFixer(pmtilesUrl, options?)
46
52
  ```
47
53
 
48
54
  | Parameter | Type | Description |
49
55
  |-----------|------|-------------|
50
56
  | `pmtilesUrl` | string | URL to the PMTiles file |
51
- | `options.cacheSize` | number | Maximum tiles to cache (default: 64) |
57
+ | `options.cacheMaxFeatures` | number | Maximum features to cache |
52
58
 
53
59
  #### Methods
54
60
 
55
61
  | Method | Returns | Description |
56
62
  |--------|---------|-------------|
57
63
  | `getCorrections(z, x, y)` | `Promise<Object>` | Get correction features for a tile. Supports overzoom beyond zoom 14. |
58
- | `fixTile(corrections, rasterTile, layerConfig, zoom, tileSize?)` | `Promise<ArrayBuffer>` | Apply corrections to a raster tile and return corrected PNG. |
64
+ | `fixTile(corrections, rasterTile, layerConfig, zoom)` | `Promise<ArrayBuffer>` | Apply corrections to a raster tile and return corrected PNG. Tile size is derived from the image. |
59
65
  | `fetchAndFixTile(tileUrl, z, x, y, layerConfig, options?)` | `Promise<Object>` | Fetch a tile, apply corrections, and return result. See below. |
60
66
  | `getSource()` | `PMTiles` | Get the underlying PMTiles source object |
61
67
  | `clearCache()` | `void` | Clear the tile cache |
@@ -64,7 +70,6 @@ new BoundaryCorrector(pmtilesUrl, options?)
64
70
 
65
71
  ```javascript
66
72
  const result = await corrector.fetchAndFixTile(tileUrl, z, x, y, layerConfig, {
67
- tileSize: 256, // Tile size in pixels (default: 256)
68
73
  signal: abortSignal, // AbortSignal for cancellation
69
74
  mode: 'cors', // Fetch mode
70
75
  });
package/dist/index.cjs CHANGED
@@ -3,6 +3,7 @@ var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
6
7
  var __export = (target, all) => {
7
8
  for (var name in all)
8
9
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -16,12 +17,14 @@ var __copyProps = (to, from, except, desc) => {
16
17
  return to;
17
18
  };
18
19
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
19
21
 
20
22
  // src/index.js
21
23
  var index_exports = {};
22
24
  __export(index_exports, {
23
- BoundaryCorrector: () => BoundaryCorrector,
24
25
  MIN_LINE_WIDTH: () => MIN_LINE_WIDTH,
26
+ TileFetchError: () => TileFetchError,
27
+ TileFixer: () => TileFixer,
25
28
  getLineWidth: () => getLineWidth
26
29
  });
27
30
  module.exports = __toCommonJS(index_exports);
@@ -2052,10 +2055,16 @@ function writeUtf8(buf, str, pos) {
2052
2055
  }
2053
2056
 
2054
2057
  // src/corrections.js
2055
- var DEFAULT_CACHE_SIZE = 64;
2056
2058
  function toIndex(z2, x2, y) {
2057
2059
  return `${x2}:${y}:${z2}`;
2058
2060
  }
2061
+ function countFeatures(corrections) {
2062
+ let count = 0;
2063
+ for (const features of Object.values(corrections)) {
2064
+ count += features.length;
2065
+ }
2066
+ return count;
2067
+ }
2059
2068
  function parseTile(buffer) {
2060
2069
  const tile = new VectorTile(new Pbf(buffer));
2061
2070
  const result = {};
@@ -2104,16 +2113,19 @@ var CorrectionsSource = class {
2104
2113
  /**
2105
2114
  * @param {string} pmtilesUrl - URL to the PMTiles file
2106
2115
  * @param {Object} [options] - Options
2107
- * @param {number} [options.cacheSize=64] - Maximum number of tiles to cache
2116
+ * @param {number} [options.cacheMaxFeatures=10000] - Maximum number of features to cache
2108
2117
  * @param {number} [options.maxDataZoom] - Maximum zoom level in PMTiles (auto-detected if not provided)
2109
2118
  */
2110
2119
  constructor(pmtilesUrl, options = {}) {
2111
2120
  this.pmtilesUrl = pmtilesUrl;
2112
2121
  this.pmtiles = new x(pmtilesUrl);
2113
- this.cacheSize = options.cacheSize ?? DEFAULT_CACHE_SIZE;
2122
+ this.cacheMaxFeatures = options.cacheMaxFeatures;
2114
2123
  this.maxDataZoom = options.maxDataZoom;
2115
2124
  this.cache = /* @__PURE__ */ new Map();
2116
2125
  this.inflight = /* @__PURE__ */ new Map();
2126
+ this.cachedFeatureCount = 0;
2127
+ this.emptyKeys = /* @__PURE__ */ new Set();
2128
+ this.nonEmptyKeys = /* @__PURE__ */ new Set();
2117
2129
  }
2118
2130
  /**
2119
2131
  * Get the PMTiles source object.
@@ -2128,6 +2140,35 @@ var CorrectionsSource = class {
2128
2140
  clearCache() {
2129
2141
  this.cache.clear();
2130
2142
  this.inflight.clear();
2143
+ this.cachedFeatureCount = 0;
2144
+ this.emptyKeys.clear();
2145
+ this.nonEmptyKeys.clear();
2146
+ }
2147
+ /**
2148
+ * Evict cache entries to stay under the feature limit.
2149
+ * Empty entries are evicted first (cheap to re-fetch from PMTiles directory cache).
2150
+ * Within each category, evicts LRU (least recently used) entries.
2151
+ * @private
2152
+ */
2153
+ _evictIfNeeded() {
2154
+ while (this.cachedFeatureCount > this.cacheMaxFeatures && this.cache.size > 1) {
2155
+ const targetSet = this.emptyKeys.size > 0 ? this.emptyKeys : this.nonEmptyKeys;
2156
+ if (targetSet.size === 0) break;
2157
+ let evictKey = void 0;
2158
+ let minUsed = Infinity;
2159
+ for (const key of targetSet) {
2160
+ const entry = this.cache.get(key);
2161
+ if (entry && entry.used < minUsed) {
2162
+ minUsed = entry.used;
2163
+ evictKey = key;
2164
+ }
2165
+ }
2166
+ if (!evictKey) break;
2167
+ const evicted = this.cache.get(evictKey);
2168
+ this.cachedFeatureCount -= evicted.featureCount;
2169
+ targetSet.delete(evictKey);
2170
+ this.cache.delete(evictKey);
2171
+ }
2131
2172
  }
2132
2173
  /**
2133
2174
  * Auto-detect max zoom from PMTiles metadata.
@@ -2173,7 +2214,11 @@ var CorrectionsSource = class {
2173
2214
  } else {
2174
2215
  data = {};
2175
2216
  }
2176
- this.cache.set(idx, { used: performance.now(), data });
2217
+ const featureCount = countFeatures(data);
2218
+ const empty = featureCount === 0;
2219
+ this.cache.set(idx, { used: performance.now(), data, featureCount, empty });
2220
+ this.cachedFeatureCount += featureCount;
2221
+ (empty ? this.emptyKeys : this.nonEmptyKeys).add(idx);
2177
2222
  const ifentry2 = this.inflight.get(idx);
2178
2223
  if (ifentry2) {
2179
2224
  for (const waiter of ifentry2) {
@@ -2182,19 +2227,7 @@ var CorrectionsSource = class {
2182
2227
  }
2183
2228
  this.inflight.delete(idx);
2184
2229
  resolve(data);
2185
- if (this.cache.size > this.cacheSize) {
2186
- let minUsed = Infinity;
2187
- let minKey = void 0;
2188
- this.cache.forEach((value, key) => {
2189
- if (value.used < minUsed) {
2190
- minUsed = value.used;
2191
- minKey = key;
2192
- }
2193
- });
2194
- if (minKey) {
2195
- this.cache.delete(minKey);
2196
- }
2197
- }
2230
+ this._evictIfNeeded();
2198
2231
  }).catch((e) => {
2199
2232
  const ifentry2 = this.inflight.get(idx);
2200
2233
  if (ifentry2) {
@@ -2235,6 +2268,33 @@ var CorrectionsSource = class {
2235
2268
  };
2236
2269
 
2237
2270
  // src/index.js
2271
+ var TileFetchError = class _TileFetchError extends Error {
2272
+ /**
2273
+ * @param {number} status - HTTP status code
2274
+ * @param {string} [url] - The URL that failed
2275
+ * @param {string} [body] - Response body text
2276
+ */
2277
+ constructor(status, url, body) {
2278
+ super(`Tile fetch failed: ${status}`);
2279
+ this.name = "TileFetchError";
2280
+ this.status = status;
2281
+ this.url = url;
2282
+ this.body = body;
2283
+ }
2284
+ /**
2285
+ * Create a TileFetchError from a failed Response.
2286
+ * @param {Response} response - The failed fetch response
2287
+ * @returns {Promise<TileFetchError>}
2288
+ */
2289
+ static async fromResponse(response) {
2290
+ let body;
2291
+ try {
2292
+ body = await response.text();
2293
+ } catch {
2294
+ }
2295
+ return new _TileFetchError(response.status, response.url, body);
2296
+ }
2297
+ };
2238
2298
  var MIN_LINE_WIDTH = 0.5;
2239
2299
  function getLineWidth(zoom, lineWidthStops) {
2240
2300
  const zooms = Object.keys(lineWidthStops).map(Number).sort((a, b2) => a - b2);
@@ -2452,11 +2512,34 @@ function drawFeatures(ctx, features, color, lineWidth, tileSize, dashArray, alph
2452
2512
  ctx.globalAlpha = prevAlpha;
2453
2513
  }
2454
2514
  }
2455
- var BoundaryCorrector = class {
2515
+ var _TileFixer = class _TileFixer {
2516
+ /**
2517
+ * Set the default maximum features to cache for new TileFixer instances.
2518
+ * @param {number} maxFeatures - Maximum features to cache
2519
+ */
2520
+ static setDefaultCacheMaxFeatures(maxFeatures) {
2521
+ _TileFixer._defaultCacheMaxFeatures = maxFeatures;
2522
+ }
2523
+ /**
2524
+ * Get or create a TileFixer instance for a given PMTiles URL.
2525
+ * Reuses existing instances for the same URL.
2526
+ * @param {string} pmtilesUrl - URL to the PMTiles file
2527
+ * @returns {TileFixer}
2528
+ */
2529
+ static getOrCreate(pmtilesUrl) {
2530
+ let instance = _TileFixer._instances.get(pmtilesUrl);
2531
+ if (!instance) {
2532
+ instance = new _TileFixer(pmtilesUrl, {
2533
+ cacheMaxFeatures: _TileFixer._defaultCacheMaxFeatures
2534
+ });
2535
+ _TileFixer._instances.set(pmtilesUrl, instance);
2536
+ }
2537
+ return instance;
2538
+ }
2456
2539
  /**
2457
2540
  * @param {string} pmtilesUrl - URL to the PMTiles file
2458
2541
  * @param {Object} [options] - Options
2459
- * @param {number} [options.cacheSize=64] - Maximum number of tiles to cache
2542
+ * @param {number} [options.cacheMaxFeatures] - Maximum number of features to cache
2460
2543
  * @param {number} [options.maxDataZoom] - Maximum zoom level in PMTiles (auto-detected if not provided)
2461
2544
  */
2462
2545
  constructor(pmtilesUrl, options = {}) {
@@ -2493,10 +2576,9 @@ var BoundaryCorrector = class {
2493
2576
  * @param {ArrayBuffer} rasterTile - The original raster tile as ArrayBuffer
2494
2577
  * @param {Object} layerConfig - Layer configuration with colors and styles
2495
2578
  * @param {number} zoom - Current zoom level
2496
- * @param {number} [tileSize=256] - Size of the tile in pixels
2497
2579
  * @returns {Promise<ArrayBuffer>} The corrected tile as ArrayBuffer (PNG)
2498
2580
  */
2499
- async fixTile(corrections, rasterTile, layerConfig, zoom, tileSize = 256) {
2581
+ async fixTile(corrections, rasterTile, layerConfig, zoom) {
2500
2582
  const {
2501
2583
  startZoom = 0,
2502
2584
  zoomThreshold,
@@ -2520,13 +2602,14 @@ var BoundaryCorrector = class {
2520
2602
  const useOsm = zoom >= zoomThreshold;
2521
2603
  const addLayerName = useOsm ? "to-add-osm" : "to-add-ne";
2522
2604
  const delLayerName = useOsm ? "to-del-osm" : "to-del-ne";
2605
+ const blob = new Blob([rasterTile]);
2606
+ const imageBitmap = await createImageBitmap(blob);
2607
+ const tileSize = imageBitmap.width;
2523
2608
  if (!this._canvas || this._canvas.width !== tileSize) {
2524
2609
  this._canvas = new OffscreenCanvas(tileSize, tileSize);
2525
2610
  }
2526
2611
  const canvas = this._canvas;
2527
2612
  const ctx = canvas.getContext("2d", { willReadFrequently: true });
2528
- const blob = new Blob([rasterTile]);
2529
- const imageBitmap = await createImageBitmap(blob);
2530
2613
  ctx.drawImage(imageBitmap, 0, 0, tileSize, tileSize);
2531
2614
  const baseLineWidth = getLineWidth(zoom, lineWidthStops);
2532
2615
  const maxWidthFraction = activeLineStyles.length > 0 ? Math.max(...activeLineStyles.map((s) => s.widthFraction ?? 1)) : 1;
@@ -2563,24 +2646,24 @@ var BoundaryCorrector = class {
2563
2646
  * @param {number} y - Tile Y coordinate
2564
2647
  * @param {Object} layerConfig - Layer configuration with colors and styles
2565
2648
  * @param {Object} [options] - Fetch options
2566
- * @param {number} [options.tileSize=256] - Tile size in pixels
2567
2649
  * @param {AbortSignal} [options.signal] - Abort signal for fetch
2568
2650
  * @param {RequestMode} [options.mode] - Fetch mode (e.g., 'cors')
2651
+ * @param {boolean} [options.fallbackOnCorrectionFailure=true] - Return original tile if corrections fail
2569
2652
  * @returns {Promise<{data: ArrayBuffer, wasFixed: boolean}>}
2570
2653
  */
2571
2654
  async fetchAndFixTile(tileUrl, z2, x2, y, layerConfig, options = {}) {
2572
- const { tileSize = 256, signal, mode } = options;
2655
+ const { signal, mode, fallbackOnCorrectionFailure = true } = options;
2573
2656
  const fetchOptions = {};
2574
2657
  if (signal) fetchOptions.signal = signal;
2575
2658
  if (mode) fetchOptions.mode = mode;
2576
2659
  if (!layerConfig) {
2577
2660
  const response = await fetch(tileUrl, fetchOptions);
2578
- if (!response.ok) throw new Error(`Tile fetch failed: ${response.status}`);
2661
+ if (!response.ok) throw await TileFetchError.fromResponse(response);
2579
2662
  return { data: await response.arrayBuffer(), wasFixed: false };
2580
2663
  }
2581
2664
  const [tileResult, correctionsResult] = await Promise.allSettled([
2582
- fetch(tileUrl, fetchOptions).then((r) => {
2583
- if (!r.ok) throw new Error(`Tile fetch failed: ${r.status}`);
2665
+ fetch(tileUrl, fetchOptions).then(async (r) => {
2666
+ if (!r.ok) throw await TileFetchError.fromResponse(r);
2584
2667
  return r.arrayBuffer();
2585
2668
  }),
2586
2669
  this.getCorrections(z2, x2, y)
@@ -2594,13 +2677,21 @@ var BoundaryCorrector = class {
2594
2677
  const tileData = tileResult.value;
2595
2678
  const correctionsFailed = correctionsResult.status === "rejected";
2596
2679
  const correctionsError = correctionsFailed ? correctionsResult.reason : null;
2680
+ if (correctionsFailed && !fallbackOnCorrectionFailure) {
2681
+ throw correctionsError;
2682
+ }
2597
2683
  const corrections = correctionsResult.status === "fulfilled" ? correctionsResult.value : {};
2598
2684
  const hasCorrections = Object.values(corrections).some((arr) => arr && arr.length > 0);
2599
2685
  if (!hasCorrections) {
2600
2686
  return { data: tileData, wasFixed: false, correctionsFailed, correctionsError };
2601
2687
  }
2602
- const fixedData = await this.fixTile(corrections, tileData, layerConfig, z2, tileSize);
2688
+ const fixedData = await this.fixTile(corrections, tileData, layerConfig, z2);
2603
2689
  return { data: fixedData, wasFixed: true, correctionsFailed: false, correctionsError: null };
2604
2690
  }
2605
2691
  };
2692
+ /** @type {Map<string, TileFixer>} */
2693
+ __publicField(_TileFixer, "_instances", /* @__PURE__ */ new Map());
2694
+ /** @type {number} */
2695
+ __publicField(_TileFixer, "_defaultCacheMaxFeatures", 25e3);
2696
+ var TileFixer = _TileFixer;
2606
2697
  //# sourceMappingURL=index.cjs.map