@versatiles/svg-renderer 0.3.0 → 0.5.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/README.md +32 -29
- package/dist/index.cjs +540 -290
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +540 -290
- package/dist/index.js.map +1 -1
- package/dist/maplibre.cjs +584 -291
- package/dist/maplibre.cjs.map +1 -1
- package/dist/maplibre.js +584 -291
- package/dist/maplibre.js.map +1 -1
- package/dist/maplibre.umd.js +584 -291
- package/dist/maplibre.umd.js.map +1 -1
- package/package.json +17 -14
package/dist/index.js
CHANGED
|
@@ -9031,7 +9031,9 @@ class Color {
|
|
|
9031
9031
|
this.values = [args[0], args[1], args[2], args[3]];
|
|
9032
9032
|
return;
|
|
9033
9033
|
}
|
|
9034
|
-
throw Error('Unsupported Color arguments: ' +
|
|
9034
|
+
throw Error('Unsupported Color arguments: ' +
|
|
9035
|
+
JSON.stringify(args) +
|
|
9036
|
+
'. Expected a MaplibreColor, hex string (#RRGGBB or #RRGGBBAA), or 3-4 numeric components.');
|
|
9035
9037
|
function h2d(text) {
|
|
9036
9038
|
return parseInt(text, 16);
|
|
9037
9039
|
}
|
|
@@ -9041,14 +9043,12 @@ class Color {
|
|
|
9041
9043
|
}
|
|
9042
9044
|
get hex() {
|
|
9043
9045
|
return `#${d2h(this.values[0])}${d2h(this.values[1])}${d2h(this.values[2])}${this.values[3] === 255 ? '' : d2h(this.values[3])}`;
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
return str.length < 2 ? '0' + str : str;
|
|
9051
|
-
}
|
|
9046
|
+
}
|
|
9047
|
+
get rgb() {
|
|
9048
|
+
return `#${d2h(this.values[0])}${d2h(this.values[1])}${d2h(this.values[2])}`;
|
|
9049
|
+
}
|
|
9050
|
+
get opacity() {
|
|
9051
|
+
return this.values[3] / 255;
|
|
9052
9052
|
}
|
|
9053
9053
|
get alpha() {
|
|
9054
9054
|
return this.values[3];
|
|
@@ -9060,6 +9060,126 @@ class Color {
|
|
|
9060
9060
|
return new Color(...this.values);
|
|
9061
9061
|
}
|
|
9062
9062
|
}
|
|
9063
|
+
function d2h(num) {
|
|
9064
|
+
if (num < 0)
|
|
9065
|
+
num = 0;
|
|
9066
|
+
if (num > 255)
|
|
9067
|
+
num = 255;
|
|
9068
|
+
const str = Math.round(num).toString(16).toUpperCase();
|
|
9069
|
+
return str.length < 2 ? '0' + str : str;
|
|
9070
|
+
}
|
|
9071
|
+
|
|
9072
|
+
function chainSegments(segments) {
|
|
9073
|
+
// Phase 1: normalize segments left-to-right, then chain
|
|
9074
|
+
normalizeSegments(segments, 0);
|
|
9075
|
+
let chains = greedyChain(segments);
|
|
9076
|
+
// Phase 2: normalize remaining chains top-to-bottom, then chain again
|
|
9077
|
+
normalizeSegments(chains, 1);
|
|
9078
|
+
chains = greedyChain(chains);
|
|
9079
|
+
return chains;
|
|
9080
|
+
}
|
|
9081
|
+
function normalizeSegments(segments, coordIndex) {
|
|
9082
|
+
for (const seg of segments) {
|
|
9083
|
+
const first = seg[0];
|
|
9084
|
+
const last = seg[seg.length - 1];
|
|
9085
|
+
if (first && last && last[coordIndex] < first[coordIndex])
|
|
9086
|
+
seg.reverse();
|
|
9087
|
+
}
|
|
9088
|
+
}
|
|
9089
|
+
function greedyChain(segments) {
|
|
9090
|
+
const byStart = new Map();
|
|
9091
|
+
for (const seg of segments) {
|
|
9092
|
+
const start = seg[0];
|
|
9093
|
+
if (!start)
|
|
9094
|
+
continue;
|
|
9095
|
+
const key = String(start[0]) + ',' + String(start[1]);
|
|
9096
|
+
let list = byStart.get(key);
|
|
9097
|
+
if (!list) {
|
|
9098
|
+
list = [];
|
|
9099
|
+
byStart.set(key, list);
|
|
9100
|
+
}
|
|
9101
|
+
list.push(seg);
|
|
9102
|
+
}
|
|
9103
|
+
const visited = new Set();
|
|
9104
|
+
const chains = [];
|
|
9105
|
+
for (const seg of segments) {
|
|
9106
|
+
if (visited.has(seg))
|
|
9107
|
+
continue;
|
|
9108
|
+
visited.add(seg);
|
|
9109
|
+
const chain = [...seg];
|
|
9110
|
+
let endPoint = chain[chain.length - 1];
|
|
9111
|
+
let candidates = endPoint
|
|
9112
|
+
? byStart.get(String(endPoint[0]) + ',' + String(endPoint[1]))
|
|
9113
|
+
: undefined;
|
|
9114
|
+
while (candidates) {
|
|
9115
|
+
let next;
|
|
9116
|
+
for (const c of candidates) {
|
|
9117
|
+
if (!visited.has(c)) {
|
|
9118
|
+
next = c;
|
|
9119
|
+
break;
|
|
9120
|
+
}
|
|
9121
|
+
}
|
|
9122
|
+
if (!next)
|
|
9123
|
+
break;
|
|
9124
|
+
visited.add(next);
|
|
9125
|
+
for (let i = 1; i < next.length; i++)
|
|
9126
|
+
chain.push(next[i]);
|
|
9127
|
+
endPoint = chain[chain.length - 1];
|
|
9128
|
+
candidates = endPoint
|
|
9129
|
+
? byStart.get(String(endPoint[0]) + ',' + String(endPoint[1]))
|
|
9130
|
+
: undefined;
|
|
9131
|
+
}
|
|
9132
|
+
chains.push(chain);
|
|
9133
|
+
}
|
|
9134
|
+
return chains;
|
|
9135
|
+
}
|
|
9136
|
+
function segmentsToPath(chains, close = false) {
|
|
9137
|
+
let d = '';
|
|
9138
|
+
for (const chain of chains) {
|
|
9139
|
+
const first = chain[0];
|
|
9140
|
+
if (!first)
|
|
9141
|
+
continue;
|
|
9142
|
+
d += 'M' + formatNum(first[0]) + ',' + formatNum(first[1]);
|
|
9143
|
+
let px = first[0];
|
|
9144
|
+
let py = first[1];
|
|
9145
|
+
for (let i = 1; i < chain.length; i++) {
|
|
9146
|
+
const x = chain[i][0];
|
|
9147
|
+
const y = chain[i][1];
|
|
9148
|
+
const dx = x - px;
|
|
9149
|
+
const dy = y - py;
|
|
9150
|
+
if (dy === 0) {
|
|
9151
|
+
const rel = 'h' + formatNum(dx);
|
|
9152
|
+
const abs = 'H' + formatNum(x);
|
|
9153
|
+
d += rel.length <= abs.length ? rel : abs;
|
|
9154
|
+
}
|
|
9155
|
+
else if (dx === 0) {
|
|
9156
|
+
const rel = 'v' + formatNum(dy);
|
|
9157
|
+
const abs = 'V' + formatNum(y);
|
|
9158
|
+
d += rel.length <= abs.length ? rel : abs;
|
|
9159
|
+
}
|
|
9160
|
+
else {
|
|
9161
|
+
const rel = 'l' + formatNum(dx) + ',' + formatNum(dy);
|
|
9162
|
+
const abs = 'L' + formatNum(x) + ',' + formatNum(y);
|
|
9163
|
+
d += rel.length <= abs.length ? rel : abs;
|
|
9164
|
+
}
|
|
9165
|
+
px = x;
|
|
9166
|
+
py = y;
|
|
9167
|
+
}
|
|
9168
|
+
if (close)
|
|
9169
|
+
d += 'z';
|
|
9170
|
+
}
|
|
9171
|
+
return d;
|
|
9172
|
+
}
|
|
9173
|
+
function formatNum(tenths) {
|
|
9174
|
+
if (tenths % 10 === 0)
|
|
9175
|
+
return String(tenths / 10);
|
|
9176
|
+
const negative = tenths < 0;
|
|
9177
|
+
if (negative)
|
|
9178
|
+
tenths = -tenths;
|
|
9179
|
+
const whole = Math.floor(tenths / 10);
|
|
9180
|
+
const frac = tenths % 10;
|
|
9181
|
+
return (negative ? '-' : '') + String(whole) + '.' + String(frac);
|
|
9182
|
+
}
|
|
9063
9183
|
|
|
9064
9184
|
class SVGRenderer {
|
|
9065
9185
|
width;
|
|
@@ -9075,78 +9195,86 @@ class SVGRenderer {
|
|
|
9075
9195
|
this.#backgroundColor = Color.transparent;
|
|
9076
9196
|
}
|
|
9077
9197
|
drawBackgroundFill(style) {
|
|
9078
|
-
const color = style.color
|
|
9198
|
+
const color = new Color(style.color);
|
|
9079
9199
|
color.alpha *= style.opacity;
|
|
9080
9200
|
this.#backgroundColor = color;
|
|
9081
9201
|
}
|
|
9082
|
-
drawPolygons(features
|
|
9202
|
+
drawPolygons(features) {
|
|
9083
9203
|
if (features.length === 0)
|
|
9084
9204
|
return;
|
|
9085
|
-
if (opacity <= 0)
|
|
9086
|
-
return;
|
|
9087
|
-
this.#svg.push(`<g opacity="${String(opacity)}">`);
|
|
9088
9205
|
const groups = new Map();
|
|
9089
9206
|
features.forEach(([feature, style]) => {
|
|
9090
|
-
if (style.
|
|
9207
|
+
if (style.opacity <= 0)
|
|
9208
|
+
return;
|
|
9209
|
+
const color = new Color(style.color);
|
|
9210
|
+
if (color.alpha <= 0)
|
|
9091
9211
|
return;
|
|
9092
|
-
const translate = style.translate.
|
|
9212
|
+
const translate = style.translate[0] === 0 && style.translate[1] === 0
|
|
9093
9213
|
? ''
|
|
9094
9214
|
: ` transform="translate(${formatPoint(style.translate, this.#scale)})"`;
|
|
9095
|
-
const
|
|
9215
|
+
const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
|
|
9216
|
+
const key = color.hex + translate + opacityAttr;
|
|
9096
9217
|
let group = groups.get(key);
|
|
9097
9218
|
if (!group) {
|
|
9098
|
-
group = { segments: [], attrs:
|
|
9219
|
+
group = { segments: [], attrs: `${fillAttr(color)}${translate}${opacityAttr}` };
|
|
9099
9220
|
groups.set(key, group);
|
|
9100
9221
|
}
|
|
9101
9222
|
feature.geometry.forEach((ring) => {
|
|
9102
|
-
group.segments.push(ring.map((p) => roundXY(p, this.#scale)));
|
|
9223
|
+
group.segments.push(ring.map((p) => roundXY(p.x, p.y, this.#scale)));
|
|
9103
9224
|
});
|
|
9104
9225
|
});
|
|
9105
9226
|
for (const { segments, attrs } of groups.values()) {
|
|
9106
9227
|
const d = segmentsToPath(segments, true);
|
|
9107
9228
|
this.#svg.push(`<path d="${d}" ${attrs} />`);
|
|
9108
9229
|
}
|
|
9109
|
-
this.#svg.push('</g>');
|
|
9110
9230
|
}
|
|
9111
|
-
drawLineStrings(features
|
|
9231
|
+
drawLineStrings(features) {
|
|
9112
9232
|
if (features.length === 0)
|
|
9113
9233
|
return;
|
|
9114
|
-
if (opacity <= 0)
|
|
9115
|
-
return;
|
|
9116
|
-
this.#svg.push(`<g opacity="${String(opacity)}">`);
|
|
9117
9234
|
const groups = new Map();
|
|
9118
9235
|
features.forEach(([feature, style]) => {
|
|
9119
|
-
if (style.
|
|
9236
|
+
if (style.opacity <= 0)
|
|
9237
|
+
return;
|
|
9238
|
+
const color = new Color(style.color);
|
|
9239
|
+
if (style.width <= 0 || color.alpha <= 0)
|
|
9120
9240
|
return;
|
|
9121
|
-
const translate = style.translate.
|
|
9241
|
+
const translate = style.translate[0] === 0 && style.translate[1] === 0
|
|
9122
9242
|
? ''
|
|
9123
9243
|
: ` transform="translate(${formatPoint(style.translate, this.#scale)})"`;
|
|
9124
|
-
const roundedWidth =
|
|
9244
|
+
const roundedWidth = formatScaled(style.width, this.#scale);
|
|
9245
|
+
const dasharrayStr = style.dasharray
|
|
9246
|
+
? style.dasharray.map((v) => formatScaled(v * style.width, this.#scale)).join(',')
|
|
9247
|
+
: '';
|
|
9248
|
+
const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
|
|
9125
9249
|
const key = [
|
|
9126
|
-
|
|
9250
|
+
color.hex,
|
|
9127
9251
|
roundedWidth,
|
|
9128
9252
|
style.cap,
|
|
9129
9253
|
style.join,
|
|
9130
9254
|
String(style.miterLimit),
|
|
9255
|
+
dasharrayStr,
|
|
9256
|
+
opacityAttr,
|
|
9131
9257
|
translate,
|
|
9132
9258
|
].join('\0');
|
|
9133
9259
|
let group = groups.get(key);
|
|
9134
9260
|
if (!group) {
|
|
9261
|
+
const attrs = [
|
|
9262
|
+
'fill="none"',
|
|
9263
|
+
strokeAttr(color, roundedWidth),
|
|
9264
|
+
`stroke-linecap="${style.cap}"`,
|
|
9265
|
+
`stroke-linejoin="${style.join}"`,
|
|
9266
|
+
`stroke-miterlimit="${String(style.miterLimit)}"`,
|
|
9267
|
+
];
|
|
9268
|
+
if (dasharrayStr)
|
|
9269
|
+
attrs.push(`stroke-dasharray="${dasharrayStr}"`);
|
|
9135
9270
|
group = {
|
|
9136
9271
|
segments: [],
|
|
9137
|
-
attrs:
|
|
9138
|
-
'fill="none"',
|
|
9139
|
-
`stroke="${style.color.hex}"`,
|
|
9140
|
-
`stroke-width="${roundedWidth}"`,
|
|
9141
|
-
`stroke-linecap="${style.cap}"`,
|
|
9142
|
-
`stroke-linejoin="${style.join}"`,
|
|
9143
|
-
`stroke-miterlimit="${String(style.miterLimit)}"`,
|
|
9144
|
-
].join(' ') + translate,
|
|
9272
|
+
attrs: attrs.join(' ') + translate + opacityAttr,
|
|
9145
9273
|
};
|
|
9146
9274
|
groups.set(key, group);
|
|
9147
9275
|
}
|
|
9148
9276
|
feature.geometry.forEach((line) => {
|
|
9149
|
-
group.segments.push(line.map((p) => roundXY(p, this.#scale)));
|
|
9277
|
+
group.segments.push(line.map((p) => roundXY(p.x, p.y, this.#scale)));
|
|
9150
9278
|
});
|
|
9151
9279
|
});
|
|
9152
9280
|
for (const { segments, attrs } of groups.values()) {
|
|
@@ -9154,7 +9282,46 @@ class SVGRenderer {
|
|
|
9154
9282
|
const d = segmentsToPath(chains);
|
|
9155
9283
|
this.#svg.push(`<path d="${d}" ${attrs} />`);
|
|
9156
9284
|
}
|
|
9157
|
-
|
|
9285
|
+
}
|
|
9286
|
+
drawCircles(features) {
|
|
9287
|
+
if (features.length === 0)
|
|
9288
|
+
return;
|
|
9289
|
+
const groups = new Map();
|
|
9290
|
+
features.forEach(([feature, style]) => {
|
|
9291
|
+
if (style.opacity <= 0)
|
|
9292
|
+
return;
|
|
9293
|
+
const color = new Color(style.color);
|
|
9294
|
+
if (style.radius <= 0 || color.alpha <= 0)
|
|
9295
|
+
return;
|
|
9296
|
+
const translate = style.translate[0] === 0 && style.translate[1] === 0
|
|
9297
|
+
? ''
|
|
9298
|
+
: ` transform="translate(${formatPoint(style.translate, this.#scale)})"`;
|
|
9299
|
+
const roundedRadius = formatScaled(style.radius, this.#scale);
|
|
9300
|
+
const strokeColor = new Color(style.strokeColor);
|
|
9301
|
+
const strokeAttrs = style.strokeWidth > 0
|
|
9302
|
+
? ` ${strokeAttr(strokeColor, formatScaled(style.strokeWidth, this.#scale))}`
|
|
9303
|
+
: '';
|
|
9304
|
+
const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
|
|
9305
|
+
const key = [color.hex, roundedRadius, strokeAttrs, opacityAttr, translate].join('\0');
|
|
9306
|
+
let group = groups.get(key);
|
|
9307
|
+
if (!group) {
|
|
9308
|
+
group = {
|
|
9309
|
+
points: [],
|
|
9310
|
+
attrs: `r="${roundedRadius}" ${fillAttr(color)}${strokeAttrs}${translate}${opacityAttr}`,
|
|
9311
|
+
};
|
|
9312
|
+
groups.set(key, group);
|
|
9313
|
+
}
|
|
9314
|
+
feature.geometry.forEach((ring) => {
|
|
9315
|
+
const p = ring[0];
|
|
9316
|
+
if (p)
|
|
9317
|
+
group.points.push(roundXY(p.x, p.y, this.#scale));
|
|
9318
|
+
});
|
|
9319
|
+
});
|
|
9320
|
+
for (const { points, attrs } of groups.values()) {
|
|
9321
|
+
for (const [x, y] of points) {
|
|
9322
|
+
this.#svg.push(`<circle cx="${formatNum(x)}" cy="${formatNum(y)}" ${attrs} />`);
|
|
9323
|
+
}
|
|
9324
|
+
}
|
|
9158
9325
|
}
|
|
9159
9326
|
drawRasterTiles(tiles, style) {
|
|
9160
9327
|
if (tiles.length === 0)
|
|
@@ -9180,7 +9347,7 @@ class SVGRenderer {
|
|
|
9180
9347
|
for (const tile of tiles) {
|
|
9181
9348
|
const overlap = Math.min(tile.width, tile.height) / 10000; // slight overlap to prevent sub-pixel gaps between tiles
|
|
9182
9349
|
const s = this.#scale;
|
|
9183
|
-
let attrs = `x="${
|
|
9350
|
+
let attrs = `x="${formatScaled(tile.x - overlap, s)}" y="${formatScaled(tile.y - overlap, s)}" width="${formatScaled(tile.width + overlap * 2, s)}" height="${formatScaled(tile.height + overlap * 2, s)}" href="${tile.dataUri}"`;
|
|
9184
9351
|
if (pixelated)
|
|
9185
9352
|
attrs += ' style="image-rendering:pixelated"';
|
|
9186
9353
|
this.#svg.push(`<image ${attrs} />`);
|
|
@@ -9188,192 +9355,41 @@ class SVGRenderer {
|
|
|
9188
9355
|
this.#svg.push('</g>');
|
|
9189
9356
|
}
|
|
9190
9357
|
getString() {
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
}
|
|
9201
|
-
function roundXY(p, scale) {
|
|
9202
|
-
return [Math.round(p.x * scale * 10), Math.round(p.y * scale * 10)];
|
|
9203
|
-
}
|
|
9204
|
-
function formatPoint(p, scale) {
|
|
9205
|
-
const [x, y] = roundXY(p, scale);
|
|
9206
|
-
return formatNum(x) + ',' + formatNum(y);
|
|
9207
|
-
}
|
|
9208
|
-
function chainSegments(segments) {
|
|
9209
|
-
// Phase 1: normalize segments left-to-right, then chain
|
|
9210
|
-
normalizeSegments(segments, 0);
|
|
9211
|
-
let chains = greedyChain(segments);
|
|
9212
|
-
// Phase 2: normalize remaining chains top-to-bottom, then chain again
|
|
9213
|
-
normalizeSegments(chains, 1);
|
|
9214
|
-
chains = greedyChain(chains);
|
|
9215
|
-
return chains;
|
|
9216
|
-
}
|
|
9217
|
-
function normalizeSegments(segments, coordIndex) {
|
|
9218
|
-
for (const seg of segments) {
|
|
9219
|
-
if (seg[seg.length - 1][coordIndex] < seg[0][coordIndex])
|
|
9220
|
-
seg.reverse();
|
|
9221
|
-
}
|
|
9222
|
-
}
|
|
9223
|
-
function greedyChain(segments) {
|
|
9224
|
-
const byStart = new Map();
|
|
9225
|
-
for (const seg of segments) {
|
|
9226
|
-
const key = String(seg[0][0]) + ',' + String(seg[0][1]);
|
|
9227
|
-
let list = byStart.get(key);
|
|
9228
|
-
if (!list) {
|
|
9229
|
-
list = [];
|
|
9230
|
-
byStart.set(key, list);
|
|
9231
|
-
}
|
|
9232
|
-
list.push(seg);
|
|
9233
|
-
}
|
|
9234
|
-
const visited = new Set();
|
|
9235
|
-
const chains = [];
|
|
9236
|
-
for (const seg of segments) {
|
|
9237
|
-
if (visited.has(seg))
|
|
9238
|
-
continue;
|
|
9239
|
-
visited.add(seg);
|
|
9240
|
-
const chain = [...seg];
|
|
9241
|
-
let endPoint = chain[chain.length - 1];
|
|
9242
|
-
let candidates = byStart.get(String(endPoint[0]) + ',' + String(endPoint[1]));
|
|
9243
|
-
while (candidates) {
|
|
9244
|
-
let next;
|
|
9245
|
-
for (const c of candidates) {
|
|
9246
|
-
if (!visited.has(c)) {
|
|
9247
|
-
next = c;
|
|
9248
|
-
break;
|
|
9249
|
-
}
|
|
9250
|
-
}
|
|
9251
|
-
if (!next)
|
|
9252
|
-
break;
|
|
9253
|
-
visited.add(next);
|
|
9254
|
-
for (let i = 1; i < next.length; i++)
|
|
9255
|
-
chain.push(next[i]);
|
|
9256
|
-
endPoint = chain[chain.length - 1];
|
|
9257
|
-
candidates = byStart.get(String(endPoint[0]) + ',' + String(endPoint[1]));
|
|
9358
|
+
const w = this.width.toFixed(0);
|
|
9359
|
+
const h = this.height.toFixed(0);
|
|
9360
|
+
const parts = [
|
|
9361
|
+
`<svg viewBox="0 0 ${w} ${h}" width="${w}" height="${h}" xmlns="http://www.w3.org/2000/svg">`,
|
|
9362
|
+
`<defs><clipPath id="vb"><rect width="${w}" height="${h}"/></clipPath></defs>`,
|
|
9363
|
+
`<g clip-path="url(#vb)">`,
|
|
9364
|
+
];
|
|
9365
|
+
if (this.#backgroundColor.alpha > 0) {
|
|
9366
|
+
parts.push(`<rect x="-1" y="-1" width="${(this.width + 2).toFixed(0)}" height="${(this.height + 2).toFixed(0)}" ${fillAttr(this.#backgroundColor)} />`);
|
|
9258
9367
|
}
|
|
9259
|
-
|
|
9368
|
+
parts.push(...this.#svg, '</g>', '</svg>');
|
|
9369
|
+
return parts.join('\n');
|
|
9260
9370
|
}
|
|
9261
|
-
return chains;
|
|
9262
9371
|
}
|
|
9263
|
-
function
|
|
9264
|
-
let
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
let py = chain[0][1];
|
|
9269
|
-
for (let i = 1; i < chain.length; i++) {
|
|
9270
|
-
const x = chain[i][0];
|
|
9271
|
-
const y = chain[i][1];
|
|
9272
|
-
const dx = x - px;
|
|
9273
|
-
const dy = y - py;
|
|
9274
|
-
if (dy === 0) {
|
|
9275
|
-
const rel = 'h' + formatNum(dx);
|
|
9276
|
-
const abs = 'H' + formatNum(x);
|
|
9277
|
-
d += rel.length <= abs.length ? rel : abs;
|
|
9278
|
-
}
|
|
9279
|
-
else if (dx === 0) {
|
|
9280
|
-
const rel = 'v' + formatNum(dy);
|
|
9281
|
-
const abs = 'V' + formatNum(y);
|
|
9282
|
-
d += rel.length <= abs.length ? rel : abs;
|
|
9283
|
-
}
|
|
9284
|
-
else {
|
|
9285
|
-
const rel = 'l' + formatNum(dx) + ',' + formatNum(dy);
|
|
9286
|
-
const abs = 'L' + formatNum(x) + ',' + formatNum(y);
|
|
9287
|
-
d += rel.length <= abs.length ? rel : abs;
|
|
9288
|
-
}
|
|
9289
|
-
px = x;
|
|
9290
|
-
py = y;
|
|
9291
|
-
}
|
|
9292
|
-
if (close)
|
|
9293
|
-
d += 'z';
|
|
9294
|
-
}
|
|
9295
|
-
return d;
|
|
9372
|
+
function fillAttr(color) {
|
|
9373
|
+
let attr = `fill="${color.rgb}"`;
|
|
9374
|
+
if (color.alpha < 255)
|
|
9375
|
+
attr += ` fill-opacity="${color.opacity.toFixed(3)}"`;
|
|
9376
|
+
return attr;
|
|
9296
9377
|
}
|
|
9297
|
-
function
|
|
9298
|
-
|
|
9299
|
-
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
tenths = -tenths;
|
|
9303
|
-
const whole = Math.floor(tenths / 10);
|
|
9304
|
-
const frac = tenths % 10;
|
|
9305
|
-
return (negative ? '-' : '') + String(whole) + '.' + String(frac);
|
|
9378
|
+
function strokeAttr(color, width) {
|
|
9379
|
+
let attr = `stroke="${color.rgb}" stroke-width="${width}"`;
|
|
9380
|
+
if (color.alpha < 255)
|
|
9381
|
+
attr += ` stroke-opacity="${color.opacity.toFixed(3)}"`;
|
|
9382
|
+
return attr;
|
|
9306
9383
|
}
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
x;
|
|
9310
|
-
y;
|
|
9311
|
-
constructor(x, y) {
|
|
9312
|
-
this.x = x;
|
|
9313
|
-
this.y = y;
|
|
9314
|
-
}
|
|
9315
|
-
isZero() {
|
|
9316
|
-
return this.x === 0 && this.y === 0;
|
|
9317
|
-
}
|
|
9318
|
-
scale(factor) {
|
|
9319
|
-
this.x *= factor;
|
|
9320
|
-
this.y *= factor;
|
|
9321
|
-
return this;
|
|
9322
|
-
}
|
|
9323
|
-
translate(offset) {
|
|
9324
|
-
this.x += offset.x;
|
|
9325
|
-
this.y += offset.y;
|
|
9326
|
-
return this;
|
|
9327
|
-
}
|
|
9328
|
-
getProject2Pixel() {
|
|
9329
|
-
const s = Math.sin((this.y * Math.PI) / 180.0);
|
|
9330
|
-
return new Point2D(this.x / 360.0 + 0.5, 0.5 - (0.25 * Math.log((1 + s) / (1 - s))) / Math.PI);
|
|
9331
|
-
}
|
|
9384
|
+
function formatScaled(v, scale) {
|
|
9385
|
+
return formatNum(Math.round(v * scale * 10));
|
|
9332
9386
|
}
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
constructor(opt) {
|
|
9340
|
-
this.type = opt.type;
|
|
9341
|
-
this.id = opt.id;
|
|
9342
|
-
this.properties = opt.properties;
|
|
9343
|
-
this.patterns = opt.patterns;
|
|
9344
|
-
this.geometry = opt.geometry;
|
|
9345
|
-
}
|
|
9346
|
-
getBbox() {
|
|
9347
|
-
let xMin = Infinity;
|
|
9348
|
-
let yMin = Infinity;
|
|
9349
|
-
let xMax = -Infinity;
|
|
9350
|
-
let yMax = -Infinity;
|
|
9351
|
-
this.geometry.forEach((ring) => {
|
|
9352
|
-
ring.forEach((point) => {
|
|
9353
|
-
if (xMin > point.x)
|
|
9354
|
-
xMin = point.x;
|
|
9355
|
-
if (yMin > point.y)
|
|
9356
|
-
yMin = point.y;
|
|
9357
|
-
if (xMax < point.x)
|
|
9358
|
-
xMax = point.x;
|
|
9359
|
-
if (yMax < point.y)
|
|
9360
|
-
yMax = point.y;
|
|
9361
|
-
});
|
|
9362
|
-
});
|
|
9363
|
-
return [xMin, yMin, xMax, yMax];
|
|
9364
|
-
}
|
|
9365
|
-
doesOverlap(bbox) {
|
|
9366
|
-
const featureBbox = this.getBbox();
|
|
9367
|
-
if (featureBbox[0] > bbox[2])
|
|
9368
|
-
return false;
|
|
9369
|
-
if (featureBbox[1] > bbox[3])
|
|
9370
|
-
return false;
|
|
9371
|
-
if (featureBbox[2] < bbox[0])
|
|
9372
|
-
return false;
|
|
9373
|
-
if (featureBbox[3] < bbox[1])
|
|
9374
|
-
return false;
|
|
9375
|
-
return true;
|
|
9376
|
-
}
|
|
9387
|
+
function roundXY(x, y, scale) {
|
|
9388
|
+
return [Math.round(x * scale * 10), Math.round(y * scale * 10)];
|
|
9389
|
+
}
|
|
9390
|
+
function formatPoint(p, scale) {
|
|
9391
|
+
const [x, y] = roundXY(p[0], p[1], scale);
|
|
9392
|
+
return formatNum(x) + ',' + formatNum(y);
|
|
9377
9393
|
}
|
|
9378
9394
|
|
|
9379
9395
|
/*
|
|
@@ -14007,9 +14023,84 @@ function union2(features, options = {}) {
|
|
|
14007
14023
|
else return multiPolygon(unioned, options.properties);
|
|
14008
14024
|
}
|
|
14009
14025
|
|
|
14026
|
+
class Point2D {
|
|
14027
|
+
x;
|
|
14028
|
+
y;
|
|
14029
|
+
constructor(x, y) {
|
|
14030
|
+
this.x = x;
|
|
14031
|
+
this.y = y;
|
|
14032
|
+
}
|
|
14033
|
+
isZero() {
|
|
14034
|
+
return this.x === 0 && this.y === 0;
|
|
14035
|
+
}
|
|
14036
|
+
scale(factor) {
|
|
14037
|
+
this.x *= factor;
|
|
14038
|
+
this.y *= factor;
|
|
14039
|
+
return this;
|
|
14040
|
+
}
|
|
14041
|
+
translate(offset) {
|
|
14042
|
+
this.x += offset.x;
|
|
14043
|
+
this.y += offset.y;
|
|
14044
|
+
return this;
|
|
14045
|
+
}
|
|
14046
|
+
getProject2Pixel() {
|
|
14047
|
+
const s = Math.sin((this.y * Math.PI) / 180.0);
|
|
14048
|
+
return new Point2D(this.x / 360.0 + 0.5, 0.5 - (0.25 * Math.log((1 + s) / (1 - s))) / Math.PI);
|
|
14049
|
+
}
|
|
14050
|
+
}
|
|
14051
|
+
class Feature {
|
|
14052
|
+
type;
|
|
14053
|
+
id;
|
|
14054
|
+
properties;
|
|
14055
|
+
patterns;
|
|
14056
|
+
geometry;
|
|
14057
|
+
#bbox;
|
|
14058
|
+
constructor(opt) {
|
|
14059
|
+
this.type = opt.type;
|
|
14060
|
+
this.id = opt.id;
|
|
14061
|
+
this.properties = opt.properties;
|
|
14062
|
+
this.patterns = opt.patterns;
|
|
14063
|
+
this.geometry = opt.geometry;
|
|
14064
|
+
}
|
|
14065
|
+
getBbox() {
|
|
14066
|
+
if (this.#bbox)
|
|
14067
|
+
return this.#bbox;
|
|
14068
|
+
let xMin = Infinity;
|
|
14069
|
+
let yMin = Infinity;
|
|
14070
|
+
let xMax = -Infinity;
|
|
14071
|
+
let yMax = -Infinity;
|
|
14072
|
+
this.geometry.forEach((ring) => {
|
|
14073
|
+
ring.forEach((point) => {
|
|
14074
|
+
if (xMin > point.x)
|
|
14075
|
+
xMin = point.x;
|
|
14076
|
+
if (yMin > point.y)
|
|
14077
|
+
yMin = point.y;
|
|
14078
|
+
if (xMax < point.x)
|
|
14079
|
+
xMax = point.x;
|
|
14080
|
+
if (yMax < point.y)
|
|
14081
|
+
yMax = point.y;
|
|
14082
|
+
});
|
|
14083
|
+
});
|
|
14084
|
+
this.#bbox = [xMin, yMin, xMax, yMax];
|
|
14085
|
+
return this.#bbox;
|
|
14086
|
+
}
|
|
14087
|
+
doesOverlap(bbox) {
|
|
14088
|
+
const featureBbox = this.getBbox();
|
|
14089
|
+
if (featureBbox[0] > bbox[2])
|
|
14090
|
+
return false;
|
|
14091
|
+
if (featureBbox[1] > bbox[3])
|
|
14092
|
+
return false;
|
|
14093
|
+
if (featureBbox[2] < bbox[0])
|
|
14094
|
+
return false;
|
|
14095
|
+
if (featureBbox[3] < bbox[1])
|
|
14096
|
+
return false;
|
|
14097
|
+
return true;
|
|
14098
|
+
}
|
|
14099
|
+
}
|
|
14100
|
+
|
|
14010
14101
|
function geojsonToFeature(id, polygonFeature) {
|
|
14011
14102
|
const geometry = polygonFeature.geometry.coordinates.map((ring) => {
|
|
14012
|
-
return ring.map((coord) => new Point2D(coord[0], coord[1]));
|
|
14103
|
+
return ring.map((coord) => new Point2D(coord[0] ?? 0, coord[1] ?? 0));
|
|
14013
14104
|
});
|
|
14014
14105
|
return new Feature({
|
|
14015
14106
|
type: 'Polygon',
|
|
@@ -14018,7 +14109,7 @@ function geojsonToFeature(id, polygonFeature) {
|
|
|
14018
14109
|
properties: polygonFeature.properties ?? {},
|
|
14019
14110
|
});
|
|
14020
14111
|
}
|
|
14021
|
-
function
|
|
14112
|
+
function mergePolygonsByFeatureId(featureList) {
|
|
14022
14113
|
const featuresById = new Map();
|
|
14023
14114
|
let nextId = -1;
|
|
14024
14115
|
for (const feature of featureList) {
|
|
@@ -14040,15 +14131,13 @@ function mergePolygons(featureList) {
|
|
|
14040
14131
|
const turfFeatures = [];
|
|
14041
14132
|
features.forEach((f) => {
|
|
14042
14133
|
const rings = f.geometry.map((ring) => ring.map((p) => [p.x, p.y]));
|
|
14043
|
-
|
|
14044
|
-
|
|
14045
|
-
|
|
14046
|
-
|
|
14047
|
-
|
|
14048
|
-
|
|
14049
|
-
|
|
14050
|
-
properties: f.properties,
|
|
14051
|
-
});
|
|
14134
|
+
turfFeatures.push({
|
|
14135
|
+
type: 'Feature',
|
|
14136
|
+
geometry: {
|
|
14137
|
+
type: 'Polygon',
|
|
14138
|
+
coordinates: rings,
|
|
14139
|
+
},
|
|
14140
|
+
properties: f.properties,
|
|
14052
14141
|
});
|
|
14053
14142
|
});
|
|
14054
14143
|
const merged = union2({
|
|
@@ -14080,7 +14169,9 @@ function mergePolygons(featureList) {
|
|
|
14080
14169
|
|
|
14081
14170
|
function calculateTileGrid(width, height, center, zoom, maxzoom) {
|
|
14082
14171
|
const zoomLevel = Math.min(Math.floor(zoom), maxzoom ?? Infinity);
|
|
14083
|
-
const tileCenterCoordinate = center
|
|
14172
|
+
const tileCenterCoordinate = new Point2D(center[0], center[1])
|
|
14173
|
+
.getProject2Pixel()
|
|
14174
|
+
.scale(2 ** zoomLevel);
|
|
14084
14175
|
const tileSize = 2 ** (zoom - zoomLevel + 9); // 512 (2^9) is the standard tile size
|
|
14085
14176
|
const tileCols = width / tileSize;
|
|
14086
14177
|
const tileRows = height / tileSize;
|
|
@@ -14088,11 +14179,15 @@ function calculateTileGrid(width, height, center, zoom, maxzoom) {
|
|
|
14088
14179
|
const tileMinY = Math.floor(tileCenterCoordinate.y - tileRows / 2);
|
|
14089
14180
|
const tileMaxX = Math.floor(tileCenterCoordinate.x + tileCols / 2);
|
|
14090
14181
|
const tileMaxY = Math.floor(tileCenterCoordinate.y + tileRows / 2);
|
|
14182
|
+
const tilesPerZoom = 2 ** zoomLevel;
|
|
14091
14183
|
const tiles = [];
|
|
14092
14184
|
for (let x = tileMinX; x <= tileMaxX; x++) {
|
|
14185
|
+
const wrappedX = ((x % tilesPerZoom) + tilesPerZoom) % tilesPerZoom;
|
|
14093
14186
|
for (let y = tileMinY; y <= tileMaxY; y++) {
|
|
14187
|
+
if (y < 0 || y >= tilesPerZoom)
|
|
14188
|
+
continue;
|
|
14094
14189
|
tiles.push({
|
|
14095
|
-
x,
|
|
14190
|
+
x: wrappedX,
|
|
14096
14191
|
y,
|
|
14097
14192
|
offsetX: width / 2 + (x - tileCenterCoordinate.x) * tileSize,
|
|
14098
14193
|
offsetY: height / 2 + (y - tileCenterCoordinate.y) * tileSize,
|
|
@@ -14111,8 +14206,8 @@ async function getTile(url, z, x, y) {
|
|
|
14111
14206
|
const contentType = response.headers.get('content-type') ?? 'application/octet-stream';
|
|
14112
14207
|
return { buffer, contentType };
|
|
14113
14208
|
}
|
|
14114
|
-
catch {
|
|
14115
|
-
console.warn(`Failed to load tile: ${tileUrl}
|
|
14209
|
+
catch (error) {
|
|
14210
|
+
console.warn(`Failed to load tile: ${tileUrl}`, error);
|
|
14116
14211
|
return null;
|
|
14117
14212
|
}
|
|
14118
14213
|
}
|
|
@@ -15657,24 +15752,17 @@ function writeUtf8(buf, str, pos) {
|
|
|
15657
15752
|
}
|
|
15658
15753
|
|
|
15659
15754
|
const TILE_EXTENT = 4096;
|
|
15660
|
-
|
|
15755
|
+
const VTFeatureType = { Unknown: 0, Point: 1, LineString: 2, Polygon: 3 };
|
|
15756
|
+
async function loadVectorSource(source, job, layerFeatures) {
|
|
15757
|
+
const tiles = source.tiles;
|
|
15758
|
+
if (!tiles)
|
|
15759
|
+
return;
|
|
15661
15760
|
const { width, height } = job.renderer;
|
|
15662
15761
|
const { zoom, center } = job.view;
|
|
15663
|
-
const { sources } = job.style;
|
|
15664
|
-
const source = sources['versatiles-shortbread'];
|
|
15665
|
-
if (!source)
|
|
15666
|
-
return new Map();
|
|
15667
|
-
if (source.type !== 'vector' || !source.tiles) {
|
|
15668
|
-
console.error('Invalid source configuration. Expected a vector source with tile URLs.');
|
|
15669
|
-
console.error('Source config:', source);
|
|
15670
|
-
throw Error('Invalid source');
|
|
15671
|
-
}
|
|
15672
|
-
const sourceUrl = source.tiles[0];
|
|
15673
15762
|
const { zoomLevel, tileSize, tiles: tileCoordinates, } = calculateTileGrid(width, height, center, zoom, source.maxzoom);
|
|
15674
|
-
const layerFeatures = new Map();
|
|
15675
15763
|
await Promise.all(tileCoordinates.map(async ({ x, y, offsetX, offsetY }) => {
|
|
15676
15764
|
const offset = new Point2D(offsetX, offsetY);
|
|
15677
|
-
const tile = await getTile(
|
|
15765
|
+
const tile = await getTile(tiles[0], zoomLevel, x, y);
|
|
15678
15766
|
if (!tile)
|
|
15679
15767
|
return;
|
|
15680
15768
|
const vectorTile = new VectorTile(new Pbf(tile.buffer));
|
|
@@ -15692,17 +15780,17 @@ async function getLayerFeatures(job) {
|
|
|
15692
15780
|
let type;
|
|
15693
15781
|
let list;
|
|
15694
15782
|
switch (featureSrc.type) {
|
|
15695
|
-
case
|
|
15783
|
+
case VTFeatureType.Unknown:
|
|
15696
15784
|
throw Error('Unknown feature type in vector tile');
|
|
15697
|
-
case
|
|
15785
|
+
case VTFeatureType.Point:
|
|
15698
15786
|
type = 'Point';
|
|
15699
15787
|
list = features.points;
|
|
15700
15788
|
break;
|
|
15701
|
-
case
|
|
15789
|
+
case VTFeatureType.LineString:
|
|
15702
15790
|
type = 'LineString';
|
|
15703
15791
|
list = features.linestrings;
|
|
15704
15792
|
break;
|
|
15705
|
-
case
|
|
15793
|
+
case VTFeatureType.Polygon:
|
|
15706
15794
|
type = 'Polygon';
|
|
15707
15795
|
list = features.polygons;
|
|
15708
15796
|
break;
|
|
@@ -15718,14 +15806,109 @@ async function getLayerFeatures(job) {
|
|
|
15718
15806
|
}
|
|
15719
15807
|
}
|
|
15720
15808
|
}));
|
|
15721
|
-
|
|
15722
|
-
|
|
15723
|
-
|
|
15724
|
-
|
|
15725
|
-
|
|
15726
|
-
|
|
15809
|
+
}
|
|
15810
|
+
|
|
15811
|
+
function loadGeoJSONSource(options) {
|
|
15812
|
+
const { sourceName, data, width, height, zoom, center, layerFeatures } = options;
|
|
15813
|
+
const existing = layerFeatures.get(sourceName);
|
|
15814
|
+
const features = existing ?? { points: [], linestrings: [], polygons: [] };
|
|
15815
|
+
if (!existing)
|
|
15816
|
+
layerFeatures.set(sourceName, features);
|
|
15817
|
+
const worldSize = 512 * 2 ** zoom;
|
|
15818
|
+
const centerMercator = new Point2D(center[0], center[1]).getProject2Pixel();
|
|
15819
|
+
function projectCoord(coord) {
|
|
15820
|
+
const mercator = new Point2D(coord[0], coord[1]).getProject2Pixel();
|
|
15821
|
+
return new Point2D((mercator.x - centerMercator.x) * worldSize + width / 2, (mercator.y - centerMercator.y) * worldSize + height / 2);
|
|
15822
|
+
}
|
|
15823
|
+
function makeFeature(type, geometry, id, properties) {
|
|
15824
|
+
const feature = new Feature({ type, geometry, id, properties });
|
|
15825
|
+
if (!feature.doesOverlap([0, 0, width, height]))
|
|
15826
|
+
return null;
|
|
15827
|
+
return feature;
|
|
15828
|
+
}
|
|
15829
|
+
function extractPoints(geometry) {
|
|
15830
|
+
return geometry.flatMap((ring) => ring.map((p) => [p]));
|
|
15831
|
+
}
|
|
15832
|
+
function addFeature(type, geometry, id, properties) {
|
|
15833
|
+
switch (type) {
|
|
15834
|
+
case 'Point': {
|
|
15835
|
+
const f = makeFeature('Point', geometry, id, properties);
|
|
15836
|
+
if (f)
|
|
15837
|
+
features.points.push(f);
|
|
15838
|
+
break;
|
|
15839
|
+
}
|
|
15840
|
+
case 'LineString': {
|
|
15841
|
+
const f = makeFeature('LineString', geometry, id, properties);
|
|
15842
|
+
if (f) {
|
|
15843
|
+
features.linestrings.push(f);
|
|
15844
|
+
features.points.push(new Feature({ type: 'Point', geometry: extractPoints(geometry), id, properties }));
|
|
15845
|
+
}
|
|
15846
|
+
break;
|
|
15847
|
+
}
|
|
15848
|
+
case 'Polygon': {
|
|
15849
|
+
geometry.forEach((ring, ringIndex) => {
|
|
15850
|
+
const needsCW = ringIndex === 0;
|
|
15851
|
+
let area = 0;
|
|
15852
|
+
for (let i = 0; i < ring.length; i++) {
|
|
15853
|
+
const j = (i + 1) % ring.length;
|
|
15854
|
+
area += ring[i].x * ring[j].y;
|
|
15855
|
+
area -= ring[j].x * ring[i].y;
|
|
15856
|
+
}
|
|
15857
|
+
if (area < 0 !== needsCW)
|
|
15858
|
+
ring.reverse();
|
|
15859
|
+
});
|
|
15860
|
+
const f = makeFeature('Polygon', geometry, id, properties);
|
|
15861
|
+
if (f) {
|
|
15862
|
+
features.polygons.push(f);
|
|
15863
|
+
features.linestrings.push(new Feature({ type: 'LineString', geometry, id, properties }));
|
|
15864
|
+
features.points.push(new Feature({ type: 'Point', geometry: extractPoints(geometry), id, properties }));
|
|
15865
|
+
}
|
|
15866
|
+
break;
|
|
15867
|
+
}
|
|
15868
|
+
}
|
|
15869
|
+
}
|
|
15870
|
+
function processGeometry(geom, id, properties) {
|
|
15871
|
+
switch (geom.type) {
|
|
15872
|
+
case 'Point':
|
|
15873
|
+
addFeature('Point', [[projectCoord(geom.coordinates)]], id, properties);
|
|
15874
|
+
break;
|
|
15875
|
+
case 'MultiPoint':
|
|
15876
|
+
addFeature('Point', geom.coordinates.map((c) => [projectCoord(c)]), id, properties);
|
|
15877
|
+
break;
|
|
15878
|
+
case 'LineString':
|
|
15879
|
+
addFeature('LineString', [geom.coordinates.map((c) => projectCoord(c))], id, properties);
|
|
15880
|
+
break;
|
|
15881
|
+
case 'MultiLineString':
|
|
15882
|
+
addFeature('LineString', geom.coordinates.map((line) => line.map((c) => projectCoord(c))), id, properties);
|
|
15883
|
+
break;
|
|
15884
|
+
case 'Polygon':
|
|
15885
|
+
addFeature('Polygon', geom.coordinates.map((ring) => ring.map((c) => projectCoord(c))), id, properties);
|
|
15886
|
+
break;
|
|
15887
|
+
case 'MultiPolygon':
|
|
15888
|
+
for (const polygon of geom.coordinates) {
|
|
15889
|
+
addFeature('Polygon', polygon.map((ring) => ring.map((c) => projectCoord(c))), id, properties);
|
|
15890
|
+
}
|
|
15891
|
+
break;
|
|
15892
|
+
case 'GeometryCollection':
|
|
15893
|
+
for (const g of geom.geometries) {
|
|
15894
|
+
processGeometry(g, id, properties);
|
|
15895
|
+
}
|
|
15896
|
+
break;
|
|
15897
|
+
}
|
|
15898
|
+
}
|
|
15899
|
+
switch (data.type) {
|
|
15900
|
+
case 'FeatureCollection':
|
|
15901
|
+
for (const f of data.features) {
|
|
15902
|
+
processGeometry(f.geometry, f.id, (f.properties ?? {}));
|
|
15903
|
+
}
|
|
15904
|
+
break;
|
|
15905
|
+
case 'Feature':
|
|
15906
|
+
processGeometry(data.geometry, data.id, (data.properties ?? {}));
|
|
15907
|
+
break;
|
|
15908
|
+
default:
|
|
15909
|
+
processGeometry(data, undefined, {});
|
|
15910
|
+
break;
|
|
15727
15911
|
}
|
|
15728
|
-
return layerFeatures;
|
|
15729
15912
|
}
|
|
15730
15913
|
|
|
15731
15914
|
async function getRasterTiles(job, sourceName) {
|
|
@@ -15733,7 +15916,7 @@ async function getRasterTiles(job, sourceName) {
|
|
|
15733
15916
|
const { zoom, center } = job.view;
|
|
15734
15917
|
const source = job.style.sources[sourceName];
|
|
15735
15918
|
if (source?.type !== 'raster' || !source.tiles) {
|
|
15736
|
-
throw Error(
|
|
15919
|
+
throw Error(`Invalid raster source "${sourceName}": expected type "raster" with a "tiles" array`);
|
|
15737
15920
|
}
|
|
15738
15921
|
const sourceUrl = source.tiles[0];
|
|
15739
15922
|
const { zoomLevel, tileSize, tiles } = calculateTileGrid(width, height, center, zoom, source.maxzoom);
|
|
@@ -15756,6 +15939,44 @@ async function getRasterTiles(job, sourceName) {
|
|
|
15756
15939
|
return rasterTiles.filter((tile) => tile !== null);
|
|
15757
15940
|
}
|
|
15758
15941
|
|
|
15942
|
+
async function getLayerFeatures(job) {
|
|
15943
|
+
const { width, height } = job.renderer;
|
|
15944
|
+
const { zoom, center } = job.view;
|
|
15945
|
+
const { sources } = job.style;
|
|
15946
|
+
const layerFeatures = new Map();
|
|
15947
|
+
const loadPromises = [];
|
|
15948
|
+
for (const [sourceName, sourceSpec] of Object.entries(sources)) {
|
|
15949
|
+
const source = sourceSpec;
|
|
15950
|
+
switch (source.type) {
|
|
15951
|
+
case 'vector':
|
|
15952
|
+
loadPromises.push(loadVectorSource(source, job, layerFeatures));
|
|
15953
|
+
break;
|
|
15954
|
+
case 'geojson':
|
|
15955
|
+
if (source.data) {
|
|
15956
|
+
loadGeoJSONSource({
|
|
15957
|
+
sourceName,
|
|
15958
|
+
data: source.data,
|
|
15959
|
+
width,
|
|
15960
|
+
height,
|
|
15961
|
+
zoom,
|
|
15962
|
+
center,
|
|
15963
|
+
layerFeatures,
|
|
15964
|
+
});
|
|
15965
|
+
}
|
|
15966
|
+
break;
|
|
15967
|
+
}
|
|
15968
|
+
}
|
|
15969
|
+
await Promise.all(loadPromises);
|
|
15970
|
+
for (const [name, features] of layerFeatures) {
|
|
15971
|
+
layerFeatures.set(name, {
|
|
15972
|
+
points: features.points,
|
|
15973
|
+
linestrings: features.linestrings,
|
|
15974
|
+
polygons: mergePolygonsByFeatureId(features.polygons),
|
|
15975
|
+
});
|
|
15976
|
+
}
|
|
15977
|
+
return layerFeatures;
|
|
15978
|
+
}
|
|
15979
|
+
|
|
15759
15980
|
/**
|
|
15760
15981
|
* Wraps a source/composite expression for per-feature evaluation.
|
|
15761
15982
|
*/
|
|
@@ -15798,6 +16019,7 @@ class StyleLayer {
|
|
|
15798
16019
|
minzoom;
|
|
15799
16020
|
maxzoom;
|
|
15800
16021
|
filter;
|
|
16022
|
+
filterFn;
|
|
15801
16023
|
paint;
|
|
15802
16024
|
layout;
|
|
15803
16025
|
paintExpressions;
|
|
@@ -15816,6 +16038,7 @@ class StyleLayer {
|
|
|
15816
16038
|
this.source = spec.source;
|
|
15817
16039
|
this.sourceLayer = spec['source-layer'];
|
|
15818
16040
|
this.filter = spec.filter;
|
|
16041
|
+
this.filterFn = featureFilter(this.filter);
|
|
15819
16042
|
}
|
|
15820
16043
|
this.visibility = (spec.layout?.visibility ?? 'visible');
|
|
15821
16044
|
// Initialize paint property expressions
|
|
@@ -15853,7 +16076,7 @@ class StyleLayer {
|
|
|
15853
16076
|
this.layout = new EvaluatedProperties();
|
|
15854
16077
|
for (const [name, expr] of this.paintExpressions) {
|
|
15855
16078
|
if (expr.kind === 'constant' || expr.kind === 'camera') {
|
|
15856
|
-
this.paint.set(name, expr.evaluate(params,
|
|
16079
|
+
this.paint.set(name, expr.evaluate(params, undefined, {}, undefined, availableImages));
|
|
15857
16080
|
}
|
|
15858
16081
|
else {
|
|
15859
16082
|
this.paint.set(name, new PossiblyEvaluatedPropertyValue(expr, params));
|
|
@@ -15861,7 +16084,7 @@ class StyleLayer {
|
|
|
15861
16084
|
}
|
|
15862
16085
|
for (const [name, expr] of this.layoutExpressions) {
|
|
15863
16086
|
if (expr.kind === 'constant' || expr.kind === 'camera') {
|
|
15864
|
-
this.layout.set(name, expr.evaluate(params,
|
|
16087
|
+
this.layout.set(name, expr.evaluate(params, undefined, {}, undefined, availableImages));
|
|
15865
16088
|
}
|
|
15866
16089
|
else {
|
|
15867
16090
|
this.layout.set(name, new PossiblyEvaluatedPropertyValue(expr, params));
|
|
@@ -15872,18 +16095,17 @@ class StyleLayer {
|
|
|
15872
16095
|
function createStyleLayer(spec) {
|
|
15873
16096
|
return new StyleLayer(spec);
|
|
15874
16097
|
}
|
|
15875
|
-
|
|
15876
16098
|
function getLayerStyles(layers) {
|
|
15877
|
-
return layers.map(
|
|
15878
|
-
const styleLayer = createStyleLayer(layerSpecification);
|
|
15879
|
-
return styleLayer;
|
|
15880
|
-
});
|
|
16099
|
+
return layers.map(createStyleLayer);
|
|
15881
16100
|
}
|
|
15882
16101
|
|
|
15883
|
-
async function
|
|
16102
|
+
async function renderMap(job) {
|
|
15884
16103
|
await render(job);
|
|
15885
16104
|
return job.renderer.getString();
|
|
15886
16105
|
}
|
|
16106
|
+
function getFeatures(layerFeatures, layerStyle) {
|
|
16107
|
+
return layerFeatures.get(layerStyle.sourceLayer) ?? layerFeatures.get(layerStyle.source);
|
|
16108
|
+
}
|
|
15887
16109
|
async function render(job) {
|
|
15888
16110
|
const { renderer } = job;
|
|
15889
16111
|
const { zoom } = job.view;
|
|
@@ -15914,54 +16136,55 @@ async function render(job) {
|
|
|
15914
16136
|
case 'background':
|
|
15915
16137
|
{
|
|
15916
16138
|
renderer.drawBackgroundFill({
|
|
15917
|
-
color:
|
|
16139
|
+
color: getPaint('background-color'),
|
|
15918
16140
|
opacity: getPaint('background-opacity'),
|
|
15919
16141
|
});
|
|
15920
16142
|
}
|
|
15921
16143
|
continue;
|
|
15922
16144
|
case 'fill':
|
|
15923
16145
|
{
|
|
15924
|
-
const polygons = layerFeatures
|
|
16146
|
+
const polygons = getFeatures(layerFeatures, layerStyle)?.polygons;
|
|
15925
16147
|
if (!polygons || polygons.length === 0)
|
|
15926
16148
|
continue;
|
|
15927
|
-
const
|
|
15928
|
-
|
|
16149
|
+
const polygonFeatures = layerStyle.filterFn
|
|
16150
|
+
? polygons.filter((feature) => layerStyle.filterFn.filter({ zoom }, feature))
|
|
16151
|
+
: polygons;
|
|
15929
16152
|
if (polygonFeatures.length === 0)
|
|
15930
16153
|
continue;
|
|
15931
16154
|
renderer.drawPolygons(polygonFeatures.map((feature) => [
|
|
15932
16155
|
feature,
|
|
15933
16156
|
{
|
|
15934
|
-
color:
|
|
15935
|
-
|
|
16157
|
+
color: getPaint('fill-color', feature),
|
|
16158
|
+
opacity: getPaint('fill-opacity', feature),
|
|
16159
|
+
translate: getPaint('fill-translate', feature),
|
|
15936
16160
|
},
|
|
15937
|
-
])
|
|
16161
|
+
]));
|
|
15938
16162
|
}
|
|
15939
16163
|
continue;
|
|
15940
16164
|
case 'line':
|
|
15941
16165
|
{
|
|
15942
|
-
const lineStrings = layerFeatures
|
|
16166
|
+
const lineStrings = getFeatures(layerFeatures, layerStyle)?.linestrings;
|
|
15943
16167
|
if (!lineStrings || lineStrings.length === 0)
|
|
15944
16168
|
continue;
|
|
15945
|
-
const
|
|
15946
|
-
|
|
16169
|
+
const lineStringFeatures = layerStyle.filterFn
|
|
16170
|
+
? lineStrings.filter((feature) => layerStyle.filterFn.filter({ zoom }, feature))
|
|
16171
|
+
: lineStrings;
|
|
15947
16172
|
if (lineStringFeatures.length === 0)
|
|
15948
16173
|
continue;
|
|
15949
16174
|
renderer.drawLineStrings(lineStringFeatures.map((feature) => [
|
|
15950
16175
|
feature,
|
|
15951
16176
|
{
|
|
15952
|
-
color:
|
|
15953
|
-
translate:
|
|
15954
|
-
blur: getPaint('line-blur', feature),
|
|
16177
|
+
color: getPaint('line-color', feature),
|
|
16178
|
+
translate: getPaint('line-translate', feature),
|
|
15955
16179
|
cap: getLayout('line-cap', feature),
|
|
15956
16180
|
dasharray: getPaint('line-dasharray', feature),
|
|
15957
|
-
gapWidth: getPaint('line-gap-width', feature),
|
|
15958
16181
|
join: getLayout('line-join', feature),
|
|
15959
16182
|
miterLimit: getLayout('line-miter-limit', feature),
|
|
15960
16183
|
offset: getPaint('line-offset', feature),
|
|
15961
|
-
|
|
16184
|
+
opacity: getPaint('line-opacity', feature),
|
|
15962
16185
|
width: getPaint('line-width', feature),
|
|
15963
16186
|
},
|
|
15964
|
-
])
|
|
16187
|
+
]));
|
|
15965
16188
|
}
|
|
15966
16189
|
continue;
|
|
15967
16190
|
case 'raster':
|
|
@@ -15979,6 +16202,28 @@ async function render(job) {
|
|
|
15979
16202
|
}
|
|
15980
16203
|
continue;
|
|
15981
16204
|
case 'circle':
|
|
16205
|
+
{
|
|
16206
|
+
const points = getFeatures(layerFeatures, layerStyle)?.points;
|
|
16207
|
+
if (!points || points.length === 0)
|
|
16208
|
+
continue;
|
|
16209
|
+
const pointFeatures = layerStyle.filterFn
|
|
16210
|
+
? points.filter((feature) => layerStyle.filterFn.filter({ zoom }, feature))
|
|
16211
|
+
: points;
|
|
16212
|
+
if (pointFeatures.length === 0)
|
|
16213
|
+
continue;
|
|
16214
|
+
renderer.drawCircles(pointFeatures.map((feature) => [
|
|
16215
|
+
feature,
|
|
16216
|
+
{
|
|
16217
|
+
color: getPaint('circle-color', feature),
|
|
16218
|
+
opacity: getPaint('circle-opacity', feature),
|
|
16219
|
+
radius: getPaint('circle-radius', feature),
|
|
16220
|
+
translate: getPaint('circle-translate', feature),
|
|
16221
|
+
strokeWidth: getPaint('circle-stroke-width', feature),
|
|
16222
|
+
strokeColor: getPaint('circle-stroke-color', feature),
|
|
16223
|
+
},
|
|
16224
|
+
]));
|
|
16225
|
+
}
|
|
16226
|
+
continue;
|
|
15982
16227
|
case 'color-relief':
|
|
15983
16228
|
case 'fill-extrusion':
|
|
15984
16229
|
case 'heatmap':
|
|
@@ -15992,15 +16237,20 @@ async function render(job) {
|
|
|
15992
16237
|
}
|
|
15993
16238
|
|
|
15994
16239
|
async function renderToSVG(options) {
|
|
15995
|
-
|
|
15996
|
-
|
|
15997
|
-
|
|
15998
|
-
|
|
15999
|
-
|
|
16000
|
-
|
|
16240
|
+
const width = options.width ?? 1024;
|
|
16241
|
+
const height = options.height ?? 1024;
|
|
16242
|
+
const scale = options.scale ?? 1;
|
|
16243
|
+
if (width <= 0)
|
|
16244
|
+
throw new Error('width must be positive');
|
|
16245
|
+
if (height <= 0)
|
|
16246
|
+
throw new Error('height must be positive');
|
|
16247
|
+
if (scale <= 0)
|
|
16248
|
+
throw new Error('scale must be positive');
|
|
16249
|
+
return await renderMap({
|
|
16250
|
+
renderer: new SVGRenderer({ width, height, scale }),
|
|
16001
16251
|
style: options.style,
|
|
16002
16252
|
view: {
|
|
16003
|
-
center:
|
|
16253
|
+
center: [options.lon ?? 0, options.lat ?? 0],
|
|
16004
16254
|
zoom: options.zoom ?? 2,
|
|
16005
16255
|
},
|
|
16006
16256
|
});
|