stipple-maplibre 0.1.0
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/LICENSE +21 -0
- package/README.md +289 -0
- package/dist/index.d.ts +605 -0
- package/dist/index.global.js +1787 -0
- package/dist/index.js +1762 -0
- package/package.json +66 -0
- package/schema/pattern-metadata-v1.schema.json +173 -0
|
@@ -0,0 +1,1787 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var MaplibrePatternFills = (() => {
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var src_exports = {};
|
|
23
|
+
__export(src_exports, {
|
|
24
|
+
PATTERN_METADATA_KEY: () => PATTERN_METADATA_KEY,
|
|
25
|
+
buildStyleFragment: () => buildStyleFragment,
|
|
26
|
+
createFontPatternDefinition: () => createFontPatternDefinition,
|
|
27
|
+
createFontPatternTile: () => createFontPatternTile,
|
|
28
|
+
createMiniContext: () => createMiniContext,
|
|
29
|
+
createSvgPatternDefinition: () => createSvgPatternDefinition,
|
|
30
|
+
createSvgScatterLayout: () => createSvgScatterLayout,
|
|
31
|
+
createSvgScatterTile: () => createSvgScatterTile,
|
|
32
|
+
hashStringToSeed: () => hashStringToSeed,
|
|
33
|
+
importGeoJsonPolygons: () => importGeoJsonPolygons,
|
|
34
|
+
installFontPatternFill: () => installFontPatternFill,
|
|
35
|
+
installPatternFills: () => installPatternFills,
|
|
36
|
+
installSvgIconScatter: () => installSvgIconScatter,
|
|
37
|
+
installSvgPatternFill: () => installSvgPatternFill,
|
|
38
|
+
makeTile: () => makeTile,
|
|
39
|
+
mulberry32: () => mulberry32,
|
|
40
|
+
observePatternFills: () => observePatternFills,
|
|
41
|
+
parsePatternDefinition: () => parsePatternDefinition,
|
|
42
|
+
parsePatternMetadata: () => parsePatternMetadata,
|
|
43
|
+
patternDefinitionId: () => patternDefinitionId,
|
|
44
|
+
scalePatternForZoom: () => scalePatternForZoom,
|
|
45
|
+
scatterIconPoints: () => scatterIconPoints,
|
|
46
|
+
scatterPointsInPolygon: () => scatterPointsInPolygon,
|
|
47
|
+
screenPatternPhase: () => screenPatternPhase,
|
|
48
|
+
serializePatternDefinition: () => serializePatternDefinition,
|
|
49
|
+
syncPatternTexture: () => syncPatternTexture
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// src/engine/miniContext.ts
|
|
53
|
+
function parseColor(v) {
|
|
54
|
+
const hex8 = v.match(/^#([0-9a-f]{8})$/i);
|
|
55
|
+
if (hex8) {
|
|
56
|
+
const n = parseInt(hex8[1], 16);
|
|
57
|
+
return { r: n >>> 24 & 255, g: n >>> 16 & 255, b: n >>> 8 & 255, a: (n & 255) / 255 };
|
|
58
|
+
}
|
|
59
|
+
const hex6 = v.match(/^#([0-9a-f]{6})$/i);
|
|
60
|
+
if (hex6) {
|
|
61
|
+
const n = parseInt(hex6[1], 16);
|
|
62
|
+
return { r: n >> 16 & 255, g: n >> 8 & 255, b: n & 255, a: 1 };
|
|
63
|
+
}
|
|
64
|
+
const hex4 = v.match(/^#([0-9a-f]{4})$/i);
|
|
65
|
+
if (hex4) {
|
|
66
|
+
const [r, g, b, a] = hex4[1].split("").map((c) => parseInt(c + c, 16));
|
|
67
|
+
return { r, g, b, a: a / 255 };
|
|
68
|
+
}
|
|
69
|
+
const hex3 = v.match(/^#([0-9a-f]{3})$/i);
|
|
70
|
+
if (hex3) {
|
|
71
|
+
const [r, g, b] = hex3[1].split("").map((c) => parseInt(c + c, 16));
|
|
72
|
+
return { r, g, b, a: 1 };
|
|
73
|
+
}
|
|
74
|
+
const rgbaComma = v.match(
|
|
75
|
+
/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+%?)\s*)?\)$/i
|
|
76
|
+
);
|
|
77
|
+
const rgbaSpace = v.match(
|
|
78
|
+
/^rgba?\(\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*(?:\/\s*([\d.]+%?)\s*)?\)$/i
|
|
79
|
+
);
|
|
80
|
+
const rgba = rgbaComma ?? rgbaSpace;
|
|
81
|
+
if (rgba) {
|
|
82
|
+
const alphaToken = rgba[4];
|
|
83
|
+
const a = alphaToken === void 0 ? 1 : alphaToken.endsWith("%") ? parseFloat(alphaToken) / 100 : +alphaToken;
|
|
84
|
+
return { r: +rgba[1], g: +rgba[2], b: +rgba[3], a };
|
|
85
|
+
}
|
|
86
|
+
throw new TypeError(
|
|
87
|
+
`Unsupported color "${v}" in the Node mini rasterizer. Supported: #rgb, #rgba, #rrggbb, #rrggbbaa, and rgb()/rgba() (comma or space/slash syntax). Named colors and hsl() require a real browser canvas.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
var clamp01 = (v) => Math.max(0, Math.min(1, v));
|
|
91
|
+
function createMiniContext(size) {
|
|
92
|
+
const buf = new Uint8ClampedArray(size * size * 4);
|
|
93
|
+
let strokeColor = { r: 0, g: 0, b: 0, a: 1 };
|
|
94
|
+
let fillColor = { r: 0, g: 0, b: 0, a: 1 };
|
|
95
|
+
let lineWidth = 1;
|
|
96
|
+
let subpaths = [];
|
|
97
|
+
let pendingArc = null;
|
|
98
|
+
function setPixel(x, y, c) {
|
|
99
|
+
if (x < 0 || y < 0 || x >= size || y >= size || c.a <= 0) return;
|
|
100
|
+
const i = (y * size + x) * 4;
|
|
101
|
+
if (c.a >= 1) {
|
|
102
|
+
buf[i] = c.r;
|
|
103
|
+
buf[i + 1] = c.g;
|
|
104
|
+
buf[i + 2] = c.b;
|
|
105
|
+
buf[i + 3] = 255;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const dstA = buf[i + 3] / 255;
|
|
109
|
+
const outA = c.a + dstA * (1 - c.a);
|
|
110
|
+
if (outA <= 0) return;
|
|
111
|
+
buf[i] = (c.r * c.a + buf[i] * dstA * (1 - c.a)) / outA;
|
|
112
|
+
buf[i + 1] = (c.g * c.a + buf[i + 1] * dstA * (1 - c.a)) / outA;
|
|
113
|
+
buf[i + 2] = (c.b * c.a + buf[i + 2] * dstA * (1 - c.a)) / outA;
|
|
114
|
+
buf[i + 3] = outA * 255;
|
|
115
|
+
}
|
|
116
|
+
function strokeSegment(x0, y0, x1, y1) {
|
|
117
|
+
const half = lineWidth / 2;
|
|
118
|
+
const minX = Math.max(0, Math.floor(Math.min(x0, x1) - half - 1));
|
|
119
|
+
const maxX = Math.min(size - 1, Math.ceil(Math.max(x0, x1) + half + 1));
|
|
120
|
+
const minY = Math.max(0, Math.floor(Math.min(y0, y1) - half - 1));
|
|
121
|
+
const maxY = Math.min(size - 1, Math.ceil(Math.max(y0, y1) + half + 1));
|
|
122
|
+
const dx = x1 - x0;
|
|
123
|
+
const dy = y1 - y0;
|
|
124
|
+
const lenSq = dx * dx + dy * dy;
|
|
125
|
+
for (let py = minY; py <= maxY; py++) {
|
|
126
|
+
for (let px = minX; px <= maxX; px++) {
|
|
127
|
+
const cx = px + 0.5;
|
|
128
|
+
const cy = py + 0.5;
|
|
129
|
+
let t = lenSq === 0 ? 0 : ((cx - x0) * dx + (cy - y0) * dy) / lenSq;
|
|
130
|
+
t = clamp01(t);
|
|
131
|
+
const projX = x0 + t * dx;
|
|
132
|
+
const projY = y0 + t * dy;
|
|
133
|
+
const dist = Math.hypot(cx - projX, cy - projY);
|
|
134
|
+
const coverage = clamp01(half + 0.5 - dist);
|
|
135
|
+
if (coverage > 0) setPixel(px, py, { ...strokeColor, a: strokeColor.a * coverage });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function fillCircle(cx, cy, r) {
|
|
140
|
+
const minX = Math.max(0, Math.floor(cx - r - 1));
|
|
141
|
+
const maxX = Math.min(size - 1, Math.ceil(cx + r + 1));
|
|
142
|
+
const minY = Math.max(0, Math.floor(cy - r - 1));
|
|
143
|
+
const maxY = Math.min(size - 1, Math.ceil(cy + r + 1));
|
|
144
|
+
for (let py = minY; py <= maxY; py++) {
|
|
145
|
+
for (let px = minX; px <= maxX; px++) {
|
|
146
|
+
const dist = Math.hypot(px + 0.5 - cx, py + 0.5 - cy);
|
|
147
|
+
const coverage = clamp01(r + 0.5 - dist);
|
|
148
|
+
if (coverage > 0) setPixel(px, py, { ...fillColor, a: fillColor.a * coverage });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const ctx = {
|
|
153
|
+
get strokeStyle() {
|
|
154
|
+
return `rgba(${strokeColor.r},${strokeColor.g},${strokeColor.b},${strokeColor.a})`;
|
|
155
|
+
},
|
|
156
|
+
set strokeStyle(v) {
|
|
157
|
+
strokeColor = parseColor(v);
|
|
158
|
+
},
|
|
159
|
+
get fillStyle() {
|
|
160
|
+
return `rgba(${fillColor.r},${fillColor.g},${fillColor.b},${fillColor.a})`;
|
|
161
|
+
},
|
|
162
|
+
set fillStyle(v) {
|
|
163
|
+
fillColor = parseColor(v);
|
|
164
|
+
},
|
|
165
|
+
get lineWidth() {
|
|
166
|
+
return lineWidth;
|
|
167
|
+
},
|
|
168
|
+
set lineWidth(v) {
|
|
169
|
+
lineWidth = v;
|
|
170
|
+
},
|
|
171
|
+
lineCap: "round",
|
|
172
|
+
clearRect() {
|
|
173
|
+
buf.fill(0);
|
|
174
|
+
},
|
|
175
|
+
beginPath() {
|
|
176
|
+
subpaths = [];
|
|
177
|
+
pendingArc = null;
|
|
178
|
+
},
|
|
179
|
+
moveTo(x, y) {
|
|
180
|
+
subpaths.push([{ x, y }]);
|
|
181
|
+
},
|
|
182
|
+
lineTo(x, y) {
|
|
183
|
+
const current = subpaths[subpaths.length - 1];
|
|
184
|
+
if (current) current.push({ x, y });
|
|
185
|
+
},
|
|
186
|
+
stroke() {
|
|
187
|
+
for (const sp of subpaths) {
|
|
188
|
+
for (let i = 1; i < sp.length; i++) strokeSegment(sp[i - 1].x, sp[i - 1].y, sp[i].x, sp[i].y);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
arc(x, y, radius) {
|
|
192
|
+
pendingArc = { x, y, r: radius };
|
|
193
|
+
},
|
|
194
|
+
fill() {
|
|
195
|
+
if (pendingArc) fillCircle(pendingArc.x, pendingArc.y, pendingArc.r);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
return {
|
|
199
|
+
ctx,
|
|
200
|
+
toTileImage: () => ({ width: size, height: size, data: new Uint8Array(buf.buffer) })
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/engine/seededRandom.ts
|
|
205
|
+
function mulberry32(seed) {
|
|
206
|
+
let a = seed >>> 0;
|
|
207
|
+
return function next() {
|
|
208
|
+
a |= 0;
|
|
209
|
+
a = a + 1831565813 | 0;
|
|
210
|
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
211
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
212
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function hashStringToSeed(str) {
|
|
216
|
+
let h = 1779033703 ^ str.length;
|
|
217
|
+
for (let i = 0; i < str.length; i++) {
|
|
218
|
+
h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
|
|
219
|
+
h = h << 13 | h >>> 19;
|
|
220
|
+
}
|
|
221
|
+
return (h ^ h >>> 16) >>> 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/engine/makeTile.ts
|
|
225
|
+
var DEFAULT_STIPPLE_COUNT = 7;
|
|
226
|
+
function toroidalDistance(a, b, period) {
|
|
227
|
+
const dx = Math.min(Math.abs(a.x - b.x), period - Math.abs(a.x - b.x));
|
|
228
|
+
const dy = Math.min(Math.abs(a.y - b.y), period - Math.abs(a.y - b.y));
|
|
229
|
+
return Math.hypot(dx, dy);
|
|
230
|
+
}
|
|
231
|
+
function defaultContextFactory(size) {
|
|
232
|
+
if (typeof document !== "undefined") {
|
|
233
|
+
const canvas = document.createElement("canvas");
|
|
234
|
+
canvas.width = canvas.height = size;
|
|
235
|
+
const canvasCtx = canvas.getContext("2d");
|
|
236
|
+
if (!canvasCtx) throw new Error("2D canvas context unavailable");
|
|
237
|
+
return {
|
|
238
|
+
ctx: canvasCtx,
|
|
239
|
+
toTileImage: () => {
|
|
240
|
+
const img = canvasCtx.getImageData(0, 0, size, size);
|
|
241
|
+
return { width: size, height: size, data: new Uint8Array(img.data.buffer) };
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return createMiniContext(size);
|
|
246
|
+
}
|
|
247
|
+
function makeTile(pattern, size, color, weight, angle, options = {}) {
|
|
248
|
+
const pixelRatio = options.pixelRatio ?? 1;
|
|
249
|
+
if (!Number.isFinite(pixelRatio) || pixelRatio <= 0) {
|
|
250
|
+
throw new RangeError("pixelRatio must be a finite number greater than 0");
|
|
251
|
+
}
|
|
252
|
+
if (!Number.isFinite(size) || size <= 0) {
|
|
253
|
+
throw new RangeError("size must be a finite number greater than 0");
|
|
254
|
+
}
|
|
255
|
+
if (!Number.isFinite(weight) || weight <= 0) {
|
|
256
|
+
throw new RangeError("weight must be a finite number greater than 0");
|
|
257
|
+
}
|
|
258
|
+
if (!Number.isFinite(angle)) {
|
|
259
|
+
throw new TypeError("angle must be a finite number");
|
|
260
|
+
}
|
|
261
|
+
if (options.stippleCount !== void 0 && (!Number.isInteger(options.stippleCount) || options.stippleCount <= 0)) {
|
|
262
|
+
throw new RangeError("stippleCount must be a positive integer");
|
|
263
|
+
}
|
|
264
|
+
if (typeof color !== "string" || color.trim().length === 0) {
|
|
265
|
+
throw new TypeError("color must be a non-empty string");
|
|
266
|
+
}
|
|
267
|
+
const physicalSize = Math.max(1, Math.round(size * pixelRatio));
|
|
268
|
+
const renderScale = physicalSize / size;
|
|
269
|
+
const { ctx, toTileImage } = (options.contextFactory ?? defaultContextFactory)(physicalSize);
|
|
270
|
+
ctx.clearRect(0, 0, physicalSize, physicalSize);
|
|
271
|
+
ctx.strokeStyle = color;
|
|
272
|
+
ctx.fillStyle = color;
|
|
273
|
+
ctx.lineWidth = weight * renderScale;
|
|
274
|
+
ctx.lineCap = "round";
|
|
275
|
+
const drawHachures = (deg) => {
|
|
276
|
+
ctx.beginPath();
|
|
277
|
+
const step = Math.max(physicalSize / 2, 4 * renderScale);
|
|
278
|
+
if (deg === 0) {
|
|
279
|
+
for (let y = 0; y <= physicalSize; y += step) {
|
|
280
|
+
ctx.moveTo(0, y + 0.5 * renderScale);
|
|
281
|
+
ctx.lineTo(physicalSize, y + 0.5 * renderScale);
|
|
282
|
+
}
|
|
283
|
+
} else if (deg === 90) {
|
|
284
|
+
for (let x = 0; x <= physicalSize; x += step) {
|
|
285
|
+
ctx.moveTo(x + 0.5 * renderScale, 0);
|
|
286
|
+
ctx.lineTo(x + 0.5 * renderScale, physicalSize);
|
|
287
|
+
}
|
|
288
|
+
} else if (deg === 45) {
|
|
289
|
+
for (let o = -physicalSize; o <= physicalSize * 2; o += step) {
|
|
290
|
+
ctx.moveTo(o, 0);
|
|
291
|
+
ctx.lineTo(o + physicalSize, physicalSize);
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
for (let o = -physicalSize; o <= physicalSize * 2; o += step) {
|
|
295
|
+
ctx.moveTo(o + physicalSize, 0);
|
|
296
|
+
ctx.lineTo(o, physicalSize);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
ctx.stroke();
|
|
300
|
+
};
|
|
301
|
+
switch (pattern) {
|
|
302
|
+
case "hachures":
|
|
303
|
+
drawHachures(angle);
|
|
304
|
+
break;
|
|
305
|
+
case "cross":
|
|
306
|
+
drawHachures(angle);
|
|
307
|
+
drawHachures((angle + 90 + 45) % 180 - 45);
|
|
308
|
+
break;
|
|
309
|
+
case "grid": {
|
|
310
|
+
ctx.beginPath();
|
|
311
|
+
ctx.moveTo(0.5 * renderScale, 0);
|
|
312
|
+
ctx.lineTo(0.5 * renderScale, physicalSize);
|
|
313
|
+
ctx.moveTo(0, 0.5 * renderScale);
|
|
314
|
+
ctx.lineTo(physicalSize, 0.5 * renderScale);
|
|
315
|
+
ctx.stroke();
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
case "stipple": {
|
|
319
|
+
const r = weight * renderScale * 0.6;
|
|
320
|
+
const count = options.stippleCount ?? DEFAULT_STIPPLE_COUNT;
|
|
321
|
+
const seedKey = `fixed:${count}`;
|
|
322
|
+
const rand = mulberry32(hashStringToSeed(`stipple:${seedKey}`));
|
|
323
|
+
const candidateCount = 20;
|
|
324
|
+
const positions = [];
|
|
325
|
+
for (let i = 0; i < count; i++) {
|
|
326
|
+
let best = { x: rand() * physicalSize, y: rand() * physicalSize };
|
|
327
|
+
let bestDistance = -1;
|
|
328
|
+
for (let c = 0; c < candidateCount; c++) {
|
|
329
|
+
const candidate = { x: rand() * physicalSize, y: rand() * physicalSize };
|
|
330
|
+
const nearest = positions.length === 0 ? Infinity : Math.min(...positions.map((p) => toroidalDistance(candidate, p, physicalSize)));
|
|
331
|
+
if (nearest > bestDistance) {
|
|
332
|
+
best = candidate;
|
|
333
|
+
bestDistance = nearest;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
positions.push(best);
|
|
337
|
+
}
|
|
338
|
+
for (const { x, y } of positions) {
|
|
339
|
+
const minShiftX = Math.ceil((-r - x) / physicalSize);
|
|
340
|
+
const maxShiftX = Math.floor((physicalSize + r - x) / physicalSize);
|
|
341
|
+
const minShiftY = Math.ceil((-r - y) / physicalSize);
|
|
342
|
+
const maxShiftY = Math.floor((physicalSize + r - y) / physicalSize);
|
|
343
|
+
for (let shiftY = minShiftY; shiftY <= maxShiftY; shiftY++) {
|
|
344
|
+
for (let shiftX = minShiftX; shiftX <= maxShiftX; shiftX++) {
|
|
345
|
+
ctx.beginPath();
|
|
346
|
+
ctx.arc(x + shiftX * physicalSize, y + shiftY * physicalSize, r, 0, Math.PI * 2);
|
|
347
|
+
ctx.fill();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
case "dots": {
|
|
354
|
+
const r = Math.max(weight * renderScale * 0.5, renderScale);
|
|
355
|
+
ctx.beginPath();
|
|
356
|
+
ctx.arc(physicalSize / 2, physicalSize / 2, r, 0, Math.PI * 2);
|
|
357
|
+
ctx.fill();
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
case "solid":
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
return toTileImage();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/engine/scatterPoints.ts
|
|
367
|
+
function isInsideRings(x, y, rings) {
|
|
368
|
+
let inside = false;
|
|
369
|
+
for (const ring2 of rings) {
|
|
370
|
+
for (let i = 0, j = ring2.length - 1; i < ring2.length; j = i++) {
|
|
371
|
+
const [xi, yi] = ring2[i];
|
|
372
|
+
const [xj, yj] = ring2[j];
|
|
373
|
+
const crosses = yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi;
|
|
374
|
+
if (crosses) inside = !inside;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return inside;
|
|
378
|
+
}
|
|
379
|
+
function squaredDistanceToSegment(x, y, start, end) {
|
|
380
|
+
const dx = end[0] - start[0];
|
|
381
|
+
const dy = end[1] - start[1];
|
|
382
|
+
if (dx === 0 && dy === 0) return (x - start[0]) ** 2 + (y - start[1]) ** 2;
|
|
383
|
+
const projection = Math.max(
|
|
384
|
+
0,
|
|
385
|
+
Math.min(1, ((x - start[0]) * dx + (y - start[1]) * dy) / (dx * dx + dy * dy))
|
|
386
|
+
);
|
|
387
|
+
const nearestX = start[0] + projection * dx;
|
|
388
|
+
const nearestY = start[1] + projection * dy;
|
|
389
|
+
return (x - nearestX) ** 2 + (y - nearestY) ** 2;
|
|
390
|
+
}
|
|
391
|
+
function discFitsInside(x, y, radius, rings) {
|
|
392
|
+
if (!isInsideRings(x, y, rings)) return false;
|
|
393
|
+
const squaredRadius = radius * radius;
|
|
394
|
+
for (const ring2 of rings) {
|
|
395
|
+
for (let index = 0; index < ring2.length; index++) {
|
|
396
|
+
const start = ring2[index];
|
|
397
|
+
const end = ring2[(index + 1) % ring2.length];
|
|
398
|
+
if (squaredDistanceToSegment(x, y, start, end) < squaredRadius) return false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
function scatterPointsInPolygon(rings, options) {
|
|
404
|
+
const {
|
|
405
|
+
radius,
|
|
406
|
+
density = 1,
|
|
407
|
+
seed = 1,
|
|
408
|
+
rotationJitterDeg = 0,
|
|
409
|
+
scaleJitter = 0,
|
|
410
|
+
positionJitter = 0.15,
|
|
411
|
+
stagger = true,
|
|
412
|
+
distribution = stagger ? "offset" : "regular",
|
|
413
|
+
minSpacing = 0,
|
|
414
|
+
clipBounds
|
|
415
|
+
} = options;
|
|
416
|
+
if (!Number.isFinite(radius) || radius < 0) {
|
|
417
|
+
throw new RangeError("radius must be a finite number greater than or equal to 0");
|
|
418
|
+
}
|
|
419
|
+
if (!Number.isFinite(density) || density <= 0) {
|
|
420
|
+
throw new RangeError("density must be a finite number greater than 0");
|
|
421
|
+
}
|
|
422
|
+
if (!Number.isFinite(rotationJitterDeg) || rotationJitterDeg < 0) {
|
|
423
|
+
throw new RangeError("rotationJitterDeg must be a finite number greater than or equal to 0");
|
|
424
|
+
}
|
|
425
|
+
if (!Number.isFinite(scaleJitter) || scaleJitter < 0 || scaleJitter >= 1) {
|
|
426
|
+
throw new RangeError("scaleJitter must be a finite number greater than or equal to 0 and less than 1");
|
|
427
|
+
}
|
|
428
|
+
if (!Number.isFinite(positionJitter) || positionJitter < 0) {
|
|
429
|
+
throw new RangeError("positionJitter must be a finite number greater than or equal to 0");
|
|
430
|
+
}
|
|
431
|
+
if (!["regular", "offset", "natural"].includes(distribution)) {
|
|
432
|
+
throw new TypeError("distribution must be regular, offset, or natural");
|
|
433
|
+
}
|
|
434
|
+
if (!Number.isFinite(minSpacing) || minSpacing < 0) {
|
|
435
|
+
throw new RangeError("minSpacing must be a finite number greater than or equal to 0");
|
|
436
|
+
}
|
|
437
|
+
const exterior = rings[0] ?? [];
|
|
438
|
+
if (exterior.length === 0) return [];
|
|
439
|
+
let minX = Infinity;
|
|
440
|
+
let minY = Infinity;
|
|
441
|
+
let maxX = -Infinity;
|
|
442
|
+
let maxY = -Infinity;
|
|
443
|
+
for (const [x, y] of exterior) {
|
|
444
|
+
if (x < minX) minX = x;
|
|
445
|
+
if (x > maxX) maxX = x;
|
|
446
|
+
if (y < minY) minY = y;
|
|
447
|
+
if (y > maxY) maxY = y;
|
|
448
|
+
}
|
|
449
|
+
const seedNum = typeof seed === "string" ? hashStringToSeed(seed) : seed;
|
|
450
|
+
const rand = mulberry32(seedNum);
|
|
451
|
+
const cell = 100 / Math.sqrt(density);
|
|
452
|
+
const points = [];
|
|
453
|
+
const width = maxX - minX;
|
|
454
|
+
const height = maxY - minY;
|
|
455
|
+
const clipMinX = Math.max(minX, clipBounds?.minX ?? minX);
|
|
456
|
+
const clipMinY = Math.max(minY, clipBounds?.minY ?? minY);
|
|
457
|
+
const clipMaxX = Math.min(maxX, clipBounds?.maxX ?? maxX);
|
|
458
|
+
const clipMaxY = Math.min(maxY, clipBounds?.maxY ?? maxY);
|
|
459
|
+
if (clipMaxX <= clipMinX || clipMaxY <= clipMinY) return [];
|
|
460
|
+
if (distribution === "natural") {
|
|
461
|
+
const clippedWidth = clipMaxX - clipMinX;
|
|
462
|
+
const clippedHeight = clipMaxY - clipMinY;
|
|
463
|
+
const targetCount = Math.max(1, Math.round(density * clippedWidth * clippedHeight / 1e4));
|
|
464
|
+
const minimumDistance = Math.max(
|
|
465
|
+
cell * 0.76,
|
|
466
|
+
radius * 2 * (1 + scaleJitter) + minSpacing
|
|
467
|
+
);
|
|
468
|
+
const conservativeRadius = radius * (1 + scaleJitter);
|
|
469
|
+
const minimumDistanceSquared = minimumDistance ** 2;
|
|
470
|
+
const gridCell = Math.max(minimumDistance / Math.SQRT2, 1);
|
|
471
|
+
const grid = /* @__PURE__ */ new Map();
|
|
472
|
+
const active = [];
|
|
473
|
+
const gridCoordinate = (value, origin) => Math.floor((value - origin) / gridCell);
|
|
474
|
+
const gridKey = (column, row) => `${column}:${row}`;
|
|
475
|
+
const isFarEnough = (x, y) => {
|
|
476
|
+
if (minimumDistanceSquared === 0) return true;
|
|
477
|
+
const column = gridCoordinate(x, clipMinX);
|
|
478
|
+
const row = gridCoordinate(y, clipMinY);
|
|
479
|
+
for (let offsetY = -2; offsetY <= 2; offsetY++) {
|
|
480
|
+
for (let offsetX = -2; offsetX <= 2; offsetX++) {
|
|
481
|
+
const pointIndex = grid.get(gridKey(column + offsetX, row + offsetY));
|
|
482
|
+
if (pointIndex === void 0) continue;
|
|
483
|
+
const point = points[pointIndex];
|
|
484
|
+
if ((x - point.x) ** 2 + (y - point.y) ** 2 < minimumDistanceSquared) return false;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return true;
|
|
488
|
+
};
|
|
489
|
+
const addPoint = (x, y) => {
|
|
490
|
+
const pointIndex = points.length;
|
|
491
|
+
points.push({
|
|
492
|
+
x,
|
|
493
|
+
y,
|
|
494
|
+
rotation: (rand() * 2 - 1) * rotationJitterDeg,
|
|
495
|
+
scale: 1 + (rand() * 2 - 1) * scaleJitter
|
|
496
|
+
});
|
|
497
|
+
active.push(pointIndex);
|
|
498
|
+
grid.set(gridKey(
|
|
499
|
+
gridCoordinate(x, clipMinX),
|
|
500
|
+
gridCoordinate(y, clipMinY)
|
|
501
|
+
), pointIndex);
|
|
502
|
+
};
|
|
503
|
+
for (let attempt = 0; attempt < 128 && points.length === 0; attempt++) {
|
|
504
|
+
const x = clipMinX + rand() * clippedWidth;
|
|
505
|
+
const y = clipMinY + rand() * clippedHeight;
|
|
506
|
+
if (discFitsInside(x, y, conservativeRadius, rings)) addPoint(x, y);
|
|
507
|
+
}
|
|
508
|
+
while (active.length > 0 && points.length < targetCount) {
|
|
509
|
+
const activeSlot = Math.floor(rand() * active.length);
|
|
510
|
+
const origin = points[active[activeSlot]];
|
|
511
|
+
let accepted = false;
|
|
512
|
+
for (let attempt = 0; attempt < 30; attempt++) {
|
|
513
|
+
const angle = rand() * Math.PI * 2;
|
|
514
|
+
const distance = minimumDistance * (1 + rand());
|
|
515
|
+
const x = origin.x + Math.cos(angle) * distance;
|
|
516
|
+
const y = origin.y + Math.sin(angle) * distance;
|
|
517
|
+
if (x < clipMinX || x > clipMaxX || y < clipMinY || y > clipMaxY || !discFitsInside(x, y, conservativeRadius, rings) || !isFarEnough(x, y)) continue;
|
|
518
|
+
addPoint(x, y);
|
|
519
|
+
accepted = true;
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
if (!accepted) active.splice(activeSlot, 1);
|
|
523
|
+
}
|
|
524
|
+
return points;
|
|
525
|
+
}
|
|
526
|
+
const columns = Math.max(1, Math.floor(width / cell));
|
|
527
|
+
const rows = Math.max(1, Math.floor(height / cell));
|
|
528
|
+
const startX = minX + (width - (columns - 1) * cell) / 2;
|
|
529
|
+
const startY = minY + (height - (rows - 1) * cell) / 2;
|
|
530
|
+
const jitterMargin = cell * positionJitter / 2;
|
|
531
|
+
const firstRow = Math.max(0, Math.ceil((clipMinY - jitterMargin - startY) / cell));
|
|
532
|
+
const lastRow = Math.min(rows - 1, Math.floor((clipMaxY + jitterMargin - startY) / cell));
|
|
533
|
+
for (let row = firstRow; row <= lastRow; row++) {
|
|
534
|
+
const rowOffset = distribution === "offset" && columns > 1 ? row % 2 === 0 ? -cell / 4 : cell / 4 : 0;
|
|
535
|
+
const firstColumn = Math.max(
|
|
536
|
+
0,
|
|
537
|
+
Math.ceil((clipMinX - jitterMargin - startX - rowOffset) / cell)
|
|
538
|
+
);
|
|
539
|
+
const lastColumn = Math.min(
|
|
540
|
+
columns - 1,
|
|
541
|
+
Math.floor((clipMaxX + jitterMargin - startX - rowOffset) / cell)
|
|
542
|
+
);
|
|
543
|
+
for (let column = firstColumn; column <= lastColumn; column++) {
|
|
544
|
+
const jitter = distribution === "offset" ? positionJitter : 0;
|
|
545
|
+
const jx = startX + column * cell + rowOffset + (rand() - 0.5) * cell * jitter;
|
|
546
|
+
const jy = startY + row * cell + (rand() - 0.5) * cell * jitter;
|
|
547
|
+
if (jx < clipMinX || jx > clipMaxX || jy < clipMinY || jy > clipMaxY) continue;
|
|
548
|
+
const rotation = (rand() * 2 - 1) * rotationJitterDeg;
|
|
549
|
+
const scale = 1 + (rand() * 2 - 1) * scaleJitter;
|
|
550
|
+
if (discFitsInside(jx, jy, radius * scale, rings)) points.push({ x: jx, y: jy, rotation, scale });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return points;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/engine/svgScatterLayout.ts
|
|
557
|
+
function positiveFinite(name, value) {
|
|
558
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
559
|
+
throw new RangeError(`${name} must be a finite number greater than 0`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
function nonNegativeFinite(name, value) {
|
|
563
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
564
|
+
throw new RangeError(`${name} must be a finite number greater than or equal to 0`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function modulo(value, divisor) {
|
|
568
|
+
return (value % divisor + divisor) % divisor;
|
|
569
|
+
}
|
|
570
|
+
function toroidalDistance2(first, second, tileSize) {
|
|
571
|
+
const dx = Math.min(Math.abs(first.x - second.x), tileSize - Math.abs(first.x - second.x));
|
|
572
|
+
const dy = Math.min(Math.abs(first.y - second.y), tileSize - Math.abs(first.y - second.y));
|
|
573
|
+
return Math.hypot(dx, dy);
|
|
574
|
+
}
|
|
575
|
+
function createNaturalPositions(count, tileSize, minimumDistance, rand) {
|
|
576
|
+
const positions = [];
|
|
577
|
+
const candidateCount = 24;
|
|
578
|
+
while (positions.length < count) {
|
|
579
|
+
let best;
|
|
580
|
+
let bestDistance = -1;
|
|
581
|
+
for (let candidateIndex = 0; candidateIndex < candidateCount; candidateIndex++) {
|
|
582
|
+
const candidate = { x: rand() * tileSize, y: rand() * tileSize };
|
|
583
|
+
const nearest = positions.length === 0 ? Infinity : Math.min(...positions.map((position2) => toroidalDistance2(candidate, position2, tileSize)));
|
|
584
|
+
if (nearest > bestDistance) {
|
|
585
|
+
best = candidate;
|
|
586
|
+
bestDistance = nearest;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (!best || positions.length > 0 && bestDistance < minimumDistance) break;
|
|
590
|
+
positions.push(best);
|
|
591
|
+
}
|
|
592
|
+
return positions;
|
|
593
|
+
}
|
|
594
|
+
function createSvgScatterLayout(options) {
|
|
595
|
+
const {
|
|
596
|
+
tileSize,
|
|
597
|
+
stampSize,
|
|
598
|
+
density,
|
|
599
|
+
seed,
|
|
600
|
+
rotationJitterDeg,
|
|
601
|
+
scaleJitter,
|
|
602
|
+
positionJitter,
|
|
603
|
+
stagger
|
|
604
|
+
} = options;
|
|
605
|
+
const distribution = options.distribution ?? (stagger ? "offset" : "regular");
|
|
606
|
+
const minSpacing = options.minSpacing ?? 0;
|
|
607
|
+
positiveFinite("tileSize", tileSize);
|
|
608
|
+
positiveFinite("stampSize", stampSize);
|
|
609
|
+
positiveFinite("density", density);
|
|
610
|
+
nonNegativeFinite("rotationJitterDeg", rotationJitterDeg);
|
|
611
|
+
nonNegativeFinite("scaleJitter", scaleJitter);
|
|
612
|
+
nonNegativeFinite("positionJitter", positionJitter);
|
|
613
|
+
nonNegativeFinite("minSpacing", minSpacing);
|
|
614
|
+
if (!["regular", "offset", "natural"].includes(distribution)) {
|
|
615
|
+
throw new TypeError("distribution must be regular, offset, or natural");
|
|
616
|
+
}
|
|
617
|
+
if (scaleJitter >= 1) {
|
|
618
|
+
throw new RangeError("scaleJitter must be less than 1 so every stamp has a positive scale");
|
|
619
|
+
}
|
|
620
|
+
const seedNum = typeof seed === "string" ? hashStringToSeed(seed) : seed;
|
|
621
|
+
if (!Number.isFinite(seedNum)) throw new RangeError("seed must be a finite number or a string");
|
|
622
|
+
const rand = mulberry32(seedNum);
|
|
623
|
+
const targetCount = Math.max(1, Math.round(density * (tileSize * tileSize) / 1e4));
|
|
624
|
+
let positions;
|
|
625
|
+
if (distribution === "natural") {
|
|
626
|
+
const minimumDistance = stampSize * Math.SQRT2 * (1 + scaleJitter) + minSpacing;
|
|
627
|
+
positions = createNaturalPositions(targetCount, tileSize, minimumDistance, rand);
|
|
628
|
+
} else {
|
|
629
|
+
const targetCell = 100 / Math.sqrt(density);
|
|
630
|
+
const cols = Math.max(1, Math.round(tileSize / targetCell));
|
|
631
|
+
let rows = Math.max(1, Math.round(tileSize / targetCell));
|
|
632
|
+
const isOffset = distribution === "offset";
|
|
633
|
+
if (isOffset && rows % 2 !== 0) {
|
|
634
|
+
const lower = Math.max(2, rows - 1);
|
|
635
|
+
const upper = rows + 1;
|
|
636
|
+
rows = Math.abs(cols * lower - targetCount) <= Math.abs(cols * upper - targetCount) ? lower : upper;
|
|
637
|
+
}
|
|
638
|
+
const cellWidth = tileSize / cols;
|
|
639
|
+
const cellHeight = tileSize / rows;
|
|
640
|
+
const jitterAmount = distribution === "regular" ? 0 : positionJitter;
|
|
641
|
+
positions = [];
|
|
642
|
+
for (let row = 0; row < rows; row++) {
|
|
643
|
+
const rowOffset = isOffset && row % 2 === 1 ? cellWidth / 2 : 0;
|
|
644
|
+
for (let col = 0; col < cols; col++) {
|
|
645
|
+
const jitterX = (rand() - 0.5) * cellWidth * jitterAmount;
|
|
646
|
+
const jitterY = (rand() - 0.5) * cellHeight * jitterAmount;
|
|
647
|
+
positions.push({
|
|
648
|
+
x: modulo(col * cellWidth + cellWidth / 2 + rowOffset + jitterX, tileSize),
|
|
649
|
+
y: modulo(row * cellHeight + cellHeight / 2 + jitterY, tileSize)
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
const placements = [];
|
|
655
|
+
positions.forEach(({ x, y }, stampIndex) => {
|
|
656
|
+
const rotationRad = (rand() * 2 - 1) * rotationJitterDeg * (Math.PI / 180);
|
|
657
|
+
const scale = 1 + (rand() * 2 - 1) * scaleJitter;
|
|
658
|
+
const extent = stampSize * scale * Math.SQRT2 / 2;
|
|
659
|
+
const minShiftX = Math.ceil((-extent - x) / tileSize);
|
|
660
|
+
const maxShiftX = Math.floor((tileSize + extent - x) / tileSize);
|
|
661
|
+
const minShiftY = Math.ceil((-extent - y) / tileSize);
|
|
662
|
+
const maxShiftY = Math.floor((tileSize + extent - y) / tileSize);
|
|
663
|
+
for (let shiftY = minShiftY; shiftY <= maxShiftY; shiftY++) {
|
|
664
|
+
for (let shiftX = minShiftX; shiftX <= maxShiftX; shiftX++) {
|
|
665
|
+
placements.push({
|
|
666
|
+
stampIndex,
|
|
667
|
+
x: x + shiftX * tileSize,
|
|
668
|
+
y: y + shiftY * tileSize,
|
|
669
|
+
rotationRad,
|
|
670
|
+
scale
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
return placements;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// src/engine/patternScale.ts
|
|
679
|
+
function finite(value, name) {
|
|
680
|
+
if (!Number.isFinite(value)) throw new TypeError(`${name} must be finite`);
|
|
681
|
+
return value;
|
|
682
|
+
}
|
|
683
|
+
function positive(value, name) {
|
|
684
|
+
finite(value, name);
|
|
685
|
+
if (value <= 0) throw new RangeError(`${name} must be greater than 0`);
|
|
686
|
+
return value;
|
|
687
|
+
}
|
|
688
|
+
function clamp(value, minimum, maximum) {
|
|
689
|
+
return Math.max(minimum, Math.min(maximum, value));
|
|
690
|
+
}
|
|
691
|
+
function scalePatternForZoom(options) {
|
|
692
|
+
const zoom = finite(options.zoom, "zoom");
|
|
693
|
+
const referenceZoom = finite(options.referenceZoom, "referenceZoom");
|
|
694
|
+
const visualSize = positive(options.visualSize, "visualSize");
|
|
695
|
+
const spacing = positive(options.spacing, "spacing");
|
|
696
|
+
const opticalScale = positive(options.opticalScale ?? 1, "opticalScale");
|
|
697
|
+
const minReadableSize = positive(
|
|
698
|
+
options.minReadableSize ?? 10,
|
|
699
|
+
"minReadableSize"
|
|
700
|
+
);
|
|
701
|
+
const maxVisualSize = positive(
|
|
702
|
+
options.maxVisualSize ?? 72,
|
|
703
|
+
"maxVisualSize"
|
|
704
|
+
);
|
|
705
|
+
const maxSpacingAtReadableFloorRatio = options.maxSpacingAtReadableFloorRatio === void 0 ? void 0 : positive(
|
|
706
|
+
options.maxSpacingAtReadableFloorRatio,
|
|
707
|
+
"maxSpacingAtReadableFloorRatio"
|
|
708
|
+
);
|
|
709
|
+
if (maxVisualSize < minReadableSize) {
|
|
710
|
+
throw new RangeError("maxVisualSize must be greater than or equal to minReadableSize");
|
|
711
|
+
}
|
|
712
|
+
const rawScale = options.mode === "map" ? 2 ** (zoom - referenceZoom) : 1;
|
|
713
|
+
const rawVisualSize = visualSize * rawScale;
|
|
714
|
+
const minimumVisualSize = Math.min(visualSize, minReadableSize);
|
|
715
|
+
const minimumScale = minimumVisualSize / visualSize;
|
|
716
|
+
const maximumScale = maxVisualSize / visualSize;
|
|
717
|
+
const scale = clamp(rawScale, minimumScale, maximumScale);
|
|
718
|
+
const renderedVisualSize = visualSize * scale;
|
|
719
|
+
let renderedSpacing = spacing * scale;
|
|
720
|
+
if (maxSpacingAtReadableFloorRatio !== void 0 && rawScale <= minimumScale) {
|
|
721
|
+
renderedSpacing = Math.min(
|
|
722
|
+
renderedSpacing,
|
|
723
|
+
renderedVisualSize * maxSpacingAtReadableFloorRatio
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
scale,
|
|
728
|
+
rawVisualSize,
|
|
729
|
+
visualSize: renderedVisualSize,
|
|
730
|
+
stampSize: renderedVisualSize / opticalScale,
|
|
731
|
+
spacing: renderedSpacing,
|
|
732
|
+
density: 1e4 / (renderedSpacing * renderedSpacing),
|
|
733
|
+
opacity: 1,
|
|
734
|
+
floored: rawScale < minimumScale,
|
|
735
|
+
capped: rawScale > maximumScale
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// src/engine/screenPatternPhase.ts
|
|
740
|
+
function screenPatternPhase(zoom, steps = 8) {
|
|
741
|
+
if (!Number.isFinite(zoom)) {
|
|
742
|
+
throw new TypeError("zoom must be a finite number");
|
|
743
|
+
}
|
|
744
|
+
if (!Number.isInteger(steps) || steps < 1) {
|
|
745
|
+
throw new RangeError("steps must be a positive integer");
|
|
746
|
+
}
|
|
747
|
+
const fraction = zoom - Math.floor(zoom);
|
|
748
|
+
const index = Math.min(steps, Math.round(fraction * steps));
|
|
749
|
+
return {
|
|
750
|
+
index,
|
|
751
|
+
pixelRatioScale: 2 ** (index / steps)
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/engine/geojsonImport.ts
|
|
756
|
+
function record(value) {
|
|
757
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
758
|
+
}
|
|
759
|
+
function position(value, path) {
|
|
760
|
+
if (!Array.isArray(value) || value.length < 2 || value.some((coordinate) => typeof coordinate !== "number" || !Number.isFinite(coordinate))) {
|
|
761
|
+
throw new TypeError(`${path} must contain finite coordinate positions`);
|
|
762
|
+
}
|
|
763
|
+
const [longitude, latitude] = value;
|
|
764
|
+
if (longitude < -180 || longitude > 180 || latitude < -90 || latitude > 90) {
|
|
765
|
+
throw new RangeError(`${path} must use WGS84 longitude and latitude`);
|
|
766
|
+
}
|
|
767
|
+
return value.slice();
|
|
768
|
+
}
|
|
769
|
+
function samePosition(a, b) {
|
|
770
|
+
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
771
|
+
}
|
|
772
|
+
function ring(value, path) {
|
|
773
|
+
if (!Array.isArray(value) || value.length < 3) {
|
|
774
|
+
throw new TypeError(`${path} must contain at least three positions`);
|
|
775
|
+
}
|
|
776
|
+
const positions = value.map((item, index) => position(item, `${path}[${index}]`));
|
|
777
|
+
if (!samePosition(positions[0], positions[positions.length - 1])) {
|
|
778
|
+
positions.push(positions[0].slice());
|
|
779
|
+
}
|
|
780
|
+
const unique = new Set(
|
|
781
|
+
positions.slice(0, -1).map((item) => `${item[0]},${item[1]}`)
|
|
782
|
+
);
|
|
783
|
+
if (unique.size < 3) {
|
|
784
|
+
throw new TypeError(`${path} must contain at least three distinct positions`);
|
|
785
|
+
}
|
|
786
|
+
return positions;
|
|
787
|
+
}
|
|
788
|
+
function polygonCoordinates(value, path) {
|
|
789
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
790
|
+
throw new TypeError(`${path} must contain at least one linear ring`);
|
|
791
|
+
}
|
|
792
|
+
return value.map((item, index) => ring(item, `${path}[${index}]`));
|
|
793
|
+
}
|
|
794
|
+
function properties(value) {
|
|
795
|
+
return record(value) ? { ...value } : {};
|
|
796
|
+
}
|
|
797
|
+
function featureId(value) {
|
|
798
|
+
return typeof value === "string" || typeof value === "number" ? value : void 0;
|
|
799
|
+
}
|
|
800
|
+
function importGeoJsonPolygons(input, maximumFeatures = 48) {
|
|
801
|
+
if (!Number.isInteger(maximumFeatures) || maximumFeatures < 1) {
|
|
802
|
+
throw new RangeError("maximumFeatures must be a positive integer");
|
|
803
|
+
}
|
|
804
|
+
const result = [];
|
|
805
|
+
let ignoredFeatures = 0;
|
|
806
|
+
function appendPolygon(coordinates, sourceProperties, id, path) {
|
|
807
|
+
result.push({
|
|
808
|
+
type: "Feature",
|
|
809
|
+
...featureId(id) === void 0 ? {} : { id: featureId(id) },
|
|
810
|
+
properties: properties(sourceProperties),
|
|
811
|
+
geometry: {
|
|
812
|
+
type: "Polygon",
|
|
813
|
+
coordinates: polygonCoordinates(coordinates, path)
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
if (result.length > maximumFeatures) {
|
|
817
|
+
throw new RangeError(
|
|
818
|
+
`GeoJSON contains more than ${maximumFeatures} polygon parts`
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
function visitGeometry(geometry, sourceProperties, id, path) {
|
|
823
|
+
if (!record(geometry) || typeof geometry.type !== "string") return false;
|
|
824
|
+
if (geometry.type === "Polygon") {
|
|
825
|
+
appendPolygon(geometry.coordinates, sourceProperties, id, `${path}.coordinates`);
|
|
826
|
+
return true;
|
|
827
|
+
}
|
|
828
|
+
if (geometry.type === "MultiPolygon") {
|
|
829
|
+
if (!Array.isArray(geometry.coordinates) || geometry.coordinates.length === 0) {
|
|
830
|
+
throw new TypeError(`${path}.coordinates must contain polygons`);
|
|
831
|
+
}
|
|
832
|
+
geometry.coordinates.forEach((coordinates, index) => {
|
|
833
|
+
appendPolygon(
|
|
834
|
+
coordinates,
|
|
835
|
+
sourceProperties,
|
|
836
|
+
id,
|
|
837
|
+
`${path}.coordinates[${index}]`
|
|
838
|
+
);
|
|
839
|
+
});
|
|
840
|
+
return true;
|
|
841
|
+
}
|
|
842
|
+
if (geometry.type === "GeometryCollection") {
|
|
843
|
+
if (!Array.isArray(geometry.geometries)) {
|
|
844
|
+
throw new TypeError(`${path}.geometries must be an array`);
|
|
845
|
+
}
|
|
846
|
+
let found = false;
|
|
847
|
+
geometry.geometries.forEach((item, index) => {
|
|
848
|
+
found = visitGeometry(
|
|
849
|
+
item,
|
|
850
|
+
sourceProperties,
|
|
851
|
+
id,
|
|
852
|
+
`${path}.geometries[${index}]`
|
|
853
|
+
) || found;
|
|
854
|
+
});
|
|
855
|
+
return found;
|
|
856
|
+
}
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
859
|
+
function visit(value, path) {
|
|
860
|
+
if (!record(value) || typeof value.type !== "string") {
|
|
861
|
+
throw new TypeError(`${path} must be a GeoJSON object`);
|
|
862
|
+
}
|
|
863
|
+
if (value.type === "FeatureCollection") {
|
|
864
|
+
if (!Array.isArray(value.features)) {
|
|
865
|
+
throw new TypeError(`${path}.features must be an array`);
|
|
866
|
+
}
|
|
867
|
+
value.features.forEach((item, index) => {
|
|
868
|
+
if (!record(item) || item.type !== "Feature") {
|
|
869
|
+
throw new TypeError(`${path}.features[${index}] must be a Feature`);
|
|
870
|
+
}
|
|
871
|
+
const found = visitGeometry(
|
|
872
|
+
item.geometry,
|
|
873
|
+
item.properties,
|
|
874
|
+
item.id,
|
|
875
|
+
`${path}.features[${index}].geometry`
|
|
876
|
+
);
|
|
877
|
+
if (!found) ignoredFeatures++;
|
|
878
|
+
});
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (value.type === "Feature") {
|
|
882
|
+
const found = visitGeometry(
|
|
883
|
+
value.geometry,
|
|
884
|
+
value.properties,
|
|
885
|
+
value.id,
|
|
886
|
+
`${path}.geometry`
|
|
887
|
+
);
|
|
888
|
+
if (!found) ignoredFeatures++;
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
if (!visitGeometry(value, {}, void 0, path)) ignoredFeatures++;
|
|
892
|
+
}
|
|
893
|
+
visit(input, "GeoJSON");
|
|
894
|
+
if (result.length === 0) {
|
|
895
|
+
throw new TypeError("GeoJSON does not contain Polygon or MultiPolygon geometry");
|
|
896
|
+
}
|
|
897
|
+
return { features: result, ignoredFeatures };
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// src/maplibre/patternDefinition.ts
|
|
901
|
+
var PATTERN_METADATA_KEY = "maplibre-pattern-fills:v1";
|
|
902
|
+
var geometricTypes = /* @__PURE__ */ new Set([
|
|
903
|
+
"stipple",
|
|
904
|
+
"hachures",
|
|
905
|
+
"cross",
|
|
906
|
+
"grid",
|
|
907
|
+
"dots"
|
|
908
|
+
]);
|
|
909
|
+
function isRecord(value) {
|
|
910
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
911
|
+
}
|
|
912
|
+
function finiteNumber(record2, key) {
|
|
913
|
+
const value = record2[key];
|
|
914
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
915
|
+
throw new TypeError(`${key} must be a finite number`);
|
|
916
|
+
}
|
|
917
|
+
return value;
|
|
918
|
+
}
|
|
919
|
+
function positiveNumber(record2, key) {
|
|
920
|
+
const value = finiteNumber(record2, key);
|
|
921
|
+
if (value <= 0) throw new RangeError(`${key} must be greater than 0`);
|
|
922
|
+
return value;
|
|
923
|
+
}
|
|
924
|
+
function nonNegativeNumber(record2, key) {
|
|
925
|
+
const value = finiteNumber(record2, key);
|
|
926
|
+
if (value < 0) throw new RangeError(`${key} must be greater than or equal to 0`);
|
|
927
|
+
return value;
|
|
928
|
+
}
|
|
929
|
+
function nonEmptyString(record2, key) {
|
|
930
|
+
const value = record2[key];
|
|
931
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
932
|
+
throw new TypeError(`${key} must be a non-empty string`);
|
|
933
|
+
}
|
|
934
|
+
return value;
|
|
935
|
+
}
|
|
936
|
+
function parsePatternDefinition(value) {
|
|
937
|
+
if (!isRecord(value)) throw new TypeError("pattern definition must be an object");
|
|
938
|
+
if (value.kind === "geometric") {
|
|
939
|
+
if (typeof value.pattern !== "string" || !geometricTypes.has(value.pattern)) {
|
|
940
|
+
throw new TypeError("pattern must be a supported non-solid geometric pattern");
|
|
941
|
+
}
|
|
942
|
+
const definition = {
|
|
943
|
+
kind: "geometric",
|
|
944
|
+
pattern: value.pattern,
|
|
945
|
+
size: positiveNumber(value, "size"),
|
|
946
|
+
color: nonEmptyString(value, "color"),
|
|
947
|
+
weight: positiveNumber(value, "weight"),
|
|
948
|
+
angle: finiteNumber(value, "angle")
|
|
949
|
+
};
|
|
950
|
+
if (value.pixelRatio !== void 0) {
|
|
951
|
+
definition.pixelRatio = positiveNumber(value, "pixelRatio");
|
|
952
|
+
}
|
|
953
|
+
if (value.stippleCount !== void 0) {
|
|
954
|
+
const stippleCount = positiveNumber(value, "stippleCount");
|
|
955
|
+
if (!Number.isInteger(stippleCount)) {
|
|
956
|
+
throw new RangeError("stippleCount must be a positive integer");
|
|
957
|
+
}
|
|
958
|
+
definition.stippleCount = stippleCount;
|
|
959
|
+
}
|
|
960
|
+
return definition;
|
|
961
|
+
}
|
|
962
|
+
if (value.kind === "svg") {
|
|
963
|
+
const scaleJitter = nonNegativeNumber(value, "scaleJitter");
|
|
964
|
+
if (scaleJitter >= 1) throw new RangeError("scaleJitter must be less than 1");
|
|
965
|
+
const seed = value.seed;
|
|
966
|
+
if ((typeof seed !== "string" || seed.length === 0) && (typeof seed !== "number" || !Number.isFinite(seed))) {
|
|
967
|
+
throw new TypeError("seed must be a finite number or a non-empty string");
|
|
968
|
+
}
|
|
969
|
+
if (typeof value.stagger !== "boolean") throw new TypeError("stagger must be a boolean");
|
|
970
|
+
const distribution = value.distribution ?? (value.stagger ? "offset" : "regular");
|
|
971
|
+
if (distribution !== "regular" && distribution !== "offset" && distribution !== "natural") {
|
|
972
|
+
throw new TypeError("distribution must be regular, offset, or natural");
|
|
973
|
+
}
|
|
974
|
+
return {
|
|
975
|
+
kind: "svg",
|
|
976
|
+
svg: nonEmptyString(value, "svg"),
|
|
977
|
+
tileSize: positiveNumber(value, "tileSize"),
|
|
978
|
+
stampSize: positiveNumber(value, "stampSize"),
|
|
979
|
+
density: positiveNumber(value, "density"),
|
|
980
|
+
seed,
|
|
981
|
+
rotationJitterDeg: nonNegativeNumber(value, "rotationJitterDeg"),
|
|
982
|
+
scaleJitter,
|
|
983
|
+
positionJitter: nonNegativeNumber(value, "positionJitter"),
|
|
984
|
+
stagger: distribution === "offset",
|
|
985
|
+
distribution,
|
|
986
|
+
minSpacing: value.minSpacing === void 0 ? 0 : nonNegativeNumber(value, "minSpacing")
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
if (value.kind === "font") {
|
|
990
|
+
const fontStyle = value.fontStyle;
|
|
991
|
+
if (fontStyle !== "normal" && fontStyle !== "italic") {
|
|
992
|
+
throw new TypeError("fontStyle must be normal or italic");
|
|
993
|
+
}
|
|
994
|
+
if (typeof value.stagger !== "boolean") throw new TypeError("stagger must be a boolean");
|
|
995
|
+
return {
|
|
996
|
+
kind: "font",
|
|
997
|
+
text: nonEmptyString(value, "text"),
|
|
998
|
+
fontFamily: nonEmptyString(value, "fontFamily"),
|
|
999
|
+
fontSize: positiveNumber(value, "fontSize"),
|
|
1000
|
+
fontWeight: nonEmptyString(value, "fontWeight"),
|
|
1001
|
+
fontStyle,
|
|
1002
|
+
letterSpacing: finiteNumber(value, "letterSpacing"),
|
|
1003
|
+
horizontalSpacing: nonNegativeNumber(value, "horizontalSpacing"),
|
|
1004
|
+
verticalSpacing: nonNegativeNumber(value, "verticalSpacing"),
|
|
1005
|
+
rotationDeg: finiteNumber(value, "rotationDeg"),
|
|
1006
|
+
stagger: value.stagger,
|
|
1007
|
+
color: nonEmptyString(value, "color")
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
throw new TypeError('pattern definition kind must be "geometric", "svg", or "font"');
|
|
1011
|
+
}
|
|
1012
|
+
function parsePatternMetadata(value) {
|
|
1013
|
+
if (!isRecord(value)) throw new TypeError("pattern metadata must be an object");
|
|
1014
|
+
const metadata = {
|
|
1015
|
+
imageId: nonEmptyString(value, "imageId"),
|
|
1016
|
+
definition: parsePatternDefinition(value.definition)
|
|
1017
|
+
};
|
|
1018
|
+
if (value.variants !== void 0) {
|
|
1019
|
+
if (!Array.isArray(value.variants) || value.variants.length === 0) {
|
|
1020
|
+
throw new TypeError("variants must be a non-empty array");
|
|
1021
|
+
}
|
|
1022
|
+
metadata.variants = value.variants.map((variant, index) => {
|
|
1023
|
+
if (!isRecord(variant)) {
|
|
1024
|
+
throw new TypeError(`variants[${index}] must be an object`);
|
|
1025
|
+
}
|
|
1026
|
+
return {
|
|
1027
|
+
zoom: finiteNumber(variant, "zoom"),
|
|
1028
|
+
imageId: nonEmptyString(variant, "imageId"),
|
|
1029
|
+
definition: parsePatternDefinition(variant.definition)
|
|
1030
|
+
};
|
|
1031
|
+
});
|
|
1032
|
+
for (let index = 1; index < metadata.variants.length; index++) {
|
|
1033
|
+
if (metadata.variants[index].zoom <= metadata.variants[index - 1].zoom) {
|
|
1034
|
+
throw new RangeError("variant zoom levels must be strictly increasing");
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
return metadata;
|
|
1039
|
+
}
|
|
1040
|
+
function serializePatternDefinition(definition) {
|
|
1041
|
+
return JSON.stringify(parsePatternDefinition(definition));
|
|
1042
|
+
}
|
|
1043
|
+
function patternDefinitionId(definition, prefix = "mpf") {
|
|
1044
|
+
const serialized = serializePatternDefinition(definition);
|
|
1045
|
+
let hash = 2166136261;
|
|
1046
|
+
for (let index = 0; index < serialized.length; index++) {
|
|
1047
|
+
hash ^= serialized.charCodeAt(index);
|
|
1048
|
+
hash = Math.imul(hash, 16777619);
|
|
1049
|
+
}
|
|
1050
|
+
return `${prefix}_${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
1051
|
+
}
|
|
1052
|
+
function createSvgPatternDefinition(options) {
|
|
1053
|
+
const distribution = options.distribution ?? (options.stagger === false ? "regular" : "offset");
|
|
1054
|
+
return parsePatternDefinition({
|
|
1055
|
+
kind: "svg",
|
|
1056
|
+
svg: options.svg,
|
|
1057
|
+
tileSize: options.tileSize ?? 288,
|
|
1058
|
+
stampSize: options.stampSize ?? 28,
|
|
1059
|
+
density: options.density ?? 1.4,
|
|
1060
|
+
seed: options.seed ?? 1,
|
|
1061
|
+
rotationJitterDeg: options.rotationJitterDeg ?? 0,
|
|
1062
|
+
scaleJitter: options.scaleJitter ?? 0,
|
|
1063
|
+
positionJitter: options.positionJitter ?? 0.15,
|
|
1064
|
+
stagger: distribution === "offset",
|
|
1065
|
+
distribution,
|
|
1066
|
+
minSpacing: options.minSpacing ?? 0
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
function createFontPatternDefinition(options) {
|
|
1070
|
+
return parsePatternDefinition({
|
|
1071
|
+
kind: "font",
|
|
1072
|
+
text: options.text,
|
|
1073
|
+
fontFamily: options.fontFamily ?? "sans-serif",
|
|
1074
|
+
fontSize: options.fontSize ?? 18,
|
|
1075
|
+
fontWeight: options.fontWeight ?? "500",
|
|
1076
|
+
fontStyle: options.fontStyle ?? "normal",
|
|
1077
|
+
letterSpacing: options.letterSpacing ?? 0,
|
|
1078
|
+
horizontalSpacing: options.horizontalSpacing ?? 26,
|
|
1079
|
+
verticalSpacing: options.verticalSpacing ?? 22,
|
|
1080
|
+
rotationDeg: options.rotationDeg ?? 0,
|
|
1081
|
+
stagger: options.stagger ?? true,
|
|
1082
|
+
color: options.color ?? "#000000"
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// src/maplibre/syncPatternTexture.ts
|
|
1087
|
+
var installedPatterns = /* @__PURE__ */ new WeakMap();
|
|
1088
|
+
function resolvePixelRatio(value) {
|
|
1089
|
+
const displayRatio = typeof globalThis.devicePixelRatio === "number" ? globalThis.devicePixelRatio : 1;
|
|
1090
|
+
const ratio = value ?? Math.min(Math.max(displayRatio, 1), 2);
|
|
1091
|
+
if (!Number.isFinite(ratio) || ratio <= 0) {
|
|
1092
|
+
throw new RangeError("pixelRatio must be a finite number greater than 0");
|
|
1093
|
+
}
|
|
1094
|
+
return ratio;
|
|
1095
|
+
}
|
|
1096
|
+
function syncPatternTexture(map, options) {
|
|
1097
|
+
const { imageId, pattern, size, color, weight, angle, stippleCount } = options;
|
|
1098
|
+
if (pattern === "solid") return;
|
|
1099
|
+
const pixelRatio = resolvePixelRatio(options.pixelRatio);
|
|
1100
|
+
let byImage = installedPatterns.get(map);
|
|
1101
|
+
if (!byImage) {
|
|
1102
|
+
byImage = /* @__PURE__ */ new Map();
|
|
1103
|
+
installedPatterns.set(map, byImage);
|
|
1104
|
+
}
|
|
1105
|
+
const signature = JSON.stringify({ pattern, size, color, weight, angle, pixelRatio, stippleCount });
|
|
1106
|
+
const previous = byImage.get(imageId);
|
|
1107
|
+
if (map.hasImage(imageId) && previous?.signature === signature) return;
|
|
1108
|
+
const tile = makeTile(pattern, size, color, weight, angle, { pixelRatio, stippleCount });
|
|
1109
|
+
if (map.hasImage(imageId) && previous?.width === tile.width && previous.height === tile.height && previous.pixelRatio === pixelRatio) {
|
|
1110
|
+
map.updateImage(imageId, tile);
|
|
1111
|
+
} else {
|
|
1112
|
+
if (map.hasImage(imageId)) map.removeImage(imageId);
|
|
1113
|
+
map.addImage(imageId, tile, { pixelRatio });
|
|
1114
|
+
}
|
|
1115
|
+
byImage.set(imageId, {
|
|
1116
|
+
signature,
|
|
1117
|
+
width: tile.width,
|
|
1118
|
+
height: tile.height,
|
|
1119
|
+
pixelRatio
|
|
1120
|
+
});
|
|
1121
|
+
map.triggerRepaint();
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// src/maplibre/buildStyleFragment.ts
|
|
1125
|
+
function buildStyleFragment(options) {
|
|
1126
|
+
const { source, sourceLayer, sourceUrl, bg, pattern, line } = options;
|
|
1127
|
+
const layers = [];
|
|
1128
|
+
if (bg?.enabled) {
|
|
1129
|
+
layers.push({
|
|
1130
|
+
id: `${source}__bg`,
|
|
1131
|
+
type: "fill",
|
|
1132
|
+
source,
|
|
1133
|
+
"source-layer": sourceLayer,
|
|
1134
|
+
paint: { "fill-color": bg.color, "fill-opacity": bg.opacity }
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
if (pattern.pattern === "solid") {
|
|
1138
|
+
layers.push({
|
|
1139
|
+
id: `${source}__pat`,
|
|
1140
|
+
type: "fill",
|
|
1141
|
+
source,
|
|
1142
|
+
"source-layer": sourceLayer,
|
|
1143
|
+
paint: { "fill-color": pattern.color, "fill-opacity": pattern.opacity }
|
|
1144
|
+
});
|
|
1145
|
+
} else {
|
|
1146
|
+
const definition = {
|
|
1147
|
+
kind: "geometric",
|
|
1148
|
+
pattern: pattern.pattern,
|
|
1149
|
+
size: pattern.tile,
|
|
1150
|
+
color: pattern.color,
|
|
1151
|
+
weight: pattern.weight,
|
|
1152
|
+
angle: pattern.angle,
|
|
1153
|
+
...pattern.pixelRatio === void 0 ? {} : { pixelRatio: pattern.pixelRatio },
|
|
1154
|
+
...pattern.stippleCount === void 0 ? {} : { stippleCount: pattern.stippleCount }
|
|
1155
|
+
};
|
|
1156
|
+
const imageId = patternDefinitionId(definition);
|
|
1157
|
+
layers.push({
|
|
1158
|
+
id: `${source}__pat`,
|
|
1159
|
+
type: "fill",
|
|
1160
|
+
source,
|
|
1161
|
+
"source-layer": sourceLayer,
|
|
1162
|
+
paint: { "fill-pattern": imageId, "fill-opacity": pattern.opacity },
|
|
1163
|
+
metadata: {
|
|
1164
|
+
[PATTERN_METADATA_KEY]: { imageId, definition }
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
if (line?.enabled) {
|
|
1169
|
+
layers.push({
|
|
1170
|
+
id: `${source}__line`,
|
|
1171
|
+
type: "line",
|
|
1172
|
+
source,
|
|
1173
|
+
"source-layer": sourceLayer,
|
|
1174
|
+
paint: {
|
|
1175
|
+
"line-color": line.color,
|
|
1176
|
+
"line-width": line.width,
|
|
1177
|
+
...line.dash.length ? { "line-dasharray": line.dash } : {}
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
return {
|
|
1182
|
+
sources: { [source]: { type: "vector", url: sourceUrl ?? `<url>/${source}` } },
|
|
1183
|
+
layers
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// src/maplibre/loadSvgImage.ts
|
|
1188
|
+
async function loadSvgImage(svg) {
|
|
1189
|
+
const blob = new Blob([svg], { type: "image/svg+xml" });
|
|
1190
|
+
const url = URL.createObjectURL(blob);
|
|
1191
|
+
try {
|
|
1192
|
+
const img = new Image();
|
|
1193
|
+
img.src = url;
|
|
1194
|
+
await img.decode();
|
|
1195
|
+
return img;
|
|
1196
|
+
} finally {
|
|
1197
|
+
URL.revokeObjectURL(url);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// src/maplibre/svgPattern.ts
|
|
1202
|
+
function resolvePixelRatio2(value, useDisplayRatio) {
|
|
1203
|
+
const displayRatio = useDisplayRatio && typeof globalThis.devicePixelRatio === "number" ? globalThis.devicePixelRatio : 1;
|
|
1204
|
+
const ratio = value ?? Math.min(Math.max(displayRatio, 1), 2);
|
|
1205
|
+
if (!Number.isFinite(ratio) || ratio <= 0) {
|
|
1206
|
+
throw new RangeError("pixelRatio must be a finite number greater than 0");
|
|
1207
|
+
}
|
|
1208
|
+
return ratio;
|
|
1209
|
+
}
|
|
1210
|
+
async function createSvgScatterTile(options) {
|
|
1211
|
+
const definition = createSvgPatternDefinition(options);
|
|
1212
|
+
const pixelRatio = resolvePixelRatio2(options.pixelRatio, false);
|
|
1213
|
+
const {
|
|
1214
|
+
svg,
|
|
1215
|
+
tileSize,
|
|
1216
|
+
stampSize,
|
|
1217
|
+
density,
|
|
1218
|
+
seed,
|
|
1219
|
+
rotationJitterDeg,
|
|
1220
|
+
scaleJitter,
|
|
1221
|
+
positionJitter,
|
|
1222
|
+
stagger,
|
|
1223
|
+
distribution,
|
|
1224
|
+
minSpacing
|
|
1225
|
+
} = definition;
|
|
1226
|
+
const image = await loadSvgImage(svg);
|
|
1227
|
+
const canvas = document.createElement("canvas");
|
|
1228
|
+
const physicalTileSize = Math.max(1, Math.round(tileSize * pixelRatio));
|
|
1229
|
+
const renderScale = physicalTileSize / tileSize;
|
|
1230
|
+
canvas.width = canvas.height = physicalTileSize;
|
|
1231
|
+
const ctx = canvas.getContext("2d");
|
|
1232
|
+
if (!ctx) throw new Error("2D canvas context unavailable");
|
|
1233
|
+
ctx.clearRect(0, 0, physicalTileSize, physicalTileSize);
|
|
1234
|
+
ctx.scale(renderScale, renderScale);
|
|
1235
|
+
const placements = createSvgScatterLayout({
|
|
1236
|
+
tileSize,
|
|
1237
|
+
stampSize,
|
|
1238
|
+
density,
|
|
1239
|
+
seed,
|
|
1240
|
+
rotationJitterDeg,
|
|
1241
|
+
scaleJitter,
|
|
1242
|
+
positionJitter,
|
|
1243
|
+
stagger,
|
|
1244
|
+
distribution,
|
|
1245
|
+
minSpacing
|
|
1246
|
+
});
|
|
1247
|
+
for (const placement of placements) {
|
|
1248
|
+
ctx.save();
|
|
1249
|
+
ctx.translate(placement.x, placement.y);
|
|
1250
|
+
ctx.rotate(placement.rotationRad);
|
|
1251
|
+
ctx.scale(placement.scale, placement.scale);
|
|
1252
|
+
ctx.drawImage(image, -stampSize / 2, -stampSize / 2, stampSize, stampSize);
|
|
1253
|
+
ctx.restore();
|
|
1254
|
+
}
|
|
1255
|
+
const imageData = ctx.getImageData(0, 0, physicalTileSize, physicalTileSize);
|
|
1256
|
+
return {
|
|
1257
|
+
width: physicalTileSize,
|
|
1258
|
+
height: physicalTileSize,
|
|
1259
|
+
data: new Uint8Array(imageData.data.buffer)
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
var installedSvgPatterns = /* @__PURE__ */ new WeakMap();
|
|
1263
|
+
var svgPatternGenerations = /* @__PURE__ */ new WeakMap();
|
|
1264
|
+
async function installSvgPatternFill(map, options) {
|
|
1265
|
+
const definition = createSvgPatternDefinition(options);
|
|
1266
|
+
const pixelRatio = resolvePixelRatio2(options.pixelRatio, true);
|
|
1267
|
+
const signature = `${serializePatternDefinition(definition)}@${pixelRatio}`;
|
|
1268
|
+
let installedById = installedSvgPatterns.get(map);
|
|
1269
|
+
if (!installedById) {
|
|
1270
|
+
installedById = /* @__PURE__ */ new Map();
|
|
1271
|
+
installedSvgPatterns.set(map, installedById);
|
|
1272
|
+
}
|
|
1273
|
+
if (map.hasImage(options.imageId) && installedById.get(options.imageId)?.signature === signature) {
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
let generationsById = svgPatternGenerations.get(map);
|
|
1277
|
+
if (!generationsById) {
|
|
1278
|
+
generationsById = /* @__PURE__ */ new Map();
|
|
1279
|
+
svgPatternGenerations.set(map, generationsById);
|
|
1280
|
+
}
|
|
1281
|
+
const generation = (generationsById.get(options.imageId) ?? 0) + 1;
|
|
1282
|
+
generationsById.set(options.imageId, generation);
|
|
1283
|
+
const tile = await createSvgScatterTile({ ...options, pixelRatio });
|
|
1284
|
+
if (generationsById.get(options.imageId) !== generation) return;
|
|
1285
|
+
const previous = installedById.get(options.imageId);
|
|
1286
|
+
if (map.hasImage(options.imageId) && previous?.width === tile.width && previous.height === tile.height && previous.pixelRatio === pixelRatio) {
|
|
1287
|
+
map.updateImage(options.imageId, tile);
|
|
1288
|
+
} else {
|
|
1289
|
+
if (map.hasImage(options.imageId)) map.removeImage(options.imageId);
|
|
1290
|
+
map.addImage(options.imageId, tile, { pixelRatio });
|
|
1291
|
+
}
|
|
1292
|
+
installedById.set(options.imageId, {
|
|
1293
|
+
signature,
|
|
1294
|
+
width: tile.width,
|
|
1295
|
+
height: tile.height,
|
|
1296
|
+
pixelRatio
|
|
1297
|
+
});
|
|
1298
|
+
map.triggerRepaint();
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// src/maplibre/fontPattern.ts
|
|
1302
|
+
function resolvePixelRatio3(value, useDisplayRatio) {
|
|
1303
|
+
const displayRatio = useDisplayRatio && typeof globalThis.devicePixelRatio === "number" ? globalThis.devicePixelRatio : 1;
|
|
1304
|
+
const ratio = value ?? Math.min(Math.max(displayRatio, 1), 2);
|
|
1305
|
+
if (!Number.isFinite(ratio) || ratio <= 0) {
|
|
1306
|
+
throw new RangeError("pixelRatio must be a finite number greater than 0");
|
|
1307
|
+
}
|
|
1308
|
+
return ratio;
|
|
1309
|
+
}
|
|
1310
|
+
function textWidth(ctx, characters, letterSpacing) {
|
|
1311
|
+
const glyphs = characters.reduce((total, character) => total + ctx.measureText(character).width, 0);
|
|
1312
|
+
return Math.max(1, glyphs + Math.max(0, characters.length - 1) * letterSpacing);
|
|
1313
|
+
}
|
|
1314
|
+
function fillSpacedText(ctx, characters, width, letterSpacing) {
|
|
1315
|
+
let x = -width / 2;
|
|
1316
|
+
for (const character of characters) {
|
|
1317
|
+
const characterWidth = ctx.measureText(character).width;
|
|
1318
|
+
ctx.fillText(character, x, 0);
|
|
1319
|
+
x += characterWidth + letterSpacing;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
async function createFontPatternTile(options) {
|
|
1323
|
+
const definition = createFontPatternDefinition(options);
|
|
1324
|
+
const pixelRatio = resolvePixelRatio3(options.pixelRatio, false);
|
|
1325
|
+
const {
|
|
1326
|
+
text,
|
|
1327
|
+
fontFamily,
|
|
1328
|
+
fontSize,
|
|
1329
|
+
fontWeight,
|
|
1330
|
+
fontStyle,
|
|
1331
|
+
letterSpacing,
|
|
1332
|
+
horizontalSpacing,
|
|
1333
|
+
verticalSpacing,
|
|
1334
|
+
rotationDeg,
|
|
1335
|
+
stagger,
|
|
1336
|
+
color
|
|
1337
|
+
} = definition;
|
|
1338
|
+
if (typeof document === "undefined") {
|
|
1339
|
+
throw new Error("Font pattern rasterization requires a browser canvas");
|
|
1340
|
+
}
|
|
1341
|
+
const font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
|
|
1342
|
+
if (document.fonts?.load) {
|
|
1343
|
+
await document.fonts.load(font, text.slice(0, 32)).catch(() => void 0);
|
|
1344
|
+
}
|
|
1345
|
+
const measureCanvas = document.createElement("canvas");
|
|
1346
|
+
const measureContext = measureCanvas.getContext("2d");
|
|
1347
|
+
if (!measureContext) throw new Error("2D canvas context unavailable");
|
|
1348
|
+
measureContext.font = font;
|
|
1349
|
+
const characters = Array.from(text);
|
|
1350
|
+
const measuredWidth = textWidth(measureContext, characters, letterSpacing);
|
|
1351
|
+
const metrics = measureContext.measureText(text);
|
|
1352
|
+
const measuredHeight = Math.max(
|
|
1353
|
+
fontSize,
|
|
1354
|
+
(metrics.actualBoundingBoxAscent || fontSize * 0.8) + (metrics.actualBoundingBoxDescent || fontSize * 0.2)
|
|
1355
|
+
);
|
|
1356
|
+
const radians = rotationDeg * Math.PI / 180;
|
|
1357
|
+
const rotatedWidth = Math.abs(measuredWidth * Math.cos(radians)) + Math.abs(measuredHeight * Math.sin(radians));
|
|
1358
|
+
const rotatedHeight = Math.abs(measuredWidth * Math.sin(radians)) + Math.abs(measuredHeight * Math.cos(radians));
|
|
1359
|
+
const cellWidth = Math.max(1, Math.ceil(rotatedWidth + horizontalSpacing));
|
|
1360
|
+
const rowHeight = Math.max(1, Math.ceil(rotatedHeight + verticalSpacing));
|
|
1361
|
+
const logicalWidth = cellWidth * (stagger ? 2 : 1);
|
|
1362
|
+
const logicalHeight = rowHeight * (stagger ? 2 : 1);
|
|
1363
|
+
const physicalWidth = Math.max(1, Math.round(logicalWidth * pixelRatio));
|
|
1364
|
+
const physicalHeight = Math.max(1, Math.round(logicalHeight * pixelRatio));
|
|
1365
|
+
const canvas = document.createElement("canvas");
|
|
1366
|
+
canvas.width = physicalWidth;
|
|
1367
|
+
canvas.height = physicalHeight;
|
|
1368
|
+
const ctx = canvas.getContext("2d");
|
|
1369
|
+
if (!ctx) throw new Error("2D canvas context unavailable");
|
|
1370
|
+
ctx.clearRect(0, 0, physicalWidth, physicalHeight);
|
|
1371
|
+
ctx.scale(physicalWidth / logicalWidth, physicalHeight / logicalHeight);
|
|
1372
|
+
ctx.font = font;
|
|
1373
|
+
ctx.textAlign = "left";
|
|
1374
|
+
ctx.textBaseline = "middle";
|
|
1375
|
+
ctx.fillStyle = color;
|
|
1376
|
+
const rowCount = stagger ? 2 : 1;
|
|
1377
|
+
const columnCount = stagger ? 2 : 1;
|
|
1378
|
+
for (let row = -2; row <= rowCount + 1; row++) {
|
|
1379
|
+
const offset = stagger && Math.abs(row % 2) === 1 ? cellWidth / 2 : 0;
|
|
1380
|
+
for (let column = -2; column <= columnCount + 1; column++) {
|
|
1381
|
+
const x = column * cellWidth + cellWidth / 2 + offset;
|
|
1382
|
+
const y = row * rowHeight + rowHeight / 2;
|
|
1383
|
+
ctx.save();
|
|
1384
|
+
ctx.translate(x, y);
|
|
1385
|
+
ctx.rotate(radians);
|
|
1386
|
+
fillSpacedText(ctx, characters, measuredWidth, letterSpacing);
|
|
1387
|
+
ctx.restore();
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
const imageData = ctx.getImageData(0, 0, physicalWidth, physicalHeight);
|
|
1391
|
+
return {
|
|
1392
|
+
width: physicalWidth,
|
|
1393
|
+
height: physicalHeight,
|
|
1394
|
+
data: new Uint8Array(imageData.data.buffer)
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
var installedFontPatterns = /* @__PURE__ */ new WeakMap();
|
|
1398
|
+
var fontPatternGenerations = /* @__PURE__ */ new WeakMap();
|
|
1399
|
+
async function installFontPatternFill(map, options) {
|
|
1400
|
+
if (typeof options.imageId !== "string" || options.imageId.trim().length === 0) {
|
|
1401
|
+
throw new TypeError("imageId must be a non-empty string");
|
|
1402
|
+
}
|
|
1403
|
+
const definition = createFontPatternDefinition(options);
|
|
1404
|
+
const pixelRatio = resolvePixelRatio3(options.pixelRatio, true);
|
|
1405
|
+
const signature = `${serializePatternDefinition(definition)}@${pixelRatio}`;
|
|
1406
|
+
let installedById = installedFontPatterns.get(map);
|
|
1407
|
+
if (!installedById) {
|
|
1408
|
+
installedById = /* @__PURE__ */ new Map();
|
|
1409
|
+
installedFontPatterns.set(map, installedById);
|
|
1410
|
+
}
|
|
1411
|
+
if (map.hasImage(options.imageId) && installedById.get(options.imageId)?.signature === signature) {
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
let generationsById = fontPatternGenerations.get(map);
|
|
1415
|
+
if (!generationsById) {
|
|
1416
|
+
generationsById = /* @__PURE__ */ new Map();
|
|
1417
|
+
fontPatternGenerations.set(map, generationsById);
|
|
1418
|
+
}
|
|
1419
|
+
const generation = (generationsById.get(options.imageId) ?? 0) + 1;
|
|
1420
|
+
generationsById.set(options.imageId, generation);
|
|
1421
|
+
const tile = await createFontPatternTile({ ...options, pixelRatio });
|
|
1422
|
+
if (generationsById.get(options.imageId) !== generation) return;
|
|
1423
|
+
const previous = installedById.get(options.imageId);
|
|
1424
|
+
if (map.hasImage(options.imageId) && previous?.width === tile.width && previous.height === tile.height && previous.pixelRatio === pixelRatio) {
|
|
1425
|
+
map.updateImage(options.imageId, tile);
|
|
1426
|
+
} else {
|
|
1427
|
+
if (map.hasImage(options.imageId)) map.removeImage(options.imageId);
|
|
1428
|
+
map.addImage(options.imageId, tile, { pixelRatio });
|
|
1429
|
+
}
|
|
1430
|
+
installedById.set(options.imageId, {
|
|
1431
|
+
signature,
|
|
1432
|
+
width: tile.width,
|
|
1433
|
+
height: tile.height,
|
|
1434
|
+
pixelRatio
|
|
1435
|
+
});
|
|
1436
|
+
map.triggerRepaint();
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// src/maplibre/installPatternFills.ts
|
|
1440
|
+
async function installPatternFills(map, style) {
|
|
1441
|
+
const installed = /* @__PURE__ */ new Map();
|
|
1442
|
+
const pending = [];
|
|
1443
|
+
for (const layer of style.layers) {
|
|
1444
|
+
const metadata = layer.metadata;
|
|
1445
|
+
if (!metadata) continue;
|
|
1446
|
+
const rawDefinition = metadata[PATTERN_METADATA_KEY];
|
|
1447
|
+
if (!rawDefinition) continue;
|
|
1448
|
+
const registration = parsePatternMetadata(rawDefinition);
|
|
1449
|
+
const definitions = [
|
|
1450
|
+
{ imageId: registration.imageId, definition: registration.definition },
|
|
1451
|
+
...registration.variants ?? []
|
|
1452
|
+
];
|
|
1453
|
+
for (const item of definitions) {
|
|
1454
|
+
const signature = serializePatternDefinition(item.definition);
|
|
1455
|
+
const previous = installed.get(item.imageId);
|
|
1456
|
+
if (previous && previous !== signature) {
|
|
1457
|
+
throw new Error(`Conflicting pattern definitions use image id "${item.imageId}"`);
|
|
1458
|
+
}
|
|
1459
|
+
if (previous) continue;
|
|
1460
|
+
installed.set(item.imageId, signature);
|
|
1461
|
+
const p = item.definition;
|
|
1462
|
+
if (p.kind === "geometric") {
|
|
1463
|
+
syncPatternTexture(map, {
|
|
1464
|
+
imageId: item.imageId,
|
|
1465
|
+
pattern: p.pattern,
|
|
1466
|
+
size: p.size,
|
|
1467
|
+
color: p.color,
|
|
1468
|
+
weight: p.weight,
|
|
1469
|
+
angle: p.angle,
|
|
1470
|
+
pixelRatio: p.pixelRatio,
|
|
1471
|
+
stippleCount: p.stippleCount
|
|
1472
|
+
});
|
|
1473
|
+
} else if (p.kind === "svg") {
|
|
1474
|
+
pending.push(installSvgPatternFill(map, { imageId: item.imageId, ...p }));
|
|
1475
|
+
} else {
|
|
1476
|
+
pending.push(installFontPatternFill(map, { imageId: item.imageId, ...p }));
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
await Promise.all(pending);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// src/maplibre/observePatternFills.ts
|
|
1484
|
+
function observePatternFills(map, options = {}) {
|
|
1485
|
+
const getStyle = options.getStyle ?? (() => map.getStyle());
|
|
1486
|
+
const onError = options.onError ?? ((error) => {
|
|
1487
|
+
console.error("stipple-maplibre: failed to restore pattern images", error);
|
|
1488
|
+
});
|
|
1489
|
+
let disposed = false;
|
|
1490
|
+
const refresh = async () => {
|
|
1491
|
+
if (disposed) return;
|
|
1492
|
+
await installPatternFills(map, getStyle());
|
|
1493
|
+
};
|
|
1494
|
+
const onStyleLoad = () => {
|
|
1495
|
+
void refresh().catch(onError);
|
|
1496
|
+
};
|
|
1497
|
+
map.on("style.load", onStyleLoad);
|
|
1498
|
+
return {
|
|
1499
|
+
refresh,
|
|
1500
|
+
dispose() {
|
|
1501
|
+
if (disposed) return;
|
|
1502
|
+
disposed = true;
|
|
1503
|
+
map.off("style.load", onStyleLoad);
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// src/maplibre/scatterIconPoints.ts
|
|
1509
|
+
function toPolygons(polygon) {
|
|
1510
|
+
return polygon.type === "Polygon" ? [polygon.coordinates] : polygon.coordinates;
|
|
1511
|
+
}
|
|
1512
|
+
function componentSeed(seed, index) {
|
|
1513
|
+
const base = seed ?? 1;
|
|
1514
|
+
return typeof base === "string" ? `${base}:${index}` : base + Math.imul(index, 2654435761);
|
|
1515
|
+
}
|
|
1516
|
+
function scatterIconPoints(options) {
|
|
1517
|
+
const {
|
|
1518
|
+
map,
|
|
1519
|
+
polygon,
|
|
1520
|
+
iconRadiusPx,
|
|
1521
|
+
density,
|
|
1522
|
+
seed,
|
|
1523
|
+
rotationJitterDeg,
|
|
1524
|
+
scaleJitter,
|
|
1525
|
+
positionJitter,
|
|
1526
|
+
stagger,
|
|
1527
|
+
distribution,
|
|
1528
|
+
minSpacing,
|
|
1529
|
+
viewportPaddingPx
|
|
1530
|
+
} = options;
|
|
1531
|
+
let clipBounds;
|
|
1532
|
+
if (viewportPaddingPx !== void 0) {
|
|
1533
|
+
if (!Number.isFinite(viewportPaddingPx) || viewportPaddingPx < 0) {
|
|
1534
|
+
throw new RangeError("viewportPaddingPx must be a finite number greater than or equal to 0");
|
|
1535
|
+
}
|
|
1536
|
+
const canvas = map.getCanvas?.();
|
|
1537
|
+
if (canvas) {
|
|
1538
|
+
const pixelRatio = globalThis.devicePixelRatio || 1;
|
|
1539
|
+
const width = canvas.clientWidth || canvas.width / pixelRatio;
|
|
1540
|
+
const height = canvas.clientHeight || canvas.height / pixelRatio;
|
|
1541
|
+
clipBounds = {
|
|
1542
|
+
minX: -viewportPaddingPx,
|
|
1543
|
+
minY: -viewportPaddingPx,
|
|
1544
|
+
maxX: width + viewportPaddingPx,
|
|
1545
|
+
maxY: height + viewportPaddingPx
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
const points = toPolygons(polygon).flatMap((polygonRings, polygonIndex) => {
|
|
1550
|
+
const pixelRings = polygonRings.map(
|
|
1551
|
+
(ring2) => ring2.map(([lng, lat]) => {
|
|
1552
|
+
const p = map.project([lng, lat]);
|
|
1553
|
+
return [p.x, p.y];
|
|
1554
|
+
})
|
|
1555
|
+
);
|
|
1556
|
+
return scatterPointsInPolygon(pixelRings, {
|
|
1557
|
+
radius: iconRadiusPx,
|
|
1558
|
+
density,
|
|
1559
|
+
seed: componentSeed(seed, polygonIndex),
|
|
1560
|
+
rotationJitterDeg,
|
|
1561
|
+
scaleJitter,
|
|
1562
|
+
positionJitter,
|
|
1563
|
+
stagger,
|
|
1564
|
+
distribution,
|
|
1565
|
+
minSpacing,
|
|
1566
|
+
clipBounds
|
|
1567
|
+
});
|
|
1568
|
+
});
|
|
1569
|
+
const features = points.map(({ x, y, rotation, scale }, index) => {
|
|
1570
|
+
const lngLat = map.unproject([x, y]);
|
|
1571
|
+
return {
|
|
1572
|
+
type: "Feature",
|
|
1573
|
+
id: index,
|
|
1574
|
+
geometry: { type: "Point", coordinates: [lngLat.lng, lngLat.lat] },
|
|
1575
|
+
properties: { rotation, scale }
|
|
1576
|
+
};
|
|
1577
|
+
});
|
|
1578
|
+
return { type: "FeatureCollection", features };
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// src/maplibre/svgIcon.ts
|
|
1582
|
+
var installedSvgIcons = /* @__PURE__ */ new WeakMap();
|
|
1583
|
+
async function addSvgIcon(map, options) {
|
|
1584
|
+
const { id, svg, size = 32, pixelRatio = 2 } = options;
|
|
1585
|
+
const px = Math.round(size * pixelRatio);
|
|
1586
|
+
const signature = `${size}@${pixelRatio}:${svg}`;
|
|
1587
|
+
let installedById = installedSvgIcons.get(map);
|
|
1588
|
+
if (!installedById) {
|
|
1589
|
+
installedById = /* @__PURE__ */ new Map();
|
|
1590
|
+
installedSvgIcons.set(map, installedById);
|
|
1591
|
+
}
|
|
1592
|
+
const previous = installedById.get(id);
|
|
1593
|
+
if (map.hasImage(id) && previous?.signature === signature) {
|
|
1594
|
+
return { id, width: previous.width, height: previous.height };
|
|
1595
|
+
}
|
|
1596
|
+
const image = await loadSvgImage(svg);
|
|
1597
|
+
const canvas = document.createElement("canvas");
|
|
1598
|
+
canvas.width = canvas.height = px;
|
|
1599
|
+
const ctx = canvas.getContext("2d");
|
|
1600
|
+
if (!ctx) throw new Error("2D canvas context unavailable");
|
|
1601
|
+
ctx.clearRect(0, 0, px, px);
|
|
1602
|
+
ctx.drawImage(image, 0, 0, px, px);
|
|
1603
|
+
const imageData = ctx.getImageData(0, 0, px, px);
|
|
1604
|
+
const tile = { width: px, height: px, data: new Uint8Array(imageData.data.buffer) };
|
|
1605
|
+
if (map.hasImage(id) && previous?.width === px && previous.height === px && previous.pixelRatio === pixelRatio) {
|
|
1606
|
+
map.updateImage(id, tile);
|
|
1607
|
+
} else {
|
|
1608
|
+
if (map.hasImage(id)) map.removeImage(id);
|
|
1609
|
+
map.addImage(id, tile, { pixelRatio });
|
|
1610
|
+
}
|
|
1611
|
+
installedById.set(id, {
|
|
1612
|
+
signature,
|
|
1613
|
+
width: px,
|
|
1614
|
+
height: px,
|
|
1615
|
+
pixelRatio
|
|
1616
|
+
});
|
|
1617
|
+
return { id, width: px, height: px };
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// src/maplibre/svgIconScatter.ts
|
|
1621
|
+
var installedScatterFeatures = /* @__PURE__ */ new WeakMap();
|
|
1622
|
+
var installedScatterLayerStates = /* @__PURE__ */ new WeakMap();
|
|
1623
|
+
function featureDiff(previous, next) {
|
|
1624
|
+
const [previousLng, previousLat] = previous.geometry.coordinates;
|
|
1625
|
+
const [nextLng, nextLat] = next.geometry.coordinates;
|
|
1626
|
+
const geometryChanged = previousLng !== nextLng || previousLat !== nextLat;
|
|
1627
|
+
const propertiesChanged = previous.properties.rotation !== next.properties.rotation || previous.properties.scale !== next.properties.scale;
|
|
1628
|
+
if (!geometryChanged && !propertiesChanged) return void 0;
|
|
1629
|
+
return {
|
|
1630
|
+
id: next.id,
|
|
1631
|
+
...geometryChanged ? { newGeometry: next.geometry } : {},
|
|
1632
|
+
...propertiesChanged ? {
|
|
1633
|
+
addOrUpdateProperties: [
|
|
1634
|
+
{ key: "rotation", value: next.properties.rotation },
|
|
1635
|
+
{ key: "scale", value: next.properties.scale }
|
|
1636
|
+
]
|
|
1637
|
+
} : {}
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
function scatterSourceDiff(previous, next) {
|
|
1641
|
+
const sharedCount = Math.min(previous.length, next.length);
|
|
1642
|
+
const update = [];
|
|
1643
|
+
for (let index = 0; index < sharedCount; index++) {
|
|
1644
|
+
const diff = featureDiff(previous[index], next[index]);
|
|
1645
|
+
if (diff) update.push(diff);
|
|
1646
|
+
}
|
|
1647
|
+
return {
|
|
1648
|
+
...previous.length > next.length ? { remove: previous.slice(next.length).map(({ id }) => id) } : {},
|
|
1649
|
+
...next.length > previous.length ? { add: next.slice(previous.length) } : {},
|
|
1650
|
+
...update.length ? { update } : {}
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
async function updateScatterSource(map, sourceId, points) {
|
|
1654
|
+
let featuresBySource = installedScatterFeatures.get(map);
|
|
1655
|
+
if (!featuresBySource) {
|
|
1656
|
+
featuresBySource = /* @__PURE__ */ new Map();
|
|
1657
|
+
installedScatterFeatures.set(map, featuresBySource);
|
|
1658
|
+
}
|
|
1659
|
+
const previous = featuresBySource.get(sourceId);
|
|
1660
|
+
featuresBySource.set(sourceId, points.features);
|
|
1661
|
+
const existingSource = map.getSource(sourceId);
|
|
1662
|
+
if (!existingSource) {
|
|
1663
|
+
map.addSource(sourceId, {
|
|
1664
|
+
type: "geojson",
|
|
1665
|
+
data: points,
|
|
1666
|
+
// Point geometry does not need high-zoom vector-tile subdivision.
|
|
1667
|
+
// Overscaling above z14 preserves positions while reducing rebuild work.
|
|
1668
|
+
maxzoom: 14
|
|
1669
|
+
});
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
if (previous && typeof existingSource.updateData === "function") {
|
|
1673
|
+
const diff = scatterSourceDiff(previous, points.features);
|
|
1674
|
+
if (!diff.add && !diff.remove && !diff.update) return;
|
|
1675
|
+
try {
|
|
1676
|
+
await existingSource.updateData(diff);
|
|
1677
|
+
return;
|
|
1678
|
+
} catch {
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
existingSource.setData(points);
|
|
1682
|
+
}
|
|
1683
|
+
async function installSvgIconScatter(map, options) {
|
|
1684
|
+
const {
|
|
1685
|
+
sourceId,
|
|
1686
|
+
layerId,
|
|
1687
|
+
iconId,
|
|
1688
|
+
polygon,
|
|
1689
|
+
svg,
|
|
1690
|
+
size = 32,
|
|
1691
|
+
density,
|
|
1692
|
+
seed,
|
|
1693
|
+
rotationJitterDeg,
|
|
1694
|
+
scaleJitter,
|
|
1695
|
+
positionJitter,
|
|
1696
|
+
stagger,
|
|
1697
|
+
distribution,
|
|
1698
|
+
minSpacing,
|
|
1699
|
+
opacity = 1,
|
|
1700
|
+
scaleMode = "screen",
|
|
1701
|
+
edgeClearance = true,
|
|
1702
|
+
viewportPaddingPx
|
|
1703
|
+
} = options;
|
|
1704
|
+
if (scaleMode !== "screen" && scaleMode !== "map") {
|
|
1705
|
+
throw new TypeError("scaleMode must be screen or map");
|
|
1706
|
+
}
|
|
1707
|
+
await addSvgIcon(map, { id: iconId, svg, size });
|
|
1708
|
+
const approximateSpacing = density && density > 0 ? 100 / Math.sqrt(density) : 100;
|
|
1709
|
+
const screenPadding = viewportPaddingPx ?? Math.max(
|
|
1710
|
+
192,
|
|
1711
|
+
size * (1 + (scaleJitter ?? 0)) * 2,
|
|
1712
|
+
approximateSpacing * 2
|
|
1713
|
+
);
|
|
1714
|
+
const points = scatterIconPoints({
|
|
1715
|
+
map,
|
|
1716
|
+
polygon,
|
|
1717
|
+
iconRadiusPx: edgeClearance ? size / Math.SQRT2 : 0,
|
|
1718
|
+
density,
|
|
1719
|
+
seed,
|
|
1720
|
+
rotationJitterDeg,
|
|
1721
|
+
scaleJitter,
|
|
1722
|
+
positionJitter,
|
|
1723
|
+
stagger,
|
|
1724
|
+
distribution,
|
|
1725
|
+
minSpacing,
|
|
1726
|
+
viewportPaddingPx: scaleMode === "screen" ? screenPadding : void 0
|
|
1727
|
+
});
|
|
1728
|
+
await updateScatterSource(map, sourceId, points);
|
|
1729
|
+
const iconSize = scaleMode === "map" ? [
|
|
1730
|
+
"interpolate",
|
|
1731
|
+
["exponential", 2],
|
|
1732
|
+
["zoom"],
|
|
1733
|
+
map.getZoom() - 8,
|
|
1734
|
+
["*", ["get", "scale"], 1 / 256],
|
|
1735
|
+
map.getZoom(),
|
|
1736
|
+
["get", "scale"],
|
|
1737
|
+
map.getZoom() + 8,
|
|
1738
|
+
["*", ["get", "scale"], 256]
|
|
1739
|
+
] : ["get", "scale"];
|
|
1740
|
+
let layerStates = installedScatterLayerStates.get(map);
|
|
1741
|
+
if (!layerStates) {
|
|
1742
|
+
layerStates = /* @__PURE__ */ new Map();
|
|
1743
|
+
installedScatterLayerStates.set(map, layerStates);
|
|
1744
|
+
}
|
|
1745
|
+
const layerModeSignature = scaleMode === "screen" ? "screen" : `map:${map.getZoom()}`;
|
|
1746
|
+
if (!map.getLayer(layerId)) {
|
|
1747
|
+
map.addLayer({
|
|
1748
|
+
id: layerId,
|
|
1749
|
+
type: "symbol",
|
|
1750
|
+
source: sourceId,
|
|
1751
|
+
layout: {
|
|
1752
|
+
"icon-image": iconId,
|
|
1753
|
+
"icon-rotate": ["get", "rotation"],
|
|
1754
|
+
"icon-size": iconSize,
|
|
1755
|
+
"icon-allow-overlap": true,
|
|
1756
|
+
"icon-ignore-placement": true,
|
|
1757
|
+
"symbol-z-order": "source"
|
|
1758
|
+
},
|
|
1759
|
+
paint: {
|
|
1760
|
+
"icon-opacity": opacity
|
|
1761
|
+
}
|
|
1762
|
+
});
|
|
1763
|
+
layerStates.set(layerId, {
|
|
1764
|
+
mode: layerModeSignature,
|
|
1765
|
+
iconId,
|
|
1766
|
+
opacity
|
|
1767
|
+
});
|
|
1768
|
+
} else {
|
|
1769
|
+
const previousLayer = layerStates.get(layerId);
|
|
1770
|
+
if (previousLayer?.mode !== layerModeSignature) {
|
|
1771
|
+
map.setLayoutProperty(layerId, "icon-size", iconSize);
|
|
1772
|
+
}
|
|
1773
|
+
if (previousLayer?.iconId !== iconId) {
|
|
1774
|
+
map.setLayoutProperty(layerId, "icon-image", iconId);
|
|
1775
|
+
}
|
|
1776
|
+
if (previousLayer?.opacity !== opacity) {
|
|
1777
|
+
map.setPaintProperty(layerId, "icon-opacity", opacity);
|
|
1778
|
+
}
|
|
1779
|
+
layerStates.set(layerId, {
|
|
1780
|
+
mode: layerModeSignature,
|
|
1781
|
+
iconId,
|
|
1782
|
+
opacity
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
return __toCommonJS(src_exports);
|
|
1787
|
+
})();
|