ixmaps-gl 0.2.22 → 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/README.md +7 -4
- package/ixmaps-gl.js +1477 -184
- 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,
|
|
@@ -1542,16 +1557,37 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1542
1557
|
_filterCache.set(expr, pred);
|
|
1543
1558
|
return pred;
|
|
1544
1559
|
}
|
|
1545
|
-
// a
|
|
1546
|
-
//
|
|
1547
|
-
//
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1560
|
+
// a theme's data after its .filter() (spec.filter, or expr) and, for a table,
|
|
1561
|
+
// the join to geometry. A load-time .filter() drops records for good, so a
|
|
1562
|
+
// later changeThemeStyle "filter" re-runs this on the unfiltered data (flat
|
|
1563
|
+
// re-processes the theme with the new filter) instead of narrowing what the
|
|
1564
|
+
// first filter left.
|
|
1565
|
+
function filterAndJoin(raw, spec, runtimes, expr = spec.filter) {
|
|
1566
|
+
const f = applyWhereFilter(raw, expr);
|
|
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
|
-
|
|
1555
1591
|
function applyWhereFilter(fc, filterExpr) {
|
|
1556
1592
|
if (!filterExpr) return fc;
|
|
1557
1593
|
const pred = flatFilterPredicate(String(filterExpr));
|
|
@@ -1659,6 +1695,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1659
1695
|
// each layer where its shape is found (maptheme.js 9324, 10376); records
|
|
1660
1696
|
// with no shape are left out (10387-10392)
|
|
1661
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
|
+
//
|
|
1662
1705
|
// flat runs the after-aggregation steps after EACH layer's pass, over all
|
|
1663
1706
|
// items; DIFFERENCE turns a record's value fields into their difference
|
|
1664
1707
|
// and drops the last, so a second pass leaves the earlier layers' items
|
|
@@ -1673,7 +1716,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1673
1716
|
const features = [];
|
|
1674
1717
|
const indexes = layers.map(layer => {
|
|
1675
1718
|
const index = new Map();
|
|
1676
|
-
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 => {
|
|
1677
1720
|
// in theme order, as flat's layer holds the themes' shapes
|
|
1678
1721
|
for (const [k, v] of featurePositionIndex(r)) { if (flatShapeWins(index.get(k), v.area)) index.set(k, v); }
|
|
1679
1722
|
});
|
|
@@ -1760,6 +1803,25 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1760
1803
|
return Math.min(0.9, o);
|
|
1761
1804
|
}
|
|
1762
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
|
+
|
|
1763
1825
|
function joinChoroplethFeatures(spec, table, runtimes) {
|
|
1764
1826
|
// a page can define several FEATURE layers of one name — e.g. the
|
|
1765
1827
|
// comuni as polygons and as center points (for chart positions); flat
|
|
@@ -2092,6 +2154,36 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2092
2154
|
// colorscheme (isGeneratedColorScheme above), never as the step count.
|
|
2093
2155
|
function resolveColorScheme(colorscheme, labels, classes) {
|
|
2094
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
|
+
}
|
|
2095
2187
|
colorscheme = applyClassesToColorScheme(colorscheme, classes);
|
|
2096
2188
|
if (isGeneratedColorScheme(colorscheme)) {
|
|
2097
2189
|
const [, cc1, cc2, nParam1, nParam2] = colorscheme;
|
|
@@ -2197,7 +2289,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2197
2289
|
// mean — see the NOOUTLIER block in _buildChartLayers) has real
|
|
2198
2290
|
// behavior now; bare OUTLIER (the real source's "keep ONLY the
|
|
2199
2291
|
// outliers" inverse mode) stays unimplemented — no config here uses it.
|
|
2200
|
-
|
|
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',
|
|
2201
2296
|
'POW2', 'POW3', 'NOSORT'];
|
|
2202
2297
|
// flat's PIE / DONUT variants with no rendering here (see _buildPieLayers)
|
|
2203
2298
|
const PIE_INERT_FLAGS = [/3D/, /VOLUME/, /HEIGHT/, /STARBURST/, /FLOWER/, /RAYS/, /BOW/, /ZOOM/, /DIRECTION/, /DIRECTED/, /POLAR/];
|
|
@@ -2478,6 +2573,27 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2478
2573
|
return construct;
|
|
2479
2574
|
}
|
|
2480
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
|
+
}
|
|
2481
2597
|
|
|
2482
2598
|
// ---------------------------------------------------------------
|
|
2483
2599
|
// Binding aliases. Real ixmaps-flat accepts ~50 .binding() keys and maps
|
|
@@ -2488,7 +2604,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2488
2604
|
// edit it by hand; `npm run unit` fails when it drifts.
|
|
2489
2605
|
// ---------------------------------------------------------------
|
|
2490
2606
|
// <grammar:binding-aliases>
|
|
2491
|
-
// 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
|
|
2492
2608
|
const FLAT_BINDING_ALIASES = {
|
|
2493
2609
|
"aggregation": "style.aggregationfield",
|
|
2494
2610
|
"aggregationfield": "style.aggregationfield",
|
|
@@ -2550,7 +2666,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2550
2666
|
// both — so .style({tooltip}) is a tooltip and .meta({fillopacity}) a style
|
|
2551
2667
|
// property. GENERATED by test/sync-grammar.mjs — do not edit.
|
|
2552
2668
|
// <grammar:meta-keys>
|
|
2553
|
-
// generated from ixmaps-grammar 0.1.
|
|
2669
|
+
// generated from ixmaps-grammar 0.1.28 — 5 meta keys
|
|
2554
2670
|
const FLAT_META_KEYS = ["description","name","snippet","title","tooltip"];
|
|
2555
2671
|
// </grammar:meta-keys>
|
|
2556
2672
|
|
|
@@ -2742,7 +2858,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2742
2858
|
'sizepow', 'rangescale', 'minvalue', 'maxvalue', 'markersize', 'boxopacity', 'outlierscale', 'valuescale',
|
|
2743
2859
|
'brightness', 'fractionscale', 'dopacityscale', 'dopacitypow', 'gridwidthpx', 'textscale', 'rangecentervalue',
|
|
2744
2860
|
'shadowblur', 'shadowdx', 'shadowdy', 'maxshadow', 'offsetx', 'offsety', 'gridx', 'boxmargin', 'borderwidth',
|
|
2745
|
-
'borderradius', 'maxcharts'];
|
|
2861
|
+
'borderradius', 'maxcharts', 'archeight'];
|
|
2746
2862
|
function toNumberIfNumeric(v) {
|
|
2747
2863
|
if (typeof v !== 'string' || !v.trim()) return v;
|
|
2748
2864
|
const n = Number(v);
|
|
@@ -2906,6 +3022,15 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2906
3022
|
}
|
|
2907
3023
|
|
|
2908
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
|
+
}
|
|
2909
3034
|
return { name: def.layer, data: def.data, binding, targets, geometry, flags, typeStr, style, meta, filter };
|
|
2910
3035
|
}
|
|
2911
3036
|
|
|
@@ -3062,7 +3187,58 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3062
3187
|
light: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
|
|
3063
3188
|
dark: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
|
|
3064
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
|
+
}
|
|
3065
3239
|
function resolveBasemapStyleUrl(mapType) {
|
|
3240
|
+
const raster = flatRasterBasemap(mapType);
|
|
3241
|
+
if (raster) return buildRasterBasemapStyle(raster);
|
|
3066
3242
|
return typeof mapType === 'string' && /dark|black|night|matter/i.test(mapType) ? BASEMAP_STYLE_URLS.dark : BASEMAP_STYLE_URLS.light;
|
|
3067
3243
|
}
|
|
3068
3244
|
// Real ixmaps-flat's legend look (tools/legend.js 3773-3845, via
|
|
@@ -3171,6 +3347,29 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3171
3347
|
return false;
|
|
3172
3348
|
}
|
|
3173
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
|
+
|
|
3174
3373
|
function buildBlankBackgroundStyle(color) {
|
|
3175
3374
|
return {
|
|
3176
3375
|
version: 8,
|
|
@@ -3660,6 +3859,14 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3660
3859
|
// explicit request (real engine's own fallback is right-anchored
|
|
3661
3860
|
// too, just without an opinion on vertical placement).
|
|
3662
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());
|
|
3663
3870
|
// Real ixmaps-flat's SUB-THEME (MapTheme.createSubTheme,
|
|
3664
3871
|
// maptheme.js 14284-14292 / 15010-15160; map.Themes.enableSubThemes
|
|
3665
3872
|
// defaults to true): marking fields of a multi-field choropleth
|
|
@@ -3729,7 +3936,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3729
3936
|
if (legendStack && legendStack.parentNode) return legendStack;
|
|
3730
3937
|
legendStack = document.createElement('div');
|
|
3731
3938
|
legendStack.className = 'ix-legend-stack';
|
|
3732
|
-
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;'
|
|
3733
3945
|
+ 'display:flex;flex-direction:column;max-height:66.6%;overflow-y:auto;'
|
|
3734
3946
|
+ 'border-radius:6px;box-shadow:' + shadow + ';pointer-events:auto;';
|
|
3735
3947
|
if (!document.getElementById('ix-legend-stack-css')) {
|
|
@@ -3839,7 +4051,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3839
4051
|
const looksDark = typeof builder.__effectiveDark === 'function'
|
|
3840
4052
|
? builder.__effectiveDark(isNaN(opacityOpt) ? 1 : opacityOpt, builder.mapOptions.mapType, legendBackground)
|
|
3841
4053
|
: flatLook.dark;
|
|
3842
|
-
const isLightLegend = rt.style.legendtheme === 'light' || (rt.style.legendtheme !== 'dark' && !looksDark);
|
|
4054
|
+
const isLightLegend = legendCenter || rt.style.legendtheme === 'light' || (rt.style.legendtheme !== 'dark' && !looksDark);
|
|
3843
4055
|
const colors = isLightLegend
|
|
3844
4056
|
? { bg: 'rgba(255,255,255,0.92)', fg: '#1a1a1a', shadow: '0 2px 10px rgba(0,0,0,0.18)',
|
|
3845
4057
|
rowMarked: 'rgba(0,0,0,0.08)', track: 'rgba(0,0,0,0.08)',
|
|
@@ -3850,7 +4062,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3850
4062
|
// the panel bg follows the same effective decision (flatLegendLook
|
|
3851
4063
|
// would hand the dark look a light bg here: its dark branch never
|
|
3852
4064
|
// ran for this map, its light bg would clash with dark rows/text)
|
|
3853
|
-
if (!rt.style.legendtheme) colors.bg = looksDark ? '#111' : flatLook.bg;
|
|
4065
|
+
if (!rt.style.legendtheme && !legendCenter) colors.bg = looksDark ? '#111' : flatLook.bg;
|
|
3854
4066
|
return colors;
|
|
3855
4067
|
};
|
|
3856
4068
|
const legendColors = lookColors();
|
|
@@ -3868,7 +4080,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3868
4080
|
// one box for every theme, as flat's #map-legend (legend.js
|
|
3869
4081
|
// 2200-2460): the panels stack in the legend stack in theme
|
|
3870
4082
|
// order, a line between them (see layoutLegends)
|
|
3871
|
-
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;'
|
|
3872
4090
|
+ 'display:flex;flex-direction:column;text-align:left;'
|
|
3873
4091
|
+ 'background:' + legendColors.bg + ';color:' + legendColors.fg + ';font:12px/1.4 -apple-system,Arial,sans-serif;'
|
|
3874
4092
|
+ 'padding:10px 12px 12px;pointer-events:auto;';
|
|
@@ -3938,6 +4156,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3938
4156
|
// branch uses (no antimeridian wraparound handling — matches
|
|
3939
4157
|
// that existing convention, not a new gap this introduces).
|
|
3940
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;
|
|
3941
4162
|
// No-op default: TEXTLEGEND never reassigns this (see below),
|
|
3942
4163
|
// so the LATER unconditional-looking `renderRows()` call
|
|
3943
4164
|
// safely does nothing rather than needing its own isTextOnly
|
|
@@ -3959,8 +4180,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3959
4180
|
const c = f.geometry && f.geometry.coordinates;
|
|
3960
4181
|
if (!c || typeof c[0] !== 'number') return;
|
|
3961
4182
|
const [lng, lat] = c;
|
|
3962
|
-
if (lng
|
|
3963
|
-
if (globeCenter && !isOnVisibleHemisphere(lng, lat, globeCenter)) return;
|
|
4183
|
+
if (!inViewport(lng, lat, bbox, globeCenter)) return;
|
|
3964
4184
|
rt._multiFields.forEach((k, i) => { totals[i] += parseFloat(f.properties[k]) || 0; });
|
|
3965
4185
|
});
|
|
3966
4186
|
maxTotal = Math.max(0, ...totals);
|
|
@@ -3981,8 +4201,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3981
4201
|
while (Array.isArray(pt) && Array.isArray(pt[0])) pt = pt[0];
|
|
3982
4202
|
if (!Array.isArray(pt)) return;
|
|
3983
4203
|
const [lng, lat] = pt;
|
|
3984
|
-
if (lng
|
|
3985
|
-
if (globeCenter && !isOnVisibleHemisphere(lng, lat, globeCenter)) return;
|
|
4204
|
+
if (!inViewport(lng, lat, bbox, globeCenter)) return;
|
|
3986
4205
|
totals[idx] += useSum ? (parseFloat(f.properties[valueField]) || 0) : 1;
|
|
3987
4206
|
});
|
|
3988
4207
|
maxTotal = Math.max(0, ...totals);
|
|
@@ -3996,8 +4215,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3996
4215
|
// share, then overflow-y:auto scrolls just the row list) —
|
|
3997
4216
|
// the header above and description/slider below stay put.
|
|
3998
4217
|
rowsScroll = document.createElement('div');
|
|
3999
|
-
rowsScroll.style.cssText =
|
|
4000
|
-
|
|
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');
|
|
4001
4224
|
rowsEl.className = 'ix-legend-rows';
|
|
4002
4225
|
// .type("...|SIMPLELEGEND") — real ixmaps-flat's OTHER legend
|
|
4003
4226
|
// variant (confirmed against a real-engine screenshot, per
|
|
@@ -4022,7 +4245,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4022
4245
|
// smaller categories packed 3-4 per row) rather than a fixed
|
|
4023
4246
|
// column count.
|
|
4024
4247
|
if (rt.flags.has('SIMPLELEGEND') || rt.flags.has('COMPACTLEGEND')) {
|
|
4025
|
-
|
|
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;';
|
|
4026
4252
|
}
|
|
4027
4253
|
rowsScroll.appendChild(rowsEl);
|
|
4028
4254
|
|
|
@@ -4066,7 +4292,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4066
4292
|
&& !(Array.isArray(rt.style.ranges) && rt.style.ranges.length);
|
|
4067
4293
|
if (flatSingleRowLegend) {
|
|
4068
4294
|
renderRows = function() {
|
|
4069
|
-
const rgb = rt.
|
|
4295
|
+
const rgb = rt.classRgb(0);
|
|
4070
4296
|
const unit = String(rt.style.legendunits || rt.style.units || '').replace(/ /g, ' ');
|
|
4071
4297
|
const vMin = styleNum(rt.style.minvalue) || rt._valueMin, vMax = styleNum(rt.style.maxvalue) || rt._valueMax;
|
|
4072
4298
|
const label = rt.style.label != null && !Array.isArray(rt.style.label) ? String(rt.style.label) : String(rt.binding.value || '');
|
|
@@ -4099,7 +4325,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4099
4325
|
for (let line = 0; line < nLines; line++) {
|
|
4100
4326
|
for (let c = 0; c < n; c++) {
|
|
4101
4327
|
const ix = flatFlag(rt.flags, 'INVERT') ? n - c - 1 : c;
|
|
4102
|
-
const rgb = rt.
|
|
4328
|
+
const rgb = rt.classRgb(ix); // colorscheme "none": no class colors
|
|
4103
4329
|
const part = rt.partsA[ix];
|
|
4104
4330
|
const title = part ? flatFormatValue(part.min, dec, '') + unit + ' ... ' + flatFormatValue(part.max, dec, '') + unit : '';
|
|
4105
4331
|
const marked = rt._markedClasses.has(ix);
|
|
@@ -4130,7 +4356,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4130
4356
|
// flat drops the rows without a count once any row has one
|
|
4131
4357
|
// (legend.js 754-760, 787-789: fCountBars && !count)
|
|
4132
4358
|
rowsEl.innerHTML = order.filter(i => !(maxTotal > 0) || totals[i]).map(i => {
|
|
4133
|
-
const rgb = rt.
|
|
4359
|
+
const rgb = rt.classRgb(i); // colorscheme "none": no class colors
|
|
4134
4360
|
const color = legendFill(i) || `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;
|
|
4135
4361
|
const marked = rt._markedClasses.has(i);
|
|
4136
4362
|
const dimmed = rt._markedClasses.size > 0 && !marked;
|
|
@@ -4332,6 +4558,101 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4332
4558
|
bodyEl.appendChild(desc);
|
|
4333
4559
|
}
|
|
4334
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
|
+
|
|
4335
4656
|
// Chart-size slider — real engine's own range (legend.js:
|
|
4336
4657
|
// 3196-3202), 100% == scale:1, wired to changeThemeStyle(
|
|
4337
4658
|
// themeId,"scale:"+pct/100,"set")+redrawTheme there; this
|
|
@@ -4356,20 +4677,30 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4356
4677
|
: Math.round((styleNum(rt.style.scale) || 1) * 100);
|
|
4357
4678
|
const sliderLabel = isOpacitySlider ? 'Opacity' : 'Chart size';
|
|
4358
4679
|
sliderRow.innerHTML = sliderLabel + ': <span class="ix-legend-scale-val">' + initialPct + '</span>%';
|
|
4359
|
-
|
|
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);
|
|
4360
4689
|
const slider = document.createElement('input');
|
|
4361
4690
|
slider.type = 'range';
|
|
4362
4691
|
slider.min = isOpacitySlider ? '0' : '25';
|
|
4363
4692
|
slider.max = isOpacitySlider ? '100' : '200';
|
|
4364
4693
|
slider.value = String(initialPct);
|
|
4365
|
-
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)
|
|
4366
4697
|
slider.addEventListener('input', () => {
|
|
4367
4698
|
const pct = parseInt(slider.value, 10);
|
|
4368
4699
|
sliderRow.querySelector('.ix-legend-scale-val').textContent = pct;
|
|
4369
4700
|
rt.setStyle(isOpacitySlider ? { fillopacity: pct / 100 } : { scale: pct / 100 });
|
|
4370
4701
|
quietRefresh();
|
|
4371
4702
|
});
|
|
4372
|
-
|
|
4703
|
+
sliderHost.appendChild(slider);
|
|
4373
4704
|
}
|
|
4374
4705
|
}
|
|
4375
4706
|
};
|
|
@@ -4437,7 +4768,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4437
4768
|
this._layerBuilders.push(lb);
|
|
4438
4769
|
return lb;
|
|
4439
4770
|
}
|
|
4440
|
-
this._layerBuilders.push(
|
|
4771
|
+
this._layerBuilders.push(asLayerBuilder(nameOrBuilder));
|
|
4441
4772
|
return this;
|
|
4442
4773
|
}
|
|
4443
4774
|
// .Layer() — alias of .layer()
|
|
@@ -4462,8 +4793,28 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4462
4793
|
// the Map(id, opts, map => map.options(...)) callback form worked).
|
|
4463
4794
|
// One microtask lets the caller's whole synchronous chain land first.
|
|
4464
4795
|
await null;
|
|
4465
|
-
|
|
4466
|
-
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
|
+
}
|
|
4467
4818
|
// flat's init replaces the container's content with its own map page
|
|
4468
4819
|
// (mappage.html via innerHTML), so a page's placeholder inside the
|
|
4469
4820
|
// map div ("connecting to Internet ...", the fuel-price page) goes
|
|
@@ -4515,8 +4866,15 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4515
4866
|
_titleHost = el;
|
|
4516
4867
|
// a flat SVG map in another projection (equalearth.svg): its view
|
|
4517
4868
|
// framing and map scale (see FLAT_SVG_PROJECTIONS)
|
|
4518
|
-
const svgProjection = flatSvgProjectionOf(this.mapOptions.map)
|
|
4869
|
+
const svgProjection = flatSvgProjectionOf(this.mapOptions.map)
|
|
4870
|
+
|| (isFlatOrthographicName(this.mapOptions.mapProjection) ? FLAT_ORTHOGRAPHIC_PROJECTION : null);
|
|
4519
4871
|
if (svgProjection) flatSvgProjectionByOptions.set(this._engineOptions, svgProjection);
|
|
4872
|
+
const planeProjection = flatPlaneProjectionOf(this.mapOptions);
|
|
4873
|
+
if (planeProjection) flatPlaneProjectionByOptions.set(this._engineOptions, planeProjection);
|
|
4874
|
+
if (isFlatOrthographicName(this.mapOptions.mapProjection)) {
|
|
4875
|
+
flatOrthographicByOptions.set(this._engineOptions, true);
|
|
4876
|
+
if (this._viewCenter) flatViewCenterByOptions.set(this._engineOptions, { lat: this._viewCenter[0], lng: this._viewCenter[1] });
|
|
4877
|
+
}
|
|
4520
4878
|
const [vLat, vLng] = this._viewCenter || [45.5, 9.2];
|
|
4521
4879
|
const initialZoom = this._viewZoom != null && this._viewZoom !== ''
|
|
4522
4880
|
? flatViewToMapLibreZoom(Number(this._viewZoom), Number(vLat), Number(vLng), el.clientWidth, el.clientHeight, svgProjection)
|
|
@@ -4558,6 +4916,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4558
4916
|
// build() and every later defineLayer()/refreshTheme() share it
|
|
4559
4917
|
const dataCache = this._dataCache = this._dataCache || new Map();
|
|
4560
4918
|
const runtimes = [];
|
|
4919
|
+
const sphereBackdrops = []; // {type:"Sphere"} layers read before the map exists
|
|
4561
4920
|
// themes whose data comes by name (see namedDataTheme) start empty and
|
|
4562
4921
|
// load on their own once the map is built, as in flat, where every
|
|
4563
4922
|
// theme loads independently: a broker that answers late — or only
|
|
@@ -4589,9 +4948,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4589
4948
|
// instead of colliding with each other under one 'null' entry.
|
|
4590
4949
|
let raw, cacheKey;
|
|
4591
4950
|
const deferred = deferredFeatureLoad(spec, initialZoom);
|
|
4592
|
-
|
|
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) {
|
|
4593
4960
|
raw = { type: 'FeatureCollection', features: [] };
|
|
4594
|
-
cacheKey = named ? 'name:' + spec.data.name : 'deferred:' + spec.name;
|
|
4961
|
+
cacheKey = named ? 'name:' + spec.data.name : wms ? 'wms:' + spec.name : 'deferred:' + spec.name;
|
|
4595
4962
|
} else if (spec.data && spec.data.obj) {
|
|
4596
4963
|
raw = await fetchLayerData(spec.data, spec.binding, this._engineOptions);
|
|
4597
4964
|
cacheKey = 'obj:' + spec.name;
|
|
@@ -4600,10 +4967,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4600
4967
|
cacheKey = cached.sourceKey;
|
|
4601
4968
|
raw = await cached.promise;
|
|
4602
4969
|
}
|
|
4603
|
-
const
|
|
4604
|
-
const fc = filtered.type === 'Table' ? joinTableFeatures(spec, filtered, runtimes) : filtered;
|
|
4970
|
+
const fc = filterAndJoin(raw, spec, runtimes);
|
|
4605
4971
|
const rt = new LayerRuntime(spec, fc, this._engineOptions);
|
|
4606
|
-
if (spec.filter) rt._refilter =
|
|
4972
|
+
if (spec.filter) rt._refilter = expr => filterAndJoin(raw, spec, runtimes, expr);
|
|
4607
4973
|
// tags which underlying data source this runtime came from — see
|
|
4608
4974
|
// setFacetFilter/clearFacetFilter/clearAllFacetFilters below,
|
|
4609
4975
|
// which use this to propagate a facet filter to every theme
|
|
@@ -4615,6 +4981,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4615
4981
|
rt._named = named;
|
|
4616
4982
|
rt._zoomToPending = wantsZoomToExtent(spec.flags);
|
|
4617
4983
|
runtimes.push(rt);
|
|
4984
|
+
if (fc._sphere) sphereBackdrops.push(rt.style);
|
|
4618
4985
|
notifyNewTheme(rt);
|
|
4619
4986
|
_globalThemeRegistry.set(rt.name, rt);
|
|
4620
4987
|
// See findRuntime's own comment on the three real theme-id
|
|
@@ -4642,6 +5009,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4642
5009
|
: resolveBasemapStyleUrl(this.mapOptions.mapType),
|
|
4643
5010
|
center: [lon, lat],
|
|
4644
5011
|
zoom: initialZoom,
|
|
5012
|
+
...cameraOptions(this._engineOptions, this.mapOptions),
|
|
5013
|
+
// flat's orthographic globe can be centered anywhere (the mechanical-goods page: 70°N);
|
|
5014
|
+
// MapLibre's default keeps the world's Mercator edge out of view, which drags the
|
|
5015
|
+
// center south (64° at 1024×768) and forces a larger zoom (MapLibre >= 6)
|
|
5016
|
+
...(isFlatOrthographicName(this.mapOptions.mapProjection)
|
|
5017
|
+
? { transformConstrain: (lngLat, zoom) => ({ center: new maplibregl.LngLat(lngLat.lng, globeCenterLat(lngLat.lat)), zoom }) }
|
|
5018
|
+
: {}),
|
|
4645
5019
|
// MapLibre's own AttributionControl (bottom right) keeps the
|
|
4646
5020
|
// basemap's required CARTO/OpenStreetMap credit; the page's own
|
|
4647
5021
|
// .attribution() goes bottom left, as flat shows it
|
|
@@ -4666,6 +5040,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4666
5040
|
renderWorldCopies: this._engineOptions.worldcopies !== false
|
|
4667
5041
|
});
|
|
4668
5042
|
engineOptionsByMap.set(map, this._engineOptions);
|
|
5043
|
+
sphereBackdrops.forEach(style => applySphereBackdrop(map, style));
|
|
4669
5044
|
// the live-SVG pane USER charts render into (see chartHostFor) —
|
|
4670
5045
|
// registered here, keyed by this map's options object, so every
|
|
4671
5046
|
// runtime (including later loadProject/map.layer ones) finds it
|
|
@@ -5156,6 +5531,16 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5156
5531
|
const rt = findRuntime(themeId);
|
|
5157
5532
|
if (rt) { rt._hidden = !visible; refresh(); }
|
|
5158
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
|
+
},
|
|
5159
5544
|
getCategoryColor: (themeId, value) => {
|
|
5160
5545
|
const rt = findRuntime(themeId);
|
|
5161
5546
|
if (!rt || !rt.categoryIndexByLabel) return null;
|
|
@@ -5290,13 +5675,14 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5290
5675
|
for (const rt of targets) {
|
|
5291
5676
|
if (rt._named) { await loadNamedTheme(rt, true); continue; }
|
|
5292
5677
|
const spec = normalizeTheme(rt._definition);
|
|
5293
|
-
//
|
|
5294
|
-
//
|
|
5295
|
-
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
|
|
5296
5683
|
? await fetchLayerData(spec.data, rt._specBinding, builder._engineOptions)
|
|
5297
5684
|
: await cachedLayerData(builder._dataCache, spec.data, rt._specBinding, builder._engineOptions, { reload: true }).promise;
|
|
5298
|
-
|
|
5299
|
-
rt.replaceFeatures(filtered.type === 'Table' ? joinTableFeatures(spec, filtered, runtimes) : filtered);
|
|
5685
|
+
rt.replaceFeatures(filterAndJoin(raw, spec, runtimes));
|
|
5300
5686
|
maybeZoomToTheme(rt); // a scale-deferred theme gets its first data here
|
|
5301
5687
|
}
|
|
5302
5688
|
if (targets.length) { refresh(); notifyRedraw(); }
|
|
@@ -5440,9 +5826,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5440
5826
|
if (broker) { pendingLoads--; rt._brokerLoading--; setDataLoading(); }
|
|
5441
5827
|
}
|
|
5442
5828
|
}
|
|
5443
|
-
function namedThemeFeatures(spec, rows, binding) {
|
|
5444
|
-
|
|
5445
|
-
return filtered.type === 'Table' ? joinTableFeatures(spec, filtered, runtimes) : filtered;
|
|
5829
|
+
function namedThemeFeatures(spec, rows, binding, expr) {
|
|
5830
|
+
return filterAndJoin(rowsResult(rows, binding || spec.binding), spec, runtimes, expr);
|
|
5446
5831
|
}
|
|
5447
5832
|
function fillNamedTheme(rt, spec, table, patch) {
|
|
5448
5833
|
rt._lastTable = table;
|
|
@@ -5461,7 +5846,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5461
5846
|
spec = pspec;
|
|
5462
5847
|
}
|
|
5463
5848
|
rt.replaceFeatures(namedThemeFeatures(spec, rows, rt._specBinding));
|
|
5464
|
-
rt._refilter = spec.filter ? expr => namedThemeFeatures(
|
|
5849
|
+
rt._refilter = spec.filter ? expr => namedThemeFeatures(spec, rows, rt._specBinding, expr) : null;
|
|
5465
5850
|
if (glDebug()) console.info(`[ixmaps-gl debug] data for ${themeIdOf(rt)}: ${rows.length} rows → ${rt.features.length} features`);
|
|
5466
5851
|
// the legend is built from the theme's classes: build it again
|
|
5467
5852
|
if (rt._legendPanel && rt._legendPanel.parentNode) rt._legendPanel.parentNode.removeChild(rt._legendPanel);
|
|
@@ -5523,8 +5908,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5523
5908
|
let raw;
|
|
5524
5909
|
const deferred = deferredFeatureLoad(spec, map.getZoom());
|
|
5525
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');
|
|
5526
5913
|
let sourceKey = null;
|
|
5527
|
-
if (deferred || named) {
|
|
5914
|
+
if (deferred || named || wms) {
|
|
5528
5915
|
raw = { type: 'FeatureCollection', features: [] };
|
|
5529
5916
|
} else if (spec.data && spec.data.obj) {
|
|
5530
5917
|
// in memory already — nothing to cache (see build())
|
|
@@ -5534,10 +5921,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5534
5921
|
sourceKey = cached.sourceKey;
|
|
5535
5922
|
raw = await cached.promise;
|
|
5536
5923
|
}
|
|
5537
|
-
const
|
|
5538
|
-
const fc = filtered.type === 'Table' ? joinTableFeatures(spec, filtered, runtimes) : filtered;
|
|
5924
|
+
const fc = filterAndJoin(raw, spec, runtimes);
|
|
5539
5925
|
const rt = new LayerRuntime(spec, fc, builder._engineOptions);
|
|
5540
|
-
if (spec.filter) rt._refilter =
|
|
5926
|
+
if (spec.filter) rt._refilter = expr => filterAndJoin(raw, spec, runtimes, expr);
|
|
5541
5927
|
rt._definition = pdef; // for refreshTheme
|
|
5542
5928
|
rt._deferredLoad = deferred;
|
|
5543
5929
|
rt._named = named;
|
|
@@ -5545,6 +5931,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5545
5931
|
rt._dataSourceKey = named ? 'name:' + spec.data.name
|
|
5546
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) });
|
|
5547
5933
|
runtimes.push(rt);
|
|
5934
|
+
if (fc._sphere) applySphereBackdrop(map, rt.style);
|
|
5548
5935
|
_globalThemeRegistry.set(rt.name, rt);
|
|
5549
5936
|
if (rt.meta && rt.meta.name && rt.meta.name !== rt.name) _globalThemeRegistry.set(rt.meta.name, rt);
|
|
5550
5937
|
if (themeIdOf(rt) !== rt.name) _globalThemeRegistry.set(themeIdOf(rt), rt);
|
|
@@ -5612,9 +5999,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5612
5999
|
const svg = String(m.map || '');
|
|
5613
6000
|
// its view and map scale follow the project's map file, as on flat
|
|
5614
6001
|
if (svg) {
|
|
5615
|
-
const svgProjection = flatSvgProjectionOf(svg);
|
|
6002
|
+
const svgProjection = flatSvgProjectionOf(svg) || (/orthographic\.svg$/i.test(svg) ? FLAT_ORTHOGRAPHIC_PROJECTION : null);
|
|
5616
6003
|
if (svgProjection) flatSvgProjectionByOptions.set(builder._engineOptions, svgProjection);
|
|
5617
6004
|
else flatSvgProjectionByOptions.delete(builder._engineOptions);
|
|
6005
|
+
if (svgProjection && svgProjection.invert) flatPlaneProjectionByOptions.set(builder._engineOptions, svgProjection);
|
|
6006
|
+
else flatPlaneProjectionByOptions.delete(builder._engineOptions);
|
|
6007
|
+
if (/orthographic\.svg$/i.test(svg)) flatOrthographicByOptions.set(builder._engineOptions, true);
|
|
6008
|
+
else { flatOrthographicByOptions.delete(builder._engineOptions); flatViewCenterByOptions.delete(builder._engineOptions); }
|
|
5618
6009
|
}
|
|
5619
6010
|
if (svg && !/generic\/(mercator|orthographic)\.svg$/i.test(svg)) report.notes.push(`flat SVG map "${svg.split('/').pop()}" is not available in ixmaps-gl — mercator used`);
|
|
5620
6011
|
if (typeof map.setProjection === 'function') {
|
|
@@ -5717,7 +6108,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5717
6108
|
return Math.round((z - Math.log2(Math.cos(lat))) * 1e6) / 1e6;
|
|
5718
6109
|
}
|
|
5719
6110
|
function refreshLayers() {
|
|
5720
|
-
const
|
|
6111
|
+
const projNow = typeof map.getProjection === 'function' && map.getProjection();
|
|
6112
|
+
const liveZoom = projNow && projNow.type === 'globe' && flatOrthographicByOptions.has(builder._engineOptions)
|
|
6113
|
+
? globeSizingZoom(map.getZoom(), map.getCenter().lat, map.getContainer().clientHeight) : map.getZoom();
|
|
5721
6114
|
// frozen only while MapLibre really is zooming (see 'moveend')
|
|
5722
6115
|
const zoom = isZooming && map.isZooming() ? stableGridZoom : gridZoomOf();
|
|
5723
6116
|
const viewMoving = map.isMoving();
|
|
@@ -5781,7 +6174,71 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5781
6174
|
if (order !== refreshLayers._lastOrder) { refreshLayers._lastOrder = order; console.info('[ixmaps-gl debug] draw order (bottom → top): ' + order); }
|
|
5782
6175
|
}
|
|
5783
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
|
+
});
|
|
5784
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());
|
|
5785
6242
|
|
|
5786
6243
|
// isZooming flips true/false on every 'zoomstart'/'zoomend' pair —
|
|
5787
6244
|
// and a scroll-wheel or trackpad zoom fires MANY such pairs per
|
|
@@ -6077,7 +6534,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6077
6534
|
// theirs and are not overridden.
|
|
6078
6535
|
for (const m of ['replace', 'add', 'newTheme', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
6079
6536
|
'refreshTheme', 'redrawTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'setProjection', 'resize',
|
|
6080
|
-
'loadProject', 'project', 'setThemeVisible', 'setThemeTimeFrame', 'show', 'hide', 'getZoom', 'getThemeObj',
|
|
6537
|
+
'loadProject', 'project', 'setThemeVisible', 'switchLayer', 'setThemeTimeFrame', 'show', 'hide', 'getZoom', 'getThemeObj',
|
|
6081
6538
|
'getThemes', 'theme', 'setView']) {
|
|
6082
6539
|
if (!MapBuilder.prototype[m]) {
|
|
6083
6540
|
MapBuilder.prototype[m] = function (...args) {
|
|
@@ -6100,7 +6557,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6100
6557
|
// .catch()/await still sees the real failure — this only guarantees
|
|
6101
6558
|
// the splash doesn't outlive it.
|
|
6102
6559
|
const promise = builder.build().catch(err => {
|
|
6103
|
-
|
|
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);
|
|
6104
6563
|
const splash = el && el.querySelector('.ixmaps-splash');
|
|
6105
6564
|
if (splash) hideSplash(splash);
|
|
6106
6565
|
throw err;
|
|
@@ -6345,6 +6804,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6345
6804
|
const M = Math.sqrt(3) / 2, A1 = 1.340264, A2 = -0.081106, A3 = 0.000893, A4 = 0.003796;
|
|
6346
6805
|
const l = Math.asin(M * Math.sin(lat * Math.PI / 180)), l2 = l * l, l6 = l2 * l2 * l2;
|
|
6347
6806
|
return [lon * Math.PI / 180 * Math.cos(l) / (M * (A1 + 3 * A2 * l2 + l6 * (7 * A3 + 9 * A4 * l2))), l * (A1 + A2 * l2 + l6 * (A3 + A4 * l2))];
|
|
6807
|
+
},
|
|
6808
|
+
// [lat, lon] in degrees of a projected point (Newton on the parametric latitude)
|
|
6809
|
+
invert: (x, y) => {
|
|
6810
|
+
const M = Math.sqrt(3) / 2, A1 = 1.340264, A2 = -0.081106, A3 = 0.000893, A4 = 0.003796;
|
|
6811
|
+
let l = y, l2 = l * l, l6 = l2 * l2 * l2;
|
|
6812
|
+
for (let i = 0; i < 12; i++) {
|
|
6813
|
+
const delta = (l * (A1 + A2 * l2 + l6 * (A3 + A4 * l2)) - y) / (A1 + 3 * A2 * l2 + l6 * (7 * A3 + 9 * A4 * l2));
|
|
6814
|
+
l -= delta; l2 = l * l; l6 = l2 * l2 * l2;
|
|
6815
|
+
if (Math.abs(delta) < 1e-12) break;
|
|
6816
|
+
}
|
|
6817
|
+
return [Math.asin(Math.sin(l) / M) * 180 / Math.PI, M * x * (A1 + 3 * A2 * l2 + l6 * (7 * A3 + 9 * A4 * l2)) / Math.cos(l) * 180 / Math.PI];
|
|
6348
6818
|
}
|
|
6349
6819
|
},
|
|
6350
6820
|
winkel: {
|
|
@@ -6368,6 +6838,59 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6368
6838
|
// the flat SVG projection a map's options object belongs to (set by the
|
|
6369
6839
|
// map's init and loadProject; the runtimes only see the options object)
|
|
6370
6840
|
const flatSvgProjectionByOptions = new WeakMap();
|
|
6841
|
+
// the projection plane a map's VECTOR / BEZIER flows are drawn in: flat's
|
|
6842
|
+
// map file, or the Map() option mapProjection ("equalearth") — flat computes
|
|
6843
|
+
// the curves in its map's own coordinates, so the same flow bows around the
|
|
6844
|
+
// pole there. (Unlike flatSvgProjectionByOptions it leaves the view framing alone.)
|
|
6845
|
+
const flatPlaneProjectionByOptions = new WeakMap();
|
|
6846
|
+
// a map that is flat's orthographic one (drawn as MapLibre's globe): its object scale is
|
|
6847
|
+
// flat's (scale × the globe's pixels per degree at its center, measured on flat 2026-10-10
|
|
6848
|
+
// on the mechanical-goods globe page, the same at view zooms 2.5 and 3.5: 197.44e6; the
|
|
6849
|
+
// Mercator map's is FLAT_MERCATOR_SCALE_CONSTANT). MapLibre's globe zoom moves with the
|
|
6850
|
+
// center latitude (the globe keeps its size), so symbols are sized at the zoom of the
|
|
6851
|
+
// globe's real radius (globeSizingZoom). The view center the page asked for stays the
|
|
6852
|
+
// plane its flows are curved in.
|
|
6853
|
+
const FLAT_ORTHOGRAPHIC_SCALE_CONSTANT = 197.44e6;
|
|
6854
|
+
// flat fits its orthographic globe to the map's height: the disc is 0.88 of it at the
|
|
6855
|
+
// view zoom 2.5 and doubles per zoom level (338.6 px of 768 on the mechanical-goods
|
|
6856
|
+
// globe page, as 450 px of 1013 in a real window)
|
|
6857
|
+
const FLAT_ORTHOGRAPHIC_RADIUS_PER_HEIGHT = 338.6 / 768;
|
|
6858
|
+
const FLAT_ORTHOGRAPHIC_PROJECTION = { orthographic: true, scaleConstant: FLAT_ORTHOGRAPHIC_SCALE_CONSTANT };
|
|
6859
|
+
const FLAT_ORTHOGRAPHIC_FLOW_WIDTH = 8.96 / 5.585;
|
|
6860
|
+
const flatOrthographicByOptions = new WeakMap();
|
|
6861
|
+
const flatViewCenterByOptions = new WeakMap();
|
|
6862
|
+
function isFlatOrthographicName(v) { return /^orth?ographic$/i.test(String(v || '').trim()); }
|
|
6863
|
+
// MapLibre's zoom at which a Mercator map of the same pixel scale shows the globe: its
|
|
6864
|
+
// radius is 512 · 2^zoom / 2π / cos(center latitude)
|
|
6865
|
+
// MapLibre's globe is seen in perspective (vertical field of view 36.87°, so a focal length
|
|
6866
|
+
// of 1.5 · the map height, in the px the surface is drawn at in the center): a globe of
|
|
6867
|
+
// radius R (px per radian at its center) fills a disc of radius R / sqrt(1 + 2R / f) on
|
|
6868
|
+
// the screen. Flat's orthographic globe has no perspective: its radius is its disc.
|
|
6869
|
+
function globeDiscRadius(radiusAtCenter, height) {
|
|
6870
|
+
return radiusAtCenter / Math.sqrt(1 + 2 * radiusAtCenter / (1.5 * height));
|
|
6871
|
+
}
|
|
6872
|
+
function globeRadiusOfDisc(discRadius, height) {
|
|
6873
|
+
const f = 1.5 * height, a = discRadius * discRadius / f;
|
|
6874
|
+
return a + Math.sqrt(a * a + discRadius * discRadius);
|
|
6875
|
+
}
|
|
6876
|
+
// the zoom a Mercator map would have for the globe's disc radius — what symbols are sized at,
|
|
6877
|
+
// so they match flat's, whose scale follows its disc
|
|
6878
|
+
// The globe's center stays within 85° of the equator: MapLibre's globe zoom is compensated by
|
|
6879
|
+
// 1 / cos(latitude), so a pole-centered globe would need a zoom of about -7, where the
|
|
6880
|
+
// overlay's layers collapse to one point (flat can center the pole).
|
|
6881
|
+
const GLOBE_MAX_CENTER_LAT = 85;
|
|
6882
|
+
function globeCenterLat(lat) { return Math.max(-GLOBE_MAX_CENTER_LAT, Math.min(GLOBE_MAX_CENTER_LAT, lat)); }
|
|
6883
|
+
function globeSizingZoom(zoom, centerLat, height) {
|
|
6884
|
+
const c = Math.cos(globeCenterLat(centerLat) * Math.PI / 180);
|
|
6885
|
+
const radius = 512 * Math.pow(2, zoom) / (2 * Math.PI * c);
|
|
6886
|
+
return Math.log2(globeDiscRadius(radius, height > 0 ? height : 768) * 2 * Math.PI / 512);
|
|
6887
|
+
}
|
|
6888
|
+
function flatPlaneProjectionOf(mapOptions) {
|
|
6889
|
+
const byUrl = flatSvgProjectionOf(mapOptions.map);
|
|
6890
|
+
if (byUrl) return byUrl;
|
|
6891
|
+
const named = FLAT_SVG_PROJECTIONS[String(mapOptions.mapProjection || '').trim().toLowerCase()];
|
|
6892
|
+
return named || null;
|
|
6893
|
+
}
|
|
6371
6894
|
// flat SVG map scale ÷ the Mercator one at the same on-screen size (1
|
|
6372
6895
|
// without a flat SVG projection)
|
|
6373
6896
|
function flatMapScaleRatio(mapOptions) {
|
|
@@ -6380,6 +6903,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6380
6903
|
// FLAT_SVG_PROJECTIONS), else flatToMapLibreZoom
|
|
6381
6904
|
function flatViewToMapLibreZoom(flatZoom, lat, lng, width, height, proj) {
|
|
6382
6905
|
if (!proj || !(width > 0) || !(height > 0) || !Number.isFinite(lat) || !Number.isFinite(lng)) return flatToMapLibreZoom(flatZoom);
|
|
6906
|
+
if (proj.orthographic) {
|
|
6907
|
+
// the globe's radius as flat draws it; MapLibre's is 512 · 2^zoom / 2π / cos(center lat)
|
|
6908
|
+
const disc = FLAT_ORTHOGRAPHIC_RADIUS_PER_HEIGHT * height * Math.pow(2, flatZoom - 2.5);
|
|
6909
|
+
return Math.log2(globeRadiusOfDisc(disc, height) * 2 * Math.PI * Math.cos(globeCenterLat(lat) * Math.PI / 180) / 512);
|
|
6910
|
+
}
|
|
6383
6911
|
// flat's Leaflet bounds (256 px tiles) at this zoom
|
|
6384
6912
|
const [sw, ne] = viewBounds(lat, lng, flatToMapLibreZoom(flatZoom), width, height);
|
|
6385
6913
|
const a = proj.project(sw.lat, sw.lng), b = proj.project(ne.lat, ne.lng);
|
|
@@ -7437,6 +7965,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7437
7965
|
// independently-rendered systems (deck.gl's icon quad, MapLibre's own
|
|
7438
7966
|
// globe-sphere mesh) competing for the same depth-buffer real estate.
|
|
7439
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==';
|
|
7440
7971
|
|
|
7441
7972
|
// GLOBE_HORIZON_DEG / isOnVisibleHemisphere: `depthCompare: 'always'`
|
|
7442
7973
|
// above (needed to stop the near-surface z-fighting/half-moon bug —
|
|
@@ -7461,6 +7992,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7461
7992
|
// only to stop the far side bleeding through, not to hem in near-limb
|
|
7462
7993
|
// points that are still legitimately visible (if foreshortened).
|
|
7463
7994
|
const GLOBE_HORIZON_DEG = 90;
|
|
7995
|
+
// a point inside the lng/lat bbox [w, s, e, n] and, under globe projection
|
|
7996
|
+
// (globeCenter set), on the visible hemisphere
|
|
7997
|
+
function inViewport(lng, lat, bbox, globeCenter) {
|
|
7998
|
+
if (lng < bbox[0] || lng > bbox[2] || lat < bbox[1] || lat > bbox[3]) return false;
|
|
7999
|
+
return !globeCenter || isOnVisibleHemisphere(lng, lat, globeCenter);
|
|
8000
|
+
}
|
|
8001
|
+
|
|
7464
8002
|
function isOnVisibleHemisphere(lng, lat, center) {
|
|
7465
8003
|
const toRad = d => d * Math.PI / 180;
|
|
7466
8004
|
const phi0 = toRad(center.lat), phi1 = toRad(lat);
|
|
@@ -7701,6 +8239,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7701
8239
|
switch (shape) {
|
|
7702
8240
|
case 'squarefull': return Math.abs(dx) <= r && Math.abs(dy) <= r;
|
|
7703
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;
|
|
7704
8243
|
case 'roundrect': return Math.abs(dx) <= r * 0.8 && Math.abs(dy) <= r * 0.8;
|
|
7705
8244
|
case 'diamond': case 'carot': return Math.abs(dx) + Math.abs(dy) <= r;
|
|
7706
8245
|
case 'empty': return false;
|
|
@@ -8584,6 +9123,66 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8584
9123
|
return out;
|
|
8585
9124
|
}
|
|
8586
9125
|
|
|
9126
|
+
// The orthographic plane around a view center (flat's orthographic map): project
|
|
9127
|
+
// (lat, lon) → [x, y] in earth radii, y up; invert is the way back ([NaN, NaN]
|
|
9128
|
+
// outside the disc); inFront([lng, lat]) tells the face the viewer sees
|
|
9129
|
+
function orthographicPlane(lat0, lng0) {
|
|
9130
|
+
const r = Math.PI / 180, p0 = lat0 * r, s0 = Math.sin(p0), c0 = Math.cos(p0);
|
|
9131
|
+
return {
|
|
9132
|
+
project: (lat, lon) => {
|
|
9133
|
+
const p = lat * r, dl = (lon - lng0) * r;
|
|
9134
|
+
return [Math.cos(p) * Math.sin(dl), c0 * Math.sin(p) - s0 * Math.cos(p) * Math.cos(dl)];
|
|
9135
|
+
},
|
|
9136
|
+
invert: (x, y) => {
|
|
9137
|
+
const rho = Math.hypot(x, y);
|
|
9138
|
+
if (!(rho <= 1)) return [NaN, NaN];
|
|
9139
|
+
if (!rho) return [lat0, lng0];
|
|
9140
|
+
const c = Math.asin(rho), sc = Math.sin(c), cc = Math.cos(c);
|
|
9141
|
+
const lat = Math.asin(cc * s0 + y * sc * c0 / rho);
|
|
9142
|
+
const lng = lng0 * r + Math.atan2(x * sc, rho * c0 * cc - y * s0 * sc);
|
|
9143
|
+
return [lat / r, ((lng / r + 540) % 360) - 180];
|
|
9144
|
+
},
|
|
9145
|
+
inFront: ([lng, lat]) => s0 * Math.sin(lat * r) + c0 * Math.cos(lat * r) * Math.cos((lng - lng0) * r) > 0
|
|
9146
|
+
};
|
|
9147
|
+
}
|
|
9148
|
+
|
|
9149
|
+
// CHART|VECTOR|BEZIER with 3D (ixmaps-gl only; flat draws its flows flat): the
|
|
9150
|
+
// flow arcs above the map. Height in meters of the vertex i of last+1, a half
|
|
9151
|
+
// sine over the flow: both ends stay on the ground (arrow heads sit there),
|
|
9152
|
+
// the top is ratio · the flow's length (style.archeight, default 0.3)
|
|
9153
|
+
const VECTOR_ARC_HEIGHT_RATIO = 0.3;
|
|
9154
|
+
function vectorArcHeight(i, last, chordMeters, ratio) {
|
|
9155
|
+
return last > 0 ? chordMeters * ratio * Math.sin(Math.PI * i / last) : 0;
|
|
9156
|
+
}
|
|
9157
|
+
// the fractional vertex index of a path (px points) `back` px before its last
|
|
9158
|
+
// point (0 when the path is shorter): where an arrow head's base sits on the
|
|
9159
|
+
// arc, so the head is lifted with the line it ends
|
|
9160
|
+
function pathBackIndex(pts, back) {
|
|
9161
|
+
let left = back;
|
|
9162
|
+
for (let i = pts.length - 1; i > 0; i--) {
|
|
9163
|
+
const len = Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]);
|
|
9164
|
+
if (left <= len) return len ? i - left / len : i;
|
|
9165
|
+
left -= len;
|
|
9166
|
+
}
|
|
9167
|
+
return 0;
|
|
9168
|
+
}
|
|
9169
|
+
// great-circle distance between two [lng, lat], meters
|
|
9170
|
+
function greatCircleMeters(p1, p2) {
|
|
9171
|
+
const r = Math.PI / 180, dLat = (p2[1] - p1[1]) * r, dLng = (p2[0] - p1[0]) * r;
|
|
9172
|
+
const h = Math.sin(dLat / 2) ** 2 + Math.cos(p1[1] * r) * Math.cos(p2[1] * r) * Math.sin(dLng / 2) ** 2;
|
|
9173
|
+
return 2 * 6371008.8 * Math.asin(Math.sqrt(Math.min(1, h)));
|
|
9174
|
+
}
|
|
9175
|
+
// the map's initial camera tilt: .options({pitch, bearing}) or the Map() option
|
|
9176
|
+
// of that name (degrees); maxPitch is raised to the pitch asked for
|
|
9177
|
+
function cameraOptions(engineOptions, mapOptions) {
|
|
9178
|
+
const num = k => { const v = engineOptions && engineOptions[k] != null ? engineOptions[k] : mapOptions && mapOptions[k]; const n = Number(v); return v == null || v === '' || !isFinite(n) ? undefined : n; };
|
|
9179
|
+
const out = {};
|
|
9180
|
+
const pitch = num('pitch'), bearing = num('bearing');
|
|
9181
|
+
if (pitch !== undefined) { out.pitch = Math.max(0, Math.min(85, pitch)); out.maxPitch = Math.max(60, out.pitch); }
|
|
9182
|
+
if (bearing !== undefined) out.bearing = bearing;
|
|
9183
|
+
return out;
|
|
9184
|
+
}
|
|
9185
|
+
|
|
8587
9186
|
// flat's arrow marker (an SVG <marker>, markerUnits strokeWidth, orient
|
|
8588
9187
|
// auto) at a line's end: the triangle M0,s L s,s/2 L0,0 in units of the
|
|
8589
9188
|
// line width sw, its reference point (refX, refY) on the end point, its
|
|
@@ -8644,17 +9243,34 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8644
9243
|
// - HORZ: side by side from the center to the right
|
|
8645
9244
|
// - otherwise (or CENTER): all on the center
|
|
8646
9245
|
// Returns the parts in drawing order: { i, v, r, x, y } (pixels, y down).
|
|
8647
|
-
|
|
9246
|
+
// the flag tests sequenceLayout makes, resolved once per flag set: it runs for
|
|
9247
|
+
// every chart on every redraw, and scanning the flags (and compiling a RegExp
|
|
9248
|
+
// per word) each time cost ~135 ms per redraw on the accidents demo
|
|
9249
|
+
const _sequenceFlagCache = new WeakMap();
|
|
9250
|
+
function sequenceFlags(flags) {
|
|
9251
|
+
const hit = _sequenceFlagCache.get(flags);
|
|
9252
|
+
if (hit && hit.size === flags.size) return hit.info;
|
|
8648
9253
|
const has = f => flatFlag(flags, f);
|
|
8649
|
-
const word = w =>
|
|
9254
|
+
const word = w => { const re = new RegExp('\\b' + w + '\\b'); return [...flags].some(f => re.test(f)); };
|
|
9255
|
+
const info = {
|
|
9256
|
+
sort: word('SORT'), up: word('UP'), down: word('DOWN'),
|
|
9257
|
+
star: has('STAR') && !flatFlag(flags, 'CENTER'), horz: has('HORZ') && !flatFlag(flags, 'CENTER'),
|
|
9258
|
+
expand: has('EXPAND'), expandmax: has('EXPANDMAX'), linear: has('LINEAR'), sizep1: has('SIZEP1'),
|
|
9259
|
+
compress: has('COMPRESS'), compressmax: has('COMPRESSMAX')
|
|
9260
|
+
};
|
|
9261
|
+
_sequenceFlagCache.set(flags, { size: flags.size, info });
|
|
9262
|
+
return info;
|
|
9263
|
+
}
|
|
9264
|
+
function sequenceLayout(counts, flags, radiusOf, opts = {}) {
|
|
9265
|
+
const fl = sequenceFlags(flags);
|
|
8650
9266
|
let order = counts.map((v, i) => ({ i, v }));
|
|
8651
|
-
if (
|
|
8652
|
-
if (
|
|
8653
|
-
else if (
|
|
9267
|
+
if (fl.sort) {
|
|
9268
|
+
if (fl.up) order.sort((a, b) => a.v - b.v);
|
|
9269
|
+
else if (fl.down) order.sort((a, b) => b.v - a.v);
|
|
8654
9270
|
}
|
|
8655
9271
|
const parts = [];
|
|
8656
|
-
const star =
|
|
8657
|
-
const up =
|
|
9272
|
+
const star = fl.star, horz = fl.horz;
|
|
9273
|
+
const up = fl.up;
|
|
8658
9274
|
const maxR = opts.maxRadius || 0;
|
|
8659
9275
|
const nStarParts = opts.nParts || counts.length;
|
|
8660
9276
|
let angle = 0, starR = null, x = 0;
|
|
@@ -8666,11 +9282,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8666
9282
|
if (star) {
|
|
8667
9283
|
if (starR === null) {
|
|
8668
9284
|
starR = r;
|
|
8669
|
-
if (
|
|
9285
|
+
if (fl.expand && fl.sort && up) {
|
|
8670
9286
|
const big = order[order.length - 1].v, mv = opts.maxValue || big || 1;
|
|
8671
|
-
starR = (
|
|
9287
|
+
starR = (fl.expandmax ? 2 : 0.5) * maxR * (fl.linear || fl.sizep1 ? big / mv : Math.sqrt(big / mv));
|
|
8672
9288
|
}
|
|
8673
|
-
if (
|
|
9289
|
+
if (fl.compress) starR *= fl.compressmax ? 0.25 : 0.5;
|
|
8674
9290
|
}
|
|
8675
9291
|
if (parts.length > 0 || up) {
|
|
8676
9292
|
const c = starR + r;
|
|
@@ -8682,7 +9298,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8682
9298
|
dist = starR * (1 + nStarParts / 5);
|
|
8683
9299
|
}
|
|
8684
9300
|
if (opts.rangeScale) dist *= opts.rangeScale;
|
|
8685
|
-
else dist =
|
|
9301
|
+
else dist = fl.expandmax ? dist * 2 : (fl.expand ? dist * 1.5 : dist);
|
|
8686
9302
|
angle += f;
|
|
8687
9303
|
px = Math.cos(angle / 180 * Math.PI) * dist;
|
|
8688
9304
|
py = Math.sin(angle / 180 * Math.PI) * dist;
|
|
@@ -8886,6 +9502,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8886
9502
|
// which every other caller here still relies on unchanged.
|
|
8887
9503
|
function resolvePartsClass(value, partsA) {
|
|
8888
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;
|
|
8889
9508
|
for (let i = 0; i < partsA.length; i++) {
|
|
8890
9509
|
const isLast = i === partsA.length - 1;
|
|
8891
9510
|
const inLower = value >= partsA[i].min;
|
|
@@ -9234,6 +9853,27 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9234
9853
|
}
|
|
9235
9854
|
return bins;
|
|
9236
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
|
+
|
|
9237
9877
|
function flatRangeParts(values, nMin, nMax, nParts, flags, centerValue) {
|
|
9238
9878
|
const rawMin = nMin, rawMax = nMax;
|
|
9239
9879
|
if (centerValue != null && !isNaN(centerValue)) {
|
|
@@ -9357,6 +9997,33 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9357
9997
|
// On copies of the features' properties — the page's data is not changed.
|
|
9358
9998
|
// AGGREGATE themes: not here — flat computes field100 on the aggregated
|
|
9359
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
|
+
}
|
|
9360
10027
|
function applyField100(features, binding, flags, style) {
|
|
9361
10028
|
if (!binding || !binding.value || !Array.isArray(features)) return features;
|
|
9362
10029
|
// AGGREGATE: flat applies field100 to the aggregated sums
|
|
@@ -9366,7 +10033,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9366
10033
|
const fields = String(binding.value).split('|');
|
|
9367
10034
|
const auto100 = flags.has('AUTO100') && !flags.has('AGGREGATE');
|
|
9368
10035
|
const seriesDifference = flags.has('DIFFERENCE') && fields.length > 1;
|
|
9369
|
-
|
|
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;
|
|
9370
10040
|
const f100A = f100 ? String(f100).split('|') : null;
|
|
9371
10041
|
const fractionScale = styleNum(style && style.fractionscale) || 1;
|
|
9372
10042
|
const names = fields.map(field => (field[0] === '!' ? field.slice(1) : field));
|
|
@@ -9375,6 +10045,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9375
10045
|
if (!f || !f.properties) return f;
|
|
9376
10046
|
const props = Object.assign({}, f.properties);
|
|
9377
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)));
|
|
9378
10052
|
if (f100A || auto100) {
|
|
9379
10053
|
const sum = auto100 ? values.reduce((a, v) => a + (isNaN(v) ? 0 : v), 0) : 0;
|
|
9380
10054
|
values = values.map((v, i) => {
|
|
@@ -9452,11 +10126,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9452
10126
|
out._valueMax = nMax;
|
|
9453
10127
|
const sorted = values.slice().sort((a, b) => a - b);
|
|
9454
10128
|
out._valueMedian = sorted[Math.floor((sorted.length - 1) / 2)];
|
|
9455
|
-
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;
|
|
9456
10132
|
const placeholders = new Array(nParts).fill('');
|
|
9457
|
-
const colorsRgb = resolveClassColors(style.colorscheme, placeholders, style.classes);
|
|
10133
|
+
const colorsRgb = resolveClassColors(style.colorscheme, placeholders, breaks ? nParts : style.classes);
|
|
9458
10134
|
|
|
9459
|
-
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));
|
|
9460
10136
|
out._nRangeParts = nParts; // the configured count; partsA may have fewer (all values equal)
|
|
9461
10137
|
|
|
9462
10138
|
out.categoryLabels = out.partsA.map(p => `${formatValue(p.min)} - ${formatValue(p.max)}`);
|
|
@@ -9594,6 +10270,15 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9594
10270
|
// (maptheme.js 11086-11101): each value becomes the next minus it
|
|
9595
10271
|
// (RELATIVE: in % of it when it is above field100min, else 0), the last
|
|
9596
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
|
+
|
|
9597
10282
|
function cellAggregatedValues(cell, flags, style) {
|
|
9598
10283
|
const useMean = flags.has('MEAN');
|
|
9599
10284
|
const values = cell.sums.map((s, i) => (useMean && cell.counts[i] > 0 ? s / cell.counts[i] : s));
|
|
@@ -9659,6 +10344,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9659
10344
|
// relocates a field aggregation, maptheme.js 11141) — instead of a grid
|
|
9660
10345
|
function aggregateOnGrid(features, zoom, cellPx, flags, post, field) {
|
|
9661
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');
|
|
9662
10354
|
const cells = new Map();
|
|
9663
10355
|
for (const f of features) {
|
|
9664
10356
|
const [lng, lat] = f.geometry.coordinates;
|
|
@@ -9680,16 +10372,30 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9680
10372
|
(cell.raws = cell.raws || []).push(f.properties.raw);
|
|
9681
10373
|
}
|
|
9682
10374
|
cell.sumX += p.x; cell.sumY += p.y; cell.n++;
|
|
9683
|
-
cell.
|
|
9684
|
-
|
|
9685
|
-
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);
|
|
9686
10379
|
// an AGGREGATE / CATEGORICAL pie's record (flatPieRecord): its parts
|
|
9687
10380
|
// and size added to the cell's item as flat does (flatPieAccumulate)
|
|
9688
10381
|
if (f.properties.pie) cell.pie = flatPieAccumulate(cell.pie || null, f.properties.pie, flags);
|
|
9689
10382
|
const series = f.properties.series;
|
|
9690
10383
|
if (series) {
|
|
9691
10384
|
if (!cell.series) cell.series = new Array(series.length).fill(0);
|
|
9692
|
-
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
|
+
}
|
|
9693
10399
|
}
|
|
9694
10400
|
}
|
|
9695
10401
|
const out = [];
|
|
@@ -9700,14 +10406,15 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9700
10406
|
const geometry = { type: 'Point', coordinates: [ll.lng, ll.lat] };
|
|
9701
10407
|
const hasClass = cell.first.properties.classValue !== undefined;
|
|
9702
10408
|
let props = cell.n > 1
|
|
9703
|
-
// titleRaw: the cell's
|
|
9704
|
-
//
|
|
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)
|
|
9705
10411
|
? { cluster: true, point_count: cell.n, value: cell.value, ...(hasClass ? { classValue: cell.classValue } : {}),
|
|
9706
|
-
titleRaw: cell.first.properties.raw || cell.first.properties,
|
|
10412
|
+
titleRaw: (cell.titleFrom || cell.first).properties.raw || (cell.titleFrom || cell.first).properties,
|
|
9707
10413
|
...(cell.raws && cell.raws.length ? { raws: cell.raws } : {}) }
|
|
9708
10414
|
: cell.first.properties;
|
|
9709
10415
|
if (cell.series && cell.n > 1) props = Object.assign({}, props, { series: cell.series });
|
|
9710
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 });
|
|
9711
10418
|
// a field aggregation's cell key (the field value): the categories of
|
|
9712
10419
|
// one cell are merged by it (groupCoLocated), not by their positions
|
|
9713
10420
|
if (field) props = Object.assign({}, props, { aggKey: cell.key });
|
|
@@ -9771,7 +10478,18 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9771
10478
|
let cell = cells.get(key);
|
|
9772
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); }
|
|
9773
10480
|
cell.sumX += p.x; cell.sumY += p.y; cell.n++;
|
|
9774
|
-
|
|
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);
|
|
9775
10493
|
// the members' own records, for the tooltip's {{theme.item.data}} —
|
|
9776
10494
|
// flat's __add_data tables them (numbered, up to 50) for aggregated
|
|
9777
10495
|
// items too; references only, so no copy is made. A member can itself
|
|
@@ -9786,6 +10504,18 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9786
10504
|
}
|
|
9787
10505
|
cell.counts[f.properties.cat] += f.properties.value;
|
|
9788
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
|
+
}
|
|
9789
10519
|
// point_count is the aggregated-record count of a multi-record
|
|
9790
10520
|
// cell (aggregateOnGrid; absent on a single-record cell)
|
|
9791
10521
|
// — tracked separately from counts (the summed bound VALUE) purely
|
|
@@ -9799,7 +10529,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9799
10529
|
geometry: { type: 'Point', coordinates: [ll.lng, ll.lat] },
|
|
9800
10530
|
properties: { counts: cell.counts, total: cell.counts.reduce((a, c) => a + c, 0), recordCounts: cell.recordCounts, ...(cell.classTotal !== undefined ? { classTotal: cell.classTotal } : {}),
|
|
9801
10531
|
...(cell.raws && cell.raws.length ? { raws: cell.raws } : {}),
|
|
9802
|
-
...(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 } : {}) }
|
|
9803
10535
|
};
|
|
9804
10536
|
});
|
|
9805
10537
|
}
|
|
@@ -9829,8 +10561,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9829
10561
|
const c = f.geometry && f.geometry.coordinates;
|
|
9830
10562
|
if (!c || typeof c[0] !== 'number') return;
|
|
9831
10563
|
const [lng, lat] = c;
|
|
9832
|
-
if (lng
|
|
9833
|
-
if (globeCenter && !isOnVisibleHemisphere(lng, lat, globeCenter)) return;
|
|
10564
|
+
if (!inViewport(lng, lat, bbox, globeCenter)) return;
|
|
9834
10565
|
const v = parseFloat(f.properties[rt.binding.value]);
|
|
9835
10566
|
add(rt._resolvePartsClass(v), v);
|
|
9836
10567
|
});
|
|
@@ -10204,6 +10935,33 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10204
10935
|
// pure: resolveComposedColor → see "Classification — pure functions"
|
|
10205
10936
|
_resolveComposedColor(props) { return resolveComposedColor(this, props); }
|
|
10206
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
|
+
|
|
10207
10965
|
// pure: buildPartsA → see "Classification — pure functions"
|
|
10208
10966
|
_buildPartsA() { const r = computeRangeClasses(this.features, this.binding, this.style, this.flags, v => this._formatTooltipValue(v)); if (r) Object.assign(this, r); }
|
|
10209
10967
|
|
|
@@ -10272,11 +11030,18 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10272
11030
|
// 7-class color once it knows each cell's actual aggregated total.
|
|
10273
11031
|
if (this._multiFields && this._isMultiFieldSequence()) {
|
|
10274
11032
|
// one bucket; each record carries its field values as the chart's
|
|
10275
|
-
// parts
|
|
10276
|
-
//
|
|
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;
|
|
10277
11040
|
this._featuresByCategory = [sourceFeatures.map(f => {
|
|
10278
11041
|
const parts = this._multiFields.map(k => parseFloat(f.properties[k]) || 0);
|
|
10279
|
-
|
|
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 } };
|
|
10280
11045
|
})];
|
|
10281
11046
|
this._clusterIndices = null;
|
|
10282
11047
|
this._clusterRadiusPx = null;
|
|
@@ -10723,7 +11488,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10723
11488
|
const post = this._seriesDifference
|
|
10724
11489
|
? (v, v100, series) => (series ? cellAggregatedValues({ sums: series, counts: series.map(() => 1) }, this.flags, this.style)[0] : v)
|
|
10725
11490
|
: this._aggregateField100 ? (v, v100) => aggregateField100(v, v100, this.flags, this.style) : undefined;
|
|
10726
|
-
|
|
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));
|
|
10727
11497
|
}
|
|
10728
11498
|
|
|
10729
11499
|
// Reverted from the source-accurate two-flat-circle GLOW (radius*6
|
|
@@ -10891,6 +11661,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10891
11661
|
// reasonable rather than wrong.
|
|
10892
11662
|
// borderOpacity (default 1): the border's own alpha (flat's
|
|
10893
11663
|
// stroke-opacity, see flatBubbleLook)
|
|
11664
|
+
// the color of class i; neutral grey when the theme has no class colors
|
|
11665
|
+
// (colorscheme ["none"], or a class beyond the scheme)
|
|
11666
|
+
classRgb(i) { return (this.categoryColorsRgb || [])[i] || [128, 128, 128]; }
|
|
11667
|
+
|
|
10894
11668
|
// colorscheme ["none"] — flat's explicit no-fill symbol
|
|
10895
11669
|
_noFillScheme() {
|
|
10896
11670
|
const cs = this.style.colorscheme;
|
|
@@ -10970,6 +11744,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10970
11744
|
const raw = props.raw || props.titleRaw;
|
|
10971
11745
|
const title = this.binding.title && raw && raw[this.binding.title] != null ? String(raw[this.binding.title]) : '';
|
|
10972
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;
|
|
10973
11751
|
if (aggregated && this._clusterRadiusPx && !this._clusterField) {
|
|
10974
11752
|
const n = groupRecordCount(props);
|
|
10975
11753
|
if (!title || n > 3) return '(' + n + ')';
|
|
@@ -11139,7 +11917,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11139
11917
|
let rows = fields.map((field, i) => ({ i, v: parseFloat(raw[field]) })).filter(r => Number.isFinite(r.v));
|
|
11140
11918
|
if (!rows.length) return '';
|
|
11141
11919
|
if (this.flags.has('SORT')) rows.sort((a, b) => (this.flags.has('UP') ? a.v - b.v : b.v - a.v));
|
|
11142
|
-
const color = i => { const c =
|
|
11920
|
+
const color = i => { const c = this.classRgb(i); return `rgb(${c.slice(0, 3).join(',')})`; };
|
|
11143
11921
|
const frac = v => (this._multiFieldMax ? Math.max(0, Math.abs(v) / this._multiFieldMax) : 0);
|
|
11144
11922
|
const esc = t => String(t).replace(/&/g, '&').replace(/</g, '<');
|
|
11145
11923
|
if (this.flags.has('HORZ')) {
|
|
@@ -11329,6 +12107,15 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11329
12107
|
// unmatched polygon, so the donor's fill serves no purpose once
|
|
11330
12108
|
// superseded.
|
|
11331
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 [];
|
|
11332
12119
|
if (this.flags.has('FEATURE') || this.flags.has('FEATURES')) {
|
|
11333
12120
|
return featuresHiddenByScale(this.style, liveZoom) ? [] : this._buildFeaturesLayers(liveZoom, bbox);
|
|
11334
12121
|
}
|
|
@@ -11370,6 +12157,20 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11370
12157
|
// each shape by its class), not one flat fill
|
|
11371
12158
|
const byCategory = filled && this.flags.has('CATEGORICAL') && this.categoryIndexByLabel && this.binding.value
|
|
11372
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
|
+
};
|
|
11373
12174
|
const catAlpha = Math.round(255 * (styleNum(this.style.fillopacity) || 1));
|
|
11374
12175
|
const nodataRgb = parseCssColor(this.style.nodatacolor) || [255, 255, 255];
|
|
11375
12176
|
const shadow = filled && flatShadowOn(this.style, this.features.length, zoom)
|
|
@@ -11389,14 +12190,19 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11389
12190
|
// flat puts fillopacity into the shape's fill-opacity only
|
|
11390
12191
|
// (maptheme.js 13407, 14071-14078): the outline stays opaque
|
|
11391
12192
|
// (a fill color's own alpha — rgba(…, 0.5) — times fillopacity)
|
|
11392
|
-
...(byCategory ? { updateTriggers: { getFillColor: JSON.stringify(this.categoryColorsRgb) } } : {}),
|
|
12193
|
+
...(byCategory || byClass ? { updateTriggers: { getFillColor: JSON.stringify(this.categoryColorsRgb), getLineColor: JSON.stringify(this.categoryColorsRgb) } } : {}),
|
|
11393
12194
|
getFillColor: byCategory
|
|
11394
12195
|
? f => {
|
|
11395
12196
|
const cat = this._resolveClassIndex(f.properties[this.binding.value]);
|
|
11396
12197
|
return [...(cat != null ? (this.categoryColorsRgb || [])[cat] || nodataRgb : nodataRgb).slice(0, 3), catAlpha];
|
|
11397
12198
|
}
|
|
12199
|
+
: byClass ? f => [...classColorOf(f).slice(0, 3), catAlpha]
|
|
11398
12200
|
: filled ? [...hexOrNamedToRgb(raw).slice(0, 3), Math.round(255 * cssColorAlpha(raw) * (styleNum(this.style.fillopacity) || 1))] : [0, 0, 0, 0],
|
|
11399
|
-
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
|
|
11400
12206
|
? withAlpha(hexOrNamedToRgb(styleLineColor(this.style.linecolor)), cssColorAlpha(styleLineColor(this.style.linecolor)))
|
|
11401
12207
|
: [130, 130, 130],
|
|
11402
12208
|
lineWidthMinPixels: styleNum(this.style.linewidth) || 1,
|
|
@@ -11486,12 +12292,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11486
12292
|
const isComposeColor = this.flags.has('COMPOSECOLOR');
|
|
11487
12293
|
const baseOpacity = resolveFillOpacity(this.style, this.mapOptions, zoom);
|
|
11488
12294
|
// DOPACITY needs a PER-FEATURE opacity, which deck.gl only supports
|
|
11489
|
-
// via
|
|
11490
|
-
// prop is one number for every feature) — see
|
|
11491
|
-
//
|
|
11492
|
-
//
|
|
11493
|
-
//
|
|
11494
|
-
|
|
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 =
|
|
11495
12302
|
(this.flags.has('DOPACITY') || this.flags.has('DOPACITYMIN') || this.flags.has('DOPACITYMAX') || this.flags.has('DOPACITYMINMAX'));
|
|
11496
12303
|
// AGGREGATE CATEGORICAL: every polygon's parts and dominant class
|
|
11497
12304
|
// (aggregatedCategoricalClass), the biggest part per class over all
|
|
@@ -11534,7 +12341,15 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11534
12341
|
return {
|
|
11535
12342
|
type: 'Feature',
|
|
11536
12343
|
geometry: f.geometry,
|
|
11537
|
-
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
|
+
}
|
|
11538
12353
|
};
|
|
11539
12354
|
}
|
|
11540
12355
|
if (isDominant) {
|
|
@@ -11590,6 +12405,29 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11590
12405
|
// deck.gl re-runs the color accessors only for new data or a changed trigger
|
|
11591
12406
|
const colorKey = JSON.stringify([[...this._markedClasses], !!this._onMarksChanged, evidenceMode,
|
|
11592
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;
|
|
11593
12431
|
return [new GeoJsonLayer({
|
|
11594
12432
|
id: this._dataLayerId(`ix-choropleth-${this.name}`),
|
|
11595
12433
|
data: collection,
|
|
@@ -11597,21 +12435,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11597
12435
|
pickable: true,
|
|
11598
12436
|
stroked: true,
|
|
11599
12437
|
filled: true,
|
|
11600
|
-
getFillColor: d =>
|
|
11601
|
-
|
|
11602
|
-
|
|
11603
|
-
|
|
11604
|
-
|
|
11605
|
-
|
|
11606
|
-
if (evidence === 'gray') return [...grayOf(rgb), 90];
|
|
11607
|
-
const a = alphaOfClass(d.properties.cat);
|
|
11608
|
-
return d.properties.dopacityAlpha != null ? [...rgb.slice(0, 3), Math.round(d.properties.dopacityAlpha * a * 255)] : withAlpha(rgb, a);
|
|
11609
|
-
},
|
|
11610
|
-
// a per-polygon accessor only while a class is isolated
|
|
11611
|
-
getLineColor: this._markedClasses.size && !this._onMarksChanged && evidenceMode === 'isolate'
|
|
11612
|
-
? d => (unmarkedEvidence(d) === 'hide' ? [0, 0, 0, 0] : lineRgb)
|
|
11613
|
-
: lineRgb,
|
|
11614
|
-
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,
|
|
11615
12444
|
// per-feature alpha (baked above) already carries the resolved
|
|
11616
12445
|
// base opacity — a layer-level opacity on TOP of that would
|
|
11617
12446
|
// multiply it a second time.
|
|
@@ -11648,6 +12477,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11648
12477
|
const categories = this._plotCategories();
|
|
11649
12478
|
|
|
11650
12479
|
const cells = new Map();
|
|
12480
|
+
const combine = aggCombiner(this.flags);
|
|
11651
12481
|
source.forEach(f => {
|
|
11652
12482
|
const ci = this._gridCategoryIndex(f.properties[this.binding.value]);
|
|
11653
12483
|
if (ci == null) return;
|
|
@@ -11665,7 +12495,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11665
12495
|
cells.set(key, cell);
|
|
11666
12496
|
}
|
|
11667
12497
|
const v = this._resolveAggregateValue(f.properties);
|
|
11668
|
-
cell.sums[ci]
|
|
12498
|
+
cell.sums[ci] = combine(cell.sums[ci], v, cell.counts[ci] === 0);
|
|
11669
12499
|
cell.counts[ci]++;
|
|
11670
12500
|
});
|
|
11671
12501
|
|
|
@@ -11703,8 +12533,14 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11703
12533
|
// a per-item SEQUENCE chart: CHART|SYMBOL|SEQUENCE, several value
|
|
11704
12534
|
// fields per item, one part per field (no CATEGORICAL, no AGGREGATE)
|
|
11705
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
|
|
11706
12542
|
return this.flags.has('CHART') && this.flags.has('SYMBOL') && flatFlag(this.flags, 'SEQUENCE')
|
|
11707
|
-
&& !this.flags.has('CATEGORICAL') && !this.flags.has('
|
|
12543
|
+
&& !this.flags.has('CATEGORICAL') && !this.flags.has('PLOT')
|
|
11708
12544
|
&& String(this.binding.value || '').includes('|');
|
|
11709
12545
|
}
|
|
11710
12546
|
_itemPlotFields() {
|
|
@@ -12324,7 +13160,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12324
13160
|
_userChartColor(cat) {
|
|
12325
13161
|
const cs = this.style.colorscheme;
|
|
12326
13162
|
if (Array.isArray(cs) && cs.length === (this.categoryColorsRgb || []).length && !isGeneratedColorScheme(cs)) return String(cs[cat]);
|
|
12327
|
-
const c =
|
|
13163
|
+
const c = this.classRgb(cat);
|
|
12328
13164
|
return `rgb(${c[0]},${c[1]},${c[2]})`;
|
|
12329
13165
|
}
|
|
12330
13166
|
// USER charts (see resolveUserChartFunction): one icon per chart, drawn
|
|
@@ -12515,8 +13351,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12515
13351
|
ctx.arcTo(x, y + hh, x, y, rr); ctx.arcTo(x, y, x + ww, y, rr); ctx.closePath();
|
|
12516
13352
|
};
|
|
12517
13353
|
path();
|
|
12518
|
-
ctx.globalAlpha = look.fillOpacity; ctx.fillStyle = look.fill; ctx.fill();
|
|
12519
|
-
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(); }
|
|
12520
13356
|
// getSize is the icon's height: the padding is part of it
|
|
12521
13357
|
const icon = { url: canvas.toDataURL(), width: canvas.width, height: canvas.height, anchorX: canvas.width / 2, anchorY: canvas.height / 2, id: key };
|
|
12522
13358
|
return this._cacheIcon(key, icon);
|
|
@@ -12628,7 +13464,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12628
13464
|
this._featuresByCategory.forEach((feats, cat) => {
|
|
12629
13465
|
feats.forEach(f => {
|
|
12630
13466
|
const [lng, lat] = f.geometry.coordinates;
|
|
12631
|
-
if (lng
|
|
13467
|
+
if (!inViewport(lng, lat, bbox)) return;
|
|
12632
13468
|
individual.push({ geometry: f.geometry, properties: { ...f.properties, cat } });
|
|
12633
13469
|
});
|
|
12634
13470
|
});
|
|
@@ -12767,7 +13603,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12767
13603
|
const rangeSource = this._classSeparate && this.binding.size && !normalize ? rawSizeTotals : totals;
|
|
12768
13604
|
let nMin = Infinity, nMax = -Infinity;
|
|
12769
13605
|
for (const v of rangeSource) { if (v < nMin) nMin = v; if (v > nMax) nMax = v; }
|
|
12770
|
-
|
|
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));
|
|
12771
13609
|
}
|
|
12772
13610
|
|
|
12773
13611
|
// The theme's own classes follow the aggregated items, as flat's do
|
|
@@ -12981,7 +13819,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12981
13819
|
const singleBorderColorRgb = this.style.linecolor && styleLineColor(this.style.linecolor) !== 'none' ? hexOrNamedToRgb(styleLineColor(this.style.linecolor)) : null;
|
|
12982
13820
|
const singleBorderWidthPx = styleNum(this.style.linewidth) || 0;
|
|
12983
13821
|
|
|
12984
|
-
const itemRgb = d => this.
|
|
13822
|
+
const itemRgb = d => this.classRgb(d.properties.counts ? dominant(d.properties.counts) : d.properties.cat);
|
|
12985
13823
|
const radiusOf = d => vRadius(sizeValueOf(d));
|
|
12986
13824
|
// align / offsetx / offsety: the whole chart moves (flatChartAlignOffset)
|
|
12987
13825
|
// MULTIQUAD / MULTISQUARE: the items at one position side by side
|
|
@@ -13119,7 +13957,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13119
13957
|
const titleFontPx = titleFontOf(w);
|
|
13120
13958
|
let title = null;
|
|
13121
13959
|
if (titleShown) {
|
|
13122
|
-
|
|
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;
|
|
13123
13964
|
if (text) {
|
|
13124
13965
|
const f = titleFontPx;
|
|
13125
13966
|
const lines = wrapTitleLines(text, f, circular ? boxR * 2 : w).split('\n');
|
|
@@ -13146,9 +13987,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13146
13987
|
return { d, r, w, boxR, rect, title };
|
|
13147
13988
|
}) : null;
|
|
13148
13989
|
if (boxShown && circular) {
|
|
13149
|
-
const
|
|
13150
|
-
const
|
|
13151
|
-
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');
|
|
13152
13994
|
// ScatterplotLayer has no pixel offset: a moved chart (align,
|
|
13153
13995
|
// offsetx/offsety) gets its circle as an icon
|
|
13154
13996
|
if (alignOf.active) layers.push(new IconLayer({
|
|
@@ -13156,7 +13998,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13156
13998
|
data: boxLayout, pickable: false,
|
|
13157
13999
|
getPosition: b => b.d.geometry.coordinates,
|
|
13158
14000
|
getIcon: b => this._buildSingleIcon(boxRgb, boxAlpha / 255, 'circle', borderRgb,
|
|
13159
|
-
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),
|
|
13160
14002
|
getSize: b => b.boxR * 2,
|
|
13161
14003
|
getPixelOffset: b => alignOf(b.d),
|
|
13162
14004
|
sizeUnits: 'pixels',
|
|
@@ -13169,9 +14011,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13169
14011
|
getPosition: b => b.d.geometry.coordinates,
|
|
13170
14012
|
getRadius: b => b.boxR,
|
|
13171
14013
|
radiusUnits: 'pixels',
|
|
13172
|
-
stroked:
|
|
14014
|
+
stroked: !borderNone,
|
|
13173
14015
|
getFillColor: [...boxRgb.slice(0, 3), boxAlpha],
|
|
13174
|
-
getLineColor: [...borderRgb.slice(0, 3), 255],
|
|
14016
|
+
getLineColor: [...(borderRgb || boxRgb).slice(0, 3), 255],
|
|
13175
14017
|
getLineWidth: unitPx / 20,
|
|
13176
14018
|
lineWidthUnits: 'pixels',
|
|
13177
14019
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
@@ -13181,7 +14023,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13181
14023
|
const boxLook = {
|
|
13182
14024
|
fill: this.style.boxcolor || 'white',
|
|
13183
14025
|
fillOpacity: boxOpacity,
|
|
13184
|
-
stroke: borderStyle === 'none' ? null : styleLineColor(this.style.bordercolor) || '#bbbbbb',
|
|
14026
|
+
stroke: borderStyle === 'none' || isNoneColor(styleLineColor(this.style.bordercolor)) ? null : styleLineColor(this.style.bordercolor) || '#bbbbbb',
|
|
13185
14027
|
strokeOpacity: /^(solid|dotted|dashed)$/.test(borderStyle) ? 1 : 0.3,
|
|
13186
14028
|
};
|
|
13187
14029
|
const borderWidth = styleNum(this.style.borderwidth) || 1;
|
|
@@ -13233,11 +14075,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13233
14075
|
}
|
|
13234
14076
|
|
|
13235
14077
|
// flat's per-part SYMBOL branch (everything but CATEGORICAL without
|
|
13236
|
-
// AGGREGATE/GROUP) draws a square of side 2r, the other
|
|
13237
|
-
// (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)
|
|
13238
14080
|
const perPartBranch = flatChartBranch(this.flags) === 'symbol'
|
|
13239
14081
|
&& !(this.flags.has('CATEGORICAL') && !this.flags.has('AGGREGATE') && !flatFlag(this.flags, 'GROUP'));
|
|
13240
|
-
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; };
|
|
13241
14083
|
// a range-classed cell is ONE class: one symbol in its class color,
|
|
13242
14084
|
// fillopacity and linecolor, as a single item (flat draws one
|
|
13243
14085
|
// symbol) — the packed multi-category icon is for CATEGORICAL cells
|
|
@@ -13270,7 +14112,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13270
14112
|
if (!symbolOutline) return singleBorderColorRgb && singleBorderWidthPx > 0 ? singleBorderColorRgb : null;
|
|
13271
14113
|
if (singleBorderColorRgb) return singleBorderColorRgb;
|
|
13272
14114
|
if (flatFlag(this.flags, 'NOLINES') || (this.style.linecolor && styleLineColor(this.style.linecolor) === 'none')) return null;
|
|
13273
|
-
const fill =
|
|
14115
|
+
const fill = this.classRgb(d.properties.cat);
|
|
13274
14116
|
if (perPartBranch) return flatChartTextRgb(fill);
|
|
13275
14117
|
return fill.slice(0, 3).every(c => c >= 255) ? [128, 128, 128] : fill;
|
|
13276
14118
|
};
|
|
@@ -13295,7 +14137,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13295
14137
|
const lookUnit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1);
|
|
13296
14138
|
// colorscheme ["none"]: flat draws the symbol unfilled (only its values / box / title)
|
|
13297
14139
|
const noFill = this._noFillScheme();
|
|
13298
|
-
const circleLook = flatLook ? d => flatBubbleLook(
|
|
14140
|
+
const circleLook = flatLook ? d => flatBubbleLook(this.classRgb(d.properties.cat), radiusPx(d), lookUnit, this.style, this.flags) : null;
|
|
13299
14141
|
const lineRgb = d => (circleLook ? circleLook(d).stroke : outlineRgb(d)) || [0, 0, 0];
|
|
13300
14142
|
layers.push(new ScatterplotLayer({
|
|
13301
14143
|
id: `ix-bubbles-${this.name}-circles-g${this._iconGeneration}`,
|
|
@@ -13303,7 +14145,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13303
14145
|
getPosition: d => d.geometry.coordinates,
|
|
13304
14146
|
getRadius: d => radiusPx(d) - (symbolOutline || circleLook ? 0 : outlinePx(d, radiusPx(d)) / 2),
|
|
13305
14147
|
radiusUnits: 'pixels',
|
|
13306
|
-
getFillColor: d => [...(
|
|
14148
|
+
getFillColor: d => [...(this.classRgb(d.properties.cat)).slice(0, 3), noFill ? 0 : Math.round(this._iconAlpha(d) * (circleLook ? circleLook(d).fillOpacity : fillOpacity))],
|
|
13307
14149
|
stroked: circleLook ? combined.some(d => circleLook(d).stroke) : combined.some(d => outlineRgb(d)),
|
|
13308
14150
|
getLineColor: d => [...lineRgb(d).slice(0, 3), Math.round(this._iconAlpha(d) * (circleLook ? circleLook(d).strokeOpacity : 1))],
|
|
13309
14151
|
getLineWidth: d => (circleLook ? (circleLook(d).stroke ? circleLook(d).width : 0) : outlinePx(d, radiusPx(d))),
|
|
@@ -13561,13 +14403,55 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13561
14403
|
_buildSequenceChartLayers(charts, zoom, liveZoom) {
|
|
13562
14404
|
const st = this.style;
|
|
13563
14405
|
const multi = !!this._multiFields && this._isMultiFieldSequence();
|
|
13564
|
-
|
|
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);
|
|
13565
14435
|
const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(st.scale) || 1);
|
|
13566
14436
|
const normalR = NORMAL_RADIUS_PX * unit;
|
|
13567
14437
|
const sizePow = resolveSizePow(st, this.flags);
|
|
13568
|
-
|
|
13569
|
-
|
|
13570
|
-
|
|
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
|
+
};
|
|
13571
14455
|
const rings = flatFlag(this.flags, 'RINGS');
|
|
13572
14456
|
const centerValues = flatFlag(this.flags, 'CENTERVALUES');
|
|
13573
14457
|
const noFill = this._partNoFill || new Set();
|
|
@@ -13575,21 +14459,103 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13575
14459
|
const marking = this._markedClasses.size && !this._onMarksChanged;
|
|
13576
14460
|
const gray = marking && String(st.evidence || 'isolate') === 'isolate_gray';
|
|
13577
14461
|
const sorted = [...this.flags].some(f => /\bSORT\b/.test(f));
|
|
14462
|
+
const star = flatFlag(this.flags, 'STAR');
|
|
13578
14463
|
const fillOpacity = styleNum(st.fillopacity) || 0.85;
|
|
13579
14464
|
const borderRgb = st.linecolor && styleLineColor(st.linecolor) !== 'none' ? hexOrNamedToRgb(styleLineColor(st.linecolor)) : null;
|
|
13580
14465
|
const lineWidth = styleNum(st.linewidth) || 1;
|
|
13581
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
|
+
};
|
|
13582
14527
|
const shapeOf = i => { const sh = symbols && (symbols[i] || symbols[0]); return normalizeSymbolShape(sh); };
|
|
13583
|
-
const colorOf = i => this.
|
|
14528
|
+
const colorOf = i => this.classRgb(i);
|
|
13584
14529
|
const parts = [], glows = [];
|
|
13585
14530
|
for (const d of charts) {
|
|
13586
|
-
|
|
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);
|
|
13587
14545
|
let layout = rings ? ringsLayout(counts, partRadius)
|
|
13588
14546
|
: sequenceLayout(counts, this.flags, partRadius, { maxRadius: normalR, maxValue: seqMax, nParts: nCats, rangeScale: styleNum(st.rangescale) || 0 });
|
|
13589
14547
|
if (!layout.length) continue;
|
|
13590
14548
|
// GLOW belongs to the first part (sorted) or to every part, and
|
|
13591
14549
|
// hides with it when its class is not marked
|
|
13592
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];
|
|
13593
14559
|
if (marking && !gray) {
|
|
13594
14560
|
layout = layout.filter(p => this._markedClasses.has(p.i)).map(p => ({ ...p, x: 0, y: 0 }));
|
|
13595
14561
|
if (!layout.length) continue;
|
|
@@ -13602,13 +14568,169 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13602
14568
|
return item;
|
|
13603
14569
|
});
|
|
13604
14570
|
// STAR: the first part (its center) is put on top at the end (23145)
|
|
13605
|
-
if (
|
|
14571
|
+
if (star && items.length > 1) items.push(items.shift());
|
|
13606
14572
|
parts.push(...items);
|
|
13607
14573
|
}
|
|
13608
14574
|
const alignOf = this._chartAlignFn(liveZoom, d => (d.chart || d)._seqLast || 0);
|
|
13609
14575
|
const offsetOf = item => { const o = alignOf(item.chart); return [o[0] + item.part.x, o[1] + item.part.y]; };
|
|
13610
14576
|
const alphaOf = item => (gray && !this._markedClasses.has(item.part.i) ? DIMMED_ICON_ALPHA : 255);
|
|
13611
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
|
+
}
|
|
13612
14734
|
if (glows.length && this.flags.has('GLOW') && !glowHiddenByScale(st, liveZoom)) {
|
|
13613
14735
|
layers.push(new IconLayer({
|
|
13614
14736
|
id: `ix-glow-${this.name}-g${this._iconGeneration}`,
|
|
@@ -13639,20 +14761,35 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13639
14761
|
data: parts, pickable: true,
|
|
13640
14762
|
getPosition: d => d.geometry.coordinates,
|
|
13641
14763
|
// the stroke, in the icon raster's pixels (the icon is drawn at 2r)
|
|
13642
|
-
getIcon: d =>
|
|
13643
|
-
|
|
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
|
+
},
|
|
13644
14773
|
getSize: d => d.part.r * 2,
|
|
13645
14774
|
getPixelOffset: offsetOf,
|
|
13646
14775
|
getColor: d => [255, 255, 255, alphaOf(d)],
|
|
13647
14776
|
sizeUnits: 'pixels', billboard: true,
|
|
13648
14777
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
13649
14778
|
}));
|
|
13650
|
-
// VALUES on each part (maptheme.js 22188-
|
|
14779
|
+
// VALUES on each part (maptheme.js 22188-22260): the part's value — with
|
|
13651
14780
|
// COUNT its record count, with valuefield on the categorical field the
|
|
13652
|
-
// class name — formatted with valuedecimals (else none) plus the unit
|
|
13653
|
-
// font min(1, 3.2 / L) · r · valuescale,
|
|
13654
|
-
// biggest value text (szMaxText), normal
|
|
13655
|
-
// 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)
|
|
13656
14793
|
// CENTERVALUES: only on the first part (class 0), its own value
|
|
13657
14794
|
if ((this.flags.has('VALUES') || centerValues) && !valuesHiddenByScale(st, zoom)) {
|
|
13658
14795
|
const valueScale = styleNum(st.valuescale) || 1;
|
|
@@ -13672,14 +14809,88 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13672
14809
|
const name = rec ? String(rec[st.valuefield]) : null;
|
|
13673
14810
|
const t = counting && d.properties.recordCounts ? d.properties.recordCounts[d.part.i] : d.part.v;
|
|
13674
14811
|
const text = name != null ? name : fmt(t);
|
|
14812
|
+
const image = !!urlSymbolOf(d.part.i);
|
|
13675
14813
|
const L = Math.max((name != null ? name.length : 0) + 1, maxLen);
|
|
13676
|
-
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;
|
|
13677
14817
|
if (fontSize > VALUES_MIN_FONT_PX) {
|
|
13678
14818
|
labels.push({ geometry: d.geometry, text, fontSize, pixelOffset: offsetOf(d), alpha: alphaOf(d),
|
|
13679
|
-
|
|
14819
|
+
image,
|
|
14820
|
+
color: image ? flatDerivateRgb(colorOf(d.part.i), 0.7)
|
|
14821
|
+
: textOverride ? hexOrNamedToRgb(textOverride) : flatChartTextRgb(colorOf(d.part.i)) });
|
|
13680
14822
|
}
|
|
13681
14823
|
});
|
|
13682
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 {
|
|
13683
14894
|
layers.push(new TextLayer({
|
|
13684
14895
|
id: `ix-cluster-values-${this.name}`,
|
|
13685
14896
|
data: labels, pickable: false,
|
|
@@ -13692,6 +14903,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13692
14903
|
characterSet: 'auto',
|
|
13693
14904
|
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
13694
14905
|
}));
|
|
14906
|
+
}
|
|
13695
14907
|
}
|
|
13696
14908
|
}
|
|
13697
14909
|
return layers;
|
|
@@ -13803,7 +15015,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13803
15015
|
return null;
|
|
13804
15016
|
};
|
|
13805
15017
|
const sliceRgb = sl => (sl.complement ? (parseCssColor(String(st.nodatacolor || '')) || [0xee, 0xee, 0xee])
|
|
13806
|
-
: ((byClass || sl.single) && classColor(sl.value)) ||
|
|
15018
|
+
: ((byClass || sl.single) && classColor(sl.value)) || this.classRgb(sl.i));
|
|
13807
15019
|
const fo = styleNum(st.fillopacity), op = styleNum(st.opacity);
|
|
13808
15020
|
// flat sets the group's fill-opacity only for a truthy value
|
|
13809
15021
|
const fillAlpha = op > 0 ? op : fo > 0 ? fo : 1;
|
|
@@ -13849,8 +15061,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13849
15061
|
const c = f.geometry && f.geometry.coordinates;
|
|
13850
15062
|
if (!c || typeof c[0] !== 'number') continue;
|
|
13851
15063
|
const [lng, lat] = c;
|
|
13852
|
-
if (lng
|
|
13853
|
-
if (globeCenter && !isOnVisibleHemisphere(lng, lat, globeCenter)) continue;
|
|
15064
|
+
if (!inViewport(lng, lat, bbox, globeCenter)) continue;
|
|
13854
15065
|
const raw = f.properties || {};
|
|
13855
15066
|
const values = fields.map(k => { const v = flatNumber(raw[k], this.flags); return isNaN(v) ? 0 : v; });
|
|
13856
15067
|
const layout = pieSliceLayout(values, t, { centerPart: st.centerpart });
|
|
@@ -13903,7 +15114,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13903
15114
|
if (center) {
|
|
13904
15115
|
const cr = ch.r * Math.sqrt(center.size / 100);
|
|
13905
15116
|
ch.centerR = cr;
|
|
13906
|
-
const rgb =
|
|
15117
|
+
const rgb = this.classRgb(center.i);
|
|
13907
15118
|
if (cr > 0) centers.push({ position: ch.c, r: cr * 0.9, off: ch.off, properties: ch.properties, chart: ch, fill: [...rgb.slice(0, 3), Math.round(255 * fillAlpha)] });
|
|
13908
15119
|
if (glowOn && cr > 0) {
|
|
13909
15120
|
glows.push({ position: ch.c, r: cr * 6, off: ch.off, fill: [...rgb.slice(0, 3), Math.round(255 * 0.05)] });
|
|
@@ -14030,7 +15241,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14030
15241
|
const cv = ch.layout.center.value;
|
|
14031
15242
|
const text = fmt(cv, dec != null ? dec : 0) + (unitText.length <= 5 ? unitText : '');
|
|
14032
15243
|
const fontSize = Math.min(ch.centerR * 0.8, ch.centerR * (3.3 / Math.max(1, text.length))) * valueScale;
|
|
14033
|
-
const rgb =
|
|
15244
|
+
const rgb = this.classRgb(ch.layout.center.i);
|
|
14034
15245
|
if (fontSize > VALUES_MIN_FONT_PX) centerTexts.push({ position: ch.c, text, fontSize, off: o, color: flatChartTextRgb(rgb), bold: true });
|
|
14035
15246
|
} else if (!ch.layout.center && has(/CENTERVALUE/) && (ch.count || (ch.size !== undefined && !isNaN(ch.size)))) {
|
|
14036
15247
|
// flat prints String(nCount || nSize): a record's item has no
|
|
@@ -14240,8 +15451,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14240
15451
|
const titleFont = 5 * (num(st.textscale) || num(st.valuescale) || 1);
|
|
14241
15452
|
const boxOpacity = isNaN(styleNum(st.boxopacity)) ? 1 : styleNum(st.boxopacity);
|
|
14242
15453
|
const borderStyle = String(st.borderstyle || '');
|
|
14243
|
-
const boxFill = [...hexOrNamedToRgb(st.boxcolor || 'white').slice(0, 3), Math.round(255 * boxOpacity)];
|
|
14244
|
-
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]
|
|
14245
15456
|
: [...hexOrNamedToRgb(styleLineColor(st.bordercolor) || '#bbbbbb').slice(0, 3), Math.round(255 * (/^(solid|dotted|dashed)$/.test(borderStyle) ? 1 : 0.3))];
|
|
14246
15457
|
const borderWidth = num(st.borderwidth) || 1;
|
|
14247
15458
|
const lc = st.linecolor != null && st.linecolor !== '' ? styleLineColor(st.linecolor) : null;
|
|
@@ -14283,7 +15494,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14283
15494
|
off: [box.title.x * unit + ch.off[0], (box.title.baseline - 0.8 * box.title.font) * unit + ch.off[1]] });
|
|
14284
15495
|
}
|
|
14285
15496
|
for (const b of L.bars) {
|
|
14286
|
-
const rgb =
|
|
15497
|
+
const rgb = this.classRgb(b.cls);
|
|
14287
15498
|
const a = Math.round(255 * b.fill.opacity * b.opacity);
|
|
14288
15499
|
const line = [...lineRgbOf(b.line, rgb).slice(0, 3), Math.round(255 * b.line.opacity * b.opacity)];
|
|
14289
15500
|
shape(ch, b, [...rgb.slice(0, 3), a], line, b.line.width, ch.properties, false);
|
|
@@ -14291,7 +15502,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14291
15502
|
for (const tx of L.texts) {
|
|
14292
15503
|
const font = tx.font * unit;
|
|
14293
15504
|
if (font <= VALUES_MIN_FONT_PX) continue;
|
|
14294
|
-
const rgb =
|
|
15505
|
+
const rgb = this.classRgb(tx.cls);
|
|
14295
15506
|
const color = tx.color === 'value' ? [0x79, 0x86, 0x97] : tx.color === 'contrast' ? flatChartTextRgb(rgb) : hexOrNamedToRgb(tx.color);
|
|
14296
15507
|
// the baseline's start → the text's middle-left (arial: 0.35 font
|
|
14297
15508
|
// above the baseline), perpendicular to its reading direction
|
|
@@ -14523,6 +15734,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14523
15734
|
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
14524
15735
|
const has = re => re.test(t);
|
|
14525
15736
|
const bezier = has(/\bBEZIER\b/);
|
|
15737
|
+
const lift = has(/\b3D\b/);
|
|
15738
|
+
// flat's orthographic map draws a flow 1.604 times as wide as its Mercator formula gives at the
|
|
15739
|
+
// same object scale (measured on the mechanical-goods globe page: the widest ribbon 8.96 px
|
|
15740
|
+
// where scale 0.31 · 1.8 · 10 units gives 5.6; its pies, at the same scale, match)
|
|
15741
|
+
const flowWidthK = flatOrthographicByOptions.has(this.mapOptions) ? FLAT_ORTHOGRAPHIC_FLOW_WIDTH : 1;
|
|
15742
|
+
const archRatio = isNaN(styleNum(st.archeight)) ? VECTOR_ARC_HEIGHT_RATIO : styleNum(st.archeight);
|
|
14526
15743
|
if (!this.binding.lookup2) {
|
|
14527
15744
|
if (!this._warnedNoPos2) {
|
|
14528
15745
|
this._warnedNoPos2 = true;
|
|
@@ -14535,11 +15752,27 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14535
15752
|
const scale = styleNum(st.scale) || 1;
|
|
14536
15753
|
const unit = oz * scale;
|
|
14537
15754
|
const world = 512 * Math.pow(2, liveZoom);
|
|
14538
|
-
const
|
|
15755
|
+
const mercToPx = ([lng, lat]) => {
|
|
14539
15756
|
const s = Math.sin(Math.max(-85.0511, Math.min(85.0511, lat)) * Math.PI / 180);
|
|
14540
15757
|
return [(lng + 180) / 360 * world, (0.5 - Math.log((1 + s) / (1 - s)) / (4 * Math.PI)) * world];
|
|
14541
15758
|
};
|
|
14542
|
-
const
|
|
15759
|
+
const mercToLngLat = ([x, y]) => [x / world * 360 - 180, (2 * Math.atan(Math.exp((0.5 - y / world) * 2 * Math.PI)) - Math.PI / 2) * 180 / Math.PI];
|
|
15760
|
+
// the curves are computed in the plane of flat's map (Equal Earth), at the screen
|
|
15761
|
+
// size of this map's degree of longitude on the equator, then mapped back: the
|
|
15762
|
+
// flow bows around the pole as on flat. Mercator's plane stays the default.
|
|
15763
|
+
// On the globe (flat's orthographic map) the plane is the one of the view center,
|
|
15764
|
+
// for the flows whose two ends face the viewer; the others keep Mercator's.
|
|
15765
|
+
const planeFns = (proj, k) => ({
|
|
15766
|
+
toPx: ([lng, lat]) => { const q = proj.project(lat, lng); return [q[0] * k, -q[1] * k]; },
|
|
15767
|
+
toLngLat: ([x, y]) => { const q = proj.invert(x / k, -y / k); return [q[1], q[0]]; }
|
|
15768
|
+
});
|
|
15769
|
+
const mercFns = { toPx: mercToPx, toLngLat: mercToLngLat };
|
|
15770
|
+
const fixedPlane = flatPlaneProjectionByOptions.get(this.mapOptions);
|
|
15771
|
+
const fixedFns = fixedPlane && fixedPlane.invert ? planeFns(fixedPlane, (world / 360) / fixedPlane.project(0, 1)[0]) : null;
|
|
15772
|
+
const planeCenter = flatViewCenterByOptions.get(this.mapOptions) || globeCenter;
|
|
15773
|
+
const ortho = !fixedFns && globeCenter ? orthographicPlane(planeCenter.lat, planeCenter.lng) : null;
|
|
15774
|
+
const orthoFns = ortho ? planeFns(ortho, world / (2 * Math.PI)) : null;
|
|
15775
|
+
const fnsOf = it => (fixedFns || (orthoFns && ortho.inFront(it.p1) && ortho.inFront(it.p2) ? orthoFns : mercFns));
|
|
14543
15776
|
const inBox = c => c && c[0] >= bbox[0] && c[0] <= bbox[2] && c[1] >= bbox[1] && c[1] <= bbox[3];
|
|
14544
15777
|
|
|
14545
15778
|
const sizePow = resolveSizePow(st, this.flags);
|
|
@@ -14569,7 +15802,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14569
15802
|
if (c) return c;
|
|
14570
15803
|
}
|
|
14571
15804
|
if (it.cat != null) return it.cat >= 0 && colors[it.cat] ? colors[it.cat] : [0, 0, 0];
|
|
14572
|
-
|
|
15805
|
+
// no value field: the item's value is 1, its color the scheme's first (flat's
|
|
15806
|
+
// colorScheme[nValuesA[0] - 1]; mechanical_goods_flows.html draws one color per series)
|
|
15807
|
+
const v = it.value === undefined ? 1 : it.value;
|
|
15808
|
+
return Number.isInteger(v) && colors[v - 1] ? colors[v - 1] : [0, 0, 0];
|
|
14573
15809
|
};
|
|
14574
15810
|
const fade = has(/\bFADEIN\b/) ? 0.2 : 2;
|
|
14575
15811
|
const gradient = bezier && has(/\bGRADIENT\b|\bFADEIN\b/);
|
|
@@ -14600,8 +15836,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14600
15836
|
|
|
14601
15837
|
const paths = [], heads = [];
|
|
14602
15838
|
const N = 32;
|
|
14603
|
-
const visible = c => !globeCenter || isOnVisibleHemisphere(c[0], c[1], globeCenter);
|
|
15839
|
+
const visible = c => Number.isFinite(c[0]) && Number.isFinite(c[1]) && (!globeCenter || isOnVisibleHemisphere(c[0], c[1], globeCenter));
|
|
14604
15840
|
for (const it of order) {
|
|
15841
|
+
const { toPx, toLngLat } = fnsOf(it);
|
|
14605
15842
|
let p1 = it.p1, p2 = it.p2;
|
|
14606
15843
|
if (!p2) continue;
|
|
14607
15844
|
if (minValue && it.nSize < minValue) continue;
|
|
@@ -14612,7 +15849,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14612
15849
|
if (p1[0] === p2[0] && p1[1] === p2[1]) continue;
|
|
14613
15850
|
const w = rawLw != null && rawLw !== '' && lwNum ? lwNum
|
|
14614
15851
|
: 10 / Math.pow(nsv || maxSize, 1 / sizePow) * Math.pow(Math.abs(it.nSize), 1 / sizePow);
|
|
14615
|
-
sw = w * unit;
|
|
15852
|
+
sw = w * unit * flowWidthK;
|
|
14616
15853
|
if (!(sw > 0)) continue;
|
|
14617
15854
|
if (has(/\bREVERSE\b/) || it.nSize < 0) [p1, p2] = [p2, p1];
|
|
14618
15855
|
const a = toPx(p1), b = toPx(p2);
|
|
@@ -14639,7 +15876,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14639
15876
|
end = [end[0] + a[0], end[1] + a[1]];
|
|
14640
15877
|
} else {
|
|
14641
15878
|
const w = (rawLw != null && rawLw !== '' && lwNum) || (2 / (nsv || 1) / maxSize * it.nSize) || 0.1;
|
|
14642
|
-
sw = w * oz;
|
|
15879
|
+
sw = w * oz * flowWidthK;
|
|
14643
15880
|
const a = toPx(p1), b = toPx(p2);
|
|
14644
15881
|
pts = [];
|
|
14645
15882
|
for (let i = 0; i <= N; i++) pts.push([a[0] + (b[0] - a[0]) * i / N, a[1] + (b[1] - a[1]) * i / N]);
|
|
@@ -14664,15 +15901,47 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14664
15901
|
vector: { itemValue: it.cat != null ? (it.cat >= 0 ? it.cat + 1 : 0) : it.value } };
|
|
14665
15902
|
// the line in map coordinates; on the globe only its visible runs
|
|
14666
15903
|
const ll = pts.map(toLngLat);
|
|
15904
|
+
// a flow that crosses the antimeridian (over the pole, or the Pacific) continues past ±180°
|
|
15905
|
+
// instead of jumping back, which would draw it the long way round the globe
|
|
15906
|
+
for (let i = 1; i < ll.length; i++) {
|
|
15907
|
+
if (!Number.isFinite(ll[i][0]) || !Number.isFinite(ll[i - 1][0])) continue;
|
|
15908
|
+
while (ll[i][0] - ll[i - 1][0] > 180) ll[i][0] -= 360;
|
|
15909
|
+
while (ll[i][0] - ll[i - 1][0] < -180) ll[i][0] += 360;
|
|
15910
|
+
}
|
|
14667
15911
|
let run = [], runColors = [];
|
|
14668
15912
|
const flush = () => {
|
|
14669
|
-
if (run.length > 1)
|
|
15913
|
+
if (run.length > 1) {
|
|
15914
|
+
const dash = dashArr ? [dashArr[0] / drawW, dashArr[1] / drawW] : [0, 0];
|
|
15915
|
+
if (globeCenter && !dashArr) {
|
|
15916
|
+
// on the globe deck.gl draws only the first three quarters of a path that has a
|
|
15917
|
+
// color per vertex (also an equal one): one color per path, a gradient as one
|
|
15918
|
+
// path per segment, its two colors averaged
|
|
15919
|
+
const same = runColors.every(c => c[0] === runColors[0][0] && c[1] === runColors[0][1] && c[2] === runColors[0][2] && c[3] === runColors[0][3]);
|
|
15920
|
+
if (same) paths.push({ path: run, colors: runColors[0], width: drawW, dash, properties, item: it });
|
|
15921
|
+
else for (let k = 0; k < run.length - 1; k++) {
|
|
15922
|
+
const c0 = runColors[k], c1 = runColors[k + 1];
|
|
15923
|
+
paths.push({ path: [run[k], run[k + 1]], colors: [0, 1, 2, 3].map(j => Math.round((c0[j] + c1[j]) / 2)), width: drawW, dash, properties, item: it });
|
|
15924
|
+
}
|
|
15925
|
+
} else paths.push({ path: run, colors: runColors, width: drawW, dash, properties, item: it });
|
|
15926
|
+
}
|
|
14670
15927
|
run = []; runColors = [];
|
|
14671
15928
|
};
|
|
14672
|
-
|
|
15929
|
+
const chordM = lift ? greatCircleMeters(p1, p2) : 0;
|
|
15930
|
+
ll.forEach((c, i) => { if (visible(c)) { run.push(lift ? [c[0], c[1], vectorArcHeight(i, ll.length - 1, chordM, archRatio)] : c); runColors.push(vertexColor(i)); } else flush(); });
|
|
14673
15931
|
flush();
|
|
14674
15932
|
if (arrow && visible(toLngLat(end))) {
|
|
14675
|
-
const
|
|
15933
|
+
const triPx = arrowMarkerTriangle(end, tan[0], tan[1], sw, headS, refX, refY);
|
|
15934
|
+
let tri = triPx.map(toLngLat);
|
|
15935
|
+
if (lift) {
|
|
15936
|
+
// the head lies in the arc's plane: a vertex behind the line's end is as
|
|
15937
|
+
// high as the arc there, the tip (past the end) on the ground
|
|
15938
|
+
const tl = Math.hypot(tan[0], tan[1]) || 1;
|
|
15939
|
+
tri = tri.map((c, k) => {
|
|
15940
|
+
const back = -((triPx[k][0] - end[0]) * tan[0] + (triPx[k][1] - end[1]) * tan[1]) / tl;
|
|
15941
|
+
return [c[0], c[1], back > 0 ? vectorArcHeight(pathBackIndex(pts, back), pts.length - 1, chordM, archRatio) : 0];
|
|
15942
|
+
});
|
|
15943
|
+
}
|
|
15944
|
+
if (tri.some(c => !Number.isFinite(c[0]) || !Number.isFinite(c[1]))) continue; // off the projection's outline
|
|
14676
15945
|
heads.push({ type: 'Feature', properties, geometry: { type: 'Polygon', coordinates: [[...tri, tri[0]]] },
|
|
14677
15946
|
fill: [...rgb.slice(0, 3), Math.round(255 * Math.min(1, headAlpha) * groupAlpha)] });
|
|
14678
15947
|
}
|
|
@@ -14898,7 +16167,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14898
16167
|
const valueScale = styleNum(this.style.valuescale) || 1;
|
|
14899
16168
|
const textOverride = this.style.valuecolor || this.style.textcolor;
|
|
14900
16169
|
const opts = this._valueTextOpts();
|
|
14901
|
-
const itemRgb = props => this.
|
|
16170
|
+
const itemRgb = props => this.classRgb(props.counts ? dominant(props.counts) : props.cat);
|
|
14902
16171
|
const valueOf = props => (props.counts ? props.total : (props.classValue !== undefined ? props.classValue : props.value));
|
|
14903
16172
|
const textOf = props => flatValueText(props.counts ? this._categoryValueRecord(dominant(props.counts)) : props.raw,
|
|
14904
16173
|
this.binding.title && props.raw ? props.raw[this.binding.title] : undefined, valueOf(props), this.style, this.flags, opts);
|
|
@@ -15068,8 +16337,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15068
16337
|
ctx.lineTo(cx + r, cy - r * 0.6);
|
|
15069
16338
|
ctx.lineTo(cx, cy + r);
|
|
15070
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':
|
|
15071
16343
|
case 'roundrect': {
|
|
15072
|
-
const s = r * 0.8;
|
|
16344
|
+
const s = shape === 'roundrectfull' ? r : r * 0.8;
|
|
15073
16345
|
if (ctx.roundRect) ctx.roundRect(cx - s, cy - s, s * 2, s * 2, r * 0.3);
|
|
15074
16346
|
else ctx.rect(cx - s, cy - s, s * 2, s * 2);
|
|
15075
16347
|
break;
|
|
@@ -15138,6 +16410,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15138
16410
|
function withAlpha(rgb, a) {
|
|
15139
16411
|
return a >= 1 ? rgb.slice(0, 3) : [...rgb.slice(0, 3), Math.round(255 * Math.max(0, a))];
|
|
15140
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
|
+
}
|
|
15141
16418
|
function hexOrNamedToRgb(v) {
|
|
15142
16419
|
const NAMED = { gray: [128, 128, 128], grey: [128, 128, 128], black: [0, 0, 0], white: [255, 255, 255] };
|
|
15143
16420
|
return parseCssColor(v) || NAMED[v] || [130, 130, 130];
|
|
@@ -15254,7 +16531,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15254
16531
|
// last built map; called while the map is still building, they run once
|
|
15255
16532
|
// it is ready.
|
|
15256
16533
|
const MAP_HANDLE_METHODS = ['replace', 'add', 'newTheme', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
15257
|
-
'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'];
|
|
15258
16535
|
const _mapHandles = Object.create(null);
|
|
15259
16536
|
function makeMapHandle(szMap) {
|
|
15260
16537
|
const h = {};
|
|
@@ -15269,6 +16546,15 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15269
16546
|
}
|
|
15270
16547
|
h.getZoom = () => { const a = api(); return a ? a.getZoom() : undefined; };
|
|
15271
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();
|
|
15272
16558
|
// flat's mapApi.theme(szTheme) (htmlgui_flat.js 1571): the theme handle
|
|
15273
16559
|
h.theme = szTheme => new themeApi(szMap, szTheme);
|
|
15274
16560
|
// flat's mapApi aliases (htmlgui_flat.js 1286+): show/hide a theme,
|
|
@@ -15837,7 +17123,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15837
17123
|
// known real page's globe toggle actually uses (see
|
|
15838
17124
|
// example-world-bubble-projection-toggle.md) — so `ixmaps.szResourceBase`
|
|
15839
17125
|
// only needs to be SOME string, not a real, working resource base URL.
|
|
15840
|
-
|
|
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/';
|
|
15841
17131
|
function getProjectString() {
|
|
15842
17132
|
if (!_lastMapApi || !_lastMapApi.map) return JSON.stringify({ map: {} });
|
|
15843
17133
|
const map = _lastMapApi.map;
|
|
@@ -15930,6 +17220,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15930
17220
|
newTheme: (title, theme, flag) => _mapHandle.newTheme(title, theme, flag),
|
|
15931
17221
|
// flat's time slider: show the records of a theme (null = all themes) whose timefield lies in [min, max)
|
|
15932
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),
|
|
15933
17226
|
setBasemapOpacity: compatSetBasemapOpacity,
|
|
15934
17227
|
// this engine's version (flat's own ixmaps.version numbers flat)
|
|
15935
17228
|
glVersion: IXMAPS_GL_VERSION,
|
|
@@ -15954,15 +17247,15 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15954
17247
|
// test-only: lets test/unit/*.test.mjs call pure internals directly (the
|
|
15955
17248
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
15956
17249
|
global.__ixmapsGlInternals = {
|
|
15957
|
-
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, bezierVectorLayout, cubicBezierPoints, arrowMarkerTriangle, fadeGradientStops, flatToArray, hashUnit, multiQuadOffsets, pixelOffsetLngLat, chartTextTransmittance, insideSymbolShape,
|
|
15958
|
-
normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
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,
|
|
17251
|
+
choroplethOutlineColor, normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
15959
17252
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
15960
17253
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|
|
15961
17254
|
computeAlphaStats, computeMultiFieldClasses, computeDominantStats, resolveDominantClass, computeComposeColorStats,
|
|
15962
17255
|
resolveComposedColor, computeRangeClasses, colorSchemeClassCount, resolveDopacityAlpha,
|
|
15963
17256
|
flatColorSweep, applyClassesToColorScheme, resolveClassColors, flatOutlierStats, parseCssColor, flatLegendLook,
|
|
15964
17257
|
flatToMapLibreZoom, mapLibreToFlatZoom, flatViewToMapLibreZoom, mapLibreToFlatViewZoom, flatSvgProjectionOf, flatSvgProjectionByOptions, normalSizeScaleOf, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
|
|
15965
|
-
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,
|
|
15966
17259
|
applyWhereFilter, joinChartPositions, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatBubbleLook, bubbleIconLook, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
15967
17260
|
drawSymbolPath, normalizeSymbolShape,
|
|
15968
17261
|
flatFormatValue, themeStyleArgs, flatChangedStyleValue, explicitValueDecimals, legendDecimals,
|