@india-boundary-corrector/tilefixer 0.0.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/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@india-boundary-corrector/tilefixer",
3
+ "version": "0.0.1",
4
+ "description": "Tile fixer for India boundary corrections using PMTiles",
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.js",
8
+ "types": "src/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./src/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./src/index.d.ts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "src",
23
+ "dist"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsup"
27
+ },
28
+ "dependencies": {
29
+ "@mapbox/vector-tile": "^2.0.3",
30
+ "pbf": "^4.0.1",
31
+ "pmtiles": "^4.0.0"
32
+ },
33
+ "keywords": [
34
+ "india",
35
+ "boundary",
36
+ "map",
37
+ "pmtiles",
38
+ "tiles"
39
+ ],
40
+ "author": "ramSeraph",
41
+ "license": "Unlicense",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/ramSeraph/india_boundary_corrector.git",
45
+ "directory": "packages/tilefixer"
46
+ },
47
+ "bugs": {
48
+ "url": "https://github.com/ramSeraph/india_boundary_corrector/issues"
49
+ },
50
+ "homepage": "https://github.com/ramSeraph/india_boundary_corrector#readme"
51
+ }
@@ -0,0 +1,280 @@
1
+ /**
2
+ * PMTiles corrections fetcher with LRU caching and overzoom support
3
+ *
4
+ * This implementation is heavily inspired by and adapts code from:
5
+ * protomaps-leaflet (https://github.com/protomaps/protomaps-leaflet)
6
+ * Copyright (c) 2021 Brandon Liu
7
+ * Licensed under BSD 3-Clause License
8
+ *
9
+ * Protomaps-leaflet concepts and patterns used:
10
+ * - TileCache: In-flight request deduplication, LRU cache with performance.now()
11
+ * - PmtilesSource: PMTiles tile fetching pattern
12
+ * - View.dataTileForDisplayTile(): Overzoom coordinate calculation logic
13
+ * - toIndex(): Tile key generation (x:y:z format)
14
+ * - parseTile(): Vector tile parsing with @mapbox/vector-tile
15
+ *
16
+ * Key adaptations for India boundary corrections:
17
+ * - Modified feature format to include 'extent' field needed for coordinate scaling
18
+ * - Simplified overzoom logic (removed levelDiff complexity)
19
+ * - Configurable cache size (protomaps uses fixed 64)
20
+ * - Plain geometry objects {x, y} instead of Point class
21
+ * - transformForOverzoom() adapted from View's coordinate transform logic
22
+ *
23
+ * Thank you to the protomaps team for the excellent reference implementation!
24
+ */
25
+
26
+ import { PMTiles } from 'pmtiles';
27
+ import { VectorTile } from '@mapbox/vector-tile';
28
+ import Protobuf from 'pbf';
29
+
30
+ const DEFAULT_CACHE_SIZE = 64;
31
+
32
+ /**
33
+ * Generate cache key from tile coordinates.
34
+ * Based on protomaps-leaflet toIndex function.
35
+ * @param {number} z
36
+ * @param {number} x
37
+ * @param {number} y
38
+ * @returns {string}
39
+ */
40
+ function toIndex(z, x, y) {
41
+ return `${x}:${y}:${z}`;
42
+ }
43
+
44
+ /**
45
+ * Parse a vector tile buffer into a map of layer name to features.
46
+ * Adapted from protomaps-leaflet parseTile function.
47
+ * @param {ArrayBuffer} buffer - The raw tile data
48
+ * @returns {Object<string, Array>} Map of layer name to array of features
49
+ */
50
+ function parseTile(buffer) {
51
+ const tile = new VectorTile(new Protobuf(buffer));
52
+ const result = {};
53
+ for (const [layerName, layer] of Object.entries(tile.layers)) {
54
+ const features = [];
55
+ for (let i = 0; i < layer.length; i++) {
56
+ const feature = layer.feature(i);
57
+ features.push({
58
+ id: feature.id,
59
+ type: feature.type,
60
+ properties: feature.properties,
61
+ geometry: feature.loadGeometry(),
62
+ extent: layer.extent,
63
+ });
64
+ }
65
+ result[layerName] = features;
66
+ }
67
+ return result;
68
+ }
69
+
70
+ /**
71
+ * Transform features for overzoom by scaling and translating geometry.
72
+ * When overzooming, we take a parent tile and extract/scale the relevant quadrant.
73
+ * @param {Object<string, Array>} corrections - Original corrections
74
+ * @param {number} scale - Scale factor (2^(zoom - maxDataZoom))
75
+ * @param {number} offsetX - X offset within parent (0 to scale-1)
76
+ * @param {number} offsetY - Y offset within parent (0 to scale-1)
77
+ * @returns {Object<string, Array>} Transformed corrections
78
+ */
79
+ function transformForOverzoom(corrections, scale, offsetX, offsetY) {
80
+ const result = {};
81
+ for (const [layerName, features] of Object.entries(corrections)) {
82
+ result[layerName] = features.map(feature => {
83
+ const extent = feature.extent;
84
+ // Each child tile covers (extent/scale) units of the parent
85
+ const childExtent = extent / scale;
86
+ // The child tile starts at this position in parent coordinates
87
+ const startX = offsetX * childExtent;
88
+ const startY = offsetY * childExtent;
89
+
90
+ const newGeometry = feature.geometry.map(ring => {
91
+ return ring.map(point => {
92
+ // Translate to child tile origin, then scale up to full extent
93
+ const x = (point.x - startX) * scale;
94
+ const y = (point.y - startY) * scale;
95
+ return { x, y };
96
+ });
97
+ });
98
+ return {
99
+ ...feature,
100
+ geometry: newGeometry,
101
+ // Keep original extent since we scaled coordinates to match
102
+ extent: extent,
103
+ };
104
+ });
105
+ }
106
+ return result;
107
+ }
108
+
109
+ /**
110
+ * PMTiles corrections source with LRU caching and overzoom support.
111
+ * Based on protomaps-leaflet TileCache and PmtilesSource.
112
+ */
113
+ export class CorrectionsSource {
114
+ /**
115
+ * @param {string} pmtilesUrl - URL to the PMTiles file
116
+ * @param {Object} [options] - Options
117
+ * @param {number} [options.cacheSize=64] - Maximum number of tiles to cache
118
+ * @param {number} [options.maxDataZoom] - Maximum zoom level in PMTiles (auto-detected if not provided)
119
+ */
120
+ constructor(pmtilesUrl, options = {}) {
121
+ this.pmtilesUrl = pmtilesUrl;
122
+ this.pmtiles = new PMTiles(pmtilesUrl);
123
+ this.cacheSize = options.cacheSize ?? DEFAULT_CACHE_SIZE;
124
+ this.maxDataZoom = options.maxDataZoom;
125
+
126
+ // Cache based on protomaps-leaflet TileCache pattern
127
+ this.cache = new Map(); // Maps toIndex(z,x,y) -> {used: timestamp, data: corrections}
128
+ this.inflight = new Map(); // Maps toIndex(z,x,y) -> [{resolve, reject}]
129
+ }
130
+
131
+ /**
132
+ * Get the PMTiles source object.
133
+ * @returns {PMTiles}
134
+ */
135
+ getSource() {
136
+ return this.pmtiles;
137
+ }
138
+
139
+ /**
140
+ * Clear the tile cache.
141
+ */
142
+ clearCache() {
143
+ this.cache.clear();
144
+ this.inflight.clear();
145
+ }
146
+
147
+ /**
148
+ * Auto-detect max zoom from PMTiles metadata.
149
+ * @returns {Promise<number>}
150
+ * @private
151
+ */
152
+ async _getMaxDataZoom() {
153
+ if (this.maxDataZoom !== undefined) {
154
+ return this.maxDataZoom;
155
+ }
156
+
157
+ const header = await this.pmtiles.getHeader();
158
+ this.maxDataZoom = header.maxZoom;
159
+ return this.maxDataZoom;
160
+ }
161
+
162
+ /**
163
+ * Fetch and parse a tile from PMTiles.
164
+ * Implements in-flight request deduplication from protomaps-leaflet.
165
+ * @param {number} z
166
+ * @param {number} x
167
+ * @param {number} y
168
+ * @returns {Promise<Object<string, Array>>}
169
+ * @private
170
+ */
171
+ async _fetchTile(z, x, y) {
172
+ const idx = toIndex(z, x, y);
173
+
174
+ return new Promise((resolve, reject) => {
175
+ // Check cache first
176
+ const entry = this.cache.get(idx);
177
+ if (entry) {
178
+ // Update LRU timestamp (protomaps pattern)
179
+ entry.used = performance.now();
180
+ resolve(entry.data);
181
+ return;
182
+ }
183
+
184
+ // Check if already in-flight
185
+ const ifentry = this.inflight.get(idx);
186
+ if (ifentry) {
187
+ // Add to waiting list
188
+ ifentry.push({ resolve, reject });
189
+ return;
190
+ }
191
+
192
+ // Start new fetch
193
+ this.inflight.set(idx, []);
194
+
195
+ this.pmtiles.getZxy(z, x, y)
196
+ .then((result) => {
197
+ let data;
198
+ if (result) {
199
+ data = parseTile(result.data);
200
+ } else {
201
+ // Cache empty result to avoid repeated fetches
202
+ data = {};
203
+ }
204
+
205
+ // Cache the result
206
+ this.cache.set(idx, { used: performance.now(), data });
207
+
208
+ // Resolve all waiting promises
209
+ const ifentry2 = this.inflight.get(idx);
210
+ if (ifentry2) {
211
+ for (const waiter of ifentry2) {
212
+ waiter.resolve(data);
213
+ }
214
+ }
215
+ this.inflight.delete(idx);
216
+ resolve(data);
217
+
218
+ // Evict LRU entry if cache is full (protomaps pattern)
219
+ if (this.cache.size > this.cacheSize) {
220
+ let minUsed = Infinity;
221
+ let minKey = undefined;
222
+ this.cache.forEach((value, key) => {
223
+ if (value.used < minUsed) {
224
+ minUsed = value.used;
225
+ minKey = key;
226
+ }
227
+ });
228
+ if (minKey) {
229
+ this.cache.delete(minKey);
230
+ }
231
+ }
232
+ })
233
+ .catch((e) => {
234
+ // Reject all waiting promises
235
+ const ifentry2 = this.inflight.get(idx);
236
+ if (ifentry2) {
237
+ for (const waiter of ifentry2) {
238
+ waiter.reject(e);
239
+ }
240
+ }
241
+ this.inflight.delete(idx);
242
+ reject(e);
243
+ });
244
+ });
245
+ }
246
+
247
+ /**
248
+ * Get corrections for a tile as a dict of layer name to features.
249
+ * Supports overzoom beyond maxDataZoom by scaling parent tile data.
250
+ * @param {number} z - Zoom level
251
+ * @param {number} x - Tile X coordinate
252
+ * @param {number} y - Tile Y coordinate
253
+ * @returns {Promise<Object<string, Array>>} Map of layer name to array of features
254
+ */
255
+ async get(z, x, y) {
256
+ const maxDataZoom = await this._getMaxDataZoom();
257
+
258
+ // Handle overzoom: fetch parent tile and transform
259
+ if (z > maxDataZoom) {
260
+ const zoomDiff = z - maxDataZoom;
261
+ const scale = 1 << zoomDiff; // 2^zoomDiff
262
+
263
+ // Calculate parent tile coordinates
264
+ const parentX = Math.floor(x / scale);
265
+ const parentY = Math.floor(y / scale);
266
+
267
+ // Calculate offset within parent tile (0 to scale-1)
268
+ const offsetX = x % scale;
269
+ const offsetY = y % scale;
270
+
271
+ const corrections = await this._fetchTile(maxDataZoom, parentX, parentY);
272
+ if (Object.keys(corrections).length > 0) {
273
+ return transformForOverzoom(corrections, scale, offsetX, offsetY);
274
+ }
275
+ return {};
276
+ }
277
+
278
+ return await this._fetchTile(z, x, y);
279
+ }
280
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,154 @@
1
+ import { PMTiles } from 'pmtiles';
2
+
3
+ /**
4
+ * Minimum line width used when extrapolating below the lowest zoom stop.
5
+ */
6
+ export const MIN_LINE_WIDTH: number;
7
+
8
+ /**
9
+ * Interpolate or extrapolate line width from a zoom-to-width map.
10
+ * @param zoom - Zoom level
11
+ * @param lineWidthStops - Map of zoom level to line width (at least 2 entries)
12
+ * @returns Interpolated/extrapolated line width (minimum 0.5)
13
+ */
14
+ export function getLineWidth(zoom: number, lineWidthStops: Record<number, number>): number;
15
+
16
+ /**
17
+ * A parsed vector tile feature.
18
+ */
19
+ export interface Feature {
20
+ id: number | undefined;
21
+ type: number;
22
+ properties: Record<string, unknown>;
23
+ geometry: Array<Array<{ x: number; y: number }>>;
24
+ extent: number;
25
+ }
26
+
27
+ /**
28
+ * Map of layer name to array of features.
29
+ */
30
+ export type CorrectionResult = Record<string, Feature[]>;
31
+
32
+ /**
33
+ * Line style definition for drawing boundary lines
34
+ */
35
+ export interface LineStyle {
36
+ /** Line color (CSS color string) */
37
+ color: string;
38
+ /** Width as fraction of base line width (default: 1.0) */
39
+ widthFraction?: number;
40
+ /** Dash pattern array (omit for solid line) */
41
+ dashArray?: number[];
42
+ /** Opacity/alpha value from 0 (transparent) to 1 (opaque) (default: 1.0) */
43
+ alpha?: number;
44
+ }
45
+
46
+ /**
47
+ * Layer configuration for styling corrections.
48
+ */
49
+ export interface LayerConfig {
50
+ startZoom?: number;
51
+ zoomThreshold: number;
52
+ lineWidthStops: Record<number, number>;
53
+ lineStyles: LineStyle[];
54
+ delWidthFactor?: number;
55
+ }
56
+
57
+ /**
58
+ * Options for BoundaryCorrector constructor.
59
+ */
60
+ export interface BoundaryCorrectorOptions {
61
+ /** Maximum number of tiles to cache (default: 64) */
62
+ cacheSize?: number;
63
+ /** Maximum zoom level in PMTiles (auto-detected if not provided) */
64
+ maxDataZoom?: number;
65
+ }
66
+
67
+ /**
68
+ * Boundary corrector that fetches correction data and applies it to raster tiles.
69
+ */
70
+ export declare class BoundaryCorrector {
71
+ /**
72
+ * Create a new BoundaryCorrector.
73
+ * @param pmtilesUrl - URL to the PMTiles file
74
+ * @param options - Options
75
+ */
76
+ constructor(pmtilesUrl: string, options?: BoundaryCorrectorOptions);
77
+
78
+ /**
79
+ * Get the PMTiles source object.
80
+ */
81
+ getSource(): PMTiles;
82
+
83
+ /**
84
+ * Clear the tile cache.
85
+ */
86
+ clearCache(): void;
87
+
88
+ /**
89
+ * Get corrections for a tile as a dict of layer name to features.
90
+ * Supports overzoom beyond maxDataZoom (14) by scaling parent tile data.
91
+ * @param z - Zoom level
92
+ * @param x - Tile X coordinate
93
+ * @param y - Tile Y coordinate
94
+ */
95
+ getCorrections(z: number, x: number, y: number): Promise<CorrectionResult>;
96
+
97
+ /**
98
+ * Apply corrections to a raster tile.
99
+ * @param corrections - Feature map from getCorrections
100
+ * @param rasterTile - The original raster tile as ArrayBuffer
101
+ * @param layerConfig - Layer configuration with colors and styles
102
+ * @param zoom - Current zoom level
103
+ * @param tileSize - Size of the tile in pixels (default: 256)
104
+ * @returns The corrected tile as ArrayBuffer (PNG)
105
+ */
106
+ fixTile(
107
+ corrections: CorrectionResult,
108
+ rasterTile: ArrayBuffer,
109
+ layerConfig: LayerConfig,
110
+ zoom: number,
111
+ tileSize?: number
112
+ ): Promise<ArrayBuffer>;
113
+
114
+ /**
115
+ * Fetch a tile, apply corrections, and return the result.
116
+ * @param tileUrl - URL of the raster tile
117
+ * @param z - Zoom level
118
+ * @param x - Tile X coordinate
119
+ * @param y - Tile Y coordinate
120
+ * @param layerConfig - Layer configuration with colors and styles
121
+ * @param options - Fetch options
122
+ * @returns The tile data and whether corrections were applied
123
+ */
124
+ fetchAndFixTile(
125
+ tileUrl: string,
126
+ z: number,
127
+ x: number,
128
+ y: number,
129
+ layerConfig: LayerConfig,
130
+ options?: FetchAndFixTileOptions
131
+ ): Promise<FetchAndFixTileResult>;
132
+ }
133
+
134
+ /**
135
+ * Options for fetchAndFixTile method.
136
+ */
137
+ export interface FetchAndFixTileOptions {
138
+ /** Tile size in pixels (default: 256) */
139
+ tileSize?: number;
140
+ /** Abort signal for fetch */
141
+ signal?: AbortSignal;
142
+ /** Fetch mode (e.g., 'cors') */
143
+ mode?: RequestMode;
144
+ }
145
+
146
+ /**
147
+ * Result of fetchAndFixTile method.
148
+ */
149
+ export interface FetchAndFixTileResult {
150
+ /** The tile data as ArrayBuffer */
151
+ data: ArrayBuffer;
152
+ /** Whether corrections were applied */
153
+ wasFixed: boolean;
154
+ }