ixmaps-gl 0.2.15 → 0.2.17
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 +13 -3
- package/ixmaps-gl.js +807 -227
- package/package.json +1 -1
package/ixmaps-gl.js
CHANGED
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
// ---------------------------------------------------------------
|
|
83
83
|
// this engine's release (= package.json "version", checked by
|
|
84
84
|
// test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
|
|
85
|
-
const IXMAPS_GL_VERSION = '0.2.
|
|
85
|
+
const IXMAPS_GL_VERSION = '0.2.17';
|
|
86
86
|
|
|
87
87
|
const LIB_URLS = {
|
|
88
88
|
// Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
|
|
@@ -716,6 +716,9 @@
|
|
|
716
716
|
// another theme's data: .data({name})). See loadNamedTheme in build().
|
|
717
717
|
const _namedDataWaiters = new Map(); // name → Set of resolve(table)
|
|
718
718
|
const _namedDataListeners = new Map(); // name → Set of fn(table)
|
|
719
|
+
// names whose next publications feed only the theme that asked for them
|
|
720
|
+
// (a FEATURE's dependent broker refresh, see featureDependents) → count
|
|
721
|
+
const _quietNamedData = new Map();
|
|
719
722
|
function namedTable(name) {
|
|
720
723
|
try { const t = global[name]; return t && typeof t === 'object' && typeof t.json === 'function' ? t : null; } catch (e) { return null; }
|
|
721
724
|
}
|
|
@@ -744,6 +747,7 @@
|
|
|
744
747
|
const waiters = _namedDataWaiters.get(name);
|
|
745
748
|
if (waiters) [...waiters].forEach(resolve => resolve(table));
|
|
746
749
|
const listeners = _namedDataListeners.get(name);
|
|
750
|
+
if (_quietNamedData.get(name)) return;
|
|
747
751
|
// after the handing-over code finished (a broker's own theme first)
|
|
748
752
|
if (listeners && listeners.size && table) setTimeout(() => [...listeners].forEach(fn => fn(table)), 0);
|
|
749
753
|
}
|
|
@@ -1411,7 +1415,7 @@
|
|
|
1411
1415
|
const key = String(id), area = geometryArea(f.geometry);
|
|
1412
1416
|
if (!flatShapeWins(best.get(key), area)) continue;
|
|
1413
1417
|
const pos = flatShapeCenter(f.geometry);
|
|
1414
|
-
if (pos) best.set(key, { area, pos });
|
|
1418
|
+
if (pos) best.set(key, { area, pos, areaKm2: flatShapeAreaKm2(rt, f) });
|
|
1415
1419
|
}
|
|
1416
1420
|
}
|
|
1417
1421
|
rt._positionIndex = best;
|
|
@@ -1445,7 +1449,11 @@
|
|
|
1445
1449
|
const v = row[lookupField];
|
|
1446
1450
|
if (v == null || v === '') continue;
|
|
1447
1451
|
const hit = index.get(flatLookupKey(v, spec.style));
|
|
1448
|
-
|
|
1452
|
+
// the joined feature is a POINT — DENSITY (value per km² of the
|
|
1453
|
+
// joined shape, as flat) needs the shape's km² area carried along
|
|
1454
|
+
// (featureAreaKm2 prefers this marker), exactly as the choropleth
|
|
1455
|
+
// join stamps it
|
|
1456
|
+
if (hit) features.push({ type: 'Feature', geometry: { type: 'Point', coordinates: hit.pos }, properties: row, _flatAreaKm2: hit.areaKm2 });
|
|
1449
1457
|
}
|
|
1450
1458
|
}
|
|
1451
1459
|
return { type: 'FeatureCollection', features };
|
|
@@ -1852,12 +1860,22 @@
|
|
|
1852
1860
|
if (typeof colorscheme === 'string') {
|
|
1853
1861
|
try {
|
|
1854
1862
|
const fn = new Function('return (' + colorscheme + ')')();
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
}
|
|
1863
|
+
if (typeof fn === 'function') {
|
|
1864
|
+
const objTheme = { szLabelA: labels, colorScheme: new Array(labels.length) };
|
|
1865
|
+
fn(objTheme);
|
|
1866
|
+
return objTheme.colorScheme;
|
|
1867
|
+
}
|
|
1868
|
+
} catch (err) { /* not code — a flat color-list string, below */ }
|
|
1869
|
+
// GL-PORT COMPAT: flat's Themes.toArray (maptheme.js 999-1017) — a
|
|
1870
|
+
// string colorscheme that isn't a colorscheme FUNCTION is a COLOR
|
|
1871
|
+
// LIST: '|' separated, or ',' separated when there's no RGB() token
|
|
1872
|
+
// (kept whole). A single "#dd0088" is ONE single-color class (the
|
|
1873
|
+
// fire page's KDE isobands), not a broken function silently falling
|
|
1874
|
+
// back to the default palette.
|
|
1875
|
+
const list = (/\|/.test(colorscheme) || /rgb/i.test(colorscheme)) ? colorscheme.split('|') : colorscheme.split(',');
|
|
1876
|
+
if (list.length === 1 && list[0].trim() === 'none') return null; // no fill, the array branch's ['none']
|
|
1877
|
+
if (list.every(c => parseCssColor(c))) return labels.length ? labels.map((_, i) => list[i % list.length]) : list;
|
|
1878
|
+
console.warn('[ixmaps-gl] colorscheme "' + colorscheme + '" is neither a colorscheme function nor a color list, using the fallback palette');
|
|
1861
1879
|
}
|
|
1862
1880
|
return labels.map((_, i) => FALLBACK_PALETTE[i % FALLBACK_PALETTE.length]);
|
|
1863
1881
|
}
|
|
@@ -1910,6 +1928,11 @@
|
|
|
1910
1928
|
// _prepareDominant) doing its own `.map(hexToRgb)` that would break on
|
|
1911
1929
|
// the diverging sweep's own output.
|
|
1912
1930
|
function resolveClassColors(colorscheme, labels, classes) {
|
|
1931
|
+
// flat's colorscheme ["none"] is an explicit NO-FILL (a chart of
|
|
1932
|
+
// unfilled symbols, only its values/boxes/titles drawn — the fire
|
|
1933
|
+
// areas page's SIF chart): NO class colors, never the fallback
|
|
1934
|
+
// palette, which would paint symbol circles flat never draws
|
|
1935
|
+
if (Array.isArray(colorscheme) && colorscheme.length === 1 && colorscheme[0] === 'none') return null;
|
|
1913
1936
|
const resolved = resolveColorScheme(colorscheme, labels, classes);
|
|
1914
1937
|
return (resolved || labels.map((_, i) => FALLBACK_PALETTE[i % FALLBACK_PALETTE.length]))
|
|
1915
1938
|
.map(c => Array.isArray(c) ? c : (parseCssColor(c) || [130, 130, 130]));
|
|
@@ -2010,16 +2033,57 @@
|
|
|
2010
2033
|
// the user chart's size unit: flat's normalX(15) = 300 is the chart's
|
|
2011
2034
|
// normal radius (maxSize), shown at gl's normal bubble radius
|
|
2012
2035
|
const USER_CHART_MAX_SIZE = 300;
|
|
2013
|
-
//
|
|
2014
|
-
|
|
2036
|
+
// ---- the pane USER charts render into: one per map, live over the canvas ----
|
|
2037
|
+
// flat draws its user charts as LIVE SVG over the map — vector-crisp at
|
|
2038
|
+
// every zoom, labels as real text. The first port here rasterized every
|
|
2039
|
+
// chart into one texture atlas instead, and a page with thousands of
|
|
2040
|
+
// charts got ~50 px per chart (the atlas area is fixed) — icons scaled
|
|
2041
|
+
// up several times over: blur, mushy gradients, unreadable labels. In
|
|
2042
|
+
// this pane each chart is a <g> mounted once; a map 'move' only updates
|
|
2043
|
+
// its transform (translate + scale), which the browser composites —
|
|
2044
|
+
// the same thing flat's own engine does over Leaflet, at the same item
|
|
2045
|
+
// counts. Keyed by the map's options object, so runtimes defined later
|
|
2046
|
+
// (loadProject, post-build map.layer) find it too.
|
|
2047
|
+
const _chartHosts = new WeakMap();
|
|
2048
|
+
function chartHostFor(mapOptions, map, el) {
|
|
2049
|
+
let host = _chartHosts.get(mapOptions);
|
|
2050
|
+
if (host) return host;
|
|
2051
|
+
const pane = document.createElementNS(SVG_NS, 'svg');
|
|
2052
|
+
pane.setAttribute('class', 'ix-user-charts');
|
|
2053
|
+
pane.style.cssText = 'position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;overflow:visible;z-index:3;';
|
|
2054
|
+
const defs = document.createElementNS(SVG_NS, 'defs');
|
|
2055
|
+
pane.appendChild(defs);
|
|
2056
|
+
el.appendChild(pane);
|
|
2057
|
+
host = { pane, defs, map, el, runtimes: new Set(), raf: 0 };
|
|
2058
|
+
host.requestUpdate = () => {
|
|
2059
|
+
if (host.raf) return;
|
|
2060
|
+
host.raf = global.requestAnimationFrame(() => { host.raf = 0; host.update(); });
|
|
2061
|
+
};
|
|
2062
|
+
// transform-only per move: positions follow the map live; the charts'
|
|
2063
|
+
// own markup is redrawn on the settled-refresh cadence (freezeOnPan)
|
|
2064
|
+
host.update = () => {
|
|
2065
|
+
const w = el.clientWidth, h = el.clientHeight, margin = 256;
|
|
2066
|
+
for (const rt of host.runtimes) {
|
|
2067
|
+
const items = rt._chartItems;
|
|
2068
|
+
if (!items) continue;
|
|
2069
|
+
for (const it of items) {
|
|
2070
|
+
const p = map.project(it.coords);
|
|
2071
|
+
const off = p.x < -margin || p.y < -margin || p.x > w + margin || p.y > h + margin;
|
|
2072
|
+
if (off) { if (it.el.style.display !== 'none') it.el.style.display = 'none'; continue; }
|
|
2073
|
+
if (it.el.style.display === 'none') it.el.style.display = '';
|
|
2074
|
+
it.el.setAttribute('transform', `translate(${p.x} ${p.y}) scale(${it.s}) translate(${-it.ox} ${-it.oy})`);
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
map.on('move', host.requestUpdate);
|
|
2079
|
+
map.on('resize', host.requestUpdate);
|
|
2080
|
+
_chartHosts.set(mapOptions, host);
|
|
2081
|
+
return host;
|
|
2082
|
+
}
|
|
2015
2083
|
// ?ixgl-debug in the page URL: ixmaps-gl logs what it draws (console.info)
|
|
2016
2084
|
function glDebug() {
|
|
2017
2085
|
try { return new URLSearchParams(global.location.search).has('ixgl-debug'); } catch (e) { return false; }
|
|
2018
2086
|
}
|
|
2019
|
-
// the pixel area all of a layer's chart images may take together: deck.gl
|
|
2020
|
-
// packs them into one texture (1024 px wide), which must stay well below
|
|
2021
|
-
// the GPU's texture size limit — see _userChartIcon
|
|
2022
|
-
const USER_CHART_ATLAS_AREA = 1024 * 8192;
|
|
2023
2087
|
class LayerBuilder {
|
|
2024
2088
|
constructor(name) {
|
|
2025
2089
|
this.name = name;
|
|
@@ -2114,11 +2178,24 @@
|
|
|
2114
2178
|
define() { return this; }
|
|
2115
2179
|
}
|
|
2116
2180
|
|
|
2181
|
+
// flat-era pages keep the layer construct and CALL it like a function
|
|
2182
|
+
// when handing it back to the map later (censimenti's sidebar:
|
|
2183
|
+
// map().add(__theme(),'clear')) — the call returns the builder; pages also
|
|
2184
|
+
// chain builder methods right on the factory result, so the construct
|
|
2185
|
+
// delegates every one of them (chaining stays on the construct). Every
|
|
2186
|
+
// consumption point unwraps __ixLayer, so the object form keeps working.
|
|
2117
2187
|
function layer(name, configFn) {
|
|
2118
2188
|
const builder = new LayerBuilder(name);
|
|
2119
2189
|
if (configFn) configFn(builder);
|
|
2120
|
-
return builder;
|
|
2190
|
+
const construct = function () { return builder; };
|
|
2191
|
+
construct.__ixLayer = builder;
|
|
2192
|
+
for (const k of Object.getOwnPropertyNames(LayerBuilder.prototype)) {
|
|
2193
|
+
if (k === 'constructor') continue;
|
|
2194
|
+
construct[k] = function (...args) { const r = builder[k](...args); return r === builder ? construct : r; };
|
|
2195
|
+
}
|
|
2196
|
+
return construct;
|
|
2121
2197
|
}
|
|
2198
|
+
const unwrapLayer = x => (typeof x === 'function' && x.__ixLayer) ? x.__ixLayer : x;
|
|
2122
2199
|
|
|
2123
2200
|
// ---------------------------------------------------------------
|
|
2124
2201
|
// Binding aliases. Real ixmaps-flat accepts ~50 .binding() keys and maps
|
|
@@ -2373,7 +2450,10 @@
|
|
|
2373
2450
|
// (flat stores most of these as given and lets arithmetic coerce them;
|
|
2374
2451
|
// its runtime changeThemeStyle applies Number() — the same result for
|
|
2375
2452
|
// every clean value.) A linewidth list is typed element by element.
|
|
2376
|
-
|
|
2453
|
+
// Not valuedecimals: flat's `nValueDecimals || default` depends on the raw
|
|
2454
|
+
// type (a string "0" counts, a number 0 doesn't) — read it through
|
|
2455
|
+
// explicitValueDecimals / legendDecimals.
|
|
2456
|
+
const STYLE_NUMBER_KEYS = ['linewidth', 'fillopacity', 'scale', 'classes', 'normalsizevalue',
|
|
2377
2457
|
'sizepow', 'rangescale', 'minvalue', 'maxvalue', 'markersize', 'boxopacity', 'outlierscale', 'valuescale',
|
|
2378
2458
|
'brightness', 'fractionscale', 'dopacityscale', 'dopacitypow', 'gridwidthpx', 'textscale', 'rangecentervalue',
|
|
2379
2459
|
'shadowblur', 'shadowdx', 'shadowdy', 'maxshadow', 'offsetx', 'offsety', 'gridx', 'boxmargin', 'borderwidth',
|
|
@@ -2531,6 +2611,16 @@
|
|
|
2531
2611
|
return String(text).split(' ').map(w => (dict["'" + w + "'"] !== undefined ? String(dict["'" + w + "'"]) : w)).join(' ');
|
|
2532
2612
|
};
|
|
2533
2613
|
}
|
|
2614
|
+
// make the map container the positioning context of what the engine
|
|
2615
|
+
// mounts inside it (splash, pinned tooltip, legends) — only when it is
|
|
2616
|
+
// unpositioned. The RENDERED position decides, never el.style (inline
|
|
2617
|
+
// only): a page's #map_div { position:absolute; top:0; bottom:0 } from a
|
|
2618
|
+
// stylesheet reads as "" there, and overwriting it with relative drops
|
|
2619
|
+
// the top/bottom stretch — the div's height collapses to 0, a blank map
|
|
2620
|
+
// (accidents_app.html from 0.2.15 on)
|
|
2621
|
+
function anchorInside(el) {
|
|
2622
|
+
if (el && getComputedStyle(el).position === 'static') el.style.position = 'relative';
|
|
2623
|
+
}
|
|
2534
2624
|
function showSplash(el, text) {
|
|
2535
2625
|
ensureSplashStyle();
|
|
2536
2626
|
// Checks the actual RENDERED position, not el.style (the inline
|
|
@@ -2544,7 +2634,7 @@
|
|
|
2544
2634
|
// height silently collapsed to 0 — invisible map, invisible splash,
|
|
2545
2635
|
// both zero-height. Only truly static (unpositioned) elements need
|
|
2546
2636
|
// this at all.
|
|
2547
|
-
|
|
2637
|
+
anchorInside(el);
|
|
2548
2638
|
const splash = document.createElement('div');
|
|
2549
2639
|
splash.className = 'ixmaps-splash';
|
|
2550
2640
|
splash.innerHTML = '<div class="ixmaps-splash-mark"><span class="ixmaps-splash-ping"></span><span class="ixmaps-splash-dot"></span></div>' +
|
|
@@ -2875,8 +2965,20 @@
|
|
|
2875
2965
|
if (__hoverTooltipWatch || !el.parentElement) return;
|
|
2876
2966
|
const host = el.parentElement;
|
|
2877
2967
|
const attach = (tipEl) => {
|
|
2968
|
+
// deck nests its tooltip inside its widget root (.fill, z-index 2),
|
|
2969
|
+
// a stacking context under the USER-chart pane (z 3) — the tooltip's
|
|
2970
|
+
// own z-index can never cross that. Moved out to the map container,
|
|
2971
|
+
// a sibling of the pane, where z-index 7 puts it above everything;
|
|
2972
|
+
// same coordinate origin, deck keeps updating the same node.
|
|
2973
|
+
// Re-asserted on every style write (deck sets z-index 1 itself).
|
|
2974
|
+
const liftTooltip = () => {
|
|
2975
|
+
if (tipEl.parentNode !== el) el.appendChild(tipEl);
|
|
2976
|
+
if (tipEl.style.zIndex !== '7') tipEl.style.zIndex = 7;
|
|
2977
|
+
};
|
|
2978
|
+
liftTooltip();
|
|
2878
2979
|
let lastSetTransform = null;
|
|
2879
2980
|
const mo = new MutationObserver(() => {
|
|
2981
|
+
liftTooltip();
|
|
2880
2982
|
if (tipEl.style.display === 'none' || !tipEl.textContent.trim()) return;
|
|
2881
2983
|
const t = tipEl.style.transform;
|
|
2882
2984
|
if (!t || t === lastSetTransform) return;
|
|
@@ -2976,7 +3078,10 @@
|
|
|
2976
3078
|
// touches the tooltip DOM in the same tick.
|
|
2977
3079
|
function hideDefaultHoverTooltip() {
|
|
2978
3080
|
requestAnimationFrame(() => {
|
|
2979
|
-
|
|
3081
|
+
// the hover tooltip is reparented into el (see watchHoverTooltip) —
|
|
3082
|
+
// look there first, then where deck originally nested it
|
|
3083
|
+
const defaultTooltipEl = (el && el.querySelector('.deck-tooltip')) ||
|
|
3084
|
+
(el.parentElement && el.parentElement.querySelector('.deck-tooltip'));
|
|
2980
3085
|
if (defaultTooltipEl) defaultTooltipEl.style.display = 'none';
|
|
2981
3086
|
});
|
|
2982
3087
|
}
|
|
@@ -3071,7 +3176,7 @@
|
|
|
3071
3176
|
// relative pixels, and styling the page's own body (relative) breaks
|
|
3072
3177
|
// any page element absolutely positioned against it (AirBnB's
|
|
3073
3178
|
// #onmap-buttons on a zero-height all-float body flew off-screen)
|
|
3074
|
-
el
|
|
3179
|
+
anchorInside(el);
|
|
3075
3180
|
el.appendChild(pinnedTooltipEl);
|
|
3076
3181
|
}
|
|
3077
3182
|
return { getTooltip, onClick, mount };
|
|
@@ -3085,6 +3190,33 @@
|
|
|
3085
3190
|
// calls: addLegendPanel(rt), setLegendOption(value), updateSubTheme(rt),
|
|
3086
3191
|
// layoutLegends().
|
|
3087
3192
|
// ---------------------------------------------------------------
|
|
3193
|
+
// The map-level `align` option positions the legend — BOTH the native
|
|
3194
|
+
// panel stack (createLegend) and a page legend (map.legend(html), the
|
|
3195
|
+
// legendEl built in build()): one helper, so they can never disagree.
|
|
3196
|
+
// gl's four corners, else flat's own values (htmlgui_flat.js 421-470):
|
|
3197
|
+
// "…left" → left (the part before "left", else 55px), 12px from the top;
|
|
3198
|
+
// "…right" → right (else 25px); "center" / "top" → top center.
|
|
3199
|
+
// Default "top-right" per explicit request (real engine's own fallback is
|
|
3200
|
+
// right-anchored too, just without an opinion on vertical placement).
|
|
3201
|
+
function legendAlignCssFor(mapOptions) {
|
|
3202
|
+
const ALIGN_CSS = {
|
|
3203
|
+
'top-left': 'left:10px;top:10px;',
|
|
3204
|
+
'top-right': 'right:10px;top:10px;',
|
|
3205
|
+
'bottom-left': 'left:10px;bottom:10px;',
|
|
3206
|
+
'bottom-right': 'right:10px;bottom:10px;'
|
|
3207
|
+
};
|
|
3208
|
+
const flatAlignCss = align => {
|
|
3209
|
+
const a = String(align || '');
|
|
3210
|
+
// flat hands these to jQuery .css(), which adds px to a unitless
|
|
3211
|
+
// number — a raw cssText needs it explicit
|
|
3212
|
+
const px = v => /^\d+(\.\d+)?$/.test(v) ? v + 'px' : v;
|
|
3213
|
+
if (/left/.test(a)) return 'left:' + px(a.split('left')[0] || '55px') + ';top:12px;';
|
|
3214
|
+
if (/right/.test(a)) return 'right:' + px(a.split('right')[0] || '25px') + ';top:10px;';
|
|
3215
|
+
if (a === 'center' || a === 'top') return 'left:50%;transform:translateX(-50%);top:10px;';
|
|
3216
|
+
return null;
|
|
3217
|
+
};
|
|
3218
|
+
return ALIGN_CSS[mapOptions.align] || flatAlignCss(mapOptions.align) || ALIGN_CSS['top-right'];
|
|
3219
|
+
}
|
|
3088
3220
|
function createLegend(ctx) {
|
|
3089
3221
|
const { builder, map, el, overlay, runtimes, engineApi, built, refresh, scheduleRefresh, notifyRedraw, quietRefresh } = ctx;
|
|
3090
3222
|
let addLegendPanel = () => {};
|
|
@@ -3128,6 +3260,10 @@
|
|
|
3128
3260
|
const legendOpt = builder.mapOptions.legend;
|
|
3129
3261
|
// let, not const: map.setLegend(value) switches them at runtime (see setLegendOption)
|
|
3130
3262
|
let legendOn = legendIsOn(legendOpt);
|
|
3263
|
+
// flat: a page legend (map.legend(html) → ixmaps.legend.url) REPLACES
|
|
3264
|
+
// the theme-generated legend — legend.js 2076 renders the layerlist
|
|
3265
|
+
// legend instead (useLayerListLegend) — so the native panels stay off
|
|
3266
|
+
if (builder._legendHtml) legendOn = false;
|
|
3131
3267
|
let legendFolded = legendOpt === 'closed';
|
|
3132
3268
|
{
|
|
3133
3269
|
// Map-level `align` option positions the legend panel — a map-
|
|
@@ -3146,24 +3282,7 @@
|
|
|
3146
3282
|
// shape choice earlier this session. Default "top-right" per
|
|
3147
3283
|
// explicit request (real engine's own fallback is right-anchored
|
|
3148
3284
|
// too, just without an opinion on vertical placement).
|
|
3149
|
-
const
|
|
3150
|
-
'top-left': 'left:10px;top:10px;',
|
|
3151
|
-
'top-right': 'right:10px;top:10px;',
|
|
3152
|
-
'bottom-left': 'left:10px;bottom:10px;',
|
|
3153
|
-
'bottom-right': 'right:10px;bottom:10px;'
|
|
3154
|
-
};
|
|
3155
|
-
// gl's four corners, else flat's own values (htmlgui_flat.js
|
|
3156
|
-
// 421-470): "…left" → left (the part before "left", else 55px), 12px
|
|
3157
|
-
// from the top; "…right" → right (else 25px); "center" / "top" → top
|
|
3158
|
-
// center
|
|
3159
|
-
const flatAlignCss = align => {
|
|
3160
|
-
const a = String(align || '');
|
|
3161
|
-
if (/left/.test(a)) return 'left:' + (a.split('left')[0] || '55px') + ';top:12px;';
|
|
3162
|
-
if (/right/.test(a)) return 'right:' + (a.split('right')[0] || '25px') + ';top:10px;';
|
|
3163
|
-
if (a === 'center' || a === 'top') return 'left:50%;transform:translateX(-50%);top:10px;';
|
|
3164
|
-
return null;
|
|
3165
|
-
};
|
|
3166
|
-
const legendAlignCss = ALIGN_CSS[builder.mapOptions.align] || flatAlignCss(builder.mapOptions.align) || ALIGN_CSS['top-right'];
|
|
3285
|
+
const legendAlignCss = legendAlignCssFor(builder.mapOptions);
|
|
3167
3286
|
// Real ixmaps-flat's SUB-THEME (MapTheme.createSubTheme,
|
|
3168
3287
|
// maptheme.js 14284-14292 / 15010-15160; map.Themes.enableSubThemes
|
|
3169
3288
|
// defaults to true): marking fields of a multi-field choropleth
|
|
@@ -3254,7 +3373,7 @@
|
|
|
3254
3373
|
// panel's max-height resolves against the MAP's height — the stack's
|
|
3255
3374
|
// former parent anchor collapsed it to 0 on pages whose body has
|
|
3256
3375
|
// only floated children (height 0)
|
|
3257
|
-
el
|
|
3376
|
+
anchorInside(el);
|
|
3258
3377
|
el.appendChild(legendStack);
|
|
3259
3378
|
return legendStack;
|
|
3260
3379
|
};
|
|
@@ -3265,7 +3384,7 @@
|
|
|
3265
3384
|
setLegendOption = (opt) => {
|
|
3266
3385
|
builder.mapOptions.legend = opt;
|
|
3267
3386
|
legendFolded = opt === 'closed';
|
|
3268
|
-
legendOn = legendIsOn(opt);
|
|
3387
|
+
legendOn = builder._legendHtml ? false : legendIsOn(opt);
|
|
3269
3388
|
runtimes.forEach(rt => {
|
|
3270
3389
|
if (!legendOn) {
|
|
3271
3390
|
if (rt._legendPanel && rt._legendPanel.parentNode) rt._legendPanel.parentNode.removeChild(rt._legendPanel);
|
|
@@ -3300,7 +3419,7 @@
|
|
|
3300
3419
|
rt._triggerRedraw = () => { refresh(); };
|
|
3301
3420
|
if (subThemeCapable(rt)) rt._onMarksChanged = () => updateSubTheme(rt);
|
|
3302
3421
|
if (!legendOn || !el.parentElement || !legendApplies(rt)) return;
|
|
3303
|
-
el
|
|
3422
|
+
anchorInside(el);
|
|
3304
3423
|
{
|
|
3305
3424
|
// .type("...|NOLEGEND") — the fourth real legend-related type()
|
|
3306
3425
|
// token: opts a theme OUT of the legend entirely (real engine's
|
|
@@ -3385,6 +3504,17 @@
|
|
|
3385
3504
|
// themselves authored with their own leading space (e.g.
|
|
3386
3505
|
// " MW") in real pages.
|
|
3387
3506
|
const legendUnit = rt.style.legendunits || rt.style.units || '';
|
|
3507
|
+
// a per-item chart over several value fields: the row values as
|
|
3508
|
+
// flat's legend prints them (legend.js 610, 810: __formatValue
|
|
3509
|
+
// with valuedecimals, else 2, "BLANK" — whole numbers without
|
|
3510
|
+
// decimals), a colorscheme "none" part as an outline in linecolor
|
|
3511
|
+
const multiFieldLegend = !!rt._multiFields && rt._isMultiFieldSequence();
|
|
3512
|
+
const legendTotalText = v => (multiFieldLegend
|
|
3513
|
+
? flatFormatValue(v, legendDecimals(rt.style), 'BLANK')
|
|
3514
|
+
: rt._formatTooltipValue(v));
|
|
3515
|
+
const outlineRgb = rt.style.linecolor && styleLineColor(rt.style.linecolor) !== 'none' ? hexOrNamedToRgb(styleLineColor(rt.style.linecolor)) : [128, 128, 128];
|
|
3516
|
+
const legendFill = i => (multiFieldLegend && rt._partNoFill && rt._partNoFill.has(i)
|
|
3517
|
+
? `transparent;box-shadow:inset 0 0 0 1px rgb(${outlineRgb.join(',')})` : null);
|
|
3388
3518
|
// row labels go into innerHTML: the page's own .style({label})
|
|
3389
3519
|
// stays HTML, as in flat; categories read from the data are
|
|
3390
3520
|
// escaped (a value could carry markup)
|
|
@@ -3426,6 +3556,22 @@
|
|
|
3426
3556
|
const globeCenter = proj.type === 'globe' ? map.getCenter() : null;
|
|
3427
3557
|
totals = new Array(rt.categoryLabels.length).fill(0);
|
|
3428
3558
|
if (rt._rangeClassed) { totals = rangeClassLegendTotals(rt, bbox, globeCenter); maxTotal = Math.max(0, ...totals); order = totals.map((v, i) => i).sort((a, b) => totals[b] - totals[a]); return; }
|
|
3559
|
+
// a per-item chart over several value fields: a row per field,
|
|
3560
|
+
// the field's sum over the visible items (flat's NYC status
|
|
3561
|
+
// legend: bikes / empty docks / e-bikes, biggest sum first)
|
|
3562
|
+
if (rt._multiFields && rt._isMultiFieldSequence()) {
|
|
3563
|
+
(rt._activeFeatures || rt.features).forEach(f => {
|
|
3564
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
3565
|
+
if (!c || typeof c[0] !== 'number') return;
|
|
3566
|
+
const [lng, lat] = c;
|
|
3567
|
+
if (lng < bbox[0] || lng > bbox[2] || lat < bbox[1] || lat > bbox[3]) return;
|
|
3568
|
+
if (globeCenter && !isOnVisibleHemisphere(lng, lat, globeCenter)) return;
|
|
3569
|
+
rt._multiFields.forEach((k, i) => { totals[i] += parseFloat(f.properties[k]) || 0; });
|
|
3570
|
+
});
|
|
3571
|
+
maxTotal = Math.max(0, ...totals);
|
|
3572
|
+
order = totals.map((v, i) => i).sort((a, b) => totals[b] - totals[a]);
|
|
3573
|
+
return;
|
|
3574
|
+
}
|
|
3429
3575
|
// _activeFeatures (set by setFacetFilter/clearFacetFilter,
|
|
3430
3576
|
// e.g. the country-select dropdown below) is the facet-
|
|
3431
3577
|
// filtered subset when a filter is active, null otherwise —
|
|
@@ -3558,7 +3704,7 @@
|
|
|
3558
3704
|
for (let line = 0; line < nLines; line++) {
|
|
3559
3705
|
for (let c = 0; c < n; c++) {
|
|
3560
3706
|
const ix = flatFlag(rt.flags, 'INVERT') ? n - c - 1 : c;
|
|
3561
|
-
const rgb = rt.categoryColorsRgb[ix];
|
|
3707
|
+
const rgb = rt.categoryColorsRgb[ix] || [128, 128, 128]; // colorscheme "none": no class colors
|
|
3562
3708
|
const part = rt.partsA[ix];
|
|
3563
3709
|
const title = part ? flatFormatValue(part.min, dec, '') + unit + ' ... ' + flatFormatValue(part.max, dec, '') + unit : '';
|
|
3564
3710
|
const marked = rt._markedClasses.has(ix);
|
|
@@ -3589,8 +3735,8 @@
|
|
|
3589
3735
|
// flat drops the rows without a count once any row has one
|
|
3590
3736
|
// (legend.js 754-760, 787-789: fCountBars && !count)
|
|
3591
3737
|
rowsEl.innerHTML = order.filter(i => !(maxTotal > 0) || totals[i]).map(i => {
|
|
3592
|
-
const rgb = rt.categoryColorsRgb[i];
|
|
3593
|
-
const color = `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;
|
|
3738
|
+
const rgb = rt.categoryColorsRgb[i] || [128, 128, 128]; // colorscheme "none": no class colors
|
|
3739
|
+
const color = legendFill(i) || `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;
|
|
3594
3740
|
const marked = rt._markedClasses.has(i);
|
|
3595
3741
|
const dimmed = rt._markedClasses.size > 0 && !marked;
|
|
3596
3742
|
const rowStyle = 'padding:4px 3px;border-radius:4px;cursor:pointer;opacity:' + (dimmed ? 0.4 : 1) + ';'
|
|
@@ -3617,7 +3763,7 @@
|
|
|
3617
3763
|
+ '<div style="display:flex;align-items:center;gap:6px;">'
|
|
3618
3764
|
+ '<span style="flex:0 0 8px;width:8px;height:8px;border-radius:50%;background:' + color + ';"></span>'
|
|
3619
3765
|
+ '<span style="flex:0 0 auto;width:' + barPx + 'px;height:6px;background:' + color + ';border-radius:3px;"></span>'
|
|
3620
|
-
+ '<span style="flex:0 0 auto;white-space:nowrap;">' +
|
|
3766
|
+
+ '<span style="flex:0 0 auto;white-space:nowrap;">' + legendTotalText(totals[i]) + legendUnit + '</span>'
|
|
3621
3767
|
+ '</div>'
|
|
3622
3768
|
+ '</div>';
|
|
3623
3769
|
}
|
|
@@ -3628,7 +3774,7 @@
|
|
|
3628
3774
|
+ '<span style="flex:1 1 auto;min-width:0;height:6px;background:' + legendColors.track + ';border-radius:3px;overflow:hidden;">'
|
|
3629
3775
|
+ '<span style="display:block;height:100%;width:' + pct + '%;background:' + color + ';border-radius:3px;"></span>'
|
|
3630
3776
|
+ '</span>'
|
|
3631
|
-
+ '<span style="flex:0 0 auto;text-align:right;min-width:60px;">' +
|
|
3777
|
+
+ '<span style="flex:0 0 auto;text-align:right;min-width:60px;">' + legendTotalText(totals[i]) + legendUnit + '</span>'
|
|
3632
3778
|
+ '</div>'
|
|
3633
3779
|
+ '</div>';
|
|
3634
3780
|
}).join('');
|
|
@@ -3873,7 +4019,7 @@
|
|
|
3873
4019
|
this._layerBuilders.push(lb);
|
|
3874
4020
|
return lb;
|
|
3875
4021
|
}
|
|
3876
|
-
this._layerBuilders.push(nameOrBuilder);
|
|
4022
|
+
this._layerBuilders.push(unwrapLayer(nameOrBuilder));
|
|
3877
4023
|
return this;
|
|
3878
4024
|
}
|
|
3879
4025
|
// .Layer() — alias of .layer()
|
|
@@ -4076,6 +4222,10 @@
|
|
|
4076
4222
|
// world once, at the cost of that zoom limit.
|
|
4077
4223
|
renderWorldCopies: this._engineOptions.worldcopies !== false
|
|
4078
4224
|
});
|
|
4225
|
+
// the live-SVG pane USER charts render into (see chartHostFor) —
|
|
4226
|
+
// registered here, keyed by this map's options object, so every
|
|
4227
|
+
// runtime (including later loadProject/map.layer ones) finds it
|
|
4228
|
+
chartHostFor(this._engineOptions, map, el);
|
|
4079
4229
|
// ixmaps.Map(id, {mapProjection}): flat's orthographic map is MapLibre's
|
|
4080
4230
|
// native globe here (same mapping as loadProject, applyProjectMap);
|
|
4081
4231
|
// "ortographic" is the spelling flat pages commonly carry. Any other
|
|
@@ -4199,11 +4349,13 @@
|
|
|
4199
4349
|
|
|
4200
4350
|
if (this._legendHtml) {
|
|
4201
4351
|
const legendEl = document.createElement('div');
|
|
4202
|
-
|
|
4352
|
+
// a page legend (map.legend(html)) is positioned by the map's
|
|
4353
|
+
// own align option — the same rule that places the native stack
|
|
4354
|
+
legendEl.style.cssText = 'position:absolute;' + legendAlignCssFor(this.mapOptions) + 'z-index:5;pointer-events:none';
|
|
4203
4355
|
legendEl.innerHTML = this._legendHtml;
|
|
4204
4356
|
// inside the map container, never the page's body (see the pinned
|
|
4205
4357
|
// tooltip mount) — styling body breaks page-positioned elements
|
|
4206
|
-
el
|
|
4358
|
+
anchorInside(el);
|
|
4207
4359
|
el.appendChild(legendEl);
|
|
4208
4360
|
}
|
|
4209
4361
|
|
|
@@ -4325,6 +4477,7 @@
|
|
|
4325
4477
|
// the layer name): add(theme, flag) — "replace" swaps a theme of the
|
|
4326
4478
|
// same id —, replace(id, theme, flag), remove(id)
|
|
4327
4479
|
add: (layerBuilder, flag) => {
|
|
4480
|
+
layerBuilder = unwrapLayer(layerBuilder);
|
|
4328
4481
|
_chainedLayers = _chainedLayers.then(() => {
|
|
4329
4482
|
if (/replace/i.test(flag || '')) {
|
|
4330
4483
|
const def = layerBuilder.definition();
|
|
@@ -4336,6 +4489,7 @@
|
|
|
4336
4489
|
return engineApi;
|
|
4337
4490
|
},
|
|
4338
4491
|
replace: (id, layerBuilder, flag) => {
|
|
4492
|
+
layerBuilder = unwrapLayer(layerBuilder);
|
|
4339
4493
|
_chainedLayers = _chainedLayers.then(() => { removeThemeById(id); return engineApi.defineLayer(layerBuilder); })
|
|
4340
4494
|
.catch(err => console.error('[ixmaps-gl] map.replace():', err));
|
|
4341
4495
|
return engineApi;
|
|
@@ -4367,6 +4521,23 @@
|
|
|
4367
4521
|
},
|
|
4368
4522
|
setMapTypeId: (id) => engineApi.setMapType(id),
|
|
4369
4523
|
mapType: (id) => engineApi.setMapType(id),
|
|
4524
|
+
// projection swap WITHOUT the project machinery: setProjectJSON
|
|
4525
|
+
// tears down and re-defines every theme to change the projection
|
|
4526
|
+
// (a projection toggle on a theme-heavy page paid a full data
|
|
4527
|
+
// re-aggregation — the power plants stage page). The projection
|
|
4528
|
+
// itself is one MapLibre call; the layers are projection-
|
|
4529
|
+
// dependent (globe far-hemisphere cull, chart placement) so they
|
|
4530
|
+
// are rebuilt — refreshLayers, once now and again when the new
|
|
4531
|
+
// projection has rendered — but THEMES and VIEW are never touched
|
|
4532
|
+
setProjection: (type) => {
|
|
4533
|
+
if (typeof map.setProjection !== 'function') { console.warn('[ixmaps-gl] map.setProjection not available on the loaded maplibre-gl build (needs >=5.0.1) — projection unchanged'); return engineApi; }
|
|
4534
|
+
const t = /globe/i.test(String(type || '')) ? 'globe' : 'mercator';
|
|
4535
|
+
try { map.setProjection({ type: t }); } catch (e) { console.warn('[ixmaps-gl] map.setProjection failed:', e); return engineApi; }
|
|
4536
|
+
refreshLayers();
|
|
4537
|
+
map.triggerRepaint();
|
|
4538
|
+
map.once('idle', refreshLayers);
|
|
4539
|
+
return engineApi;
|
|
4540
|
+
},
|
|
4370
4541
|
// flat's getMapTypeId(): the basemap type name in use
|
|
4371
4542
|
getMapTypeId: () => String(builder.mapOptions.mapType || ''),
|
|
4372
4543
|
// flat's attribution (htmlgui.js htmlgui_set/getAttributionString)
|
|
@@ -4515,7 +4686,7 @@
|
|
|
4515
4686
|
// exactly how this engine's own facet-filter/style setters below
|
|
4516
4687
|
// already push responsibility for "what should happen" to the
|
|
4517
4688
|
// caller rather than guessing.
|
|
4518
|
-
defineLayer: (layerBuilder) => defineFromDefinition(layerBuilder.definition()),
|
|
4689
|
+
defineLayer: (layerBuilder) => defineFromDefinition(unwrapLayer(layerBuilder).definition()),
|
|
4519
4690
|
// GL-PORT COMPAT: real ixmaps-flat's map handle, as a page gets it
|
|
4520
4691
|
// from `myMap.then(map => map.view(...).options(...).layer(a)
|
|
4521
4692
|
// .layer(b))` — the common multi-layer idiom — is chainable on the
|
|
@@ -4551,10 +4722,17 @@
|
|
|
4551
4722
|
.catch(err => console.error('[ixmaps-gl] map.layer():', err));
|
|
4552
4723
|
return engineApi;
|
|
4553
4724
|
}
|
|
4554
|
-
// map.layer(name).data()...define():
|
|
4725
|
+
// map.layer(name).data()...define(): adds a theme on that layer.
|
|
4726
|
+
// NO same-name auto-remove here: flat lets several themes SHARE one
|
|
4727
|
+
// .layer() name (a page's feature base + its chart overlay, the
|
|
4728
|
+
// NYC status page's two "Station" themes — the overlay joins onto
|
|
4729
|
+
// the base's geometry by lookup, removing the base would kill
|
|
4730
|
+
// both). Replacement is by THEME ID (style.name/meta.name), and
|
|
4731
|
+
// happens inside defineFromDefinition instead — a re-defined id
|
|
4732
|
+
// (the page's 10s live refresh) replaces its own previous theme.
|
|
4555
4733
|
const lb = new LayerBuilder(nameOrBuilder);
|
|
4556
4734
|
lb.define = () => {
|
|
4557
|
-
_chainedLayers = _chainedLayers.then(() =>
|
|
4735
|
+
_chainedLayers = _chainedLayers.then(() => engineApi.defineLayer(lb))
|
|
4558
4736
|
.catch(err => console.error('[ixmaps-gl] map.layer().define():', err));
|
|
4559
4737
|
return _chainedLayers;
|
|
4560
4738
|
};
|
|
@@ -4583,8 +4761,22 @@
|
|
|
4583
4761
|
// Removes every runtime whose OWN .layer(name) matches — a theme
|
|
4584
4762
|
// may legitimately be split across more than one runtime sharing
|
|
4585
4763
|
// one name (the CHOROPLETH/FEATURE-donor pair above), so this
|
|
4586
|
-
// removes all of them, not just the first match.
|
|
4587
|
-
|
|
4764
|
+
// removes all of them, not just the first match. GL-PORT COMPAT:
|
|
4765
|
+
// flat addresses a theme by its ID — style.name / meta.name —
|
|
4766
|
+
// as much as by its layer name (htmlgui.js removeTheme), and a
|
|
4767
|
+
// real page's own rebuild loop typically knows only the id it
|
|
4768
|
+
// set via .meta({name}) (the fire page's KDE: removeTheme
|
|
4769
|
+
// ("kde_fires") for the layer "kde_fires_layer"). Layer name
|
|
4770
|
+
// first, then the id — without the fallback every rebuild
|
|
4771
|
+
// removed nothing and stacked another copy of the theme,
|
|
4772
|
+
// compounding opacity (same semantics removeThemeById already
|
|
4773
|
+
// implements for the handle's .remove(id)).
|
|
4774
|
+
removeTheme: (name) => {
|
|
4775
|
+
const byLayer = runtimes.filter(r => r.name === name);
|
|
4776
|
+
if (byLayer.length) return removeRuntimes(name, byLayer);
|
|
4777
|
+
const byId = runtimes.filter(r => (r.style && r.style.name === name) || (r.meta && r.meta.name === name));
|
|
4778
|
+
return byId.length ? removeRuntimes(byId[0].name, byId) : false;
|
|
4779
|
+
},
|
|
4588
4780
|
// flat's refreshTheme (maptheme.js Themes.refreshTheme): the
|
|
4589
4781
|
// theme's data is loaded again — a broker is called again — and
|
|
4590
4782
|
// replaces the old features in place (draw order, legend, style
|
|
@@ -4617,6 +4809,11 @@
|
|
|
4617
4809
|
if (rt._unlisten) rt._unlisten();
|
|
4618
4810
|
if (rt._cancelWait) rt._cancelWait();
|
|
4619
4811
|
clearTimeout(rt._dependentTimer);
|
|
4812
|
+
// a removed theme's live USER-chart SVG is not part of the deck
|
|
4813
|
+
// layers — leaving its groups mounted keeps the charts on the
|
|
4814
|
+
// map after map.remove()/replace() (censimenti's sidebar)
|
|
4815
|
+
if (rt._clearUserChartPane) rt._clearUserChartPane();
|
|
4816
|
+
if (rt._chartHost && rt._chartHost.runtimes) rt._chartHost.runtimes.delete(rt);
|
|
4620
4817
|
});
|
|
4621
4818
|
const removed = removedRuntimes.length > 0;
|
|
4622
4819
|
// stale-registry guard: _globalThemeRegistry is a bare name->
|
|
@@ -4708,10 +4905,15 @@
|
|
|
4708
4905
|
else map.fitBounds(bounds, { padding: 30, animate: false, maxZoom: 16 });
|
|
4709
4906
|
}
|
|
4710
4907
|
|
|
4711
|
-
|
|
4908
|
+
// quiet: the broker's data fills only this theme — not broadcast to
|
|
4909
|
+
// the other themes of the name (flat's refreshTheme reloads one theme;
|
|
4910
|
+
// setExternalData only releases themes still waiting for the data,
|
|
4911
|
+
// maptheme.js 2570-2591)
|
|
4912
|
+
async function loadNamedTheme(rt, force, quiet = false) {
|
|
4712
4913
|
const spec = normalizeTheme(rt._definition);
|
|
4713
4914
|
const name = String(spec.data.name);
|
|
4714
4915
|
const broker = brokerTheme(spec);
|
|
4916
|
+
if (quiet && broker) _quietNamedData.set(name, (_quietNamedData.get(name) || 0) + 1);
|
|
4715
4917
|
rt._loading = (rt._loading || 0) + 1;
|
|
4716
4918
|
if (broker) { pendingLoads++; rt._brokerLoading = (rt._brokerLoading || 0) + 1; setDataLoading(); }
|
|
4717
4919
|
try {
|
|
@@ -4733,6 +4935,7 @@
|
|
|
4733
4935
|
else console.error(`[ixmaps-gl] theme "${themeIdOf(rt)}": no data —`, err);
|
|
4734
4936
|
} finally {
|
|
4735
4937
|
rt._loading--;
|
|
4938
|
+
if (quiet && broker) { const n = (_quietNamedData.get(name) || 1) - 1; if (n) _quietNamedData.set(name, n); else _quietNamedData.delete(name); }
|
|
4736
4939
|
if (broker) { pendingLoads--; rt._brokerLoading--; setDataLoading(); }
|
|
4737
4940
|
}
|
|
4738
4941
|
}
|
|
@@ -4781,7 +4984,11 @@
|
|
|
4781
4984
|
}
|
|
4782
4985
|
// flat, after a FEATURE theme is drawn (maptheme.js 18947-18962): the
|
|
4783
4986
|
// other themes on its layer are placed again, and a data.query broker
|
|
4784
|
-
// among them is refreshed 500 ms later
|
|
4987
|
+
// among them is refreshed 500 ms later — quietly: its new data fills
|
|
4988
|
+
// that theme only. Broadcast to every theme of the name, a FEATURE
|
|
4989
|
+
// theme sharing the broker's data name (the fire areas page: SIF
|
|
4990
|
+
// polygons and chart both "data_2") was refilled, drawn, refreshed
|
|
4991
|
+
// its dependent again — an endless reload of ~15k records every 3 s
|
|
4785
4992
|
function featureDependents(featureRt) {
|
|
4786
4993
|
runtimes.forEach(dep => {
|
|
4787
4994
|
if (dep === featureRt || dep.flags.has('FEATURE') || dep.flags.has('FEATURES')) return;
|
|
@@ -4789,7 +4996,7 @@
|
|
|
4789
4996
|
const spec = normalizeTheme(dep._definition || {});
|
|
4790
4997
|
if (dep._named && spec.data && spec.data.query) {
|
|
4791
4998
|
clearTimeout(dep._dependentTimer);
|
|
4792
|
-
dep._dependentTimer = setTimeout(() => { if (runtimes.includes(dep)) loadNamedTheme(dep, true); }, 500);
|
|
4999
|
+
dep._dependentTimer = setTimeout(() => { if (runtimes.includes(dep)) loadNamedTheme(dep, true, true); }, 500);
|
|
4793
5000
|
} else if (dep._named && dep._lastTable && !dep._loading) {
|
|
4794
5001
|
fillNamedTheme(dep, spec, dep._lastTable, null);
|
|
4795
5002
|
}
|
|
@@ -4798,10 +5005,18 @@
|
|
|
4798
5005
|
|
|
4799
5006
|
// One theme definition (real ixmaps-flat's shape — from a builder's
|
|
4800
5007
|
// definition() or a project file) → a new runtime on this map. Shared
|
|
4801
|
-
// by defineLayer and loadProject. Purely additive
|
|
5008
|
+
// by defineLayer and loadProject. Purely additive across LAYER names —
|
|
5009
|
+
// several themes legitimately share one (a FEATURE base + its CHART
|
|
5010
|
+
// overlay, the CHOROPLETH/FEATURE-donor pair) — but a re-defined
|
|
5011
|
+
// THEME ID (style.name/meta.name) replaces its own previous theme, as
|
|
5012
|
+
// flat itself does ("defining a theme with the same name replaces it
|
|
5013
|
+
// in place"; a page's live-refresh loop relies on it, e.g. the NYC
|
|
5014
|
+
// status page re-defining its two themes every 10s).
|
|
4802
5015
|
async function defineFromDefinition(def) {
|
|
4803
5016
|
if (validation) validation.definition(def);
|
|
4804
5017
|
const pdef = projectThemeToDefinition(def);
|
|
5018
|
+
const replacedId = (pdef.meta && pdef.meta.name) || (pdef.style && pdef.style.name);
|
|
5019
|
+
if (replacedId) removeThemeById(replacedId);
|
|
4805
5020
|
let spec = normalizeTheme(pdef);
|
|
4806
5021
|
let raw;
|
|
4807
5022
|
const deferred = deferredFeatureLoad(spec, map.getZoom());
|
|
@@ -5336,7 +5551,7 @@
|
|
|
5336
5551
|
// semantics — layer, view, options, require, attribution, on — keep
|
|
5337
5552
|
// theirs and are not overridden.
|
|
5338
5553
|
for (const m of ['replace', 'add', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
5339
|
-
'refreshTheme', 'redrawTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'resize',
|
|
5554
|
+
'refreshTheme', 'redrawTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'setProjection', 'resize',
|
|
5340
5555
|
'loadProject', 'project', 'setThemeVisible', 'setThemeTimeFrame', 'show', 'hide', 'getZoom', 'getThemeObj',
|
|
5341
5556
|
'getThemes', 'theme', 'setView']) {
|
|
5342
5557
|
if (!MapBuilder.prototype[m]) {
|
|
@@ -5412,6 +5627,9 @@
|
|
|
5412
5627
|
}
|
|
5413
5628
|
return promise;
|
|
5414
5629
|
};
|
|
5630
|
+
// flat's mapApi.setView(center, zoom) (htmlgui_flat.js 1327) — the
|
|
5631
|
+
// same live-or-build-time split as .view() above
|
|
5632
|
+
promise.setView = (...args) => promise.view(...args);
|
|
5415
5633
|
promise.options = (...args) => { builder.options(...args); return promise; };
|
|
5416
5634
|
promise.attribution = (...args) => { builder.attribution(...args); return promise; };
|
|
5417
5635
|
promise.legend = (...args) => { builder.legend(...args); return promise; };
|
|
@@ -5452,8 +5670,11 @@
|
|
|
5452
5670
|
// itself, which stays purely additive for every other caller —
|
|
5453
5671
|
// see its own comment for why (the CHOROPLETH/FEATURE-donor
|
|
5454
5672
|
// pairing needs two runtimes sharing one name to coexist).
|
|
5673
|
+
// NO same-LAYER-name auto-remove (see engineApi.layer's own
|
|
5674
|
+
// comment — several themes legitimately share one layer name);
|
|
5675
|
+
// a re-defined THEME ID replaces its own previous theme inside
|
|
5676
|
+
// defineFromDefinition instead
|
|
5455
5677
|
lb.define = () => {
|
|
5456
|
-
resolvedApi.removeTheme(nameOrBuilder);
|
|
5457
5678
|
return resolvedApi.defineLayer(lb);
|
|
5458
5679
|
};
|
|
5459
5680
|
}
|
|
@@ -5736,6 +5957,41 @@
|
|
|
5736
5957
|
* (flatChartBranch(flags) === 'bubble' ? 0.5 : 1);
|
|
5737
5958
|
return Math.max(VALUE_RADIUS_MIN, Math.min(VALUE_RADIUS_MAX, fixed * zoomFactor));
|
|
5738
5959
|
}
|
|
5960
|
+
// NOSIZE: every chart ONE fixed radius, whatever the item's value;
|
|
5961
|
+
// normalsizevalue doesn't scale it. Which one depends on flat's SYMBOL
|
|
5962
|
+
// sub-branch:
|
|
5963
|
+
// - a CATEGORICAL theme without AGGREGATE/GROUP ("symbol equals
|
|
5964
|
+
// value", maptheme.js 21566-21625, "GR 01.07.2023 redesign size
|
|
5965
|
+
// handling"): the normal radius itself
|
|
5966
|
+
// - every other symbol chart (the per-part ladder, 21829-21896):
|
|
5967
|
+
// SEQUENCE normalX(chart size / 3), 2/3 of it; otherwise
|
|
5968
|
+
// nMaxRadius / 5 — measured on the fire areas page's SIF chart
|
|
5969
|
+
// (CHART|SYMBOL|NOSIZE: r 60 units at normalX(1) 20, i.e. 3 of the
|
|
5970
|
+
// 15-unit normal radius)
|
|
5971
|
+
// The BUBBLE draw keeps its own older NOSIZE value, half of it
|
|
5972
|
+
// (maptheme.js 19839)
|
|
5973
|
+
if (flags && flags.has('NOSIZE') && !flags.has('PLOT') && !flags.has('GRIDSIZE')) {
|
|
5974
|
+
const symbolEqualsValue = flags.has('CATEGORICAL') && !flags.has('AGGREGATE') && !flatFlag(flags, 'GROUP');
|
|
5975
|
+
const fixed = NORMAL_RADIUS_PX * (styleNum(style.scale) || 1)
|
|
5976
|
+
* (flatChartBranch(flags) === 'bubble' ? 0.5
|
|
5977
|
+
: symbolEqualsValue ? 1
|
|
5978
|
+
: flatFlag(flags, 'SEQUENCE') ? 2 / 3 : 1 / 5);
|
|
5979
|
+
return Math.max(VALUE_RADIUS_MIN, Math.min(VALUE_RADIUS_MAX, fixed * zoomFactor));
|
|
5980
|
+
}
|
|
5981
|
+
// Same redesign, the rule around it (maptheme.js 21612-21625) — only in
|
|
5982
|
+
// flat's "symbol equals value" sub-branch, a CATEGORICAL theme without
|
|
5983
|
+
// AGGREGATE/GROUP: the symbol resizes by itemA[a].nSize, which flat only
|
|
5984
|
+
// sets for a theme with a bound szSizeField (maptheme.js 9144-9153), so
|
|
5985
|
+
// WITHOUT a size field every item keeps the DEFAULT normal radius,
|
|
5986
|
+
// whatever its value. Every other symbol chart (the per-part ladder,
|
|
5987
|
+
// 21850-21851) and BUBBLE (19851) scale by the value — below.
|
|
5988
|
+
if (flags && flags.has('SYMBOL') && !flags.has('BUBBLE') && !flags.has('USER')
|
|
5989
|
+
&& !flags.has('FIXSIZE') && !flags.has('PLOT') && !flags.has('GRIDSIZE')
|
|
5990
|
+
&& flags.has('CATEGORICAL') && !flags.has('AGGREGATE') && !flatFlag(flags, 'GROUP')
|
|
5991
|
+
&& !style.sizefield) {
|
|
5992
|
+
const fixed = NORMAL_RADIUS_PX * (styleNum(style.scale) || 1);
|
|
5993
|
+
return Math.max(VALUE_RADIUS_MIN, Math.min(VALUE_RADIUS_MAX, fixed * zoomFactor));
|
|
5994
|
+
}
|
|
5739
5995
|
// Real default (maptheme.js ~line 5059) when the theme sets no
|
|
5740
5996
|
// normalsizevalue: the dataset's own max value on the bound size
|
|
5741
5997
|
// field (maxSizeValue, from LayerRuntime._prepare), not a fixed
|
|
@@ -6048,9 +6304,9 @@
|
|
|
6048
6304
|
|
|
6049
6305
|
// a chart's VALUES text (maptheme.js 20979-21017): an explicit valuefield
|
|
6050
6306
|
// prints that field of the record ($title$: the title; a non-number as
|
|
6051
|
-
// is), else the value; numbers formatted with valuedecimals
|
|
6052
|
-
// 0
|
|
6053
|
-
// else none
|
|
6307
|
+
// is), else the value; numbers formatted with valuedecimals when set
|
|
6308
|
+
// (explicitValueDecimals: a given "0" counts), else 1 decimal below 1 —
|
|
6309
|
+
// or when the max value is at most 1 — else none, plus the unit, which flat stores with a leading space unless
|
|
6054
6310
|
// it starts with "." (9675) and appends only up to 5 characters.
|
|
6055
6311
|
// A CATEGORICAL bubble prints its size value when size is bound to a
|
|
6056
6312
|
// field (its value, see resolveAggregateValue), as flat does; with
|
|
@@ -6064,11 +6320,30 @@
|
|
|
6064
6320
|
if (!field || field !== binding.value || !flags.has('CATEGORICAL') || !labels || labels[i] == null) return null;
|
|
6065
6321
|
return { [field]: labels[i] };
|
|
6066
6322
|
}
|
|
6323
|
+
// style.valuedecimals as flat reads it: nValueDecimals keeps the raw
|
|
6324
|
+
// style string, so an explicit "0" is truthy in flat's `nValueDecimals ||
|
|
6325
|
+
// default` and counts (fire areas page: valuedecimals "0", 0.48 → "1 ha");
|
|
6326
|
+
// null when not set
|
|
6327
|
+
// (valuedecimals is therefore not typed: STYLE_NUMBER_KEYS). Flat's
|
|
6328
|
+
// truthiness: unset, "" and a numeric 0 fall back to the default
|
|
6329
|
+
function explicitValueDecimals(style) {
|
|
6330
|
+
const v = style && style.valuedecimals;
|
|
6331
|
+
if (!v) return null;
|
|
6332
|
+
const n = Number(v);
|
|
6333
|
+
return Number.isFinite(n) ? n : null;
|
|
6334
|
+
}
|
|
6335
|
+
// the legend's decimals (legend.js 610): valuedecimals when defined, else 2
|
|
6336
|
+
function legendDecimals(style) {
|
|
6337
|
+
const v = style && style.valuedecimals;
|
|
6338
|
+
const n = v == null || v === '' ? NaN : Number(v);
|
|
6339
|
+
return Number.isFinite(n) ? n : 2;
|
|
6340
|
+
}
|
|
6067
6341
|
function flatValueText(raw, title, value, style, flags, opts = {}) {
|
|
6068
6342
|
const units = style.units ? String(style.units) : '';
|
|
6069
6343
|
const unit = units ? (units[0] === '.' ? '' : ' ') + units : '';
|
|
6070
6344
|
const unitText = unit.length <= 5 ? unit : '';
|
|
6071
|
-
const
|
|
6345
|
+
const explicitDec = explicitValueDecimals(style);
|
|
6346
|
+
const dec = v => (explicitDec != null ? explicitDec : ((v < 1 || (opts.maxValue != null && opts.maxValue <= 1)) ? 1 : 0));
|
|
6072
6347
|
const signed = (v, text) => {
|
|
6073
6348
|
if (flags && (flags.has('DIFFERENCE') || flags.has('RELATIVE') || flags.has('SIGN'))) {
|
|
6074
6349
|
if (v === 0) return '+/-' + text;
|
|
@@ -6081,7 +6356,7 @@
|
|
|
6081
6356
|
if (field && raw && raw[field] != null && raw[field] !== '') {
|
|
6082
6357
|
if (isNaN(raw[field])) return String(raw[field]);
|
|
6083
6358
|
const v = Number(raw[field]);
|
|
6084
|
-
return signed(v, flatGroupedValue(v,
|
|
6359
|
+
return signed(v, flatGroupedValue(v, explicitDec != null ? explicitDec : (v < 1 ? 1 : 0), opts.noBreaks) + unitText);
|
|
6085
6360
|
}
|
|
6086
6361
|
return signed(value, flatGroupedValue(value, dec(value), opts.noBreaks) + unitText);
|
|
6087
6362
|
}
|
|
@@ -6183,19 +6458,29 @@
|
|
|
6183
6458
|
}
|
|
6184
6459
|
|
|
6185
6460
|
function valuesHiddenByScale(style, zoom) {
|
|
6186
|
-
|
|
6461
|
+
// flat accepts both 'valueupper' and 'valuesupper' (maptheme.js 1710,
|
|
6462
|
+
// "GR 15.03.2017 accept also 'valuesupper'"), same for the lower pair
|
|
6463
|
+
const m = /^1:(\d+(?:\.\d+)?)$/.exec(style.valueupper || style.valuesupper || '');
|
|
6187
6464
|
if (!m) return false;
|
|
6188
6465
|
const upperRatio = parseFloat(m[1]);
|
|
6189
6466
|
const scaleDenominator = scaleDenominatorAt(zoom);
|
|
6190
6467
|
return scaleDenominator > upperRatio;
|
|
6191
6468
|
}
|
|
6469
|
+
// flat's chart LABEL scale gate (maptheme.js 24536-24537): the label is
|
|
6470
|
+
// visible while the scale passes <= against BOTH bounds — flat's own
|
|
6471
|
+
// lower test reads <= too, kept as-is for parity
|
|
6472
|
+
function labelHiddenByScale(style, zoom) {
|
|
6473
|
+
const upper = scaleDenom(style.labelupper), lower = scaleDenom(style.labellower);
|
|
6474
|
+
const scale = scaleDenominatorAt(zoom);
|
|
6475
|
+
return !!((upper && scale > upper) || (lower && scale > lower));
|
|
6476
|
+
}
|
|
6192
6477
|
|
|
6193
6478
|
// contrasting text color against a bubble's own fill — standard relative
|
|
6194
6479
|
// luminance; approximates the real engine's __maptheme_getChartColors(...)
|
|
6195
6480
|
// .textColor (white text on ixmaps' typically dark/saturated palette,
|
|
6196
6481
|
// dark text on pale fills) without needing its exact color-space table.
|
|
6197
6482
|
function contrastTextColor(fillRgb) {
|
|
6198
|
-
const [r, g, b] = fillRgb;
|
|
6483
|
+
const [r, g, b] = fillRgb || [128, 128, 128];
|
|
6199
6484
|
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
6200
6485
|
return luminance > 0.6 ? [30, 30, 30] : [255, 255, 255];
|
|
6201
6486
|
}
|
|
@@ -6710,6 +6995,25 @@
|
|
|
6710
6995
|
return parts;
|
|
6711
6996
|
}
|
|
6712
6997
|
|
|
6998
|
+
// CHART|SYMBOL|SEQUENCE|RINGS (flat maptheme.js 21293-21318): the parts
|
|
6999
|
+
// nest on the center — each non-zero part sized by the running total of
|
|
7000
|
+
// the non-zero parts up to it (its own value included), biggest first
|
|
7001
|
+
// so the smaller ones stay on top; parts of value 0 are left out.
|
|
7002
|
+
// radiusOf(total) is the ring's radius. Measured on flat (NYC status:
|
|
7003
|
+
// 63 | 75 | 3 → r 161.5 | 353.8 | 361.5 at max radius 300, max value
|
|
7004
|
+
// 117). Returns { i, v (the part's own value), c (running total), r, x, y }.
|
|
7005
|
+
function ringsLayout(counts, radiusOf) {
|
|
7006
|
+
const parts = [];
|
|
7007
|
+
let total = 0;
|
|
7008
|
+
counts.forEach((v, i) => {
|
|
7009
|
+
if (!v) return;
|
|
7010
|
+
total += v;
|
|
7011
|
+
const r = radiusOf(total);
|
|
7012
|
+
if (r > 0) parts.push({ i, v, c: total, r, x: 0, y: 0 });
|
|
7013
|
+
});
|
|
7014
|
+
return parts.sort((a, b) => b.c - a.c);
|
|
7015
|
+
}
|
|
7016
|
+
|
|
6713
7017
|
// ---------------------------------------------------------------
|
|
6714
7018
|
// Classification — pure functions (no LayerRuntime state): class
|
|
6715
7019
|
// breaks for a numeric field, and which class a value falls in.
|
|
@@ -7846,6 +8150,18 @@
|
|
|
7846
8150
|
return out;
|
|
7847
8151
|
}
|
|
7848
8152
|
|
|
8153
|
+
// Every runtime ever built gets a fresh number: a theme rebuilt through
|
|
8154
|
+
// removeTheme() + defineLayer() (a page's own refresh loop, e.g. the fire
|
|
8155
|
+
// page's KDE isobands) is a NEW runtime whose data may differ from the
|
|
8156
|
+
// runtime it replaces — if its deck.gl layer kept the SAME id, deck would
|
|
8157
|
+
// update the old layer's state in place, and under the interleaved overlay
|
|
8158
|
+
// those in-place updates can leave the PREVIOUS generation's geometry on
|
|
8159
|
+
// screen (the exact failure _dataLayerId's own comment documents for
|
|
8160
|
+
// replaceFeatures: "updates in place, arriving over each other, left
|
|
8161
|
+
// triangles across sections"). A unique -dN suffix per runtime forces deck
|
|
8162
|
+
// to build a genuinely new layer and dispose the old one.
|
|
8163
|
+
let LAYER_DATA_GENERATION = 0;
|
|
8164
|
+
|
|
7849
8165
|
class LayerRuntime {
|
|
7850
8166
|
// spec: a normalizeTheme() result — never a raw LayerBuilder
|
|
7851
8167
|
constructor(spec, fc, mapOptions) {
|
|
@@ -7854,6 +8170,8 @@
|
|
|
7854
8170
|
this.style = spec.style;
|
|
7855
8171
|
this.flags = symbolsFlagCompat(spec.flags, spec.style);
|
|
7856
8172
|
this.meta = spec.meta;
|
|
8173
|
+
// see LAYER_DATA_GENERATION above — unique per runtime, see _dataLayerId
|
|
8174
|
+
this._dataGen = ++LAYER_DATA_GENERATION;
|
|
7857
8175
|
// GL-PORT COMPAT: retained only for ixmaps.getThemeObj()'s szFilter
|
|
7858
8176
|
// (real ixmaps-flat global compat shim, see _globalThemeRegistry) —
|
|
7859
8177
|
// this engine's own filtering is fully static/load-time (see
|
|
@@ -8024,6 +8342,21 @@
|
|
|
8024
8342
|
// ("investimento"/"tamponamento"/...) needs the display text, not
|
|
8025
8343
|
// a raw numeric code it could never match against.
|
|
8026
8344
|
this.categoryColorsRgb = resolveClassColors(this.style.colorscheme, this.categoryDisplayLabels, this.style.classes);
|
|
8345
|
+
} else if (this._isMultiFieldSequence()) {
|
|
8346
|
+
// CHART|SYMBOL|SEQUENCE over several value fields (no CATEGORICAL,
|
|
8347
|
+
// no AGGREGATE): one part per field, colored by the colorscheme in
|
|
8348
|
+
// field order, labeled by .style({label}) (flat's szFieldsA /
|
|
8349
|
+
// szLabelA). A colorscheme entry "none" is a part without fill —
|
|
8350
|
+
// only its outline in linecolor (flat: fill:none). The parts' max
|
|
8351
|
+
// value is flat's nMax: the biggest single field value of any
|
|
8352
|
+
// record (maptheme.js nMaxA → nMax), measured on the NYC status page
|
|
8353
|
+
Object.assign(this, computeMultiFieldClasses(this.binding, this.style));
|
|
8354
|
+
this.categoryIndexByLabel = new Map(this.categoryLabels.map((v, i) => [v, i]));
|
|
8355
|
+
const scheme = Array.isArray(this.style.colorscheme) ? this.style.colorscheme : [];
|
|
8356
|
+
this._partNoFill = new Set(this._multiFields.map((f, i) => i).filter(i => String(scheme[i]).trim().toLowerCase() === 'none'));
|
|
8357
|
+
let max = 0;
|
|
8358
|
+
for (const f of this.features) for (const field of this._multiFields) { const v = Math.abs(parseFloat(f.properties[field])); if (v > max) max = v; }
|
|
8359
|
+
this._multiFieldPartMax = max || 1;
|
|
8027
8360
|
} else if (this.binding.value) {
|
|
8028
8361
|
// real engine's OTHER coloring mode (maptheme.js distributeValues,
|
|
8029
8362
|
// partsA): a NUMERIC bound value, not CATEGORICAL, is classed into
|
|
@@ -8036,6 +8369,24 @@
|
|
|
8036
8369
|
// "which bucket does this feature belong to", and once buckets +
|
|
8037
8370
|
// colors exist, everything downstream is identical.
|
|
8038
8371
|
this._buildPartsA();
|
|
8372
|
+
} else if (this.style.colorscheme != null) {
|
|
8373
|
+
// GL-PORT COMPAT: a theme with NO classification at all (no
|
|
8374
|
+
// CATEGORICAL categories, no numeric binding.value to range-class)
|
|
8375
|
+
// still has classes in the real engine — exactly ONE, its implicit
|
|
8376
|
+
// bucket: _resolveClassIndex returns 0 for every feature, and flat
|
|
8377
|
+
// colors them all with colorScheme[0] (every MapTheme is built
|
|
8378
|
+
// from its colorscheme array — one entry, one class, e.g. the
|
|
8379
|
+
// fire page's KDE isobands: FEATURE|CHOROPLETH, colorscheme
|
|
8380
|
+
// "#dd0088", no value field — every iso band one flat magenta).
|
|
8381
|
+
// Without this branch categoryColorsRgb stayed undefined while
|
|
8382
|
+
// cat was still 0, and the choropleth's getFillColor crashed on
|
|
8383
|
+
// categoryColorsRgb[0] — deck.gl killed the layer at init and
|
|
8384
|
+
// the theme never drew. A colorscheme that resolves to NO colors
|
|
8385
|
+
// (the explicit no-fill ["none"] — flat draws unfilled symbols,
|
|
8386
|
+
// only their values/boxes/titles) keeps that: resolving it to the
|
|
8387
|
+
// fallback palette would paint symbol circles flat never draws.
|
|
8388
|
+
const resolved = resolveColorScheme(this.style.colorscheme, [''], this.style.classes);
|
|
8389
|
+
this.categoryColorsRgb = resolved ? resolved.map(c => Array.isArray(c) ? c : (parseCssColor(c) || [130, 130, 130])) : null;
|
|
8039
8390
|
}
|
|
8040
8391
|
|
|
8041
8392
|
// facet-driven dynamic filter state (see setFacetFilter/getFacets) —
|
|
@@ -8105,6 +8456,17 @@
|
|
|
8105
8456
|
// this with AGGREGATE clustering yet, so a clustered GROUP bubble
|
|
8106
8457
|
// always stays circular (_buildBubbleIcon, untouched).
|
|
8107
8458
|
_resolveSymbolShape(props) {
|
|
8459
|
+
// flat's class→symbol mapping (maptheme.js 1417, 2074-2077, 12937):
|
|
8460
|
+
// `symbols` WITHOUT a symbolfield names the shape per COLOR class —
|
|
8461
|
+
// szSymbolsA[class], the last entry repeating when the array is
|
|
8462
|
+
// shorter than the classes (a single entry, like the fire page's
|
|
8463
|
+
// symbols: ["square"], shapes every class). Only GL's implemented
|
|
8464
|
+
// shapes draw (drawSymbolPath); flat-only names fall back to circle.
|
|
8465
|
+
if (!this.style.symbolfield && Array.isArray(this.style.symbols)) {
|
|
8466
|
+
const s = this.style.symbols;
|
|
8467
|
+
const cat = props && props.cat != null ? props.cat : 0;
|
|
8468
|
+
return normalizeSymbolShape(s[Math.min(cat, s.length - 1)]);
|
|
8469
|
+
}
|
|
8108
8470
|
if (!this.style.symbolfield || !Array.isArray(this.style.symbolvalues) || !Array.isArray(this.style.symbols)) return 'circle';
|
|
8109
8471
|
// CORRECTED: found live via pickObject, not assumed — the properties
|
|
8110
8472
|
// this actually receives at render time (from _computeAggregatedItems'
|
|
@@ -8166,6 +8528,19 @@
|
|
|
8166
8528
|
// they'd never even share a cell in the first place). Every
|
|
8167
8529
|
// feature goes into ONE bucket; _buildChartLayers resolves the real
|
|
8168
8530
|
// 7-class color once it knows each cell's actual aggregated total.
|
|
8531
|
+
if (this._multiFields && this._isMultiFieldSequence()) {
|
|
8532
|
+
// one bucket; each record carries its field values as the chart's
|
|
8533
|
+
// parts. Its value — the draw order — is the first field's, as
|
|
8534
|
+
// flat's chart node value (NYC status: value="63" = e-bikes)
|
|
8535
|
+
this._featuresByCategory = [sourceFeatures.map(f => {
|
|
8536
|
+
const parts = this._multiFields.map(k => parseFloat(f.properties[k]) || 0);
|
|
8537
|
+
return { type: 'Feature', geometry: f.geometry, properties: { value: parts[0], parts, raw: f.properties } };
|
|
8538
|
+
})];
|
|
8539
|
+
this._clusterIndices = null;
|
|
8540
|
+
this._clusterRadiusPx = null;
|
|
8541
|
+
this._aggregateStatsCache = null;
|
|
8542
|
+
return;
|
|
8543
|
+
}
|
|
8169
8544
|
if (this._rangeClassed) {
|
|
8170
8545
|
// flat colors/classes an aggregated cell by the SUM of the value
|
|
8171
8546
|
// field (nValuesA, maptheme.js:10870ff; classified by nValuesA[0],
|
|
@@ -8652,7 +9027,7 @@
|
|
|
8652
9027
|
return this._glowIconCache.get(key);
|
|
8653
9028
|
}
|
|
8654
9029
|
|
|
8655
|
-
_buildBubbleIcon(counts, colors, marked = null) {
|
|
9030
|
+
_buildBubbleIcon(counts, colors, marked = null, shapes = null) {
|
|
8656
9031
|
// The cache key quantizes each count's RATIO to the group's total,
|
|
8657
9032
|
// not the raw count — computeBubblePackLayout's radii are purely
|
|
8658
9033
|
// sqrt(c/total), so two clusters with wildly different absolute
|
|
@@ -8682,7 +9057,7 @@
|
|
|
8682
9057
|
// coarse share buckets that can put a small marked part at 0
|
|
8683
9058
|
const key = counts.map(c => Math.round((c / total) * RATIO_BUCKETS)).join('-') + (marked
|
|
8684
9059
|
? '|iso:' + counts.map((c, i) => (c > 0 && marked.has(i) ? i + ':' + Math.round(Math.sqrt(c / total) * 100) : null)).filter(Boolean).join(',')
|
|
8685
|
-
: '');
|
|
9060
|
+
: '') + (shapes ? '|sym:' + shapes.map(v => String(v).toLowerCase()).join(',') : '');
|
|
8686
9061
|
if (this._iconCache.has(key)) return this._iconCache.get(key);
|
|
8687
9062
|
const size = BUBBLE_ICON_SIZE;
|
|
8688
9063
|
const canvas = document.createElement('canvas');
|
|
@@ -8691,8 +9066,16 @@
|
|
|
8691
9066
|
const { present, radii, offsets, fitScale } = (marked && isolatedBubblePackLayout(counts, size, marked)) || computeBubblePackLayout(counts, size);
|
|
8692
9067
|
const cx = size / 2, cy = size / 2;
|
|
8693
9068
|
present.forEach((p, i) => {
|
|
9069
|
+
// flat draws each part of an aggregated SYMBOL theme as ITS class's
|
|
9070
|
+
// symbol shape (the szSymbol switch runs per part, maptheme.js
|
|
9071
|
+
// 21995-22060) — `symbols` names them per class like
|
|
9072
|
+
// _resolveSymbolShape; circles when the name isn't one GL draws
|
|
9073
|
+
const shape = shapes ? normalizeSymbolShape(shapes[Math.min(p.i, shapes.length - 1)]) : 'circle';
|
|
9074
|
+
const px = cx + offsets[i].x * fitScale, py = cy + offsets[i].y * fitScale;
|
|
8694
9075
|
ctx.beginPath();
|
|
8695
|
-
ctx.arc(
|
|
9076
|
+
if (shape === 'circle') ctx.arc(px, py, radii[i] * fitScale, 0, Math.PI * 2);
|
|
9077
|
+
else drawSymbolPath(ctx, shape, px, py, radii[i] * fitScale);
|
|
9078
|
+
ctx.closePath();
|
|
8696
9079
|
ctx.fillStyle = `rgb(${colors[p.i].join(',')})`;
|
|
8697
9080
|
ctx.globalAlpha = 0.9; ctx.fill(); ctx.globalAlpha = 1;
|
|
8698
9081
|
ctx.lineWidth = 1; ctx.strokeStyle = '#fff'; ctx.stroke();
|
|
@@ -8742,14 +9125,22 @@
|
|
|
8742
9125
|
ctx.beginPath();
|
|
8743
9126
|
drawSymbolPath(ctx, shape, c, c, r);
|
|
8744
9127
|
ctx.closePath();
|
|
9128
|
+
// flat's "empty" (maptheme.js 21629-21631): the circle geometry, but
|
|
9129
|
+
// a fully transparent fill and a gray outline (fill-opacity:0,
|
|
9130
|
+
// stroke #888888) — a shapeless spot marker
|
|
8745
9131
|
ctx.fillStyle = `rgb(${colorRgb.join(',')})`;
|
|
8746
|
-
ctx.globalAlpha = opacity;
|
|
9132
|
+
ctx.globalAlpha = shape === 'empty' ? 0 : opacity;
|
|
8747
9133
|
ctx.fill();
|
|
8748
9134
|
if (borderWidth > 0 && borderColorRgb) {
|
|
8749
9135
|
ctx.globalAlpha = 1;
|
|
8750
9136
|
ctx.strokeStyle = `rgb(${borderColorRgb.join(',')})`;
|
|
8751
9137
|
ctx.lineWidth = borderWidth;
|
|
8752
9138
|
ctx.stroke();
|
|
9139
|
+
} else if (shape === 'empty') {
|
|
9140
|
+
ctx.globalAlpha = 1;
|
|
9141
|
+
ctx.strokeStyle = '#888888';
|
|
9142
|
+
ctx.lineWidth = 1;
|
|
9143
|
+
ctx.stroke();
|
|
8753
9144
|
}
|
|
8754
9145
|
const icon = { url: canvas.toDataURL(), width: size, height: size, anchorX: size / 2, anchorY: size / 2, id: key };
|
|
8755
9146
|
return this._cacheIcon(key, icon);
|
|
@@ -9026,7 +9417,8 @@
|
|
|
9026
9417
|
_formatTooltipValue(value) {
|
|
9027
9418
|
const num = Number(value);
|
|
9028
9419
|
if (isNaN(num)) return String(value);
|
|
9029
|
-
const
|
|
9420
|
+
const vd = this.style.valuedecimals;
|
|
9421
|
+
const rawDecimals = vd == null || vd === '' ? NaN : Number(vd);
|
|
9030
9422
|
const decimals = isNaN(rawDecimals) ? 2 : rawDecimals;
|
|
9031
9423
|
// flat's __formatValue (mapscript.js 6268ff) rounds twice: first to
|
|
9032
9424
|
// one decimal more (toFixed), then to the precision (Math.round) —
|
|
@@ -9080,8 +9472,12 @@
|
|
|
9080
9472
|
buildDeckLayers(zoom, bbox, liveZoom = zoom, globeCenter = null) {
|
|
9081
9473
|
this._lastZoom = zoom;
|
|
9082
9474
|
this._lastBbox = bbox;
|
|
9083
|
-
|
|
9084
|
-
|
|
9475
|
+
// the live USER-chart SVG pane is not part of the deck layers — an
|
|
9476
|
+
// early return here must also unmount its groups, or the pane keeps
|
|
9477
|
+
// showing the last drawing after the theme went out of scale (or was
|
|
9478
|
+
// hidden via setThemeVisible)
|
|
9479
|
+
if (this._hidden) { this._clearUserChartPane(); return []; }
|
|
9480
|
+
if (!this.flags.has('FEATURE') && !this.flags.has('FEATURES') && chartHiddenByScale(this.style, liveZoom)) { this._clearUserChartPane(); return []; }
|
|
9085
9481
|
// DOT is checked FIRST, matching the real engine's own precedence
|
|
9086
9482
|
// (its DOT branch is a loop-level fast-path that bypasses the whole
|
|
9087
9483
|
// drawChart/modifier machinery entirely — a layer flagged DOT never
|
|
@@ -9135,7 +9531,16 @@
|
|
|
9135
9531
|
|
|
9136
9532
|
_buildFeaturesLayers(zoom, bbox) {
|
|
9137
9533
|
const cs = this.style.colorscheme;
|
|
9138
|
-
|
|
9534
|
+
// a generated scheme's first entry is the class COUNT, not a color —
|
|
9535
|
+
// flat colors an unclassed theme with the sweep's FIRST generated
|
|
9536
|
+
// color (maptheme colorScheme[0] after realize generates the sweep),
|
|
9537
|
+
// never the bare count string (a near-white dot for "5","white","blue"
|
|
9538
|
+
// — the NYC page's station base)
|
|
9539
|
+
let raw = Array.isArray(cs) ? cs[0] : cs;
|
|
9540
|
+
if (Array.isArray(cs) && isGeneratedColorScheme(cs)) {
|
|
9541
|
+
const sweep = flatColorSweep(cs[1], cs[2], Number(cs[0]) || 5, cs[3], cs[4]);
|
|
9542
|
+
if (sweep && sweep.length) raw = sweep[0];
|
|
9543
|
+
}
|
|
9139
9544
|
const filled = raw !== 'none';
|
|
9140
9545
|
// FEATURE|CATEGORICAL: polygons coloured by their category (flat colours
|
|
9141
9546
|
// each shape by its class), not one flat fill
|
|
@@ -9164,7 +9569,7 @@
|
|
|
9164
9569
|
getFillColor: byCategory
|
|
9165
9570
|
? f => {
|
|
9166
9571
|
const cat = this._resolveClassIndex(f.properties[this.binding.value]);
|
|
9167
|
-
return [...(cat != null ? this.categoryColorsRgb[cat] : nodataRgb).slice(0, 3), catAlpha];
|
|
9572
|
+
return [...(cat != null ? (this.categoryColorsRgb || [])[cat] || nodataRgb : nodataRgb).slice(0, 3), catAlpha];
|
|
9168
9573
|
}
|
|
9169
9574
|
: filled ? [...hexOrNamedToRgb(raw).slice(0, 3), Math.round(255 * cssColorAlpha(raw) * (styleNum(this.style.fillopacity) || 1))] : [0, 0, 0, 0],
|
|
9170
9575
|
getLineColor: this.style.linecolor
|
|
@@ -9371,7 +9776,7 @@
|
|
|
9371
9776
|
getFillColor: d => {
|
|
9372
9777
|
if (!hasData(d)) return [0, 0, 0, 0];
|
|
9373
9778
|
const rgb = d.properties.composedColor
|
|
9374
|
-
|| (d.properties.cat != null ? this.categoryColorsRgb[d.properties.cat] : null) || fallbackRgb;
|
|
9779
|
+
|| (d.properties.cat != null ? (this.categoryColorsRgb || [])[d.properties.cat] : null) || fallbackRgb;
|
|
9375
9780
|
const evidence = unmarkedEvidence(d);
|
|
9376
9781
|
if (evidence === 'hide') return [0, 0, 0, 0];
|
|
9377
9782
|
if (evidence === 'gray') return [...grayOf(rgb), 90];
|
|
@@ -9386,7 +9791,15 @@
|
|
|
9386
9791
|
// per-feature alpha (baked above) already carries the resolved
|
|
9387
9792
|
// base opacity — a layer-level opacity on TOP of that would
|
|
9388
9793
|
// multiply it a second time.
|
|
9389
|
-
opacity: dopacityActive ? 1 : baseOpacity
|
|
9794
|
+
opacity: dopacityActive ? 1 : baseOpacity,
|
|
9795
|
+
// Same coplanar z-fighting fix as the FEATURE/FEATURES GeoJsonLayer
|
|
9796
|
+
// (see _buildFeaturesLayers' own comment): under globe projection
|
|
9797
|
+
// the interleaved polygon surface and MapLibre's own sphere mesh
|
|
9798
|
+
// compete for the same depth-buffer real estate and the fill comes
|
|
9799
|
+
// out moiré/hatched ("holes"), never under flat Mercator. Draw
|
|
9800
|
+
// order against other polygon layers is unchanged — depthCompare
|
|
9801
|
+
// only settles the fight against the map's own surface.
|
|
9802
|
+
parameters: { depthCompare: 'always' }
|
|
9390
9803
|
})];
|
|
9391
9804
|
}
|
|
9392
9805
|
|
|
@@ -9442,6 +9855,13 @@
|
|
|
9442
9855
|
_isItemPlot() {
|
|
9443
9856
|
return this.flags.has('PLOT') && !this.flags.has('GRIDSIZE') && String(this.binding.value || '').includes('|');
|
|
9444
9857
|
}
|
|
9858
|
+
// a per-item SEQUENCE chart: CHART|SYMBOL|SEQUENCE, several value
|
|
9859
|
+
// fields per item, one part per field (no CATEGORICAL, no AGGREGATE)
|
|
9860
|
+
_isMultiFieldSequence() {
|
|
9861
|
+
return this.flags.has('CHART') && this.flags.has('SYMBOL') && flatFlag(this.flags, 'SEQUENCE')
|
|
9862
|
+
&& !this.flags.has('CATEGORICAL') && !this.flags.has('AGGREGATE') && !this.flags.has('PLOT')
|
|
9863
|
+
&& String(this.binding.value || '').includes('|');
|
|
9864
|
+
}
|
|
9445
9865
|
_itemPlotFields() {
|
|
9446
9866
|
return String(this.binding.value || '').split('|');
|
|
9447
9867
|
}
|
|
@@ -9952,6 +10372,11 @@
|
|
|
9952
10372
|
}
|
|
9953
10373
|
// USER charts (see resolveUserChartFunction): one icon per chart, drawn
|
|
9954
10374
|
// by the page's function; null → no such function (drawn as bubbles)
|
|
10375
|
+
_clearUserChartPane() {
|
|
10376
|
+
if (this._chartPaneGroup) this._chartPaneGroup.remove();
|
|
10377
|
+
this._chartPaneGroup = null;
|
|
10378
|
+
this._chartItems = null;
|
|
10379
|
+
}
|
|
9955
10380
|
_buildUserChartLayers(combined, liveZoom, sizeValueOf) {
|
|
9956
10381
|
const name = String(this.style.userdraw || '');
|
|
9957
10382
|
const draw = resolveUserChartFunction(name);
|
|
@@ -9987,9 +10412,17 @@
|
|
|
9987
10412
|
// radius of it (see USER_CHART_MAX_SIZE)
|
|
9988
10413
|
const pxPerUnit = NORMAL_RADIUS_PX * objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1) / USER_CHART_MAX_SIZE;
|
|
9989
10414
|
const flag = theme.szFlag;
|
|
9990
|
-
const
|
|
9991
|
-
|
|
9992
|
-
|
|
10415
|
+
const chartHost = _chartHosts.get(this.mapOptions);
|
|
10416
|
+
if (!chartHost) {
|
|
10417
|
+
if (!this._userChartWarned) { this._userChartWarned = true; console.warn(`[ixmaps-gl] USER chart "${name}": no chart pane — drawn as bubbles`); }
|
|
10418
|
+
return null;
|
|
10419
|
+
}
|
|
10420
|
+
// this view's charts, mounted as live <g> elements in the pane (see
|
|
10421
|
+
// chartHostFor) — flat's own SVG rendering: no atlas, no raster, no
|
|
10422
|
+
// async image decode, the whole drawing is synchronous
|
|
10423
|
+
const items = [];
|
|
10424
|
+
const defsSeen = new Set();
|
|
10425
|
+
let html = '';
|
|
9993
10426
|
for (const d of combined) {
|
|
9994
10427
|
const value = valueOf(d);
|
|
9995
10428
|
// the item's class (range classes set it for groups too — a negative
|
|
@@ -9997,133 +10430,69 @@
|
|
|
9997
10430
|
const cat = d.properties.cat != null ? d.properties.cat : (d.properties.counts ? dominant(d.properties.counts) : 0);
|
|
9998
10431
|
const radiusPx = valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue);
|
|
9999
10432
|
const size = radiusPx / pxPerUnit;
|
|
10000
|
-
const
|
|
10001
|
-
if (!
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
|
|
10010
|
-
|
|
10011
|
-
|
|
10012
|
-
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
10016
|
-
|
|
10017
|
-
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
this.
|
|
10433
|
+
const chart = this._userChartSvg(draw, name, theme, flag, d, value, cat, size, sizeValueOf(d));
|
|
10434
|
+
if (!chart) continue;
|
|
10435
|
+
html += chart.markup;
|
|
10436
|
+
// the chart's own defs (gradients) go to the pane once per id —
|
|
10437
|
+
// several charts share one (pinnacle's per-color gradient)
|
|
10438
|
+
for (const id of chart.ids) {
|
|
10439
|
+
if (defsSeen.has(id)) continue;
|
|
10440
|
+
defsSeen.add(id);
|
|
10441
|
+
const def = this._userChartGroup.ownerDocument.getElementById(id);
|
|
10442
|
+
if (def) chartHost.defs.appendChild(def.cloneNode(true));
|
|
10443
|
+
}
|
|
10444
|
+
items.push({ d, coords: d.geometry.coordinates, ox: chart.ox, oy: chart.oy, s: pxPerUnit,
|
|
10445
|
+
r: Math.max(chart.vw, chart.vh) * pxPerUnit / 2 });
|
|
10446
|
+
}
|
|
10447
|
+
if (glDebug()) console.info(`[ixmaps-gl debug] ${themeIdOf(this)}: zoom ${mapLibreToFlatZoom(liveZoom).toFixed(2)}, ${combined.length} charts, ${items.length} drawn${this._viewMoving ? ' (moving)' : ''}`);
|
|
10448
|
+
if (!items.length) {
|
|
10449
|
+
if (this._chartPaneGroup) this._chartPaneGroup.remove();
|
|
10450
|
+
this._chartPaneGroup = null;
|
|
10451
|
+
this._chartItems = null;
|
|
10452
|
+
return (this._userChartLastLayers = []);
|
|
10453
|
+
}
|
|
10454
|
+
if (this._chartPaneGroup) this._chartPaneGroup.remove();
|
|
10455
|
+
this._chartPaneGroup = document.createElementNS(SVG_NS, 'g');
|
|
10456
|
+
chartHost.pane.appendChild(this._chartPaneGroup);
|
|
10457
|
+
this._chartPaneGroup.innerHTML = html;
|
|
10458
|
+
const els = this._chartPaneGroup.children;
|
|
10459
|
+
for (let i = 0; i < items.length; i++) items[i].el = els[i];
|
|
10460
|
+
this._chartItems = items;
|
|
10461
|
+
chartHost.runtimes.add(this);
|
|
10462
|
+
chartHost.update();
|
|
10463
|
+
// picking: a transparent circle per chart — the VISIBLE chart is the
|
|
10464
|
+
// live SVG in the pane; this deck layer only serves hover, click and
|
|
10465
|
+
// the tooltips (pickedFeature unwraps the {d, icon} entry shape)
|
|
10466
|
+
this._userChartLastLayers = [new ScatterplotLayer({
|
|
10023
10467
|
id: `ix-bubbles-${this.name}`,
|
|
10024
|
-
data,
|
|
10025
|
-
|
|
10468
|
+
data: items.map(it => ({ d: it.d, icon: true, r: it.r })),
|
|
10469
|
+
pickable: true,
|
|
10026
10470
|
getPosition: e => e.d.geometry.coordinates,
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
|
|
10030
|
-
billboard: true,
|
|
10031
|
-
// no mipmaps: the charts are drawn at their size on screen
|
|
10032
|
-
textureParameters: { minFilter: 'linear', magFilter: 'linear' },
|
|
10471
|
+
getRadius: e => Math.max(2, e.r),
|
|
10472
|
+
radiusUnits: 'pixels', radiusMinPixels: 1,
|
|
10473
|
+
stroked: false, filled: true, getFillColor: [0, 0, 0, 0],
|
|
10033
10474
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
10034
10475
|
})];
|
|
10035
10476
|
return this._userChartLastLayers;
|
|
10036
10477
|
}
|
|
10037
|
-
//
|
|
10038
|
-
//
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
const
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
if (seen.has(icon.id)) continue;
|
|
10047
|
-
seen.add(icon.id);
|
|
10048
|
-
if (x + icon.width + PAD > W) { x = 0; y += rowH + PAD; rowH = 0; }
|
|
10049
|
-
place.push([icon, x, y]);
|
|
10050
|
-
mapping[icon.id] = { x, y, width: icon.width, height: icon.height, anchorX: icon.anchorX, anchorY: icon.anchorY, mask: false };
|
|
10051
|
-
x += icon.width + PAD; rowH = Math.max(rowH, icon.height);
|
|
10052
|
-
}
|
|
10053
|
-
const canvas = document.createElement('canvas');
|
|
10054
|
-
canvas.width = W; canvas.height = Math.max(1, y + rowH);
|
|
10055
|
-
const ctx = canvas.getContext('2d');
|
|
10056
|
-
for (const [icon, px, py] of place) ctx.drawImage(icon.canvas, px, py);
|
|
10057
|
-
this._userChartAtlasCache = { sig, canvas, mapping };
|
|
10058
|
-
return this._userChartAtlasCache;
|
|
10059
|
-
}
|
|
10060
|
-
// user chart images become ready one by one: one redraw once they are
|
|
10061
|
-
_scheduleUserChartRedraw() {
|
|
10062
|
-
if (this._userChartRedrawTimer) return;
|
|
10063
|
-
this._userChartRedrawTimer = setTimeout(() => {
|
|
10064
|
-
this._userChartRedrawTimer = null;
|
|
10065
|
-
if (this._userChartWaiting && this._triggerRedraw) this._triggerRedraw();
|
|
10066
|
-
}, 30);
|
|
10067
|
-
}
|
|
10068
|
-
// The chart is drawn once (its SVG kept in _userChartSvgs) and rasterised
|
|
10069
|
-
// at its size on screen (× devicePixelRatio, in steps of USER_CHART_RASTER
|
|
10070
|
-
// px) onto a canvas (icon.canvas, once the SVG image has decoded): one
|
|
10071
|
-
// image per chart at a fixed 256 px made the texture of a few hundred
|
|
10072
|
-
// charts outgrow the GPU's texture size — WebGL errors, and deck.gl
|
|
10073
|
-
// frames that failed, leaving the map un-updated after a zoom.
|
|
10074
|
-
_userChartIcon(draw, name, theme, flag, d, value, cat, size, nSize, pxPerUnit, maxRasterPx = Infinity) {
|
|
10075
|
-
const base = `user|${name}|${cat}|${Math.round(value * 100) / 100}|${Math.round(size * 2) / 2}|${flag}`;
|
|
10478
|
+
// the chart's SVG, cached by its draw parameters — a failing draw
|
|
10479
|
+
// (undefined) is tried again on the next redraw (null: draws nothing)
|
|
10480
|
+
_userChartSvg(draw, name, theme, flag, d, value, cat, size, nSize) {
|
|
10481
|
+
// the title is part of the markup now (item.szTitle) — items sharing
|
|
10482
|
+
// value/size/class but not a title must not share a cache entry
|
|
10483
|
+
const raw = d.properties.raw || d.properties;
|
|
10484
|
+
const titleField = this.style.titlefield || this.binding.title;
|
|
10485
|
+
const title = titleField && raw && raw[titleField] != null ? String(raw[titleField]) : undefined;
|
|
10486
|
+
const base = `user|${name}|${cat}|${Math.round(value * 100) / 100}|${Math.round(size * 2) / 2}|${flag}|${title || ''}`;
|
|
10076
10487
|
if (!this._userChartSvgs) this._userChartSvgs = new Map();
|
|
10077
|
-
if (!this._userChartRasters) this._userChartRasters = new Map();
|
|
10078
10488
|
let chart = this._userChartSvgs.get(base);
|
|
10079
10489
|
if (chart === undefined) {
|
|
10080
10490
|
chart = this._drawUserChartSvg(draw, name, theme, flag, d, value, cat, size, nSize);
|
|
10081
|
-
if (chart === undefined) return null;
|
|
10491
|
+
if (chart === undefined) return null;
|
|
10082
10492
|
if (this._userChartSvgs.size >= ICON_CACHE_MAX) this._userChartSvgs.delete(this._userChartSvgs.keys().next().value);
|
|
10083
10493
|
this._userChartSvgs.set(base, chart);
|
|
10084
10494
|
}
|
|
10085
|
-
|
|
10086
|
-
const dpr = (global.devicePixelRatio || 1);
|
|
10087
|
-
const want = Math.max(chart.vw, chart.vh) * pxPerUnit * dpr;
|
|
10088
|
-
const fits = USER_CHART_RASTER.filter(r => r <= maxRasterPx);
|
|
10089
|
-
const steps = fits.length ? fits : USER_CHART_RASTER.slice(0, 1);
|
|
10090
|
-
const px = steps.find(r => r >= want) || steps[steps.length - 1];
|
|
10091
|
-
const key = base + '|' + px;
|
|
10092
|
-
const rasters = this._userChartRasters;
|
|
10093
|
-
if (rasters.has(key)) {
|
|
10094
|
-
// least recently used goes first
|
|
10095
|
-
const hit = rasters.get(key);
|
|
10096
|
-
rasters.delete(key); rasters.set(key, hit);
|
|
10097
|
-
return hit;
|
|
10098
|
-
}
|
|
10099
|
-
const s = px / Math.max(chart.vw, chart.vh);
|
|
10100
|
-
const w = Math.max(1, Math.round(chart.vw * s)), h = Math.max(1, Math.round(chart.vh * s));
|
|
10101
|
-
const icon = { canvas: null, width: w, height: h, anchorX: (chart.ox - chart.vx) * s, anchorY: (chart.oy - chart.vy) * s, id: key, unitHeight: chart.vh };
|
|
10102
|
-
// a chart whose image fails is left out (not waited for)
|
|
10103
|
-
const failed = e => {
|
|
10104
|
-
icon.failed = true;
|
|
10105
|
-
if (!this._userChartImageError) console.warn(`[ixmaps-gl] USER chart ${name}: chart image failed`, e);
|
|
10106
|
-
this._userChartImageError = e || true;
|
|
10107
|
-
this._scheduleUserChartRedraw();
|
|
10108
|
-
};
|
|
10109
|
-
// the SVG goes through a canvas: deck.gl blends an SVG image as decoded
|
|
10110
|
-
// (premultiplied), which darkened half-transparent fills (a
|
|
10111
|
-
// fadenegative arrow turned grey)
|
|
10112
|
-
const img = new Image();
|
|
10113
|
-
img.onerror = failed;
|
|
10114
|
-
img.onload = () => {
|
|
10115
|
-
try {
|
|
10116
|
-
const canvas = document.createElement('canvas');
|
|
10117
|
-
canvas.width = w; canvas.height = h;
|
|
10118
|
-
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
|
|
10119
|
-
icon.canvas = canvas;
|
|
10120
|
-
} catch (e) { failed(e); return; }
|
|
10121
|
-
this._scheduleUserChartRedraw();
|
|
10122
|
-
};
|
|
10123
|
-
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(chart.svg.replace('__W__', w).replace('__H__', h));
|
|
10124
|
-
if (rasters.size >= ICON_CACHE_MAX) rasters.delete(rasters.keys().next().value);
|
|
10125
|
-
rasters.set(key, icon);
|
|
10126
|
-
return icon;
|
|
10495
|
+
return chart || null;
|
|
10127
10496
|
}
|
|
10128
10497
|
// the page's chart function draws one chart into the hidden SVG: its
|
|
10129
10498
|
// markup and box (chart units), null when it draws nothing, undefined
|
|
@@ -10134,11 +10503,19 @@
|
|
|
10134
10503
|
const color = this._userChartColor(cat);
|
|
10135
10504
|
const raw = d.properties.raw || d.properties;
|
|
10136
10505
|
const label = this.binding.title && raw && raw[this.binding.title] != null ? String(raw[this.binding.title]) : undefined;
|
|
10506
|
+
// flat's chart title (style.titlefield, maptheme's item.szTitle) —
|
|
10507
|
+
// pinnacleChart draws it below the value text (chart.js 204-216)
|
|
10508
|
+
const titleField = this.style.titlefield || this.binding.title;
|
|
10509
|
+
const title = titleField && raw && raw[titleField] != null ? String(raw[titleField]) : undefined;
|
|
10137
10510
|
let chart = null;
|
|
10138
10511
|
try {
|
|
10139
10512
|
const ret = draw.call(global.ixmaps, document, {
|
|
10140
10513
|
target: g, theme, value, values: [value], size, maxSize: USER_CHART_MAX_SIZE, color, class: cat, flag,
|
|
10141
|
-
|
|
10514
|
+
// flat hands the chart the item's color CLASS object (maptheme's
|
|
10515
|
+
// ColorScheme entry) — pinnacleChart reads ccolor.lowColor for its
|
|
10516
|
+
// VALUES label fill (usercharts/d3/chart.js 199)
|
|
10517
|
+
ccolor: { szColor: color, lowColor: color, highColor: color },
|
|
10518
|
+
item: { szColor: color, szLabel: label, szTitle: title, nCount: d.properties.point_count || 1, nSize }, dbRecord: raw
|
|
10142
10519
|
});
|
|
10143
10520
|
const bb = ret ? g.getBBox() : null;
|
|
10144
10521
|
if (bb && bb.width > 0 && bb.height > 0) {
|
|
@@ -10150,7 +10527,7 @@
|
|
|
10150
10527
|
const vx = bb.x - pad, vy = bb.y - pad, vw = bb.width + 2 * pad, vh = bb.height + 2 * pad;
|
|
10151
10528
|
// the chart's origin (0,0 — or the point flat moves it to, ret) sits at the map position
|
|
10152
10529
|
const ox = (ret && ret.x) || 0, oy = (ret && ret.y) || 0;
|
|
10153
|
-
chart = { vx, vy, vw, vh, ox, oy,
|
|
10530
|
+
chart = { vx, vy, vw, vh, ox, oy, markup, ids: [...ids],
|
|
10154
10531
|
svg: `<svg xmlns="${SVG_NS}" width="__W__" height="__H__" viewBox="${vx} ${vy} ${vw} ${vh}"><defs>${defs}</defs>${markup}</svg>` };
|
|
10155
10532
|
}
|
|
10156
10533
|
} catch (e) {
|
|
@@ -10607,7 +10984,7 @@
|
|
|
10607
10984
|
if (userLayers) return layers.concat(userLayers);
|
|
10608
10985
|
}
|
|
10609
10986
|
if (this.flags.has('LABEL')) return layers.concat(this._buildLabelChartLayers(combined, zoom, liveZoom, sizeValueOf));
|
|
10610
|
-
if (flatFlag(this.flags, 'SEQUENCE') && this.flags.has('CATEGORICAL') && this.flags.has('AGGREGATE')) {
|
|
10987
|
+
if ((flatFlag(this.flags, 'SEQUENCE') && this.flags.has('CATEGORICAL') && this.flags.has('AGGREGATE')) || (this._multiFields && this._isMultiFieldSequence())) {
|
|
10611
10988
|
return layers.concat(this._buildSequenceChartLayers(combined, zoom, liveZoom));
|
|
10612
10989
|
}
|
|
10613
10990
|
const fillOpacity = styleNum(this.style.fillopacity) || 0.85;
|
|
@@ -10645,7 +11022,7 @@
|
|
|
10645
11022
|
id: `ix-symbolshadow-${this.name}-g${this._iconGeneration}`,
|
|
10646
11023
|
data: combined, pickable: false,
|
|
10647
11024
|
getPosition: d => d.geometry.coordinates,
|
|
10648
|
-
getIcon: d => this._getShadowIcon(radiusOf(d), blurPx, d.properties.counts ? 'circle' : this._resolveSymbolShape(d.properties)),
|
|
11025
|
+
getIcon: d => this._getShadowIcon(radiusOf(d), blurPx, d.properties.counts && !Array.isArray(this.style.symbols) ? 'circle' : this._resolveSymbolShape(d.properties)),
|
|
10649
11026
|
getSize: d => radiusOf(d) * 3, // disc = 2/3 of the icon → its diameter is 2r
|
|
10650
11027
|
getPixelOffset: alignOf.active ? d => { const o = alignOf(d); return [o[0] + dx, o[1] + dy]; } : [dx, dy],
|
|
10651
11028
|
getColor: d => {
|
|
@@ -10670,7 +11047,7 @@
|
|
|
10670
11047
|
id: `ix-glow-${this.name}-g${this._iconGeneration}`,
|
|
10671
11048
|
data: combined, pickable: false,
|
|
10672
11049
|
getPosition: d => d.geometry.coordinates,
|
|
10673
|
-
getIcon: d => this._getGlowIcon(this.categoryColorsRgb[d.properties.counts ? dominant(this._groupIsolation(d) ? d.properties.counts.map((c, i) => (this._markedClasses.has(i) ? c : 0)) : d.properties.counts) : d.properties.cat]),
|
|
11050
|
+
getIcon: d => this._getGlowIcon((this.categoryColorsRgb || [])[d.properties.counts ? dominant(this._groupIsolation(d) ? d.properties.counts.map((c, i) => (this._markedClasses.has(i) ? c : 0)) : d.properties.counts) : d.properties.cat]),
|
|
10674
11051
|
getSize: d => valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue) * (d.properties.counts ? 9 : 11),
|
|
10675
11052
|
getColor: d => [255, 255, 255, this._iconAlpha(d)],
|
|
10676
11053
|
...(alignOf.active ? { getPixelOffset: alignOf } : {}),
|
|
@@ -10717,15 +11094,29 @@
|
|
|
10717
11094
|
// Box and title are removed outside boxupper/boxlower.
|
|
10718
11095
|
// Flat draws box and title in the chart group, which is scaled by
|
|
10719
11096
|
// style.scale: the units below are u · scale. Its size measure W is
|
|
10720
|
-
// the
|
|
10721
|
-
//
|
|
10722
|
-
//
|
|
11097
|
+
// the width of the drawn chart (flat's getBox of the chart group,
|
|
11098
|
+
// 18785): the symbol (with MULTIQUAD the whole grid at the position,
|
|
11099
|
+
// the box enclosing all of it), its value text and its label
|
|
11100
|
+
// (chartItemExtent); flat's chart size is 30 units.
|
|
11101
|
+
// DTEXT: the title font follows the chart, max(0.9u, min(W/5,
|
|
11102
|
+
// 14u / scale)) (18815) — measured on the fire areas page's SIF chart
|
|
11103
|
+
// (W 362 units → font 72.35).
|
|
10723
11104
|
const boxShown = flatFlag(this.flags, 'BOX') && !boxHiddenByScale(this.style, liveZoom);
|
|
10724
11105
|
const circular = flatFlag(this.flags, 'CIRCULAR');
|
|
10725
11106
|
const unitPx = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1);
|
|
10726
11107
|
const boxMarginU = styleNum(this.style.boxmargin) || 2;
|
|
10727
11108
|
const titleShown = boxShown && flatFlag(this.flags, 'TITLE') && !!this.binding.title;
|
|
10728
|
-
const
|
|
11109
|
+
const titleScale = styleNum(this.style.textscale) || styleNum(this.style.valuescale) || 1;
|
|
11110
|
+
const titleFontOf = W => (flatFlag(this.flags, 'DTEXT')
|
|
11111
|
+
? Math.max(0.9 * unitPx, Math.min(W / 5, 14 * unitPx / (styleNum(this.style.scale) || 1)))
|
|
11112
|
+
: 5 * unitPx) * titleScale;
|
|
11113
|
+
// the chart's value text and label (see VALUES and LABEL below), shared
|
|
11114
|
+
// with the box: flat's box encloses whatever the chart group draws
|
|
11115
|
+
const valuesShown = this.flags.has('VALUES') && !valuesHiddenByScale(this.style, zoom);
|
|
11116
|
+
const valueTextOf = valuesShown ? this._chartValueTextFn() : null;
|
|
11117
|
+
const labelShown = !!this.style.labelfield && !flatFlag(this.flags, 'NORMSIZE') && !flatFlag(this.flags, 'ZOOM')
|
|
11118
|
+
&& !labelHiddenByScale(this.style, zoom);
|
|
11119
|
+
const labelOf = labelShown ? this._chartLabelFn(unitPx) : null;
|
|
10729
11120
|
// flat: fill-opacity (boxopacity || 1) on the raw style value — the
|
|
10730
11121
|
// usual string "0" is transparent; typed here, a given 0 is too
|
|
10731
11122
|
const boxOpacity = isNaN(styleNum(this.style.boxopacity)) ? 1 : styleNum(this.style.boxopacity);
|
|
@@ -10736,9 +11127,13 @@
|
|
|
10736
11127
|
const r = valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue);
|
|
10737
11128
|
const ext = multi ? multi.extent.get(d) : [0, 0, 0, 0];
|
|
10738
11129
|
const chart = [ext[0] - r, ext[1] - r, ext[2] + r, ext[3] + r];
|
|
11130
|
+
const grow = e => { if (!e) return; chart[0] = Math.min(chart[0], e[0]); chart[1] = Math.min(chart[1], e[1]); chart[2] = Math.max(chart[2], e[2]); chart[3] = Math.max(chart[3], e[3]); };
|
|
11131
|
+
if (valueTextOf && !d.properties.counts) { const v = valueTextOf(d, r); if (v) grow(v.extent); }
|
|
11132
|
+
if (labelOf) { const l = labelOf(d, r); if (l) grow(l.extent); }
|
|
10739
11133
|
const w = chart[2] - chart[0];
|
|
10740
11134
|
const margin = Math.min(2 * boxMarginU * unitPx, boxMarginU * w / 30);
|
|
10741
11135
|
const boxR = circular ? r * 1.1 + margin : r;
|
|
11136
|
+
const titleFontPx = titleFontOf(w);
|
|
10742
11137
|
let title = null;
|
|
10743
11138
|
if (titleShown) {
|
|
10744
11139
|
const text = this._itemTitle(d.properties);
|
|
@@ -10754,7 +11149,7 @@
|
|
|
10754
11149
|
const anchor = circular ? 'middle' : (alignRight ? 'end' : 'start');
|
|
10755
11150
|
const x0 = anchor === 'middle' ? x - width / 2 : anchor === 'end' ? x - width : x;
|
|
10756
11151
|
// arial: ascent 0.905, descent 0.212 of the font size
|
|
10757
|
-
title = { text: lines.join('\n'), x, anchor, top: base - 0.8 * f,
|
|
11152
|
+
title = { text: lines.join('\n'), x, anchor, top: base - 0.8 * f, f,
|
|
10758
11153
|
extent: [x0, base - 0.905 * f, x0 + width, base + (lines.length - 1) * f + 0.212 * f] };
|
|
10759
11154
|
}
|
|
10760
11155
|
}
|
|
@@ -10859,8 +11254,8 @@
|
|
|
10859
11254
|
data: combined, pickable: true,
|
|
10860
11255
|
getPosition: d => d.geometry.coordinates,
|
|
10861
11256
|
getIcon: d => d.properties.counts
|
|
10862
|
-
? this._buildBubbleIcon(d.properties.counts, this.categoryColorsRgb, this._groupIsolation(d))
|
|
10863
|
-
: this._buildSingleIcon(this.categoryColorsRgb[d.properties.cat], fillOpacity, this._resolveSymbolShape(d.properties), singleBorderColorRgb, singleBorderWidthPx),
|
|
11257
|
+
? this._buildBubbleIcon(d.properties.counts, this.categoryColorsRgb, this._groupIsolation(d), Array.isArray(this.style.symbols) ? this.style.symbols : null)
|
|
11258
|
+
: this._buildSingleIcon((this.categoryColorsRgb || [])[d.properties.cat], fillOpacity, this._resolveSymbolShape(d.properties), singleBorderColorRgb, singleBorderWidthPx),
|
|
10864
11259
|
getSize: d => valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue) * 2,
|
|
10865
11260
|
// group icons bake a fixed 0.9 alpha; an explicit fillopacity scales it
|
|
10866
11261
|
// so a near-transparent theme doesn't show opaque 1-px-cell clusters
|
|
@@ -10878,7 +11273,7 @@
|
|
|
10878
11273
|
data: titleData, pickable: false,
|
|
10879
11274
|
getPosition: b => b.d.geometry.coordinates,
|
|
10880
11275
|
getText: b => b.title.text,
|
|
10881
|
-
getSize:
|
|
11276
|
+
getSize: b => b.title.f,
|
|
10882
11277
|
sizeUnits: 'pixels',
|
|
10883
11278
|
fontFamily: 'arial',
|
|
10884
11279
|
characterSet: 'auto',
|
|
@@ -10903,18 +11298,20 @@
|
|
|
10903
11298
|
// computed font size would be sub-pixel are dropped rather than
|
|
10904
11299
|
// rendered at getSize: 0, matching the real engine's own "too
|
|
10905
11300
|
// small to bother" gate.
|
|
10906
|
-
if (
|
|
11301
|
+
if (valuesShown) {
|
|
10907
11302
|
const valueScale = styleNum(this.style.valuescale) || 1;
|
|
10908
11303
|
const textOpts = this._valueTextOpts();
|
|
11304
|
+
const pointValueOf = valueTextOf || this._chartValueTextFn();
|
|
10909
11305
|
|
|
10910
11306
|
const pointLabels = individual.reduce((out, d) => {
|
|
10911
11307
|
const radius = valueRadius(d.properties.value, liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue);
|
|
10912
|
-
// flat's VALUES label prints the value (nValuesA), not the size
|
|
10913
|
-
|
|
10914
|
-
|
|
10915
|
-
const fontSize = valuesFontSizePx(radius, text, valueScale);
|
|
11308
|
+
// flat's VALUES label prints the value (nValuesA), not the size;
|
|
11309
|
+
// its font by flat's chart branch (_chartValueTextFn)
|
|
11310
|
+
const { text, fontSize, bold, baseline } = pointValueOf(d, radius);
|
|
10916
11311
|
if (fontSize > VALUES_MIN_FONT_PX) {
|
|
10917
|
-
|
|
11312
|
+
const o = alignOf(d);
|
|
11313
|
+
out.push({ geometry: d.geometry, text, fontSize, bold, color: resolveTextColor(this.style, contrastTextColor((this.categoryColorsRgb || [])[d.properties.cat])),
|
|
11314
|
+
pixelOffset: baseline ? [o[0], o[1] + baseline - 0.35 * fontSize] : o });
|
|
10918
11315
|
}
|
|
10919
11316
|
return out;
|
|
10920
11317
|
}, []);
|
|
@@ -10945,7 +11342,7 @@
|
|
|
10945
11342
|
if (fontSize > VALUES_MIN_FONT_PX) {
|
|
10946
11343
|
out.push({
|
|
10947
11344
|
geometry: d.geometry, text, fontSize,
|
|
10948
|
-
color: resolveTextColor(this.style, contrastTextColor(this.categoryColorsRgb[p.i])),
|
|
11345
|
+
color: resolveTextColor(this.style, contrastTextColor((this.categoryColorsRgb || [])[p.i])),
|
|
10949
11346
|
pixelOffset: [offsets[i].x * fitScale * pxPerCanvasUnit + alignOf(d)[0], offsets[i].y * fitScale * pxPerCanvasUnit + alignOf(d)[1]]
|
|
10950
11347
|
});
|
|
10951
11348
|
}
|
|
@@ -10965,10 +11362,55 @@
|
|
|
10965
11362
|
getTextAnchor: 'middle',
|
|
10966
11363
|
getAlignmentBaseline: 'center'
|
|
10967
11364
|
};
|
|
10968
|
-
|
|
11365
|
+
// flat's SYMBOL branch writes its values in normal weight
|
|
11366
|
+
if (pointLabels.length) layers.push(new TextLayer({ id: `ix-points-values-${this.name}`, data: pointLabels, getPixelOffset: d => d.pixelOffset, ...textLayerCommonProps,
|
|
11367
|
+
fontWeight: pointLabels[0].bold === false ? 'normal' : 'bold' }));
|
|
10969
11368
|
if (groupLabels.length) layers.push(new TextLayer({ id: `ix-cluster-values-${this.name}`, data: groupLabels, getPixelOffset: d => d.pixelOffset, ...textLayerCommonProps }));
|
|
10970
11369
|
}
|
|
10971
11370
|
|
|
11371
|
+
// LABEL: flat's chart label (maptheme.js 24533-24590) — a small text
|
|
11372
|
+
// below-right of each chart, the .style({labelfield}) record value
|
|
11373
|
+
// (the fire page's labelfield ANNO: the fire year "2 014"), black at
|
|
11374
|
+
// 0.7 opacity on a white 0.4-opacity rounded background box, font
|
|
11375
|
+
// 5·unit·textscale. Gated by labelupper/labellower (flat's own test
|
|
11376
|
+
// compares <= against BOTH), suppressed by NORMSIZE/ZOOM, a numeric
|
|
11377
|
+
// label formatted like a value (valuedecimals) plus labelunits.
|
|
11378
|
+
if (labelShown) {
|
|
11379
|
+
const labelFontPx = 5 * unitPx * (styleNum(this.style.textscale) || 1);
|
|
11380
|
+
if (labelFontPx > VALUES_MIN_FONT_PX) {
|
|
11381
|
+
const labelData = [];
|
|
11382
|
+
for (const d of individual) {
|
|
11383
|
+
if (!d.properties || !d.properties.raw) continue;
|
|
11384
|
+
// _chartLabelFn: text, and its place below-right of the chart
|
|
11385
|
+
const l = labelOf(d, radiusOf(d));
|
|
11386
|
+
if (!l) continue;
|
|
11387
|
+
const o = alignOf(d);
|
|
11388
|
+
// drawn centered: flat's baseline sits 0.35 font below the
|
|
11389
|
+
// middle of its background box (-0.95 .. +0.25 font)
|
|
11390
|
+
labelData.push({ geometry: d.geometry, text: l.text, fontSize: l.fontSize,
|
|
11391
|
+
pixelOffset: [l.offset[0] + o[0], l.offset[1] - 0.35 * l.fontSize + o[1]] });
|
|
11392
|
+
}
|
|
11393
|
+
if (labelData.length) layers.push(new TextLayer({
|
|
11394
|
+
id: `ix-labels-${this.name}`,
|
|
11395
|
+
data: labelData, pickable: false,
|
|
11396
|
+
getPosition: d => d.geometry.coordinates,
|
|
11397
|
+
getText: d => d.text,
|
|
11398
|
+
getSize: d => d.fontSize,
|
|
11399
|
+
getColor: [0, 0, 0, Math.round(0.7 * 255)],
|
|
11400
|
+
sizeUnits: 'pixels',
|
|
11401
|
+
fontFamily: 'arial',
|
|
11402
|
+
characterSet: 'auto',
|
|
11403
|
+
getTextAnchor: 'middle',
|
|
11404
|
+
getAlignmentBaseline: 'center',
|
|
11405
|
+
getPixelOffset: d => d.pixelOffset,
|
|
11406
|
+
background: true,
|
|
11407
|
+
getBackgroundColor: [255, 255, 255, Math.round(0.4 * 255)],
|
|
11408
|
+
backgroundPadding: [labelFontPx * 0.25, labelFontPx * 0.1],
|
|
11409
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
11410
|
+
}));
|
|
11411
|
+
}
|
|
11412
|
+
}
|
|
11413
|
+
|
|
10972
11414
|
return layers;
|
|
10973
11415
|
}
|
|
10974
11416
|
|
|
@@ -11002,12 +11444,25 @@
|
|
|
11002
11444
|
// - VALUES: each part's value (COUNT: its records; valuefield: the class name)
|
|
11003
11445
|
// - align / offsets as the SYMBOL branch: half size the normal radius
|
|
11004
11446
|
// A part's pick object carries its chart's properties (tooltip).
|
|
11447
|
+
// The same for a per-item chart over several value fields
|
|
11448
|
+
// (_isMultiFieldSequence): a part per field, max value the biggest
|
|
11449
|
+
// field value of any record (normalsizevalue overrides), the part
|
|
11450
|
+
// radius value-scaled as above; a colorscheme "none" part is only its
|
|
11451
|
+
// outline. RINGS nests the parts (ringsLayout); CENTERVALUES prints
|
|
11452
|
+
// only the first part's value (flat: nClass 0, maptheme.js 22216).
|
|
11005
11453
|
_buildSequenceChartLayers(charts, zoom, liveZoom) {
|
|
11006
11454
|
const st = this.style;
|
|
11007
|
-
const
|
|
11008
|
-
const
|
|
11455
|
+
const multi = !!this._multiFields && this._isMultiFieldSequence();
|
|
11456
|
+
const seqMax = multi ? (styleNum(st.normalsizevalue) || this._multiFieldPartMax) : this._sequenceMaxValue(zoom);
|
|
11009
11457
|
const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(st.scale) || 1);
|
|
11010
11458
|
const normalR = NORMAL_RADIUS_PX * unit;
|
|
11459
|
+
const sizePow = resolveSizePow(st, this.flags);
|
|
11460
|
+
const partRadius = multi
|
|
11461
|
+
? v => Math.max(VALUE_RADIUS_MIN, normalR * Math.pow(Math.abs(v) / seqMax, 1 / sizePow))
|
|
11462
|
+
: v => valueRadius(v, liveZoom, st, this.mapOptions, this.flags, seqMax);
|
|
11463
|
+
const rings = flatFlag(this.flags, 'RINGS');
|
|
11464
|
+
const centerValues = flatFlag(this.flags, 'CENTERVALUES');
|
|
11465
|
+
const noFill = this._partNoFill || new Set();
|
|
11011
11466
|
const nCats = this.categoryLabels ? this.categoryLabels.length : 1;
|
|
11012
11467
|
const marking = this._markedClasses.size && !this._onMarksChanged;
|
|
11013
11468
|
const gray = marking && String(st.evidence || 'isolate') === 'isolate_gray';
|
|
@@ -11016,11 +11471,13 @@
|
|
|
11016
11471
|
const borderRgb = st.linecolor && styleLineColor(st.linecolor) !== 'none' ? hexOrNamedToRgb(styleLineColor(st.linecolor)) : null;
|
|
11017
11472
|
const lineWidth = styleNum(st.linewidth) || 1;
|
|
11018
11473
|
const symbols = Array.isArray(st.symbols) ? st.symbols : null;
|
|
11019
|
-
const shapeOf = i => { const sh = symbols && (symbols[i] || symbols[0]); return
|
|
11474
|
+
const shapeOf = i => { const sh = symbols && (symbols[i] || symbols[0]); return normalizeSymbolShape(sh); };
|
|
11020
11475
|
const colorOf = i => this.categoryColorsRgb[i] || [128, 128, 128];
|
|
11021
11476
|
const parts = [], glows = [];
|
|
11022
11477
|
for (const d of charts) {
|
|
11023
|
-
|
|
11478
|
+
const counts = d.properties.parts || d.properties.counts;
|
|
11479
|
+
let layout = rings ? ringsLayout(counts, partRadius)
|
|
11480
|
+
: sequenceLayout(counts, this.flags, partRadius, { maxRadius: normalR, maxValue: seqMax, nParts: nCats, rangeScale: styleNum(st.rangescale) || 0 });
|
|
11024
11481
|
if (!layout.length) continue;
|
|
11025
11482
|
// GLOW belongs to the first part (sorted) or to every part, and
|
|
11026
11483
|
// hides with it when its class is not marked
|
|
@@ -11074,7 +11531,7 @@
|
|
|
11074
11531
|
data: parts, pickable: true,
|
|
11075
11532
|
getPosition: d => d.geometry.coordinates,
|
|
11076
11533
|
// the stroke, in the icon raster's pixels (the icon is drawn at 2r)
|
|
11077
|
-
getIcon: d => this._buildSingleIcon(colorOf(d.part.i), fillOpacity, shapeOf(d.part.i), borderRgb,
|
|
11534
|
+
getIcon: d => this._buildSingleIcon(colorOf(d.part.i), noFill.has(d.part.i) ? 0 : fillOpacity, shapeOf(d.part.i), borderRgb,
|
|
11078
11535
|
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),
|
|
11079
11536
|
getSize: d => d.part.r * 2,
|
|
11080
11537
|
getPixelOffset: offsetOf,
|
|
@@ -11087,20 +11544,22 @@
|
|
|
11087
11544
|
// class name — formatted with valuedecimals (else none) plus the unit;
|
|
11088
11545
|
// font min(1, 3.2 / L) · r · valuescale, L the length of the theme's
|
|
11089
11546
|
// biggest value text (szMaxText), normal weight, in valuecolor /
|
|
11090
|
-
// textcolor, else ChartColors.textColor of the part color
|
|
11091
|
-
|
|
11547
|
+
// textcolor, else ChartColors.textColor of the part color.
|
|
11548
|
+
// CENTERVALUES: only on the first part (class 0), its own value
|
|
11549
|
+
if ((this.flags.has('VALUES') || centerValues) && !valuesHiddenByScale(st, zoom)) {
|
|
11092
11550
|
const valueScale = styleNum(st.valuescale) || 1;
|
|
11093
11551
|
const { noBreaks } = this._valueTextOpts();
|
|
11094
11552
|
const units = st.units ? String(st.units) : '';
|
|
11095
11553
|
const unit = units ? (units[0] === '.' ? '' : ' ') + units : '';
|
|
11096
11554
|
const unitText = unit.length <= 5 ? unit : '';
|
|
11097
|
-
const dec =
|
|
11555
|
+
const dec = explicitValueDecimals(st) || 0;
|
|
11098
11556
|
const fmt = v => flatGroupedValue(v, dec, noBreaks) + unitText;
|
|
11099
11557
|
const maxLen = fmt(seqMax).length;
|
|
11100
11558
|
const textOverride = st.valuecolor || st.textcolor;
|
|
11101
11559
|
const counting = flatFlag(this.flags, 'COUNT');
|
|
11102
11560
|
const labels = [];
|
|
11103
11561
|
parts.forEach(d => {
|
|
11562
|
+
if (centerValues && d.part.i !== 0) return;
|
|
11104
11563
|
const rec = this._categoryValueRecord(d.part.i);
|
|
11105
11564
|
const name = rec ? String(rec[st.valuefield]) : null;
|
|
11106
11565
|
const t = counting && d.properties.recordCounts ? d.properties.recordCounts[d.part.i] : d.part.v;
|
|
@@ -11160,6 +11619,69 @@
|
|
|
11160
11619
|
// the class name (the records' own valuefield value) — otherwise none
|
|
11161
11620
|
_categoryValueRecord(i) { return categoryValueRecord(this.style, this.binding, this.flags, this.categoryLabels, i); }
|
|
11162
11621
|
|
|
11622
|
+
// A single chart's VALUES text: (item, its radius px) → { text,
|
|
11623
|
+
// fontSize, extent } — extent [x0, y0, x1, y1] in px around the chart
|
|
11624
|
+
// center (y down), as the box encloses it. The font by flat's branch:
|
|
11625
|
+
// - SYMBOL (maptheme.js 22200-22202, 21705-21708): min(1, 3.2 / L)
|
|
11626
|
+
// (hexagon 2.7) · r · valuescale, L the length of the theme's biggest
|
|
11627
|
+
// value text (szMaxText: the max value formatted, plus the unit up to
|
|
11628
|
+
// 5 characters, 21479) — normal weight, baseline 0.33 font below the
|
|
11629
|
+
// center (fire areas SIF chart: r 60, valuescale 3, "7 242 ha" → 72)
|
|
11630
|
+
// - BUBBLE: valuesFontSizePx, bold, centered
|
|
11631
|
+
_chartValueTextFn() {
|
|
11632
|
+
const valueScale = styleNum(this.style.valuescale) || 1;
|
|
11633
|
+
const textOpts = this._valueTextOpts();
|
|
11634
|
+
const symbol = flatChartBranch(this.flags) === 'symbol';
|
|
11635
|
+
let maxLen = 1;
|
|
11636
|
+
if (symbol) {
|
|
11637
|
+
const vMax = this._valueMax !== undefined ? this._valueMax : (this._valueTextRange || [])[1];
|
|
11638
|
+
if (isFinite(vMax)) maxLen = Math.max(1, flatValueText(null, null, vMax, this.style, this.flags, textOpts).length);
|
|
11639
|
+
}
|
|
11640
|
+
return (d, r) => {
|
|
11641
|
+
const p = d.properties;
|
|
11642
|
+
const text = flatValueText(p.raw, this.binding.title && p.raw ? p.raw[this.binding.title] : undefined,
|
|
11643
|
+
p.classValue !== undefined ? p.classValue : p.value, this.style, this.flags, textOpts);
|
|
11644
|
+
if (!symbol) {
|
|
11645
|
+
const fontSize = valuesFontSizePx(r, text, valueScale);
|
|
11646
|
+
const tw = titleTextWidth(text, fontSize);
|
|
11647
|
+
return { text, fontSize, bold: true, extent: [-tw / 2, -fontSize / 2, tw / 2, fontSize / 2] };
|
|
11648
|
+
}
|
|
11649
|
+
const k = this._resolveSymbolShape(p) === 'hexagon' ? 2.7 : 3.2;
|
|
11650
|
+
const fontSize = Math.min(1, k / maxLen) * r * valueScale;
|
|
11651
|
+
const tw = titleTextWidth(text, fontSize);
|
|
11652
|
+
return { text, fontSize, bold: false, baseline: 0.33 * fontSize,
|
|
11653
|
+
extent: [-tw / 2, (0.33 - 0.905) * fontSize, tw / 2, (0.33 + 0.212) * fontSize] };
|
|
11654
|
+
};
|
|
11655
|
+
}
|
|
11656
|
+
|
|
11657
|
+
// A single chart's LABEL (.style({labelfield}), maptheme.js 24533-24590):
|
|
11658
|
+
// (item, its radius px) → { text, fontSize, offset, extent } or null.
|
|
11659
|
+
// Font 5u · textscale; a number formatted like a value — valuedecimals
|
|
11660
|
+
// when set (flat's nValueDecimals is the raw style string, so "0"
|
|
11661
|
+
// counts), else 2 — plus labelunits. The background box is the wider of
|
|
11662
|
+
// the text and (length + 2) · font / 2, 1.2 font high from 0.95 font
|
|
11663
|
+
// above the baseline; the label sits at (width / 3, font / 3 + the
|
|
11664
|
+
// chart's anchor y: r + 5u). Fire areas page: "2 010" at (116.7, 193.3)
|
|
11665
|
+
// units, box 350 × 120.
|
|
11666
|
+
_chartLabelFn(unitPx) {
|
|
11667
|
+
const f = 5 * unitPx * (styleNum(this.style.textscale) || 1);
|
|
11668
|
+
const dec = explicitValueDecimals(this.style);
|
|
11669
|
+
const { noBreaks } = this._valueTextOpts();
|
|
11670
|
+
const units = String(this.style.labelunits || '');
|
|
11671
|
+
return (d, r) => {
|
|
11672
|
+
const raw = d.properties && d.properties.raw;
|
|
11673
|
+
const v = raw && raw[this.style.labelfield];
|
|
11674
|
+
let text = v == null || v === '' ? '' : String(v);
|
|
11675
|
+
if (!text) return null;
|
|
11676
|
+
const num = parseFloat(text.replace(',', '.'));
|
|
11677
|
+
text = (!isNaN(num) ? flatGroupedValue(num, dec != null ? dec : 2, noBreaks) : text) + units;
|
|
11678
|
+
const width = Math.max(titleTextWidth(text, f), (text.length + 2) * f * 0.5);
|
|
11679
|
+
const x = width / 3, y = f / 3 + r + 5 * unitPx;
|
|
11680
|
+
return { text, fontSize: f, offset: [x, y], width,
|
|
11681
|
+
extent: [x - width / 2, y - 0.95 * f, x + width / 2, y + 0.25 * f] };
|
|
11682
|
+
};
|
|
11683
|
+
}
|
|
11684
|
+
|
|
11163
11685
|
// value-text options (flatValueText): flat's formatValue NOBREAKS for a
|
|
11164
11686
|
// theme of values within 1000-3000, the max value for the decimals
|
|
11165
11687
|
_valueTextOpts() {
|
|
@@ -11350,6 +11872,14 @@
|
|
|
11350
11872
|
// DOWN, confirmed against a real ported page's own comment ("il motore
|
|
11351
11873
|
// disegna il triangolo con la punta in basso" — "the engine draws the
|
|
11352
11874
|
// triangle with the tip at the bottom").
|
|
11875
|
+
// flat's symbol shapes (maptheme.js 21625-21680). Geometry follows flat
|
|
11876
|
+
// where GL didn't have an established shape already (square, diamond,
|
|
11877
|
+
// triangle keep GL's own proportions — pages already shipped with them):
|
|
11878
|
+
// roundrect — rect 1.6r, rounded 0.3r; label — wide flat rect 1.6r×0.8r;
|
|
11879
|
+
// carot — flat's alias of diamond; hexagon — vertices (±r,0),(±r/2,±r·sin60°);
|
|
11880
|
+
// cross — flat's 8-arm path (A=0.4r, B=0.8r); empty — the circle
|
|
11881
|
+
// geometry, but _buildSingleIcon gives it flat's transparent fill +
|
|
11882
|
+
// gray stroke (fill-opacity:0, stroke #888888)
|
|
11353
11883
|
function drawSymbolPath(ctx, shape, cx, cy, r) {
|
|
11354
11884
|
switch (shape) {
|
|
11355
11885
|
case 'square': {
|
|
@@ -11358,6 +11888,7 @@
|
|
|
11358
11888
|
break;
|
|
11359
11889
|
}
|
|
11360
11890
|
case 'diamond':
|
|
11891
|
+
case 'carot': // flat: carot and diamond are the same rotated rect
|
|
11361
11892
|
ctx.moveTo(cx, cy - r);
|
|
11362
11893
|
ctx.lineTo(cx + r, cy);
|
|
11363
11894
|
ctx.lineTo(cx, cy + r);
|
|
@@ -11368,10 +11899,58 @@
|
|
|
11368
11899
|
ctx.lineTo(cx + r, cy - r * 0.6);
|
|
11369
11900
|
ctx.lineTo(cx, cy + r);
|
|
11370
11901
|
break;
|
|
11902
|
+
case 'roundrect': {
|
|
11903
|
+
const s = r * 0.8;
|
|
11904
|
+
if (ctx.roundRect) ctx.roundRect(cx - s, cy - s, s * 2, s * 2, r * 0.3);
|
|
11905
|
+
else ctx.rect(cx - s, cy - s, s * 2, s * 2);
|
|
11906
|
+
break;
|
|
11907
|
+
}
|
|
11908
|
+
case 'label': {
|
|
11909
|
+
const w = r * 0.8, h = r * 0.4;
|
|
11910
|
+
ctx.rect(cx - w, cy - h, w * 2, h * 2);
|
|
11911
|
+
break;
|
|
11912
|
+
}
|
|
11913
|
+
case 'hexagon': {
|
|
11914
|
+
const A = r / 2, B = r * Math.sin(Math.PI / 3);
|
|
11915
|
+
ctx.moveTo(cx - r, cy);
|
|
11916
|
+
ctx.lineTo(cx - A, cy - B);
|
|
11917
|
+
ctx.lineTo(cx + A, cy - B);
|
|
11918
|
+
ctx.lineTo(cx + r, cy);
|
|
11919
|
+
ctx.lineTo(cx + A, cy + B);
|
|
11920
|
+
ctx.lineTo(cx - A, cy + B);
|
|
11921
|
+
break;
|
|
11922
|
+
}
|
|
11923
|
+
case 'cross': {
|
|
11924
|
+
// flat's own path, vertex for vertex: a plus of arm thickness A
|
|
11925
|
+
// reaching to ±(B + A/2) (maptheme.js 21652-21655)
|
|
11926
|
+
const A = r * 0.4, B = r * 0.8, E = B + A / 2;
|
|
11927
|
+
ctx.moveTo(cx, cy - E);
|
|
11928
|
+
ctx.lineTo(cx + A / 2, cy - E);
|
|
11929
|
+
ctx.lineTo(cx + A / 2, cy - A / 2);
|
|
11930
|
+
ctx.lineTo(cx + E, cy - A / 2);
|
|
11931
|
+
ctx.lineTo(cx + E, cy + A / 2);
|
|
11932
|
+
ctx.lineTo(cx + A / 2, cy + A / 2);
|
|
11933
|
+
ctx.lineTo(cx + A / 2, cy + E);
|
|
11934
|
+
ctx.lineTo(cx - A / 2, cy + E);
|
|
11935
|
+
ctx.lineTo(cx - A / 2, cy + A / 2);
|
|
11936
|
+
ctx.lineTo(cx - E, cy + A / 2);
|
|
11937
|
+
ctx.lineTo(cx - E, cy - A / 2);
|
|
11938
|
+
ctx.lineTo(cx - A / 2, cy - A / 2);
|
|
11939
|
+
ctx.lineTo(cx - A / 2, cy - E);
|
|
11940
|
+
break;
|
|
11941
|
+
}
|
|
11942
|
+
case 'empty':
|
|
11371
11943
|
default: // circle
|
|
11372
11944
|
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
11373
11945
|
}
|
|
11374
11946
|
}
|
|
11947
|
+
// the shape names flat's szSymbol switch knows (maptheme.js 21625) —
|
|
11948
|
+
// anything else falls back to circle, never silently dropped
|
|
11949
|
+
const SYMBOL_SHAPE_RE = /^(circle|square|diamond|triangle|roundrect|carot|hexagon|cross|empty|label)$/;
|
|
11950
|
+
function normalizeSymbolShape(name) {
|
|
11951
|
+
const n = String(name || '').toLowerCase();
|
|
11952
|
+
return SYMBOL_SHAPE_RE.test(n) ? n : 'circle';
|
|
11953
|
+
}
|
|
11375
11954
|
|
|
11376
11955
|
// a color's own opacity, 0..1, as SVG paints it in flat: "none" (SVG's
|
|
11377
11956
|
// no paint — a polygon with only its outline, or a class not painted)
|
|
@@ -11506,7 +12085,7 @@
|
|
|
11506
12085
|
// last built map; called while the map is still building, they run once
|
|
11507
12086
|
// it is ready.
|
|
11508
12087
|
const MAP_HANDLE_METHODS = ['replace', 'add', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
11509
|
-
'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible', 'setThemeTimeFrame', 'attribution'];
|
|
12088
|
+
'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'setProjection', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible', 'setThemeTimeFrame', 'attribution'];
|
|
11510
12089
|
const _mapHandles = Object.create(null);
|
|
11511
12090
|
function makeMapHandle(szMap) {
|
|
11512
12091
|
const h = {};
|
|
@@ -12196,7 +12775,7 @@
|
|
|
12196
12775
|
// test-only: lets test/unit/*.test.mjs call pure internals directly (the
|
|
12197
12776
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
12198
12777
|
global.__ixmapsGlInternals = {
|
|
12199
|
-
IXMAPS_GL_VERSION, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout, multiQuadOffsets, pixelOffsetLngLat,
|
|
12778
|
+
IXMAPS_GL_VERSION, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout, ringsLayout, multiQuadOffsets, pixelOffsetLngLat,
|
|
12200
12779
|
normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
12201
12780
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
12202
12781
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|
|
@@ -12206,7 +12785,8 @@
|
|
|
12206
12785
|
flatToMapLibreZoom, mapLibreToFlatZoom, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
|
|
12207
12786
|
fetchLayerData, parseCsvText, dataTableRows, geometryRowsToFeatureCollection, filterFlatValues, applyField100, field100Binding, rangeClassLegendTotals, resolveAggregateValue, classValueSeparate, cellAggregatedValues, oneHot, groupCoLocated, aggregateOnGrid, GridAggregateIndex, flatRangeParts, aggregateField100, valueRadius, itemPlotGeometry, itemAnchor, objectZoomFactor, resolveZoomReference, defaultNormalSizeValue,
|
|
12208
12787
|
applyWhereFilter, joinChartPositions, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
12209
|
-
|
|
12788
|
+
drawSymbolPath, normalizeSymbolShape,
|
|
12789
|
+
flatFormatValue, themeStyleArgs, explicitValueDecimals, legendDecimals,
|
|
12210
12790
|
dataCacheKey, dataCacheDisabled, cachedLayerData, featuresBounds, wantsZoomToExtent, legendIsOn,
|
|
12211
12791
|
};
|
|
12212
12792
|
global.__setFilter = __setFilter;
|