ixmaps-gl 0.2.23 → 0.2.24
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/ixmaps-gl.js +1180 -114
- package/package.json +1 -1
package/ixmaps-gl.js
CHANGED
|
@@ -45,7 +45,11 @@
|
|
|
45
45
|
// range adjustments; see flatRangeParts). A CHOROPLETH
|
|
46
46
|
// layer's geometry is borrowed from a FEATURE base layer sharing the same
|
|
47
47
|
// .layer() name (the real engine's own convention), joined via
|
|
48
|
-
// binding.lookup — see joinChoroplethFeatures.
|
|
48
|
+
// binding.lookup — see joinChoroplethFeatures. POSITION (maptheme.js
|
|
49
|
+
// 8328-8353) is a theme that renders nothing itself: it loads a table of
|
|
50
|
+
// id + position pairs and provides the id→position join that other themes
|
|
51
|
+
// on the same .layer() name consume via binding.lookup — see
|
|
52
|
+
// joinChartPositions. HEADTAIL breaks are flat's
|
|
49
53
|
// (headTailBreaks); LOG/POW2/POW3 are recognized type flags but not
|
|
50
54
|
// implemented, see KNOWN_INERT_FLAGS. CHART|USER draws a page's own chart
|
|
51
55
|
// function (style.userdraw, _buildUserChartLayers), CHART|LABEL flat's
|
|
@@ -96,7 +100,7 @@
|
|
|
96
100
|
// ---------------------------------------------------------------
|
|
97
101
|
// this engine's release (= package.json "version", checked by
|
|
98
102
|
// test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
|
|
99
|
-
const IXMAPS_GL_VERSION = '0.2.
|
|
103
|
+
const IXMAPS_GL_VERSION = '0.2.24';
|
|
100
104
|
|
|
101
105
|
const LIB_URLS = {
|
|
102
106
|
// Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
|
|
@@ -530,7 +534,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
530
534
|
// crafted link must not be able to load arbitrary code into the page.
|
|
531
535
|
// ---------------------------------------------------------------
|
|
532
536
|
// v0.1.6: knows this engine's aliases, lookupfield rule, meta↔style, builder methods, project loading
|
|
533
|
-
const VALIDATOR_URL_DEFAULT = 'https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-grammar@v0.1.
|
|
537
|
+
const VALIDATOR_URL_DEFAULT = 'https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-grammar@v0.1.28/dist/validate.mjs';
|
|
534
538
|
// a page may set ixmaps.validate BEFORE loading this script — the
|
|
535
539
|
// `global.ixmaps = {...}` export below would otherwise overwrite it
|
|
536
540
|
const _preloadValidate = global.ixmaps && global.ixmaps.validate;
|
|
@@ -670,8 +674,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
670
674
|
}
|
|
671
675
|
}
|
|
672
676
|
|
|
673
|
-
function topojsonToFeatureCollection(topo) {
|
|
674
|
-
|
|
677
|
+
function topojsonToFeatureCollection(topo, objectName) {
|
|
678
|
+
// .data({object: "NAME"}) selects the topojson object to show; without
|
|
679
|
+
// one (or with an unknown name) it stays the file's FIRST object, the
|
|
680
|
+
// previous behaviour
|
|
681
|
+
const objName = (objectName && topo.objects[objectName]) ? objectName : Object.keys(topo.objects)[0];
|
|
675
682
|
const obj = topo.objects[objName];
|
|
676
683
|
const geoms = obj.type === 'GeometryCollection' ? obj.geometries : [obj];
|
|
677
684
|
const features = geoms.map(g => ({
|
|
@@ -787,7 +794,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
787
794
|
}
|
|
788
795
|
return false;
|
|
789
796
|
});
|
|
790
|
-
|
|
797
|
+
const out = features.length === fc.features.length ? fc : Object.assign({}, fc, { features });
|
|
798
|
+
// a {type:"Sphere"} feature is flat's ocean backdrop: not drawn as a shape, the globe's own
|
|
799
|
+
// surface takes its color (see applySphereBackdrop)
|
|
800
|
+
if (fc.features.some(f => f && f.geometry && f.geometry.type === 'Sphere')) Object.defineProperty(out, '_sphere', { value: true, enumerable: false });
|
|
801
|
+
return out;
|
|
791
802
|
}
|
|
792
803
|
|
|
793
804
|
// CHOROPLETH's own .data() (e.g. the comuni demographics CSV in
|
|
@@ -826,7 +837,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
826
837
|
function dataSourceKey(data) {
|
|
827
838
|
const d = data || {};
|
|
828
839
|
return JSON.stringify({ url: d.url, urls: d.urls, type: d.type, query: d.query,
|
|
829
|
-
process: d.process && String(d.process), ext: d.ext, name: d.ext ? d.name : undefined
|
|
840
|
+
process: d.process && String(d.process), ext: d.ext, name: d.ext ? d.name : undefined,
|
|
841
|
+
// .data({object}) selects a topojson object — two themes on the same
|
|
842
|
+
// URL with different objects must NOT share one cache entry (the
|
|
843
|
+
// DISS page's 5 layers of one file)
|
|
844
|
+
object: d.object });
|
|
830
845
|
}
|
|
831
846
|
function dataCacheKey(data, binding) {
|
|
832
847
|
const b = binding || {};
|
|
@@ -1371,7 +1386,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1371
1386
|
// genuinely CSV-shaped .obj with a missing binding.position would
|
|
1372
1387
|
// throw, even though this .obj was never row data in the first place.
|
|
1373
1388
|
if (dataConfig.obj) {
|
|
1374
|
-
if (dataConfig.type === 'topojson') return topojsonToFeatureCollection(dataConfig.obj);
|
|
1389
|
+
if (dataConfig.type === 'topojson') return topojsonToFeatureCollection(dataConfig.obj, dataConfig.object);
|
|
1375
1390
|
if (dataConfig.type === 'geojson') return sanitizeGeoJSON(dataConfig.obj);
|
|
1376
1391
|
let obj = dataConfig.obj;
|
|
1377
1392
|
if (dataConfig.process) {
|
|
@@ -1444,7 +1459,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1444
1459
|
// them into a table with a geometry column, and back)
|
|
1445
1460
|
const resp = await fetch(dataConfig.url);
|
|
1446
1461
|
if (!resp.ok) throw new Error(`[ixmaps-gl] failed to fetch ${dataConfig.url}: ${resp.status}`);
|
|
1447
|
-
if (dataConfig.type === 'topojson') return topojsonToFeatureCollection(await resp.json());
|
|
1462
|
+
if (dataConfig.type === 'topojson') return topojsonToFeatureCollection(await resp.json(), dataConfig.object);
|
|
1448
1463
|
return sanitizeGeoJSON(await resp.json());
|
|
1449
1464
|
}
|
|
1450
1465
|
// every other format (json, jsonstat, ndjson, rss, kml, gml, geobuf,
|
|
@@ -1549,7 +1564,29 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1549
1564
|
// first filter left.
|
|
1550
1565
|
function filterAndJoin(raw, spec, runtimes, expr = spec.filter) {
|
|
1551
1566
|
const f = applyWhereFilter(raw, expr);
|
|
1552
|
-
return f.type === 'Table' ? joinTableFeatures(spec, f, runtimes) : f;
|
|
1567
|
+
return f.type === 'Table' ? joinTableFeatures(spec, f, runtimes) : chartAnchoredFeatures(f, spec);
|
|
1568
|
+
}
|
|
1569
|
+
// a CHART theme over geojson SHAPES (the TELRAAM page binds
|
|
1570
|
+
// lookup:"geometry" — each chart at its own feature's shape): lines and
|
|
1571
|
+
// polygons carry no [lng, lat] position, so every chart pipeline test
|
|
1572
|
+
// (typeof c[0] === 'number', inViewport, ...) drops them silently and
|
|
1573
|
+
// nothing renders. flat positions such a chart at the shape's center
|
|
1574
|
+
// (its ptPos, flatShapeCenter) — normalize the features to Point at
|
|
1575
|
+
// that anchor up front. FEATURE(S)/CHOROPLETH keep their real geometry,
|
|
1576
|
+
// VECTOR/BEZIER keep their lines (their own renderers)
|
|
1577
|
+
function chartAnchoredFeatures(fc, spec) {
|
|
1578
|
+
const chart = fc && fc.type === 'FeatureCollection' && spec.flags && spec.flags.has('CHART')
|
|
1579
|
+
&& !spec.flags.has('FEATURE') && !spec.flags.has('FEATURES')
|
|
1580
|
+
&& !spec.flags.has('CHOROPLETH') && !isVectorChart(spec.flags)
|
|
1581
|
+
&& fc.features.some(f => f && f.geometry && f.geometry.type !== 'Point');
|
|
1582
|
+
if (!chart) return fc;
|
|
1583
|
+
const features = fc.features.map(f => {
|
|
1584
|
+
const t = f && f.geometry && f.geometry.type;
|
|
1585
|
+
if (!t || t === 'Point') return f;
|
|
1586
|
+
const anchor = flatShapeCenter(f.geometry);
|
|
1587
|
+
return anchor ? { type: 'Feature', geometry: { type: 'Point', coordinates: anchor }, properties: f.properties } : f;
|
|
1588
|
+
});
|
|
1589
|
+
return { type: 'FeatureCollection', features };
|
|
1553
1590
|
}
|
|
1554
1591
|
function applyWhereFilter(fc, filterExpr) {
|
|
1555
1592
|
if (!filterExpr) return fc;
|
|
@@ -1658,6 +1695,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1658
1695
|
// each layer where its shape is found (maptheme.js 9324, 10376); records
|
|
1659
1696
|
// with no shape are left out (10387-10392)
|
|
1660
1697
|
//
|
|
1698
|
+
// A POSITION theme is a layer's id→position PROVIDER: flat loads it, draws
|
|
1699
|
+
// nothing, and only registers its records as themeNodesPosA entries
|
|
1700
|
+
// (maptheme.js 8328-8353 — "don't create SVG objects but only entries in
|
|
1701
|
+
// map.Themes.themeNodesPosA[]"), the same registry getNodePosition reads
|
|
1702
|
+
// — so here it takes part in the join index exactly like a FEATURE layer
|
|
1703
|
+
// of center points, its binding.id against the consumer's lookup key
|
|
1704
|
+
//
|
|
1661
1705
|
// flat runs the after-aggregation steps after EACH layer's pass, over all
|
|
1662
1706
|
// items; DIFFERENCE turns a record's value fields into their difference
|
|
1663
1707
|
// and drops the last, so a second pass leaves the earlier layers' items
|
|
@@ -1672,7 +1716,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1672
1716
|
const features = [];
|
|
1673
1717
|
const indexes = layers.map(layer => {
|
|
1674
1718
|
const index = new Map();
|
|
1675
|
-
runtimes.filter(r => r.name === layer && (r.flags.has('FEATURE') || r.flags.has('FEATURES'))).forEach(r => {
|
|
1719
|
+
runtimes.filter(r => r.name === layer && (r.flags.has('FEATURE') || r.flags.has('FEATURES') || r.flags.has('POSITION'))).forEach(r => {
|
|
1676
1720
|
// in theme order, as flat's layer holds the themes' shapes
|
|
1677
1721
|
for (const [k, v] of featurePositionIndex(r)) { if (flatShapeWins(index.get(k), v.area)) index.set(k, v); }
|
|
1678
1722
|
});
|
|
@@ -1759,6 +1803,25 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1759
1803
|
return Math.min(0.9, o);
|
|
1760
1804
|
}
|
|
1761
1805
|
|
|
1806
|
+
// flat's DOPACITY formula inside the COMPOSECOLOR branch (maptheme.js
|
|
1807
|
+
// 13489-13505): (dopacityscale || 1) · (v/max)^(1/dopacitypow) — no 0.9
|
|
1808
|
+
// cap and no 0.0001 clip here, unlike the DOMINANT branch: flat writes
|
|
1809
|
+
// the raw SVG stroke-/fill-opacity property and the browser clamps at 1
|
|
1810
|
+
function composedDopacityAlpha(style, flags, v, max) {
|
|
1811
|
+
const scale = styleNum(style.dopacityscale) || 1;
|
|
1812
|
+
const pow = 1 / (styleNum(style.dopacitypow) || 1);
|
|
1813
|
+
const o = scale * Math.pow(v, pow) / Math.pow(max, pow);
|
|
1814
|
+
return isFinite(o) ? Math.max(0, Math.min(1, o)) : 0;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// a choropleth shape's outline color: a LINE shape strokes with its value
|
|
1818
|
+
// color; any other shape keeps the theme's outline, hidden while a class
|
|
1819
|
+
// is isolated and the shape is not in it
|
|
1820
|
+
function choroplethOutlineColor(isLine, valueColorOf, d, isolating, unmarkedEvidence, lineRgb) {
|
|
1821
|
+
if (isLine) return valueColorOf(d);
|
|
1822
|
+
return isolating && unmarkedEvidence(d) === 'hide' ? [0, 0, 0, 0] : lineRgb;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1762
1825
|
function joinChoroplethFeatures(spec, table, runtimes) {
|
|
1763
1826
|
// a page can define several FEATURE layers of one name — e.g. the
|
|
1764
1827
|
// comuni as polygons and as center points (for chart positions); flat
|
|
@@ -2091,6 +2154,36 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2091
2154
|
// colorscheme (isGeneratedColorScheme above), never as the step count.
|
|
2092
2155
|
function resolveColorScheme(colorscheme, labels, classes) {
|
|
2093
2156
|
if (!colorscheme) return labels.map((_, i) => FALLBACK_PALETTE[i % FALLBACK_PALETTE.length]);
|
|
2157
|
+
// flat's custom per-value color function (maptheme.js 12677-12705):
|
|
2158
|
+
// colorscheme: ["byvalue", function(value){ return color; }] — every
|
|
2159
|
+
// known category value gets its color from the function and the
|
|
2160
|
+
// result REPLACES the whole scheme (so neither classes nor the
|
|
2161
|
+
// generated-sweep machinery below ever see it, as in flat). A
|
|
2162
|
+
// function that throws, or one a non-categorical theme hands no
|
|
2163
|
+
// values to iterate, falls back to the default palette — exactly
|
|
2164
|
+
// flat's own two fallbacks. The function may also arrive serialized
|
|
2165
|
+
// (a project-JSON string), like the whole-scheme string function
|
|
2166
|
+
// below evaluates one.
|
|
2167
|
+
if (Array.isArray(colorscheme) && colorscheme.length > 1 && colorscheme[0] === 'byvalue') {
|
|
2168
|
+
let fnByValue = null;
|
|
2169
|
+
if (typeof colorscheme[1] === 'function') fnByValue = colorscheme[1];
|
|
2170
|
+
else if (typeof colorscheme[1] === 'string') {
|
|
2171
|
+
try {
|
|
2172
|
+
const f = new Function('return (' + colorscheme[1] + ')')();
|
|
2173
|
+
if (typeof f === 'function') fnByValue = f;
|
|
2174
|
+
} catch (err) { /* not code */ }
|
|
2175
|
+
}
|
|
2176
|
+
if (fnByValue) {
|
|
2177
|
+
try {
|
|
2178
|
+
const byValueA = labels.map(l => fnByValue(l));
|
|
2179
|
+
if (byValueA.length) return byValueA;
|
|
2180
|
+
console.warn('[ixmaps-gl] colorscheme "byvalue" function needs a categorical theme with known values — using the fallback palette');
|
|
2181
|
+
} catch (err) {
|
|
2182
|
+
console.warn('[ixmaps-gl] colorscheme "byvalue" function threw "' + err + '" — using the fallback palette');
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
return labels.map((_, i) => FALLBACK_PALETTE[i % FALLBACK_PALETTE.length]);
|
|
2186
|
+
}
|
|
2094
2187
|
colorscheme = applyClassesToColorScheme(colorscheme, classes);
|
|
2095
2188
|
if (isGeneratedColorScheme(colorscheme)) {
|
|
2096
2189
|
const [, cc1, cc2, nParam1, nParam2] = colorscheme;
|
|
@@ -2196,7 +2289,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2196
2289
|
// mean — see the NOOUTLIER block in _buildChartLayers) has real
|
|
2197
2290
|
// behavior now; bare OUTLIER (the real source's "keep ONLY the
|
|
2198
2291
|
// outliers" inverse mode) stays unimplemented — no config here uses it.
|
|
2199
|
-
|
|
2292
|
+
// SEQUENCE/STAR/SORT/DOWN/RECT are implemented now (the sequence
|
|
2293
|
+
// symbol chart, sequenceLayout + snapToAggregationGrid) — removed from
|
|
2294
|
+
// this list; the rest is still recognized-but-inert
|
|
2295
|
+
const KNOWN_INERT_FLAGS = ['CLIPTOGEOBOUNDS', 'OUTLIER',
|
|
2200
2296
|
'POW2', 'POW3', 'NOSORT'];
|
|
2201
2297
|
// flat's PIE / DONUT variants with no rendering here (see _buildPieLayers)
|
|
2202
2298
|
const PIE_INERT_FLAGS = [/3D/, /VOLUME/, /HEIGHT/, /STARBURST/, /FLOWER/, /RAYS/, /BOW/, /ZOOM/, /DIRECTION/, /DIRECTED/, /POLAR/];
|
|
@@ -2477,6 +2573,27 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2477
2573
|
return construct;
|
|
2478
2574
|
}
|
|
2479
2575
|
const unwrapLayer = x => (typeof x === 'function' && x.__ixLayer) ? x.__ixLayer : x;
|
|
2576
|
+
// flat's layer builder finalizers — definition(), define() AND json()
|
|
2577
|
+
// (htmlgui_flat.js 2056-2083) — all return the plain DEFINITION object,
|
|
2578
|
+
// so a real page's .layer(ixmaps.layer("x").data()...json()) hands
|
|
2579
|
+
// map.layer() that object rather than the builder. Wrap it back into a
|
|
2580
|
+
// LayerBuilder whose definition() reproduces it (the def's own slots;
|
|
2581
|
+
// its type/filter/title live INSIDE style, where definition() put them)
|
|
2582
|
+
function asLayerBuilder(x) {
|
|
2583
|
+
const lb = unwrapLayer(x);
|
|
2584
|
+
if (lb instanceof LayerBuilder) return lb;
|
|
2585
|
+
if (lb && typeof lb === 'object' && (lb.layer || lb.data || lb.style)) {
|
|
2586
|
+
const b = new LayerBuilder(lb.layer || 'layer');
|
|
2587
|
+
b._data = lb.data || null;
|
|
2588
|
+
b._binding = lb.binding || {};
|
|
2589
|
+
b._style = lb.style || {};
|
|
2590
|
+
b._meta = lb.meta || {};
|
|
2591
|
+
if (lb.field != null) b._field = lb.field;
|
|
2592
|
+
if (lb.field100 != null) b._field100 = lb.field100;
|
|
2593
|
+
return b;
|
|
2594
|
+
}
|
|
2595
|
+
return lb;
|
|
2596
|
+
}
|
|
2480
2597
|
|
|
2481
2598
|
// ---------------------------------------------------------------
|
|
2482
2599
|
// Binding aliases. Real ixmaps-flat accepts ~50 .binding() keys and maps
|
|
@@ -2487,7 +2604,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2487
2604
|
// edit it by hand; `npm run unit` fails when it drifts.
|
|
2488
2605
|
// ---------------------------------------------------------------
|
|
2489
2606
|
// <grammar:binding-aliases>
|
|
2490
|
-
// generated from ixmaps-grammar 0.1.
|
|
2607
|
+
// generated from ixmaps-grammar 0.1.28 (ixmaps-flat 1.0.42, 2b978d5 2026-10-02) — 52 aliases
|
|
2491
2608
|
const FLAT_BINDING_ALIASES = {
|
|
2492
2609
|
"aggregation": "style.aggregationfield",
|
|
2493
2610
|
"aggregationfield": "style.aggregationfield",
|
|
@@ -2549,7 +2666,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2549
2666
|
// both — so .style({tooltip}) is a tooltip and .meta({fillopacity}) a style
|
|
2550
2667
|
// property. GENERATED by test/sync-grammar.mjs — do not edit.
|
|
2551
2668
|
// <grammar:meta-keys>
|
|
2552
|
-
// generated from ixmaps-grammar 0.1.
|
|
2669
|
+
// generated from ixmaps-grammar 0.1.28 — 5 meta keys
|
|
2553
2670
|
const FLAT_META_KEYS = ["description","name","snippet","title","tooltip"];
|
|
2554
2671
|
// </grammar:meta-keys>
|
|
2555
2672
|
|
|
@@ -2905,6 +3022,15 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2905
3022
|
}
|
|
2906
3023
|
|
|
2907
3024
|
typeStyleNumbers(style);
|
|
3025
|
+
// flat's own fill-opacity fallback (maptheme.js 13406:
|
|
3026
|
+
// `this.fillOpacity = this.fillOpacity ? this.fillOpacity : this.nOpacity`)
|
|
3027
|
+
// — an "opacity" style is the fill opacity whenever "fillopacity" is
|
|
3028
|
+
// not set, as flat realizes every theme
|
|
3029
|
+
// (Number("") and Number(null) are 0: an empty opacity must not hide the theme)
|
|
3030
|
+
if (!styleNum(style.fillopacity) && style.opacity != null && String(style.opacity).trim() !== ''
|
|
3031
|
+
&& Number.isFinite(Number(style.opacity))) {
|
|
3032
|
+
style.fillopacity = Number(style.opacity);
|
|
3033
|
+
}
|
|
2908
3034
|
return { name: def.layer, data: def.data, binding, targets, geometry, flags, typeStr, style, meta, filter };
|
|
2909
3035
|
}
|
|
2910
3036
|
|
|
@@ -3061,7 +3187,58 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3061
3187
|
light: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
|
|
3062
3188
|
dark: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
|
|
3063
3189
|
};
|
|
3190
|
+
// flat's own raster-tile basemaps (htmlgui_sync_Leaflet.js __addTileLayer
|
|
3191
|
+
// list, ArcGIS block): a mapType NAME flat resolves to real raster tiles
|
|
3192
|
+
// resolves to the SAME tiles here, as a built MapLibre raster style —
|
|
3193
|
+
// "ArcGIS - Topo" was falling through to CARTO Positron before, a
|
|
3194
|
+
// non-topographic look nothing like flat's. Styles are built once per
|
|
3195
|
+
// name, so the same name is the SAME style object and setMapType's
|
|
3196
|
+
// "same style: nothing to reload" identity test keeps working.
|
|
3197
|
+
const ESRI_ATTRIBUTION = 'Tiles © Esri — Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community';
|
|
3198
|
+
const FLAT_RASTER_BASEMAPS = [
|
|
3199
|
+
{ name: 'ArcGIS - Topo', aliases: ['terrain'], maxzoom: 19,
|
|
3200
|
+
tiles: 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}.png' },
|
|
3201
|
+
{ name: 'ArcGIS - Light Gray Base', maxzoom: 19,
|
|
3202
|
+
tiles: 'https://services.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}.png' },
|
|
3203
|
+
{ name: 'ArcGIS - Ocean Basemap', maxzoom: 13,
|
|
3204
|
+
tiles: 'https://services.arcgisonline.com/ArcGIS/rest/services/Ocean_Basemap/MapServer/tile/{z}/{y}/{x}.png' },
|
|
3205
|
+
{ name: 'ArcGIS - Hillshade', maxzoom: 16,
|
|
3206
|
+
tiles: 'https://server.arcgisonline.com/arcgis/rest/services/Elevation/World_Hillshade/MapServer/tile/{z}/{y}/{x}.png' },
|
|
3207
|
+
];
|
|
3208
|
+
const _rasterBasemapStyles = new Map();
|
|
3209
|
+
function buildRasterBasemapStyle(entry) {
|
|
3210
|
+
if (_rasterBasemapStyles.has(entry.name)) return _rasterBasemapStyles.get(entry.name);
|
|
3211
|
+
const sourceId = 'ixmaps-raster-basemap';
|
|
3212
|
+
const style = {
|
|
3213
|
+
version: 8,
|
|
3214
|
+
sources: {
|
|
3215
|
+
[sourceId]: {
|
|
3216
|
+
type: 'raster',
|
|
3217
|
+
tiles: [entry.tiles],
|
|
3218
|
+
tileSize: 256,
|
|
3219
|
+
maxzoom: entry.maxzoom,
|
|
3220
|
+
attribution: ESRI_ATTRIBUTION
|
|
3221
|
+
}
|
|
3222
|
+
},
|
|
3223
|
+
// raster-opacity (applyBasemapOpacity's raster entry) fades this
|
|
3224
|
+
// layer like any other basemap
|
|
3225
|
+
layers: [{ id: sourceId, type: 'raster', source: sourceId, paint: {} }]
|
|
3226
|
+
};
|
|
3227
|
+
_rasterBasemapStyles.set(entry.name, style);
|
|
3228
|
+
return style;
|
|
3229
|
+
}
|
|
3230
|
+
// flat's mapTypeTranslate ("terrain" → "ArcGIS - Topo", ...) applies to
|
|
3231
|
+
// the raster names too
|
|
3232
|
+
function flatRasterBasemap(mapType) {
|
|
3233
|
+
if (typeof mapType !== 'string') return null;
|
|
3234
|
+
const t = mapType.trim();
|
|
3235
|
+
for (const entry of FLAT_RASTER_BASEMAPS) {
|
|
3236
|
+
if (t === entry.name || (entry.aliases || []).includes(t)) return entry;
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3064
3239
|
function resolveBasemapStyleUrl(mapType) {
|
|
3240
|
+
const raster = flatRasterBasemap(mapType);
|
|
3241
|
+
if (raster) return buildRasterBasemapStyle(raster);
|
|
3065
3242
|
return typeof mapType === 'string' && /dark|black|night|matter/i.test(mapType) ? BASEMAP_STYLE_URLS.dark : BASEMAP_STYLE_URLS.light;
|
|
3066
3243
|
}
|
|
3067
3244
|
// Real ixmaps-flat's legend look (tools/legend.js 3773-3845, via
|
|
@@ -3170,6 +3347,29 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3170
3347
|
return false;
|
|
3171
3348
|
}
|
|
3172
3349
|
|
|
3350
|
+
// The ocean of flat's world / orthographic maps is a {type:"Sphere"} feature: the whole map disc,
|
|
3351
|
+
// filled with the layer's color. Here it is the surface of the map (the globe, or the flat
|
|
3352
|
+
// map): a background layer under everything, in that color and opacity.
|
|
3353
|
+
const SPHERE_BACKDROP_LAYER_ID = '__ixmaps_gl_sphere_backdrop';
|
|
3354
|
+
function applySphereBackdrop(map, style) {
|
|
3355
|
+
const cs = style && style.colorscheme;
|
|
3356
|
+
const color = Array.isArray(cs) ? cs[0] : cs;
|
|
3357
|
+
if (!color || color === 'none') return;
|
|
3358
|
+
const opacity = Number.isFinite(styleNum(style.fillopacity)) ? styleNum(style.fillopacity) : 1;
|
|
3359
|
+
const paint = () => {
|
|
3360
|
+
try {
|
|
3361
|
+
const id = map.getLayer(BLANK_BACKGROUND_LAYER_ID) ? BLANK_BACKGROUND_LAYER_ID : SPHERE_BACKDROP_LAYER_ID;
|
|
3362
|
+
if (id === SPHERE_BACKDROP_LAYER_ID && !map.getLayer(id)) {
|
|
3363
|
+
const first = (map.getStyle().layers || [])[0];
|
|
3364
|
+
map.addLayer({ id, type: 'background', paint: {} }, first && first.id);
|
|
3365
|
+
}
|
|
3366
|
+
map.setPaintProperty(id, 'background-color', color);
|
|
3367
|
+
map.setPaintProperty(id, 'background-opacity', opacity);
|
|
3368
|
+
} catch (e) { console.warn('[ixmaps-gl] the {type:"Sphere"} ocean backdrop could not be set:', e); }
|
|
3369
|
+
};
|
|
3370
|
+
if (map.isStyleLoaded()) paint(); else map.once('style.load', paint);
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3173
3373
|
function buildBlankBackgroundStyle(color) {
|
|
3174
3374
|
return {
|
|
3175
3375
|
version: 8,
|
|
@@ -3659,6 +3859,14 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3659
3859
|
// explicit request (real engine's own fallback is right-anchored
|
|
3660
3860
|
// too, just without an opinion on vertical placement).
|
|
3661
3861
|
const legendAlignCss = legendAlignCssFor(builder.mapOptions);
|
|
3862
|
+
// flat's align:"center"/"top" legend realizing (htmlgui_flat.js
|
|
3863
|
+
// 445-480, legend.css 516-550): not a floating corner panel but a
|
|
3864
|
+
// full-width bar over the map's top edge — width 100% capped at
|
|
3865
|
+
// legendMaxWidth (1200px), legendBackground (default
|
|
3866
|
+
// rgba(255,255,255,0.9)), rounded BOTTOM corners only, the title
|
|
3867
|
+
// centered above, the class chips inline-block side by side and
|
|
3868
|
+
// the chart-size slider inline in that same row
|
|
3869
|
+
const legendCenter = /^(center|top)$/i.test(String(builder.mapOptions.align || '').trim());
|
|
3662
3870
|
// Real ixmaps-flat's SUB-THEME (MapTheme.createSubTheme,
|
|
3663
3871
|
// maptheme.js 14284-14292 / 15010-15160; map.Themes.enableSubThemes
|
|
3664
3872
|
// defaults to true): marking fields of a multi-field choropleth
|
|
@@ -3728,7 +3936,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3728
3936
|
if (legendStack && legendStack.parentNode) return legendStack;
|
|
3729
3937
|
legendStack = document.createElement('div');
|
|
3730
3938
|
legendStack.className = 'ix-legend-stack';
|
|
3731
|
-
legendStack.style.cssText =
|
|
3939
|
+
legendStack.style.cssText = legendCenter
|
|
3940
|
+
// flat's center legend: the full-width bar, the panel carries
|
|
3941
|
+
// the background (no stack-level radius or shadow)
|
|
3942
|
+
? 'position:absolute;left:50%;transform:translateX(-50%);top:0;z-index:6;width:100%;max-width:1200px;'
|
|
3943
|
+
+ 'display:flex;flex-direction:column;pointer-events:none;'
|
|
3944
|
+
: 'position:absolute;' + legendAlignCss + 'z-index:6;width:280px;'
|
|
3732
3945
|
+ 'display:flex;flex-direction:column;max-height:66.6%;overflow-y:auto;'
|
|
3733
3946
|
+ 'border-radius:6px;box-shadow:' + shadow + ';pointer-events:auto;';
|
|
3734
3947
|
if (!document.getElementById('ix-legend-stack-css')) {
|
|
@@ -3838,7 +4051,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3838
4051
|
const looksDark = typeof builder.__effectiveDark === 'function'
|
|
3839
4052
|
? builder.__effectiveDark(isNaN(opacityOpt) ? 1 : opacityOpt, builder.mapOptions.mapType, legendBackground)
|
|
3840
4053
|
: flatLook.dark;
|
|
3841
|
-
const isLightLegend = rt.style.legendtheme === 'light' || (rt.style.legendtheme !== 'dark' && !looksDark);
|
|
4054
|
+
const isLightLegend = legendCenter || rt.style.legendtheme === 'light' || (rt.style.legendtheme !== 'dark' && !looksDark);
|
|
3842
4055
|
const colors = isLightLegend
|
|
3843
4056
|
? { bg: 'rgba(255,255,255,0.92)', fg: '#1a1a1a', shadow: '0 2px 10px rgba(0,0,0,0.18)',
|
|
3844
4057
|
rowMarked: 'rgba(0,0,0,0.08)', track: 'rgba(0,0,0,0.08)',
|
|
@@ -3849,7 +4062,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3849
4062
|
// the panel bg follows the same effective decision (flatLegendLook
|
|
3850
4063
|
// would hand the dark look a light bg here: its dark branch never
|
|
3851
4064
|
// ran for this map, its light bg would clash with dark rows/text)
|
|
3852
|
-
if (!rt.style.legendtheme) colors.bg = looksDark ? '#111' : flatLook.bg;
|
|
4065
|
+
if (!rt.style.legendtheme && !legendCenter) colors.bg = looksDark ? '#111' : flatLook.bg;
|
|
3853
4066
|
return colors;
|
|
3854
4067
|
};
|
|
3855
4068
|
const legendColors = lookColors();
|
|
@@ -3867,7 +4080,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3867
4080
|
// one box for every theme, as flat's #map-legend (legend.js
|
|
3868
4081
|
// 2200-2460): the panels stack in the legend stack in theme
|
|
3869
4082
|
// order, a line between them (see layoutLegends)
|
|
3870
|
-
panel.style.cssText =
|
|
4083
|
+
panel.style.cssText = legendCenter
|
|
4084
|
+
// flat's center legend bar: full width, centered text, the
|
|
4085
|
+
// light legendBackground, rounded bottom corners only
|
|
4086
|
+
? 'position:relative;display:flex;flex-direction:column;text-align:center;'
|
|
4087
|
+
+ 'background:' + legendColors.bg + ';color:' + legendColors.fg + ';font:12px/1.4 -apple-system,Arial,sans-serif;'
|
|
4088
|
+
+ 'padding:6px 12px 10px;pointer-events:auto;border-radius:0 0 6px 6px;'
|
|
4089
|
+
: 'position:relative;flex:0 1 auto;min-height:0;overflow:hidden;'
|
|
3871
4090
|
+ 'display:flex;flex-direction:column;text-align:left;'
|
|
3872
4091
|
+ 'background:' + legendColors.bg + ';color:' + legendColors.fg + ';font:12px/1.4 -apple-system,Arial,sans-serif;'
|
|
3873
4092
|
+ 'padding:10px 12px 12px;pointer-events:auto;';
|
|
@@ -3937,6 +4156,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3937
4156
|
// branch uses (no antimeridian wraparound handling — matches
|
|
3938
4157
|
// that existing convention, not a new gap this introduces).
|
|
3939
4158
|
let totals, maxTotal, order, rowsScroll;
|
|
4159
|
+
// hoisted so the chart-size slider below can join the chips row
|
|
4160
|
+
// in flat's center legend (legend-top-align's inline slider)
|
|
4161
|
+
let rowsEl = null;
|
|
3940
4162
|
// No-op default: TEXTLEGEND never reassigns this (see below),
|
|
3941
4163
|
// so the LATER unconditional-looking `renderRows()` call
|
|
3942
4164
|
// safely does nothing rather than needing its own isTextOnly
|
|
@@ -3993,8 +4215,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3993
4215
|
// share, then overflow-y:auto scrolls just the row list) —
|
|
3994
4216
|
// the header above and description/slider below stay put.
|
|
3995
4217
|
rowsScroll = document.createElement('div');
|
|
3996
|
-
rowsScroll.style.cssText =
|
|
3997
|
-
|
|
4218
|
+
rowsScroll.style.cssText = legendCenter
|
|
4219
|
+
// flat's center bar: one wrapped, centered row holding the
|
|
4220
|
+
// chips AND the inline chart-size slider (legend.css 521-550)
|
|
4221
|
+
? 'flex:0 1 auto;display:flex;flex-wrap:wrap;justify-content:center;align-items:center;'
|
|
4222
|
+
: 'flex:1 1 auto;min-height:0;overflow-y:auto;';
|
|
4223
|
+
rowsEl = document.createElement('div');
|
|
3998
4224
|
rowsEl.className = 'ix-legend-rows';
|
|
3999
4225
|
// .type("...|SIMPLELEGEND") — real ixmaps-flat's OTHER legend
|
|
4000
4226
|
// variant (confirmed against a real-engine screenshot, per
|
|
@@ -4019,7 +4245,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4019
4245
|
// smaller categories packed 3-4 per row) rather than a fixed
|
|
4020
4246
|
// column count.
|
|
4021
4247
|
if (rt.flags.has('SIMPLELEGEND') || rt.flags.has('COMPACTLEGEND')) {
|
|
4022
|
-
|
|
4248
|
+
// centered: inline-flex so the chips shrink to content inside
|
|
4249
|
+
// the wrapping centered row (the slider joins as a sibling)
|
|
4250
|
+
rowsEl.style.cssText = legendCenter ? 'display:inline-flex;flex-wrap:wrap;gap:4px;'
|
|
4251
|
+
: 'display:flex;flex-wrap:wrap;gap:4px;';
|
|
4023
4252
|
}
|
|
4024
4253
|
rowsScroll.appendChild(rowsEl);
|
|
4025
4254
|
|
|
@@ -4329,6 +4558,101 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4329
4558
|
bodyEl.appendChild(desc);
|
|
4330
4559
|
}
|
|
4331
4560
|
|
|
4561
|
+
// flat's TIME SLIDER (legend.js 2488-2530, the szTimeField
|
|
4562
|
+
// branch of makeColorLegendHTMLLong): a theme with a timefield
|
|
4563
|
+
// (style.timefield, resolved into binding.time) gets a slider
|
|
4564
|
+
// over its records' time span — at the span's start every
|
|
4565
|
+
// record shows (setTimeFrame's own start-of-frame rule),
|
|
4566
|
+
// elsewhere a [value, value+range) window. The window size is
|
|
4567
|
+
// flat's own default (a day; 28 days for a span over 120 days,
|
|
4568
|
+
// an hour under 7 — legend.js 2722-2729) and the range buttons
|
|
4569
|
+
// below (hour/day/week/month/year, only the ones the span
|
|
4570
|
+
// supports, and only when there's more than one — legend.js
|
|
4571
|
+
// 2512-2543) replace it, as flat's ixmaps.setSliderRange.
|
|
4572
|
+
if (rt.binding && rt.binding.time) {
|
|
4573
|
+
const timeField = rt.binding.time;
|
|
4574
|
+
const toMs = v => (typeof v === 'number' ? v : new Date(v).getTime());
|
|
4575
|
+
let uMin = Infinity, uMax = -Infinity;
|
|
4576
|
+
rt.features.forEach(f => {
|
|
4577
|
+
const t = toMs((f.properties || {})[timeField]);
|
|
4578
|
+
if (!isNaN(t)) { if (t < uMin) uMin = t; if (t > uMax) uMax = t; }
|
|
4579
|
+
});
|
|
4580
|
+
// one step guard: no span (no/broken time values) — no slider
|
|
4581
|
+
if (Number.isFinite(uMin) && Number.isFinite(uMax) && uMax > uMin) {
|
|
4582
|
+
const uDay = 1000 * 60 * 60 * 24;
|
|
4583
|
+
const days = (uMax - uMin) / uDay;
|
|
4584
|
+
let range = days > 120 ? uDay * 28 : days < 7 ? 1000 * 60 * 60 : uDay;
|
|
4585
|
+
const timeRow = document.createElement('div');
|
|
4586
|
+
timeRow.style.cssText = 'margin-top:10px;font-size:11px;opacity:0.8;';
|
|
4587
|
+
// textContent: the field name comes from the page / project JSON
|
|
4588
|
+
const timeLabel = document.createElement('span');
|
|
4589
|
+
timeLabel.style.cssText = 'font-weight:600;';
|
|
4590
|
+
timeLabel.textContent = String(rt.style.timefield || timeField);
|
|
4591
|
+
const timeSpan = document.createElement('span');
|
|
4592
|
+
timeSpan.className = 'ix-legend-time-span';
|
|
4593
|
+
timeRow.append(timeLabel, ': ', timeSpan);
|
|
4594
|
+
bodyEl.appendChild(timeRow);
|
|
4595
|
+
const timeSlider = document.createElement('input');
|
|
4596
|
+
timeSlider.type = 'range';
|
|
4597
|
+
timeSlider.min = String(uMin);
|
|
4598
|
+
timeSlider.max = String(uMax);
|
|
4599
|
+
timeSlider.value = String(uMin);
|
|
4600
|
+
timeSlider.style.cssText = 'width:100%;margin-top:0.4em;'; // flat's own slider width (legend.js 2503)
|
|
4601
|
+
timeRow.appendChild(timeSlider);
|
|
4602
|
+
const spanEl = () => timeRow.querySelector('.ix-legend-time-span');
|
|
4603
|
+
const applyTimeFrame = () => {
|
|
4604
|
+
const v = Number(timeSlider.value);
|
|
4605
|
+
if (v <= uMin) {
|
|
4606
|
+
// flat: slider at its start position = every record
|
|
4607
|
+
rt.setTimeFrame(null, null);
|
|
4608
|
+
spanEl().textContent = '';
|
|
4609
|
+
} else {
|
|
4610
|
+
rt.setTimeFrame(v, v + range);
|
|
4611
|
+
// flat's span text (legend.js 2737-2745): date - date
|
|
4612
|
+
// for a day-and-up window, date-time under it
|
|
4613
|
+
spanEl().textContent = range < uDay
|
|
4614
|
+
? new Date(v).toLocaleDateString() + ' ' + new Date(v).toLocaleTimeString()
|
|
4615
|
+
: new Date(v).toLocaleDateString() + ' - ' + new Date(v + range).toLocaleDateString();
|
|
4616
|
+
}
|
|
4617
|
+
// a discrete control, not a pan/zoom stream: full refresh,
|
|
4618
|
+
// so the rows/totals (onRedraw → computeTotals) follow live
|
|
4619
|
+
refresh();
|
|
4620
|
+
};
|
|
4621
|
+
timeSlider.addEventListener('input', applyTimeFrame);
|
|
4622
|
+
const rangeChoices = [
|
|
4623
|
+
['hour', 1000 * 60 * 60, days < 2],
|
|
4624
|
+
['day', uDay, days > 1],
|
|
4625
|
+
['week', uDay * 7, days > 13],
|
|
4626
|
+
['month', uDay * 28, days > 55],
|
|
4627
|
+
['year', uDay * 356, days > 365]
|
|
4628
|
+
].filter(c => c[2]);
|
|
4629
|
+
if (rangeChoices.length > 1) {
|
|
4630
|
+
const btnRow = document.createElement('div');
|
|
4631
|
+
btnRow.style.cssText = 'display:flex;gap:4px;margin-top:4px;';
|
|
4632
|
+
rangeChoices.forEach(([name, ms]) => {
|
|
4633
|
+
const b = document.createElement('button');
|
|
4634
|
+
b.type = 'button';
|
|
4635
|
+
b.textContent = name;
|
|
4636
|
+
const paint = active => {
|
|
4637
|
+
b.style.cssText = 'background:transparent;border:1px solid '
|
|
4638
|
+
+ (active ? 'currentColor' : 'rgba(128,128,128,0.5)')
|
|
4639
|
+
+ ';color:inherit;font:inherit;font-size:10px;padding:1px 7px;'
|
|
4640
|
+
+ 'border-radius:4px;cursor:pointer;opacity:' + (active ? 1 : 0.55) + ';'
|
|
4641
|
+
+ (active ? 'font-weight:600;' : '');
|
|
4642
|
+
};
|
|
4643
|
+
paint(ms === range);
|
|
4644
|
+
b.addEventListener('click', () => {
|
|
4645
|
+
range = ms;
|
|
4646
|
+
[...btnRow.children].forEach(x => paint(x === b));
|
|
4647
|
+
applyTimeFrame();
|
|
4648
|
+
});
|
|
4649
|
+
btnRow.appendChild(b);
|
|
4650
|
+
});
|
|
4651
|
+
timeRow.appendChild(btnRow);
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
}
|
|
4655
|
+
|
|
4332
4656
|
// Chart-size slider — real engine's own range (legend.js:
|
|
4333
4657
|
// 3196-3202), 100% == scale:1, wired to changeThemeStyle(
|
|
4334
4658
|
// themeId,"scale:"+pct/100,"set")+redrawTheme there; this
|
|
@@ -4353,20 +4677,30 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4353
4677
|
: Math.round((styleNum(rt.style.scale) || 1) * 100);
|
|
4354
4678
|
const sliderLabel = isOpacitySlider ? 'Opacity' : 'Chart size';
|
|
4355
4679
|
sliderRow.innerHTML = sliderLabel + ': <span class="ix-legend-scale-val">' + initialPct + '</span>%';
|
|
4356
|
-
|
|
4680
|
+
// flat's center legend: the slider is INLINE in the chips row
|
|
4681
|
+
// (legend.css 541-550: inline-block, one item width, no wrap,
|
|
4682
|
+
// its input 80px) — a sibling of the chips container so the
|
|
4683
|
+
// row re-render can't wipe it
|
|
4684
|
+
const sliderHost = legendCenter && rowsScroll ? rowsScroll : bodyEl;
|
|
4685
|
+
sliderRow.style.cssText = legendCenter && rowsScroll
|
|
4686
|
+
? 'display:inline-block;margin:0 0 0.5em 1.5em;font-size:11px;opacity:0.8;white-space:nowrap;'
|
|
4687
|
+
: sliderRow.style.cssText;
|
|
4688
|
+
sliderHost.appendChild(sliderRow);
|
|
4357
4689
|
const slider = document.createElement('input');
|
|
4358
4690
|
slider.type = 'range';
|
|
4359
4691
|
slider.min = isOpacitySlider ? '0' : '25';
|
|
4360
4692
|
slider.max = isOpacitySlider ? '100' : '200';
|
|
4361
4693
|
slider.value = String(initialPct);
|
|
4362
|
-
slider.style.cssText =
|
|
4694
|
+
slider.style.cssText = legendCenter && rowsScroll
|
|
4695
|
+
? 'width:80px;vertical-align:middle;margin:0 0 0 0.5em;' // flat's inline slider width (legend.css 548)
|
|
4696
|
+
: 'width:50%;margin-top:0.4em;'; // flat's own inline slider style (legend.js 2392)
|
|
4363
4697
|
slider.addEventListener('input', () => {
|
|
4364
4698
|
const pct = parseInt(slider.value, 10);
|
|
4365
4699
|
sliderRow.querySelector('.ix-legend-scale-val').textContent = pct;
|
|
4366
4700
|
rt.setStyle(isOpacitySlider ? { fillopacity: pct / 100 } : { scale: pct / 100 });
|
|
4367
4701
|
quietRefresh();
|
|
4368
4702
|
});
|
|
4369
|
-
|
|
4703
|
+
sliderHost.appendChild(slider);
|
|
4370
4704
|
}
|
|
4371
4705
|
}
|
|
4372
4706
|
};
|
|
@@ -4434,7 +4768,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4434
4768
|
this._layerBuilders.push(lb);
|
|
4435
4769
|
return lb;
|
|
4436
4770
|
}
|
|
4437
|
-
this._layerBuilders.push(
|
|
4771
|
+
this._layerBuilders.push(asLayerBuilder(nameOrBuilder));
|
|
4438
4772
|
return this;
|
|
4439
4773
|
}
|
|
4440
4774
|
// .Layer() — alias of .layer()
|
|
@@ -4459,8 +4793,28 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4459
4793
|
// the Map(id, opts, map => map.options(...)) callback form worked).
|
|
4460
4794
|
// One microtask lets the caller's whole synchronous chain land first.
|
|
4461
4795
|
await null;
|
|
4462
|
-
|
|
4463
|
-
if (!el)
|
|
4796
|
+
let el = document.getElementById(this.containerId);
|
|
4797
|
+
if (!el) {
|
|
4798
|
+
// flat's own fallback for a missing target div (htmlgui_flat.js
|
|
4799
|
+
// 1055-1082): auto-create it instead of hard-failing, with a
|
|
4800
|
+
// RANDOMIZED id suffix — two calls that both forgot their div
|
|
4801
|
+
// and both name the same id must not collide on one element —
|
|
4802
|
+
// opt.width/height (else 100%), appended to document.body, and a
|
|
4803
|
+
// loud warning; reassigning this.containerId keeps every
|
|
4804
|
+
// downstream lookup (the MapLibre container, the splash cleanup
|
|
4805
|
+
// in createMap) consistent automatically
|
|
4806
|
+
const szOriginalDiv = this.containerId;
|
|
4807
|
+
do {
|
|
4808
|
+
this.containerId = szOriginalDiv + '-' + Math.random().toString(36).slice(2, 8);
|
|
4809
|
+
} while (document.getElementById(this.containerId));
|
|
4810
|
+
console.warn('[ixmaps-gl] container #' + szOriginalDiv + ' not found in the document — creating #' + this.containerId
|
|
4811
|
+
+ ' automatically. Add <div id="' + szOriginalDiv + '"></div> to the page to avoid this warning.');
|
|
4812
|
+
el = document.createElement('div');
|
|
4813
|
+
el.id = this.containerId;
|
|
4814
|
+
el.style.width = (this.mapOptions && this.mapOptions.width) ? String(this.mapOptions.width) : '100%';
|
|
4815
|
+
el.style.height = (this.mapOptions && this.mapOptions.height) ? String(this.mapOptions.height) : '100%';
|
|
4816
|
+
document.body.appendChild(el);
|
|
4817
|
+
}
|
|
4464
4818
|
// flat's init replaces the container's content with its own map page
|
|
4465
4819
|
// (mappage.html via innerHTML), so a page's placeholder inside the
|
|
4466
4820
|
// map div ("connecting to Internet ...", the fuel-price page) goes
|
|
@@ -4562,6 +4916,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4562
4916
|
// build() and every later defineLayer()/refreshTheme() share it
|
|
4563
4917
|
const dataCache = this._dataCache = this._dataCache || new Map();
|
|
4564
4918
|
const runtimes = [];
|
|
4919
|
+
const sphereBackdrops = []; // {type:"Sphere"} layers read before the map exists
|
|
4565
4920
|
// themes whose data comes by name (see namedDataTheme) start empty and
|
|
4566
4921
|
// load on their own once the map is built, as in flat, where every
|
|
4567
4922
|
// theme loads independently: a broker that answers late — or only
|
|
@@ -4593,9 +4948,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4593
4948
|
// instead of colliding with each other under one 'null' entry.
|
|
4594
4949
|
let raw, cacheKey;
|
|
4595
4950
|
const deferred = deferredFeatureLoad(spec, initialZoom);
|
|
4596
|
-
|
|
4951
|
+
// flat's WMS theme type (maptheme.js 8207-8247, the szFlag /\bWMS|IMAGE\b/
|
|
4952
|
+
// realize branch) has NO data of its own — .data({server}) is an
|
|
4953
|
+
// Esri MapServer export endpoint rendered as one image per view.
|
|
4954
|
+
// Here the same theme becomes a MapLibre raster tile source (one
|
|
4955
|
+
// 256px image per XYZ tile via the {bbox-epsg-3857} placeholder —
|
|
4956
|
+
// real cached tiles, not a re-fetched viewport image per pan), see
|
|
4957
|
+
// syncWmsLayers: no records to load, ever
|
|
4958
|
+
const wms = spec.flags.has('WMS') || spec.flags.has('IMAGE');
|
|
4959
|
+
if (deferred || named || wms) {
|
|
4597
4960
|
raw = { type: 'FeatureCollection', features: [] };
|
|
4598
|
-
cacheKey = named ? 'name:' + spec.data.name : 'deferred:' + spec.name;
|
|
4961
|
+
cacheKey = named ? 'name:' + spec.data.name : wms ? 'wms:' + spec.name : 'deferred:' + spec.name;
|
|
4599
4962
|
} else if (spec.data && spec.data.obj) {
|
|
4600
4963
|
raw = await fetchLayerData(spec.data, spec.binding, this._engineOptions);
|
|
4601
4964
|
cacheKey = 'obj:' + spec.name;
|
|
@@ -4618,6 +4981,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4618
4981
|
rt._named = named;
|
|
4619
4982
|
rt._zoomToPending = wantsZoomToExtent(spec.flags);
|
|
4620
4983
|
runtimes.push(rt);
|
|
4984
|
+
if (fc._sphere) sphereBackdrops.push(rt.style);
|
|
4621
4985
|
notifyNewTheme(rt);
|
|
4622
4986
|
_globalThemeRegistry.set(rt.name, rt);
|
|
4623
4987
|
// See findRuntime's own comment on the three real theme-id
|
|
@@ -4676,6 +5040,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4676
5040
|
renderWorldCopies: this._engineOptions.worldcopies !== false
|
|
4677
5041
|
});
|
|
4678
5042
|
engineOptionsByMap.set(map, this._engineOptions);
|
|
5043
|
+
sphereBackdrops.forEach(style => applySphereBackdrop(map, style));
|
|
4679
5044
|
// the live-SVG pane USER charts render into (see chartHostFor) —
|
|
4680
5045
|
// registered here, keyed by this map's options object, so every
|
|
4681
5046
|
// runtime (including later loadProject/map.layer ones) finds it
|
|
@@ -5166,6 +5531,16 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5166
5531
|
const rt = findRuntime(themeId);
|
|
5167
5532
|
if (rt) { rt._hidden = !visible; refresh(); }
|
|
5168
5533
|
},
|
|
5534
|
+
// flat's Api.switchLayer (htmlgui.js 3383, htmlgui_flat.js 1531):
|
|
5535
|
+
// every theme of the LAYER named — setThemeVisible takes one theme
|
|
5536
|
+
// id, this takes the layer name and switches all of its themes
|
|
5537
|
+
switchLayer: (layerName, visible) => {
|
|
5538
|
+
const targets = runtimes.filter(r => r.name === layerName);
|
|
5539
|
+
if (!targets.length) console.warn(`[ixmaps-gl] switchLayer: no layer named "${layerName}"`);
|
|
5540
|
+
targets.forEach(rt => { rt._hidden = !visible; });
|
|
5541
|
+
if (targets.length) refresh();
|
|
5542
|
+
return engineApi;
|
|
5543
|
+
},
|
|
5169
5544
|
getCategoryColor: (themeId, value) => {
|
|
5170
5545
|
const rt = findRuntime(themeId);
|
|
5171
5546
|
if (!rt || !rt.categoryIndexByLabel) return null;
|
|
@@ -5300,9 +5675,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5300
5675
|
for (const rt of targets) {
|
|
5301
5676
|
if (rt._named) { await loadNamedTheme(rt, true); continue; }
|
|
5302
5677
|
const spec = normalizeTheme(rt._definition);
|
|
5303
|
-
//
|
|
5304
|
-
//
|
|
5305
|
-
const raw = spec.
|
|
5678
|
+
// a WMS theme has no records to reload (see build()'s wms
|
|
5679
|
+
// branch) — its raster layer is re-synced by the refresh below
|
|
5680
|
+
const raw = (spec.flags.has('WMS') || spec.flags.has('IMAGE'))
|
|
5681
|
+
? { type: 'FeatureCollection', features: [] }
|
|
5682
|
+
: spec.data && spec.data.obj
|
|
5306
5683
|
? await fetchLayerData(spec.data, rt._specBinding, builder._engineOptions)
|
|
5307
5684
|
: await cachedLayerData(builder._dataCache, spec.data, rt._specBinding, builder._engineOptions, { reload: true }).promise;
|
|
5308
5685
|
rt.replaceFeatures(filterAndJoin(raw, spec, runtimes));
|
|
@@ -5531,8 +5908,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5531
5908
|
let raw;
|
|
5532
5909
|
const deferred = deferredFeatureLoad(spec, map.getZoom());
|
|
5533
5910
|
const named = namedDataTheme(spec);
|
|
5911
|
+
// WMS themes carry no records (see build()'s own wms branch)
|
|
5912
|
+
const wms = spec.flags.has('WMS') || spec.flags.has('IMAGE');
|
|
5534
5913
|
let sourceKey = null;
|
|
5535
|
-
if (deferred || named) {
|
|
5914
|
+
if (deferred || named || wms) {
|
|
5536
5915
|
raw = { type: 'FeatureCollection', features: [] };
|
|
5537
5916
|
} else if (spec.data && spec.data.obj) {
|
|
5538
5917
|
// in memory already — nothing to cache (see build())
|
|
@@ -5552,6 +5931,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5552
5931
|
rt._dataSourceKey = named ? 'name:' + spec.data.name
|
|
5553
5932
|
: sourceKey || JSON.stringify({ url: spec.data && spec.data.url, urls: spec.data && spec.data.urls, type: spec.data && spec.data.type, query: spec.data && spec.data.query, obj: !!(spec.data && spec.data.obj) });
|
|
5554
5933
|
runtimes.push(rt);
|
|
5934
|
+
if (fc._sphere) applySphereBackdrop(map, rt.style);
|
|
5555
5935
|
_globalThemeRegistry.set(rt.name, rt);
|
|
5556
5936
|
if (rt.meta && rt.meta.name && rt.meta.name !== rt.name) _globalThemeRegistry.set(rt.meta.name, rt);
|
|
5557
5937
|
if (themeIdOf(rt) !== rt.name) _globalThemeRegistry.set(themeIdOf(rt), rt);
|
|
@@ -5794,7 +6174,71 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5794
6174
|
if (order !== refreshLayers._lastOrder) { refreshLayers._lastOrder = order; console.info('[ixmaps-gl debug] draw order (bottom → top): ' + order); }
|
|
5795
6175
|
}
|
|
5796
6176
|
overlay.setProps({ layers });
|
|
6177
|
+
// flat's WMS themes render as native raster layers, not deck.gl —
|
|
6178
|
+
// reconcile them with the current runtimes on every rebuild
|
|
6179
|
+
syncWmsLayers();
|
|
6180
|
+
}
|
|
6181
|
+
|
|
6182
|
+
// flat's WMS theme type (szFlag /\bWMS|IMAGE\b/, maptheme.js 8207-8247):
|
|
6183
|
+
// .data({server}) names an Esri MapServer export endpoint, flat draws
|
|
6184
|
+
// ONE f=image request per view (bbox in 4326, the view's pixel size),
|
|
6185
|
+
// opacity from fillOpacity || nOpacity || 0.8. Here the same theme is
|
|
6186
|
+
// a MapLibre raster tile source instead — the SAME export call per
|
|
6187
|
+
// 256px XYZ tile ({bbox-epsg-3857} → bboxSR=3857), which MapLibre
|
|
6188
|
+
// caches, loads progressively and never re-fetches per pan step;
|
|
6189
|
+
// deck.gl's interleaved layers stay on top of it. server is read
|
|
6190
|
+
// from .style() as flat does (maptheme.js 1408: styleObj.server),
|
|
6191
|
+
// or from .data() as this port's own pages write it.
|
|
6192
|
+
// ids of the raster layers this function ever added — lets it skip
|
|
6193
|
+
// map.getStyle() (a deep copy of the whole style) on every rebuild
|
|
6194
|
+
// of a page that has no WMS theme
|
|
6195
|
+
const wmsLayerIds = new Set();
|
|
6196
|
+
function syncWmsLayers() {
|
|
6197
|
+
if (!map || !map.getLayer) return;
|
|
6198
|
+
const idOf = name => 'ixmaps-wms:' + String(name).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
6199
|
+
const desired = new Map();
|
|
6200
|
+
runtimes.forEach(rt => {
|
|
6201
|
+
if (!(rt.flags.has('WMS') || rt.flags.has('IMAGE'))) return;
|
|
6202
|
+
const server = (rt.style && rt.style.server) || (rt._definition && rt._definition.data && rt._definition.data.server);
|
|
6203
|
+
if (server) desired.set(rt.name, { rt, server });
|
|
6204
|
+
});
|
|
6205
|
+
if (!desired.size && !wmsLayerIds.size) return;
|
|
6206
|
+
// a removed WMS theme takes its raster layer with it — as flat's
|
|
6207
|
+
// clearWMS does (ids are sanitized names, compare id-to-id)
|
|
6208
|
+
const ownedIds = new Set([...desired.keys()].map(idOf));
|
|
6209
|
+
[...wmsLayerIds].forEach(id => {
|
|
6210
|
+
if (ownedIds.has(id)) return;
|
|
6211
|
+
wmsLayerIds.delete(id);
|
|
6212
|
+
try { if (map.getLayer(id)) map.removeLayer(id); } catch (e) { /* already gone */ }
|
|
6213
|
+
try { if (map.getSource(id)) map.removeSource(id); } catch (e) { /* already gone */ }
|
|
6214
|
+
});
|
|
6215
|
+
desired.forEach(({ rt, server }, name) => {
|
|
6216
|
+
const id = idOf(name);
|
|
6217
|
+
wmsLayerIds.add(id);
|
|
6218
|
+
const sep = server.indexOf('?') >= 0 ? '&' : '?';
|
|
6219
|
+
const tiles = [server + sep + 'f=image&transparent=true&bbox={bbox-epsg-3857}&bboxSR=3857&size=256,256'];
|
|
6220
|
+
if (!map.getSource(id)) {
|
|
6221
|
+
map.addSource(id, { type: 'raster', tiles, tileSize: 256 });
|
|
6222
|
+
}
|
|
6223
|
+
if (!map.getLayer(id)) {
|
|
6224
|
+
map.addLayer({ id, type: 'raster', source: id, paint: { 'raster-opacity': 1 } });
|
|
6225
|
+
}
|
|
6226
|
+
// flat's own opacity reading (maptheme.js 8240): fillOpacity ||
|
|
6227
|
+
// nOpacity || 0.8 — style "opacity" already folded into
|
|
6228
|
+
// fillopacity by normalizeTheme
|
|
6229
|
+
const opacity = styleNum(rt.style.fillopacity) || 0.8;
|
|
6230
|
+
if ((map.getPaintProperty(id, 'raster-opacity') || 1) !== opacity) {
|
|
6231
|
+
map.setPaintProperty(id, 'raster-opacity', opacity);
|
|
6232
|
+
}
|
|
6233
|
+
const vis = rt._hidden ? 'none' : 'visible';
|
|
6234
|
+
if ((map.getLayoutProperty(id, 'visibility') || 'visible') !== vis) {
|
|
6235
|
+
map.setLayoutProperty(id, 'visibility', vis);
|
|
6236
|
+
}
|
|
6237
|
+
});
|
|
5797
6238
|
}
|
|
6239
|
+
// a style swap (setMapType) drops every native layer — rebuild the
|
|
6240
|
+
// WMS ones once the new style stands
|
|
6241
|
+
map.on('style.load', () => syncWmsLayers());
|
|
5798
6242
|
|
|
5799
6243
|
// isZooming flips true/false on every 'zoomstart'/'zoomend' pair —
|
|
5800
6244
|
// and a scroll-wheel or trackpad zoom fires MANY such pairs per
|
|
@@ -6090,7 +6534,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6090
6534
|
// theirs and are not overridden.
|
|
6091
6535
|
for (const m of ['replace', 'add', 'newTheme', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
6092
6536
|
'refreshTheme', 'redrawTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'setProjection', 'resize',
|
|
6093
|
-
'loadProject', 'project', 'setThemeVisible', 'setThemeTimeFrame', 'show', 'hide', 'getZoom', 'getThemeObj',
|
|
6537
|
+
'loadProject', 'project', 'setThemeVisible', 'switchLayer', 'setThemeTimeFrame', 'show', 'hide', 'getZoom', 'getThemeObj',
|
|
6094
6538
|
'getThemes', 'theme', 'setView']) {
|
|
6095
6539
|
if (!MapBuilder.prototype[m]) {
|
|
6096
6540
|
MapBuilder.prototype[m] = function (...args) {
|
|
@@ -6113,7 +6557,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6113
6557
|
// .catch()/await still sees the real failure — this only guarantees
|
|
6114
6558
|
// the splash doesn't outlive it.
|
|
6115
6559
|
const promise = builder.build().catch(err => {
|
|
6116
|
-
|
|
6560
|
+
// builder.containerId, not the closure's: build() reassigns it when
|
|
6561
|
+
// it had to auto-create a missing container (flat's fallback above)
|
|
6562
|
+
const el = document.getElementById(builder.containerId);
|
|
6117
6563
|
const splash = el && el.querySelector('.ixmaps-splash');
|
|
6118
6564
|
if (splash) hideSplash(splash);
|
|
6119
6565
|
throw err;
|
|
@@ -7519,6 +7965,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7519
7965
|
// independently-rendered systems (deck.gl's icon quad, MapLibre's own
|
|
7520
7966
|
// globe-sphere mesh) competing for the same depth-buffer real estate.
|
|
7521
7967
|
const ICON_LAYER_GLOBE_PARAMETERS = { cullMode: 'none', depthCompare: 'always' };
|
|
7968
|
+
// a transparent 1x1 PNG — the placeholder icon of an external symbol
|
|
7969
|
+
// file still being rasterized (see _buildSequenceChartLayers' urlIcon)
|
|
7970
|
+
const TRANSPARENT_1PX_PNG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
|
|
7522
7971
|
|
|
7523
7972
|
// GLOBE_HORIZON_DEG / isOnVisibleHemisphere: `depthCompare: 'always'`
|
|
7524
7973
|
// above (needed to stop the near-surface z-fighting/half-moon bug —
|
|
@@ -7790,6 +8239,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7790
8239
|
switch (shape) {
|
|
7791
8240
|
case 'squarefull': return Math.abs(dx) <= r && Math.abs(dy) <= r;
|
|
7792
8241
|
case 'square': return Math.abs(dx) <= r * Math.SQRT1_2 && Math.abs(dy) <= r * Math.SQRT1_2;
|
|
8242
|
+
case 'roundrectfull': return Math.abs(dx) <= r && Math.abs(dy) <= r;
|
|
7793
8243
|
case 'roundrect': return Math.abs(dx) <= r * 0.8 && Math.abs(dy) <= r * 0.8;
|
|
7794
8244
|
case 'diamond': case 'carot': return Math.abs(dx) + Math.abs(dy) <= r;
|
|
7795
8245
|
case 'empty': return false;
|
|
@@ -8804,7 +9254,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8804
9254
|
const word = w => { const re = new RegExp('\\b' + w + '\\b'); return [...flags].some(f => re.test(f)); };
|
|
8805
9255
|
const info = {
|
|
8806
9256
|
sort: word('SORT'), up: word('UP'), down: word('DOWN'),
|
|
8807
|
-
star: has('STAR') && !
|
|
9257
|
+
star: has('STAR') && !flatFlag(flags, 'CENTER'), horz: has('HORZ') && !flatFlag(flags, 'CENTER'),
|
|
8808
9258
|
expand: has('EXPAND'), expandmax: has('EXPANDMAX'), linear: has('LINEAR'), sizep1: has('SIZEP1'),
|
|
8809
9259
|
compress: has('COMPRESS'), compressmax: has('COMPRESSMAX')
|
|
8810
9260
|
};
|
|
@@ -9052,6 +9502,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9052
9502
|
// which every other caller here still relies on unchanged.
|
|
9053
9503
|
function resolvePartsClass(value, partsA) {
|
|
9054
9504
|
if (!partsA || isNaN(value)) return null;
|
|
9505
|
+
// explicit ranges: a value below the first break is class 0, as flat's
|
|
9506
|
+
// class loop (value < parts[0].max) puts it
|
|
9507
|
+
if (partsA.length && partsA[0].clampLow && value < partsA[0].min) return 0;
|
|
9055
9508
|
for (let i = 0; i < partsA.length; i++) {
|
|
9056
9509
|
const isLast = i === partsA.length - 1;
|
|
9057
9510
|
const inLower = value >= partsA[i].min;
|
|
@@ -9400,6 +9853,27 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9400
9853
|
}
|
|
9401
9854
|
return bins;
|
|
9402
9855
|
}
|
|
9856
|
+
// Explicit class breaks: .style({ranges}) with the RANGES flag (flat,
|
|
9857
|
+
// maptheme.js 2324 / 12905-12960). n+1 break values (a comma or "|"
|
|
9858
|
+
// separated string, or an array) are n classes [r_i, r_i+1] — a class
|
|
9859
|
+
// max is nudged by 1e-9 as flat does, so a break value belongs to the class
|
|
9860
|
+
// below it. Not for CATEGORICAL (there the values are exact categories).
|
|
9861
|
+
// null when the theme has no usable explicit ranges.
|
|
9862
|
+
function explicitRangeBreaks(style, flags) {
|
|
9863
|
+
if (!flags || !flags.has('RANGES') || flags.has('CATEGORICAL')) return null;
|
|
9864
|
+
let r = style && style.ranges;
|
|
9865
|
+
if (typeof r === 'string') r = r.split(r.includes('|') ? '|' : ',');
|
|
9866
|
+
if (!Array.isArray(r)) return null;
|
|
9867
|
+
r = r.map(Number);
|
|
9868
|
+
return r.length > 1 && r.every(isFinite) ? r : null;
|
|
9869
|
+
}
|
|
9870
|
+
function explicitRangeParts(breaks) {
|
|
9871
|
+
const parts = [];
|
|
9872
|
+
for (let i = 0; i < breaks.length - 1; i++) parts.push({ min: breaks[i], max: breaks[i + 1] + 0.000000001 });
|
|
9873
|
+
parts[0].clampLow = true;
|
|
9874
|
+
return parts;
|
|
9875
|
+
}
|
|
9876
|
+
|
|
9403
9877
|
function flatRangeParts(values, nMin, nMax, nParts, flags, centerValue) {
|
|
9404
9878
|
const rawMin = nMin, rawMax = nMax;
|
|
9405
9879
|
if (centerValue != null && !isNaN(centerValue)) {
|
|
@@ -9523,6 +9997,33 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9523
9997
|
// On copies of the features' properties — the page's data is not changed.
|
|
9524
9998
|
// AGGREGATE themes: not here — flat computes field100 on the aggregated
|
|
9525
9999
|
// sums (aggregateField100).
|
|
10000
|
+
// .style({weights}) — flat's weighted values: every record's per-field
|
|
10001
|
+
// value × its field's weight, BEFORE the field100 arithmetic (the
|
|
10002
|
+
// per-record multiply in addItemValues, maptheme.js 9007-9009; the
|
|
10003
|
+
// aggregate loader reads the weight per value field at 10506-10508).
|
|
10004
|
+
// One number, or a pipe list with one weight per value field; a field
|
|
10005
|
+
// beyond the list takes the first weight (flat: `szWeightsA[i] ||
|
|
10006
|
+
// szWeightsA[0]`), the aggregate loader instead repeats the LAST one
|
|
10007
|
+
// (10262-10266) — `repeatLast`; no weights at all leaves the values raw.
|
|
10008
|
+
// The Milano page: weights 0.1 over km-long street lengths + FRACTION →
|
|
10009
|
+
// cars per 100 m
|
|
10010
|
+
function fieldWeightOf(style, nFields, repeatLast) {
|
|
10011
|
+
const w = style && style.weights;
|
|
10012
|
+
if (w == null || w === '') return null;
|
|
10013
|
+
const list = (Array.isArray(w) ? w.join('|') : String(w)).split('|').map(v => parseFloat(v));
|
|
10014
|
+
if (!list.some(v => !isNaN(v))) return null;
|
|
10015
|
+
if (repeatLast) { const last = list[list.length - 1]; while (list.length < nFields) list.push(last); }
|
|
10016
|
+
return i => list[i] || list[0] || 1;
|
|
10017
|
+
}
|
|
10018
|
+
// flat's weighted record for the AGGREGATE path (maptheme.js 10506-10508):
|
|
10019
|
+
// the value fields' own values weighted — the class value and the field100
|
|
10020
|
+
// value stay raw, exactly as flat reads them
|
|
10021
|
+
function weightedRecord(f, weightOf) {
|
|
10022
|
+
const p = Object.assign({}, f.properties);
|
|
10023
|
+
if (p.value !== undefined && !isNaN(p.value)) p.value = p.value * weightOf(0);
|
|
10024
|
+
if (Array.isArray(p.series)) p.series = p.series.map((v, i) => (isNaN(v) ? v : v * weightOf(i)));
|
|
10025
|
+
return Object.assign({}, f, { properties: p });
|
|
10026
|
+
}
|
|
9526
10027
|
function applyField100(features, binding, flags, style) {
|
|
9527
10028
|
if (!binding || !binding.value || !Array.isArray(features)) return features;
|
|
9528
10029
|
// AGGREGATE: flat applies field100 to the aggregated sums
|
|
@@ -9532,7 +10033,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9532
10033
|
const fields = String(binding.value).split('|');
|
|
9533
10034
|
const auto100 = flags.has('AUTO100') && !flags.has('AGGREGATE');
|
|
9534
10035
|
const seriesDifference = flags.has('DIFFERENCE') && fields.length > 1;
|
|
9535
|
-
|
|
10036
|
+
// flat's weights run even without a field100 (addItemValues multiplies
|
|
10037
|
+
// every record's values, maptheme.js 9007-9009)
|
|
10038
|
+
const weightOf = fieldWeightOf(style, fields.length, false);
|
|
10039
|
+
if (!f100 && !auto100 && !seriesDifference && !weightOf) return features;
|
|
9536
10040
|
const f100A = f100 ? String(f100).split('|') : null;
|
|
9537
10041
|
const fractionScale = styleNum(style && style.fractionscale) || 1;
|
|
9538
10042
|
const names = fields.map(field => (field[0] === '!' ? field.slice(1) : field));
|
|
@@ -9541,6 +10045,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9541
10045
|
if (!f || !f.properties) return f;
|
|
9542
10046
|
const props = Object.assign({}, f.properties);
|
|
9543
10047
|
let values = names.map(name => parseFloat(f.properties[name]));
|
|
10048
|
+
// the weights run BEFORE the field100 arithmetic, as in flat's
|
|
10049
|
+
// addItemValues — the FRACTION of a weighted value is the weighted
|
|
10050
|
+
// per-unit value
|
|
10051
|
+
if (weightOf) values = values.map((v, i) => (isNaN(v) ? v : v * weightOf(i)));
|
|
9544
10052
|
if (f100A || auto100) {
|
|
9545
10053
|
const sum = auto100 ? values.reduce((a, v) => a + (isNaN(v) ? 0 : v), 0) : 0;
|
|
9546
10054
|
values = values.map((v, i) => {
|
|
@@ -9618,11 +10126,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9618
10126
|
out._valueMax = nMax;
|
|
9619
10127
|
const sorted = values.slice().sort((a, b) => a - b);
|
|
9620
10128
|
out._valueMedian = sorted[Math.floor((sorted.length - 1) / 2)];
|
|
9621
|
-
const
|
|
10129
|
+
const breaks = explicitRangeBreaks(style, flags);
|
|
10130
|
+
const nParts = breaks ? breaks.length - 1
|
|
10131
|
+
: Math.trunc(styleNum(style.classes)) || colorSchemeClassCount(style.colorscheme) || DEFAULT_RANGE_CLASSES;
|
|
9622
10132
|
const placeholders = new Array(nParts).fill('');
|
|
9623
|
-
const colorsRgb = resolveClassColors(style.colorscheme, placeholders, style.classes);
|
|
10133
|
+
const colorsRgb = resolveClassColors(style.colorscheme, placeholders, breaks ? nParts : style.classes);
|
|
9624
10134
|
|
|
9625
|
-
out.partsA = flatRangeParts(values, nMin, nMax, nParts, flags, styleNum(style.rangecentervalue));
|
|
10135
|
+
out.partsA = breaks ? explicitRangeParts(breaks) : flatRangeParts(values, nMin, nMax, nParts, flags, styleNum(style.rangecentervalue));
|
|
9626
10136
|
out._nRangeParts = nParts; // the configured count; partsA may have fewer (all values equal)
|
|
9627
10137
|
|
|
9628
10138
|
out.categoryLabels = out.partsA.map(p => `${formatValue(p.min)} - ${formatValue(p.max)}`);
|
|
@@ -9760,6 +10270,15 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9760
10270
|
// (maptheme.js 11086-11101): each value becomes the next minus it
|
|
9761
10271
|
// (RELATIVE: in % of it when it is above field100min, else 0), the last
|
|
9762
10272
|
// one is dropped
|
|
10273
|
+
// How a record joins its cell's running value (flat's MAX / MIN / SUM
|
|
10274
|
+
// aggregation, maptheme.js 10880-10901): MAX / MIN keep the extreme
|
|
10275
|
+
// (a missing part counts 0, as in flat), SUM adds. `first`: the cell's
|
|
10276
|
+
// first record, which just starts the value.
|
|
10277
|
+
function aggCombiner(flags) {
|
|
10278
|
+
const max = flags.has('MAX'), min = flags.has('MIN');
|
|
10279
|
+
return (acc, v, first) => (first ? v : max ? Math.max(acc || 0, v) : min ? Math.min(acc || 0, v) : acc + v);
|
|
10280
|
+
}
|
|
10281
|
+
|
|
9763
10282
|
function cellAggregatedValues(cell, flags, style) {
|
|
9764
10283
|
const useMean = flags.has('MEAN');
|
|
9765
10284
|
const values = cell.sums.map((s, i) => (useMean && cell.counts[i] > 0 ? s / cell.counts[i] : s));
|
|
@@ -9825,6 +10344,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9825
10344
|
// relocates a field aggregation, maptheme.js 11141) — instead of a grid
|
|
9826
10345
|
function aggregateOnGrid(features, zoom, cellPx, flags, post, field) {
|
|
9827
10346
|
const relocate = flags.has('RELOCATE') || !!field;
|
|
10347
|
+
const combine = aggCombiner(flags);
|
|
10348
|
+
// MAX / MIN aggregation: the cell IS one defined record — the one
|
|
10349
|
+
// holding the biggest (smallest) size — so its title follows THAT
|
|
10350
|
+
// record, not the first one's (flat re-sets the merging record's
|
|
10351
|
+
// szTitle under __fMax, maptheme.js 10870-10874; here it is the
|
|
10352
|
+
// record that actually holds it)
|
|
10353
|
+
const maxAgg = flags.has('MAX'), minAgg = flags.has('MIN');
|
|
9828
10354
|
const cells = new Map();
|
|
9829
10355
|
for (const f of features) {
|
|
9830
10356
|
const [lng, lat] = f.geometry.coordinates;
|
|
@@ -9846,16 +10372,30 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9846
10372
|
(cell.raws = cell.raws || []).push(f.properties.raw);
|
|
9847
10373
|
}
|
|
9848
10374
|
cell.sumX += p.x; cell.sumY += p.y; cell.n++;
|
|
9849
|
-
cell.
|
|
9850
|
-
|
|
9851
|
-
if (
|
|
10375
|
+
const first = cell.n === 1;
|
|
10376
|
+
cell.value = combine(cell.value, f.properties.value, first);
|
|
10377
|
+
if (f.properties.classValue !== undefined) cell.classValue = combine(cell.classValue, f.properties.classValue, first);
|
|
10378
|
+
if (post) cell.value100 = combine(cell.value100, f.properties.value100 || 0, first);
|
|
9852
10379
|
// an AGGREGATE / CATEGORICAL pie's record (flatPieRecord): its parts
|
|
9853
10380
|
// and size added to the cell's item as flat does (flatPieAccumulate)
|
|
9854
10381
|
if (f.properties.pie) cell.pie = flatPieAccumulate(cell.pie || null, f.properties.pie, flags);
|
|
9855
10382
|
const series = f.properties.series;
|
|
9856
10383
|
if (series) {
|
|
9857
10384
|
if (!cell.series) cell.series = new Array(series.length).fill(0);
|
|
9858
|
-
for (let i = 0; i < series.length; i++) cell.series[i]
|
|
10385
|
+
for (let i = 0; i < series.length; i++) cell.series[i] = combine(cell.series[i], series[i], first);
|
|
10386
|
+
}
|
|
10387
|
+
// a multi-field SEQUENCE record's size-field value (flat's
|
|
10388
|
+
// nValueSize): MAX/MIN/summed into the item's nSize, as flat does
|
|
10389
|
+
if (f.properties.size !== undefined) cell.size = combine(typeof cell.size === 'number' ? cell.size : 0, f.properties.size, first);
|
|
10390
|
+
// MAX / MIN: which record the cell's title belongs to (see the
|
|
10391
|
+
// comment at the top — the size field carries the magnitude, else
|
|
10392
|
+
// the combined value)
|
|
10393
|
+
if (maxAgg || minAgg) {
|
|
10394
|
+
const m = typeof f.properties.size === 'number' ? Math.abs(f.properties.size) : Math.abs(f.properties.value || 0);
|
|
10395
|
+
if (cell.titleMax === undefined || (maxAgg ? m > cell.titleMax : m < cell.titleMax)) {
|
|
10396
|
+
cell.titleMax = m;
|
|
10397
|
+
cell.titleFrom = f;
|
|
10398
|
+
}
|
|
9859
10399
|
}
|
|
9860
10400
|
}
|
|
9861
10401
|
const out = [];
|
|
@@ -9866,14 +10406,15 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9866
10406
|
const geometry = { type: 'Point', coordinates: [ll.lng, ll.lat] };
|
|
9867
10407
|
const hasClass = cell.first.properties.classValue !== undefined;
|
|
9868
10408
|
let props = cell.n > 1
|
|
9869
|
-
// titleRaw: the cell's
|
|
9870
|
-
//
|
|
10409
|
+
// titleRaw: the record the cell's title belongs to — with MAX/MIN
|
|
10410
|
+
// the one holding the max (min), else the first (flat's itemTitle)
|
|
9871
10411
|
? { cluster: true, point_count: cell.n, value: cell.value, ...(hasClass ? { classValue: cell.classValue } : {}),
|
|
9872
|
-
titleRaw: cell.first.properties.raw || cell.first.properties,
|
|
10412
|
+
titleRaw: (cell.titleFrom || cell.first).properties.raw || (cell.titleFrom || cell.first).properties,
|
|
9873
10413
|
...(cell.raws && cell.raws.length ? { raws: cell.raws } : {}) }
|
|
9874
10414
|
: cell.first.properties;
|
|
9875
10415
|
if (cell.series && cell.n > 1) props = Object.assign({}, props, { series: cell.series });
|
|
9876
10416
|
if (cell.pie) props = Object.assign({}, props, { pie: cell.pie });
|
|
10417
|
+
if (typeof cell.size === 'number' && cell.n > 1) props = Object.assign({}, props, { size: cell.size });
|
|
9877
10418
|
// a field aggregation's cell key (the field value): the categories of
|
|
9878
10419
|
// one cell are merged by it (groupCoLocated), not by their positions
|
|
9879
10420
|
if (field) props = Object.assign({}, props, { aggKey: cell.key });
|
|
@@ -9937,7 +10478,18 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9937
10478
|
let cell = cells.get(key);
|
|
9938
10479
|
if (!cell) { cell = { sumX: 0, sumY: 0, n: 0, cx: snapped.x, cy: snapped.y, counts: new Array(n).fill(0), recordCounts: new Array(n).fill(0), classTotal: undefined }; cells.set(key, cell); }
|
|
9939
10480
|
cell.sumX += p.x; cell.sumY += p.y; cell.n++;
|
|
9940
|
-
|
|
10481
|
+
// the cell's title: with MAX/MIN aggregation the member holding the
|
|
10482
|
+
// biggest (smallest) size (the cell IS that one item), else the
|
|
10483
|
+
// first — flat's itemTitle
|
|
10484
|
+
const maxAgg = flags.has('MAX'), minAgg = flags.has('MIN');
|
|
10485
|
+
const titleOf = f => f.properties.titleRaw || f.properties.raw || null;
|
|
10486
|
+
if (maxAgg || minAgg) {
|
|
10487
|
+
const m = typeof f.properties.size === 'number' ? Math.abs(f.properties.size) : Math.abs(f.properties.total || f.properties.value || 0);
|
|
10488
|
+
if (cell.titleMax === undefined || (maxAgg ? m > cell.titleMax : m < cell.titleMax)) {
|
|
10489
|
+
cell.titleMax = m;
|
|
10490
|
+
cell.titleRaw = titleOf(f);
|
|
10491
|
+
}
|
|
10492
|
+
} else if (!cell.titleRaw) cell.titleRaw = titleOf(f);
|
|
9941
10493
|
// the members' own records, for the tooltip's {{theme.item.data}} —
|
|
9942
10494
|
// flat's __add_data tables them (numbered, up to 50) for aggregated
|
|
9943
10495
|
// items too; references only, so no copy is made. A member can itself
|
|
@@ -9952,6 +10504,18 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9952
10504
|
}
|
|
9953
10505
|
cell.counts[f.properties.cat] += f.properties.value;
|
|
9954
10506
|
if (f.properties.classValue !== undefined) cell.classTotal = (cell.classTotal || 0) + f.properties.classValue;
|
|
10507
|
+
// a multi-field SEQUENCE cell's per-field parts and its size-field
|
|
10508
|
+
// value (see _buildAggregationIndex) — combined with the theme's
|
|
10509
|
+
// own MAX/MIN/sum combiner, so a re-grouped cell keeps them
|
|
10510
|
+
const combineSeries = cell.combineSeries || (cell.combineSeries = aggCombiner(flags));
|
|
10511
|
+
const memberSeries = f.properties.series;
|
|
10512
|
+
if (memberSeries) {
|
|
10513
|
+
if (!cell.series) cell.series = new Array(memberSeries.length).fill(0);
|
|
10514
|
+
for (let i = 0; i < memberSeries.length; i++) cell.series[i] = combineSeries(cell.series[i], memberSeries[i], cell.n === 1);
|
|
10515
|
+
}
|
|
10516
|
+
if (typeof f.properties.size === 'number') {
|
|
10517
|
+
cell.size = combineSeries(typeof cell.size === 'number' ? cell.size : 0, f.properties.size, cell.n === 1);
|
|
10518
|
+
}
|
|
9955
10519
|
// point_count is the aggregated-record count of a multi-record
|
|
9956
10520
|
// cell (aggregateOnGrid; absent on a single-record cell)
|
|
9957
10521
|
// — tracked separately from counts (the summed bound VALUE) purely
|
|
@@ -9965,7 +10529,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9965
10529
|
geometry: { type: 'Point', coordinates: [ll.lng, ll.lat] },
|
|
9966
10530
|
properties: { counts: cell.counts, total: cell.counts.reduce((a, c) => a + c, 0), recordCounts: cell.recordCounts, ...(cell.classTotal !== undefined ? { classTotal: cell.classTotal } : {}),
|
|
9967
10531
|
...(cell.raws && cell.raws.length ? { raws: cell.raws } : {}),
|
|
9968
|
-
...(cell.titleRaw ? { titleRaw: cell.titleRaw } : {})
|
|
10532
|
+
...(cell.titleRaw ? { titleRaw: cell.titleRaw } : {}),
|
|
10533
|
+
...(cell.series ? { series: cell.series } : {}),
|
|
10534
|
+
...(typeof cell.size === 'number' ? { size: cell.size } : {}) }
|
|
9969
10535
|
};
|
|
9970
10536
|
});
|
|
9971
10537
|
}
|
|
@@ -10369,6 +10935,33 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10369
10935
|
// pure: resolveComposedColor → see "Classification — pure functions"
|
|
10370
10936
|
_resolveComposedColor(props) { return resolveComposedColor(this, props); }
|
|
10371
10937
|
|
|
10938
|
+
// flat's DOPACITY value inside the COMPOSECOLOR branch (maptheme.js
|
|
10939
|
+
// 13489-13505): nMyValue = the item's raw weighted value sum /
|
|
10940
|
+
// ((field100/100) || 1), the max the color blend's own (nMax). The
|
|
10941
|
+
// raw weighted sum is reconstructed from the part values the field100
|
|
10942
|
+
// arithmetic left in the properties — those carry the arithmetic's own
|
|
10943
|
+
// multiplier (FRACTION ×fractionscale, PERMILLE ×1000, the default
|
|
10944
|
+
// percent ×100, see applyField100), which must be divided back out:
|
|
10945
|
+
// flat's nValueSum is the RAW weighted sum, whatever the flags.
|
|
10946
|
+
// (RELATIVE/INVERT subtract after the multiply — not reconstructable
|
|
10947
|
+
// from the parts; flat's own composed branch never pairs them.)
|
|
10948
|
+
_composedDopacityAlpha(props) {
|
|
10949
|
+
const fields = this._multiFields || [];
|
|
10950
|
+
const f100A = this.binding && this.binding.field100 ? String(this.binding.field100).split('|') : null;
|
|
10951
|
+
const multiplier = this.flags.has('FRACTION') ? (styleNum(this.style.fractionscale) || 1)
|
|
10952
|
+
: this.flags.has('PERMILLE') ? 1000 : 100;
|
|
10953
|
+
let sum = 0;
|
|
10954
|
+
fields.forEach((field, i) => {
|
|
10955
|
+
const part = parseFloat(props[field]);
|
|
10956
|
+
if (isNaN(part)) return;
|
|
10957
|
+
const v100 = f100A ? parseFloat(props[f100A[Math.min(i, f100A.length - 1)]]) : NaN;
|
|
10958
|
+
sum += isNaN(v100) ? part : (part * v100) / multiplier;
|
|
10959
|
+
});
|
|
10960
|
+
const v100_0 = f100A ? parseFloat(props[f100A[0]]) : NaN;
|
|
10961
|
+
const nMyValue = sum / ((v100_0 / 100) || 1);
|
|
10962
|
+
return composedDopacityAlpha(this.style, this.flags, nMyValue, this._composeColorMax || 1);
|
|
10963
|
+
}
|
|
10964
|
+
|
|
10372
10965
|
// pure: buildPartsA → see "Classification — pure functions"
|
|
10373
10966
|
_buildPartsA() { const r = computeRangeClasses(this.features, this.binding, this.style, this.flags, v => this._formatTooltipValue(v)); if (r) Object.assign(this, r); }
|
|
10374
10967
|
|
|
@@ -10437,11 +11030,18 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10437
11030
|
// 7-class color once it knows each cell's actual aggregated total.
|
|
10438
11031
|
if (this._multiFields && this._isMultiFieldSequence()) {
|
|
10439
11032
|
// one bucket; each record carries its field values as the chart's
|
|
10440
|
-
// parts
|
|
10441
|
-
//
|
|
11033
|
+
// parts — and, the same array, as `series` so that an AGGREGATE
|
|
11034
|
+
// theme's grid cells combine them PER FIELD (aggregateOnGrid), as
|
|
11035
|
+
// flat accumulates its per-field nValuesA (maptheme.js 10876-10889)
|
|
11036
|
+
// — plus its size-field value (flat's nValueSize, the item nSize
|
|
11037
|
+
// MAX/MIN/sum). Its value — the draw order — is the first field's,
|
|
11038
|
+
// as flat's chart node value (NYC status: value="63" = e-bikes)
|
|
11039
|
+
const sizeField = this.binding.size && this.binding.size !== '$item$' ? this.binding.size : null;
|
|
10442
11040
|
this._featuresByCategory = [sourceFeatures.map(f => {
|
|
10443
11041
|
const parts = this._multiFields.map(k => parseFloat(f.properties[k]) || 0);
|
|
10444
|
-
|
|
11042
|
+
const size = sizeField ? Math.abs(parseFloat(f.properties[sizeField]) || 0) : undefined;
|
|
11043
|
+
return { type: 'Feature', geometry: f.geometry,
|
|
11044
|
+
properties: { value: parts[0], parts, series: parts, ...(size !== undefined ? { size } : {}), raw: f.properties } };
|
|
10445
11045
|
})];
|
|
10446
11046
|
this._clusterIndices = null;
|
|
10447
11047
|
this._clusterRadiusPx = null;
|
|
@@ -10888,7 +11488,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10888
11488
|
const post = this._seriesDifference
|
|
10889
11489
|
? (v, v100, series) => (series ? cellAggregatedValues({ sums: series, counts: series.map(() => 1) }, this.flags, this.style)[0] : v)
|
|
10890
11490
|
: this._aggregateField100 ? (v, v100) => aggregateField100(v, v100, this.flags, this.style) : undefined;
|
|
10891
|
-
|
|
11491
|
+
// flat's weights multiply every record's value fields as they are
|
|
11492
|
+
// accumulated (maptheme.js 10506-10508), before the sums' field100
|
|
11493
|
+
// arithmetic above — a weighted SUM, not a sum weighted afterward
|
|
11494
|
+
const weightOf = fieldWeightOf(this.style, (this._multiFields || [this.binding && this.binding.value]).length, true);
|
|
11495
|
+
this._clusterIndices = this._featuresByCategory.map(feats =>
|
|
11496
|
+
new GridAggregateIndex(radiusPx, this.flags, post, aggField).load(weightOf ? feats.map(f => weightedRecord(f, weightOf)) : feats));
|
|
10892
11497
|
}
|
|
10893
11498
|
|
|
10894
11499
|
// Reverted from the source-accurate two-flat-circle GLOW (radius*6
|
|
@@ -11139,6 +11744,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11139
11744
|
const raw = props.raw || props.titleRaw;
|
|
11140
11745
|
const title = this.binding.title && raw && raw[this.binding.title] != null ? String(raw[this.binding.title]) : '';
|
|
11141
11746
|
const aggregated = !!(props.counts || props.cluster);
|
|
11747
|
+
// MAX / MIN aggregation: the item IS one defined record — the one
|
|
11748
|
+
// holding the max (min) — so it carries that record's real title;
|
|
11749
|
+
// "(n)" is the label of a summed/mean aggregate
|
|
11750
|
+
if (this.flags.has('MAX') || this.flags.has('MIN')) return title;
|
|
11142
11751
|
if (aggregated && this._clusterRadiusPx && !this._clusterField) {
|
|
11143
11752
|
const n = groupRecordCount(props);
|
|
11144
11753
|
if (!title || n > 3) return '(' + n + ')';
|
|
@@ -11498,6 +12107,15 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11498
12107
|
// unmatched polygon, so the donor's fill serves no purpose once
|
|
11499
12108
|
// superseded.
|
|
11500
12109
|
if (this._isChoroplethGeometryDonor) return [];
|
|
12110
|
+
// flat's WMS theme (one server image over the view) renders here as
|
|
12111
|
+
// a native MapLibre raster source+layer managed by syncWmsLayers —
|
|
12112
|
+
// not as deck.gl layers — so its own build returns nothing
|
|
12113
|
+
if (this.flags.has('WMS') || this.flags.has('IMAGE')) return [];
|
|
12114
|
+
// flat's POSITION theme (maptheme.js 8328-8353): loads its data and
|
|
12115
|
+
// draws nothing — it only exists as this layer's id→position provider
|
|
12116
|
+
// for the join (joinChartPositions reads its features, its binding.id
|
|
12117
|
+
// against the consumers' lookup key), so no renderer runs for it
|
|
12118
|
+
if (this.flags.has('POSITION')) return [];
|
|
11501
12119
|
if (this.flags.has('FEATURE') || this.flags.has('FEATURES')) {
|
|
11502
12120
|
return featuresHiddenByScale(this.style, liveZoom) ? [] : this._buildFeaturesLayers(liveZoom, bbox);
|
|
11503
12121
|
}
|
|
@@ -11539,6 +12157,20 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11539
12157
|
// each shape by its class), not one flat fill
|
|
11540
12158
|
const byCategory = filled && this.flags.has('CATEGORICAL') && this.categoryIndexByLabel && this.binding.value
|
|
11541
12159
|
&& this.categoryColorsRgb && this.categoryColorsRgb.length;
|
|
12160
|
+
// a NUMERIC bound value classes every shape too (flat's
|
|
12161
|
+
// distributeValues → partsA; paintShape, maptheme.js 13991-14018,
|
|
12162
|
+
// paints each shape in `colorScheme[nIndex]` — a line as
|
|
12163
|
+
// fill:none;stroke:class-color, a polygon filled with it): the
|
|
12164
|
+
// TELRAAM speed segments, green→red by v85
|
|
12165
|
+
const byClass = !byCategory && this._rangeClassed && this.partsA
|
|
12166
|
+
&& this.categoryColorsRgb && this.categoryColorsRgb.length
|
|
12167
|
+
// "$item$" (or a field list) is no numeric field: a plain FEATURE
|
|
12168
|
+
// layer keeps its one fill color
|
|
12169
|
+
&& this.binding.value && this.binding.value !== '$item$' && !String(this.binding.value).includes('|');
|
|
12170
|
+
const classColorOf = f => {
|
|
12171
|
+
const cls = this._resolvePartsClass(parseFloat(f.properties && f.properties[this.binding.value]));
|
|
12172
|
+
return this.classRgb(cls);
|
|
12173
|
+
};
|
|
11542
12174
|
const catAlpha = Math.round(255 * (styleNum(this.style.fillopacity) || 1));
|
|
11543
12175
|
const nodataRgb = parseCssColor(this.style.nodatacolor) || [255, 255, 255];
|
|
11544
12176
|
const shadow = filled && flatShadowOn(this.style, this.features.length, zoom)
|
|
@@ -11558,14 +12190,19 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11558
12190
|
// flat puts fillopacity into the shape's fill-opacity only
|
|
11559
12191
|
// (maptheme.js 13407, 14071-14078): the outline stays opaque
|
|
11560
12192
|
// (a fill color's own alpha — rgba(…, 0.5) — times fillopacity)
|
|
11561
|
-
...(byCategory ? { updateTriggers: { getFillColor: JSON.stringify(this.categoryColorsRgb) } } : {}),
|
|
12193
|
+
...(byCategory || byClass ? { updateTriggers: { getFillColor: JSON.stringify(this.categoryColorsRgb), getLineColor: JSON.stringify(this.categoryColorsRgb) } } : {}),
|
|
11562
12194
|
getFillColor: byCategory
|
|
11563
12195
|
? f => {
|
|
11564
12196
|
const cat = this._resolveClassIndex(f.properties[this.binding.value]);
|
|
11565
12197
|
return [...(cat != null ? (this.categoryColorsRgb || [])[cat] || nodataRgb : nodataRgb).slice(0, 3), catAlpha];
|
|
11566
12198
|
}
|
|
12199
|
+
: byClass ? f => [...classColorOf(f).slice(0, 3), catAlpha]
|
|
11567
12200
|
: filled ? [...hexOrNamedToRgb(raw).slice(0, 3), Math.round(255 * cssColorAlpha(raw) * (styleNum(this.style.fillopacity) || 1))] : [0, 0, 0, 0],
|
|
11568
|
-
getLineColor:
|
|
12201
|
+
getLineColor: byClass
|
|
12202
|
+
// flat's paintShape strokes a line shape with its class color
|
|
12203
|
+
// (fill:none;stroke:class-color), at stroke-opacity fillopacity
|
|
12204
|
+
? f => [...classColorOf(f).slice(0, 3), catAlpha]
|
|
12205
|
+
: this.style.linecolor
|
|
11569
12206
|
? withAlpha(hexOrNamedToRgb(styleLineColor(this.style.linecolor)), cssColorAlpha(styleLineColor(this.style.linecolor)))
|
|
11570
12207
|
: [130, 130, 130],
|
|
11571
12208
|
lineWidthMinPixels: styleNum(this.style.linewidth) || 1,
|
|
@@ -11655,12 +12292,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11655
12292
|
const isComposeColor = this.flags.has('COMPOSECOLOR');
|
|
11656
12293
|
const baseOpacity = resolveFillOpacity(this.style, this.mapOptions, zoom);
|
|
11657
12294
|
// DOPACITY needs a PER-FEATURE opacity, which deck.gl only supports
|
|
11658
|
-
// via
|
|
11659
|
-
// prop is one number for every feature) — see
|
|
11660
|
-
//
|
|
11661
|
-
//
|
|
11662
|
-
//
|
|
11663
|
-
|
|
12295
|
+
// via the color accessors' own alpha channel (its layer-level
|
|
12296
|
+
// `opacity` prop is one number for every feature) — see
|
|
12297
|
+
// _resolveDopacityAlpha. COMPOSECOLOR runs DOPACITY too, with its own
|
|
12298
|
+
// formula on the item's value sum (flat maptheme.js 13485-13506,
|
|
12299
|
+
// composedDopacityAlpha) — it is only the DOMINANT/range
|
|
12300
|
+
// _resolveDopacityAlpha that has no single driving value there
|
|
12301
|
+
const dopacityActive =
|
|
11664
12302
|
(this.flags.has('DOPACITY') || this.flags.has('DOPACITYMIN') || this.flags.has('DOPACITYMAX') || this.flags.has('DOPACITYMINMAX'));
|
|
11665
12303
|
// AGGREGATE CATEGORICAL: every polygon's parts and dominant class
|
|
11666
12304
|
// (aggregatedCategoricalClass), the biggest part per class over all
|
|
@@ -11703,7 +12341,15 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11703
12341
|
return {
|
|
11704
12342
|
type: 'Feature',
|
|
11705
12343
|
geometry: f.geometry,
|
|
11706
|
-
properties: {
|
|
12344
|
+
properties: {
|
|
12345
|
+
raw: f.properties, composedColor: this._resolveComposedColor(f.properties),
|
|
12346
|
+
// flat's DOPACITY inside the COMPOSECOLOR branch (maptheme.js
|
|
12347
|
+
// 13485-13506): opacity from the item's raw weighted value
|
|
12348
|
+
// sum over (field100/100), against the same nMax the color
|
|
12349
|
+
// blend normalizes by. The raw weighted sum is reconstructed
|
|
12350
|
+
// from the fractioned part values (raw = part × its field100)
|
|
12351
|
+
dopacityAlpha: dopacityActive ? this._composedDopacityAlpha(f.properties) : null
|
|
12352
|
+
}
|
|
11707
12353
|
};
|
|
11708
12354
|
}
|
|
11709
12355
|
if (isDominant) {
|
|
@@ -11759,6 +12405,29 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11759
12405
|
// deck.gl re-runs the color accessors only for new data or a changed trigger
|
|
11760
12406
|
const colorKey = JSON.stringify([[...this._markedClasses], !!this._onMarksChanged, evidenceMode,
|
|
11761
12407
|
this.categoryColorsRgb, fallbackRgb, classAlpha, lineRgb]);
|
|
12408
|
+
// flat paints a LINE shape's value color as its STROKE (paintShape on
|
|
12409
|
+
// a line shape, szShapeType line — its DOPACITY writes stroke-opacity
|
|
12410
|
+
// there too): a choropleth over line geometry, the Milano street lines,
|
|
12411
|
+
// shows the value color ON the line; a polygon keeps it as its fill
|
|
12412
|
+
const isLineGeometry = d => { const t = d.geometry && d.geometry.type; return t === 'LineString' || t === 'MultiLineString'; };
|
|
12413
|
+
const valueColorOf = d => {
|
|
12414
|
+
if (!hasData(d)) return [0, 0, 0, 0];
|
|
12415
|
+
const rgb = d.properties.composedColor
|
|
12416
|
+
|| (d.properties.cat != null ? (this.categoryColorsRgb || [])[d.properties.cat] : null) || fallbackRgb;
|
|
12417
|
+
const evidence = unmarkedEvidence(d);
|
|
12418
|
+
if (evidence === 'hide') return [0, 0, 0, 0];
|
|
12419
|
+
if (evidence === 'gray') return [...grayOf(rgb), 90];
|
|
12420
|
+
const a = alphaOfClass(d.properties.cat);
|
|
12421
|
+
return d.properties.dopacityAlpha != null ? [...rgb.slice(0, 3), Math.round(d.properties.dopacityAlpha * a * 255)] : withAlpha(rgb, a);
|
|
12422
|
+
};
|
|
12423
|
+
// flat re-styles the SAME shape nodes the FEATURE base drew, so a line
|
|
12424
|
+
// choropleth keeps the base's own line width — the donor's linewidth
|
|
12425
|
+
// is the fallback when the choropleth has none of its own
|
|
12426
|
+
const donorRt = [..._globalThemeRegistry.values()].find(r => r.name === this.name && r._isChoroplethGeometryDonor);
|
|
12427
|
+
const anyLine = collection.features.some(isLineGeometry);
|
|
12428
|
+
const isolating = !!(this._markedClasses.size && !this._onMarksChanged && evidenceMode === 'isolate');
|
|
12429
|
+
const lineW = styleNum(this.style.linewidth)
|
|
12430
|
+
|| (anyLine && donorRt && styleNum(donorRt.style.linewidth)) || 1;
|
|
11762
12431
|
return [new GeoJsonLayer({
|
|
11763
12432
|
id: this._dataLayerId(`ix-choropleth-${this.name}`),
|
|
11764
12433
|
data: collection,
|
|
@@ -11766,21 +12435,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11766
12435
|
pickable: true,
|
|
11767
12436
|
stroked: true,
|
|
11768
12437
|
filled: true,
|
|
11769
|
-
getFillColor: d =>
|
|
11770
|
-
|
|
11771
|
-
|
|
11772
|
-
|
|
11773
|
-
|
|
11774
|
-
|
|
11775
|
-
if (evidence === 'gray') return [...grayOf(rgb), 90];
|
|
11776
|
-
const a = alphaOfClass(d.properties.cat);
|
|
11777
|
-
return d.properties.dopacityAlpha != null ? [...rgb.slice(0, 3), Math.round(d.properties.dopacityAlpha * a * 255)] : withAlpha(rgb, a);
|
|
11778
|
-
},
|
|
11779
|
-
// a per-polygon accessor only while a class is isolated
|
|
11780
|
-
getLineColor: this._markedClasses.size && !this._onMarksChanged && evidenceMode === 'isolate'
|
|
11781
|
-
? d => (unmarkedEvidence(d) === 'hide' ? [0, 0, 0, 0] : lineRgb)
|
|
11782
|
-
: lineRgb,
|
|
11783
|
-
lineWidthMinPixels: styleNum(this.style.linewidth) || 1,
|
|
12438
|
+
getFillColor: d => valueColorOf(d),
|
|
12439
|
+
// a constant unless a shape needs its own color (line geometry, or a
|
|
12440
|
+
// class isolated) — deck.gl then skips a per-item accessor call
|
|
12441
|
+
getLineColor: !anyLine && !isolating ? lineRgb
|
|
12442
|
+
: d => choroplethOutlineColor(isLineGeometry(d), valueColorOf, d, isolating, unmarkedEvidence, lineRgb),
|
|
12443
|
+
lineWidthMinPixels: lineW,
|
|
11784
12444
|
// per-feature alpha (baked above) already carries the resolved
|
|
11785
12445
|
// base opacity — a layer-level opacity on TOP of that would
|
|
11786
12446
|
// multiply it a second time.
|
|
@@ -11817,6 +12477,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11817
12477
|
const categories = this._plotCategories();
|
|
11818
12478
|
|
|
11819
12479
|
const cells = new Map();
|
|
12480
|
+
const combine = aggCombiner(this.flags);
|
|
11820
12481
|
source.forEach(f => {
|
|
11821
12482
|
const ci = this._gridCategoryIndex(f.properties[this.binding.value]);
|
|
11822
12483
|
if (ci == null) return;
|
|
@@ -11834,7 +12495,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11834
12495
|
cells.set(key, cell);
|
|
11835
12496
|
}
|
|
11836
12497
|
const v = this._resolveAggregateValue(f.properties);
|
|
11837
|
-
cell.sums[ci]
|
|
12498
|
+
cell.sums[ci] = combine(cell.sums[ci], v, cell.counts[ci] === 0);
|
|
11838
12499
|
cell.counts[ci]++;
|
|
11839
12500
|
});
|
|
11840
12501
|
|
|
@@ -11872,8 +12533,14 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11872
12533
|
// a per-item SEQUENCE chart: CHART|SYMBOL|SEQUENCE, several value
|
|
11873
12534
|
// fields per item, one part per field (no CATEGORICAL, no AGGREGATE)
|
|
11874
12535
|
_isMultiFieldSequence() {
|
|
12536
|
+
// AGGREGATE is allowed: flat's loadAndAggregateValuesOfTheme
|
|
12537
|
+
// aggregates a multi-field SEQUENCE theme per FIELD (its per-field
|
|
12538
|
+
// nValuesA, MAX/MIN per field, else summed — maptheme.js 10876-10889)
|
|
12539
|
+
// and AUTO100 then normalizes every item's parts to % of their own
|
|
12540
|
+
// sum (12166-12176) — the parts ride the grid cells as `series` and
|
|
12541
|
+
// the per-item % is applied in _buildSequenceChartLayers
|
|
11875
12542
|
return this.flags.has('CHART') && this.flags.has('SYMBOL') && flatFlag(this.flags, 'SEQUENCE')
|
|
11876
|
-
&& !this.flags.has('CATEGORICAL') && !this.flags.has('
|
|
12543
|
+
&& !this.flags.has('CATEGORICAL') && !this.flags.has('PLOT')
|
|
11877
12544
|
&& String(this.binding.value || '').includes('|');
|
|
11878
12545
|
}
|
|
11879
12546
|
_itemPlotFields() {
|
|
@@ -12684,8 +13351,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12684
13351
|
ctx.arcTo(x, y + hh, x, y, rr); ctx.arcTo(x, y, x + ww, y, rr); ctx.closePath();
|
|
12685
13352
|
};
|
|
12686
13353
|
path();
|
|
12687
|
-
ctx.globalAlpha = look.fillOpacity; ctx.fillStyle = look.fill; ctx.fill();
|
|
12688
|
-
if (look.stroke && S > 0) { ctx.globalAlpha = look.strokeOpacity; ctx.strokeStyle = look.stroke; ctx.lineWidth = S * k; ctx.stroke(); }
|
|
13354
|
+
if (!isNoneColor(look.fill)) { ctx.globalAlpha = look.fillOpacity; ctx.fillStyle = look.fill; ctx.fill(); }
|
|
13355
|
+
if (look.stroke && !isNoneColor(look.stroke) && S > 0) { ctx.globalAlpha = look.strokeOpacity; ctx.strokeStyle = look.stroke; ctx.lineWidth = S * k; ctx.stroke(); }
|
|
12689
13356
|
// getSize is the icon's height: the padding is part of it
|
|
12690
13357
|
const icon = { url: canvas.toDataURL(), width: canvas.width, height: canvas.height, anchorX: canvas.width / 2, anchorY: canvas.height / 2, id: key };
|
|
12691
13358
|
return this._cacheIcon(key, icon);
|
|
@@ -12936,7 +13603,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12936
13603
|
const rangeSource = this._classSeparate && this.binding.size && !normalize ? rawSizeTotals : totals;
|
|
12937
13604
|
let nMin = Infinity, nMax = -Infinity;
|
|
12938
13605
|
for (const v of rangeSource) { if (v < nMin) nMin = v; if (v > nMax) nMax = v; }
|
|
12939
|
-
|
|
13606
|
+
const explicit = explicitRangeBreaks(this.style, this.flags);
|
|
13607
|
+
breaks = explicit ? explicitRangeParts(explicit)
|
|
13608
|
+
: flatRangeParts(totals, nMin, nMax, nParts, this.flags, styleNum(this.style.rangecentervalue));
|
|
12940
13609
|
}
|
|
12941
13610
|
|
|
12942
13611
|
// The theme's own classes follow the aggregated items, as flat's do
|
|
@@ -13288,7 +13957,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13288
13957
|
const titleFontPx = titleFontOf(w);
|
|
13289
13958
|
let title = null;
|
|
13290
13959
|
if (titleShown) {
|
|
13291
|
-
|
|
13960
|
+
// flat appends style.titleunits (its parse adds the leading
|
|
13961
|
+
// space, maptheme.js 1470; szTitle += at 18827)
|
|
13962
|
+
const units = this.style.titleunits ? ' ' + this.style.titleunits : '';
|
|
13963
|
+
const text = this._itemTitle(d.properties) ? this._itemTitle(d.properties) + units : null;
|
|
13292
13964
|
if (text) {
|
|
13293
13965
|
const f = titleFontPx;
|
|
13294
13966
|
const lines = wrapTitleLines(text, f, circular ? boxR * 2 : w).split('\n');
|
|
@@ -13315,9 +13987,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13315
13987
|
return { d, r, w, boxR, rect, title };
|
|
13316
13988
|
}) : null;
|
|
13317
13989
|
if (boxShown && circular) {
|
|
13318
|
-
const
|
|
13319
|
-
const
|
|
13320
|
-
const
|
|
13990
|
+
const boxNone = isNoneColor(this.style.boxcolor), borderNone = isNoneColor(this.style.bordercolor);
|
|
13991
|
+
const boxRgb = hexOrNamedToRgb(boxNone ? '#eeeeee' : this.style.boxcolor || '#eeeeee');
|
|
13992
|
+
const boxAlpha = boxNone ? 0 : Math.round(255 * boxOpacity);
|
|
13993
|
+
const borderRgb = borderNone ? null : hexOrNamedToRgb(this.style.bordercolor || '#dddddd');
|
|
13321
13994
|
// ScatterplotLayer has no pixel offset: a moved chart (align,
|
|
13322
13995
|
// offsetx/offsety) gets its circle as an icon
|
|
13323
13996
|
if (alignOf.active) layers.push(new IconLayer({
|
|
@@ -13325,7 +13998,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13325
13998
|
data: boxLayout, pickable: false,
|
|
13326
13999
|
getPosition: b => b.d.geometry.coordinates,
|
|
13327
14000
|
getIcon: b => this._buildSingleIcon(boxRgb, boxAlpha / 255, 'circle', borderRgb,
|
|
13328
|
-
Math.round(unitPx / 20 * BUBBLE_ICON_SIZE / Math.max(1, 2 * b.boxR) * 4) / 4),
|
|
14001
|
+
borderNone ? 0 : Math.round(unitPx / 20 * BUBBLE_ICON_SIZE / Math.max(1, 2 * b.boxR) * 4) / 4),
|
|
13329
14002
|
getSize: b => b.boxR * 2,
|
|
13330
14003
|
getPixelOffset: b => alignOf(b.d),
|
|
13331
14004
|
sizeUnits: 'pixels',
|
|
@@ -13338,9 +14011,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13338
14011
|
getPosition: b => b.d.geometry.coordinates,
|
|
13339
14012
|
getRadius: b => b.boxR,
|
|
13340
14013
|
radiusUnits: 'pixels',
|
|
13341
|
-
stroked:
|
|
14014
|
+
stroked: !borderNone,
|
|
13342
14015
|
getFillColor: [...boxRgb.slice(0, 3), boxAlpha],
|
|
13343
|
-
getLineColor: [...borderRgb.slice(0, 3), 255],
|
|
14016
|
+
getLineColor: [...(borderRgb || boxRgb).slice(0, 3), 255],
|
|
13344
14017
|
getLineWidth: unitPx / 20,
|
|
13345
14018
|
lineWidthUnits: 'pixels',
|
|
13346
14019
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
@@ -13350,7 +14023,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13350
14023
|
const boxLook = {
|
|
13351
14024
|
fill: this.style.boxcolor || 'white',
|
|
13352
14025
|
fillOpacity: boxOpacity,
|
|
13353
|
-
stroke: borderStyle === 'none' ? null : styleLineColor(this.style.bordercolor) || '#bbbbbb',
|
|
14026
|
+
stroke: borderStyle === 'none' || isNoneColor(styleLineColor(this.style.bordercolor)) ? null : styleLineColor(this.style.bordercolor) || '#bbbbbb',
|
|
13354
14027
|
strokeOpacity: /^(solid|dotted|dashed)$/.test(borderStyle) ? 1 : 0.3,
|
|
13355
14028
|
};
|
|
13356
14029
|
const borderWidth = styleNum(this.style.borderwidth) || 1;
|
|
@@ -13402,11 +14075,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13402
14075
|
}
|
|
13403
14076
|
|
|
13404
14077
|
// flat's per-part SYMBOL branch (everything but CATEGORICAL without
|
|
13405
|
-
// AGGREGATE/GROUP) draws a square of side 2r, the other
|
|
13406
|
-
// (drawSymbolPath's own square)
|
|
14078
|
+
// AGGREGATE/GROUP) draws a square or roundrect of side 2r, the other
|
|
14079
|
+
// one 1.6r (drawSymbolPath's own square / roundrect)
|
|
13407
14080
|
const perPartBranch = flatChartBranch(this.flags) === 'symbol'
|
|
13408
14081
|
&& !(this.flags.has('CATEGORICAL') && !this.flags.has('AGGREGATE') && !flatFlag(this.flags, 'GROUP'));
|
|
13409
|
-
const shapeOf = props => { const sh = this._resolveSymbolShape(props); return sh === 'square'
|
|
14082
|
+
const shapeOf = props => { const sh = this._resolveSymbolShape(props); return perPartBranch && (sh === 'square' || sh === 'roundrect') ? sh + 'full' : sh; };
|
|
13410
14083
|
// a range-classed cell is ONE class: one symbol in its class color,
|
|
13411
14084
|
// fillopacity and linecolor, as a single item (flat draws one
|
|
13412
14085
|
// symbol) — the packed multi-category icon is for CATEGORICAL cells
|
|
@@ -13730,13 +14403,55 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13730
14403
|
_buildSequenceChartLayers(charts, zoom, liveZoom) {
|
|
13731
14404
|
const st = this.style;
|
|
13732
14405
|
const multi = !!this._multiFields && this._isMultiFieldSequence();
|
|
13733
|
-
|
|
14406
|
+
// AGGREGATE + AUTO100 (flat normalizes the aggregated item's
|
|
14407
|
+
// nValuesA to % of their own sum, maptheme.js 12166-12176): every
|
|
14408
|
+
// item's parts in % of that item. Without AGGLEARATE, applyField100
|
|
14409
|
+
// already normalized the records' field values (auto100 there) —
|
|
14410
|
+
// this must not run twice
|
|
14411
|
+
const auto100 = multi && this.flags.has('AUTO100') && this.flags.has('AGGREGATE');
|
|
14412
|
+
// The value scale must be measured on the SAME scale the parts are
|
|
14413
|
+
// drawn in. With AGGREGATE, AUTO100 normalizes every item's parts to
|
|
14414
|
+
// % of their own sum at layout time (the records themselves stay
|
|
14415
|
+
// raw — applyField100 skips AGGREGATE themes), so the raw-features
|
|
14416
|
+
// _multiFieldPartMax (~1.0 here) is 100x off the drawn values and
|
|
14417
|
+
// would blow every part's radius up by sqrt(100). flat's own
|
|
14418
|
+
// nMaxValue fallback (nNormalSizeValue || nAllMaxValue,
|
|
14419
|
+
// maptheme.js 21796) measures nAllMaxValue on the NORMALIZED item
|
|
14420
|
+
// values (12166-12176), so the scale matches the parts. Without
|
|
14421
|
+
// AUTO100 the records were already normalized by applyField100 and
|
|
14422
|
+
// _multiFieldPartMax IS that scale (NYC status page)
|
|
14423
|
+
if (multi && auto100 && !this._seqAuto100Max) {
|
|
14424
|
+
let m = 0;
|
|
14425
|
+
for (const f of this.features) {
|
|
14426
|
+
const parts = this._multiFields.map(k => parseFloat(f.properties && f.properties[k]) || 0);
|
|
14427
|
+
const sum = parts.reduce((a, v) => a + v, 0);
|
|
14428
|
+
if (sum > 0) for (const v of parts) { const nv = v / (sum / 100); if (nv > m) m = nv; }
|
|
14429
|
+
}
|
|
14430
|
+
this._seqAuto100Max = m || 1;
|
|
14431
|
+
}
|
|
14432
|
+
const seqMax = multi
|
|
14433
|
+
? (auto100 ? this._seqAuto100Max : (styleNum(st.normalsizevalue) || this._multiFieldPartMax))
|
|
14434
|
+
: this._sequenceMaxValue(zoom);
|
|
13734
14435
|
const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(st.scale) || 1);
|
|
13735
14436
|
const normalR = NORMAL_RADIUS_PX * unit;
|
|
13736
14437
|
const sizePow = resolveSizePow(st, this.flags);
|
|
13737
|
-
|
|
13738
|
-
|
|
13739
|
-
|
|
14438
|
+
// flat's size field on a SEQUENCE chart part (maptheme.js 21851-21874):
|
|
14439
|
+
// the radius carries BOTH the part's value AND the item's size —
|
|
14440
|
+
// nMaxRadius · (nValue/nMaxValue)^(1/p) · (nSize/nMaxSize)^(1/p),
|
|
14441
|
+
// nMaxSize the biggest item size (measured on the records; MAX, the
|
|
14442
|
+
// one combiner flat's star-part charts pair with a size field, keeps
|
|
14443
|
+
// the cells' max equal to the records' max)
|
|
14444
|
+
const sizeField = multi && this.binding.size && this.binding.size !== '$item$' ? this.binding.size : null;
|
|
14445
|
+
let sizeMax = 0;
|
|
14446
|
+
if (sizeField) {
|
|
14447
|
+
for (const f of this.features) { const v = Math.abs(parseFloat(f.properties && f.properties[sizeField]) || 0); if (v > sizeMax) sizeMax = v; }
|
|
14448
|
+
}
|
|
14449
|
+
const itemSizeFactor = d => {
|
|
14450
|
+
if (!sizeField) return 1;
|
|
14451
|
+
const sz = typeof d.properties.size === 'number' ? d.properties.size
|
|
14452
|
+
: Math.abs(parseFloat((d.properties.raw || {})[sizeField]) || 0);
|
|
14453
|
+
return sizeMax > 0 ? Math.pow(sz / sizeMax, 1 / sizePow) : 0;
|
|
14454
|
+
};
|
|
13740
14455
|
const rings = flatFlag(this.flags, 'RINGS');
|
|
13741
14456
|
const centerValues = flatFlag(this.flags, 'CENTERVALUES');
|
|
13742
14457
|
const noFill = this._partNoFill || new Set();
|
|
@@ -13749,17 +14464,98 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13749
14464
|
const borderRgb = st.linecolor && styleLineColor(st.linecolor) !== 'none' ? hexOrNamedToRgb(styleLineColor(st.linecolor)) : null;
|
|
13750
14465
|
const lineWidth = styleNum(st.linewidth) || 1;
|
|
13751
14466
|
const symbols = Array.isArray(st.symbols) ? st.symbols : null;
|
|
14467
|
+
// flat draws an external .svg/.png/.jpeg symbol file as an <image>
|
|
14468
|
+
// node (maptheme.js 22190-22199), the chart's own shape otherwise.
|
|
14469
|
+
// Here the same URL is the part's IconLayer icon — deck.gl loads
|
|
14470
|
+
// it itself (its ImageBitmap loader falls back to <img>, which
|
|
14471
|
+
// rasterizes SVG) — NOT tinted: these files carry their own colors,
|
|
14472
|
+
// and flat doesn't recolor an <image> either
|
|
14473
|
+
const urlSymbolOf = i => {
|
|
14474
|
+
const s = symbols && (symbols[i] || symbols[0]);
|
|
14475
|
+
return typeof s === 'string' && /\.(svg|png|jpe?g)([?#]|$)/i.test(s.trim()) ? s.trim() : null;
|
|
14476
|
+
};
|
|
14477
|
+
if (!this._urlIconCache) this._urlIconCache = new Map();
|
|
14478
|
+
const urlIcon = u => {
|
|
14479
|
+
if (!this._urlIconCache.has(u)) {
|
|
14480
|
+
// the placeholder is a transparent 1x1 PNG, NOT the raw symbol
|
|
14481
|
+
// URL: deck.gl would immediately try to load that itself, and an
|
|
14482
|
+
// SVG without natural dimensions (a viewBox only, the bus
|
|
14483
|
+
// pictogram of the City-moving page) makes Chrome's
|
|
14484
|
+
// createImageBitmap throw — a console error and a part that
|
|
14485
|
+
// never renders. The real raster replaces it below
|
|
14486
|
+
const entry = { url: TRANSPARENT_1PX_PNG, width: 256, height: 256, anchorX: 128, anchorY: 128, mask: false, id: 'seq-sym-pending:' + u };
|
|
14487
|
+
this._urlIconCache.set(u, entry);
|
|
14488
|
+
// fetch the symbol ONCE and rasterize it into the descriptor's
|
|
14489
|
+
// own 256px cell here: deck then always sees exactly the declared
|
|
14490
|
+
// dimensions, and dimension-less SVGs work like any other. The
|
|
14491
|
+
// redraw lets the next getIcon see the patched descriptor
|
|
14492
|
+
fetch(u).then(r => r.ok ? r.text() : null).then(text => new Promise((resolve, reject) => {
|
|
14493
|
+
if (typeof text !== 'string' || !/<svg[\s>]/i.test(text)) { reject(new Error('not svg')); return; }
|
|
14494
|
+
if (!/\swidth\s*=/i.test(text) || !/\sheight\s*=/i.test(text)) {
|
|
14495
|
+
const vb = text.match(/viewBox\s*=\s*["']([^"']*)["']/i);
|
|
14496
|
+
const p = vb && vb[1].trim().split(/[\s,]+/).map(Number);
|
|
14497
|
+
if (p && p.length === 4 && isFinite(p[2]) && p[2] > 0 && isFinite(p[3]) && p[3] > 0) {
|
|
14498
|
+
text = text.replace(/<svg/i, '<svg width="' + p[2] + '" height="' + p[3] + '"');
|
|
14499
|
+
}
|
|
14500
|
+
}
|
|
14501
|
+
const img = new Image();
|
|
14502
|
+
img.onload = () => resolve(img);
|
|
14503
|
+
img.onerror = () => reject(new Error('svg decode failed'));
|
|
14504
|
+
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(text);
|
|
14505
|
+
})).then(img => {
|
|
14506
|
+
const size = entry.width;
|
|
14507
|
+
const canvas = document.createElement('canvas');
|
|
14508
|
+
canvas.width = size; canvas.height = size;
|
|
14509
|
+
const ctx = canvas.getContext('2d');
|
|
14510
|
+
const s = Math.min(size / (img.naturalWidth || size), size / (img.naturalHeight || size));
|
|
14511
|
+
const dw = (img.naturalWidth || size) * s, dh = (img.naturalHeight || size) * s;
|
|
14512
|
+
ctx.drawImage(img, (size - dw) / 2, (size - dh) / 2, dw, dh);
|
|
14513
|
+
entry.url = canvas.toDataURL('image/png');
|
|
14514
|
+
entry.id = 'seq-sym-raster:' + u;
|
|
14515
|
+
if (this._triggerRedraw) this._triggerRedraw();
|
|
14516
|
+
}).catch(() => {
|
|
14517
|
+
// not an SVG (a PNG/JPEG symbol): deck's own URL loading takes
|
|
14518
|
+
// it from here — and a pre-sized SVG that failed to rasterize
|
|
14519
|
+
// gets that same second chance
|
|
14520
|
+
entry.url = u;
|
|
14521
|
+
entry.id = 'seq-sym-natural:' + u;
|
|
14522
|
+
if (this._triggerRedraw) this._triggerRedraw();
|
|
14523
|
+
});
|
|
14524
|
+
}
|
|
14525
|
+
return this._urlIconCache.get(u);
|
|
14526
|
+
};
|
|
13752
14527
|
const shapeOf = i => { const sh = symbols && (symbols[i] || symbols[0]); return normalizeSymbolShape(sh); };
|
|
13753
14528
|
const colorOf = i => this.classRgb(i);
|
|
13754
14529
|
const parts = [], glows = [];
|
|
13755
14530
|
for (const d of charts) {
|
|
13756
|
-
|
|
14531
|
+
// parts: the record's own field values (a per-item chart, or the
|
|
14532
|
+
// single-record cell of an aggregated one); series: a multi-record
|
|
14533
|
+
// AGGREGATE cell's per-field combination (aggregateOnGrid); counts:
|
|
14534
|
+
// the CATEGORICAL variant
|
|
14535
|
+
let counts = d.properties.parts || d.properties.series || d.properties.counts;
|
|
14536
|
+
if (!Array.isArray(counts)) continue;
|
|
14537
|
+
if (auto100) {
|
|
14538
|
+
const sum = counts.reduce((a, v) => a + (v || 0), 0);
|
|
14539
|
+
counts = counts.map(v => sum > 0 ? v / (sum / 100) : 0);
|
|
14540
|
+
}
|
|
14541
|
+
const sizeFactor = itemSizeFactor(d);
|
|
14542
|
+
const partRadius = multi
|
|
14543
|
+
? v => Math.max(VALUE_RADIUS_MIN, normalR * Math.pow(Math.abs(v) / seqMax, 1 / sizePow) * sizeFactor)
|
|
14544
|
+
: v => valueRadius(v, liveZoom, st, this.mapOptions, this.flags, seqMax);
|
|
13757
14545
|
let layout = rings ? ringsLayout(counts, partRadius)
|
|
13758
14546
|
: sequenceLayout(counts, this.flags, partRadius, { maxRadius: normalR, maxValue: seqMax, nParts: nCats, rangeScale: styleNum(st.rangescale) || 0 });
|
|
13759
14547
|
if (!layout.length) continue;
|
|
13760
14548
|
// GLOW belongs to the first part (sorted) or to every part, and
|
|
13761
14549
|
// hides with it when its class is not marked
|
|
13762
14550
|
const glowParts = new Set(sorted ? [layout[0].i] : layout.map(p => p.i));
|
|
14551
|
+
// the chart's drawn extent (flat: map.Dom.getBox of its shape
|
|
14552
|
+
// group) — the parts' star cluster — for BOX / CIRCULARBOX sizing
|
|
14553
|
+
let ex0 = Infinity, ey0 = Infinity, ex1 = -Infinity, ey1 = -Infinity;
|
|
14554
|
+
layout.forEach(p => {
|
|
14555
|
+
ex0 = Math.min(ex0, p.x - p.r); ey0 = Math.min(ey0, p.y - p.r);
|
|
14556
|
+
ex1 = Math.max(ex1, p.x + p.r); ey1 = Math.max(ey1, p.y + p.r);
|
|
14557
|
+
});
|
|
14558
|
+
d._seqExtent = [ex0, ey0, ex1, ey1];
|
|
13763
14559
|
if (marking && !gray) {
|
|
13764
14560
|
layout = layout.filter(p => this._markedClasses.has(p.i)).map(p => ({ ...p, x: 0, y: 0 }));
|
|
13765
14561
|
if (!layout.length) continue;
|
|
@@ -13779,6 +14575,162 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13779
14575
|
const offsetOf = item => { const o = alignOf(item.chart); return [o[0] + item.part.x, o[1] + item.part.y]; };
|
|
13780
14576
|
const alphaOf = item => (gray && !this._markedClasses.has(item.part.i) ? DIMMED_ICON_ALPHA : 255);
|
|
13781
14577
|
const layers = [];
|
|
14578
|
+
// BOX / CIRCULARBOX with TITLE / BOTTOMTITLE — flat's own box block
|
|
14579
|
+
// (maptheme.js 18757-18925) on a SEQUENCE chart's extent: the star
|
|
14580
|
+
// cluster of parts (d._seqExtent), not one bubble's radius.
|
|
14581
|
+
// - CIRCULARBOX: a circle of max(w,h)/2 · 1.1 + margin, margin
|
|
14582
|
+
// min(2m, m·w/30u), m = boxmargin (2) · u; boxcolor (#eeeeee) at
|
|
14583
|
+
// boxopacity, stroke bordercolor (#dddddd) one SVG unit (u/20)
|
|
14584
|
+
// - the title (TITLE / BOTTOMTITLE) in textcolor (#888888) over
|
|
14585
|
+
// the half-opaque white halo, centered under the circle
|
|
14586
|
+
// (BOTTOMTITLE: one font below its bottom edge), DTEXT font
|
|
14587
|
+
// max(0.9u, min(w/5, 14u/scale)) · textscale, wrapped to the
|
|
14588
|
+
// circle's width — gated by titleupper (shown while the map
|
|
14589
|
+
// scale is that bound or coarser, 18810) and boxupper/boxlower
|
|
14590
|
+
const boxShown = flatFlag(this.flags, 'BOX') && !boxHiddenByScale(st, liveZoom);
|
|
14591
|
+
const circular = flatFlag(this.flags, 'CIRCULAR');
|
|
14592
|
+
const bottomTitle = flatFlag(this.flags, 'BOTTOMTITLE');
|
|
14593
|
+
const titleUpper = scaleDenom(st.titleupper);
|
|
14594
|
+
const titleShown = boxShown && flatFlag(this.flags, 'TITLE') && !!this.binding.title
|
|
14595
|
+
&& !(titleUpper && scaleDenominatorAt(liveZoom) > titleUpper);
|
|
14596
|
+
let boxLayout = null;
|
|
14597
|
+
if (boxShown) {
|
|
14598
|
+
const boxMarginU = styleNum(st.boxmargin) || 2;
|
|
14599
|
+
const boxOpacity = isNaN(styleNum(st.boxopacity)) ? 1 : styleNum(st.boxopacity);
|
|
14600
|
+
const titleScale = styleNum(st.textscale) || styleNum(st.valuescale) || 1;
|
|
14601
|
+
const titleFontOf = w => (flatFlag(this.flags, 'DTEXT')
|
|
14602
|
+
? Math.max(0.9 * unit, Math.min(w / 5, 14 * unit / (styleNum(st.scale) || 1)))
|
|
14603
|
+
: 5 * unit) * titleScale;
|
|
14604
|
+
const alignRight = /right/.test(String(st.align || ''));
|
|
14605
|
+
boxLayout = charts.filter(d => d._seqExtent).map(d => {
|
|
14606
|
+
const [ex0, ey0, ex1, ey1] = d._seqExtent;
|
|
14607
|
+
const w = ex1 - ex0, h = ey1 - ey0;
|
|
14608
|
+
// flat centers the circular box (and its title) on the chart's
|
|
14609
|
+
// bounding-box CENTER (maptheme.js 18793-18798: center =
|
|
14610
|
+
// bBox.x + bBox.width/2, bBox.y + bBox.height/2), not on the
|
|
14611
|
+
// chart's anchor — a star cluster's satellites spread
|
|
14612
|
+
// asymmetrically around it
|
|
14613
|
+
const cx = (ex0 + ex1) / 2, cy = (ey0 + ey1) / 2;
|
|
14614
|
+
const margin = Math.min(2 * boxMarginU * unit, boxMarginU * w / 30);
|
|
14615
|
+
const boxR = circular ? Math.max(w, h) / 2 * 1.1 + margin : 0;
|
|
14616
|
+
let title = null;
|
|
14617
|
+
if (titleShown) {
|
|
14618
|
+
// flat appends style.titleunits to the box title (its parse
|
|
14619
|
+
// gives it the leading space, maptheme.js 1470; szTitle += at
|
|
14620
|
+
// 18827): "27.5" + "km/h" -> "27.5 km/h"
|
|
14621
|
+
const units = st.titleunits ? ' ' + st.titleunits : '';
|
|
14622
|
+
const text = this._itemTitle(d.properties) ? this._itemTitle(d.properties) + units : null;
|
|
14623
|
+
if (text) {
|
|
14624
|
+
const f = titleFontOf(w);
|
|
14625
|
+
const lines = wrapTitleLines(text, f, circular ? boxR * 2 : w).split('\n');
|
|
14626
|
+
const width = Math.max(...lines.map(l => titleTextWidth(l, f)));
|
|
14627
|
+
// flat's title baselines are measured on the resized box
|
|
14628
|
+
// (the circle spans center ± radius, 18798-18830)
|
|
14629
|
+
const base = circular
|
|
14630
|
+
? (bottomTitle ? cy + boxR + f : cy - boxR - 0.7 * f - (lines.length - 1) * f)
|
|
14631
|
+
: (bottomTitle ? ey1 + f : ey0 - 0.7 * f - (lines.length - 1) * f);
|
|
14632
|
+
const x = circular ? cx + unit / 2 : (alignRight ? ex1 : ex0);
|
|
14633
|
+
const anchor = circular ? 'middle' : (alignRight ? 'end' : 'start');
|
|
14634
|
+
const x0 = anchor === 'middle' ? x - width / 2 : anchor === 'end' ? x - width : x;
|
|
14635
|
+
title = { text: lines.join('\n'), x, anchor, top: base - 0.8 * f, f,
|
|
14636
|
+
extent: [x0, base - 0.905 * f, x0 + width, base + (lines.length - 1) * f + 0.212 * f] };
|
|
14637
|
+
}
|
|
14638
|
+
}
|
|
14639
|
+
let rect = null;
|
|
14640
|
+
if (!circular) {
|
|
14641
|
+
const e = [ex0, ey0, ex1, ey1];
|
|
14642
|
+
if (title) { e[0] = Math.min(e[0], title.extent[0]); e[1] = Math.min(e[1], title.extent[1]) + 0.1 * title.f; e[2] = Math.max(e[2], title.extent[2]); e[3] = Math.max(e[3], title.extent[3]); }
|
|
14643
|
+
rect = [e[0] - margin, e[1] - margin, e[2] + margin, e[3] + margin];
|
|
14644
|
+
}
|
|
14645
|
+
return { d, w, boxR, rect, title, boxOpacity, cx, cy };
|
|
14646
|
+
});
|
|
14647
|
+
if (circular) {
|
|
14648
|
+
const boxNone = isNoneColor(st.boxcolor), borderNone = isNoneColor(st.bordercolor);
|
|
14649
|
+
const boxRgb = hexOrNamedToRgb(boxNone ? '#eeeeee' : st.boxcolor || '#eeeeee');
|
|
14650
|
+
const borderRgb = borderNone ? null : hexOrNamedToRgb(st.bordercolor || '#dddddd');
|
|
14651
|
+
// ScatterplotLayer has no pixel offset: a moved chart (align,
|
|
14652
|
+
// offsetx/offsety) gets its circle as an icon — at the chart's
|
|
14653
|
+
// bounding-box center, flat's circular box position (18793)
|
|
14654
|
+
if (alignOf.active) layers.push(new IconLayer({
|
|
14655
|
+
id: `ix-seqbox-${this.name}-g${this._iconGeneration}`,
|
|
14656
|
+
data: boxLayout, pickable: false,
|
|
14657
|
+
getPosition: b => b.d.geometry.coordinates,
|
|
14658
|
+
getIcon: b => this._buildSingleIcon(boxRgb, b.boxOpacity, 'circle', borderRgb,
|
|
14659
|
+
borderNone ? 0 : Math.round(unit / 20 * BUBBLE_ICON_SIZE / Math.max(1, 2 * b.boxR) * 4) / 4),
|
|
14660
|
+
getSize: b => b.boxR * 2,
|
|
14661
|
+
getPixelOffset: b => { const o = alignOf(b.d); return [o[0] + b.cx, o[1] + b.cy]; },
|
|
14662
|
+
sizeUnits: 'pixels', billboard: true,
|
|
14663
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14664
|
+
}));
|
|
14665
|
+
else layers.push(new ScatterplotLayer({
|
|
14666
|
+
id: `ix-seqbox-${this.name}`,
|
|
14667
|
+
data: boxLayout, pickable: false,
|
|
14668
|
+
getPosition: b => {
|
|
14669
|
+
// the circle at the chart's bbox center: with no align
|
|
14670
|
+
// offset it moves in map coordinates
|
|
14671
|
+
const o = pixelOffsetLngLat(b.d.geometry.coordinates, [b.cx, b.cy], liveZoom);
|
|
14672
|
+
return o;
|
|
14673
|
+
},
|
|
14674
|
+
getRadius: b => b.boxR,
|
|
14675
|
+
radiusUnits: 'pixels',
|
|
14676
|
+
stroked: !borderNone,
|
|
14677
|
+
getFillColor: b => [...boxRgb.slice(0, 3), Math.round(255 * b.boxOpacity)],
|
|
14678
|
+
getLineColor: [...(borderRgb || boxRgb).slice(0, 3), 255],
|
|
14679
|
+
getLineWidth: unit / 20,
|
|
14680
|
+
lineWidthUnits: 'pixels',
|
|
14681
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14682
|
+
}));
|
|
14683
|
+
} else {
|
|
14684
|
+
// flat's rectangular box (the chart and its title plus the
|
|
14685
|
+
// margin), the single-symbol path's own _buildBoxIcon drawing
|
|
14686
|
+
const borderStyle = String(st.borderstyle || '');
|
|
14687
|
+
const boxLook = {
|
|
14688
|
+
fill: st.boxcolor || 'white',
|
|
14689
|
+
fillOpacity: isNaN(styleNum(st.boxopacity)) ? 1 : styleNum(st.boxopacity),
|
|
14690
|
+
stroke: borderStyle === 'none' || isNoneColor(styleLineColor(st.bordercolor)) ? null : styleLineColor(st.bordercolor) || '#bbbbbb',
|
|
14691
|
+
strokeOpacity: /^(solid|dotted|dashed)$/.test(borderStyle) ? 1 : 0.3
|
|
14692
|
+
};
|
|
14693
|
+
const borderWidth = styleNum(st.borderwidth) || 1;
|
|
14694
|
+
const borderRadius = styleNum(st.borderradius);
|
|
14695
|
+
const strokeOf = b => borderWidth * 0.2 * b.w / 30;
|
|
14696
|
+
boxLayout.forEach(b => {
|
|
14697
|
+
b.icon = this._buildBoxIcon(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1],
|
|
14698
|
+
isNaN(borderRadius) ? 0 : borderRadius * Math.min(unit, b.w / 30),
|
|
14699
|
+
strokeOf(b), boxLook);
|
|
14700
|
+
});
|
|
14701
|
+
if (boxLayout.length) layers.push(new IconLayer({
|
|
14702
|
+
id: `ix-seqbox-${this.name}-g${this._iconGeneration}`,
|
|
14703
|
+
data: boxLayout, pickable: false,
|
|
14704
|
+
getPosition: b => b.d.geometry.coordinates,
|
|
14705
|
+
getIcon: b => b.icon,
|
|
14706
|
+
getSize: b => b.icon.height / 2,
|
|
14707
|
+
getPixelOffset: b => { const o = alignOf(b.d); return [(b.rect[0] + b.rect[2]) / 2 + o[0], (b.rect[1] + b.rect[3]) / 2 + o[1]]; },
|
|
14708
|
+
sizeUnits: 'pixels', billboard: true,
|
|
14709
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14710
|
+
}));
|
|
14711
|
+
}
|
|
14712
|
+
if (titleShown) {
|
|
14713
|
+
const titleData = boxLayout.filter(b => b.title);
|
|
14714
|
+
if (titleData.length) layers.push(new TextLayer({
|
|
14715
|
+
id: `ix-seqtitles-${this.name}`,
|
|
14716
|
+
data: titleData, pickable: false,
|
|
14717
|
+
getPosition: b => b.d.geometry.coordinates,
|
|
14718
|
+
getText: b => b.title.text,
|
|
14719
|
+
getSize: b => b.title.f,
|
|
14720
|
+
sizeUnits: 'pixels', fontFamily: 'arial',
|
|
14721
|
+
characterSet: 'auto',
|
|
14722
|
+
lineHeight: 1,
|
|
14723
|
+
getColor: [...hexOrNamedToRgb(st.textcolor || '#888888').slice(0, 3), 255],
|
|
14724
|
+
fontSettings: { sdf: true },
|
|
14725
|
+
outlineWidth: 1 / 7,
|
|
14726
|
+
outlineColor: [255, 255, 255, 128],
|
|
14727
|
+
getTextAnchor: b => b.title.anchor,
|
|
14728
|
+
getAlignmentBaseline: 'top',
|
|
14729
|
+
getPixelOffset: b => { const o = alignOf(b.d); return [b.title.x + o[0], b.title.top + o[1]]; },
|
|
14730
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14731
|
+
}));
|
|
14732
|
+
}
|
|
14733
|
+
}
|
|
13782
14734
|
if (glows.length && this.flags.has('GLOW') && !glowHiddenByScale(st, liveZoom)) {
|
|
13783
14735
|
layers.push(new IconLayer({
|
|
13784
14736
|
id: `ix-glow-${this.name}-g${this._iconGeneration}`,
|
|
@@ -13809,20 +14761,35 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13809
14761
|
data: parts, pickable: true,
|
|
13810
14762
|
getPosition: d => d.geometry.coordinates,
|
|
13811
14763
|
// the stroke, in the icon raster's pixels (the icon is drawn at 2r)
|
|
13812
|
-
getIcon: d =>
|
|
13813
|
-
|
|
14764
|
+
getIcon: d => {
|
|
14765
|
+
// an external symbol file: the URL icon as flat's <image> (see
|
|
14766
|
+
// urlSymbolOf above), scaled to the part's diameter like flat's
|
|
14767
|
+
// fu.scale(nRadius / nMaxRadius) on a nChartSize image
|
|
14768
|
+
const u = urlSymbolOf(d.part.i);
|
|
14769
|
+
if (u) return urlIcon(u);
|
|
14770
|
+
return this._buildSingleIcon(colorOf(d.part.i), noFill.has(d.part.i) ? 0 : fillOpacity, shapeOf(d.part.i), borderRgb,
|
|
14771
|
+
borderRgb ? Math.round(lineWidth * unit * Math.min(2 * d.part.r / normalR, 0.2) * BUBBLE_ICON_SIZE / Math.max(1, 2 * d.part.r) * 4) / 4 : 0);
|
|
14772
|
+
},
|
|
13814
14773
|
getSize: d => d.part.r * 2,
|
|
13815
14774
|
getPixelOffset: offsetOf,
|
|
13816
14775
|
getColor: d => [255, 255, 255, alphaOf(d)],
|
|
13817
14776
|
sizeUnits: 'pixels', billboard: true,
|
|
13818
14777
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
13819
14778
|
}));
|
|
13820
|
-
// VALUES on each part (maptheme.js 22188-
|
|
14779
|
+
// VALUES on each part (maptheme.js 22188-22260): the part's value — with
|
|
13821
14780
|
// COUNT its record count, with valuefield on the categorical field the
|
|
13822
|
-
// class name — formatted with valuedecimals (else none) plus the unit
|
|
13823
|
-
// font min(1, 3.2 / L) · r · valuescale,
|
|
13824
|
-
// biggest value text (szMaxText), normal
|
|
13825
|
-
// textcolor, else ChartColors.textColor of
|
|
14781
|
+
// class name — formatted with valuedecimals (else none) plus the unit.
|
|
14782
|
+
// - a SHAPE part (circle & kin): font min(1, 3.2 / L) · r · valuescale,
|
|
14783
|
+
// L the length of the theme's biggest value text (szMaxText), normal
|
|
14784
|
+
// weight, in valuecolor / textcolor, else ChartColors.textColor of
|
|
14785
|
+
// the part color
|
|
14786
|
+
// - an IMAGE-symbol part (an external .svg, the TELRAAM pictograms):
|
|
14787
|
+
// flat's own other branch (22253-22256) — font max(r · 0.8, 1) ·
|
|
14788
|
+
// valuescale, the text over createTextLabel's background box (white,
|
|
14789
|
+
// its rgba(…,0.3) fill × the 0.5 fill-opacity) and in
|
|
14790
|
+
// ChartColors.lowColor — the 0.7 derivate of the part color —
|
|
14791
|
+
// regardless of valuecolor/textcolor (flat passes cColor.lowColor
|
|
14792
|
+
// straight into createTextLabel)
|
|
13826
14793
|
// CENTERVALUES: only on the first part (class 0), its own value
|
|
13827
14794
|
if ((this.flags.has('VALUES') || centerValues) && !valuesHiddenByScale(st, zoom)) {
|
|
13828
14795
|
const valueScale = styleNum(st.valuescale) || 1;
|
|
@@ -13842,14 +14809,88 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13842
14809
|
const name = rec ? String(rec[st.valuefield]) : null;
|
|
13843
14810
|
const t = counting && d.properties.recordCounts ? d.properties.recordCounts[d.part.i] : d.part.v;
|
|
13844
14811
|
const text = name != null ? name : fmt(t);
|
|
14812
|
+
const image = !!urlSymbolOf(d.part.i);
|
|
13845
14813
|
const L = Math.max((name != null ? name.length : 0) + 1, maxLen);
|
|
13846
|
-
const fontSize =
|
|
14814
|
+
const fontSize = image
|
|
14815
|
+
? Math.max(d.part.r * 0.8, 1) * valueScale
|
|
14816
|
+
: Math.min(1, 3.2 / L) * d.part.r * valueScale;
|
|
13847
14817
|
if (fontSize > VALUES_MIN_FONT_PX) {
|
|
13848
14818
|
labels.push({ geometry: d.geometry, text, fontSize, pixelOffset: offsetOf(d), alpha: alphaOf(d),
|
|
13849
|
-
|
|
14819
|
+
image,
|
|
14820
|
+
color: image ? flatDerivateRgb(colorOf(d.part.i), 0.7)
|
|
14821
|
+
: textOverride ? hexOrNamedToRgb(textOverride) : flatChartTextRgb(colorOf(d.part.i)) });
|
|
13850
14822
|
}
|
|
13851
14823
|
});
|
|
13852
14824
|
if (labels.length) {
|
|
14825
|
+
// VALUEBACKGROUND on URL-symbol parts: a fading white border
|
|
14826
|
+
// around the value's own CHARACTER SHAPES — the glyphs
|
|
14827
|
+
// re-drawn in white on rings of pixel offsets (a stroke of
|
|
14828
|
+
// copies, no box): the width is a fraction of the font size,
|
|
14829
|
+
// so the halo scales with every label; the shape-part values
|
|
14830
|
+
// keep flat's plain text (no outline)
|
|
14831
|
+
if (flatFlag(this.flags, 'VALUEBACKGROUND')) {
|
|
14832
|
+
const haloLabels = labels.filter(l => l.image);
|
|
14833
|
+
const plainLabels = labels.filter(l => !l.image);
|
|
14834
|
+
if (haloLabels.length) {
|
|
14835
|
+
// a real outline around the characters: the label's own
|
|
14836
|
+
// glyphs re-drawn in white on rings of pixel offsets — a
|
|
14837
|
+
// stroke of copies that hugs the character shapes (deck's
|
|
14838
|
+
// SDF outline is only a faint, buffer-clipped rim). The
|
|
14839
|
+
// base radius is a small fraction of the font size (the
|
|
14840
|
+
// 1px floor keeps it tiny until labels get big), and the
|
|
14841
|
+
// ring alphas fall off, so the border fades to nothing
|
|
14842
|
+
const DIRS = 16, RINGS = [[1, 210], [2, 120], [3.2, 65]];
|
|
14843
|
+
const haloCopies = [];
|
|
14844
|
+
for (const l of haloLabels) {
|
|
14845
|
+
const rb = Math.max(0.75, l.fontSize * 0.015);
|
|
14846
|
+
for (let ring = 0; ring < RINGS.length; ring++) {
|
|
14847
|
+
for (let dir = 0; dir < DIRS; dir++) {
|
|
14848
|
+
const a = (dir / DIRS) * 2 * Math.PI;
|
|
14849
|
+
haloCopies.push({ ...l,
|
|
14850
|
+
dx: Math.cos(a), dy: Math.sin(a),
|
|
14851
|
+
radius: rb * RINGS[ring][0],
|
|
14852
|
+
haloAlpha: Math.round(RINGS[ring][1] * l.alpha / 255) });
|
|
14853
|
+
}
|
|
14854
|
+
}
|
|
14855
|
+
}
|
|
14856
|
+
layers.push(new TextLayer({
|
|
14857
|
+
id: `ix-cluster-values-halo-${this.name}`,
|
|
14858
|
+
data: haloCopies, pickable: false,
|
|
14859
|
+
getPosition: d => d.geometry.coordinates,
|
|
14860
|
+
getText: d => d.text,
|
|
14861
|
+
getSize: d => d.fontSize,
|
|
14862
|
+
getColor: d => [255, 255, 255, d.haloAlpha],
|
|
14863
|
+
getPixelOffset: d => [d.pixelOffset[0] + d.dx * d.radius, d.pixelOffset[1] + d.dy * d.radius],
|
|
14864
|
+
sizeUnits: 'pixels', fontFamily: 'arial', fontWeight: 'normal',
|
|
14865
|
+
characterSet: 'auto',
|
|
14866
|
+
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
14867
|
+
}));
|
|
14868
|
+
layers.push(new TextLayer({
|
|
14869
|
+
id: `ix-cluster-values-${this.name}`,
|
|
14870
|
+
data: haloLabels, pickable: false,
|
|
14871
|
+
getPosition: d => d.geometry.coordinates,
|
|
14872
|
+
getText: d => d.text,
|
|
14873
|
+
getSize: d => d.fontSize,
|
|
14874
|
+
getColor: d => [...d.color.slice(0, 3), d.alpha],
|
|
14875
|
+
getPixelOffset: d => d.pixelOffset,
|
|
14876
|
+
sizeUnits: 'pixels', fontFamily: 'arial', fontWeight: 'normal',
|
|
14877
|
+
characterSet: 'auto',
|
|
14878
|
+
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
14879
|
+
}));
|
|
14880
|
+
}
|
|
14881
|
+
if (plainLabels.length) layers.push(new TextLayer({
|
|
14882
|
+
id: `ix-cluster-values-plain-${this.name}`,
|
|
14883
|
+
data: plainLabels, pickable: false,
|
|
14884
|
+
getPosition: d => d.geometry.coordinates,
|
|
14885
|
+
getText: d => d.text,
|
|
14886
|
+
getSize: d => d.fontSize,
|
|
14887
|
+
getColor: d => [...d.color.slice(0, 3), d.alpha],
|
|
14888
|
+
getPixelOffset: d => d.pixelOffset,
|
|
14889
|
+
sizeUnits: 'pixels', fontFamily: 'arial', fontWeight: 'normal',
|
|
14890
|
+
characterSet: 'auto',
|
|
14891
|
+
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
14892
|
+
}));
|
|
14893
|
+
} else {
|
|
13853
14894
|
layers.push(new TextLayer({
|
|
13854
14895
|
id: `ix-cluster-values-${this.name}`,
|
|
13855
14896
|
data: labels, pickable: false,
|
|
@@ -13862,6 +14903,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13862
14903
|
characterSet: 'auto',
|
|
13863
14904
|
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
13864
14905
|
}));
|
|
14906
|
+
}
|
|
13865
14907
|
}
|
|
13866
14908
|
}
|
|
13867
14909
|
return layers;
|
|
@@ -14409,8 +15451,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14409
15451
|
const titleFont = 5 * (num(st.textscale) || num(st.valuescale) || 1);
|
|
14410
15452
|
const boxOpacity = isNaN(styleNum(st.boxopacity)) ? 1 : styleNum(st.boxopacity);
|
|
14411
15453
|
const borderStyle = String(st.borderstyle || '');
|
|
14412
|
-
const boxFill = [...hexOrNamedToRgb(st.boxcolor || 'white').slice(0, 3), Math.round(255 * boxOpacity)];
|
|
14413
|
-
const boxLine = borderStyle === 'none' ? [0, 0, 0, 0]
|
|
15454
|
+
const boxFill = [...hexOrNamedToRgb(isNoneColor(st.boxcolor) ? 'white' : st.boxcolor || 'white').slice(0, 3), isNoneColor(st.boxcolor) ? 0 : Math.round(255 * boxOpacity)];
|
|
15455
|
+
const boxLine = borderStyle === 'none' || isNoneColor(styleLineColor(st.bordercolor)) ? [0, 0, 0, 0]
|
|
14414
15456
|
: [...hexOrNamedToRgb(styleLineColor(st.bordercolor) || '#bbbbbb').slice(0, 3), Math.round(255 * (/^(solid|dotted|dashed)$/.test(borderStyle) ? 1 : 0.3))];
|
|
14415
15457
|
const borderWidth = num(st.borderwidth) || 1;
|
|
14416
15458
|
const lc = st.linecolor != null && st.linecolor !== '' ? styleLineColor(st.linecolor) : null;
|
|
@@ -15295,8 +16337,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15295
16337
|
ctx.lineTo(cx + r, cy - r * 0.6);
|
|
15296
16338
|
ctx.lineTo(cx, cy + r);
|
|
15297
16339
|
break;
|
|
16340
|
+
// flat's per-part SYMBOL roundrect (maptheme.js 22039): side 2r, like
|
|
16341
|
+
// 'squarefull'; the other branch (21639) is 1.6r
|
|
16342
|
+
case 'roundrectfull':
|
|
15298
16343
|
case 'roundrect': {
|
|
15299
|
-
const s = r * 0.8;
|
|
16344
|
+
const s = shape === 'roundrectfull' ? r : r * 0.8;
|
|
15300
16345
|
if (ctx.roundRect) ctx.roundRect(cx - s, cy - s, s * 2, s * 2, r * 0.3);
|
|
15301
16346
|
else ctx.rect(cx - s, cy - s, s * 2, s * 2);
|
|
15302
16347
|
break;
|
|
@@ -15365,6 +16410,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15365
16410
|
function withAlpha(rgb, a) {
|
|
15366
16411
|
return a >= 1 ? rgb.slice(0, 3) : [...rgb.slice(0, 3), Math.round(255 * Math.max(0, a))];
|
|
15367
16412
|
}
|
|
16413
|
+
// a style color of "none": no paint (flat writes it into the SVG style:
|
|
16414
|
+
// fill:none / stroke:none) — neither a color nor a canvas fillStyle
|
|
16415
|
+
function isNoneColor(v) {
|
|
16416
|
+
return typeof v === 'string' && v.trim().toLowerCase() === 'none';
|
|
16417
|
+
}
|
|
15368
16418
|
function hexOrNamedToRgb(v) {
|
|
15369
16419
|
const NAMED = { gray: [128, 128, 128], grey: [128, 128, 128], black: [0, 0, 0], white: [255, 255, 255] };
|
|
15370
16420
|
return parseCssColor(v) || NAMED[v] || [130, 130, 130];
|
|
@@ -15481,7 +16531,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15481
16531
|
// last built map; called while the map is still building, they run once
|
|
15482
16532
|
// it is ready.
|
|
15483
16533
|
const MAP_HANDLE_METHODS = ['replace', 'add', 'newTheme', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
15484
|
-
'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'setProjection', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible', 'setThemeTimeFrame', 'attribution'];
|
|
16534
|
+
'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'setProjection', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible', 'switchLayer', 'setThemeTimeFrame', 'attribution'];
|
|
15485
16535
|
const _mapHandles = Object.create(null);
|
|
15486
16536
|
function makeMapHandle(szMap) {
|
|
15487
16537
|
const h = {};
|
|
@@ -15496,6 +16546,15 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15496
16546
|
}
|
|
15497
16547
|
h.getZoom = () => { const a = api(); return a ? a.getZoom() : undefined; };
|
|
15498
16548
|
h.getThemeObj = id => getThemeObj(id);
|
|
16549
|
+
// flat's mapApi.message(szText) (htmlgui_flat.js 1381): a short status
|
|
16550
|
+
// line over the map — the same accepted no-op as the global
|
|
16551
|
+
// ixmaps.message ("flat shows a short status message on the map;
|
|
16552
|
+
// not ported"), here for a page's own map().message('...') calls
|
|
16553
|
+
h.message = () => h;
|
|
16554
|
+
// flat's mapApi.getProjectString() (htmlgui_flat.js 1375): the
|
|
16555
|
+
// project JSON of the map — the global's, of the last built map (a
|
|
16556
|
+
// page's code-view overlay calls it through the handle, WBGT port)
|
|
16557
|
+
h.getProjectString = () => getProjectString();
|
|
15499
16558
|
// flat's mapApi.theme(szTheme) (htmlgui_flat.js 1571): the theme handle
|
|
15500
16559
|
h.theme = szTheme => new themeApi(szMap, szTheme);
|
|
15501
16560
|
// flat's mapApi aliases (htmlgui_flat.js 1286+): show/hide a theme,
|
|
@@ -16064,7 +17123,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16064
17123
|
// known real page's globe toggle actually uses (see
|
|
16065
17124
|
// example-world-bubble-projection-toggle.md) — so `ixmaps.szResourceBase`
|
|
16066
17125
|
// only needs to be SOME string, not a real, working resource base URL.
|
|
16067
|
-
|
|
17126
|
+
// It is the flat release CDN anyway: flat's own tool scripts required by a
|
|
17127
|
+
// page (tooltip_mustache.js) resolve their dependencies through it —
|
|
17128
|
+
// $.getScript(ixmaps.szResourceBase + "ui/js/tools/format.js") — which
|
|
17129
|
+
// with the empty string 404s against the page's own directory
|
|
17130
|
+
const ixmapsSzResourceBase = 'https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-flat@1/';
|
|
16068
17131
|
function getProjectString() {
|
|
16069
17132
|
if (!_lastMapApi || !_lastMapApi.map) return JSON.stringify({ map: {} });
|
|
16070
17133
|
const map = _lastMapApi.map;
|
|
@@ -16157,6 +17220,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16157
17220
|
newTheme: (title, theme, flag) => _mapHandle.newTheme(title, theme, flag),
|
|
16158
17221
|
// flat's time slider: show the records of a theme (null = all themes) whose timefield lies in [min, max)
|
|
16159
17222
|
setThemeTimeFrame: (szId, nMin, nMax) => _mapHandle.setThemeTimeFrame(szId, nMin, nMax),
|
|
17223
|
+
// flat's ixmaps.switchLayer(szLayerName, fState) (htmlgui.js 3383): every
|
|
17224
|
+
// theme of the named layer on the last built map shown or hidden
|
|
17225
|
+
switchLayer: (szLayerName, fState) => _mapHandle.switchLayer(szLayerName, fState),
|
|
16160
17226
|
setBasemapOpacity: compatSetBasemapOpacity,
|
|
16161
17227
|
// this engine's version (flat's own ixmaps.version numbers flat)
|
|
16162
17228
|
glVersion: IXMAPS_GL_VERSION,
|
|
@@ -16182,14 +17248,14 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16182
17248
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
16183
17249
|
global.__ixmapsGlInternals = {
|
|
16184
17250
|
IXMAPS_GL_VERSION, applyFlatMaxCharts, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout, ringsLayout, pieSliceLayout, pieValueLabelLayout, isBarChart, flatBarLayout, flatChartBox, flatValueRules, flatPieRecord, flatPieAccumulate, flatPieItemValues, flatVectorItems, orthographicPlane, vectorArcHeight, pathBackIndex, greatCircleMeters, cameraOptions, bezierVectorLayout, cubicBezierPoints, arrowMarkerTriangle, fadeGradientStops, flatToArray, hashUnit, multiQuadOffsets, pixelOffsetLngLat, chartTextTransmittance, insideSymbolShape,
|
|
16185
|
-
normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
17251
|
+
choroplethOutlineColor, normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
16186
17252
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
16187
17253
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|
|
16188
17254
|
computeAlphaStats, computeMultiFieldClasses, computeDominantStats, resolveDominantClass, computeComposeColorStats,
|
|
16189
17255
|
resolveComposedColor, computeRangeClasses, colorSchemeClassCount, resolveDopacityAlpha,
|
|
16190
17256
|
flatColorSweep, applyClassesToColorScheme, resolveClassColors, flatOutlierStats, parseCssColor, flatLegendLook,
|
|
16191
17257
|
flatToMapLibreZoom, mapLibreToFlatZoom, flatViewToMapLibreZoom, mapLibreToFlatViewZoom, flatSvgProjectionOf, flatSvgProjectionByOptions, normalSizeScaleOf, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
|
|
16192
|
-
fetchLayerData, parseCsvText, dataTableRows, geometryRowsToFeatureCollection, filterFlatValues, applyField100, field100Binding, rangeClassLegendTotals, resolveAggregateValue, classValueSeparate, cellAggregatedValues, oneHot, groupCoLocated, aggregateOnGrid, GridAggregateIndex, flatRangeParts, aggregateField100, valueRadius, itemPlotGeometry, itemAnchor, objectZoomFactor, resolveZoomReference, defaultNormalSizeValue,
|
|
17258
|
+
fetchLayerData, parseCsvText, dataTableRows, geometryRowsToFeatureCollection, filterFlatValues, applyField100, field100Binding, rangeClassLegendTotals, resolveAggregateValue, classValueSeparate, cellAggregatedValues, oneHot, groupCoLocated, aggregateOnGrid, GridAggregateIndex, flatRangeParts, explicitRangeBreaks, explicitRangeParts, resolvePartsClass, isNoneColor, aggregateField100, valueRadius, itemPlotGeometry, itemAnchor, objectZoomFactor, resolveZoomReference, defaultNormalSizeValue,
|
|
16193
17259
|
applyWhereFilter, joinChartPositions, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatBubbleLook, bubbleIconLook, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
16194
17260
|
drawSymbolPath, normalizeSymbolShape,
|
|
16195
17261
|
flatFormatValue, themeStyleArgs, flatChangedStyleValue, explicitValueDecimals, legendDecimals,
|