@massif-maps/style-tools 6.1.2 → 6.1.3-dev.144
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 +2 -0
- package/dist/mapbox2css/expression.js +5 -14
- package/dist/mapbox2css/expression.js.map +1 -1
- package/dist/mapbox2css/index.js +185 -84
- package/dist/mapbox2css/index.js.map +1 -1
- package/dist/mapbox2css/split.js +42 -27
- package/dist/mapbox2css/split.js.map +1 -1
- package/package.json +1 -1
- package/wasm/massif-style.mjs +1 -1
- package/wasm/massif-style.wasm +0 -0
package/dist/mapbox2css/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { PLATE_MAP, asShieldDeclaration, isShieldLayer, plateRadius } from './sh
|
|
|
9
9
|
import { describeFlatPlate, extractAllIconPlates, extractAllIcons, extractIcon, extractIconPlate, hasIcon } from './sprite.js';
|
|
10
10
|
import { ICON_ALIASES, detectSourceSchema, mapSourceLayer, retargetLayer } from './schema.js';
|
|
11
11
|
import { narrowLayer } from './narrow.js';
|
|
12
|
-
import { collapseBranches, expandSetFilter, expandSortKey, splitLayer } from './split.js';
|
|
12
|
+
import { BAND_MAXZOOM, BAND_MINZOOM, collapseBranches, expandSetFilter, expandSortKey, splitLayer } from './split.js';
|
|
13
13
|
import { hoistVariables, paletteHeader } from './variables.js';
|
|
14
14
|
import { LIGHT_PRESET, importOnly, liveConfig, presetsOf, resolveConfig, sceneBrightness } from './config.js';
|
|
15
15
|
import { ICON_PARAMS, ICON_PARAM_SCOPE, RECOLOURABLE_ICON, foldConfig, foldLayer, isPitch, toHsla } from './fold.js';
|
|
@@ -227,7 +227,9 @@ export function convert(style, table, options = {}) {
|
|
|
227
227
|
continue;
|
|
228
228
|
}
|
|
229
229
|
configValues.delete(name);
|
|
230
|
-
|
|
230
|
+
// a bare object or array would read as a declaration or an enum, so a table keeps its `default`
|
|
231
|
+
const table = typeof spec.default === 'object' && spec.default !== null;
|
|
232
|
+
options.styleParams.set(name, spec.values || table ? { default: spec.default, ...(spec.values ? { values: spec.values } : {}) } : spec.default);
|
|
231
233
|
coverage.note(`config "${name}" stays a style parameter, settable on a running map`);
|
|
232
234
|
}
|
|
233
235
|
if (configValues.size > 0) {
|
|
@@ -388,18 +390,21 @@ export function convert(style, table, options = {}) {
|
|
|
388
390
|
: schemaLayer;
|
|
389
391
|
// A field-driven paint value becomes one attachment per branch, and a line-sort-key one
|
|
390
392
|
// per key value in draw order - see split.ts.
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
393
|
+
// An attachment per sort key, lowest first - the only split that has to ORDER its parts.
|
|
394
|
+
// The rest have exclusive filters, so they are rules of that one attachment: an
|
|
395
|
+
// attachment walks the whole source layer per tile, and its features keep the tile's
|
|
396
|
+
// order, as MapBox draws one layer.
|
|
397
|
+
const ordered = expandSortKey(isContourLayer(layer) ? retargeted : schemaLayer, coverage);
|
|
396
398
|
// `massif:attachment`: layers drawing into ONE attachment, so a child project's rule on it
|
|
397
399
|
// merges with whichever of them matches instead of drawing the feature a second time.
|
|
398
400
|
const sharedAttachment = layer.metadata?.['massif:attachment'];
|
|
399
|
-
|
|
400
|
-
const suffix =
|
|
401
|
+
ordered.forEach((sortKeyed, branch) => {
|
|
402
|
+
const suffix = ordered.length > 1 ? `_b${branch + 1}` : '';
|
|
401
403
|
const attachment = typeof sharedAttachment === 'string' ? attachmentName(sharedAttachment) : `${attachmentName(layer.id)}${suffix}`;
|
|
402
|
-
|
|
404
|
+
expandSetFilter(sortKeyed)
|
|
405
|
+
.flatMap((variant) => splitLayer(variant, coverage))
|
|
406
|
+
.map((variant) => narrowLayer(bandDashByZoom(variant, coverage)))
|
|
407
|
+
.forEach((variant) => emitLayer(variant, attachment, target, symbolizer, index));
|
|
403
408
|
});
|
|
404
409
|
}
|
|
405
410
|
});
|
|
@@ -554,9 +559,13 @@ export function convert(style, table, options = {}) {
|
|
|
554
559
|
declarations.push(`${property}: ${value};`);
|
|
555
560
|
}
|
|
556
561
|
const range = Array.isArray(sdk.visibleZoomRange) ? sdk.visibleZoomRange : [];
|
|
562
|
+
// `massif:maxzoom-param`: the slot ends where an app says, past the standalone layer's visibleZoomRange
|
|
563
|
+
const maxParam = layer.metadata?.['massif:maxzoom-param'];
|
|
564
|
+
const hasMaxParam = typeof maxParam === 'string' && zoomOffsetLevels() === 0;
|
|
557
565
|
let selector;
|
|
558
566
|
try {
|
|
559
|
-
selector = `#${layer.id}${[...
|
|
567
|
+
selector = `#${layer.id}${[...(hasMaxParam ? [`[zoom < 'param::${maxParam}']`] : []),
|
|
568
|
+
...zoomPredicates(range[0] || layer.minzoom, hasMaxParam ? undefined : range[1] ?? layer.maxzoom),
|
|
560
569
|
...translateFilter(layer.filter ?? null)].join('')}`;
|
|
561
570
|
}
|
|
562
571
|
catch (error) {
|
|
@@ -626,15 +635,18 @@ export function convert(style, table, options = {}) {
|
|
|
626
635
|
// when the style compiles - a child project overrides it in its `constants`, at no runtime cost.
|
|
627
636
|
const minConst = layer.metadata?.['massif:minzoom-const'];
|
|
628
637
|
const maxConst = layer.metadata?.['massif:maxzoom-const'];
|
|
629
|
-
|
|
638
|
+
// a zoom band of a split layer (splitIconByZoom) tests its own edge beside the constant
|
|
639
|
+
const bandMin = layer.metadata?.[BAND_MINZOOM];
|
|
640
|
+
const bandMax = layer.metadata?.[BAND_MAXZOOM];
|
|
641
|
+
if (typeof minConst === 'string' && typeof layer.minzoom === 'number' && bandMin === undefined)
|
|
630
642
|
constants.set(minConst, Math.floor(layer.minzoom) + zoomOffsetLevels());
|
|
631
|
-
if (typeof maxConst === 'string' && typeof layer.maxzoom === 'number')
|
|
643
|
+
if (typeof maxConst === 'string' && typeof layer.maxzoom === 'number' && bandMax === undefined)
|
|
632
644
|
constants.set(maxConst, Math.ceil(layer.maxzoom) + zoomOffsetLevels());
|
|
633
645
|
const predicates = [
|
|
634
646
|
...(hasParam ? [`[zoom >= 'param::${minParam}']`] : []),
|
|
635
647
|
...(typeof minConst === 'string' ? [`[zoom >= $${minConst}]`] : []),
|
|
636
648
|
...(typeof maxConst === 'string' ? [`[zoom < $${maxConst}]`] : []),
|
|
637
|
-
...zoomPredicates(paramStart || typeof minConst === 'string' ? undefined : layer.minzoom, typeof maxConst === 'string' ? undefined : layer.maxzoom),
|
|
649
|
+
...zoomPredicates(paramStart || (typeof minConst === 'string' && bandMin === undefined) ? undefined : layer.minzoom, typeof maxConst === 'string' && bandMax === undefined ? undefined : layer.maxzoom),
|
|
638
650
|
...(buildings ? [buildings] : []),
|
|
639
651
|
...translateFilter(filter),
|
|
640
652
|
].map((p) => (p.startsWith('when(') ? ` ${p}` : p));
|
|
@@ -735,6 +747,16 @@ export function convert(style, table, options = {}) {
|
|
|
735
747
|
options.styleParams.clear();
|
|
736
748
|
for (const [name, value] of presetParams.get('') ?? [])
|
|
737
749
|
options.styleParams.set(name, value);
|
|
750
|
+
// `massif:attachment` names an attachment a child project's rules join, so it keeps its name
|
|
751
|
+
const sharedAttachments = new Set((style.layers ?? [])
|
|
752
|
+
.map((layer) => layer.metadata?.['massif:attachment'])
|
|
753
|
+
.filter((name) => typeof name === 'string').map(attachmentName));
|
|
754
|
+
const merged = mergeIdenticalPaint(blocks, drawOrder, [...alternates.values()].map((result) => result.blocks), sharedAttachments);
|
|
755
|
+
if (merged > 0) {
|
|
756
|
+
coverage.note(`${merged} attachment(s) joined the one drawn just before them: same source-layer, ` +
|
|
757
|
+
'and every rule of both paints the same opaque lines or fills in every preset, so their ' +
|
|
758
|
+
'order cannot show and one pass over the layer draws both');
|
|
759
|
+
}
|
|
738
760
|
let hoisted = withMap(base);
|
|
739
761
|
let palette = [];
|
|
740
762
|
const presetPalettes = new Map();
|
|
@@ -836,7 +858,7 @@ export function convert(style, table, options = {}) {
|
|
|
836
858
|
for (const [preset, params] of presetParams) {
|
|
837
859
|
if (preset === '')
|
|
838
860
|
continue;
|
|
839
|
-
const differing = [...params].filter(([name, value]) => shared.get(name) !== value);
|
|
861
|
+
const differing = [...params].filter(([name, value]) => JSON.stringify(shared.get(name)) !== JSON.stringify(value));
|
|
840
862
|
if (differing.length > 0)
|
|
841
863
|
presetOverrides.set(preset, Object.fromEntries(differing.sort()));
|
|
842
864
|
}
|
|
@@ -1522,29 +1544,40 @@ function layerDeclarations(layer, symbolizer, allowed, coverage, options, layerI
|
|
|
1522
1544
|
continue;
|
|
1523
1545
|
}
|
|
1524
1546
|
const zoom = dash.zoom;
|
|
1525
|
-
const
|
|
1547
|
+
const width = dashWidth(layer.paint?.['line-width']);
|
|
1526
1548
|
// `[1, 0]` is MapBox's spelling of SOLID. Scaled by a line width it became a 430 px
|
|
1527
1549
|
// "dash", which is a 430 px bitmap rasterized to draw an unbroken line.
|
|
1528
|
-
|
|
1550
|
+
const gaps = (pattern) => pattern.some((v, i) => i % 2 === 1 && v > 0);
|
|
1551
|
+
if (layer.dashBands !== undefined) {
|
|
1552
|
+
// A banded dash reads the width in the middle of its OWN band; see bandDashByZoom.
|
|
1553
|
+
const frames = layer.dashBands.map(({ from, dashZoom }) => {
|
|
1554
|
+
const pattern = dashPatternAt(value, dashZoom) ?? dash.pattern;
|
|
1555
|
+
const scale = rampAt(width, dashZoom) ?? (zoom === null ? null : rampAt(width, zoom))
|
|
1556
|
+
?? representativeScale(width, 1);
|
|
1557
|
+
const pixels = gaps(pattern) ? pixelDashes(pattern.map((v) => v * scale)).map(round).join(',') : '';
|
|
1558
|
+
return `(${Math.floor(from) + zoomOffsetLevels()}, '${pixels}')`;
|
|
1559
|
+
});
|
|
1560
|
+
out.push(`line-dasharray: step([zoom], ${frames.join(', ')});`);
|
|
1561
|
+
coverage.emit('line-dasharray');
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1564
|
+
const pattern = dash.pattern;
|
|
1565
|
+
if (!gaps(pattern)) {
|
|
1529
1566
|
coverage.drop(name, 'the pattern has no gap, so the line is solid', layer.id);
|
|
1530
1567
|
continue;
|
|
1531
1568
|
}
|
|
1532
|
-
if (pattern !== value
|
|
1569
|
+
if (pattern !== value) {
|
|
1533
1570
|
// MapTiler ramps its path dashes over zoom and CartoCSS takes ONE pattern. The
|
|
1534
1571
|
// base (the widest band, and every stop below the first) is what is on screen at
|
|
1535
1572
|
// nearly every zoom; taking nothing left every footway drawn solid.
|
|
1536
1573
|
coverage.approximate(`line-dasharray taken at one stop, ${pattern.join(',')}: ` +
|
|
1537
1574
|
'CartoCSS takes one dash pattern, not a ramp');
|
|
1538
1575
|
}
|
|
1539
|
-
const width = dashWidth(layer.paint?.['line-width']);
|
|
1540
1576
|
// At the zoom the pattern is CHOSEN at, not the mean of the width's stops: Standard's
|
|
1541
1577
|
// steps ramp to 80 px by z22, so the mean is 43 and its 0.2 dash came out at 8.6 px
|
|
1542
1578
|
// where gl-js draws under 2 - coarse bands instead of fine treads.
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
const scale = banded ?? (zoom === null ? null : rampAt(width, zoom))
|
|
1546
|
-
?? representativeScale(width, 1);
|
|
1547
|
-
if (typeof width !== 'number' && banded === null) {
|
|
1579
|
+
const scale = (zoom === null ? null : rampAt(width, zoom)) ?? representativeScale(width, 1);
|
|
1580
|
+
if (typeof width !== 'number') {
|
|
1548
1581
|
coverage.approximate(`line-dasharray scaled by ${round(scale)}, a zoom-driven ` +
|
|
1549
1582
|
'line-width read at one zoom: CartoCSS takes one dash pattern, not a ramp');
|
|
1550
1583
|
}
|
|
@@ -2015,7 +2048,7 @@ function shieldImageDeclarations(layer, icon, scale, coverage, options) {
|
|
|
2015
2048
|
// white one round a POI glyph, and it is NOT the text halo - the two are separate in
|
|
2016
2049
|
// mapbox and now separate here.
|
|
2017
2050
|
emitTranslated(out, coverage, layer, 'icon-halo-color', 'shield-icon-halo-fill', undefined, false);
|
|
2018
|
-
emitTranslated(out, coverage, layer, 'icon-halo-width', 'shield-icon-halo-radius', undefined, false);
|
|
2051
|
+
emitTranslated(out, coverage, layer, 'icon-halo-width', 'shield-icon-halo-radius', undefined, false, options);
|
|
2019
2052
|
out.push(...iconPlateDeclarations(layer, icon, sized, coverage, options));
|
|
2020
2053
|
}
|
|
2021
2054
|
else if (layer.layout?.[RECOLOURABLE_ICON] === true) {
|
|
@@ -2074,26 +2107,26 @@ function dashPattern(value) {
|
|
|
2074
2107
|
return { pattern, zoom: stopZoomOf(value, pattern) };
|
|
2075
2108
|
}
|
|
2076
2109
|
/**
|
|
2077
|
-
* A dashed line whose WIDTH ramps over zoom, as one
|
|
2110
|
+
* A dashed line whose WIDTH ramps over zoom, as one `step([zoom], ...)` dash over zoom bands.
|
|
2078
2111
|
*
|
|
2079
|
-
* A MapBox dash length is a multiple of the line width, and CartoCSS takes
|
|
2080
|
-
*
|
|
2081
|
-
*
|
|
2112
|
+
* A MapBox dash length is a multiple of the line width, and CartoCSS takes a pattern of PIXELS - the
|
|
2113
|
+
* decoder rasterises it into a bitmap per tile, keyed by the string, so it can follow the TILE's zoom
|
|
2114
|
+
* but not the view's. One pattern is therefore only right at one zoom: Liberty's rail hatching
|
|
2082
2115
|
* ramps its width 3 -> 8 between z15 and z20, and the one scale we could pick drew the dash 1.8x too
|
|
2083
2116
|
* long at the bottom of that range and 0.7x too short at the top.
|
|
2084
2117
|
*
|
|
2085
|
-
* Banded, each
|
|
2086
|
-
*
|
|
2087
|
-
*
|
|
2118
|
+
* Banded, each band scales its dash by the width in the MIDDLE of its own band, and the bands are
|
|
2119
|
+
* cut where the width DOUBLES - so the worst error inside one is a factor of sqrt(2) instead of the
|
|
2120
|
+
* whole ramp. One rule carries them all: a band per attachment was a style each, ~200 in Massif.
|
|
2088
2121
|
*
|
|
2089
2122
|
* It lives here rather than in split.ts because it needs `dashPattern` and `rampAt`, and moving
|
|
2090
2123
|
* those would cost an import cycle for one caller.
|
|
2091
2124
|
*/
|
|
2092
|
-
function
|
|
2125
|
+
function bandDashByZoom(layer, coverage) {
|
|
2093
2126
|
const dash = layer.paint?.['line-dasharray'];
|
|
2094
2127
|
const width = dashWidth(layer.paint?.['line-width']);
|
|
2095
|
-
if (dash === undefined || layer.
|
|
2096
|
-
return
|
|
2128
|
+
if (dash === undefined || layer.dashBands !== undefined || dashPattern(dash) === null)
|
|
2129
|
+
return layer;
|
|
2097
2130
|
const zmin = layer.minzoom ?? 0;
|
|
2098
2131
|
const zmax = Math.min(layer.maxzoom ?? 24, 24);
|
|
2099
2132
|
// A dash the style STEPS over zoom is a rule per step: one pattern for all of them drew Standard's
|
|
@@ -2117,7 +2150,7 @@ function splitDashByZoom(layer, coverage) {
|
|
|
2117
2150
|
cuts.add(at);
|
|
2118
2151
|
}
|
|
2119
2152
|
if (!cuts.size)
|
|
2120
|
-
return
|
|
2153
|
+
return layer;
|
|
2121
2154
|
const edges = [zmin, ...[...cuts].sort((a, b) => a - b), zmax];
|
|
2122
2155
|
const bands = [];
|
|
2123
2156
|
for (let i = 0; i + 1 < edges.length; i++) {
|
|
@@ -2129,28 +2162,20 @@ function splitDashByZoom(layer, coverage) {
|
|
|
2129
2162
|
if (stops.length && !((rampAt(width, dashZoom) ?? 0) > 0) && Math.max(from, lo) < b) {
|
|
2130
2163
|
dashZoom = (Math.max(from, lo) + b) / 2;
|
|
2131
2164
|
}
|
|
2132
|
-
bands.push({
|
|
2133
|
-
...layer,
|
|
2134
|
-
// The ends keep whatever the layer stated, so banding never narrows what it draws.
|
|
2135
|
-
minzoom: i === 0 ? layer.minzoom : from,
|
|
2136
|
-
maxzoom: i + 2 === edges.length ? layer.maxzoom : to,
|
|
2137
|
-
dashZoom,
|
|
2138
|
-
});
|
|
2165
|
+
bands.push({ from, dashZoom });
|
|
2139
2166
|
}
|
|
2140
2167
|
// Two solid bands in a row are one: a width cut means nothing where no dash is drawn.
|
|
2141
2168
|
const solid = (band) => !(dashPatternAt(dash, band.dashZoom) ?? [0, 1]).some((v, i) => i % 2 === 1 && v > 0);
|
|
2142
2169
|
for (let i = bands.length - 1; i > 0; i--) {
|
|
2143
|
-
if (solid(bands[i]) && solid(bands[i - 1]))
|
|
2144
|
-
bands[i - 1] = { ...bands[i - 1], maxzoom: bands[i].maxzoom };
|
|
2170
|
+
if (solid(bands[i]) && solid(bands[i - 1]))
|
|
2145
2171
|
bands.splice(i, 1);
|
|
2146
|
-
}
|
|
2147
2172
|
}
|
|
2148
2173
|
if (bands.length < 2)
|
|
2149
|
-
return
|
|
2150
|
-
coverage.approximate(`line-dasharray on "${layer.id}"
|
|
2151
|
-
+ 'dash is a multiple of the line width and
|
|
2152
|
-
+ 'ramped width or a stepped dash
|
|
2153
|
-
return bands;
|
|
2174
|
+
return layer;
|
|
2175
|
+
coverage.approximate(`line-dasharray on "${layer.id}" stepped over ${bands.length} zoom bands: a `
|
|
2176
|
+
+ 'dash is a multiple of the line width and the decoder rasterises one pattern per tile, so a '
|
|
2177
|
+
+ 'ramped width or a stepped dash takes a pattern per band');
|
|
2178
|
+
return { ...layer, dashBands: bands };
|
|
2154
2179
|
}
|
|
2155
2180
|
/**
|
|
2156
2181
|
* No dash under a pixel: drawn through a bitmap, a 0.5 px sleeper lights a pixel only where it happens to land,
|
|
@@ -2603,8 +2628,8 @@ function dynamicIconField(image) {
|
|
|
2603
2628
|
* match(get(class), ['bed_and_breakfast', …], get(class), 'apartment', 'lodging', 'lodging')
|
|
2604
2629
|
* case(get(cuisine) == 'turkish', 'kebab', …, match(get(class), …))
|
|
2605
2630
|
*
|
|
2606
|
-
* A
|
|
2607
|
-
* entry, so `??` falls through exactly as coalesce and the fallback branch do. The alternative was
|
|
2631
|
+
* A parameter table keyed by label IS that lookup - `get([param::t], [class])` is null when the label
|
|
2632
|
+
* has no entry, so `??` falls through exactly as coalesce and the fallback branch do. The alternative was
|
|
2608
2633
|
* one attachment per branch, which cost a rule each and ran into MAX_VARIANTS: MapTiler's
|
|
2609
2634
|
* accommodation table has nine branches, so it did not split at all and every hotel lost its icon.
|
|
2610
2635
|
*/
|
|
@@ -2613,13 +2638,13 @@ function dynamicIconField(image) {
|
|
|
2613
2638
|
* the field's value at draw time, so a class OpenMapTiles spells `town_hall` cannot be rewritten in
|
|
2614
2639
|
* the style - only answered for. See ICON_ALIASES.
|
|
2615
2640
|
*/
|
|
2616
|
-
function aliasSprites(names, file,
|
|
2641
|
+
function aliasSprites(names, file, table, options) {
|
|
2617
2642
|
if (!options.schema)
|
|
2618
2643
|
return;
|
|
2619
2644
|
const have = new Set(names);
|
|
2620
2645
|
for (const [alias, sprite] of Object.entries(ICON_ALIASES[options.sourceSchema ?? 'maptiler'])) {
|
|
2621
2646
|
if (have.has(sprite) && !have.has(alias))
|
|
2622
|
-
options
|
|
2647
|
+
setTableEntry(options, table, alias, file.get(sprite));
|
|
2623
2648
|
}
|
|
2624
2649
|
}
|
|
2625
2650
|
function iconExpression(image, layer, coverage, options) {
|
|
@@ -2633,7 +2658,7 @@ function iconExpression(image, layer, coverage, options) {
|
|
|
2633
2658
|
// the style's colours reach it per feature (extractIconPlate). Its own parameter table, because
|
|
2634
2659
|
// the two halves live under the same NAME and a rule must not pick up the other one's file.
|
|
2635
2660
|
const plateMode = layer.layout?.[ICON_PARAMS] !== undefined;
|
|
2636
|
-
const
|
|
2661
|
+
const paramTable = plateMode ? GLYPH_TABLE : ICON_TABLE;
|
|
2637
2662
|
// The params may cover only some of the layer's features (see IconParamScope). The rest keep
|
|
2638
2663
|
// the sheet's own artwork, so the rule has to pick per feature between a field and a raster -
|
|
2639
2664
|
// which `shield-sdf` allows, being read with an expression context like every other property.
|
|
@@ -2730,8 +2755,8 @@ function iconExpression(image, layer, coverage, options) {
|
|
|
2730
2755
|
if (matching.length === 0)
|
|
2731
2756
|
return null;
|
|
2732
2757
|
for (const name of matching)
|
|
2733
|
-
options
|
|
2734
|
-
return `
|
|
2758
|
+
setTableEntry(options, paramTable, name, all.file.get(name));
|
|
2759
|
+
return tableRead(paramTable, `concat('${prefix}', [${field}])`);
|
|
2735
2760
|
};
|
|
2736
2761
|
/** Every sprite written out, each under the file the rule should name it by. */
|
|
2737
2762
|
let everySprite = null;
|
|
@@ -2888,19 +2913,19 @@ function iconExpression(image, layer, coverage, options) {
|
|
|
2888
2913
|
// and the table is what gives `??` a miss to fall through on.
|
|
2889
2914
|
const all = ensureEverySprite();
|
|
2890
2915
|
for (const name of all.names)
|
|
2891
|
-
options
|
|
2892
|
-
aliasSprites(all.names, all.file,
|
|
2893
|
-
const field = `[
|
|
2916
|
+
setTableEntry(options, paramTable, name, all.file.get(name));
|
|
2917
|
+
aliasSprites(all.names, all.file, paramTable, options);
|
|
2918
|
+
const field = tableRead(paramTable, `[${node[1]}]`);
|
|
2894
2919
|
if (glyphs || !plateWhen || node[1] !== scope.field)
|
|
2895
2920
|
return field;
|
|
2896
2921
|
// The features the params do NOT cover: their own artwork, from the raster table, and
|
|
2897
2922
|
// `shield-sdf` false for them (see plateWhen in shieldImageDeclarations).
|
|
2898
2923
|
const raster = extractAllIcons(sprites.sheets, sprites.outDir, options.flattenSdf);
|
|
2899
2924
|
for (const name of raster.names) {
|
|
2900
|
-
options
|
|
2925
|
+
setTableEntry(options, ICON_TABLE, name, `icons/${name}.png`);
|
|
2901
2926
|
}
|
|
2902
|
-
aliasSprites(raster.names, new Map(raster.names.map((n) => [n, `icons/${n}.png`])),
|
|
2903
|
-
return `(${plateWhen} ? ${field} :
|
|
2927
|
+
aliasSprites(raster.names, new Map(raster.names.map((n) => [n, `icons/${n}.png`])), ICON_TABLE, options);
|
|
2928
|
+
return `(${plateWhen} ? ${field} : ${tableRead(ICON_TABLE, `[${node[1]}]`)})`;
|
|
2904
2929
|
}
|
|
2905
2930
|
if (Array.isArray(node) && node[0] === 'coalesce') {
|
|
2906
2931
|
const parts = node.slice(1).map((b) => build(b)).filter((x) => x !== null);
|
|
@@ -2950,7 +2975,7 @@ function iconExpression(image, layer, coverage, options) {
|
|
|
2950
2975
|
const file = named(name);
|
|
2951
2976
|
if (!file)
|
|
2952
2977
|
continue;
|
|
2953
|
-
options
|
|
2978
|
+
setTableEntry(options, table, label, file);
|
|
2954
2979
|
wrote++;
|
|
2955
2980
|
}
|
|
2956
2981
|
}
|
|
@@ -2958,7 +2983,7 @@ function iconExpression(image, layer, coverage, options) {
|
|
|
2958
2983
|
const tail = rest && rest !== `''` ? ` ?? ${rest}` : '';
|
|
2959
2984
|
if (wrote === 0)
|
|
2960
2985
|
return rest;
|
|
2961
|
-
return
|
|
2986
|
+
return `${tableRead(table, `[${field}]`)}${tail}`;
|
|
2962
2987
|
};
|
|
2963
2988
|
const expr = build(image);
|
|
2964
2989
|
// In font mode there is no sprite to measure: the glyph run is sized by the label.
|
|
@@ -3103,13 +3128,13 @@ function paramiseValues(layer, options, coverage) {
|
|
|
3103
3128
|
const prefix = `${safeParamName(layer.id)}-${safeParamName(property)}`;
|
|
3104
3129
|
for (const [labels, result] of asHex) {
|
|
3105
3130
|
for (const label of labels)
|
|
3106
|
-
options
|
|
3131
|
+
setTableEntry(options, prefix, label, result);
|
|
3107
3132
|
}
|
|
3108
3133
|
const fallback = tryTranslate(found.fallback, property, layer.id, coverage);
|
|
3109
3134
|
if (fallback === null)
|
|
3110
3135
|
continue;
|
|
3111
3136
|
const sentinel = `@@param${subs.size}@@`;
|
|
3112
|
-
subs.set(`'${sentinel}'`, `(
|
|
3137
|
+
subs.set(`'${sentinel}'`, `(${tableRead(prefix, `[${found.field}]`)} ?? ${fallback})`);
|
|
3113
3138
|
if (whole) {
|
|
3114
3139
|
paint[property] = sentinel;
|
|
3115
3140
|
}
|
|
@@ -3197,7 +3222,7 @@ function fontShieldDeclarations(layer, image, coverage, options) {
|
|
|
3197
3222
|
emitTranslated(out, coverage, layer, 'icon-color', 'shield-icon-fill', undefined, false);
|
|
3198
3223
|
emitTranslated(out, coverage, layer, 'icon-opacity', 'shield-icon-opacity', undefined, false);
|
|
3199
3224
|
emitTranslated(out, coverage, layer, 'icon-halo-color', 'shield-icon-halo-fill', undefined, false);
|
|
3200
|
-
emitTranslated(out, coverage, layer, 'icon-halo-width', 'shield-icon-halo-radius', undefined, false);
|
|
3225
|
+
emitTranslated(out, coverage, layer, 'icon-halo-width', 'shield-icon-halo-radius', undefined, false, options);
|
|
3201
3226
|
// A recolourable badge keeps its disc: the plate measured off the sheet, as the sprite path does.
|
|
3202
3227
|
const plate = layer.layout?.[ICON_PARAMS] !== undefined ? fontPlateSample(options) : null;
|
|
3203
3228
|
if (plate)
|
|
@@ -3475,10 +3500,23 @@ const DEFAULT_SYMBOL_SPACING = 250;
|
|
|
3475
3500
|
* from 2x up, which is every phone.
|
|
3476
3501
|
*/
|
|
3477
3502
|
const FILL_OUTLINE_WIDTH = 0.4;
|
|
3478
|
-
/** One
|
|
3479
|
-
const
|
|
3503
|
+
/** One table entry per sprite name, so a per-feature lookup can fall through when it misses. */
|
|
3504
|
+
const ICON_TABLE = 'icon';
|
|
3480
3505
|
/** The glyph FIELD of a recolourable icon, a different file under the same name. */
|
|
3481
|
-
const
|
|
3506
|
+
const GLYPH_TABLE = 'glyph';
|
|
3507
|
+
/**
|
|
3508
|
+
* One entry of a parameter TABLE: an object parameter read with `get([param::table], key)`, unset
|
|
3509
|
+
* for a key it does not name so `??` falls through. An app changes one entry as `table.key`.
|
|
3510
|
+
*/
|
|
3511
|
+
function setTableEntry(options, table, key, value) {
|
|
3512
|
+
let spec = options.styleParams.get(table);
|
|
3513
|
+
if (!spec)
|
|
3514
|
+
options.styleParams.set(table, spec = { default: {} });
|
|
3515
|
+
spec.default[key] = value;
|
|
3516
|
+
}
|
|
3517
|
+
function tableRead(table, key) {
|
|
3518
|
+
return `get([param::${table}], ${key})`;
|
|
3519
|
+
}
|
|
3482
3520
|
/** A value in ems of the layer's own text-size, as the pixels CartoCSS wants. */
|
|
3483
3521
|
function ems(value, layer, coverage, from) {
|
|
3484
3522
|
const size = layer.layout?.['text-size'] ?? DEFAULT_TEXT_SIZE;
|
|
@@ -3495,7 +3533,7 @@ function scaledSize(size, width, translated) {
|
|
|
3495
3533
|
return translated === null ? String(round(width)) : `(${translated} * ${round(width)})`;
|
|
3496
3534
|
}
|
|
3497
3535
|
/** Translates a MapBox paint/layout value onto a CartoCSS property, or reports why it could not. */
|
|
3498
|
-
function emitTranslated(out, coverage, layer, from, to, fallback, constantOnly) {
|
|
3536
|
+
function emitTranslated(out, coverage, layer, from, to, fallback, constantOnly, options) {
|
|
3499
3537
|
const value = layer.paint?.[from] ?? layer.layout?.[from];
|
|
3500
3538
|
if (value === undefined) {
|
|
3501
3539
|
if (fallback !== undefined) {
|
|
@@ -3508,7 +3546,8 @@ function emitTranslated(out, coverage, layer, from, to, fallback, constantOnly)
|
|
|
3508
3546
|
coverage.drop(from, `a flattened bitmap needs a constant ${from}`, layer.id);
|
|
3509
3547
|
return;
|
|
3510
3548
|
}
|
|
3511
|
-
|
|
3549
|
+
// `options`: the property may be one the style asks to keep as a project.json table (massif:params)
|
|
3550
|
+
const translated = (options && fieldParamTable(value, layer, from, to, coverage, options)) ?? name(from, layer, coverage);
|
|
3512
3551
|
if (translated !== null) {
|
|
3513
3552
|
out.push(`${to}: ${translated};`);
|
|
3514
3553
|
coverage.emit(to);
|
|
@@ -3517,7 +3556,7 @@ function emitTranslated(out, coverage, layer, from, to, fallback, constantOnly)
|
|
|
3517
3556
|
/** The translated form of a layer property, counting the drop itself when it has none. */
|
|
3518
3557
|
/**
|
|
3519
3558
|
* A `match` on ONE field whose branches are all constants, written as a style-parameter LOOKUP keyed
|
|
3520
|
-
* by that field - `[param::poi-fill
|
|
3559
|
+
* by that field - `get([param::poi-fill], [class])`, one table per palette - instead of the ternary chain
|
|
3521
3560
|
* the decoder would otherwise walk per feature.
|
|
3522
3561
|
*
|
|
3523
3562
|
* Two things come with it. A category palette becomes EDITABLE in project.json without touching the
|
|
@@ -3593,18 +3632,23 @@ function fieldParamTable(value, layer, property, target, coverage, options, suff
|
|
|
3593
3632
|
entries.push([label, branch]);
|
|
3594
3633
|
}
|
|
3595
3634
|
}
|
|
3635
|
+
// The fallback may be a table on ANOTHER field (subclass, then class): a chain of lookups, the
|
|
3636
|
+
// last table keeping the plain name. Same field twice is not a chain, and keeps the ternary.
|
|
3596
3637
|
const rest = value[value.length - 1];
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
const fallback =
|
|
3638
|
+
const chained = Array.isArray(rest) && rest[0] === 'match' && Array.isArray(rest[1]) && rest[1][0] === 'get'
|
|
3639
|
+
&& rest[1][1] !== field;
|
|
3640
|
+
const fallback = chained
|
|
3641
|
+
? fieldParamTable(rest, layer, property, target, coverage, options, suffix)
|
|
3642
|
+
: typeof rest === 'string' || typeof rest === 'number' ? tryTranslate(rest, property, layer.id, coverage) : null;
|
|
3600
3643
|
if (fallback === null)
|
|
3601
3644
|
return null;
|
|
3602
|
-
const
|
|
3645
|
+
const base = `${layer['source-layer'] ?? safeParamName(layer.id)}-${target.replace(/^(text|shield|marker)-/, '')}${suffix}`;
|
|
3646
|
+
const slug = chained ? `${base}-${field}` : base;
|
|
3603
3647
|
for (const [label, branch] of entries)
|
|
3604
|
-
options
|
|
3648
|
+
setTableEntry(options, slug, label, paramValue(branch));
|
|
3605
3649
|
coverage.note(`"${layer.id}": ${property} is a ${entries.length}-entry table in project.json ` +
|
|
3606
|
-
`(${slug}
|
|
3607
|
-
return `((
|
|
3650
|
+
`(${slug}), read per feature by [${field}]`);
|
|
3651
|
+
return `((${tableRead(slug, `[${field}]`)}) ?? ${fallback})`;
|
|
3608
3652
|
}
|
|
3609
3653
|
/** A branch of a variant or brightness split: a table where it folds, a plain value where it is one. */
|
|
3610
3654
|
function foldOrConstant(value, layer, property, target, coverage, options, suffix) {
|
|
@@ -3757,8 +3801,10 @@ export function projectEntries(drawOrder) {
|
|
|
3757
3801
|
const runs = [];
|
|
3758
3802
|
for (const { sourceLayer, attachment } of sorted) {
|
|
3759
3803
|
const last = runs[runs.length - 1];
|
|
3760
|
-
if (last && last.sourceLayer === sourceLayer)
|
|
3761
|
-
last.attachments.
|
|
3804
|
+
if (last && last.sourceLayer === sourceLayer) {
|
|
3805
|
+
if (!last.attachments.includes(attachment))
|
|
3806
|
+
last.attachments.push(attachment);
|
|
3807
|
+
}
|
|
3762
3808
|
else
|
|
3763
3809
|
runs.push({ sourceLayer, attachments: [attachment] });
|
|
3764
3810
|
}
|
|
@@ -3768,6 +3814,61 @@ export function projectEntries(drawOrder) {
|
|
|
3768
3814
|
? run.attachments.map((attachment) => `${run.sourceLayer}::${attachment}`)
|
|
3769
3815
|
: [run.sourceLayer]));
|
|
3770
3816
|
}
|
|
3817
|
+
/** A declaration whose stacking order could show: anything translucent, blended or textured. */
|
|
3818
|
+
const ORDER_VISIBLE = /opacity|comp-op|draw-once|elevation-mode|pattern|rgba\(|hsla\(|transparent|#[0-9a-fA-F]{8}\b/;
|
|
3819
|
+
/**
|
|
3820
|
+
* Adjacent attachments of one source-layer whose every rule paints the same opaque lines or fills, in
|
|
3821
|
+
* every preset, renamed into the first of them. Drawn one after the other or in the tile's order, the
|
|
3822
|
+
* pixels are the same - and a feature both match is drawn once instead of twice, the same opaque
|
|
3823
|
+
* pixels again. An attachment is a pass over its source layer per tile, so this is decode time saved.
|
|
3824
|
+
* Returns how many attachments were folded into another.
|
|
3825
|
+
*/
|
|
3826
|
+
export function mergeIdenticalPaint(blocks, drawOrder, presets, keep) {
|
|
3827
|
+
// blocks[i] and drawOrder[i] are pushed together, so they share an index
|
|
3828
|
+
const sequence = drawOrder.map((entry, at) => ({ entry, at }))
|
|
3829
|
+
.sort((a, b) => a.entry.index - b.entry.index || a.at - b.at).map((item) => item.at);
|
|
3830
|
+
const groups = [];
|
|
3831
|
+
for (const at of sequence) {
|
|
3832
|
+
const { sourceLayer, attachment } = drawOrder[at];
|
|
3833
|
+
const last = groups[groups.length - 1];
|
|
3834
|
+
if (last && last.sourceLayer === sourceLayer && last.attachment === attachment)
|
|
3835
|
+
last.at.push(at);
|
|
3836
|
+
else
|
|
3837
|
+
groups.push({ sourceLayer, attachment, at: [at] });
|
|
3838
|
+
}
|
|
3839
|
+
const flat = (declaration) => /^(line|polygon)-/.test(declaration) && !ORDER_VISIBLE.test(declaration);
|
|
3840
|
+
const signature = (group) => {
|
|
3841
|
+
if (group.attachment === '' || keep.has(group.attachment))
|
|
3842
|
+
return null;
|
|
3843
|
+
const runs = [blocks, ...presets];
|
|
3844
|
+
const paints = [];
|
|
3845
|
+
for (const run of runs) {
|
|
3846
|
+
const first = run[group.at[0]].declarations;
|
|
3847
|
+
if (!first.every(flat))
|
|
3848
|
+
return null;
|
|
3849
|
+
const paint = JSON.stringify(first);
|
|
3850
|
+
if (group.at.some((at) => JSON.stringify(run[at].declarations) !== paint))
|
|
3851
|
+
return null;
|
|
3852
|
+
paints.push(paint);
|
|
3853
|
+
}
|
|
3854
|
+
return paints.join('\n');
|
|
3855
|
+
};
|
|
3856
|
+
let merged = 0;
|
|
3857
|
+
let current = null;
|
|
3858
|
+
for (const group of groups) {
|
|
3859
|
+
const paint = signature(group);
|
|
3860
|
+
if (current && paint !== null && current.paint === paint && current.sourceLayer === group.sourceLayer) {
|
|
3861
|
+
for (const at of group.at) {
|
|
3862
|
+
blocks[at].selector = blocks[at].selector.slice(0, -group.attachment.length) + current.attachment;
|
|
3863
|
+
drawOrder[at] = { ...drawOrder[at], attachment: current.attachment };
|
|
3864
|
+
}
|
|
3865
|
+
merged++;
|
|
3866
|
+
continue;
|
|
3867
|
+
}
|
|
3868
|
+
current = { sourceLayer: group.sourceLayer, attachment: group.attachment, paint };
|
|
3869
|
+
}
|
|
3870
|
+
return merged;
|
|
3871
|
+
}
|
|
3771
3872
|
/** What the split above cost, so a project that grew entries says why. */
|
|
3772
3873
|
function reportSplitLayers(drawOrder, coverage) {
|
|
3773
3874
|
const entries = projectEntries(drawOrder);
|