maplibre-tilepackage-protocol 1.1.0 → 1.2.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.
@@ -80,6 +80,7 @@ export default class SharedPromiseCache {
80
80
  this.maxCacheEntries = maxCacheEntries;
81
81
  this.counter = 1;
82
82
  //this.decompress = decompress;
83
+ this.subdivided = new Map(); // key: sourceKey|z|x|y -> Uint8Array
83
84
  }
84
85
 
85
86
  async getHeader(source) {
@@ -174,6 +175,28 @@ export default class SharedPromiseCache {
174
175
  }
175
176
  }
176
177
 
178
+ getSubdivided(source, z, x, y) {
179
+ const key = `${source.getKey()}|${z}|${x}|${y}|Subdivided`;
180
+ return this.subdivided.get(key);
181
+ }
182
+
183
+ setSubdivided(source, z, x, y, bytes) {
184
+ const key = `${source.getKey()}|${z}|${x}|${y}|Subdivided`;
185
+ if (!this.subdivided.has(key)) {
186
+ this.subdivided.set(key, bytes);
187
+ // Optional lightweight prune if huge:
188
+ if (this.subdivided.size > this.maxCacheEntries * 2) {
189
+ // Remove oldest half (no usage tracking kept; iterate arbitrary)
190
+ let toRemove = Math.floor(this.subdivided.size / 2);
191
+ for (const k of this.subdivided.keys()) {
192
+ this.subdivided.delete(k);
193
+ toRemove--;
194
+ if (toRemove <= 0) break;
195
+ }
196
+ }
197
+ }
198
+ }
199
+
177
200
  async invalidate(source) {
178
201
  const key = source.getKey();
179
202
  if (this.invalidations.get(key)) {
package/src/source.js CHANGED
@@ -18,9 +18,12 @@ export class Source {
18
18
  * see https://developer.mozilla.org/en-US/docs/Web/API/File_API
19
19
  */
20
20
  export class FileSource extends Source {
21
- constructor(file) {
21
+ constructor(file, options) {
22
22
  super();
23
23
  this.file = file;
24
+ if (options && options.coverageCheck) {
25
+ this.coverageCheck = options.coverageCheck;
26
+ }
24
27
  }
25
28
 
26
29
  getKey() {
@@ -48,14 +51,23 @@ export class FileSource extends Source {
48
51
  * This also works around browser and storage-specific edge cases.
49
52
  */
50
53
  export class FetchSource extends Source {
51
- constructor(url, customHeaders = new Headers()) {
54
+ constructor(url, options, customHeaders = new Headers()) {
52
55
  super();
53
56
  this.url = url;
54
57
  this.customHeaders = customHeaders;
55
58
  this.mustReload = false;
59
+ // Safe global detection for legacy environments without globalThis
60
+ const g =
61
+ typeof globalThis !== "undefined"
62
+ ? globalThis
63
+ : typeof window !== "undefined"
64
+ ? window
65
+ : typeof self !== "undefined"
66
+ ? self
67
+ : {};
56
68
  let userAgent = "";
57
- if ("navigator" in globalThis) {
58
- userAgent = globalThis.navigator.userAgent || "";
69
+ if (g && typeof g.navigator !== "undefined") {
70
+ userAgent = g.navigator.userAgent || "";
59
71
  }
60
72
  const isWindows = userAgent.indexOf("Windows") > -1;
61
73
  const isChromiumBased = /Chrome|Chromium|Edg|OPR|Brave/.test(userAgent);
@@ -63,6 +75,9 @@ export class FetchSource extends Source {
63
75
  if (isWindows && isChromiumBased) {
64
76
  this.chromeWindowsNoCache = true;
65
77
  }
78
+ if (options && options.coverageCheck) {
79
+ this.coverageCheck = options.coverageCheck;
80
+ }
66
81
  }
67
82
 
68
83
  getKey() {
@@ -0,0 +1,135 @@
1
+ # Vector Tile Subdivision (Tilecutter) – Current Status & Future Work
2
+
3
+ ## Implemented (Current State)
4
+
5
+ The following pieces are complete and in production code:
6
+
7
+ - `subdivideVectorTile(...)` Phase A implementation using existing decode (`@mapbox/vector-tile`) and encode (`@maplibre/vt-pbf`) libraries.
8
+ - Scale/offset math for arbitrary `dz = targetZ - parentZ` with containment validation.
9
+ - Geometry transformation and clipping (buffer default 128) for points, lines, polygons.
10
+ - Layer filtering via `includeLayers` (array or RegExp).
11
+ - Stats returned (`featuresIn`, `featuresOut`, `dz`, `ms`).
12
+ - Automatic parent selection via `coverageMap` (value=1) and subdivision fallback in `TilePackage.getZxyAttempt`.
13
+ - In‑memory cache of synthesized tiles (`SharedPromiseCache.subdivided`) with simple half-prune heuristic.
14
+ - Default `coverageCheck` enabled and propagated to both `FetchSource` and `FileSource`.
15
+ - Debug instrumentation removed (warnings only for failures & high dz).
16
+
17
+ ## Deferred / Not Yet Implemented
18
+
19
+ The items below represent remaining or future enhancements. They supersede earlier phased plan language.
20
+
21
+ 1. Unit Tests
22
+
23
+ - Synthetic fixtures for point, line, polygon, multi-dz (e.g. dz=1,2,3).
24
+ - Verify clipping correctness (edge touch, full outside, polygon ring closure).
25
+ - Regression tests for parent containment validation and empty output tiles.
26
+
27
+ 2. Benchmarks
28
+
29
+ - Measure average subdivision latency for typical dz values (1–5) and publish a README table.
30
+ - Track memory footprint of cached tiles.
31
+
32
+ 3. Cache Improvements
33
+
34
+ - Replace simple prune with true LRU (track lastUsed for subdivided tiles).
35
+ - Optional byte-size cap (e.g. max 64 MB) + eviction on overflow.
36
+ - Expose cache stats (hits, misses, evictions) via an API or debug callback.
37
+
38
+ 4. Max dz Guard
39
+
40
+ - Configurable hard cap `maxDz` (skip synthesis beyond threshold; return empty tile or parent tile).
41
+ - Distinct from warning-only `maxDzWarn` currently in function.
42
+
43
+ 5. Dependency Reduction (Minimal MVT Codec)
44
+
45
+ - Custom lightweight decoder: parse layers, feature type, properties, geometry command stream.
46
+ - Custom encoder for POINT, LINESTRING, POLYGON; property passthrough.
47
+ - Goal: shrink bundle size and reduce external dependencies.
48
+
49
+ 6. Geometry Simplification (High dz)
50
+
51
+ - Optional per-layer simplification tolerance parameter (Douglas-Peucker or grid-based).
52
+ - Preserve topology for polygons; avoid slivers.
53
+
54
+ 7. Synthetic Metadata & Flags
55
+
56
+ - Option to embed a small custom layer or feature attribute indicating `parentZ`/`dz` for diagnostics.
57
+ - API return could include `synthetic: true` marker (currently implicit via stats presence).
58
+
59
+ 8. Parent Search Strategy Options
60
+
61
+ - Fallback mode that uses nearest ancestor even if value != 1 (for packages lacking proper coverage semantics).
62
+ - Configurable allowlist of coverage values (e.g. treat `1` or `"blob"` as valid parents).
63
+
64
+ 9. Observability & Metrics
65
+
66
+ - Pluggable callback `onSubdivide(stats, context)`.
67
+ - Accumulate moving averages; expose via `TilePackage.getSubdivisionMetrics()`.
68
+
69
+ 10. Memory & Concurrency Controls
70
+
71
+ - Limit parallel subdivision operations (queue) to avoid main-thread spikes.
72
+ - Optional Web Worker offload (future) for heavy dz.
73
+
74
+ 11. Error Handling Extensions
75
+
76
+ - Graceful fallback: on subdivision failure, return blank tile vs warning.
77
+ - Configurable retry for transient decode issues.
78
+
79
+ 12. Documentation Enhancements
80
+
81
+ - README section: “Overzoom Mechanics & Tradeoffs”.
82
+ - Guidance on choosing `maxDz` based on feature density.
83
+
84
+ 13. Security / Validation
85
+
86
+ - Defensive checks on malformed PBF (guard against extremely large extents or geometry counts).
87
+
88
+ 14. Feature Count / Size Limits
89
+
90
+ - Option `maxFeaturesOut` to truncate layers—report truncation in stats.
91
+
92
+ 15. Layer-Specific Options
93
+
94
+ - Per-layer buffer overrides; per-layer simplification tolerance.
95
+
96
+ ## Updated API Roadmap (Proposed Additions)
97
+
98
+ ```ts
99
+ interface SubdivideOptions {
100
+ buffer?: number; // existing
101
+ includeLayers?: string[] | RegExp; // existing
102
+ maxDzWarn?: number; // existing
103
+ maxDz?: number; // NEW hard cap
104
+ simplifyTolerance?: number; // NEW (all layers)
105
+ onStats?: (stats, ctx) => void; // NEW callback
106
+ maxFeaturesOut?: number; // NEW truncation limit
107
+ parentStrategy?: "coverageValue" | "nearestAncestor"; // NEW
108
+ }
109
+ ```
110
+
111
+ ## Prioritization Suggestion (Next Steps)
112
+
113
+ 1. Implement unit tests (foundation for safe refactors).
114
+ 2. Add LRU cache + metrics collection.
115
+ 3. Introduce `maxDz` guard & benchmark to inform default.
116
+ 4. Optional: minimal codec prototype behind feature flag for size comparisons.
117
+ 5. Geometry simplification + truncation limits for high-density datasets.
118
+ 6. Parent strategy option for archives with unconventional coverage values.
119
+
120
+ ## Risks & Mitigations (Current)
121
+
122
+ - High dz latency: mitigate with `maxDz` and caching.
123
+ - Memory growth from cache: LRU + size cap.
124
+ - Bundle size: address via minimal codec.
125
+ - Incorrect clipping for edge cases: comprehensive unit tests.
126
+
127
+ ## Open Questions
128
+
129
+ - Should synthetic tiles preserve empty layers (current: omit empty)?
130
+ - Provide global toggle to disable stats collection for minimal overhead?
131
+ - Standardize metadata flag naming (`vtpk_synthetic`?) if added.
132
+
133
+ ---
134
+
135
+ This document now reflects current implementation and focuses on forward-looking tasks. Completed exploratory/phase details were removed for clarity.
@@ -0,0 +1,247 @@
1
+ // Phase A: Subdivide a parent vector tile into a higher zoom target tile.
2
+ // Minimal dependency implementation using @mapbox/vector-tile for decode and @maplibre/vt-pbf for encode.
3
+ // Public API: subdivideVectorTile(parentPbf, parentZ, parentX, parentY, targetZ, targetX, targetY, options)
4
+ // Returns { data: Uint8Array, stats }
5
+
6
+ import {
7
+ VectorTile,
8
+ VectorTileFeature,
9
+ VectorTileLayer,
10
+ } from "@mapbox/vector-tile";
11
+ import Protobuf from "pbf";
12
+ import Point from "@mapbox/point-geometry";
13
+ import { fromVectorTileJs } from "@maplibre/vt-pbf";
14
+
15
+ /**
16
+ * Subdivide parent tile PBF into higher zoom target tile.
17
+ * @param {ArrayBuffer|Uint8Array} parentPbf - Raw parent tile PBF bytes.
18
+ * @param {number} parentZ
19
+ * @param {number} parentX
20
+ * @param {number} parentY
21
+ * @param {number} targetZ
22
+ * @param {number} targetX
23
+ * @param {number} targetY
24
+ * @param {object} options
25
+ * @param {number} [options.buffer=128] - Clip buffer.
26
+ * @param {string[]|RegExp} [options.includeLayers] - Layer filter.
27
+ * @param {number} [options.maxDzWarn=8] - Warn if dz larger.
28
+ * @returns {{ data: Uint8Array, stats: any }}
29
+ */
30
+ export function subdivideVectorTile(
31
+ parentPbf,
32
+ parentZ,
33
+ parentX,
34
+ parentY,
35
+ targetZ,
36
+ targetX,
37
+ targetY,
38
+ options = {},
39
+ ) {
40
+ const t0 = performance.now ? performance.now() : Date.now();
41
+ const { buffer = 128, includeLayers, maxDzWarn = 8 } = options;
42
+ const dz = targetZ - parentZ;
43
+ if (dz <= 0) {
44
+ // Nothing to subdivide, caller should just use original bytes.
45
+ return { data: toUint8(parentPbf), stats: { dz, passthrough: true } };
46
+ }
47
+ if (dz > maxDzWarn && console && console.warn) {
48
+ console.warn(
49
+ `subdivideVectorTile: high dz=${dz}, performance may degrade.`,
50
+ );
51
+ }
52
+ // Validate containment.
53
+ const scale = Math.pow(2, dz);
54
+ const expectedParentX = Math.floor(targetX / scale);
55
+ const expectedParentY = Math.floor(targetY / scale);
56
+ if (expectedParentX !== parentX || expectedParentY !== parentY) {
57
+ throw new Error(
58
+ "Target tile not contained within parent tile coordinates.",
59
+ );
60
+ }
61
+
62
+ const vt = new VectorTile(new Protobuf(toUint8(parentPbf)));
63
+
64
+ const overzoomed = new OverzoomedVectorTile();
65
+ let featuresIn = 0;
66
+ let featuresOut = 0;
67
+ for (const layerName in vt.layers) {
68
+ if (!Object.prototype.hasOwnProperty.call(vt.layers, layerName)) continue;
69
+ if (includeLayers) {
70
+ if (Array.isArray(includeLayers) && !includeLayers.includes(layerName))
71
+ continue;
72
+ if (includeLayers instanceof RegExp && !includeLayers.test(layerName))
73
+ continue;
74
+ }
75
+ const sourceLayer = vt.layers[layerName];
76
+ const extent = sourceLayer.extent;
77
+ const offsetX = (targetX - parentX * scale) * extent;
78
+ const offsetY = (targetY - parentY * scale) * extent;
79
+
80
+ const featureWrappers = [];
81
+ for (let i = 0; i < sourceLayer.length; i++) {
82
+ const feature = sourceLayer.feature(i);
83
+ featuresIn++;
84
+ let geometry = feature.loadGeometry(); // Array of rings (Point[])
85
+ // Transform
86
+ for (const ring of geometry) {
87
+ for (const pt of ring) {
88
+ pt.x = pt.x * scale - offsetX;
89
+ pt.y = pt.y * scale - offsetY;
90
+ }
91
+ }
92
+ // Clip
93
+ geometry = clipGeometry(geometry, feature.type, -buffer, extent + buffer);
94
+ if (!geometry || geometry.length === 0) continue;
95
+ featureWrappers.push(
96
+ new OverzoomedFeatureWrapper(
97
+ feature.type,
98
+ geometry,
99
+ feature.properties,
100
+ feature.id,
101
+ extent,
102
+ ),
103
+ );
104
+ featuresOut++;
105
+ }
106
+ if (featureWrappers.length > 0) {
107
+ overzoomed.addLayer(
108
+ new OverzoomedTileLayer(featureWrappers, layerName, extent),
109
+ );
110
+ }
111
+ }
112
+ const pbf = fromVectorTileJs(overzoomed);
113
+ const data =
114
+ pbf.byteOffset === 0 && pbf.byteLength === pbf.buffer.byteLength
115
+ ? pbf
116
+ : new Uint8Array(pbf);
117
+ const t1 = performance.now ? performance.now() : Date.now();
118
+ return { data, stats: { dz, featuresIn, featuresOut, ms: t1 - t0 } };
119
+ }
120
+
121
+ // --- Internal wrappers (adapted from overzoomed-geojson-vector-tile.ts) ---
122
+ class OverzoomedFeatureWrapper extends VectorTileFeature {
123
+ constructor(type, geometry, properties, id, extent) {
124
+ super(new Protobuf(), 0, extent, [], []);
125
+ this.type = type; // 1=Point,2=LineString,3=Polygon
126
+ this.properties = properties ? properties : {};
127
+ this.extent = extent;
128
+ this.myGeometry = geometry;
129
+ this.id = id;
130
+ }
131
+ loadGeometry() {
132
+ return this.myGeometry.map((ring) => ring.map((p) => new Point(p.x, p.y)));
133
+ }
134
+ }
135
+
136
+ class OverzoomedTileLayer extends VectorTileLayer {
137
+ constructor(features, name, extent) {
138
+ super(new Protobuf());
139
+ this._myFeatures = features;
140
+ this.name = name;
141
+ this.length = features.length;
142
+ this.extent = extent;
143
+ this.version = 2;
144
+ }
145
+ feature(i) {
146
+ return this._myFeatures[i];
147
+ }
148
+ }
149
+
150
+ class OverzoomedVectorTile {
151
+ constructor() {
152
+ this.layers = {};
153
+ }
154
+ addLayer(layer) {
155
+ this.layers[layer.name] = layer;
156
+ }
157
+ }
158
+
159
+ // --- Clipping helpers ---
160
+ function clipGeometry(geometry, type, min, max) {
161
+ switch (type) {
162
+ case 1:
163
+ return clipPoints(geometry, min, max); // POINT
164
+ case 2:
165
+ return clipLinesWrapper(geometry, min, max, false); // LINESTRING
166
+ case 3:
167
+ return clipLinesWrapper(geometry, min, max, true); // POLYGON
168
+ default:
169
+ return [];
170
+ }
171
+ }
172
+ function clipPoints(geometry, min, max) {
173
+ const out = [];
174
+ for (const ring of geometry) {
175
+ for (const p of ring) {
176
+ if (p.x >= min && p.x <= max && p.y >= min && p.y <= max) {
177
+ out.push([p]);
178
+ }
179
+ }
180
+ }
181
+ return out;
182
+ }
183
+ function clipLinesWrapper(geometry, min, max, isPolygon) {
184
+ const out = [];
185
+ for (const line of geometry) {
186
+ const clipped = clipAxis(line, min, max, 0, isPolygon);
187
+ for (const seg of clipped) {
188
+ const clippedY = clipAxis(seg, min, max, 1, isPolygon);
189
+ for (const finalSeg of clippedY) out.push(finalSeg);
190
+ }
191
+ }
192
+ return out;
193
+ }
194
+ function clipAxis(line, start, end, axis, isPolygon) {
195
+ const intersect = axis === 0 ? intersectX : intersectY;
196
+ let slice = [];
197
+ const out = [];
198
+ for (let i = 0; i < line.length - 1; i++) {
199
+ const p1 = line[i];
200
+ const p2 = line[i + 1];
201
+ const a = axis === 0 ? p1.x : p1.y;
202
+ const b = axis === 0 ? p2.x : p2.y;
203
+ let exited = false;
204
+ if (a < start && b > start) slice.push(intersect(p1, p2, start));
205
+ else if (a > end && b < end) slice.push(intersect(p1, p2, end));
206
+ else if (a >= start && a <= end) slice.push(p1);
207
+ if (b < start && a >= start) {
208
+ slice.push(intersect(p1, p2, start));
209
+ exited = true;
210
+ }
211
+ if (b > end && a <= end) {
212
+ slice.push(intersect(p1, p2, end));
213
+ exited = true;
214
+ }
215
+ if (!isPolygon && exited) {
216
+ out.push(slice);
217
+ slice = [];
218
+ }
219
+ }
220
+ const last = line[line.length - 1];
221
+ const lastVal = axis === 0 ? last.x : last.y;
222
+ if (lastVal >= start && lastVal <= end) slice.push(last);
223
+ if (
224
+ isPolygon &&
225
+ slice.length > 0 &&
226
+ !slice[0].equals(slice[slice.length - 1])
227
+ ) {
228
+ slice.push(new Point(slice[0].x, slice[0].y));
229
+ }
230
+ if (slice.length > 0) out.push(slice);
231
+ return out;
232
+ }
233
+ function intersectX(p1, p2, x) {
234
+ const t = (x - p1.x) / (p2.x - p1.x);
235
+ return new Point(x, p1.y + (p2.y - p1.y) * t);
236
+ }
237
+ function intersectY(p1, p2, y) {
238
+ const t = (y - p1.y) / (p2.y - p1.y);
239
+ return new Point(p1.x + (p2.x - p1.x) * t, y);
240
+ }
241
+
242
+ function toUint8(data) {
243
+ if (data instanceof Uint8Array) return data;
244
+ return new Uint8Array(data);
245
+ }
246
+
247
+ export default subdivideVectorTile;