ixmaps-gl 0.2.16 → 0.2.18
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 +3 -3
- package/ixmaps-gl.js +994 -131
- 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.18';
|
|
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
|
}
|
|
@@ -1856,12 +1860,22 @@
|
|
|
1856
1860
|
if (typeof colorscheme === 'string') {
|
|
1857
1861
|
try {
|
|
1858
1862
|
const fn = new Function('return (' + colorscheme + ')')();
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
}
|
|
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');
|
|
1865
1879
|
}
|
|
1866
1880
|
return labels.map((_, i) => FALLBACK_PALETTE[i % FALLBACK_PALETTE.length]);
|
|
1867
1881
|
}
|
|
@@ -1914,6 +1928,11 @@
|
|
|
1914
1928
|
// _prepareDominant) doing its own `.map(hexToRgb)` that would break on
|
|
1915
1929
|
// the diverging sweep's own output.
|
|
1916
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;
|
|
1917
1936
|
const resolved = resolveColorScheme(colorscheme, labels, classes);
|
|
1918
1937
|
return (resolved || labels.map((_, i) => FALLBACK_PALETTE[i % FALLBACK_PALETTE.length]))
|
|
1919
1938
|
.map(c => Array.isArray(c) ? c : (parseCssColor(c) || [130, 130, 130]));
|
|
@@ -2431,7 +2450,10 @@
|
|
|
2431
2450
|
// (flat stores most of these as given and lets arithmetic coerce them;
|
|
2432
2451
|
// its runtime changeThemeStyle applies Number() — the same result for
|
|
2433
2452
|
// every clean value.) A linewidth list is typed element by element.
|
|
2434
|
-
|
|
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',
|
|
2435
2457
|
'sizepow', 'rangescale', 'minvalue', 'maxvalue', 'markersize', 'boxopacity', 'outlierscale', 'valuescale',
|
|
2436
2458
|
'brightness', 'fractionscale', 'dopacityscale', 'dopacitypow', 'gridwidthpx', 'textscale', 'rangecentervalue',
|
|
2437
2459
|
'shadowblur', 'shadowdx', 'shadowdy', 'maxshadow', 'offsetx', 'offsety', 'gridx', 'boxmargin', 'borderwidth',
|
|
@@ -2457,6 +2479,44 @@
|
|
|
2457
2479
|
return typeof v === 'number' ? v : NaN;
|
|
2458
2480
|
}
|
|
2459
2481
|
|
|
2482
|
+
// flat's changeThemeStyle arithmetic (maptheme.js doChangeThemeStyle,
|
|
2483
|
+
// 4962-5334): these numeric keys go through __calcNewValue (4486) — flag
|
|
2484
|
+
// "pow" current^v, "factor" current·v, "add"/"delta" current+v, else v.
|
|
2485
|
+
// The value is the theme's current one, or flat's own fallback where its
|
|
2486
|
+
// code has one (`mapTheme.nBrightness || 1`, …; fillopacity: 1 when the
|
|
2487
|
+
// new value is < 1, else 0)
|
|
2488
|
+
const FLAT_STYLE_CALC_KEYS = {
|
|
2489
|
+
opacity: 0, fillopacity: null, blur: 0, dopacityscale: 0, dopacitypow: 0, brightness: 1, rangescale: 1,
|
|
2490
|
+
normalsizevalue: 0, minvalue: 1, maxvalue: 1, valuescale: 0, linewidth: 0, markersize: 0, gapsize: 0,
|
|
2491
|
+
offsetx: 0, offsety: 0, rotation: 0, minvaluesize: 0, minvaluevalue: 0, clipvaluesize: 0, minsize: 0,
|
|
2492
|
+
minchartsize: 0, maxcharts: 0, sizepow: 0, outlierscale: 0, clipsize: 0, clipframerate: 1,
|
|
2493
|
+
gridwidth: 1, gridmatrix: 1, gridwidthpx: 50, gridoffsetx: 0, gridoffsety: 0
|
|
2494
|
+
};
|
|
2495
|
+
// the new value of style[key] after flat's changeThemeStyle(…, "key:value",
|
|
2496
|
+
// flag), or null when flat leaves the theme as it is. scale is a resize
|
|
2497
|
+
// FACTOR there (__calcFactor, 4502: "factor" v, "add" (current+v)/current,
|
|
2498
|
+
// else v/current; no pow/delta), multiplied into nScale by
|
|
2499
|
+
// MapTheme.resize (26544) — a factor of 0 or NaN is falsy, so the
|
|
2500
|
+
// Themes.execute loop (3326) skips the resize: scale:0 changes nothing.
|
|
2501
|
+
// Other keys: the value string as given
|
|
2502
|
+
function flatChangedStyleValue(key, current, value, flag) {
|
|
2503
|
+
const f = String(flag || '');
|
|
2504
|
+
const cur = Number(Array.isArray(current) ? current[0] : current);
|
|
2505
|
+
const v = Number(value);
|
|
2506
|
+
if (key === 'scale') {
|
|
2507
|
+
const now = Number.isFinite(cur) && cur ? cur : 1;
|
|
2508
|
+
const factor = /factor/.test(f) ? v : /add/.test(f) ? (now + v) / now : v / now;
|
|
2509
|
+
return factor ? now * factor : null;
|
|
2510
|
+
}
|
|
2511
|
+
if (!(key in FLAT_STYLE_CALC_KEYS) || !/pow|factor|add|delta/.test(f)) return value;
|
|
2512
|
+
const fallback = FLAT_STYLE_CALC_KEYS[key] == null ? (v < 1 ? 1 : 0) : FLAT_STYLE_CALC_KEYS[key];
|
|
2513
|
+
const base = Number.isFinite(cur) && cur ? cur : fallback;
|
|
2514
|
+
const n = Number.isFinite(v) ? v : 0;
|
|
2515
|
+
if (/pow/.test(f)) return Math.pow(base, n);
|
|
2516
|
+
if (/factor/.test(f)) return base * n;
|
|
2517
|
+
return base + n;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2460
2520
|
function normalizeTheme(def) {
|
|
2461
2521
|
// real ixmaps-flat (htmlgui.js newTheme) merges .meta() INTO style first
|
|
2462
2522
|
// — meta wins — and only then reads type, bindings and style properties
|
|
@@ -2589,6 +2649,16 @@
|
|
|
2589
2649
|
return String(text).split(' ').map(w => (dict["'" + w + "'"] !== undefined ? String(dict["'" + w + "'"]) : w)).join(' ');
|
|
2590
2650
|
};
|
|
2591
2651
|
}
|
|
2652
|
+
// make the map container the positioning context of what the engine
|
|
2653
|
+
// mounts inside it (splash, pinned tooltip, legends) — only when it is
|
|
2654
|
+
// unpositioned. The RENDERED position decides, never el.style (inline
|
|
2655
|
+
// only): a page's #map_div { position:absolute; top:0; bottom:0 } from a
|
|
2656
|
+
// stylesheet reads as "" there, and overwriting it with relative drops
|
|
2657
|
+
// the top/bottom stretch — the div's height collapses to 0, a blank map
|
|
2658
|
+
// (accidents_app.html from 0.2.15 on)
|
|
2659
|
+
function anchorInside(el) {
|
|
2660
|
+
if (el && getComputedStyle(el).position === 'static') el.style.position = 'relative';
|
|
2661
|
+
}
|
|
2592
2662
|
function showSplash(el, text) {
|
|
2593
2663
|
ensureSplashStyle();
|
|
2594
2664
|
// Checks the actual RENDERED position, not el.style (the inline
|
|
@@ -2602,7 +2672,7 @@
|
|
|
2602
2672
|
// height silently collapsed to 0 — invisible map, invisible splash,
|
|
2603
2673
|
// both zero-height. Only truly static (unpositioned) elements need
|
|
2604
2674
|
// this at all.
|
|
2605
|
-
|
|
2675
|
+
anchorInside(el);
|
|
2606
2676
|
const splash = document.createElement('div');
|
|
2607
2677
|
splash.className = 'ixmaps-splash';
|
|
2608
2678
|
splash.innerHTML = '<div class="ixmaps-splash-mark"><span class="ixmaps-splash-ping"></span><span class="ixmaps-splash-dot"></span></div>' +
|
|
@@ -3144,7 +3214,7 @@
|
|
|
3144
3214
|
// relative pixels, and styling the page's own body (relative) breaks
|
|
3145
3215
|
// any page element absolutely positioned against it (AirBnB's
|
|
3146
3216
|
// #onmap-buttons on a zero-height all-float body flew off-screen)
|
|
3147
|
-
el
|
|
3217
|
+
anchorInside(el);
|
|
3148
3218
|
el.appendChild(pinnedTooltipEl);
|
|
3149
3219
|
}
|
|
3150
3220
|
return { getTooltip, onClick, mount };
|
|
@@ -3341,7 +3411,7 @@
|
|
|
3341
3411
|
// panel's max-height resolves against the MAP's height — the stack's
|
|
3342
3412
|
// former parent anchor collapsed it to 0 on pages whose body has
|
|
3343
3413
|
// only floated children (height 0)
|
|
3344
|
-
el
|
|
3414
|
+
anchorInside(el);
|
|
3345
3415
|
el.appendChild(legendStack);
|
|
3346
3416
|
return legendStack;
|
|
3347
3417
|
};
|
|
@@ -3387,7 +3457,7 @@
|
|
|
3387
3457
|
rt._triggerRedraw = () => { refresh(); };
|
|
3388
3458
|
if (subThemeCapable(rt)) rt._onMarksChanged = () => updateSubTheme(rt);
|
|
3389
3459
|
if (!legendOn || !el.parentElement || !legendApplies(rt)) return;
|
|
3390
|
-
el
|
|
3460
|
+
anchorInside(el);
|
|
3391
3461
|
{
|
|
3392
3462
|
// .type("...|NOLEGEND") — the fourth real legend-related type()
|
|
3393
3463
|
// token: opts a theme OUT of the legend entirely (real engine's
|
|
@@ -3412,28 +3482,36 @@
|
|
|
3412
3482
|
// sync — no separate light/dark branch anywhere else.
|
|
3413
3483
|
// flat's look (flatLegendLook) unless the page picks one with
|
|
3414
3484
|
// this engine's own legendtheme
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
//
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
:
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3485
|
+
// The look is decided again once the basemap style exists (see
|
|
3486
|
+
// rt._relookLegend below): a panel built before the style loads
|
|
3487
|
+
// can only get effectiveMapDark's name-heuristic fallback, which
|
|
3488
|
+
// reads a dark basemap faded to <= 0.5 as light.
|
|
3489
|
+
const lookColors = () => {
|
|
3490
|
+
const opacityOpt = parseFloat(builder._engineOptions.basemapopacity);
|
|
3491
|
+
const legendBackground = builder.mapOptions.legendBackground || builder.mapOptions.legendbackground;
|
|
3492
|
+
const flatLook = flatLegendLook(builder.mapOptions.mapType, isNaN(opacityOpt) ? 1 : opacityOpt, legendBackground);
|
|
3493
|
+
// effective darkness (style background vs page background, see
|
|
3494
|
+
// effectiveMapDark): a dark basemap faded to <= 0.5 over a DARK
|
|
3495
|
+
// page still reads dark — flatLegendLook's name heuristic would
|
|
3496
|
+
// wrongly switch the panel (and the tooltip) to light there
|
|
3497
|
+
const looksDark = typeof builder.__effectiveDark === 'function'
|
|
3498
|
+
? builder.__effectiveDark(isNaN(opacityOpt) ? 1 : opacityOpt, builder.mapOptions.mapType, legendBackground)
|
|
3499
|
+
: flatLook.dark;
|
|
3500
|
+
const isLightLegend = rt.style.legendtheme === 'light' || (rt.style.legendtheme !== 'dark' && !looksDark);
|
|
3501
|
+
const colors = isLightLegend
|
|
3502
|
+
? { bg: 'rgba(255,255,255,0.92)', fg: '#1a1a1a', shadow: '0 2px 10px rgba(0,0,0,0.18)',
|
|
3503
|
+
rowMarked: 'rgba(0,0,0,0.08)', track: 'rgba(0,0,0,0.08)',
|
|
3504
|
+
selectBg: 'rgba(0,0,0,0.04)', selectBorder: 'rgba(0,0,0,0.2)' }
|
|
3505
|
+
: { bg: 'rgba(28,30,34,0.92)', fg: '#eee', shadow: '0 2px 10px rgba(0,0,0,0.45)',
|
|
3506
|
+
rowMarked: 'rgba(255,255,255,0.14)', track: 'rgba(255,255,255,0.08)',
|
|
3507
|
+
selectBg: 'rgba(255,255,255,0.08)', selectBorder: 'rgba(255,255,255,0.25)' };
|
|
3508
|
+
// the panel bg follows the same effective decision (flatLegendLook
|
|
3509
|
+
// would hand the dark look a light bg here: its dark branch never
|
|
3510
|
+
// ran for this map, its light bg would clash with dark rows/text)
|
|
3511
|
+
if (!rt.style.legendtheme) colors.bg = looksDark ? '#111' : flatLook.bg;
|
|
3512
|
+
return colors;
|
|
3513
|
+
};
|
|
3514
|
+
const legendColors = lookColors();
|
|
3437
3515
|
// max-height:66.6% resolves against el's own height (the map
|
|
3438
3516
|
// container, which always has a definite height for the map
|
|
3439
3517
|
// itself to render into) since this panel sits in the stack,
|
|
@@ -3472,6 +3550,17 @@
|
|
|
3472
3550
|
// themselves authored with their own leading space (e.g.
|
|
3473
3551
|
// " MW") in real pages.
|
|
3474
3552
|
const legendUnit = rt.style.legendunits || rt.style.units || '';
|
|
3553
|
+
// a per-item chart over several value fields: the row values as
|
|
3554
|
+
// flat's legend prints them (legend.js 610, 810: __formatValue
|
|
3555
|
+
// with valuedecimals, else 2, "BLANK" — whole numbers without
|
|
3556
|
+
// decimals), a colorscheme "none" part as an outline in linecolor
|
|
3557
|
+
const multiFieldLegend = !!rt._multiFields && rt._isMultiFieldSequence();
|
|
3558
|
+
const legendTotalText = v => (multiFieldLegend
|
|
3559
|
+
? flatFormatValue(v, legendDecimals(rt.style), 'BLANK')
|
|
3560
|
+
: rt._formatTooltipValue(v));
|
|
3561
|
+
const outlineRgb = rt.style.linecolor && styleLineColor(rt.style.linecolor) !== 'none' ? hexOrNamedToRgb(styleLineColor(rt.style.linecolor)) : [128, 128, 128];
|
|
3562
|
+
const legendFill = i => (multiFieldLegend && rt._partNoFill && rt._partNoFill.has(i)
|
|
3563
|
+
? `transparent;box-shadow:inset 0 0 0 1px rgb(${outlineRgb.join(',')})` : null);
|
|
3475
3564
|
// row labels go into innerHTML: the page's own .style({label})
|
|
3476
3565
|
// stays HTML, as in flat; categories read from the data are
|
|
3477
3566
|
// escaped (a value could carry markup)
|
|
@@ -3513,6 +3602,22 @@
|
|
|
3513
3602
|
const globeCenter = proj.type === 'globe' ? map.getCenter() : null;
|
|
3514
3603
|
totals = new Array(rt.categoryLabels.length).fill(0);
|
|
3515
3604
|
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; }
|
|
3605
|
+
// a per-item chart over several value fields: a row per field,
|
|
3606
|
+
// the field's sum over the visible items (flat's NYC status
|
|
3607
|
+
// legend: bikes / empty docks / e-bikes, biggest sum first)
|
|
3608
|
+
if (rt._multiFields && rt._isMultiFieldSequence()) {
|
|
3609
|
+
(rt._activeFeatures || rt.features).forEach(f => {
|
|
3610
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
3611
|
+
if (!c || typeof c[0] !== 'number') return;
|
|
3612
|
+
const [lng, lat] = c;
|
|
3613
|
+
if (lng < bbox[0] || lng > bbox[2] || lat < bbox[1] || lat > bbox[3]) return;
|
|
3614
|
+
if (globeCenter && !isOnVisibleHemisphere(lng, lat, globeCenter)) return;
|
|
3615
|
+
rt._multiFields.forEach((k, i) => { totals[i] += parseFloat(f.properties[k]) || 0; });
|
|
3616
|
+
});
|
|
3617
|
+
maxTotal = Math.max(0, ...totals);
|
|
3618
|
+
order = totals.map((v, i) => i).sort((a, b) => totals[b] - totals[a]);
|
|
3619
|
+
return;
|
|
3620
|
+
}
|
|
3516
3621
|
// _activeFeatures (set by setFacetFilter/clearFacetFilter,
|
|
3517
3622
|
// e.g. the country-select dropdown below) is the facet-
|
|
3518
3623
|
// filtered subset when a filter is active, null otherwise —
|
|
@@ -3645,7 +3750,7 @@
|
|
|
3645
3750
|
for (let line = 0; line < nLines; line++) {
|
|
3646
3751
|
for (let c = 0; c < n; c++) {
|
|
3647
3752
|
const ix = flatFlag(rt.flags, 'INVERT') ? n - c - 1 : c;
|
|
3648
|
-
const rgb = rt.categoryColorsRgb[ix];
|
|
3753
|
+
const rgb = rt.categoryColorsRgb[ix] || [128, 128, 128]; // colorscheme "none": no class colors
|
|
3649
3754
|
const part = rt.partsA[ix];
|
|
3650
3755
|
const title = part ? flatFormatValue(part.min, dec, '') + unit + ' ... ' + flatFormatValue(part.max, dec, '') + unit : '';
|
|
3651
3756
|
const marked = rt._markedClasses.has(ix);
|
|
@@ -3676,8 +3781,8 @@
|
|
|
3676
3781
|
// flat drops the rows without a count once any row has one
|
|
3677
3782
|
// (legend.js 754-760, 787-789: fCountBars && !count)
|
|
3678
3783
|
rowsEl.innerHTML = order.filter(i => !(maxTotal > 0) || totals[i]).map(i => {
|
|
3679
|
-
const rgb = rt.categoryColorsRgb[i];
|
|
3680
|
-
const color = `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;
|
|
3784
|
+
const rgb = rt.categoryColorsRgb[i] || [128, 128, 128]; // colorscheme "none": no class colors
|
|
3785
|
+
const color = legendFill(i) || `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;
|
|
3681
3786
|
const marked = rt._markedClasses.has(i);
|
|
3682
3787
|
const dimmed = rt._markedClasses.size > 0 && !marked;
|
|
3683
3788
|
const rowStyle = 'padding:4px 3px;border-radius:4px;cursor:pointer;opacity:' + (dimmed ? 0.4 : 1) + ';'
|
|
@@ -3704,7 +3809,7 @@
|
|
|
3704
3809
|
+ '<div style="display:flex;align-items:center;gap:6px;">'
|
|
3705
3810
|
+ '<span style="flex:0 0 8px;width:8px;height:8px;border-radius:50%;background:' + color + ';"></span>'
|
|
3706
3811
|
+ '<span style="flex:0 0 auto;width:' + barPx + 'px;height:6px;background:' + color + ';border-radius:3px;"></span>'
|
|
3707
|
-
+ '<span style="flex:0 0 auto;white-space:nowrap;">' +
|
|
3812
|
+
+ '<span style="flex:0 0 auto;white-space:nowrap;">' + legendTotalText(totals[i]) + legendUnit + '</span>'
|
|
3708
3813
|
+ '</div>'
|
|
3709
3814
|
+ '</div>';
|
|
3710
3815
|
}
|
|
@@ -3715,7 +3820,7 @@
|
|
|
3715
3820
|
+ '<span style="flex:1 1 auto;min-width:0;height:6px;background:' + legendColors.track + ';border-radius:3px;overflow:hidden;">'
|
|
3716
3821
|
+ '<span style="display:block;height:100%;width:' + pct + '%;background:' + color + ';border-radius:3px;"></span>'
|
|
3717
3822
|
+ '</span>'
|
|
3718
|
-
+ '<span style="flex:0 0 auto;text-align:right;min-width:60px;">' +
|
|
3823
|
+
+ '<span style="flex:0 0 auto;text-align:right;min-width:60px;">' + legendTotalText(totals[i]) + legendUnit + '</span>'
|
|
3719
3824
|
+ '</div>'
|
|
3720
3825
|
+ '</div>';
|
|
3721
3826
|
}).join('');
|
|
@@ -3827,6 +3932,7 @@
|
|
|
3827
3932
|
// sharing the same data source and calls refresh()), so this
|
|
3828
3933
|
// is UI wiring only, no new filtering logic.
|
|
3829
3934
|
const filterField = rt.style.legendfilter;
|
|
3935
|
+
let filterSelect = null;
|
|
3830
3936
|
if (filterField) {
|
|
3831
3937
|
const facet = engineApi.getFacets(rt.name, [filterField], { bbox: [-180, -85, 180, 85] })[0];
|
|
3832
3938
|
const values = (facet && facet.type === 'textual')
|
|
@@ -3842,8 +3948,27 @@
|
|
|
3842
3948
|
else engineApi.clearFacetFilter(rt.name, filterField);
|
|
3843
3949
|
});
|
|
3844
3950
|
bodyEl.appendChild(filterEl);
|
|
3951
|
+
filterSelect = filterEl;
|
|
3845
3952
|
}
|
|
3846
3953
|
|
|
3954
|
+
// re-apply the look in place once the basemap style is known
|
|
3955
|
+
// (createLegend's 'style.load' listener) — restyled, not rebuilt,
|
|
3956
|
+
// so the filter choice, slider and folding survive
|
|
3957
|
+
rt._relookLegend = () => {
|
|
3958
|
+
const c = lookColors();
|
|
3959
|
+
if (c.bg === legendColors.bg && c.fg === legendColors.fg) return;
|
|
3960
|
+
Object.assign(legendColors, c);
|
|
3961
|
+
panel.style.background = c.bg;
|
|
3962
|
+
panel.style.color = c.fg;
|
|
3963
|
+
if (filterSelect) {
|
|
3964
|
+
filterSelect.style.background = c.selectBg;
|
|
3965
|
+
filterSelect.style.color = c.fg;
|
|
3966
|
+
filterSelect.style.borderColor = c.selectBorder;
|
|
3967
|
+
}
|
|
3968
|
+
if (stack) stack.style.boxShadow = c.shadow;
|
|
3969
|
+
if (!isTextOnly) renderRows();
|
|
3970
|
+
};
|
|
3971
|
+
|
|
3847
3972
|
if (!isTextOnly) { bodyEl.appendChild(rowsScroll); renderRows(); }
|
|
3848
3973
|
|
|
3849
3974
|
if (rt.meta.description) {
|
|
@@ -3900,6 +4025,9 @@
|
|
|
3900
4025
|
}
|
|
3901
4026
|
};
|
|
3902
4027
|
runtimes.forEach(rt => addLegendPanel(rt));
|
|
4028
|
+
// every style (the initial one, setProjectJSON swaps) can change the
|
|
4029
|
+
// effective darkness the panels were built with
|
|
4030
|
+
map.on('style.load', () => runtimes.forEach(rt => { if (rt._legendPanel && rt._relookLegend) rt._relookLegend(); }));
|
|
3903
4031
|
}
|
|
3904
4032
|
return { addLegendPanel, setLegendOption, updateSubTheme, layoutLegends };
|
|
3905
4033
|
}
|
|
@@ -4296,7 +4424,7 @@
|
|
|
4296
4424
|
legendEl.innerHTML = this._legendHtml;
|
|
4297
4425
|
// inside the map container, never the page's body (see the pinned
|
|
4298
4426
|
// tooltip mount) — styling body breaks page-positioned elements
|
|
4299
|
-
el
|
|
4427
|
+
anchorInside(el);
|
|
4300
4428
|
el.appendChild(legendEl);
|
|
4301
4429
|
}
|
|
4302
4430
|
|
|
@@ -4436,6 +4564,33 @@
|
|
|
4436
4564
|
return engineApi;
|
|
4437
4565
|
},
|
|
4438
4566
|
replaceTheme: (id, layerBuilder, flag) => engineApi.replace(id, layerBuilder, flag),
|
|
4567
|
+
// flat's newTheme(title, theme, flag) (htmlgui.js 1540-1570, the
|
|
4568
|
+
// handle's newTheme forwards to it): with "replace" the theme of the
|
|
4569
|
+
// same id goes first, "clearcharts" every CHART theme, "clear" /
|
|
4570
|
+
// "clearall" every theme — LOCKED themes stay (flat's
|
|
4571
|
+
// Themes.removeAll, maptheme.js 3047-3050: the fuel-price page's
|
|
4572
|
+
// nuts2 base) — then the theme is added; "force" removes nothing
|
|
4573
|
+
newTheme: (title, layerBuilder, flag) => {
|
|
4574
|
+
layerBuilder = unwrapLayer(layerBuilder);
|
|
4575
|
+
const f = String(flag || '');
|
|
4576
|
+
_chainedLayers = _chainedLayers.then(() => {
|
|
4577
|
+
if (!/force/i.test(f)) {
|
|
4578
|
+
if (/replace/i.test(f)) {
|
|
4579
|
+
const def = layerBuilder.definition();
|
|
4580
|
+
const id = (def.style && def.style.name) || (def.meta && def.meta.name);
|
|
4581
|
+
if (id) removeThemeById(id);
|
|
4582
|
+
} else if (/clearcharts|clear|clearall/i.test(f)) {
|
|
4583
|
+
const charts = /clearcharts/i.test(f);
|
|
4584
|
+
const doomed = runtimes.filter(rt => !flatFlag(rt.flags, 'LOCKED') && (!charts || rt.flags.has('CHART')));
|
|
4585
|
+
const byName = new Map();
|
|
4586
|
+
doomed.forEach(rt => { if (!byName.has(rt.name)) byName.set(rt.name, []); byName.get(rt.name).push(rt); });
|
|
4587
|
+
byName.forEach((list, name) => removeRuntimes(name, list));
|
|
4588
|
+
}
|
|
4589
|
+
}
|
|
4590
|
+
return engineApi.defineLayer(layerBuilder);
|
|
4591
|
+
}).catch(err => console.error('[ixmaps-gl] map.newTheme():', err));
|
|
4592
|
+
return engineApi;
|
|
4593
|
+
},
|
|
4439
4594
|
remove: (id) => { removeThemeById(id); return engineApi; },
|
|
4440
4595
|
// flat's map.setMapType(id) / setMapTypeId / mapType (htmlgui_flat.js
|
|
4441
4596
|
// mapApi): swap the basemap at runtime. MapLibre's setStyle replaces
|
|
@@ -4557,23 +4712,40 @@
|
|
|
4557
4712
|
// "filter:WHERE ..." value reuses rt.setRuntimeFilter, the SAME
|
|
4558
4713
|
// applyWhereFilter grammar the layer's own load-time .filter()
|
|
4559
4714
|
// already parses — just invoked again at runtime.
|
|
4715
|
+
//
|
|
4716
|
+
// themeId null: every theme but the FEATURE ones (flat's
|
|
4717
|
+
// Themes.changeThemeStyle, maptheme.js 4464: `szFlag.match(/FEATURE/)`
|
|
4718
|
+
// skipped), one redraw for all. Numeric keys follow flat's
|
|
4719
|
+
// "factor"/"add"/"pow" flags and scale its resize factor
|
|
4720
|
+
// (flatChangedStyleValue) — the scale slider's
|
|
4721
|
+
// changeThemeStyle(null, null, 'scale:' + v, 'set').
|
|
4560
4722
|
changeThemeStyle: (themeId, styleKeyValue, action) => {
|
|
4561
|
-
const
|
|
4562
|
-
|
|
4723
|
+
const targets = themeId == null
|
|
4724
|
+
? runtimes.filter(r => ![...r.flags].some(f => /FEATURE/.test(f)))
|
|
4725
|
+
: [findRuntime(themeId)].filter(Boolean);
|
|
4726
|
+
if (!targets.length) return;
|
|
4563
4727
|
const colonIdx = String(styleKeyValue).indexOf(':');
|
|
4564
4728
|
const key = colonIdx === -1 ? String(styleKeyValue) : styleKeyValue.slice(0, colonIdx);
|
|
4565
4729
|
const value = colonIdx === -1 ? '' : styleKeyValue.slice(colonIdx + 1);
|
|
4566
|
-
if (
|
|
4567
|
-
if (key === 'filter') {
|
|
4568
|
-
rt.setRuntimeFilter(action === 'remove' ? '' : value);
|
|
4569
|
-
refresh();
|
|
4570
|
-
} else if (key === 'type') {
|
|
4730
|
+
if (key === 'type') {
|
|
4571
4731
|
console.warn(`[ixmaps-gl] changeThemeStyle: changing the type ("${value}") is not supported`);
|
|
4572
|
-
|
|
4573
|
-
// any other style key, as flat: "set" writes it, "remove" drops it
|
|
4574
|
-
rt.setStyle({ [key]: /remove/.test(action || '') ? undefined : value });
|
|
4575
|
-
refresh();
|
|
4732
|
+
return;
|
|
4576
4733
|
}
|
|
4734
|
+
let changed = false;
|
|
4735
|
+
targets.forEach(rt => {
|
|
4736
|
+
if (validation) validation.style(themeIdOf(rt), { [key]: value });
|
|
4737
|
+
if (key === 'filter') {
|
|
4738
|
+
rt.setRuntimeFilter(action === 'remove' ? '' : value);
|
|
4739
|
+
changed = true;
|
|
4740
|
+
return;
|
|
4741
|
+
}
|
|
4742
|
+
// any other style key, as flat: "set" writes it, "remove" drops it
|
|
4743
|
+
const next = /remove/.test(action || '') ? undefined : flatChangedStyleValue(key, rt.style[key], value, action);
|
|
4744
|
+
if (next === null) return;
|
|
4745
|
+
rt.setStyle({ [key]: next });
|
|
4746
|
+
changed = true;
|
|
4747
|
+
});
|
|
4748
|
+
if (changed) refresh();
|
|
4577
4749
|
},
|
|
4578
4750
|
// flat's ixmaps.setThemeTimeFrame(szId, min, max): show only the
|
|
4579
4751
|
// records whose timefield lies in [min, max) (ms or Date-
|
|
@@ -4663,10 +4835,17 @@
|
|
|
4663
4835
|
.catch(err => console.error('[ixmaps-gl] map.layer():', err));
|
|
4664
4836
|
return engineApi;
|
|
4665
4837
|
}
|
|
4666
|
-
// map.layer(name).data()...define():
|
|
4838
|
+
// map.layer(name).data()...define(): adds a theme on that layer.
|
|
4839
|
+
// NO same-name auto-remove here: flat lets several themes SHARE one
|
|
4840
|
+
// .layer() name (a page's feature base + its chart overlay, the
|
|
4841
|
+
// NYC status page's two "Station" themes — the overlay joins onto
|
|
4842
|
+
// the base's geometry by lookup, removing the base would kill
|
|
4843
|
+
// both). Replacement is by THEME ID (style.name/meta.name), and
|
|
4844
|
+
// happens inside defineFromDefinition instead — a re-defined id
|
|
4845
|
+
// (the page's 10s live refresh) replaces its own previous theme.
|
|
4667
4846
|
const lb = new LayerBuilder(nameOrBuilder);
|
|
4668
4847
|
lb.define = () => {
|
|
4669
|
-
_chainedLayers = _chainedLayers.then(() =>
|
|
4848
|
+
_chainedLayers = _chainedLayers.then(() => engineApi.defineLayer(lb))
|
|
4670
4849
|
.catch(err => console.error('[ixmaps-gl] map.layer().define():', err));
|
|
4671
4850
|
return _chainedLayers;
|
|
4672
4851
|
};
|
|
@@ -4695,8 +4874,22 @@
|
|
|
4695
4874
|
// Removes every runtime whose OWN .layer(name) matches — a theme
|
|
4696
4875
|
// may legitimately be split across more than one runtime sharing
|
|
4697
4876
|
// one name (the CHOROPLETH/FEATURE-donor pair above), so this
|
|
4698
|
-
// removes all of them, not just the first match.
|
|
4699
|
-
|
|
4877
|
+
// removes all of them, not just the first match. GL-PORT COMPAT:
|
|
4878
|
+
// flat addresses a theme by its ID — style.name / meta.name —
|
|
4879
|
+
// as much as by its layer name (htmlgui.js removeTheme), and a
|
|
4880
|
+
// real page's own rebuild loop typically knows only the id it
|
|
4881
|
+
// set via .meta({name}) (the fire page's KDE: removeTheme
|
|
4882
|
+
// ("kde_fires") for the layer "kde_fires_layer"). Layer name
|
|
4883
|
+
// first, then the id — without the fallback every rebuild
|
|
4884
|
+
// removed nothing and stacked another copy of the theme,
|
|
4885
|
+
// compounding opacity (same semantics removeThemeById already
|
|
4886
|
+
// implements for the handle's .remove(id)).
|
|
4887
|
+
removeTheme: (name) => {
|
|
4888
|
+
const byLayer = runtimes.filter(r => r.name === name);
|
|
4889
|
+
if (byLayer.length) return removeRuntimes(name, byLayer);
|
|
4890
|
+
const byId = runtimes.filter(r => (r.style && r.style.name === name) || (r.meta && r.meta.name === name));
|
|
4891
|
+
return byId.length ? removeRuntimes(byId[0].name, byId) : false;
|
|
4892
|
+
},
|
|
4700
4893
|
// flat's refreshTheme (maptheme.js Themes.refreshTheme): the
|
|
4701
4894
|
// theme's data is loaded again — a broker is called again — and
|
|
4702
4895
|
// replaces the old features in place (draw order, legend, style
|
|
@@ -4825,10 +5018,15 @@
|
|
|
4825
5018
|
else map.fitBounds(bounds, { padding: 30, animate: false, maxZoom: 16 });
|
|
4826
5019
|
}
|
|
4827
5020
|
|
|
4828
|
-
|
|
5021
|
+
// quiet: the broker's data fills only this theme — not broadcast to
|
|
5022
|
+
// the other themes of the name (flat's refreshTheme reloads one theme;
|
|
5023
|
+
// setExternalData only releases themes still waiting for the data,
|
|
5024
|
+
// maptheme.js 2570-2591)
|
|
5025
|
+
async function loadNamedTheme(rt, force, quiet = false) {
|
|
4829
5026
|
const spec = normalizeTheme(rt._definition);
|
|
4830
5027
|
const name = String(spec.data.name);
|
|
4831
5028
|
const broker = brokerTheme(spec);
|
|
5029
|
+
if (quiet && broker) _quietNamedData.set(name, (_quietNamedData.get(name) || 0) + 1);
|
|
4832
5030
|
rt._loading = (rt._loading || 0) + 1;
|
|
4833
5031
|
if (broker) { pendingLoads++; rt._brokerLoading = (rt._brokerLoading || 0) + 1; setDataLoading(); }
|
|
4834
5032
|
try {
|
|
@@ -4850,6 +5048,7 @@
|
|
|
4850
5048
|
else console.error(`[ixmaps-gl] theme "${themeIdOf(rt)}": no data —`, err);
|
|
4851
5049
|
} finally {
|
|
4852
5050
|
rt._loading--;
|
|
5051
|
+
if (quiet && broker) { const n = (_quietNamedData.get(name) || 1) - 1; if (n) _quietNamedData.set(name, n); else _quietNamedData.delete(name); }
|
|
4853
5052
|
if (broker) { pendingLoads--; rt._brokerLoading--; setDataLoading(); }
|
|
4854
5053
|
}
|
|
4855
5054
|
}
|
|
@@ -4898,7 +5097,11 @@
|
|
|
4898
5097
|
}
|
|
4899
5098
|
// flat, after a FEATURE theme is drawn (maptheme.js 18947-18962): the
|
|
4900
5099
|
// other themes on its layer are placed again, and a data.query broker
|
|
4901
|
-
// among them is refreshed 500 ms later
|
|
5100
|
+
// among them is refreshed 500 ms later — quietly: its new data fills
|
|
5101
|
+
// that theme only. Broadcast to every theme of the name, a FEATURE
|
|
5102
|
+
// theme sharing the broker's data name (the fire areas page: SIF
|
|
5103
|
+
// polygons and chart both "data_2") was refilled, drawn, refreshed
|
|
5104
|
+
// its dependent again — an endless reload of ~15k records every 3 s
|
|
4902
5105
|
function featureDependents(featureRt) {
|
|
4903
5106
|
runtimes.forEach(dep => {
|
|
4904
5107
|
if (dep === featureRt || dep.flags.has('FEATURE') || dep.flags.has('FEATURES')) return;
|
|
@@ -4906,7 +5109,7 @@
|
|
|
4906
5109
|
const spec = normalizeTheme(dep._definition || {});
|
|
4907
5110
|
if (dep._named && spec.data && spec.data.query) {
|
|
4908
5111
|
clearTimeout(dep._dependentTimer);
|
|
4909
|
-
dep._dependentTimer = setTimeout(() => { if (runtimes.includes(dep)) loadNamedTheme(dep, true); }, 500);
|
|
5112
|
+
dep._dependentTimer = setTimeout(() => { if (runtimes.includes(dep)) loadNamedTheme(dep, true, true); }, 500);
|
|
4910
5113
|
} else if (dep._named && dep._lastTable && !dep._loading) {
|
|
4911
5114
|
fillNamedTheme(dep, spec, dep._lastTable, null);
|
|
4912
5115
|
}
|
|
@@ -4915,10 +5118,18 @@
|
|
|
4915
5118
|
|
|
4916
5119
|
// One theme definition (real ixmaps-flat's shape — from a builder's
|
|
4917
5120
|
// definition() or a project file) → a new runtime on this map. Shared
|
|
4918
|
-
// by defineLayer and loadProject. Purely additive
|
|
5121
|
+
// by defineLayer and loadProject. Purely additive across LAYER names —
|
|
5122
|
+
// several themes legitimately share one (a FEATURE base + its CHART
|
|
5123
|
+
// overlay, the CHOROPLETH/FEATURE-donor pair) — but a re-defined
|
|
5124
|
+
// THEME ID (style.name/meta.name) replaces its own previous theme, as
|
|
5125
|
+
// flat itself does ("defining a theme with the same name replaces it
|
|
5126
|
+
// in place"; a page's live-refresh loop relies on it, e.g. the NYC
|
|
5127
|
+
// status page re-defining its two themes every 10s).
|
|
4919
5128
|
async function defineFromDefinition(def) {
|
|
4920
5129
|
if (validation) validation.definition(def);
|
|
4921
5130
|
const pdef = projectThemeToDefinition(def);
|
|
5131
|
+
const replacedId = (pdef.meta && pdef.meta.name) || (pdef.style && pdef.style.name);
|
|
5132
|
+
if (replacedId) removeThemeById(replacedId);
|
|
4922
5133
|
let spec = normalizeTheme(pdef);
|
|
4923
5134
|
let raw;
|
|
4924
5135
|
const deferred = deferredFeatureLoad(spec, map.getZoom());
|
|
@@ -5093,10 +5304,26 @@
|
|
|
5093
5304
|
// never by size, so resizing the SAME cached icon every frame
|
|
5094
5305
|
// creates no new atlas entries — none of the reclustering risk
|
|
5095
5306
|
// above applies to a pure size change.
|
|
5307
|
+
//
|
|
5308
|
+
// globe: MapLibre shifts getZoom() by log2(cos(center lat)) as the
|
|
5309
|
+
// center moves north/south (the globe keeps its size on screen); the
|
|
5310
|
+
// grid zoom takes that out, so a gridwidthpx grid changes on zoom
|
|
5311
|
+
// only, not on pan (flat's orthographic keeps its scale on rotation)
|
|
5312
|
+
// — rounded so float noise doesn't miss GridAggregateIndex's
|
|
5313
|
+
// per-zoom cache. Measured on the GHS orthographic page: center lat
|
|
5314
|
+
// 17.2° → 47.2° moved getZoom() 2.467 → 1.975, re-binning the
|
|
5315
|
+
// 3 px MEAN cloud (cells changing color) on every pan.
|
|
5316
|
+
function gridZoomOf() {
|
|
5317
|
+
const z = map.getZoom();
|
|
5318
|
+
const proj = typeof map.getProjection === 'function' && map.getProjection();
|
|
5319
|
+
if (!proj || proj.type !== 'globe') return z;
|
|
5320
|
+
const lat = map.getCenter().lat * Math.PI / 180;
|
|
5321
|
+
return Math.round((z - Math.log2(Math.cos(lat))) * 1e6) / 1e6;
|
|
5322
|
+
}
|
|
5096
5323
|
function refreshLayers() {
|
|
5097
5324
|
const liveZoom = map.getZoom();
|
|
5098
5325
|
// frozen only while MapLibre really is zooming (see 'moveend')
|
|
5099
|
-
const zoom = isZooming && map.isZooming() ? stableGridZoom :
|
|
5326
|
+
const zoom = isZooming && map.isZooming() ? stableGridZoom : gridZoomOf();
|
|
5100
5327
|
const viewMoving = map.isMoving();
|
|
5101
5328
|
const bounds = map.getBounds();
|
|
5102
5329
|
const bbox = [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()];
|
|
@@ -5179,11 +5406,11 @@
|
|
|
5179
5406
|
// zoom too) picks up the hide/show within one 150ms tick — still
|
|
5180
5407
|
// effectively instant, without the unthrottled rebuild storm.
|
|
5181
5408
|
let isZooming = false;
|
|
5182
|
-
let stableGridZoom =
|
|
5409
|
+
let stableGridZoom = gridZoomOf();
|
|
5183
5410
|
map.on('zoomstart', () => { isZooming = true; });
|
|
5184
5411
|
map.on('zoomend', () => {
|
|
5185
5412
|
isZooming = false;
|
|
5186
|
-
stableGridZoom =
|
|
5413
|
+
stableGridZoom = gridZoomOf();
|
|
5187
5414
|
// Was: clearTimeout(refreshTimer); refreshLayers(); — a DIRECT,
|
|
5188
5415
|
// un-throttled rebuild on every single zoomend. That was fine
|
|
5189
5416
|
// while every runtime's per-tick cost was cheap (viewport-scoped
|
|
@@ -5233,7 +5460,7 @@
|
|
|
5233
5460
|
// the same arrows at every zoom, while the scale gates (liveZoom)
|
|
5234
5461
|
// still switched.
|
|
5235
5462
|
isZooming = false;
|
|
5236
|
-
stableGridZoom =
|
|
5463
|
+
stableGridZoom = gridZoomOf();
|
|
5237
5464
|
refresh();
|
|
5238
5465
|
});
|
|
5239
5466
|
|
|
@@ -5452,7 +5679,7 @@
|
|
|
5452
5679
|
// as ixmaps.map() does). The builder's own methods with different
|
|
5453
5680
|
// semantics — layer, view, options, require, attribution, on — keep
|
|
5454
5681
|
// theirs and are not overridden.
|
|
5455
|
-
for (const m of ['replace', 'add', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
5682
|
+
for (const m of ['replace', 'add', 'newTheme', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
5456
5683
|
'refreshTheme', 'redrawTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'setProjection', 'resize',
|
|
5457
5684
|
'loadProject', 'project', 'setThemeVisible', 'setThemeTimeFrame', 'show', 'hide', 'getZoom', 'getThemeObj',
|
|
5458
5685
|
'getThemes', 'theme', 'setView']) {
|
|
@@ -5572,8 +5799,11 @@
|
|
|
5572
5799
|
// itself, which stays purely additive for every other caller —
|
|
5573
5800
|
// see its own comment for why (the CHOROPLETH/FEATURE-donor
|
|
5574
5801
|
// pairing needs two runtimes sharing one name to coexist).
|
|
5802
|
+
// NO same-LAYER-name auto-remove (see engineApi.layer's own
|
|
5803
|
+
// comment — several themes legitimately share one layer name);
|
|
5804
|
+
// a re-defined THEME ID replaces its own previous theme inside
|
|
5805
|
+
// defineFromDefinition instead
|
|
5575
5806
|
lb.define = () => {
|
|
5576
|
-
resolvedApi.removeTheme(nameOrBuilder);
|
|
5577
5807
|
return resolvedApi.defineLayer(lb);
|
|
5578
5808
|
};
|
|
5579
5809
|
}
|
|
@@ -5856,6 +6086,41 @@
|
|
|
5856
6086
|
* (flatChartBranch(flags) === 'bubble' ? 0.5 : 1);
|
|
5857
6087
|
return Math.max(VALUE_RADIUS_MIN, Math.min(VALUE_RADIUS_MAX, fixed * zoomFactor));
|
|
5858
6088
|
}
|
|
6089
|
+
// NOSIZE: every chart ONE fixed radius, whatever the item's value;
|
|
6090
|
+
// normalsizevalue doesn't scale it. Which one depends on flat's SYMBOL
|
|
6091
|
+
// sub-branch:
|
|
6092
|
+
// - a CATEGORICAL theme without AGGREGATE/GROUP ("symbol equals
|
|
6093
|
+
// value", maptheme.js 21566-21625, "GR 01.07.2023 redesign size
|
|
6094
|
+
// handling"): the normal radius itself
|
|
6095
|
+
// - every other symbol chart (the per-part ladder, 21829-21896):
|
|
6096
|
+
// SEQUENCE normalX(chart size / 3), 2/3 of it; otherwise
|
|
6097
|
+
// nMaxRadius / 5 — measured on the fire areas page's SIF chart
|
|
6098
|
+
// (CHART|SYMBOL|NOSIZE: r 60 units at normalX(1) 20, i.e. 3 of the
|
|
6099
|
+
// 15-unit normal radius)
|
|
6100
|
+
// The BUBBLE draw keeps its own older NOSIZE value, half of it
|
|
6101
|
+
// (maptheme.js 19839)
|
|
6102
|
+
if (flags && flags.has('NOSIZE') && !flags.has('PLOT') && !flags.has('GRIDSIZE')) {
|
|
6103
|
+
const symbolEqualsValue = flags.has('CATEGORICAL') && !flags.has('AGGREGATE') && !flatFlag(flags, 'GROUP');
|
|
6104
|
+
const fixed = NORMAL_RADIUS_PX * (styleNum(style.scale) || 1)
|
|
6105
|
+
* (flatChartBranch(flags) === 'bubble' ? 0.5
|
|
6106
|
+
: symbolEqualsValue ? 1
|
|
6107
|
+
: flatFlag(flags, 'SEQUENCE') ? 2 / 3 : 1 / 5);
|
|
6108
|
+
return Math.max(VALUE_RADIUS_MIN, Math.min(VALUE_RADIUS_MAX, fixed * zoomFactor));
|
|
6109
|
+
}
|
|
6110
|
+
// Same redesign, the rule around it (maptheme.js 21612-21625) — only in
|
|
6111
|
+
// flat's "symbol equals value" sub-branch, a CATEGORICAL theme without
|
|
6112
|
+
// AGGREGATE/GROUP: the symbol resizes by itemA[a].nSize, which flat only
|
|
6113
|
+
// sets for a theme with a bound szSizeField (maptheme.js 9144-9153), so
|
|
6114
|
+
// WITHOUT a size field every item keeps the DEFAULT normal radius,
|
|
6115
|
+
// whatever its value. Every other symbol chart (the per-part ladder,
|
|
6116
|
+
// 21850-21851) and BUBBLE (19851) scale by the value — below.
|
|
6117
|
+
if (flags && flags.has('SYMBOL') && !flags.has('BUBBLE') && !flags.has('USER')
|
|
6118
|
+
&& !flags.has('FIXSIZE') && !flags.has('PLOT') && !flags.has('GRIDSIZE')
|
|
6119
|
+
&& flags.has('CATEGORICAL') && !flags.has('AGGREGATE') && !flatFlag(flags, 'GROUP')
|
|
6120
|
+
&& !style.sizefield) {
|
|
6121
|
+
const fixed = NORMAL_RADIUS_PX * (styleNum(style.scale) || 1);
|
|
6122
|
+
return Math.max(VALUE_RADIUS_MIN, Math.min(VALUE_RADIUS_MAX, fixed * zoomFactor));
|
|
6123
|
+
}
|
|
5859
6124
|
// Real default (maptheme.js ~line 5059) when the theme sets no
|
|
5860
6125
|
// normalsizevalue: the dataset's own max value on the bound size
|
|
5861
6126
|
// field (maxSizeValue, from LayerRuntime._prepare), not a fixed
|
|
@@ -6168,9 +6433,9 @@
|
|
|
6168
6433
|
|
|
6169
6434
|
// a chart's VALUES text (maptheme.js 20979-21017): an explicit valuefield
|
|
6170
6435
|
// prints that field of the record ($title$: the title; a non-number as
|
|
6171
|
-
// is), else the value; numbers formatted with valuedecimals
|
|
6172
|
-
// 0
|
|
6173
|
-
// else none
|
|
6436
|
+
// is), else the value; numbers formatted with valuedecimals when set
|
|
6437
|
+
// (explicitValueDecimals: a given "0" counts), else 1 decimal below 1 —
|
|
6438
|
+
// or when the max value is at most 1 — else none, plus the unit, which flat stores with a leading space unless
|
|
6174
6439
|
// it starts with "." (9675) and appends only up to 5 characters.
|
|
6175
6440
|
// A CATEGORICAL bubble prints its size value when size is bound to a
|
|
6176
6441
|
// field (its value, see resolveAggregateValue), as flat does; with
|
|
@@ -6184,11 +6449,30 @@
|
|
|
6184
6449
|
if (!field || field !== binding.value || !flags.has('CATEGORICAL') || !labels || labels[i] == null) return null;
|
|
6185
6450
|
return { [field]: labels[i] };
|
|
6186
6451
|
}
|
|
6452
|
+
// style.valuedecimals as flat reads it: nValueDecimals keeps the raw
|
|
6453
|
+
// style string, so an explicit "0" is truthy in flat's `nValueDecimals ||
|
|
6454
|
+
// default` and counts (fire areas page: valuedecimals "0", 0.48 → "1 ha");
|
|
6455
|
+
// null when not set
|
|
6456
|
+
// (valuedecimals is therefore not typed: STYLE_NUMBER_KEYS). Flat's
|
|
6457
|
+
// truthiness: unset, "" and a numeric 0 fall back to the default
|
|
6458
|
+
function explicitValueDecimals(style) {
|
|
6459
|
+
const v = style && style.valuedecimals;
|
|
6460
|
+
if (!v) return null;
|
|
6461
|
+
const n = Number(v);
|
|
6462
|
+
return Number.isFinite(n) ? n : null;
|
|
6463
|
+
}
|
|
6464
|
+
// the legend's decimals (legend.js 610): valuedecimals when defined, else 2
|
|
6465
|
+
function legendDecimals(style) {
|
|
6466
|
+
const v = style && style.valuedecimals;
|
|
6467
|
+
const n = v == null || v === '' ? NaN : Number(v);
|
|
6468
|
+
return Number.isFinite(n) ? n : 2;
|
|
6469
|
+
}
|
|
6187
6470
|
function flatValueText(raw, title, value, style, flags, opts = {}) {
|
|
6188
6471
|
const units = style.units ? String(style.units) : '';
|
|
6189
6472
|
const unit = units ? (units[0] === '.' ? '' : ' ') + units : '';
|
|
6190
6473
|
const unitText = unit.length <= 5 ? unit : '';
|
|
6191
|
-
const
|
|
6474
|
+
const explicitDec = explicitValueDecimals(style);
|
|
6475
|
+
const dec = v => (explicitDec != null ? explicitDec : ((v < 1 || (opts.maxValue != null && opts.maxValue <= 1)) ? 1 : 0));
|
|
6192
6476
|
const signed = (v, text) => {
|
|
6193
6477
|
if (flags && (flags.has('DIFFERENCE') || flags.has('RELATIVE') || flags.has('SIGN'))) {
|
|
6194
6478
|
if (v === 0) return '+/-' + text;
|
|
@@ -6201,7 +6485,7 @@
|
|
|
6201
6485
|
if (field && raw && raw[field] != null && raw[field] !== '') {
|
|
6202
6486
|
if (isNaN(raw[field])) return String(raw[field]);
|
|
6203
6487
|
const v = Number(raw[field]);
|
|
6204
|
-
return signed(v, flatGroupedValue(v,
|
|
6488
|
+
return signed(v, flatGroupedValue(v, explicitDec != null ? explicitDec : (v < 1 ? 1 : 0), opts.noBreaks) + unitText);
|
|
6205
6489
|
}
|
|
6206
6490
|
return signed(value, flatGroupedValue(value, dec(value), opts.noBreaks) + unitText);
|
|
6207
6491
|
}
|
|
@@ -6303,19 +6587,29 @@
|
|
|
6303
6587
|
}
|
|
6304
6588
|
|
|
6305
6589
|
function valuesHiddenByScale(style, zoom) {
|
|
6306
|
-
|
|
6590
|
+
// flat accepts both 'valueupper' and 'valuesupper' (maptheme.js 1710,
|
|
6591
|
+
// "GR 15.03.2017 accept also 'valuesupper'"), same for the lower pair
|
|
6592
|
+
const m = /^1:(\d+(?:\.\d+)?)$/.exec(style.valueupper || style.valuesupper || '');
|
|
6307
6593
|
if (!m) return false;
|
|
6308
6594
|
const upperRatio = parseFloat(m[1]);
|
|
6309
6595
|
const scaleDenominator = scaleDenominatorAt(zoom);
|
|
6310
6596
|
return scaleDenominator > upperRatio;
|
|
6311
6597
|
}
|
|
6598
|
+
// flat's chart LABEL scale gate (maptheme.js 24536-24537): the label is
|
|
6599
|
+
// visible while the scale passes <= against BOTH bounds — flat's own
|
|
6600
|
+
// lower test reads <= too, kept as-is for parity
|
|
6601
|
+
function labelHiddenByScale(style, zoom) {
|
|
6602
|
+
const upper = scaleDenom(style.labelupper), lower = scaleDenom(style.labellower);
|
|
6603
|
+
const scale = scaleDenominatorAt(zoom);
|
|
6604
|
+
return !!((upper && scale > upper) || (lower && scale > lower));
|
|
6605
|
+
}
|
|
6312
6606
|
|
|
6313
6607
|
// contrasting text color against a bubble's own fill — standard relative
|
|
6314
6608
|
// luminance; approximates the real engine's __maptheme_getChartColors(...)
|
|
6315
6609
|
// .textColor (white text on ixmaps' typically dark/saturated palette,
|
|
6316
6610
|
// dark text on pale fills) without needing its exact color-space table.
|
|
6317
6611
|
function contrastTextColor(fillRgb) {
|
|
6318
|
-
const [r, g, b] = fillRgb;
|
|
6612
|
+
const [r, g, b] = fillRgb || [128, 128, 128];
|
|
6319
6613
|
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
6320
6614
|
return luminance > 0.6 ? [30, 30, 30] : [255, 255, 255];
|
|
6321
6615
|
}
|
|
@@ -6751,6 +7045,83 @@
|
|
|
6751
7045
|
return { offset, index, extent };
|
|
6752
7046
|
}
|
|
6753
7047
|
|
|
7048
|
+
// Chart texts covered by charts drawn after them. Flat draws each chart
|
|
7049
|
+
// as one SVG group (glow, symbol, value text, in that order) in the
|
|
7050
|
+
// sorted draw order, so a later chart's symbol and glow paint over the
|
|
7051
|
+
// texts of every chart beneath it; here the texts are separate TextLayers
|
|
7052
|
+
// above every icon. This composites that occlusion on the CPU: for each
|
|
7053
|
+
// text, the transmittance Π(1 − α) of every LATER chart's symbol parts
|
|
7054
|
+
// and glow at a few sample points of the text box, averaged — the
|
|
7055
|
+
// fraction of the text still showing through, its alpha. Partly covered
|
|
7056
|
+
// texts fade instead of being clipped (no per-pixel mask in deck.gl).
|
|
7057
|
+
// charts: in draw order, { x, y, parts: [{ dx, dy, r, shape, alpha }],
|
|
7058
|
+
// glowR, glowAlpha } in screen pixels; a part's alpha its opacity,
|
|
7059
|
+
// glowR 0 for none (glowAlpha scales glowProfileAlpha)
|
|
7060
|
+
// texts: { order (index into charts), x, y, w, h }
|
|
7061
|
+
// Returns each text's transmittance, 0..1.
|
|
7062
|
+
const OCCLUSION_CELL_PX = 64;
|
|
7063
|
+
// below this a text is as good as hidden: left out of its TextLayer
|
|
7064
|
+
const CHART_TEXT_MIN_TRANSMITTANCE = 0.08;
|
|
7065
|
+
function chartTextTransmittance(charts, texts) {
|
|
7066
|
+
const grid = new Map();
|
|
7067
|
+
charts.forEach((c, k) => {
|
|
7068
|
+
let ext = c.glowR || 0;
|
|
7069
|
+
for (const p of c.parts) ext = Math.max(ext, Math.hypot(p.dx, p.dy) + p.r * Math.SQRT2);
|
|
7070
|
+
const x0 = Math.floor((c.x - ext) / OCCLUSION_CELL_PX), x1 = Math.floor((c.x + ext) / OCCLUSION_CELL_PX);
|
|
7071
|
+
const y0 = Math.floor((c.y - ext) / OCCLUSION_CELL_PX), y1 = Math.floor((c.y + ext) / OCCLUSION_CELL_PX);
|
|
7072
|
+
for (let gx = x0; gx <= x1; gx++) {
|
|
7073
|
+
for (let gy = y0; gy <= y1; gy++) {
|
|
7074
|
+
const key = gx + ',' + gy;
|
|
7075
|
+
let cell = grid.get(key);
|
|
7076
|
+
if (!cell) grid.set(key, cell = []);
|
|
7077
|
+
cell.push(k);
|
|
7078
|
+
}
|
|
7079
|
+
}
|
|
7080
|
+
});
|
|
7081
|
+
const alphaAt = (c, x, y) => {
|
|
7082
|
+
let keep = 1;
|
|
7083
|
+
if (c.glowR) {
|
|
7084
|
+
const d = Math.hypot(x - c.x, y - c.y) / c.glowR;
|
|
7085
|
+
if (d < 1) keep *= 1 - c.glowAlpha * glowProfileAlpha(d);
|
|
7086
|
+
}
|
|
7087
|
+
for (const p of c.parts) {
|
|
7088
|
+
if (insideSymbolShape(p.shape, x - c.x - p.dx, y - c.y - p.dy, p.r)) keep *= 1 - p.alpha;
|
|
7089
|
+
}
|
|
7090
|
+
return keep;
|
|
7091
|
+
};
|
|
7092
|
+
// the box center and four points inside its corners
|
|
7093
|
+
const SAMPLES = [[0, 0], [-0.35, -0.3], [0.35, -0.3], [-0.35, 0.3], [0.35, 0.3]];
|
|
7094
|
+
return texts.map(t => {
|
|
7095
|
+
let sum = 0;
|
|
7096
|
+
for (const [sx, sy] of SAMPLES) {
|
|
7097
|
+
const x = t.x + sx * t.w, y = t.y + sy * t.h;
|
|
7098
|
+
const cell = grid.get(Math.floor(x / OCCLUSION_CELL_PX) + ',' + Math.floor(y / OCCLUSION_CELL_PX));
|
|
7099
|
+
let keep = 1;
|
|
7100
|
+
// cells list charts in draw order: only the later ones cover
|
|
7101
|
+
if (cell) for (let i = cell.length - 1; i >= 0 && cell[i] > t.order && keep > 0; i--) keep *= alphaAt(charts[cell[i]], x, y);
|
|
7102
|
+
sum += keep;
|
|
7103
|
+
}
|
|
7104
|
+
return sum / SAMPLES.length;
|
|
7105
|
+
});
|
|
7106
|
+
}
|
|
7107
|
+
// _getGlowIcon's radial gradient: 0.28 at the center, 0.09 at half the
|
|
7108
|
+
// radius, 0 at the edge (d = distance / radius)
|
|
7109
|
+
function glowProfileAlpha(d) {
|
|
7110
|
+
return d < 0.5 ? 0.28 - 0.19 * d / 0.5 : d < 1 ? 0.09 * (1 - (d - 0.5) / 0.5) : 0;
|
|
7111
|
+
}
|
|
7112
|
+
// inside drawSymbolPath's shape of radius r, (dx, dy) from its center;
|
|
7113
|
+
// the shapes without their own test as the circle
|
|
7114
|
+
function insideSymbolShape(shape, dx, dy, r) {
|
|
7115
|
+
switch (shape) {
|
|
7116
|
+
case 'squarefull': return Math.abs(dx) <= r && Math.abs(dy) <= r;
|
|
7117
|
+
case 'square': return Math.abs(dx) <= r * Math.SQRT1_2 && Math.abs(dy) <= r * Math.SQRT1_2;
|
|
7118
|
+
case 'roundrect': return Math.abs(dx) <= r * 0.8 && Math.abs(dy) <= r * 0.8;
|
|
7119
|
+
case 'diamond': case 'carot': return Math.abs(dx) + Math.abs(dy) <= r;
|
|
7120
|
+
case 'empty': return false;
|
|
7121
|
+
default: return dx * dx + dy * dy <= r * r;
|
|
7122
|
+
}
|
|
7123
|
+
}
|
|
7124
|
+
|
|
6754
7125
|
// A pixel offset [dx, dy] (y down) from lngLat at MapLibre zoom `zoom`,
|
|
6755
7126
|
// as lng/lat (Web Mercator, 512 px world tiles) — for shapes too large
|
|
6756
7127
|
// for an icon, drawn in map coordinates (rebuilt with every refresh).
|
|
@@ -6830,6 +7201,25 @@
|
|
|
6830
7201
|
return parts;
|
|
6831
7202
|
}
|
|
6832
7203
|
|
|
7204
|
+
// CHART|SYMBOL|SEQUENCE|RINGS (flat maptheme.js 21293-21318): the parts
|
|
7205
|
+
// nest on the center — each non-zero part sized by the running total of
|
|
7206
|
+
// the non-zero parts up to it (its own value included), biggest first
|
|
7207
|
+
// so the smaller ones stay on top; parts of value 0 are left out.
|
|
7208
|
+
// radiusOf(total) is the ring's radius. Measured on flat (NYC status:
|
|
7209
|
+
// 63 | 75 | 3 → r 161.5 | 353.8 | 361.5 at max radius 300, max value
|
|
7210
|
+
// 117). Returns { i, v (the part's own value), c (running total), r, x, y }.
|
|
7211
|
+
function ringsLayout(counts, radiusOf) {
|
|
7212
|
+
const parts = [];
|
|
7213
|
+
let total = 0;
|
|
7214
|
+
counts.forEach((v, i) => {
|
|
7215
|
+
if (!v) return;
|
|
7216
|
+
total += v;
|
|
7217
|
+
const r = radiusOf(total);
|
|
7218
|
+
if (r > 0) parts.push({ i, v, c: total, r, x: 0, y: 0 });
|
|
7219
|
+
});
|
|
7220
|
+
return parts.sort((a, b) => b.c - a.c);
|
|
7221
|
+
}
|
|
7222
|
+
|
|
6833
7223
|
// ---------------------------------------------------------------
|
|
6834
7224
|
// Classification — pure functions (no LayerRuntime state): class
|
|
6835
7225
|
// breaks for a numeric field, and which class a value falls in.
|
|
@@ -7966,6 +8356,18 @@
|
|
|
7966
8356
|
return out;
|
|
7967
8357
|
}
|
|
7968
8358
|
|
|
8359
|
+
// Every runtime ever built gets a fresh number: a theme rebuilt through
|
|
8360
|
+
// removeTheme() + defineLayer() (a page's own refresh loop, e.g. the fire
|
|
8361
|
+
// page's KDE isobands) is a NEW runtime whose data may differ from the
|
|
8362
|
+
// runtime it replaces — if its deck.gl layer kept the SAME id, deck would
|
|
8363
|
+
// update the old layer's state in place, and under the interleaved overlay
|
|
8364
|
+
// those in-place updates can leave the PREVIOUS generation's geometry on
|
|
8365
|
+
// screen (the exact failure _dataLayerId's own comment documents for
|
|
8366
|
+
// replaceFeatures: "updates in place, arriving over each other, left
|
|
8367
|
+
// triangles across sections"). A unique -dN suffix per runtime forces deck
|
|
8368
|
+
// to build a genuinely new layer and dispose the old one.
|
|
8369
|
+
let LAYER_DATA_GENERATION = 0;
|
|
8370
|
+
|
|
7969
8371
|
class LayerRuntime {
|
|
7970
8372
|
// spec: a normalizeTheme() result — never a raw LayerBuilder
|
|
7971
8373
|
constructor(spec, fc, mapOptions) {
|
|
@@ -7974,6 +8376,8 @@
|
|
|
7974
8376
|
this.style = spec.style;
|
|
7975
8377
|
this.flags = symbolsFlagCompat(spec.flags, spec.style);
|
|
7976
8378
|
this.meta = spec.meta;
|
|
8379
|
+
// see LAYER_DATA_GENERATION above — unique per runtime, see _dataLayerId
|
|
8380
|
+
this._dataGen = ++LAYER_DATA_GENERATION;
|
|
7977
8381
|
// GL-PORT COMPAT: retained only for ixmaps.getThemeObj()'s szFilter
|
|
7978
8382
|
// (real ixmaps-flat global compat shim, see _globalThemeRegistry) —
|
|
7979
8383
|
// this engine's own filtering is fully static/load-time (see
|
|
@@ -8144,6 +8548,21 @@
|
|
|
8144
8548
|
// ("investimento"/"tamponamento"/...) needs the display text, not
|
|
8145
8549
|
// a raw numeric code it could never match against.
|
|
8146
8550
|
this.categoryColorsRgb = resolveClassColors(this.style.colorscheme, this.categoryDisplayLabels, this.style.classes);
|
|
8551
|
+
} else if (this._isMultiFieldSequence()) {
|
|
8552
|
+
// CHART|SYMBOL|SEQUENCE over several value fields (no CATEGORICAL,
|
|
8553
|
+
// no AGGREGATE): one part per field, colored by the colorscheme in
|
|
8554
|
+
// field order, labeled by .style({label}) (flat's szFieldsA /
|
|
8555
|
+
// szLabelA). A colorscheme entry "none" is a part without fill —
|
|
8556
|
+
// only its outline in linecolor (flat: fill:none). The parts' max
|
|
8557
|
+
// value is flat's nMax: the biggest single field value of any
|
|
8558
|
+
// record (maptheme.js nMaxA → nMax), measured on the NYC status page
|
|
8559
|
+
Object.assign(this, computeMultiFieldClasses(this.binding, this.style));
|
|
8560
|
+
this.categoryIndexByLabel = new Map(this.categoryLabels.map((v, i) => [v, i]));
|
|
8561
|
+
const scheme = Array.isArray(this.style.colorscheme) ? this.style.colorscheme : [];
|
|
8562
|
+
this._partNoFill = new Set(this._multiFields.map((f, i) => i).filter(i => String(scheme[i]).trim().toLowerCase() === 'none'));
|
|
8563
|
+
let max = 0;
|
|
8564
|
+
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; }
|
|
8565
|
+
this._multiFieldPartMax = max || 1;
|
|
8147
8566
|
} else if (this.binding.value) {
|
|
8148
8567
|
// real engine's OTHER coloring mode (maptheme.js distributeValues,
|
|
8149
8568
|
// partsA): a NUMERIC bound value, not CATEGORICAL, is classed into
|
|
@@ -8156,6 +8575,24 @@
|
|
|
8156
8575
|
// "which bucket does this feature belong to", and once buckets +
|
|
8157
8576
|
// colors exist, everything downstream is identical.
|
|
8158
8577
|
this._buildPartsA();
|
|
8578
|
+
} else if (this.style.colorscheme != null) {
|
|
8579
|
+
// GL-PORT COMPAT: a theme with NO classification at all (no
|
|
8580
|
+
// CATEGORICAL categories, no numeric binding.value to range-class)
|
|
8581
|
+
// still has classes in the real engine — exactly ONE, its implicit
|
|
8582
|
+
// bucket: _resolveClassIndex returns 0 for every feature, and flat
|
|
8583
|
+
// colors them all with colorScheme[0] (every MapTheme is built
|
|
8584
|
+
// from its colorscheme array — one entry, one class, e.g. the
|
|
8585
|
+
// fire page's KDE isobands: FEATURE|CHOROPLETH, colorscheme
|
|
8586
|
+
// "#dd0088", no value field — every iso band one flat magenta).
|
|
8587
|
+
// Without this branch categoryColorsRgb stayed undefined while
|
|
8588
|
+
// cat was still 0, and the choropleth's getFillColor crashed on
|
|
8589
|
+
// categoryColorsRgb[0] — deck.gl killed the layer at init and
|
|
8590
|
+
// the theme never drew. A colorscheme that resolves to NO colors
|
|
8591
|
+
// (the explicit no-fill ["none"] — flat draws unfilled symbols,
|
|
8592
|
+
// only their values/boxes/titles) keeps that: resolving it to the
|
|
8593
|
+
// fallback palette would paint symbol circles flat never draws.
|
|
8594
|
+
const resolved = resolveColorScheme(this.style.colorscheme, [''], this.style.classes);
|
|
8595
|
+
this.categoryColorsRgb = resolved ? resolved.map(c => Array.isArray(c) ? c : (parseCssColor(c) || [130, 130, 130])) : null;
|
|
8159
8596
|
}
|
|
8160
8597
|
|
|
8161
8598
|
// facet-driven dynamic filter state (see setFacetFilter/getFacets) —
|
|
@@ -8225,6 +8662,17 @@
|
|
|
8225
8662
|
// this with AGGREGATE clustering yet, so a clustered GROUP bubble
|
|
8226
8663
|
// always stays circular (_buildBubbleIcon, untouched).
|
|
8227
8664
|
_resolveSymbolShape(props) {
|
|
8665
|
+
// flat's class→symbol mapping (maptheme.js 1417, 2074-2077, 12937):
|
|
8666
|
+
// `symbols` WITHOUT a symbolfield names the shape per COLOR class —
|
|
8667
|
+
// szSymbolsA[class], the last entry repeating when the array is
|
|
8668
|
+
// shorter than the classes (a single entry, like the fire page's
|
|
8669
|
+
// symbols: ["square"], shapes every class). Only GL's implemented
|
|
8670
|
+
// shapes draw (drawSymbolPath); flat-only names fall back to circle.
|
|
8671
|
+
if (!this.style.symbolfield && Array.isArray(this.style.symbols)) {
|
|
8672
|
+
const s = this.style.symbols;
|
|
8673
|
+
const cat = props && props.cat != null ? props.cat : 0;
|
|
8674
|
+
return normalizeSymbolShape(s[Math.min(cat, s.length - 1)]);
|
|
8675
|
+
}
|
|
8228
8676
|
if (!this.style.symbolfield || !Array.isArray(this.style.symbolvalues) || !Array.isArray(this.style.symbols)) return 'circle';
|
|
8229
8677
|
// CORRECTED: found live via pickObject, not assumed — the properties
|
|
8230
8678
|
// this actually receives at render time (from _computeAggregatedItems'
|
|
@@ -8286,6 +8734,19 @@
|
|
|
8286
8734
|
// they'd never even share a cell in the first place). Every
|
|
8287
8735
|
// feature goes into ONE bucket; _buildChartLayers resolves the real
|
|
8288
8736
|
// 7-class color once it knows each cell's actual aggregated total.
|
|
8737
|
+
if (this._multiFields && this._isMultiFieldSequence()) {
|
|
8738
|
+
// one bucket; each record carries its field values as the chart's
|
|
8739
|
+
// parts. Its value — the draw order — is the first field's, as
|
|
8740
|
+
// flat's chart node value (NYC status: value="63" = e-bikes)
|
|
8741
|
+
this._featuresByCategory = [sourceFeatures.map(f => {
|
|
8742
|
+
const parts = this._multiFields.map(k => parseFloat(f.properties[k]) || 0);
|
|
8743
|
+
return { type: 'Feature', geometry: f.geometry, properties: { value: parts[0], parts, raw: f.properties } };
|
|
8744
|
+
})];
|
|
8745
|
+
this._clusterIndices = null;
|
|
8746
|
+
this._clusterRadiusPx = null;
|
|
8747
|
+
this._aggregateStatsCache = null;
|
|
8748
|
+
return;
|
|
8749
|
+
}
|
|
8289
8750
|
if (this._rangeClassed) {
|
|
8290
8751
|
// flat colors/classes an aggregated cell by the SUM of the value
|
|
8291
8752
|
// field (nValuesA, maptheme.js:10870ff; classified by nValuesA[0],
|
|
@@ -8488,7 +8949,11 @@
|
|
|
8488
8949
|
}
|
|
8489
8950
|
|
|
8490
8951
|
_usesAggregationIndex() {
|
|
8491
|
-
|
|
8952
|
+
// GRIDSIZE bins its own grid (_ensureGridIndex) — PLOT curves and the
|
|
8953
|
+
// plain mesh; a classed GRIDSIZE symbol chart is aggregated like any
|
|
8954
|
+
// other (see _isPlainGridMesh)
|
|
8955
|
+
return (this.flags.has('AGGREGATE') || isSymbolChart(this.flags))
|
|
8956
|
+
&& (!this.flags.has('GRIDSIZE') || (!this.flags.has('PLOT') && !this._isPlainGridMesh()));
|
|
8492
8957
|
}
|
|
8493
8958
|
|
|
8494
8959
|
_rebuildActiveFeatures() {
|
|
@@ -8772,7 +9237,7 @@
|
|
|
8772
9237
|
return this._glowIconCache.get(key);
|
|
8773
9238
|
}
|
|
8774
9239
|
|
|
8775
|
-
_buildBubbleIcon(counts, colors, marked = null) {
|
|
9240
|
+
_buildBubbleIcon(counts, colors, marked = null, shapes = null) {
|
|
8776
9241
|
// The cache key quantizes each count's RATIO to the group's total,
|
|
8777
9242
|
// not the raw count — computeBubblePackLayout's radii are purely
|
|
8778
9243
|
// sqrt(c/total), so two clusters with wildly different absolute
|
|
@@ -8802,7 +9267,7 @@
|
|
|
8802
9267
|
// coarse share buckets that can put a small marked part at 0
|
|
8803
9268
|
const key = counts.map(c => Math.round((c / total) * RATIO_BUCKETS)).join('-') + (marked
|
|
8804
9269
|
? '|iso:' + counts.map((c, i) => (c > 0 && marked.has(i) ? i + ':' + Math.round(Math.sqrt(c / total) * 100) : null)).filter(Boolean).join(',')
|
|
8805
|
-
: '');
|
|
9270
|
+
: '') + (shapes ? '|sym:' + shapes.map(v => String(v).toLowerCase()).join(',') : '');
|
|
8806
9271
|
if (this._iconCache.has(key)) return this._iconCache.get(key);
|
|
8807
9272
|
const size = BUBBLE_ICON_SIZE;
|
|
8808
9273
|
const canvas = document.createElement('canvas');
|
|
@@ -8811,8 +9276,16 @@
|
|
|
8811
9276
|
const { present, radii, offsets, fitScale } = (marked && isolatedBubblePackLayout(counts, size, marked)) || computeBubblePackLayout(counts, size);
|
|
8812
9277
|
const cx = size / 2, cy = size / 2;
|
|
8813
9278
|
present.forEach((p, i) => {
|
|
9279
|
+
// flat draws each part of an aggregated SYMBOL theme as ITS class's
|
|
9280
|
+
// symbol shape (the szSymbol switch runs per part, maptheme.js
|
|
9281
|
+
// 21995-22060) — `symbols` names them per class like
|
|
9282
|
+
// _resolveSymbolShape; circles when the name isn't one GL draws
|
|
9283
|
+
const shape = shapes ? normalizeSymbolShape(shapes[Math.min(p.i, shapes.length - 1)]) : 'circle';
|
|
9284
|
+
const px = cx + offsets[i].x * fitScale, py = cy + offsets[i].y * fitScale;
|
|
8814
9285
|
ctx.beginPath();
|
|
8815
|
-
ctx.arc(
|
|
9286
|
+
if (shape === 'circle') ctx.arc(px, py, radii[i] * fitScale, 0, Math.PI * 2);
|
|
9287
|
+
else drawSymbolPath(ctx, shape, px, py, radii[i] * fitScale);
|
|
9288
|
+
ctx.closePath();
|
|
8816
9289
|
ctx.fillStyle = `rgb(${colors[p.i].join(',')})`;
|
|
8817
9290
|
ctx.globalAlpha = 0.9; ctx.fill(); ctx.globalAlpha = 1;
|
|
8818
9291
|
ctx.lineWidth = 1; ctx.strokeStyle = '#fff'; ctx.stroke();
|
|
@@ -8862,14 +9335,22 @@
|
|
|
8862
9335
|
ctx.beginPath();
|
|
8863
9336
|
drawSymbolPath(ctx, shape, c, c, r);
|
|
8864
9337
|
ctx.closePath();
|
|
9338
|
+
// flat's "empty" (maptheme.js 21629-21631): the circle geometry, but
|
|
9339
|
+
// a fully transparent fill and a gray outline (fill-opacity:0,
|
|
9340
|
+
// stroke #888888) — a shapeless spot marker
|
|
8865
9341
|
ctx.fillStyle = `rgb(${colorRgb.join(',')})`;
|
|
8866
|
-
ctx.globalAlpha = opacity;
|
|
9342
|
+
ctx.globalAlpha = shape === 'empty' ? 0 : opacity;
|
|
8867
9343
|
ctx.fill();
|
|
8868
9344
|
if (borderWidth > 0 && borderColorRgb) {
|
|
8869
9345
|
ctx.globalAlpha = 1;
|
|
8870
9346
|
ctx.strokeStyle = `rgb(${borderColorRgb.join(',')})`;
|
|
8871
9347
|
ctx.lineWidth = borderWidth;
|
|
8872
9348
|
ctx.stroke();
|
|
9349
|
+
} else if (shape === 'empty') {
|
|
9350
|
+
ctx.globalAlpha = 1;
|
|
9351
|
+
ctx.strokeStyle = '#888888';
|
|
9352
|
+
ctx.lineWidth = 1;
|
|
9353
|
+
ctx.stroke();
|
|
8873
9354
|
}
|
|
8874
9355
|
const icon = { url: canvas.toDataURL(), width: size, height: size, anchorX: size / 2, anchorY: size / 2, id: key };
|
|
8875
9356
|
return this._cacheIcon(key, icon);
|
|
@@ -9146,7 +9627,8 @@
|
|
|
9146
9627
|
_formatTooltipValue(value) {
|
|
9147
9628
|
const num = Number(value);
|
|
9148
9629
|
if (isNaN(num)) return String(value);
|
|
9149
|
-
const
|
|
9630
|
+
const vd = this.style.valuedecimals;
|
|
9631
|
+
const rawDecimals = vd == null || vd === '' ? NaN : Number(vd);
|
|
9150
9632
|
const decimals = isNaN(rawDecimals) ? 2 : rawDecimals;
|
|
9151
9633
|
// flat's __formatValue (mapscript.js 6268ff) rounds twice: first to
|
|
9152
9634
|
// one decimal more (toFixed), then to the precision (Math.round) —
|
|
@@ -9215,16 +9697,19 @@
|
|
|
9215
9697
|
// GRIDSIZE (spatial grid binning) is checked before the generic
|
|
9216
9698
|
// CHART|SYMBOL bubble pipeline, same "more specific base type wins"
|
|
9217
9699
|
// precedence as DOT: PLOT+GRIDSIZE is the per-year curves mini-chart
|
|
9218
|
-
// (_buildPlotLayers); GRIDSIZE
|
|
9219
|
-
//
|
|
9220
|
-
//
|
|
9221
|
-
//
|
|
9222
|
-
//
|
|
9223
|
-
// engine either — confirmed, not a gap in this port).
|
|
9700
|
+
// (_buildPlotLayers); GRIDSIZE without PLOT is flat's symbol chart
|
|
9701
|
+
// with `nRadius = nGridSize/2` (maptheme.js 21843) — every chart the
|
|
9702
|
+
// size of its cell, colored by its class (QUANTILE of the MEAN, the
|
|
9703
|
+
// fuel-price page). Only a plain one-color grid (the "grid"
|
|
9704
|
+
// companion layer) takes the uniform mesh shortcut.
|
|
9224
9705
|
if (this.flags.has('GRIDSIZE') && this.flags.has('PLOT')) return this._buildPlotLayers(zoom, bbox);
|
|
9225
9706
|
// PLOT without GRIDSIZE: one line chart per item over its value fields
|
|
9226
9707
|
if (this.flags.has('PLOT') && this._isItemPlot()) return this._buildItemPlotLayers(zoom);
|
|
9227
|
-
|
|
9708
|
+
// a plain grid (one color, no values or glow — the Germany page's
|
|
9709
|
+
// companion "grid" layer) as one square per cell; a classed GRIDSIZE
|
|
9710
|
+
// chart (the fuel-price page) is a symbol chart sized to the grid,
|
|
9711
|
+
// below in _buildChartLayers (_gridChartRadiusPx)
|
|
9712
|
+
if (this.flags.has('GRIDSIZE') && this._isPlainGridMesh()) return this._buildGridMeshLayers(zoom, bbox);
|
|
9228
9713
|
// CHOROPLETH is checked before the generic FEATURE dispatch — a
|
|
9229
9714
|
// CHOROPLETH layer's own type string doesn't carry FEATURE/FEATURES
|
|
9230
9715
|
// (only its geometry-donor base layer does, see
|
|
@@ -9259,7 +9744,16 @@
|
|
|
9259
9744
|
|
|
9260
9745
|
_buildFeaturesLayers(zoom, bbox) {
|
|
9261
9746
|
const cs = this.style.colorscheme;
|
|
9262
|
-
|
|
9747
|
+
// a generated scheme's first entry is the class COUNT, not a color —
|
|
9748
|
+
// flat colors an unclassed theme with the sweep's FIRST generated
|
|
9749
|
+
// color (maptheme colorScheme[0] after realize generates the sweep),
|
|
9750
|
+
// never the bare count string (a near-white dot for "5","white","blue"
|
|
9751
|
+
// — the NYC page's station base)
|
|
9752
|
+
let raw = Array.isArray(cs) ? cs[0] : cs;
|
|
9753
|
+
if (Array.isArray(cs) && isGeneratedColorScheme(cs)) {
|
|
9754
|
+
const sweep = flatColorSweep(cs[1], cs[2], Number(cs[0]) || 5, cs[3], cs[4]);
|
|
9755
|
+
if (sweep && sweep.length) raw = sweep[0];
|
|
9756
|
+
}
|
|
9263
9757
|
const filled = raw !== 'none';
|
|
9264
9758
|
// FEATURE|CATEGORICAL: polygons coloured by their category (flat colours
|
|
9265
9759
|
// each shape by its class), not one flat fill
|
|
@@ -9288,7 +9782,7 @@
|
|
|
9288
9782
|
getFillColor: byCategory
|
|
9289
9783
|
? f => {
|
|
9290
9784
|
const cat = this._resolveClassIndex(f.properties[this.binding.value]);
|
|
9291
|
-
return [...(cat != null ? this.categoryColorsRgb[cat] : nodataRgb).slice(0, 3), catAlpha];
|
|
9785
|
+
return [...(cat != null ? (this.categoryColorsRgb || [])[cat] || nodataRgb : nodataRgb).slice(0, 3), catAlpha];
|
|
9292
9786
|
}
|
|
9293
9787
|
: filled ? [...hexOrNamedToRgb(raw).slice(0, 3), Math.round(255 * cssColorAlpha(raw) * (styleNum(this.style.fillopacity) || 1))] : [0, 0, 0, 0],
|
|
9294
9788
|
getLineColor: this.style.linecolor
|
|
@@ -9495,7 +9989,7 @@
|
|
|
9495
9989
|
getFillColor: d => {
|
|
9496
9990
|
if (!hasData(d)) return [0, 0, 0, 0];
|
|
9497
9991
|
const rgb = d.properties.composedColor
|
|
9498
|
-
|| (d.properties.cat != null ? this.categoryColorsRgb[d.properties.cat] : null) || fallbackRgb;
|
|
9992
|
+
|| (d.properties.cat != null ? (this.categoryColorsRgb || [])[d.properties.cat] : null) || fallbackRgb;
|
|
9499
9993
|
const evidence = unmarkedEvidence(d);
|
|
9500
9994
|
if (evidence === 'hide') return [0, 0, 0, 0];
|
|
9501
9995
|
if (evidence === 'gray') return [...grayOf(rgb), 90];
|
|
@@ -9510,7 +10004,15 @@
|
|
|
9510
10004
|
// per-feature alpha (baked above) already carries the resolved
|
|
9511
10005
|
// base opacity — a layer-level opacity on TOP of that would
|
|
9512
10006
|
// multiply it a second time.
|
|
9513
|
-
opacity: dopacityActive ? 1 : baseOpacity
|
|
10007
|
+
opacity: dopacityActive ? 1 : baseOpacity,
|
|
10008
|
+
// Same coplanar z-fighting fix as the FEATURE/FEATURES GeoJsonLayer
|
|
10009
|
+
// (see _buildFeaturesLayers' own comment): under globe projection
|
|
10010
|
+
// the interleaved polygon surface and MapLibre's own sphere mesh
|
|
10011
|
+
// compete for the same depth-buffer real estate and the fill comes
|
|
10012
|
+
// out moiré/hatched ("holes"), never under flat Mercator. Draw
|
|
10013
|
+
// order against other polygon layers is unchanged — depthCompare
|
|
10014
|
+
// only settles the fight against the map's own surface.
|
|
10015
|
+
parameters: { depthCompare: 'always' }
|
|
9514
10016
|
})];
|
|
9515
10017
|
}
|
|
9516
10018
|
|
|
@@ -9563,9 +10065,37 @@
|
|
|
9563
10065
|
// style.values/xaxis (explicit, see _prepare) if set, else whatever
|
|
9564
10066
|
// CATEGORICAL auto-discovered.
|
|
9565
10067
|
// a per-item PLOT: no GRIDSIZE grid, several value fields per item
|
|
10068
|
+
// GRIDSIZE without PLOT drawn as one uniform square per cell: only a
|
|
10069
|
+
// plain grid — a single color, no VALUES, no GLOW
|
|
10070
|
+
_isPlainGridMesh() {
|
|
10071
|
+
return !this.flags.has('PLOT') && (this.categoryColorsRgb || []).length <= 1
|
|
10072
|
+
&& !this.flags.has('VALUES') && !this.flags.has('GLOW');
|
|
10073
|
+
}
|
|
10074
|
+
// GRIDSIZE (no PLOT) symbol chart radius, px: flat's nGridSize / 2
|
|
10075
|
+
// (maptheme.js 21843), nGridSize the aggregation cell pitch / nAutoSize
|
|
10076
|
+
// (16944-16952: RECT or no AGGREGATE 1.0, with GAP 1.15; else 0.75,
|
|
10077
|
+
// with GAP 0.8) · style.scale. Measured on the fuel-price page: 5 px
|
|
10078
|
+
// cells, RECT|GAP → nGridSize 4.35 px, circle r 2.17 px. null when not
|
|
10079
|
+
// a GRIDSIZE chart or no grid at this scale.
|
|
10080
|
+
_gridChartRadiusPx(zoom, liveZoom) {
|
|
10081
|
+
if (!this.flags.has('GRIDSIZE') || this.flags.has('PLOT') || !this._clusterRadiusPx) return null;
|
|
10082
|
+
const fromZoom = this._clusterUsesFixedZoom ? GRIDWIDTH_METERS_REFERENCE_ZOOM : zoom;
|
|
10083
|
+
const cellPx = this._clusterRadiusPx * Math.pow(2, liveZoom - fromZoom);
|
|
10084
|
+
const rect = flatFlag(this.flags, 'RECT') || !this.flags.has('AGGREGATE');
|
|
10085
|
+
const gap = flatFlag(this.flags, 'GAP');
|
|
10086
|
+
const autoSize = rect ? (gap ? 1.15 : 1.0) : (gap ? 0.8 : 0.75);
|
|
10087
|
+
return cellPx / autoSize * (styleNum(this.style.scale) || 1) / 2;
|
|
10088
|
+
}
|
|
9566
10089
|
_isItemPlot() {
|
|
9567
10090
|
return this.flags.has('PLOT') && !this.flags.has('GRIDSIZE') && String(this.binding.value || '').includes('|');
|
|
9568
10091
|
}
|
|
10092
|
+
// a per-item SEQUENCE chart: CHART|SYMBOL|SEQUENCE, several value
|
|
10093
|
+
// fields per item, one part per field (no CATEGORICAL, no AGGREGATE)
|
|
10094
|
+
_isMultiFieldSequence() {
|
|
10095
|
+
return this.flags.has('CHART') && this.flags.has('SYMBOL') && flatFlag(this.flags, 'SEQUENCE')
|
|
10096
|
+
&& !this.flags.has('CATEGORICAL') && !this.flags.has('AGGREGATE') && !this.flags.has('PLOT')
|
|
10097
|
+
&& String(this.binding.value || '').includes('|');
|
|
10098
|
+
}
|
|
9569
10099
|
_itemPlotFields() {
|
|
9570
10100
|
return String(this.binding.value || '').split('|');
|
|
9571
10101
|
}
|
|
@@ -10419,6 +10949,20 @@
|
|
|
10419
10949
|
...(f.properties.titleRaw ? { titleRaw: f.properties.titleRaw } : {}) }
|
|
10420
10950
|
};
|
|
10421
10951
|
});
|
|
10952
|
+
// MEAN (no CATEGORICAL): a cell's value is the mean of its records,
|
|
10953
|
+
// not their sum (flat's MEAN aggregation) — what its label prints,
|
|
10954
|
+
// its size and its class follow. The fuel-price page: 119 stations,
|
|
10955
|
+
// sum 270.0 → 2.27 €
|
|
10956
|
+
if (this.flags.has('MEAN') && !this.flags.has('CATEGORICAL')) {
|
|
10957
|
+
groups.forEach(g => {
|
|
10958
|
+
const p = g.properties;
|
|
10959
|
+
const n = groupRecordCount(p);
|
|
10960
|
+
if (!(n > 0)) return;
|
|
10961
|
+
if (p.recordCounts) p.counts = p.counts.map((c, i) => (p.recordCounts[i] ? c / p.recordCounts[i] : c));
|
|
10962
|
+
p.total = p.total / n;
|
|
10963
|
+
if (p.classTotal !== undefined) p.classTotal = p.classTotal / n;
|
|
10964
|
+
});
|
|
10965
|
+
}
|
|
10422
10966
|
return { individual, groups };
|
|
10423
10967
|
}
|
|
10424
10968
|
|
|
@@ -10546,6 +11090,10 @@
|
|
|
10546
11090
|
// _computeAggregatedItems below — a plain (non-AGGREGATE)
|
|
10547
11091
|
// BUBBLE/CHART theme never reads them.
|
|
10548
11092
|
if (this.flags.has('AGGREGATE')) this._ensureClusterIndices(zoom);
|
|
11093
|
+
// GRIDSIZE: every chart the size of its grid cell, whatever its value
|
|
11094
|
+
// (_gridChartRadiusPx); otherwise valueRadius
|
|
11095
|
+
const gridR = this._gridChartRadiusPx(zoom, liveZoom);
|
|
11096
|
+
const vRadius = v => (gridR != null ? gridR : valueRadius(v, liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue));
|
|
10549
11097
|
// AGGREGATE sums without a normalsizevalue: flat's default is the
|
|
10550
11098
|
// largest aggregated value (defaultNormalSizeValue leaves it unset, the
|
|
10551
11099
|
// sizes depend on the aggregation)
|
|
@@ -10688,7 +11236,7 @@
|
|
|
10688
11236
|
if (userLayers) return layers.concat(userLayers);
|
|
10689
11237
|
}
|
|
10690
11238
|
if (this.flags.has('LABEL')) return layers.concat(this._buildLabelChartLayers(combined, zoom, liveZoom, sizeValueOf));
|
|
10691
|
-
if (flatFlag(this.flags, 'SEQUENCE') && this.flags.has('CATEGORICAL') && this.flags.has('AGGREGATE')) {
|
|
11239
|
+
if ((flatFlag(this.flags, 'SEQUENCE') && this.flags.has('CATEGORICAL') && this.flags.has('AGGREGATE')) || (this._multiFields && this._isMultiFieldSequence())) {
|
|
10692
11240
|
return layers.concat(this._buildSequenceChartLayers(combined, zoom, liveZoom));
|
|
10693
11241
|
}
|
|
10694
11242
|
const fillOpacity = styleNum(this.style.fillopacity) || 0.85;
|
|
@@ -10703,7 +11251,7 @@
|
|
|
10703
11251
|
const singleBorderWidthPx = styleNum(this.style.linewidth) || 0;
|
|
10704
11252
|
|
|
10705
11253
|
const itemRgb = d => this.categoryColorsRgb[d.properties.counts ? dominant(d.properties.counts) : d.properties.cat] || [128, 128, 128];
|
|
10706
|
-
const radiusOf = d =>
|
|
11254
|
+
const radiusOf = d => vRadius(sizeValueOf(d));
|
|
10707
11255
|
// align / offsetx / offsety: the whole chart moves (flatChartAlignOffset)
|
|
10708
11256
|
// MULTIQUAD / MULTISQUARE: the items at one position side by side
|
|
10709
11257
|
// (multiQuadOffsets), added to the chart position
|
|
@@ -10726,7 +11274,7 @@
|
|
|
10726
11274
|
id: `ix-symbolshadow-${this.name}-g${this._iconGeneration}`,
|
|
10727
11275
|
data: combined, pickable: false,
|
|
10728
11276
|
getPosition: d => d.geometry.coordinates,
|
|
10729
|
-
getIcon: d => this._getShadowIcon(radiusOf(d), blurPx, d.properties.counts ? 'circle' : this._resolveSymbolShape(d.properties)),
|
|
11277
|
+
getIcon: d => this._getShadowIcon(radiusOf(d), blurPx, d.properties.counts && !Array.isArray(this.style.symbols) ? 'circle' : this._resolveSymbolShape(d.properties)),
|
|
10730
11278
|
getSize: d => radiusOf(d) * 3, // disc = 2/3 of the icon → its diameter is 2r
|
|
10731
11279
|
getPixelOffset: alignOf.active ? d => { const o = alignOf(d); return [o[0] + dx, o[1] + dy]; } : [dx, dy],
|
|
10732
11280
|
getColor: d => {
|
|
@@ -10751,8 +11299,8 @@
|
|
|
10751
11299
|
id: `ix-glow-${this.name}-g${this._iconGeneration}`,
|
|
10752
11300
|
data: combined, pickable: false,
|
|
10753
11301
|
getPosition: d => d.geometry.coordinates,
|
|
10754
|
-
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]),
|
|
10755
|
-
getSize: d =>
|
|
11302
|
+
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]),
|
|
11303
|
+
getSize: d => vRadius(sizeValueOf(d)) * (d.properties.counts ? 9 : 11),
|
|
10756
11304
|
getColor: d => [255, 255, 255, this._iconAlpha(d)],
|
|
10757
11305
|
...(alignOf.active ? { getPixelOffset: alignOf } : {}),
|
|
10758
11306
|
sizeUnits: 'pixels',
|
|
@@ -10798,15 +11346,29 @@
|
|
|
10798
11346
|
// Box and title are removed outside boxupper/boxlower.
|
|
10799
11347
|
// Flat draws box and title in the chart group, which is scaled by
|
|
10800
11348
|
// style.scale: the units below are u · scale. Its size measure W is
|
|
10801
|
-
// the
|
|
10802
|
-
//
|
|
10803
|
-
//
|
|
11349
|
+
// the width of the drawn chart (flat's getBox of the chart group,
|
|
11350
|
+
// 18785): the symbol (with MULTIQUAD the whole grid at the position,
|
|
11351
|
+
// the box enclosing all of it), its value text and its label
|
|
11352
|
+
// (chartItemExtent); flat's chart size is 30 units.
|
|
11353
|
+
// DTEXT: the title font follows the chart, max(0.9u, min(W/5,
|
|
11354
|
+
// 14u / scale)) (18815) — measured on the fire areas page's SIF chart
|
|
11355
|
+
// (W 362 units → font 72.35).
|
|
10804
11356
|
const boxShown = flatFlag(this.flags, 'BOX') && !boxHiddenByScale(this.style, liveZoom);
|
|
10805
11357
|
const circular = flatFlag(this.flags, 'CIRCULAR');
|
|
10806
11358
|
const unitPx = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1);
|
|
10807
11359
|
const boxMarginU = styleNum(this.style.boxmargin) || 2;
|
|
10808
11360
|
const titleShown = boxShown && flatFlag(this.flags, 'TITLE') && !!this.binding.title;
|
|
10809
|
-
const
|
|
11361
|
+
const titleScale = styleNum(this.style.textscale) || styleNum(this.style.valuescale) || 1;
|
|
11362
|
+
const titleFontOf = W => (flatFlag(this.flags, 'DTEXT')
|
|
11363
|
+
? Math.max(0.9 * unitPx, Math.min(W / 5, 14 * unitPx / (styleNum(this.style.scale) || 1)))
|
|
11364
|
+
: 5 * unitPx) * titleScale;
|
|
11365
|
+
// the chart's value text and label (see VALUES and LABEL below), shared
|
|
11366
|
+
// with the box: flat's box encloses whatever the chart group draws
|
|
11367
|
+
const valuesShown = this.flags.has('VALUES') && !valuesHiddenByScale(this.style, zoom);
|
|
11368
|
+
const valueTextOf = valuesShown ? this._chartValueTextFn() : null;
|
|
11369
|
+
const labelShown = !!this.style.labelfield && !flatFlag(this.flags, 'NORMSIZE') && !flatFlag(this.flags, 'ZOOM')
|
|
11370
|
+
&& !labelHiddenByScale(this.style, zoom);
|
|
11371
|
+
const labelOf = labelShown ? this._chartLabelFn(unitPx) : null;
|
|
10810
11372
|
// flat: fill-opacity (boxopacity || 1) on the raw style value — the
|
|
10811
11373
|
// usual string "0" is transparent; typed here, a given 0 is too
|
|
10812
11374
|
const boxOpacity = isNaN(styleNum(this.style.boxopacity)) ? 1 : styleNum(this.style.boxopacity);
|
|
@@ -10814,12 +11376,16 @@
|
|
|
10814
11376
|
const alignRight = /right/.test(String(this.style.align || ''));
|
|
10815
11377
|
// with MULTIQUAD only the first item of a position gets its box/title
|
|
10816
11378
|
const boxLayout = boxShown ? combined.filter(d => !multi || multi.index.get(d) === 0).map(d => {
|
|
10817
|
-
const r =
|
|
11379
|
+
const r = vRadius(sizeValueOf(d));
|
|
10818
11380
|
const ext = multi ? multi.extent.get(d) : [0, 0, 0, 0];
|
|
10819
11381
|
const chart = [ext[0] - r, ext[1] - r, ext[2] + r, ext[3] + r];
|
|
11382
|
+
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]); };
|
|
11383
|
+
if (valueTextOf && !d.properties.counts) { const v = valueTextOf(d, r); if (v) grow(v.extent); }
|
|
11384
|
+
if (labelOf) { const l = labelOf(d, r); if (l) grow(l.extent); }
|
|
10820
11385
|
const w = chart[2] - chart[0];
|
|
10821
11386
|
const margin = Math.min(2 * boxMarginU * unitPx, boxMarginU * w / 30);
|
|
10822
11387
|
const boxR = circular ? r * 1.1 + margin : r;
|
|
11388
|
+
const titleFontPx = titleFontOf(w);
|
|
10823
11389
|
let title = null;
|
|
10824
11390
|
if (titleShown) {
|
|
10825
11391
|
const text = this._itemTitle(d.properties);
|
|
@@ -10835,7 +11401,7 @@
|
|
|
10835
11401
|
const anchor = circular ? 'middle' : (alignRight ? 'end' : 'start');
|
|
10836
11402
|
const x0 = anchor === 'middle' ? x - width / 2 : anchor === 'end' ? x - width : x;
|
|
10837
11403
|
// arial: ascent 0.905, descent 0.212 of the font size
|
|
10838
|
-
title = { text: lines.join('\n'), x, anchor, top: base - 0.8 * f,
|
|
11404
|
+
title = { text: lines.join('\n'), x, anchor, top: base - 0.8 * f, f,
|
|
10839
11405
|
extent: [x0, base - 0.905 * f, x0 + width, base + (lines.length - 1) * f + 0.212 * f] };
|
|
10840
11406
|
}
|
|
10841
11407
|
}
|
|
@@ -10935,17 +11501,27 @@
|
|
|
10935
11501
|
}));
|
|
10936
11502
|
}
|
|
10937
11503
|
|
|
11504
|
+
// flat's per-part SYMBOL branch (everything but CATEGORICAL without
|
|
11505
|
+
// AGGREGATE/GROUP) draws a square of side 2r, the other one 1.6r
|
|
11506
|
+
// (drawSymbolPath's own square)
|
|
11507
|
+
const perPartBranch = flatChartBranch(this.flags) === 'symbol'
|
|
11508
|
+
&& !(this.flags.has('CATEGORICAL') && !this.flags.has('AGGREGATE') && !flatFlag(this.flags, 'GROUP'));
|
|
11509
|
+
const shapeOf = props => { const sh = this._resolveSymbolShape(props); return sh === 'square' && perPartBranch ? 'squarefull' : sh; };
|
|
11510
|
+
// a range-classed cell is ONE class: one symbol in its class color,
|
|
11511
|
+
// fillopacity and linecolor, as a single item (flat draws one
|
|
11512
|
+
// symbol) — the packed multi-category icon is for CATEGORICAL cells
|
|
11513
|
+
const singleSymbol = d => !d.properties.counts || (this._rangeClassed && d.properties.cat != null);
|
|
10938
11514
|
layers.push(new IconLayer({
|
|
10939
11515
|
id: `ix-bubbles-${this.name}-g${this._iconGeneration}`,
|
|
10940
11516
|
data: combined, pickable: true,
|
|
10941
11517
|
getPosition: d => d.geometry.coordinates,
|
|
10942
|
-
getIcon: d => d
|
|
10943
|
-
? this._buildBubbleIcon(d.properties.counts, this.categoryColorsRgb, this._groupIsolation(d))
|
|
10944
|
-
: this._buildSingleIcon(this.categoryColorsRgb[d.properties.cat], fillOpacity,
|
|
10945
|
-
getSize: d =>
|
|
11518
|
+
getIcon: d => !singleSymbol(d)
|
|
11519
|
+
? this._buildBubbleIcon(d.properties.counts, this.categoryColorsRgb, this._groupIsolation(d), Array.isArray(this.style.symbols) ? this.style.symbols : null)
|
|
11520
|
+
: this._buildSingleIcon((this.categoryColorsRgb || [])[d.properties.cat], fillOpacity, shapeOf(d.properties), singleBorderColorRgb, singleBorderWidthPx),
|
|
11521
|
+
getSize: d => vRadius(sizeValueOf(d)) * 2,
|
|
10946
11522
|
// group icons bake a fixed 0.9 alpha; an explicit fillopacity scales it
|
|
10947
11523
|
// so a near-transparent theme doesn't show opaque 1-px-cell clusters
|
|
10948
|
-
getColor: d => [255, 255, 255, Math.round(this._iconAlpha(d) * (d
|
|
11524
|
+
getColor: d => [255, 255, 255, Math.round(this._iconAlpha(d) * (!singleSymbol(d) && this.style.fillopacity != null ? fillOpacity : 1))],
|
|
10949
11525
|
...(alignOf.active ? { getPixelOffset: alignOf } : {}),
|
|
10950
11526
|
sizeUnits: 'pixels',
|
|
10951
11527
|
billboard: true,
|
|
@@ -10959,7 +11535,7 @@
|
|
|
10959
11535
|
data: titleData, pickable: false,
|
|
10960
11536
|
getPosition: b => b.d.geometry.coordinates,
|
|
10961
11537
|
getText: b => b.title.text,
|
|
10962
|
-
getSize:
|
|
11538
|
+
getSize: b => b.title.f,
|
|
10963
11539
|
sizeUnits: 'pixels',
|
|
10964
11540
|
fontFamily: 'arial',
|
|
10965
11541
|
characterSet: 'auto',
|
|
@@ -10979,23 +11555,29 @@
|
|
|
10979
11555
|
// VALUES: bold value label centered on each bubble (see
|
|
10980
11556
|
// flatValueText/valuesFontSizePx above). Labels are their own
|
|
10981
11557
|
// layers, added last in the array so they draw on top of every icon
|
|
10982
|
-
// layer
|
|
10983
|
-
//
|
|
11558
|
+
// layer; a label under later-drawn charts is faded or dropped by
|
|
11559
|
+
// _occludeChartTexts, as flat's later chart groups cover it. Entries whose
|
|
10984
11560
|
// computed font size would be sub-pixel are dropped rather than
|
|
10985
11561
|
// rendered at getSize: 0, matching the real engine's own "too
|
|
10986
11562
|
// small to bother" gate.
|
|
10987
|
-
if (
|
|
11563
|
+
if (valuesShown) {
|
|
10988
11564
|
const valueScale = styleNum(this.style.valuescale) || 1;
|
|
10989
11565
|
const textOpts = this._valueTextOpts();
|
|
11566
|
+
const pointValueOf = valueTextOf || this._chartValueTextFn();
|
|
10990
11567
|
|
|
11568
|
+
// draw order of each chart (see chartTextTransmittance below); an
|
|
11569
|
+
// item maxcharts left out draws no chart, so no text either
|
|
11570
|
+
const orderOf = new Map(combined.map((d, k) => [d, k]));
|
|
10991
11571
|
const pointLabels = individual.reduce((out, d) => {
|
|
10992
|
-
|
|
10993
|
-
|
|
10994
|
-
|
|
10995
|
-
|
|
10996
|
-
const fontSize =
|
|
11572
|
+
if (!orderOf.has(d)) return out;
|
|
11573
|
+
const radius = vRadius(d.properties.value);
|
|
11574
|
+
// flat's VALUES label prints the value (nValuesA), not the size;
|
|
11575
|
+
// its font by flat's chart branch (_chartValueTextFn)
|
|
11576
|
+
const { text, fontSize, bold, baseline } = pointValueOf(d, radius);
|
|
10997
11577
|
if (fontSize > VALUES_MIN_FONT_PX) {
|
|
10998
|
-
|
|
11578
|
+
const o = alignOf(d);
|
|
11579
|
+
out.push({ geometry: d.geometry, text, fontSize, bold, color: resolveTextColor(this.style, contrastTextColor((this.categoryColorsRgb || [])[d.properties.cat])),
|
|
11580
|
+
pixelOffset: baseline ? [o[0], o[1] + baseline - 0.35 * fontSize] : o, order: orderOf.get(d) });
|
|
10999
11581
|
}
|
|
11000
11582
|
return out;
|
|
11001
11583
|
}, []);
|
|
@@ -11011,11 +11593,25 @@
|
|
|
11011
11593
|
// would misrepresent every category's own count as one summed
|
|
11012
11594
|
// number nobody's individual bubble actually shows.
|
|
11013
11595
|
const groupLabels = groups.reduce((out, d) => {
|
|
11596
|
+
if (!orderOf.has(d)) return out;
|
|
11014
11597
|
const counts = d.properties.counts;
|
|
11015
|
-
const outerRadiusPx =
|
|
11598
|
+
const outerRadiusPx = vRadius(d.properties.total);
|
|
11016
11599
|
const iconSizePx = outerRadiusPx * 2; // matches the cluster IconLayer's own getSize (*2) below
|
|
11017
11600
|
const pxPerCanvasUnit = iconSizePx / BUBBLE_ICON_SIZE;
|
|
11018
11601
|
const iso = this._groupIsolation(d);
|
|
11602
|
+
// a range-classed cell is one symbol (see the bubbles layer): its
|
|
11603
|
+
// value text as a single chart's (_chartValueTextFn)
|
|
11604
|
+
if (this._rangeClassed && d.properties.cat != null) {
|
|
11605
|
+
const value = d.properties.classTotal !== undefined ? d.properties.classTotal : d.properties.total;
|
|
11606
|
+
const v = pointValueOf({ properties: { value, cat: d.properties.cat } }, outerRadiusPx);
|
|
11607
|
+
if (v.fontSize > VALUES_MIN_FONT_PX) {
|
|
11608
|
+
const o = alignOf(d);
|
|
11609
|
+
out.push({ geometry: d.geometry, text: v.text, fontSize: v.fontSize, bold: v.bold,
|
|
11610
|
+
color: resolveTextColor(this.style, contrastTextColor((this.categoryColorsRgb || [])[d.properties.cat])),
|
|
11611
|
+
pixelOffset: v.baseline ? [o[0], o[1] + v.baseline - 0.35 * v.fontSize] : o, order: orderOf.get(d) });
|
|
11612
|
+
}
|
|
11613
|
+
return out;
|
|
11614
|
+
}
|
|
11019
11615
|
const { present, radii, offsets, fitScale } = (iso && isolatedBubblePackLayout(counts, BUBBLE_ICON_SIZE, iso)) || computeBubblePackLayout(counts, BUBBLE_ICON_SIZE);
|
|
11020
11616
|
|
|
11021
11617
|
present.forEach((p, i) => {
|
|
@@ -11026,14 +11622,25 @@
|
|
|
11026
11622
|
if (fontSize > VALUES_MIN_FONT_PX) {
|
|
11027
11623
|
out.push({
|
|
11028
11624
|
geometry: d.geometry, text, fontSize,
|
|
11029
|
-
color: resolveTextColor(this.style, contrastTextColor(this.categoryColorsRgb[p.i])),
|
|
11030
|
-
pixelOffset: [offsets[i].x * fitScale * pxPerCanvasUnit + alignOf(d)[0], offsets[i].y * fitScale * pxPerCanvasUnit + alignOf(d)[1]]
|
|
11625
|
+
color: resolveTextColor(this.style, contrastTextColor((this.categoryColorsRgb || [])[p.i])),
|
|
11626
|
+
pixelOffset: [offsets[i].x * fitScale * pxPerCanvasUnit + alignOf(d)[0], offsets[i].y * fitScale * pxPerCanvasUnit + alignOf(d)[1]],
|
|
11627
|
+
order: orderOf.get(d)
|
|
11031
11628
|
});
|
|
11032
11629
|
}
|
|
11033
11630
|
});
|
|
11034
11631
|
return out;
|
|
11035
11632
|
}, []);
|
|
11036
11633
|
|
|
11634
|
+
// a text under later charts fades by what still shows through them
|
|
11635
|
+
// (chartTextTransmittance), as flat's per-chart SVG groups paint
|
|
11636
|
+
// each later symbol and glow over it; nearly hidden ones are dropped
|
|
11637
|
+
const visible = this._occludeChartTexts(combined, pointLabels.concat(groupLabels), {
|
|
11638
|
+
liveZoom, globeCenter, alignOf, radiusOf, sizeValueOf, singleSymbol, shapeOf, fillOpacity, vRadius,
|
|
11639
|
+
singleBorderWidthPx
|
|
11640
|
+
});
|
|
11641
|
+
const keepVisible = labels => labels.filter(l => visible.has(l));
|
|
11642
|
+
const shownPointLabels = keepVisible(pointLabels), shownGroupLabels = keepVisible(groupLabels);
|
|
11643
|
+
|
|
11037
11644
|
const textLayerCommonProps = {
|
|
11038
11645
|
pickable: false,
|
|
11039
11646
|
getPosition: d => d.geometry.coordinates,
|
|
@@ -11043,11 +11650,61 @@
|
|
|
11043
11650
|
sizeUnits: 'pixels',
|
|
11044
11651
|
fontFamily: 'arial',
|
|
11045
11652
|
fontWeight: 'bold',
|
|
11653
|
+
// the glyphs of the actual texts, not deck's ASCII default — a
|
|
11654
|
+
// unit like " €" (the fuel-price page) is outside it ("Missing
|
|
11655
|
+
// character: €" and no glyph)
|
|
11656
|
+
characterSet: 'auto',
|
|
11046
11657
|
getTextAnchor: 'middle',
|
|
11047
11658
|
getAlignmentBaseline: 'center'
|
|
11048
11659
|
};
|
|
11049
|
-
|
|
11050
|
-
if (
|
|
11660
|
+
// flat's SYMBOL branch writes its values in normal weight
|
|
11661
|
+
if (shownPointLabels.length) layers.push(new TextLayer({ id: `ix-points-values-${this.name}`, data: shownPointLabels, getPixelOffset: d => d.pixelOffset, ...textLayerCommonProps,
|
|
11662
|
+
fontWeight: shownPointLabels[0].bold === false ? 'normal' : 'bold' }));
|
|
11663
|
+
if (shownGroupLabels.length) layers.push(new TextLayer({ id: `ix-cluster-values-${this.name}`, data: shownGroupLabels, getPixelOffset: d => d.pixelOffset, ...textLayerCommonProps,
|
|
11664
|
+
fontWeight: shownGroupLabels[0].bold === false ? 'normal' : 'bold' }));
|
|
11665
|
+
}
|
|
11666
|
+
|
|
11667
|
+
// LABEL: flat's chart label (maptheme.js 24533-24590) — a small text
|
|
11668
|
+
// below-right of each chart, the .style({labelfield}) record value
|
|
11669
|
+
// (the fire page's labelfield ANNO: the fire year "2 014"), black at
|
|
11670
|
+
// 0.7 opacity on a white 0.4-opacity rounded background box, font
|
|
11671
|
+
// 5·unit·textscale. Gated by labelupper/labellower (flat's own test
|
|
11672
|
+
// compares <= against BOTH), suppressed by NORMSIZE/ZOOM, a numeric
|
|
11673
|
+
// label formatted like a value (valuedecimals) plus labelunits.
|
|
11674
|
+
if (labelShown) {
|
|
11675
|
+
const labelFontPx = 5 * unitPx * (styleNum(this.style.textscale) || 1);
|
|
11676
|
+
if (labelFontPx > VALUES_MIN_FONT_PX) {
|
|
11677
|
+
const labelData = [];
|
|
11678
|
+
for (const d of individual) {
|
|
11679
|
+
if (!d.properties || !d.properties.raw) continue;
|
|
11680
|
+
// _chartLabelFn: text, and its place below-right of the chart
|
|
11681
|
+
const l = labelOf(d, radiusOf(d));
|
|
11682
|
+
if (!l) continue;
|
|
11683
|
+
const o = alignOf(d);
|
|
11684
|
+
// drawn centered: flat's baseline sits 0.35 font below the
|
|
11685
|
+
// middle of its background box (-0.95 .. +0.25 font)
|
|
11686
|
+
labelData.push({ geometry: d.geometry, text: l.text, fontSize: l.fontSize,
|
|
11687
|
+
pixelOffset: [l.offset[0] + o[0], l.offset[1] - 0.35 * l.fontSize + o[1]] });
|
|
11688
|
+
}
|
|
11689
|
+
if (labelData.length) layers.push(new TextLayer({
|
|
11690
|
+
id: `ix-labels-${this.name}`,
|
|
11691
|
+
data: labelData, pickable: false,
|
|
11692
|
+
getPosition: d => d.geometry.coordinates,
|
|
11693
|
+
getText: d => d.text,
|
|
11694
|
+
getSize: d => d.fontSize,
|
|
11695
|
+
getColor: [0, 0, 0, Math.round(0.7 * 255)],
|
|
11696
|
+
sizeUnits: 'pixels',
|
|
11697
|
+
fontFamily: 'arial',
|
|
11698
|
+
characterSet: 'auto',
|
|
11699
|
+
getTextAnchor: 'middle',
|
|
11700
|
+
getAlignmentBaseline: 'center',
|
|
11701
|
+
getPixelOffset: d => d.pixelOffset,
|
|
11702
|
+
background: true,
|
|
11703
|
+
getBackgroundColor: [255, 255, 255, Math.round(0.4 * 255)],
|
|
11704
|
+
backgroundPadding: [labelFontPx * 0.25, labelFontPx * 0.1],
|
|
11705
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
11706
|
+
}));
|
|
11707
|
+
}
|
|
11051
11708
|
}
|
|
11052
11709
|
|
|
11053
11710
|
return layers;
|
|
@@ -11083,12 +11740,25 @@
|
|
|
11083
11740
|
// - VALUES: each part's value (COUNT: its records; valuefield: the class name)
|
|
11084
11741
|
// - align / offsets as the SYMBOL branch: half size the normal radius
|
|
11085
11742
|
// A part's pick object carries its chart's properties (tooltip).
|
|
11743
|
+
// The same for a per-item chart over several value fields
|
|
11744
|
+
// (_isMultiFieldSequence): a part per field, max value the biggest
|
|
11745
|
+
// field value of any record (normalsizevalue overrides), the part
|
|
11746
|
+
// radius value-scaled as above; a colorscheme "none" part is only its
|
|
11747
|
+
// outline. RINGS nests the parts (ringsLayout); CENTERVALUES prints
|
|
11748
|
+
// only the first part's value (flat: nClass 0, maptheme.js 22216).
|
|
11086
11749
|
_buildSequenceChartLayers(charts, zoom, liveZoom) {
|
|
11087
11750
|
const st = this.style;
|
|
11088
|
-
const
|
|
11089
|
-
const
|
|
11751
|
+
const multi = !!this._multiFields && this._isMultiFieldSequence();
|
|
11752
|
+
const seqMax = multi ? (styleNum(st.normalsizevalue) || this._multiFieldPartMax) : this._sequenceMaxValue(zoom);
|
|
11090
11753
|
const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(st.scale) || 1);
|
|
11091
11754
|
const normalR = NORMAL_RADIUS_PX * unit;
|
|
11755
|
+
const sizePow = resolveSizePow(st, this.flags);
|
|
11756
|
+
const partRadius = multi
|
|
11757
|
+
? v => Math.max(VALUE_RADIUS_MIN, normalR * Math.pow(Math.abs(v) / seqMax, 1 / sizePow))
|
|
11758
|
+
: v => valueRadius(v, liveZoom, st, this.mapOptions, this.flags, seqMax);
|
|
11759
|
+
const rings = flatFlag(this.flags, 'RINGS');
|
|
11760
|
+
const centerValues = flatFlag(this.flags, 'CENTERVALUES');
|
|
11761
|
+
const noFill = this._partNoFill || new Set();
|
|
11092
11762
|
const nCats = this.categoryLabels ? this.categoryLabels.length : 1;
|
|
11093
11763
|
const marking = this._markedClasses.size && !this._onMarksChanged;
|
|
11094
11764
|
const gray = marking && String(st.evidence || 'isolate') === 'isolate_gray';
|
|
@@ -11097,11 +11767,13 @@
|
|
|
11097
11767
|
const borderRgb = st.linecolor && styleLineColor(st.linecolor) !== 'none' ? hexOrNamedToRgb(styleLineColor(st.linecolor)) : null;
|
|
11098
11768
|
const lineWidth = styleNum(st.linewidth) || 1;
|
|
11099
11769
|
const symbols = Array.isArray(st.symbols) ? st.symbols : null;
|
|
11100
|
-
const shapeOf = i => { const sh = symbols && (symbols[i] || symbols[0]); return
|
|
11770
|
+
const shapeOf = i => { const sh = symbols && (symbols[i] || symbols[0]); return normalizeSymbolShape(sh); };
|
|
11101
11771
|
const colorOf = i => this.categoryColorsRgb[i] || [128, 128, 128];
|
|
11102
11772
|
const parts = [], glows = [];
|
|
11103
11773
|
for (const d of charts) {
|
|
11104
|
-
|
|
11774
|
+
const counts = d.properties.parts || d.properties.counts;
|
|
11775
|
+
let layout = rings ? ringsLayout(counts, partRadius)
|
|
11776
|
+
: sequenceLayout(counts, this.flags, partRadius, { maxRadius: normalR, maxValue: seqMax, nParts: nCats, rangeScale: styleNum(st.rangescale) || 0 });
|
|
11105
11777
|
if (!layout.length) continue;
|
|
11106
11778
|
// GLOW belongs to the first part (sorted) or to every part, and
|
|
11107
11779
|
// hides with it when its class is not marked
|
|
@@ -11155,7 +11827,7 @@
|
|
|
11155
11827
|
data: parts, pickable: true,
|
|
11156
11828
|
getPosition: d => d.geometry.coordinates,
|
|
11157
11829
|
// the stroke, in the icon raster's pixels (the icon is drawn at 2r)
|
|
11158
|
-
getIcon: d => this._buildSingleIcon(colorOf(d.part.i), fillOpacity, shapeOf(d.part.i), borderRgb,
|
|
11830
|
+
getIcon: d => this._buildSingleIcon(colorOf(d.part.i), noFill.has(d.part.i) ? 0 : fillOpacity, shapeOf(d.part.i), borderRgb,
|
|
11159
11831
|
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),
|
|
11160
11832
|
getSize: d => d.part.r * 2,
|
|
11161
11833
|
getPixelOffset: offsetOf,
|
|
@@ -11168,20 +11840,22 @@
|
|
|
11168
11840
|
// class name — formatted with valuedecimals (else none) plus the unit;
|
|
11169
11841
|
// font min(1, 3.2 / L) · r · valuescale, L the length of the theme's
|
|
11170
11842
|
// biggest value text (szMaxText), normal weight, in valuecolor /
|
|
11171
|
-
// textcolor, else ChartColors.textColor of the part color
|
|
11172
|
-
|
|
11843
|
+
// textcolor, else ChartColors.textColor of the part color.
|
|
11844
|
+
// CENTERVALUES: only on the first part (class 0), its own value
|
|
11845
|
+
if ((this.flags.has('VALUES') || centerValues) && !valuesHiddenByScale(st, zoom)) {
|
|
11173
11846
|
const valueScale = styleNum(st.valuescale) || 1;
|
|
11174
11847
|
const { noBreaks } = this._valueTextOpts();
|
|
11175
11848
|
const units = st.units ? String(st.units) : '';
|
|
11176
11849
|
const unit = units ? (units[0] === '.' ? '' : ' ') + units : '';
|
|
11177
11850
|
const unitText = unit.length <= 5 ? unit : '';
|
|
11178
|
-
const dec =
|
|
11851
|
+
const dec = explicitValueDecimals(st) || 0;
|
|
11179
11852
|
const fmt = v => flatGroupedValue(v, dec, noBreaks) + unitText;
|
|
11180
11853
|
const maxLen = fmt(seqMax).length;
|
|
11181
11854
|
const textOverride = st.valuecolor || st.textcolor;
|
|
11182
11855
|
const counting = flatFlag(this.flags, 'COUNT');
|
|
11183
11856
|
const labels = [];
|
|
11184
11857
|
parts.forEach(d => {
|
|
11858
|
+
if (centerValues && d.part.i !== 0) return;
|
|
11185
11859
|
const rec = this._categoryValueRecord(d.part.i);
|
|
11186
11860
|
const name = rec ? String(rec[st.valuefield]) : null;
|
|
11187
11861
|
const t = counting && d.properties.recordCounts ? d.properties.recordCounts[d.part.i] : d.part.v;
|
|
@@ -11241,6 +11915,69 @@
|
|
|
11241
11915
|
// the class name (the records' own valuefield value) — otherwise none
|
|
11242
11916
|
_categoryValueRecord(i) { return categoryValueRecord(this.style, this.binding, this.flags, this.categoryLabels, i); }
|
|
11243
11917
|
|
|
11918
|
+
// A single chart's VALUES text: (item, its radius px) → { text,
|
|
11919
|
+
// fontSize, extent } — extent [x0, y0, x1, y1] in px around the chart
|
|
11920
|
+
// center (y down), as the box encloses it. The font by flat's branch:
|
|
11921
|
+
// - SYMBOL (maptheme.js 22200-22202, 21705-21708): min(1, 3.2 / L)
|
|
11922
|
+
// (hexagon 2.7) · r · valuescale, L the length of the theme's biggest
|
|
11923
|
+
// value text (szMaxText: the max value formatted, plus the unit up to
|
|
11924
|
+
// 5 characters, 21479) — normal weight, baseline 0.33 font below the
|
|
11925
|
+
// center (fire areas SIF chart: r 60, valuescale 3, "7 242 ha" → 72)
|
|
11926
|
+
// - BUBBLE: valuesFontSizePx, bold, centered
|
|
11927
|
+
_chartValueTextFn() {
|
|
11928
|
+
const valueScale = styleNum(this.style.valuescale) || 1;
|
|
11929
|
+
const textOpts = this._valueTextOpts();
|
|
11930
|
+
const symbol = flatChartBranch(this.flags) === 'symbol';
|
|
11931
|
+
let maxLen = 1;
|
|
11932
|
+
if (symbol) {
|
|
11933
|
+
const vMax = this._valueMax !== undefined ? this._valueMax : (this._valueTextRange || [])[1];
|
|
11934
|
+
if (isFinite(vMax)) maxLen = Math.max(1, flatValueText(null, null, vMax, this.style, this.flags, textOpts).length);
|
|
11935
|
+
}
|
|
11936
|
+
return (d, r) => {
|
|
11937
|
+
const p = d.properties;
|
|
11938
|
+
const text = flatValueText(p.raw, this.binding.title && p.raw ? p.raw[this.binding.title] : undefined,
|
|
11939
|
+
p.classValue !== undefined ? p.classValue : p.value, this.style, this.flags, textOpts);
|
|
11940
|
+
if (!symbol) {
|
|
11941
|
+
const fontSize = valuesFontSizePx(r, text, valueScale);
|
|
11942
|
+
const tw = titleTextWidth(text, fontSize);
|
|
11943
|
+
return { text, fontSize, bold: true, extent: [-tw / 2, -fontSize / 2, tw / 2, fontSize / 2] };
|
|
11944
|
+
}
|
|
11945
|
+
const k = this._resolveSymbolShape(p) === 'hexagon' ? 2.7 : 3.2;
|
|
11946
|
+
const fontSize = Math.min(1, k / maxLen) * r * valueScale;
|
|
11947
|
+
const tw = titleTextWidth(text, fontSize);
|
|
11948
|
+
return { text, fontSize, bold: false, baseline: 0.33 * fontSize,
|
|
11949
|
+
extent: [-tw / 2, (0.33 - 0.905) * fontSize, tw / 2, (0.33 + 0.212) * fontSize] };
|
|
11950
|
+
};
|
|
11951
|
+
}
|
|
11952
|
+
|
|
11953
|
+
// A single chart's LABEL (.style({labelfield}), maptheme.js 24533-24590):
|
|
11954
|
+
// (item, its radius px) → { text, fontSize, offset, extent } or null.
|
|
11955
|
+
// Font 5u · textscale; a number formatted like a value — valuedecimals
|
|
11956
|
+
// when set (flat's nValueDecimals is the raw style string, so "0"
|
|
11957
|
+
// counts), else 2 — plus labelunits. The background box is the wider of
|
|
11958
|
+
// the text and (length + 2) · font / 2, 1.2 font high from 0.95 font
|
|
11959
|
+
// above the baseline; the label sits at (width / 3, font / 3 + the
|
|
11960
|
+
// chart's anchor y: r + 5u). Fire areas page: "2 010" at (116.7, 193.3)
|
|
11961
|
+
// units, box 350 × 120.
|
|
11962
|
+
_chartLabelFn(unitPx) {
|
|
11963
|
+
const f = 5 * unitPx * (styleNum(this.style.textscale) || 1);
|
|
11964
|
+
const dec = explicitValueDecimals(this.style);
|
|
11965
|
+
const { noBreaks } = this._valueTextOpts();
|
|
11966
|
+
const units = String(this.style.labelunits || '');
|
|
11967
|
+
return (d, r) => {
|
|
11968
|
+
const raw = d.properties && d.properties.raw;
|
|
11969
|
+
const v = raw && raw[this.style.labelfield];
|
|
11970
|
+
let text = v == null || v === '' ? '' : String(v);
|
|
11971
|
+
if (!text) return null;
|
|
11972
|
+
const num = parseFloat(text.replace(',', '.'));
|
|
11973
|
+
text = (!isNaN(num) ? flatGroupedValue(num, dec != null ? dec : 2, noBreaks) : text) + units;
|
|
11974
|
+
const width = Math.max(titleTextWidth(text, f), (text.length + 2) * f * 0.5);
|
|
11975
|
+
const x = width / 3, y = f / 3 + r + 5 * unitPx;
|
|
11976
|
+
return { text, fontSize: f, offset: [x, y], width,
|
|
11977
|
+
extent: [x - width / 2, y - 0.95 * f, x + width / 2, y + 0.25 * f] };
|
|
11978
|
+
};
|
|
11979
|
+
}
|
|
11980
|
+
|
|
11244
11981
|
// value-text options (flatValueText): flat's formatValue NOBREAKS for a
|
|
11245
11982
|
// theme of values within 1000-3000, the max value for the decimals
|
|
11246
11983
|
_valueTextOpts() {
|
|
@@ -11285,6 +12022,67 @@
|
|
|
11285
12022
|
// the item color (stroke font/7, opacity 0.5).
|
|
11286
12023
|
// Both moved by the chart position (flatChartAlignOffset). The text is
|
|
11287
12024
|
// the hover target of TEXTONLY, the box of LABEL.
|
|
12025
|
+
// The charts of _buildChartLayers' bubbles layer (and its GLOW / AURA)
|
|
12026
|
+
// as chartTextTransmittance's occluders, in draw order and screen
|
|
12027
|
+
// pixels, and each text's resulting alpha: the text's color gets it,
|
|
12028
|
+
// the returned Set holds the texts still visible. Screen pixels:
|
|
12029
|
+
// Web Mercator world pixels at the live zoom; under globe a local
|
|
12030
|
+
// equal-area frame around the view center at the scale MapLibre's
|
|
12031
|
+
// globe matches Mercator's there (the limb's foreshortening ignored).
|
|
12032
|
+
_occludeChartTexts(combined, texts, o) {
|
|
12033
|
+
const visible = new Set();
|
|
12034
|
+
if (!texts.length) return visible;
|
|
12035
|
+
const toRad = Math.PI / 180;
|
|
12036
|
+
let screenOf;
|
|
12037
|
+
if (o.globeCenter) {
|
|
12038
|
+
const s = 512 * Math.pow(2, o.liveZoom) / 360 / Math.cos(o.globeCenter.lat * toRad);
|
|
12039
|
+
screenOf = c => [(((c[0] - o.globeCenter.lng) % 360 + 540) % 360 - 180) * Math.cos(c[1] * toRad) * s, -c[1] * s];
|
|
12040
|
+
} else {
|
|
12041
|
+
screenOf = c => { const p = lngLatToWorldPixel(c[0], c[1], o.liveZoom); return [p.x, p.y]; };
|
|
12042
|
+
}
|
|
12043
|
+
const glowOn = this.flags.has('GLOW') && !glowHiddenByScale(this.style, o.liveZoom);
|
|
12044
|
+
const auraOn = !glowOn && this.flags.has('AURA') && !this.flags.has('ZOOM');
|
|
12045
|
+
const symbols = Array.isArray(this.style.symbols) ? this.style.symbols : null;
|
|
12046
|
+
const charts = combined.map(d => {
|
|
12047
|
+
const [x, y] = screenOf(d.geometry.coordinates);
|
|
12048
|
+
const a = o.alignOf(d), r = o.radiusOf(d), iconAlpha = this._iconAlpha(d) / 255;
|
|
12049
|
+
let parts;
|
|
12050
|
+
if (o.singleSymbol(d)) {
|
|
12051
|
+
// the icon's shape is inset by half its border (_buildSingleIcon)
|
|
12052
|
+
const shape = o.shapeOf(d.properties);
|
|
12053
|
+
parts = [{ dx: 0, dy: 0, r: r * (1 - o.singleBorderWidthPx / BUBBLE_ICON_SIZE), shape, alpha: o.fillOpacity * iconAlpha }];
|
|
12054
|
+
} else {
|
|
12055
|
+
// _buildBubbleIcon's packed parts, its baked 0.9 alpha scaled by an
|
|
12056
|
+
// explicit fillopacity like the bubbles layer's getColor
|
|
12057
|
+
const counts = d.properties.counts, iso = this._groupIsolation(d);
|
|
12058
|
+
const { present, radii, offsets, fitScale } = (iso && isolatedBubblePackLayout(counts, BUBBLE_ICON_SIZE, iso)) || computeBubblePackLayout(counts, BUBBLE_ICON_SIZE);
|
|
12059
|
+
const px = 2 * r / BUBBLE_ICON_SIZE * fitScale;
|
|
12060
|
+
const alpha = 0.9 * (this.style.fillopacity != null ? o.fillOpacity : 1) * iconAlpha;
|
|
12061
|
+
parts = present.map((p, i) => ({ dx: offsets[i].x * px, dy: offsets[i].y * px, r: radii[i] * px,
|
|
12062
|
+
shape: symbols ? normalizeSymbolShape(symbols[Math.min(p.i, symbols.length - 1)]) : 'circle', alpha }));
|
|
12063
|
+
}
|
|
12064
|
+
if (auraOn) parts.push({ dx: 0, dy: 0, r: r * 1.3, shape: 'circle', alpha: 0.3 * iconAlpha });
|
|
12065
|
+
return {
|
|
12066
|
+
x: x + a[0], y: y + a[1], parts,
|
|
12067
|
+
glowR: glowOn ? o.vRadius(o.sizeValueOf(d)) * (d.properties.counts ? 9 : 11) / 2 : 0,
|
|
12068
|
+
glowAlpha: iconAlpha
|
|
12069
|
+
};
|
|
12070
|
+
});
|
|
12071
|
+
// the text box: Arial digits are ~0.56 em wide, ~0.7 em high
|
|
12072
|
+
const boxes = texts.map(t => {
|
|
12073
|
+
const [x, y] = screenOf(t.geometry.coordinates);
|
|
12074
|
+
return { order: t.order, x: x + t.pixelOffset[0], y: y + t.pixelOffset[1], w: 0.56 * t.fontSize * String(t.text).length, h: 0.7 * t.fontSize };
|
|
12075
|
+
});
|
|
12076
|
+
const shown = chartTextTransmittance(charts, boxes);
|
|
12077
|
+
texts.forEach((t, i) => {
|
|
12078
|
+
if (shown[i] < CHART_TEXT_MIN_TRANSMITTANCE) return;
|
|
12079
|
+
const c = t.color || [0, 0, 0];
|
|
12080
|
+
if (shown[i] < 1) t.color = [c[0], c[1], c[2], Math.round((c[3] != null ? c[3] : 255) * shown[i])];
|
|
12081
|
+
visible.add(t);
|
|
12082
|
+
});
|
|
12083
|
+
return visible;
|
|
12084
|
+
}
|
|
12085
|
+
|
|
11288
12086
|
_buildLabelChartLayers(combined, zoom, liveZoom, sizeValueOf) {
|
|
11289
12087
|
const textOnly = this.flags.has('TEXTONLY');
|
|
11290
12088
|
const showValues = this.flags.has('VALUES') && !valuesHiddenByScale(this.style, zoom);
|
|
@@ -11431,14 +12229,28 @@
|
|
|
11431
12229
|
// DOWN, confirmed against a real ported page's own comment ("il motore
|
|
11432
12230
|
// disegna il triangolo con la punta in basso" — "the engine draws the
|
|
11433
12231
|
// triangle with the tip at the bottom").
|
|
12232
|
+
// flat's symbol shapes (maptheme.js 21625-21680). Geometry follows flat
|
|
12233
|
+
// where GL didn't have an established shape already (square, diamond,
|
|
12234
|
+
// triangle keep GL's own proportions — pages already shipped with them):
|
|
12235
|
+
// roundrect — rect 1.6r, rounded 0.3r; label — wide flat rect 1.6r×0.8r;
|
|
12236
|
+
// carot — flat's alias of diamond; hexagon — vertices (±r,0),(±r/2,±r·sin60°);
|
|
12237
|
+
// cross — flat's 8-arm path (A=0.4r, B=0.8r); empty — the circle
|
|
12238
|
+
// geometry, but _buildSingleIcon gives it flat's transparent fill +
|
|
12239
|
+
// gray stroke (fill-opacity:0, stroke #888888)
|
|
11434
12240
|
function drawSymbolPath(ctx, shape, cx, cy, r) {
|
|
11435
12241
|
switch (shape) {
|
|
12242
|
+
// flat's per-part SYMBOL square (maptheme.js 22037): side 2r — with
|
|
12243
|
+
// GRIDSIZE the whole cell (the fuel-price grid, contiguous squares)
|
|
12244
|
+
case 'squarefull':
|
|
12245
|
+
ctx.rect(cx - r, cy - r, 2 * r, 2 * r);
|
|
12246
|
+
break;
|
|
11436
12247
|
case 'square': {
|
|
11437
12248
|
const s = r * Math.SQRT2;
|
|
11438
12249
|
ctx.rect(cx - s / 2, cy - s / 2, s, s);
|
|
11439
12250
|
break;
|
|
11440
12251
|
}
|
|
11441
12252
|
case 'diamond':
|
|
12253
|
+
case 'carot': // flat: carot and diamond are the same rotated rect
|
|
11442
12254
|
ctx.moveTo(cx, cy - r);
|
|
11443
12255
|
ctx.lineTo(cx + r, cy);
|
|
11444
12256
|
ctx.lineTo(cx, cy + r);
|
|
@@ -11449,10 +12261,58 @@
|
|
|
11449
12261
|
ctx.lineTo(cx + r, cy - r * 0.6);
|
|
11450
12262
|
ctx.lineTo(cx, cy + r);
|
|
11451
12263
|
break;
|
|
12264
|
+
case 'roundrect': {
|
|
12265
|
+
const s = r * 0.8;
|
|
12266
|
+
if (ctx.roundRect) ctx.roundRect(cx - s, cy - s, s * 2, s * 2, r * 0.3);
|
|
12267
|
+
else ctx.rect(cx - s, cy - s, s * 2, s * 2);
|
|
12268
|
+
break;
|
|
12269
|
+
}
|
|
12270
|
+
case 'label': {
|
|
12271
|
+
const w = r * 0.8, h = r * 0.4;
|
|
12272
|
+
ctx.rect(cx - w, cy - h, w * 2, h * 2);
|
|
12273
|
+
break;
|
|
12274
|
+
}
|
|
12275
|
+
case 'hexagon': {
|
|
12276
|
+
const A = r / 2, B = r * Math.sin(Math.PI / 3);
|
|
12277
|
+
ctx.moveTo(cx - r, cy);
|
|
12278
|
+
ctx.lineTo(cx - A, cy - B);
|
|
12279
|
+
ctx.lineTo(cx + A, cy - B);
|
|
12280
|
+
ctx.lineTo(cx + r, cy);
|
|
12281
|
+
ctx.lineTo(cx + A, cy + B);
|
|
12282
|
+
ctx.lineTo(cx - A, cy + B);
|
|
12283
|
+
break;
|
|
12284
|
+
}
|
|
12285
|
+
case 'cross': {
|
|
12286
|
+
// flat's own path, vertex for vertex: a plus of arm thickness A
|
|
12287
|
+
// reaching to ±(B + A/2) (maptheme.js 21652-21655)
|
|
12288
|
+
const A = r * 0.4, B = r * 0.8, E = B + A / 2;
|
|
12289
|
+
ctx.moveTo(cx, cy - E);
|
|
12290
|
+
ctx.lineTo(cx + A / 2, cy - E);
|
|
12291
|
+
ctx.lineTo(cx + A / 2, cy - A / 2);
|
|
12292
|
+
ctx.lineTo(cx + E, cy - A / 2);
|
|
12293
|
+
ctx.lineTo(cx + E, cy + A / 2);
|
|
12294
|
+
ctx.lineTo(cx + A / 2, cy + A / 2);
|
|
12295
|
+
ctx.lineTo(cx + A / 2, cy + E);
|
|
12296
|
+
ctx.lineTo(cx - A / 2, cy + E);
|
|
12297
|
+
ctx.lineTo(cx - A / 2, cy + A / 2);
|
|
12298
|
+
ctx.lineTo(cx - E, cy + A / 2);
|
|
12299
|
+
ctx.lineTo(cx - E, cy - A / 2);
|
|
12300
|
+
ctx.lineTo(cx - A / 2, cy - A / 2);
|
|
12301
|
+
ctx.lineTo(cx - A / 2, cy - E);
|
|
12302
|
+
break;
|
|
12303
|
+
}
|
|
12304
|
+
case 'empty':
|
|
11452
12305
|
default: // circle
|
|
11453
12306
|
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
11454
12307
|
}
|
|
11455
12308
|
}
|
|
12309
|
+
// the shape names flat's szSymbol switch knows (maptheme.js 21625) —
|
|
12310
|
+
// anything else falls back to circle, never silently dropped
|
|
12311
|
+
const SYMBOL_SHAPE_RE = /^(circle|square|diamond|triangle|roundrect|carot|hexagon|cross|empty|label)$/;
|
|
12312
|
+
function normalizeSymbolShape(name) {
|
|
12313
|
+
const n = String(name || '').toLowerCase();
|
|
12314
|
+
return SYMBOL_SHAPE_RE.test(n) ? n : 'circle';
|
|
12315
|
+
}
|
|
11456
12316
|
|
|
11457
12317
|
// a color's own opacity, 0..1, as SVG paints it in flat: "none" (SVG's
|
|
11458
12318
|
// no paint — a polygon with only its outline, or a class not painted)
|
|
@@ -11586,7 +12446,7 @@
|
|
|
11586
12446
|
// map named by szMap (its Map() options name) or, without a name, the
|
|
11587
12447
|
// last built map; called while the map is still building, they run once
|
|
11588
12448
|
// it is ready.
|
|
11589
|
-
const MAP_HANDLE_METHODS = ['replace', 'add', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
12449
|
+
const MAP_HANDLE_METHODS = ['replace', 'add', 'newTheme', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
11590
12450
|
'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'setProjection', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible', 'setThemeTimeFrame', 'attribution'];
|
|
11591
12451
|
const _mapHandles = Object.create(null);
|
|
11592
12452
|
function makeMapHandle(szMap) {
|
|
@@ -12251,6 +13111,8 @@
|
|
|
12251
13111
|
htmlgui_getAttributionString: () => (_lastMapApi && _lastMapApi.getAttribution ? _lastMapApi.getAttribution() : ''),
|
|
12252
13112
|
changeThemeStyle: compatChangeThemeStyle,
|
|
12253
13113
|
removeTheme: compatRemoveTheme,
|
|
13114
|
+
// flat's ixmaps.newTheme(title, theme, flag) — on the last built map
|
|
13115
|
+
newTheme: (title, theme, flag) => _mapHandle.newTheme(title, theme, flag),
|
|
12254
13116
|
// flat's time slider: show the records of a theme (null = all themes) whose timefield lies in [min, max)
|
|
12255
13117
|
setThemeTimeFrame: (szId, nMin, nMax) => _mapHandle.setThemeTimeFrame(szId, nMin, nMax),
|
|
12256
13118
|
setBasemapOpacity: compatSetBasemapOpacity,
|
|
@@ -12277,7 +13139,7 @@
|
|
|
12277
13139
|
// test-only: lets test/unit/*.test.mjs call pure internals directly (the
|
|
12278
13140
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
12279
13141
|
global.__ixmapsGlInternals = {
|
|
12280
|
-
IXMAPS_GL_VERSION, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout, multiQuadOffsets, pixelOffsetLngLat,
|
|
13142
|
+
IXMAPS_GL_VERSION, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, chartTextTransmittance, glowProfileAlpha, insideSymbolShape, sequenceLayout, ringsLayout, multiQuadOffsets, pixelOffsetLngLat,
|
|
12281
13143
|
normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
12282
13144
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
12283
13145
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|
|
@@ -12287,7 +13149,8 @@
|
|
|
12287
13149
|
flatToMapLibreZoom, mapLibreToFlatZoom, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
|
|
12288
13150
|
fetchLayerData, parseCsvText, dataTableRows, geometryRowsToFeatureCollection, filterFlatValues, applyField100, field100Binding, rangeClassLegendTotals, resolveAggregateValue, classValueSeparate, cellAggregatedValues, oneHot, groupCoLocated, aggregateOnGrid, GridAggregateIndex, flatRangeParts, aggregateField100, valueRadius, itemPlotGeometry, itemAnchor, objectZoomFactor, resolveZoomReference, defaultNormalSizeValue,
|
|
12289
13151
|
applyWhereFilter, joinChartPositions, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
12290
|
-
|
|
13152
|
+
drawSymbolPath, normalizeSymbolShape,
|
|
13153
|
+
flatFormatValue, themeStyleArgs, flatChangedStyleValue, explicitValueDecimals, legendDecimals,
|
|
12291
13154
|
dataCacheKey, dataCacheDisabled, cachedLayerData, featuresBounds, wantsZoomToExtent, legendIsOn,
|
|
12292
13155
|
};
|
|
12293
13156
|
global.__setFilter = __setFilter;
|