ixmaps-gl 0.2.16 → 0.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/ixmaps-gl.js +571 -72
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -243,7 +243,7 @@ of your own project files gl can render.
|
|
|
243
243
|
Load the engine from jsDelivr, pinned to a release tag (or `@main` for the latest commit):
|
|
244
244
|
|
|
245
245
|
```html
|
|
246
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
246
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.17/ixmaps-gl.js"></script>
|
|
247
247
|
```
|
|
248
248
|
|
|
249
249
|
or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmaps-gl)
|
|
@@ -251,13 +251,13 @@ or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmap
|
|
|
251
251
|
browser script, which defines the global `ixmaps`):
|
|
252
252
|
|
|
253
253
|
```html
|
|
254
|
-
<script src="https://unpkg.com/ixmaps-gl@0.2.
|
|
254
|
+
<script src="https://unpkg.com/ixmaps-gl@0.2.17/ixmaps-gl.js"></script>
|
|
255
255
|
```
|
|
256
256
|
|
|
257
257
|
or use a local copy (`<script src="ixmaps-gl.js"></script>`). A minimal page:
|
|
258
258
|
|
|
259
259
|
```html
|
|
260
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
260
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.17/ixmaps-gl.js"></script>
|
|
261
261
|
<div id="map_div" style="position:absolute;inset:0;"></div>
|
|
262
262
|
|
|
263
263
|
<script>
|
package/ixmaps-gl.js
CHANGED
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
// ---------------------------------------------------------------
|
|
83
83
|
// this engine's release (= package.json "version", checked by
|
|
84
84
|
// test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
|
|
85
|
-
const IXMAPS_GL_VERSION = '0.2.
|
|
85
|
+
const IXMAPS_GL_VERSION = '0.2.17';
|
|
86
86
|
|
|
87
87
|
const LIB_URLS = {
|
|
88
88
|
// Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
|
|
@@ -716,6 +716,9 @@
|
|
|
716
716
|
// another theme's data: .data({name})). See loadNamedTheme in build().
|
|
717
717
|
const _namedDataWaiters = new Map(); // name → Set of resolve(table)
|
|
718
718
|
const _namedDataListeners = new Map(); // name → Set of fn(table)
|
|
719
|
+
// names whose next publications feed only the theme that asked for them
|
|
720
|
+
// (a FEATURE's dependent broker refresh, see featureDependents) → count
|
|
721
|
+
const _quietNamedData = new Map();
|
|
719
722
|
function namedTable(name) {
|
|
720
723
|
try { const t = global[name]; return t && typeof t === 'object' && typeof t.json === 'function' ? t : null; } catch (e) { return null; }
|
|
721
724
|
}
|
|
@@ -744,6 +747,7 @@
|
|
|
744
747
|
const waiters = _namedDataWaiters.get(name);
|
|
745
748
|
if (waiters) [...waiters].forEach(resolve => resolve(table));
|
|
746
749
|
const listeners = _namedDataListeners.get(name);
|
|
750
|
+
if (_quietNamedData.get(name)) return;
|
|
747
751
|
// after the handing-over code finished (a broker's own theme first)
|
|
748
752
|
if (listeners && listeners.size && table) setTimeout(() => [...listeners].forEach(fn => fn(table)), 0);
|
|
749
753
|
}
|
|
@@ -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',
|
|
@@ -2589,6 +2611,16 @@
|
|
|
2589
2611
|
return String(text).split(' ').map(w => (dict["'" + w + "'"] !== undefined ? String(dict["'" + w + "'"]) : w)).join(' ');
|
|
2590
2612
|
};
|
|
2591
2613
|
}
|
|
2614
|
+
// make the map container the positioning context of what the engine
|
|
2615
|
+
// mounts inside it (splash, pinned tooltip, legends) — only when it is
|
|
2616
|
+
// unpositioned. The RENDERED position decides, never el.style (inline
|
|
2617
|
+
// only): a page's #map_div { position:absolute; top:0; bottom:0 } from a
|
|
2618
|
+
// stylesheet reads as "" there, and overwriting it with relative drops
|
|
2619
|
+
// the top/bottom stretch — the div's height collapses to 0, a blank map
|
|
2620
|
+
// (accidents_app.html from 0.2.15 on)
|
|
2621
|
+
function anchorInside(el) {
|
|
2622
|
+
if (el && getComputedStyle(el).position === 'static') el.style.position = 'relative';
|
|
2623
|
+
}
|
|
2592
2624
|
function showSplash(el, text) {
|
|
2593
2625
|
ensureSplashStyle();
|
|
2594
2626
|
// Checks the actual RENDERED position, not el.style (the inline
|
|
@@ -2602,7 +2634,7 @@
|
|
|
2602
2634
|
// height silently collapsed to 0 — invisible map, invisible splash,
|
|
2603
2635
|
// both zero-height. Only truly static (unpositioned) elements need
|
|
2604
2636
|
// this at all.
|
|
2605
|
-
|
|
2637
|
+
anchorInside(el);
|
|
2606
2638
|
const splash = document.createElement('div');
|
|
2607
2639
|
splash.className = 'ixmaps-splash';
|
|
2608
2640
|
splash.innerHTML = '<div class="ixmaps-splash-mark"><span class="ixmaps-splash-ping"></span><span class="ixmaps-splash-dot"></span></div>' +
|
|
@@ -3144,7 +3176,7 @@
|
|
|
3144
3176
|
// relative pixels, and styling the page's own body (relative) breaks
|
|
3145
3177
|
// any page element absolutely positioned against it (AirBnB's
|
|
3146
3178
|
// #onmap-buttons on a zero-height all-float body flew off-screen)
|
|
3147
|
-
el
|
|
3179
|
+
anchorInside(el);
|
|
3148
3180
|
el.appendChild(pinnedTooltipEl);
|
|
3149
3181
|
}
|
|
3150
3182
|
return { getTooltip, onClick, mount };
|
|
@@ -3341,7 +3373,7 @@
|
|
|
3341
3373
|
// panel's max-height resolves against the MAP's height — the stack's
|
|
3342
3374
|
// former parent anchor collapsed it to 0 on pages whose body has
|
|
3343
3375
|
// only floated children (height 0)
|
|
3344
|
-
el
|
|
3376
|
+
anchorInside(el);
|
|
3345
3377
|
el.appendChild(legendStack);
|
|
3346
3378
|
return legendStack;
|
|
3347
3379
|
};
|
|
@@ -3387,7 +3419,7 @@
|
|
|
3387
3419
|
rt._triggerRedraw = () => { refresh(); };
|
|
3388
3420
|
if (subThemeCapable(rt)) rt._onMarksChanged = () => updateSubTheme(rt);
|
|
3389
3421
|
if (!legendOn || !el.parentElement || !legendApplies(rt)) return;
|
|
3390
|
-
el
|
|
3422
|
+
anchorInside(el);
|
|
3391
3423
|
{
|
|
3392
3424
|
// .type("...|NOLEGEND") — the fourth real legend-related type()
|
|
3393
3425
|
// token: opts a theme OUT of the legend entirely (real engine's
|
|
@@ -3472,6 +3504,17 @@
|
|
|
3472
3504
|
// themselves authored with their own leading space (e.g.
|
|
3473
3505
|
// " MW") in real pages.
|
|
3474
3506
|
const legendUnit = rt.style.legendunits || rt.style.units || '';
|
|
3507
|
+
// a per-item chart over several value fields: the row values as
|
|
3508
|
+
// flat's legend prints them (legend.js 610, 810: __formatValue
|
|
3509
|
+
// with valuedecimals, else 2, "BLANK" — whole numbers without
|
|
3510
|
+
// decimals), a colorscheme "none" part as an outline in linecolor
|
|
3511
|
+
const multiFieldLegend = !!rt._multiFields && rt._isMultiFieldSequence();
|
|
3512
|
+
const legendTotalText = v => (multiFieldLegend
|
|
3513
|
+
? flatFormatValue(v, legendDecimals(rt.style), 'BLANK')
|
|
3514
|
+
: rt._formatTooltipValue(v));
|
|
3515
|
+
const outlineRgb = rt.style.linecolor && styleLineColor(rt.style.linecolor) !== 'none' ? hexOrNamedToRgb(styleLineColor(rt.style.linecolor)) : [128, 128, 128];
|
|
3516
|
+
const legendFill = i => (multiFieldLegend && rt._partNoFill && rt._partNoFill.has(i)
|
|
3517
|
+
? `transparent;box-shadow:inset 0 0 0 1px rgb(${outlineRgb.join(',')})` : null);
|
|
3475
3518
|
// row labels go into innerHTML: the page's own .style({label})
|
|
3476
3519
|
// stays HTML, as in flat; categories read from the data are
|
|
3477
3520
|
// escaped (a value could carry markup)
|
|
@@ -3513,6 +3556,22 @@
|
|
|
3513
3556
|
const globeCenter = proj.type === 'globe' ? map.getCenter() : null;
|
|
3514
3557
|
totals = new Array(rt.categoryLabels.length).fill(0);
|
|
3515
3558
|
if (rt._rangeClassed) { totals = rangeClassLegendTotals(rt, bbox, globeCenter); maxTotal = Math.max(0, ...totals); order = totals.map((v, i) => i).sort((a, b) => totals[b] - totals[a]); return; }
|
|
3559
|
+
// a per-item chart over several value fields: a row per field,
|
|
3560
|
+
// the field's sum over the visible items (flat's NYC status
|
|
3561
|
+
// legend: bikes / empty docks / e-bikes, biggest sum first)
|
|
3562
|
+
if (rt._multiFields && rt._isMultiFieldSequence()) {
|
|
3563
|
+
(rt._activeFeatures || rt.features).forEach(f => {
|
|
3564
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
3565
|
+
if (!c || typeof c[0] !== 'number') return;
|
|
3566
|
+
const [lng, lat] = c;
|
|
3567
|
+
if (lng < bbox[0] || lng > bbox[2] || lat < bbox[1] || lat > bbox[3]) return;
|
|
3568
|
+
if (globeCenter && !isOnVisibleHemisphere(lng, lat, globeCenter)) return;
|
|
3569
|
+
rt._multiFields.forEach((k, i) => { totals[i] += parseFloat(f.properties[k]) || 0; });
|
|
3570
|
+
});
|
|
3571
|
+
maxTotal = Math.max(0, ...totals);
|
|
3572
|
+
order = totals.map((v, i) => i).sort((a, b) => totals[b] - totals[a]);
|
|
3573
|
+
return;
|
|
3574
|
+
}
|
|
3516
3575
|
// _activeFeatures (set by setFacetFilter/clearFacetFilter,
|
|
3517
3576
|
// e.g. the country-select dropdown below) is the facet-
|
|
3518
3577
|
// filtered subset when a filter is active, null otherwise —
|
|
@@ -3645,7 +3704,7 @@
|
|
|
3645
3704
|
for (let line = 0; line < nLines; line++) {
|
|
3646
3705
|
for (let c = 0; c < n; c++) {
|
|
3647
3706
|
const ix = flatFlag(rt.flags, 'INVERT') ? n - c - 1 : c;
|
|
3648
|
-
const rgb = rt.categoryColorsRgb[ix];
|
|
3707
|
+
const rgb = rt.categoryColorsRgb[ix] || [128, 128, 128]; // colorscheme "none": no class colors
|
|
3649
3708
|
const part = rt.partsA[ix];
|
|
3650
3709
|
const title = part ? flatFormatValue(part.min, dec, '') + unit + ' ... ' + flatFormatValue(part.max, dec, '') + unit : '';
|
|
3651
3710
|
const marked = rt._markedClasses.has(ix);
|
|
@@ -3676,8 +3735,8 @@
|
|
|
3676
3735
|
// flat drops the rows without a count once any row has one
|
|
3677
3736
|
// (legend.js 754-760, 787-789: fCountBars && !count)
|
|
3678
3737
|
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]})`;
|
|
3738
|
+
const rgb = rt.categoryColorsRgb[i] || [128, 128, 128]; // colorscheme "none": no class colors
|
|
3739
|
+
const color = legendFill(i) || `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;
|
|
3681
3740
|
const marked = rt._markedClasses.has(i);
|
|
3682
3741
|
const dimmed = rt._markedClasses.size > 0 && !marked;
|
|
3683
3742
|
const rowStyle = 'padding:4px 3px;border-radius:4px;cursor:pointer;opacity:' + (dimmed ? 0.4 : 1) + ';'
|
|
@@ -3704,7 +3763,7 @@
|
|
|
3704
3763
|
+ '<div style="display:flex;align-items:center;gap:6px;">'
|
|
3705
3764
|
+ '<span style="flex:0 0 8px;width:8px;height:8px;border-radius:50%;background:' + color + ';"></span>'
|
|
3706
3765
|
+ '<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;">' +
|
|
3766
|
+
+ '<span style="flex:0 0 auto;white-space:nowrap;">' + legendTotalText(totals[i]) + legendUnit + '</span>'
|
|
3708
3767
|
+ '</div>'
|
|
3709
3768
|
+ '</div>';
|
|
3710
3769
|
}
|
|
@@ -3715,7 +3774,7 @@
|
|
|
3715
3774
|
+ '<span style="flex:1 1 auto;min-width:0;height:6px;background:' + legendColors.track + ';border-radius:3px;overflow:hidden;">'
|
|
3716
3775
|
+ '<span style="display:block;height:100%;width:' + pct + '%;background:' + color + ';border-radius:3px;"></span>'
|
|
3717
3776
|
+ '</span>'
|
|
3718
|
-
+ '<span style="flex:0 0 auto;text-align:right;min-width:60px;">' +
|
|
3777
|
+
+ '<span style="flex:0 0 auto;text-align:right;min-width:60px;">' + legendTotalText(totals[i]) + legendUnit + '</span>'
|
|
3719
3778
|
+ '</div>'
|
|
3720
3779
|
+ '</div>';
|
|
3721
3780
|
}).join('');
|
|
@@ -4296,7 +4355,7 @@
|
|
|
4296
4355
|
legendEl.innerHTML = this._legendHtml;
|
|
4297
4356
|
// inside the map container, never the page's body (see the pinned
|
|
4298
4357
|
// tooltip mount) — styling body breaks page-positioned elements
|
|
4299
|
-
el
|
|
4358
|
+
anchorInside(el);
|
|
4300
4359
|
el.appendChild(legendEl);
|
|
4301
4360
|
}
|
|
4302
4361
|
|
|
@@ -4663,10 +4722,17 @@
|
|
|
4663
4722
|
.catch(err => console.error('[ixmaps-gl] map.layer():', err));
|
|
4664
4723
|
return engineApi;
|
|
4665
4724
|
}
|
|
4666
|
-
// map.layer(name).data()...define():
|
|
4725
|
+
// map.layer(name).data()...define(): adds a theme on that layer.
|
|
4726
|
+
// NO same-name auto-remove here: flat lets several themes SHARE one
|
|
4727
|
+
// .layer() name (a page's feature base + its chart overlay, the
|
|
4728
|
+
// NYC status page's two "Station" themes — the overlay joins onto
|
|
4729
|
+
// the base's geometry by lookup, removing the base would kill
|
|
4730
|
+
// both). Replacement is by THEME ID (style.name/meta.name), and
|
|
4731
|
+
// happens inside defineFromDefinition instead — a re-defined id
|
|
4732
|
+
// (the page's 10s live refresh) replaces its own previous theme.
|
|
4667
4733
|
const lb = new LayerBuilder(nameOrBuilder);
|
|
4668
4734
|
lb.define = () => {
|
|
4669
|
-
_chainedLayers = _chainedLayers.then(() =>
|
|
4735
|
+
_chainedLayers = _chainedLayers.then(() => engineApi.defineLayer(lb))
|
|
4670
4736
|
.catch(err => console.error('[ixmaps-gl] map.layer().define():', err));
|
|
4671
4737
|
return _chainedLayers;
|
|
4672
4738
|
};
|
|
@@ -4695,8 +4761,22 @@
|
|
|
4695
4761
|
// Removes every runtime whose OWN .layer(name) matches — a theme
|
|
4696
4762
|
// may legitimately be split across more than one runtime sharing
|
|
4697
4763
|
// one name (the CHOROPLETH/FEATURE-donor pair above), so this
|
|
4698
|
-
// removes all of them, not just the first match.
|
|
4699
|
-
|
|
4764
|
+
// removes all of them, not just the first match. GL-PORT COMPAT:
|
|
4765
|
+
// flat addresses a theme by its ID — style.name / meta.name —
|
|
4766
|
+
// as much as by its layer name (htmlgui.js removeTheme), and a
|
|
4767
|
+
// real page's own rebuild loop typically knows only the id it
|
|
4768
|
+
// set via .meta({name}) (the fire page's KDE: removeTheme
|
|
4769
|
+
// ("kde_fires") for the layer "kde_fires_layer"). Layer name
|
|
4770
|
+
// first, then the id — without the fallback every rebuild
|
|
4771
|
+
// removed nothing and stacked another copy of the theme,
|
|
4772
|
+
// compounding opacity (same semantics removeThemeById already
|
|
4773
|
+
// implements for the handle's .remove(id)).
|
|
4774
|
+
removeTheme: (name) => {
|
|
4775
|
+
const byLayer = runtimes.filter(r => r.name === name);
|
|
4776
|
+
if (byLayer.length) return removeRuntimes(name, byLayer);
|
|
4777
|
+
const byId = runtimes.filter(r => (r.style && r.style.name === name) || (r.meta && r.meta.name === name));
|
|
4778
|
+
return byId.length ? removeRuntimes(byId[0].name, byId) : false;
|
|
4779
|
+
},
|
|
4700
4780
|
// flat's refreshTheme (maptheme.js Themes.refreshTheme): the
|
|
4701
4781
|
// theme's data is loaded again — a broker is called again — and
|
|
4702
4782
|
// replaces the old features in place (draw order, legend, style
|
|
@@ -4825,10 +4905,15 @@
|
|
|
4825
4905
|
else map.fitBounds(bounds, { padding: 30, animate: false, maxZoom: 16 });
|
|
4826
4906
|
}
|
|
4827
4907
|
|
|
4828
|
-
|
|
4908
|
+
// quiet: the broker's data fills only this theme — not broadcast to
|
|
4909
|
+
// the other themes of the name (flat's refreshTheme reloads one theme;
|
|
4910
|
+
// setExternalData only releases themes still waiting for the data,
|
|
4911
|
+
// maptheme.js 2570-2591)
|
|
4912
|
+
async function loadNamedTheme(rt, force, quiet = false) {
|
|
4829
4913
|
const spec = normalizeTheme(rt._definition);
|
|
4830
4914
|
const name = String(spec.data.name);
|
|
4831
4915
|
const broker = brokerTheme(spec);
|
|
4916
|
+
if (quiet && broker) _quietNamedData.set(name, (_quietNamedData.get(name) || 0) + 1);
|
|
4832
4917
|
rt._loading = (rt._loading || 0) + 1;
|
|
4833
4918
|
if (broker) { pendingLoads++; rt._brokerLoading = (rt._brokerLoading || 0) + 1; setDataLoading(); }
|
|
4834
4919
|
try {
|
|
@@ -4850,6 +4935,7 @@
|
|
|
4850
4935
|
else console.error(`[ixmaps-gl] theme "${themeIdOf(rt)}": no data —`, err);
|
|
4851
4936
|
} finally {
|
|
4852
4937
|
rt._loading--;
|
|
4938
|
+
if (quiet && broker) { const n = (_quietNamedData.get(name) || 1) - 1; if (n) _quietNamedData.set(name, n); else _quietNamedData.delete(name); }
|
|
4853
4939
|
if (broker) { pendingLoads--; rt._brokerLoading--; setDataLoading(); }
|
|
4854
4940
|
}
|
|
4855
4941
|
}
|
|
@@ -4898,7 +4984,11 @@
|
|
|
4898
4984
|
}
|
|
4899
4985
|
// flat, after a FEATURE theme is drawn (maptheme.js 18947-18962): the
|
|
4900
4986
|
// other themes on its layer are placed again, and a data.query broker
|
|
4901
|
-
// among them is refreshed 500 ms later
|
|
4987
|
+
// among them is refreshed 500 ms later — quietly: its new data fills
|
|
4988
|
+
// that theme only. Broadcast to every theme of the name, a FEATURE
|
|
4989
|
+
// theme sharing the broker's data name (the fire areas page: SIF
|
|
4990
|
+
// polygons and chart both "data_2") was refilled, drawn, refreshed
|
|
4991
|
+
// its dependent again — an endless reload of ~15k records every 3 s
|
|
4902
4992
|
function featureDependents(featureRt) {
|
|
4903
4993
|
runtimes.forEach(dep => {
|
|
4904
4994
|
if (dep === featureRt || dep.flags.has('FEATURE') || dep.flags.has('FEATURES')) return;
|
|
@@ -4906,7 +4996,7 @@
|
|
|
4906
4996
|
const spec = normalizeTheme(dep._definition || {});
|
|
4907
4997
|
if (dep._named && spec.data && spec.data.query) {
|
|
4908
4998
|
clearTimeout(dep._dependentTimer);
|
|
4909
|
-
dep._dependentTimer = setTimeout(() => { if (runtimes.includes(dep)) loadNamedTheme(dep, true); }, 500);
|
|
4999
|
+
dep._dependentTimer = setTimeout(() => { if (runtimes.includes(dep)) loadNamedTheme(dep, true, true); }, 500);
|
|
4910
5000
|
} else if (dep._named && dep._lastTable && !dep._loading) {
|
|
4911
5001
|
fillNamedTheme(dep, spec, dep._lastTable, null);
|
|
4912
5002
|
}
|
|
@@ -4915,10 +5005,18 @@
|
|
|
4915
5005
|
|
|
4916
5006
|
// One theme definition (real ixmaps-flat's shape — from a builder's
|
|
4917
5007
|
// definition() or a project file) → a new runtime on this map. Shared
|
|
4918
|
-
// by defineLayer and loadProject. Purely additive
|
|
5008
|
+
// by defineLayer and loadProject. Purely additive across LAYER names —
|
|
5009
|
+
// several themes legitimately share one (a FEATURE base + its CHART
|
|
5010
|
+
// overlay, the CHOROPLETH/FEATURE-donor pair) — but a re-defined
|
|
5011
|
+
// THEME ID (style.name/meta.name) replaces its own previous theme, as
|
|
5012
|
+
// flat itself does ("defining a theme with the same name replaces it
|
|
5013
|
+
// in place"; a page's live-refresh loop relies on it, e.g. the NYC
|
|
5014
|
+
// status page re-defining its two themes every 10s).
|
|
4919
5015
|
async function defineFromDefinition(def) {
|
|
4920
5016
|
if (validation) validation.definition(def);
|
|
4921
5017
|
const pdef = projectThemeToDefinition(def);
|
|
5018
|
+
const replacedId = (pdef.meta && pdef.meta.name) || (pdef.style && pdef.style.name);
|
|
5019
|
+
if (replacedId) removeThemeById(replacedId);
|
|
4922
5020
|
let spec = normalizeTheme(pdef);
|
|
4923
5021
|
let raw;
|
|
4924
5022
|
const deferred = deferredFeatureLoad(spec, map.getZoom());
|
|
@@ -5572,8 +5670,11 @@
|
|
|
5572
5670
|
// itself, which stays purely additive for every other caller —
|
|
5573
5671
|
// see its own comment for why (the CHOROPLETH/FEATURE-donor
|
|
5574
5672
|
// pairing needs two runtimes sharing one name to coexist).
|
|
5673
|
+
// NO same-LAYER-name auto-remove (see engineApi.layer's own
|
|
5674
|
+
// comment — several themes legitimately share one layer name);
|
|
5675
|
+
// a re-defined THEME ID replaces its own previous theme inside
|
|
5676
|
+
// defineFromDefinition instead
|
|
5575
5677
|
lb.define = () => {
|
|
5576
|
-
resolvedApi.removeTheme(nameOrBuilder);
|
|
5577
5678
|
return resolvedApi.defineLayer(lb);
|
|
5578
5679
|
};
|
|
5579
5680
|
}
|
|
@@ -5856,6 +5957,41 @@
|
|
|
5856
5957
|
* (flatChartBranch(flags) === 'bubble' ? 0.5 : 1);
|
|
5857
5958
|
return Math.max(VALUE_RADIUS_MIN, Math.min(VALUE_RADIUS_MAX, fixed * zoomFactor));
|
|
5858
5959
|
}
|
|
5960
|
+
// NOSIZE: every chart ONE fixed radius, whatever the item's value;
|
|
5961
|
+
// normalsizevalue doesn't scale it. Which one depends on flat's SYMBOL
|
|
5962
|
+
// sub-branch:
|
|
5963
|
+
// - a CATEGORICAL theme without AGGREGATE/GROUP ("symbol equals
|
|
5964
|
+
// value", maptheme.js 21566-21625, "GR 01.07.2023 redesign size
|
|
5965
|
+
// handling"): the normal radius itself
|
|
5966
|
+
// - every other symbol chart (the per-part ladder, 21829-21896):
|
|
5967
|
+
// SEQUENCE normalX(chart size / 3), 2/3 of it; otherwise
|
|
5968
|
+
// nMaxRadius / 5 — measured on the fire areas page's SIF chart
|
|
5969
|
+
// (CHART|SYMBOL|NOSIZE: r 60 units at normalX(1) 20, i.e. 3 of the
|
|
5970
|
+
// 15-unit normal radius)
|
|
5971
|
+
// The BUBBLE draw keeps its own older NOSIZE value, half of it
|
|
5972
|
+
// (maptheme.js 19839)
|
|
5973
|
+
if (flags && flags.has('NOSIZE') && !flags.has('PLOT') && !flags.has('GRIDSIZE')) {
|
|
5974
|
+
const symbolEqualsValue = flags.has('CATEGORICAL') && !flags.has('AGGREGATE') && !flatFlag(flags, 'GROUP');
|
|
5975
|
+
const fixed = NORMAL_RADIUS_PX * (styleNum(style.scale) || 1)
|
|
5976
|
+
* (flatChartBranch(flags) === 'bubble' ? 0.5
|
|
5977
|
+
: symbolEqualsValue ? 1
|
|
5978
|
+
: flatFlag(flags, 'SEQUENCE') ? 2 / 3 : 1 / 5);
|
|
5979
|
+
return Math.max(VALUE_RADIUS_MIN, Math.min(VALUE_RADIUS_MAX, fixed * zoomFactor));
|
|
5980
|
+
}
|
|
5981
|
+
// Same redesign, the rule around it (maptheme.js 21612-21625) — only in
|
|
5982
|
+
// flat's "symbol equals value" sub-branch, a CATEGORICAL theme without
|
|
5983
|
+
// AGGREGATE/GROUP: the symbol resizes by itemA[a].nSize, which flat only
|
|
5984
|
+
// sets for a theme with a bound szSizeField (maptheme.js 9144-9153), so
|
|
5985
|
+
// WITHOUT a size field every item keeps the DEFAULT normal radius,
|
|
5986
|
+
// whatever its value. Every other symbol chart (the per-part ladder,
|
|
5987
|
+
// 21850-21851) and BUBBLE (19851) scale by the value — below.
|
|
5988
|
+
if (flags && flags.has('SYMBOL') && !flags.has('BUBBLE') && !flags.has('USER')
|
|
5989
|
+
&& !flags.has('FIXSIZE') && !flags.has('PLOT') && !flags.has('GRIDSIZE')
|
|
5990
|
+
&& flags.has('CATEGORICAL') && !flags.has('AGGREGATE') && !flatFlag(flags, 'GROUP')
|
|
5991
|
+
&& !style.sizefield) {
|
|
5992
|
+
const fixed = NORMAL_RADIUS_PX * (styleNum(style.scale) || 1);
|
|
5993
|
+
return Math.max(VALUE_RADIUS_MIN, Math.min(VALUE_RADIUS_MAX, fixed * zoomFactor));
|
|
5994
|
+
}
|
|
5859
5995
|
// Real default (maptheme.js ~line 5059) when the theme sets no
|
|
5860
5996
|
// normalsizevalue: the dataset's own max value on the bound size
|
|
5861
5997
|
// field (maxSizeValue, from LayerRuntime._prepare), not a fixed
|
|
@@ -6168,9 +6304,9 @@
|
|
|
6168
6304
|
|
|
6169
6305
|
// a chart's VALUES text (maptheme.js 20979-21017): an explicit valuefield
|
|
6170
6306
|
// 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
|
|
6307
|
+
// is), else the value; numbers formatted with valuedecimals when set
|
|
6308
|
+
// (explicitValueDecimals: a given "0" counts), else 1 decimal below 1 —
|
|
6309
|
+
// or when the max value is at most 1 — else none, plus the unit, which flat stores with a leading space unless
|
|
6174
6310
|
// it starts with "." (9675) and appends only up to 5 characters.
|
|
6175
6311
|
// A CATEGORICAL bubble prints its size value when size is bound to a
|
|
6176
6312
|
// field (its value, see resolveAggregateValue), as flat does; with
|
|
@@ -6184,11 +6320,30 @@
|
|
|
6184
6320
|
if (!field || field !== binding.value || !flags.has('CATEGORICAL') || !labels || labels[i] == null) return null;
|
|
6185
6321
|
return { [field]: labels[i] };
|
|
6186
6322
|
}
|
|
6323
|
+
// style.valuedecimals as flat reads it: nValueDecimals keeps the raw
|
|
6324
|
+
// style string, so an explicit "0" is truthy in flat's `nValueDecimals ||
|
|
6325
|
+
// default` and counts (fire areas page: valuedecimals "0", 0.48 → "1 ha");
|
|
6326
|
+
// null when not set
|
|
6327
|
+
// (valuedecimals is therefore not typed: STYLE_NUMBER_KEYS). Flat's
|
|
6328
|
+
// truthiness: unset, "" and a numeric 0 fall back to the default
|
|
6329
|
+
function explicitValueDecimals(style) {
|
|
6330
|
+
const v = style && style.valuedecimals;
|
|
6331
|
+
if (!v) return null;
|
|
6332
|
+
const n = Number(v);
|
|
6333
|
+
return Number.isFinite(n) ? n : null;
|
|
6334
|
+
}
|
|
6335
|
+
// the legend's decimals (legend.js 610): valuedecimals when defined, else 2
|
|
6336
|
+
function legendDecimals(style) {
|
|
6337
|
+
const v = style && style.valuedecimals;
|
|
6338
|
+
const n = v == null || v === '' ? NaN : Number(v);
|
|
6339
|
+
return Number.isFinite(n) ? n : 2;
|
|
6340
|
+
}
|
|
6187
6341
|
function flatValueText(raw, title, value, style, flags, opts = {}) {
|
|
6188
6342
|
const units = style.units ? String(style.units) : '';
|
|
6189
6343
|
const unit = units ? (units[0] === '.' ? '' : ' ') + units : '';
|
|
6190
6344
|
const unitText = unit.length <= 5 ? unit : '';
|
|
6191
|
-
const
|
|
6345
|
+
const explicitDec = explicitValueDecimals(style);
|
|
6346
|
+
const dec = v => (explicitDec != null ? explicitDec : ((v < 1 || (opts.maxValue != null && opts.maxValue <= 1)) ? 1 : 0));
|
|
6192
6347
|
const signed = (v, text) => {
|
|
6193
6348
|
if (flags && (flags.has('DIFFERENCE') || flags.has('RELATIVE') || flags.has('SIGN'))) {
|
|
6194
6349
|
if (v === 0) return '+/-' + text;
|
|
@@ -6201,7 +6356,7 @@
|
|
|
6201
6356
|
if (field && raw && raw[field] != null && raw[field] !== '') {
|
|
6202
6357
|
if (isNaN(raw[field])) return String(raw[field]);
|
|
6203
6358
|
const v = Number(raw[field]);
|
|
6204
|
-
return signed(v, flatGroupedValue(v,
|
|
6359
|
+
return signed(v, flatGroupedValue(v, explicitDec != null ? explicitDec : (v < 1 ? 1 : 0), opts.noBreaks) + unitText);
|
|
6205
6360
|
}
|
|
6206
6361
|
return signed(value, flatGroupedValue(value, dec(value), opts.noBreaks) + unitText);
|
|
6207
6362
|
}
|
|
@@ -6303,19 +6458,29 @@
|
|
|
6303
6458
|
}
|
|
6304
6459
|
|
|
6305
6460
|
function valuesHiddenByScale(style, zoom) {
|
|
6306
|
-
|
|
6461
|
+
// flat accepts both 'valueupper' and 'valuesupper' (maptheme.js 1710,
|
|
6462
|
+
// "GR 15.03.2017 accept also 'valuesupper'"), same for the lower pair
|
|
6463
|
+
const m = /^1:(\d+(?:\.\d+)?)$/.exec(style.valueupper || style.valuesupper || '');
|
|
6307
6464
|
if (!m) return false;
|
|
6308
6465
|
const upperRatio = parseFloat(m[1]);
|
|
6309
6466
|
const scaleDenominator = scaleDenominatorAt(zoom);
|
|
6310
6467
|
return scaleDenominator > upperRatio;
|
|
6311
6468
|
}
|
|
6469
|
+
// flat's chart LABEL scale gate (maptheme.js 24536-24537): the label is
|
|
6470
|
+
// visible while the scale passes <= against BOTH bounds — flat's own
|
|
6471
|
+
// lower test reads <= too, kept as-is for parity
|
|
6472
|
+
function labelHiddenByScale(style, zoom) {
|
|
6473
|
+
const upper = scaleDenom(style.labelupper), lower = scaleDenom(style.labellower);
|
|
6474
|
+
const scale = scaleDenominatorAt(zoom);
|
|
6475
|
+
return !!((upper && scale > upper) || (lower && scale > lower));
|
|
6476
|
+
}
|
|
6312
6477
|
|
|
6313
6478
|
// contrasting text color against a bubble's own fill — standard relative
|
|
6314
6479
|
// luminance; approximates the real engine's __maptheme_getChartColors(...)
|
|
6315
6480
|
// .textColor (white text on ixmaps' typically dark/saturated palette,
|
|
6316
6481
|
// dark text on pale fills) without needing its exact color-space table.
|
|
6317
6482
|
function contrastTextColor(fillRgb) {
|
|
6318
|
-
const [r, g, b] = fillRgb;
|
|
6483
|
+
const [r, g, b] = fillRgb || [128, 128, 128];
|
|
6319
6484
|
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
6320
6485
|
return luminance > 0.6 ? [30, 30, 30] : [255, 255, 255];
|
|
6321
6486
|
}
|
|
@@ -6830,6 +6995,25 @@
|
|
|
6830
6995
|
return parts;
|
|
6831
6996
|
}
|
|
6832
6997
|
|
|
6998
|
+
// CHART|SYMBOL|SEQUENCE|RINGS (flat maptheme.js 21293-21318): the parts
|
|
6999
|
+
// nest on the center — each non-zero part sized by the running total of
|
|
7000
|
+
// the non-zero parts up to it (its own value included), biggest first
|
|
7001
|
+
// so the smaller ones stay on top; parts of value 0 are left out.
|
|
7002
|
+
// radiusOf(total) is the ring's radius. Measured on flat (NYC status:
|
|
7003
|
+
// 63 | 75 | 3 → r 161.5 | 353.8 | 361.5 at max radius 300, max value
|
|
7004
|
+
// 117). Returns { i, v (the part's own value), c (running total), r, x, y }.
|
|
7005
|
+
function ringsLayout(counts, radiusOf) {
|
|
7006
|
+
const parts = [];
|
|
7007
|
+
let total = 0;
|
|
7008
|
+
counts.forEach((v, i) => {
|
|
7009
|
+
if (!v) return;
|
|
7010
|
+
total += v;
|
|
7011
|
+
const r = radiusOf(total);
|
|
7012
|
+
if (r > 0) parts.push({ i, v, c: total, r, x: 0, y: 0 });
|
|
7013
|
+
});
|
|
7014
|
+
return parts.sort((a, b) => b.c - a.c);
|
|
7015
|
+
}
|
|
7016
|
+
|
|
6833
7017
|
// ---------------------------------------------------------------
|
|
6834
7018
|
// Classification — pure functions (no LayerRuntime state): class
|
|
6835
7019
|
// breaks for a numeric field, and which class a value falls in.
|
|
@@ -7966,6 +8150,18 @@
|
|
|
7966
8150
|
return out;
|
|
7967
8151
|
}
|
|
7968
8152
|
|
|
8153
|
+
// Every runtime ever built gets a fresh number: a theme rebuilt through
|
|
8154
|
+
// removeTheme() + defineLayer() (a page's own refresh loop, e.g. the fire
|
|
8155
|
+
// page's KDE isobands) is a NEW runtime whose data may differ from the
|
|
8156
|
+
// runtime it replaces — if its deck.gl layer kept the SAME id, deck would
|
|
8157
|
+
// update the old layer's state in place, and under the interleaved overlay
|
|
8158
|
+
// those in-place updates can leave the PREVIOUS generation's geometry on
|
|
8159
|
+
// screen (the exact failure _dataLayerId's own comment documents for
|
|
8160
|
+
// replaceFeatures: "updates in place, arriving over each other, left
|
|
8161
|
+
// triangles across sections"). A unique -dN suffix per runtime forces deck
|
|
8162
|
+
// to build a genuinely new layer and dispose the old one.
|
|
8163
|
+
let LAYER_DATA_GENERATION = 0;
|
|
8164
|
+
|
|
7969
8165
|
class LayerRuntime {
|
|
7970
8166
|
// spec: a normalizeTheme() result — never a raw LayerBuilder
|
|
7971
8167
|
constructor(spec, fc, mapOptions) {
|
|
@@ -7974,6 +8170,8 @@
|
|
|
7974
8170
|
this.style = spec.style;
|
|
7975
8171
|
this.flags = symbolsFlagCompat(spec.flags, spec.style);
|
|
7976
8172
|
this.meta = spec.meta;
|
|
8173
|
+
// see LAYER_DATA_GENERATION above — unique per runtime, see _dataLayerId
|
|
8174
|
+
this._dataGen = ++LAYER_DATA_GENERATION;
|
|
7977
8175
|
// GL-PORT COMPAT: retained only for ixmaps.getThemeObj()'s szFilter
|
|
7978
8176
|
// (real ixmaps-flat global compat shim, see _globalThemeRegistry) —
|
|
7979
8177
|
// this engine's own filtering is fully static/load-time (see
|
|
@@ -8144,6 +8342,21 @@
|
|
|
8144
8342
|
// ("investimento"/"tamponamento"/...) needs the display text, not
|
|
8145
8343
|
// a raw numeric code it could never match against.
|
|
8146
8344
|
this.categoryColorsRgb = resolveClassColors(this.style.colorscheme, this.categoryDisplayLabels, this.style.classes);
|
|
8345
|
+
} else if (this._isMultiFieldSequence()) {
|
|
8346
|
+
// CHART|SYMBOL|SEQUENCE over several value fields (no CATEGORICAL,
|
|
8347
|
+
// no AGGREGATE): one part per field, colored by the colorscheme in
|
|
8348
|
+
// field order, labeled by .style({label}) (flat's szFieldsA /
|
|
8349
|
+
// szLabelA). A colorscheme entry "none" is a part without fill —
|
|
8350
|
+
// only its outline in linecolor (flat: fill:none). The parts' max
|
|
8351
|
+
// value is flat's nMax: the biggest single field value of any
|
|
8352
|
+
// record (maptheme.js nMaxA → nMax), measured on the NYC status page
|
|
8353
|
+
Object.assign(this, computeMultiFieldClasses(this.binding, this.style));
|
|
8354
|
+
this.categoryIndexByLabel = new Map(this.categoryLabels.map((v, i) => [v, i]));
|
|
8355
|
+
const scheme = Array.isArray(this.style.colorscheme) ? this.style.colorscheme : [];
|
|
8356
|
+
this._partNoFill = new Set(this._multiFields.map((f, i) => i).filter(i => String(scheme[i]).trim().toLowerCase() === 'none'));
|
|
8357
|
+
let max = 0;
|
|
8358
|
+
for (const f of this.features) for (const field of this._multiFields) { const v = Math.abs(parseFloat(f.properties[field])); if (v > max) max = v; }
|
|
8359
|
+
this._multiFieldPartMax = max || 1;
|
|
8147
8360
|
} else if (this.binding.value) {
|
|
8148
8361
|
// real engine's OTHER coloring mode (maptheme.js distributeValues,
|
|
8149
8362
|
// partsA): a NUMERIC bound value, not CATEGORICAL, is classed into
|
|
@@ -8156,6 +8369,24 @@
|
|
|
8156
8369
|
// "which bucket does this feature belong to", and once buckets +
|
|
8157
8370
|
// colors exist, everything downstream is identical.
|
|
8158
8371
|
this._buildPartsA();
|
|
8372
|
+
} else if (this.style.colorscheme != null) {
|
|
8373
|
+
// GL-PORT COMPAT: a theme with NO classification at all (no
|
|
8374
|
+
// CATEGORICAL categories, no numeric binding.value to range-class)
|
|
8375
|
+
// still has classes in the real engine — exactly ONE, its implicit
|
|
8376
|
+
// bucket: _resolveClassIndex returns 0 for every feature, and flat
|
|
8377
|
+
// colors them all with colorScheme[0] (every MapTheme is built
|
|
8378
|
+
// from its colorscheme array — one entry, one class, e.g. the
|
|
8379
|
+
// fire page's KDE isobands: FEATURE|CHOROPLETH, colorscheme
|
|
8380
|
+
// "#dd0088", no value field — every iso band one flat magenta).
|
|
8381
|
+
// Without this branch categoryColorsRgb stayed undefined while
|
|
8382
|
+
// cat was still 0, and the choropleth's getFillColor crashed on
|
|
8383
|
+
// categoryColorsRgb[0] — deck.gl killed the layer at init and
|
|
8384
|
+
// the theme never drew. A colorscheme that resolves to NO colors
|
|
8385
|
+
// (the explicit no-fill ["none"] — flat draws unfilled symbols,
|
|
8386
|
+
// only their values/boxes/titles) keeps that: resolving it to the
|
|
8387
|
+
// fallback palette would paint symbol circles flat never draws.
|
|
8388
|
+
const resolved = resolveColorScheme(this.style.colorscheme, [''], this.style.classes);
|
|
8389
|
+
this.categoryColorsRgb = resolved ? resolved.map(c => Array.isArray(c) ? c : (parseCssColor(c) || [130, 130, 130])) : null;
|
|
8159
8390
|
}
|
|
8160
8391
|
|
|
8161
8392
|
// facet-driven dynamic filter state (see setFacetFilter/getFacets) —
|
|
@@ -8225,6 +8456,17 @@
|
|
|
8225
8456
|
// this with AGGREGATE clustering yet, so a clustered GROUP bubble
|
|
8226
8457
|
// always stays circular (_buildBubbleIcon, untouched).
|
|
8227
8458
|
_resolveSymbolShape(props) {
|
|
8459
|
+
// flat's class→symbol mapping (maptheme.js 1417, 2074-2077, 12937):
|
|
8460
|
+
// `symbols` WITHOUT a symbolfield names the shape per COLOR class —
|
|
8461
|
+
// szSymbolsA[class], the last entry repeating when the array is
|
|
8462
|
+
// shorter than the classes (a single entry, like the fire page's
|
|
8463
|
+
// symbols: ["square"], shapes every class). Only GL's implemented
|
|
8464
|
+
// shapes draw (drawSymbolPath); flat-only names fall back to circle.
|
|
8465
|
+
if (!this.style.symbolfield && Array.isArray(this.style.symbols)) {
|
|
8466
|
+
const s = this.style.symbols;
|
|
8467
|
+
const cat = props && props.cat != null ? props.cat : 0;
|
|
8468
|
+
return normalizeSymbolShape(s[Math.min(cat, s.length - 1)]);
|
|
8469
|
+
}
|
|
8228
8470
|
if (!this.style.symbolfield || !Array.isArray(this.style.symbolvalues) || !Array.isArray(this.style.symbols)) return 'circle';
|
|
8229
8471
|
// CORRECTED: found live via pickObject, not assumed — the properties
|
|
8230
8472
|
// this actually receives at render time (from _computeAggregatedItems'
|
|
@@ -8286,6 +8528,19 @@
|
|
|
8286
8528
|
// they'd never even share a cell in the first place). Every
|
|
8287
8529
|
// feature goes into ONE bucket; _buildChartLayers resolves the real
|
|
8288
8530
|
// 7-class color once it knows each cell's actual aggregated total.
|
|
8531
|
+
if (this._multiFields && this._isMultiFieldSequence()) {
|
|
8532
|
+
// one bucket; each record carries its field values as the chart's
|
|
8533
|
+
// parts. Its value — the draw order — is the first field's, as
|
|
8534
|
+
// flat's chart node value (NYC status: value="63" = e-bikes)
|
|
8535
|
+
this._featuresByCategory = [sourceFeatures.map(f => {
|
|
8536
|
+
const parts = this._multiFields.map(k => parseFloat(f.properties[k]) || 0);
|
|
8537
|
+
return { type: 'Feature', geometry: f.geometry, properties: { value: parts[0], parts, raw: f.properties } };
|
|
8538
|
+
})];
|
|
8539
|
+
this._clusterIndices = null;
|
|
8540
|
+
this._clusterRadiusPx = null;
|
|
8541
|
+
this._aggregateStatsCache = null;
|
|
8542
|
+
return;
|
|
8543
|
+
}
|
|
8289
8544
|
if (this._rangeClassed) {
|
|
8290
8545
|
// flat colors/classes an aggregated cell by the SUM of the value
|
|
8291
8546
|
// field (nValuesA, maptheme.js:10870ff; classified by nValuesA[0],
|
|
@@ -8772,7 +9027,7 @@
|
|
|
8772
9027
|
return this._glowIconCache.get(key);
|
|
8773
9028
|
}
|
|
8774
9029
|
|
|
8775
|
-
_buildBubbleIcon(counts, colors, marked = null) {
|
|
9030
|
+
_buildBubbleIcon(counts, colors, marked = null, shapes = null) {
|
|
8776
9031
|
// The cache key quantizes each count's RATIO to the group's total,
|
|
8777
9032
|
// not the raw count — computeBubblePackLayout's radii are purely
|
|
8778
9033
|
// sqrt(c/total), so two clusters with wildly different absolute
|
|
@@ -8802,7 +9057,7 @@
|
|
|
8802
9057
|
// coarse share buckets that can put a small marked part at 0
|
|
8803
9058
|
const key = counts.map(c => Math.round((c / total) * RATIO_BUCKETS)).join('-') + (marked
|
|
8804
9059
|
? '|iso:' + counts.map((c, i) => (c > 0 && marked.has(i) ? i + ':' + Math.round(Math.sqrt(c / total) * 100) : null)).filter(Boolean).join(',')
|
|
8805
|
-
: '');
|
|
9060
|
+
: '') + (shapes ? '|sym:' + shapes.map(v => String(v).toLowerCase()).join(',') : '');
|
|
8806
9061
|
if (this._iconCache.has(key)) return this._iconCache.get(key);
|
|
8807
9062
|
const size = BUBBLE_ICON_SIZE;
|
|
8808
9063
|
const canvas = document.createElement('canvas');
|
|
@@ -8811,8 +9066,16 @@
|
|
|
8811
9066
|
const { present, radii, offsets, fitScale } = (marked && isolatedBubblePackLayout(counts, size, marked)) || computeBubblePackLayout(counts, size);
|
|
8812
9067
|
const cx = size / 2, cy = size / 2;
|
|
8813
9068
|
present.forEach((p, i) => {
|
|
9069
|
+
// flat draws each part of an aggregated SYMBOL theme as ITS class's
|
|
9070
|
+
// symbol shape (the szSymbol switch runs per part, maptheme.js
|
|
9071
|
+
// 21995-22060) — `symbols` names them per class like
|
|
9072
|
+
// _resolveSymbolShape; circles when the name isn't one GL draws
|
|
9073
|
+
const shape = shapes ? normalizeSymbolShape(shapes[Math.min(p.i, shapes.length - 1)]) : 'circle';
|
|
9074
|
+
const px = cx + offsets[i].x * fitScale, py = cy + offsets[i].y * fitScale;
|
|
8814
9075
|
ctx.beginPath();
|
|
8815
|
-
ctx.arc(
|
|
9076
|
+
if (shape === 'circle') ctx.arc(px, py, radii[i] * fitScale, 0, Math.PI * 2);
|
|
9077
|
+
else drawSymbolPath(ctx, shape, px, py, radii[i] * fitScale);
|
|
9078
|
+
ctx.closePath();
|
|
8816
9079
|
ctx.fillStyle = `rgb(${colors[p.i].join(',')})`;
|
|
8817
9080
|
ctx.globalAlpha = 0.9; ctx.fill(); ctx.globalAlpha = 1;
|
|
8818
9081
|
ctx.lineWidth = 1; ctx.strokeStyle = '#fff'; ctx.stroke();
|
|
@@ -8862,14 +9125,22 @@
|
|
|
8862
9125
|
ctx.beginPath();
|
|
8863
9126
|
drawSymbolPath(ctx, shape, c, c, r);
|
|
8864
9127
|
ctx.closePath();
|
|
9128
|
+
// flat's "empty" (maptheme.js 21629-21631): the circle geometry, but
|
|
9129
|
+
// a fully transparent fill and a gray outline (fill-opacity:0,
|
|
9130
|
+
// stroke #888888) — a shapeless spot marker
|
|
8865
9131
|
ctx.fillStyle = `rgb(${colorRgb.join(',')})`;
|
|
8866
|
-
ctx.globalAlpha = opacity;
|
|
9132
|
+
ctx.globalAlpha = shape === 'empty' ? 0 : opacity;
|
|
8867
9133
|
ctx.fill();
|
|
8868
9134
|
if (borderWidth > 0 && borderColorRgb) {
|
|
8869
9135
|
ctx.globalAlpha = 1;
|
|
8870
9136
|
ctx.strokeStyle = `rgb(${borderColorRgb.join(',')})`;
|
|
8871
9137
|
ctx.lineWidth = borderWidth;
|
|
8872
9138
|
ctx.stroke();
|
|
9139
|
+
} else if (shape === 'empty') {
|
|
9140
|
+
ctx.globalAlpha = 1;
|
|
9141
|
+
ctx.strokeStyle = '#888888';
|
|
9142
|
+
ctx.lineWidth = 1;
|
|
9143
|
+
ctx.stroke();
|
|
8873
9144
|
}
|
|
8874
9145
|
const icon = { url: canvas.toDataURL(), width: size, height: size, anchorX: size / 2, anchorY: size / 2, id: key };
|
|
8875
9146
|
return this._cacheIcon(key, icon);
|
|
@@ -9146,7 +9417,8 @@
|
|
|
9146
9417
|
_formatTooltipValue(value) {
|
|
9147
9418
|
const num = Number(value);
|
|
9148
9419
|
if (isNaN(num)) return String(value);
|
|
9149
|
-
const
|
|
9420
|
+
const vd = this.style.valuedecimals;
|
|
9421
|
+
const rawDecimals = vd == null || vd === '' ? NaN : Number(vd);
|
|
9150
9422
|
const decimals = isNaN(rawDecimals) ? 2 : rawDecimals;
|
|
9151
9423
|
// flat's __formatValue (mapscript.js 6268ff) rounds twice: first to
|
|
9152
9424
|
// one decimal more (toFixed), then to the precision (Math.round) —
|
|
@@ -9259,7 +9531,16 @@
|
|
|
9259
9531
|
|
|
9260
9532
|
_buildFeaturesLayers(zoom, bbox) {
|
|
9261
9533
|
const cs = this.style.colorscheme;
|
|
9262
|
-
|
|
9534
|
+
// a generated scheme's first entry is the class COUNT, not a color —
|
|
9535
|
+
// flat colors an unclassed theme with the sweep's FIRST generated
|
|
9536
|
+
// color (maptheme colorScheme[0] after realize generates the sweep),
|
|
9537
|
+
// never the bare count string (a near-white dot for "5","white","blue"
|
|
9538
|
+
// — the NYC page's station base)
|
|
9539
|
+
let raw = Array.isArray(cs) ? cs[0] : cs;
|
|
9540
|
+
if (Array.isArray(cs) && isGeneratedColorScheme(cs)) {
|
|
9541
|
+
const sweep = flatColorSweep(cs[1], cs[2], Number(cs[0]) || 5, cs[3], cs[4]);
|
|
9542
|
+
if (sweep && sweep.length) raw = sweep[0];
|
|
9543
|
+
}
|
|
9263
9544
|
const filled = raw !== 'none';
|
|
9264
9545
|
// FEATURE|CATEGORICAL: polygons coloured by their category (flat colours
|
|
9265
9546
|
// each shape by its class), not one flat fill
|
|
@@ -9288,7 +9569,7 @@
|
|
|
9288
9569
|
getFillColor: byCategory
|
|
9289
9570
|
? f => {
|
|
9290
9571
|
const cat = this._resolveClassIndex(f.properties[this.binding.value]);
|
|
9291
|
-
return [...(cat != null ? this.categoryColorsRgb[cat] : nodataRgb).slice(0, 3), catAlpha];
|
|
9572
|
+
return [...(cat != null ? (this.categoryColorsRgb || [])[cat] || nodataRgb : nodataRgb).slice(0, 3), catAlpha];
|
|
9292
9573
|
}
|
|
9293
9574
|
: filled ? [...hexOrNamedToRgb(raw).slice(0, 3), Math.round(255 * cssColorAlpha(raw) * (styleNum(this.style.fillopacity) || 1))] : [0, 0, 0, 0],
|
|
9294
9575
|
getLineColor: this.style.linecolor
|
|
@@ -9495,7 +9776,7 @@
|
|
|
9495
9776
|
getFillColor: d => {
|
|
9496
9777
|
if (!hasData(d)) return [0, 0, 0, 0];
|
|
9497
9778
|
const rgb = d.properties.composedColor
|
|
9498
|
-
|| (d.properties.cat != null ? this.categoryColorsRgb[d.properties.cat] : null) || fallbackRgb;
|
|
9779
|
+
|| (d.properties.cat != null ? (this.categoryColorsRgb || [])[d.properties.cat] : null) || fallbackRgb;
|
|
9499
9780
|
const evidence = unmarkedEvidence(d);
|
|
9500
9781
|
if (evidence === 'hide') return [0, 0, 0, 0];
|
|
9501
9782
|
if (evidence === 'gray') return [...grayOf(rgb), 90];
|
|
@@ -9510,7 +9791,15 @@
|
|
|
9510
9791
|
// per-feature alpha (baked above) already carries the resolved
|
|
9511
9792
|
// base opacity — a layer-level opacity on TOP of that would
|
|
9512
9793
|
// multiply it a second time.
|
|
9513
|
-
opacity: dopacityActive ? 1 : baseOpacity
|
|
9794
|
+
opacity: dopacityActive ? 1 : baseOpacity,
|
|
9795
|
+
// Same coplanar z-fighting fix as the FEATURE/FEATURES GeoJsonLayer
|
|
9796
|
+
// (see _buildFeaturesLayers' own comment): under globe projection
|
|
9797
|
+
// the interleaved polygon surface and MapLibre's own sphere mesh
|
|
9798
|
+
// compete for the same depth-buffer real estate and the fill comes
|
|
9799
|
+
// out moiré/hatched ("holes"), never under flat Mercator. Draw
|
|
9800
|
+
// order against other polygon layers is unchanged — depthCompare
|
|
9801
|
+
// only settles the fight against the map's own surface.
|
|
9802
|
+
parameters: { depthCompare: 'always' }
|
|
9514
9803
|
})];
|
|
9515
9804
|
}
|
|
9516
9805
|
|
|
@@ -9566,6 +9855,13 @@
|
|
|
9566
9855
|
_isItemPlot() {
|
|
9567
9856
|
return this.flags.has('PLOT') && !this.flags.has('GRIDSIZE') && String(this.binding.value || '').includes('|');
|
|
9568
9857
|
}
|
|
9858
|
+
// a per-item SEQUENCE chart: CHART|SYMBOL|SEQUENCE, several value
|
|
9859
|
+
// fields per item, one part per field (no CATEGORICAL, no AGGREGATE)
|
|
9860
|
+
_isMultiFieldSequence() {
|
|
9861
|
+
return this.flags.has('CHART') && this.flags.has('SYMBOL') && flatFlag(this.flags, 'SEQUENCE')
|
|
9862
|
+
&& !this.flags.has('CATEGORICAL') && !this.flags.has('AGGREGATE') && !this.flags.has('PLOT')
|
|
9863
|
+
&& String(this.binding.value || '').includes('|');
|
|
9864
|
+
}
|
|
9569
9865
|
_itemPlotFields() {
|
|
9570
9866
|
return String(this.binding.value || '').split('|');
|
|
9571
9867
|
}
|
|
@@ -10688,7 +10984,7 @@
|
|
|
10688
10984
|
if (userLayers) return layers.concat(userLayers);
|
|
10689
10985
|
}
|
|
10690
10986
|
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')) {
|
|
10987
|
+
if ((flatFlag(this.flags, 'SEQUENCE') && this.flags.has('CATEGORICAL') && this.flags.has('AGGREGATE')) || (this._multiFields && this._isMultiFieldSequence())) {
|
|
10692
10988
|
return layers.concat(this._buildSequenceChartLayers(combined, zoom, liveZoom));
|
|
10693
10989
|
}
|
|
10694
10990
|
const fillOpacity = styleNum(this.style.fillopacity) || 0.85;
|
|
@@ -10726,7 +11022,7 @@
|
|
|
10726
11022
|
id: `ix-symbolshadow-${this.name}-g${this._iconGeneration}`,
|
|
10727
11023
|
data: combined, pickable: false,
|
|
10728
11024
|
getPosition: d => d.geometry.coordinates,
|
|
10729
|
-
getIcon: d => this._getShadowIcon(radiusOf(d), blurPx, d.properties.counts ? 'circle' : this._resolveSymbolShape(d.properties)),
|
|
11025
|
+
getIcon: d => this._getShadowIcon(radiusOf(d), blurPx, d.properties.counts && !Array.isArray(this.style.symbols) ? 'circle' : this._resolveSymbolShape(d.properties)),
|
|
10730
11026
|
getSize: d => radiusOf(d) * 3, // disc = 2/3 of the icon → its diameter is 2r
|
|
10731
11027
|
getPixelOffset: alignOf.active ? d => { const o = alignOf(d); return [o[0] + dx, o[1] + dy]; } : [dx, dy],
|
|
10732
11028
|
getColor: d => {
|
|
@@ -10751,7 +11047,7 @@
|
|
|
10751
11047
|
id: `ix-glow-${this.name}-g${this._iconGeneration}`,
|
|
10752
11048
|
data: combined, pickable: false,
|
|
10753
11049
|
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]),
|
|
11050
|
+
getIcon: d => this._getGlowIcon((this.categoryColorsRgb || [])[d.properties.counts ? dominant(this._groupIsolation(d) ? d.properties.counts.map((c, i) => (this._markedClasses.has(i) ? c : 0)) : d.properties.counts) : d.properties.cat]),
|
|
10755
11051
|
getSize: d => valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue) * (d.properties.counts ? 9 : 11),
|
|
10756
11052
|
getColor: d => [255, 255, 255, this._iconAlpha(d)],
|
|
10757
11053
|
...(alignOf.active ? { getPixelOffset: alignOf } : {}),
|
|
@@ -10798,15 +11094,29 @@
|
|
|
10798
11094
|
// Box and title are removed outside boxupper/boxlower.
|
|
10799
11095
|
// Flat draws box and title in the chart group, which is scaled by
|
|
10800
11096
|
// style.scale: the units below are u · scale. Its size measure W is
|
|
10801
|
-
// the
|
|
10802
|
-
//
|
|
10803
|
-
//
|
|
11097
|
+
// the width of the drawn chart (flat's getBox of the chart group,
|
|
11098
|
+
// 18785): the symbol (with MULTIQUAD the whole grid at the position,
|
|
11099
|
+
// the box enclosing all of it), its value text and its label
|
|
11100
|
+
// (chartItemExtent); flat's chart size is 30 units.
|
|
11101
|
+
// DTEXT: the title font follows the chart, max(0.9u, min(W/5,
|
|
11102
|
+
// 14u / scale)) (18815) — measured on the fire areas page's SIF chart
|
|
11103
|
+
// (W 362 units → font 72.35).
|
|
10804
11104
|
const boxShown = flatFlag(this.flags, 'BOX') && !boxHiddenByScale(this.style, liveZoom);
|
|
10805
11105
|
const circular = flatFlag(this.flags, 'CIRCULAR');
|
|
10806
11106
|
const unitPx = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1);
|
|
10807
11107
|
const boxMarginU = styleNum(this.style.boxmargin) || 2;
|
|
10808
11108
|
const titleShown = boxShown && flatFlag(this.flags, 'TITLE') && !!this.binding.title;
|
|
10809
|
-
const
|
|
11109
|
+
const titleScale = styleNum(this.style.textscale) || styleNum(this.style.valuescale) || 1;
|
|
11110
|
+
const titleFontOf = W => (flatFlag(this.flags, 'DTEXT')
|
|
11111
|
+
? Math.max(0.9 * unitPx, Math.min(W / 5, 14 * unitPx / (styleNum(this.style.scale) || 1)))
|
|
11112
|
+
: 5 * unitPx) * titleScale;
|
|
11113
|
+
// the chart's value text and label (see VALUES and LABEL below), shared
|
|
11114
|
+
// with the box: flat's box encloses whatever the chart group draws
|
|
11115
|
+
const valuesShown = this.flags.has('VALUES') && !valuesHiddenByScale(this.style, zoom);
|
|
11116
|
+
const valueTextOf = valuesShown ? this._chartValueTextFn() : null;
|
|
11117
|
+
const labelShown = !!this.style.labelfield && !flatFlag(this.flags, 'NORMSIZE') && !flatFlag(this.flags, 'ZOOM')
|
|
11118
|
+
&& !labelHiddenByScale(this.style, zoom);
|
|
11119
|
+
const labelOf = labelShown ? this._chartLabelFn(unitPx) : null;
|
|
10810
11120
|
// flat: fill-opacity (boxopacity || 1) on the raw style value — the
|
|
10811
11121
|
// usual string "0" is transparent; typed here, a given 0 is too
|
|
10812
11122
|
const boxOpacity = isNaN(styleNum(this.style.boxopacity)) ? 1 : styleNum(this.style.boxopacity);
|
|
@@ -10817,9 +11127,13 @@
|
|
|
10817
11127
|
const r = valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue);
|
|
10818
11128
|
const ext = multi ? multi.extent.get(d) : [0, 0, 0, 0];
|
|
10819
11129
|
const chart = [ext[0] - r, ext[1] - r, ext[2] + r, ext[3] + r];
|
|
11130
|
+
const grow = e => { if (!e) return; chart[0] = Math.min(chart[0], e[0]); chart[1] = Math.min(chart[1], e[1]); chart[2] = Math.max(chart[2], e[2]); chart[3] = Math.max(chart[3], e[3]); };
|
|
11131
|
+
if (valueTextOf && !d.properties.counts) { const v = valueTextOf(d, r); if (v) grow(v.extent); }
|
|
11132
|
+
if (labelOf) { const l = labelOf(d, r); if (l) grow(l.extent); }
|
|
10820
11133
|
const w = chart[2] - chart[0];
|
|
10821
11134
|
const margin = Math.min(2 * boxMarginU * unitPx, boxMarginU * w / 30);
|
|
10822
11135
|
const boxR = circular ? r * 1.1 + margin : r;
|
|
11136
|
+
const titleFontPx = titleFontOf(w);
|
|
10823
11137
|
let title = null;
|
|
10824
11138
|
if (titleShown) {
|
|
10825
11139
|
const text = this._itemTitle(d.properties);
|
|
@@ -10835,7 +11149,7 @@
|
|
|
10835
11149
|
const anchor = circular ? 'middle' : (alignRight ? 'end' : 'start');
|
|
10836
11150
|
const x0 = anchor === 'middle' ? x - width / 2 : anchor === 'end' ? x - width : x;
|
|
10837
11151
|
// arial: ascent 0.905, descent 0.212 of the font size
|
|
10838
|
-
title = { text: lines.join('\n'), x, anchor, top: base - 0.8 * f,
|
|
11152
|
+
title = { text: lines.join('\n'), x, anchor, top: base - 0.8 * f, f,
|
|
10839
11153
|
extent: [x0, base - 0.905 * f, x0 + width, base + (lines.length - 1) * f + 0.212 * f] };
|
|
10840
11154
|
}
|
|
10841
11155
|
}
|
|
@@ -10940,8 +11254,8 @@
|
|
|
10940
11254
|
data: combined, pickable: true,
|
|
10941
11255
|
getPosition: d => d.geometry.coordinates,
|
|
10942
11256
|
getIcon: d => d.properties.counts
|
|
10943
|
-
? this._buildBubbleIcon(d.properties.counts, this.categoryColorsRgb, this._groupIsolation(d))
|
|
10944
|
-
: this._buildSingleIcon(this.categoryColorsRgb[d.properties.cat], fillOpacity, this._resolveSymbolShape(d.properties), singleBorderColorRgb, singleBorderWidthPx),
|
|
11257
|
+
? this._buildBubbleIcon(d.properties.counts, this.categoryColorsRgb, this._groupIsolation(d), Array.isArray(this.style.symbols) ? this.style.symbols : null)
|
|
11258
|
+
: this._buildSingleIcon((this.categoryColorsRgb || [])[d.properties.cat], fillOpacity, this._resolveSymbolShape(d.properties), singleBorderColorRgb, singleBorderWidthPx),
|
|
10945
11259
|
getSize: d => valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue) * 2,
|
|
10946
11260
|
// group icons bake a fixed 0.9 alpha; an explicit fillopacity scales it
|
|
10947
11261
|
// so a near-transparent theme doesn't show opaque 1-px-cell clusters
|
|
@@ -10959,7 +11273,7 @@
|
|
|
10959
11273
|
data: titleData, pickable: false,
|
|
10960
11274
|
getPosition: b => b.d.geometry.coordinates,
|
|
10961
11275
|
getText: b => b.title.text,
|
|
10962
|
-
getSize:
|
|
11276
|
+
getSize: b => b.title.f,
|
|
10963
11277
|
sizeUnits: 'pixels',
|
|
10964
11278
|
fontFamily: 'arial',
|
|
10965
11279
|
characterSet: 'auto',
|
|
@@ -10984,18 +11298,20 @@
|
|
|
10984
11298
|
// computed font size would be sub-pixel are dropped rather than
|
|
10985
11299
|
// rendered at getSize: 0, matching the real engine's own "too
|
|
10986
11300
|
// small to bother" gate.
|
|
10987
|
-
if (
|
|
11301
|
+
if (valuesShown) {
|
|
10988
11302
|
const valueScale = styleNum(this.style.valuescale) || 1;
|
|
10989
11303
|
const textOpts = this._valueTextOpts();
|
|
11304
|
+
const pointValueOf = valueTextOf || this._chartValueTextFn();
|
|
10990
11305
|
|
|
10991
11306
|
const pointLabels = individual.reduce((out, d) => {
|
|
10992
11307
|
const radius = valueRadius(d.properties.value, liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue);
|
|
10993
|
-
// flat's VALUES label prints the value (nValuesA), not the size
|
|
10994
|
-
|
|
10995
|
-
|
|
10996
|
-
const fontSize = valuesFontSizePx(radius, text, valueScale);
|
|
11308
|
+
// flat's VALUES label prints the value (nValuesA), not the size;
|
|
11309
|
+
// its font by flat's chart branch (_chartValueTextFn)
|
|
11310
|
+
const { text, fontSize, bold, baseline } = pointValueOf(d, radius);
|
|
10997
11311
|
if (fontSize > VALUES_MIN_FONT_PX) {
|
|
10998
|
-
|
|
11312
|
+
const o = alignOf(d);
|
|
11313
|
+
out.push({ geometry: d.geometry, text, fontSize, bold, color: resolveTextColor(this.style, contrastTextColor((this.categoryColorsRgb || [])[d.properties.cat])),
|
|
11314
|
+
pixelOffset: baseline ? [o[0], o[1] + baseline - 0.35 * fontSize] : o });
|
|
10999
11315
|
}
|
|
11000
11316
|
return out;
|
|
11001
11317
|
}, []);
|
|
@@ -11026,7 +11342,7 @@
|
|
|
11026
11342
|
if (fontSize > VALUES_MIN_FONT_PX) {
|
|
11027
11343
|
out.push({
|
|
11028
11344
|
geometry: d.geometry, text, fontSize,
|
|
11029
|
-
color: resolveTextColor(this.style, contrastTextColor(this.categoryColorsRgb[p.i])),
|
|
11345
|
+
color: resolveTextColor(this.style, contrastTextColor((this.categoryColorsRgb || [])[p.i])),
|
|
11030
11346
|
pixelOffset: [offsets[i].x * fitScale * pxPerCanvasUnit + alignOf(d)[0], offsets[i].y * fitScale * pxPerCanvasUnit + alignOf(d)[1]]
|
|
11031
11347
|
});
|
|
11032
11348
|
}
|
|
@@ -11046,10 +11362,55 @@
|
|
|
11046
11362
|
getTextAnchor: 'middle',
|
|
11047
11363
|
getAlignmentBaseline: 'center'
|
|
11048
11364
|
};
|
|
11049
|
-
|
|
11365
|
+
// flat's SYMBOL branch writes its values in normal weight
|
|
11366
|
+
if (pointLabels.length) layers.push(new TextLayer({ id: `ix-points-values-${this.name}`, data: pointLabels, getPixelOffset: d => d.pixelOffset, ...textLayerCommonProps,
|
|
11367
|
+
fontWeight: pointLabels[0].bold === false ? 'normal' : 'bold' }));
|
|
11050
11368
|
if (groupLabels.length) layers.push(new TextLayer({ id: `ix-cluster-values-${this.name}`, data: groupLabels, getPixelOffset: d => d.pixelOffset, ...textLayerCommonProps }));
|
|
11051
11369
|
}
|
|
11052
11370
|
|
|
11371
|
+
// LABEL: flat's chart label (maptheme.js 24533-24590) — a small text
|
|
11372
|
+
// below-right of each chart, the .style({labelfield}) record value
|
|
11373
|
+
// (the fire page's labelfield ANNO: the fire year "2 014"), black at
|
|
11374
|
+
// 0.7 opacity on a white 0.4-opacity rounded background box, font
|
|
11375
|
+
// 5·unit·textscale. Gated by labelupper/labellower (flat's own test
|
|
11376
|
+
// compares <= against BOTH), suppressed by NORMSIZE/ZOOM, a numeric
|
|
11377
|
+
// label formatted like a value (valuedecimals) plus labelunits.
|
|
11378
|
+
if (labelShown) {
|
|
11379
|
+
const labelFontPx = 5 * unitPx * (styleNum(this.style.textscale) || 1);
|
|
11380
|
+
if (labelFontPx > VALUES_MIN_FONT_PX) {
|
|
11381
|
+
const labelData = [];
|
|
11382
|
+
for (const d of individual) {
|
|
11383
|
+
if (!d.properties || !d.properties.raw) continue;
|
|
11384
|
+
// _chartLabelFn: text, and its place below-right of the chart
|
|
11385
|
+
const l = labelOf(d, radiusOf(d));
|
|
11386
|
+
if (!l) continue;
|
|
11387
|
+
const o = alignOf(d);
|
|
11388
|
+
// drawn centered: flat's baseline sits 0.35 font below the
|
|
11389
|
+
// middle of its background box (-0.95 .. +0.25 font)
|
|
11390
|
+
labelData.push({ geometry: d.geometry, text: l.text, fontSize: l.fontSize,
|
|
11391
|
+
pixelOffset: [l.offset[0] + o[0], l.offset[1] - 0.35 * l.fontSize + o[1]] });
|
|
11392
|
+
}
|
|
11393
|
+
if (labelData.length) layers.push(new TextLayer({
|
|
11394
|
+
id: `ix-labels-${this.name}`,
|
|
11395
|
+
data: labelData, pickable: false,
|
|
11396
|
+
getPosition: d => d.geometry.coordinates,
|
|
11397
|
+
getText: d => d.text,
|
|
11398
|
+
getSize: d => d.fontSize,
|
|
11399
|
+
getColor: [0, 0, 0, Math.round(0.7 * 255)],
|
|
11400
|
+
sizeUnits: 'pixels',
|
|
11401
|
+
fontFamily: 'arial',
|
|
11402
|
+
characterSet: 'auto',
|
|
11403
|
+
getTextAnchor: 'middle',
|
|
11404
|
+
getAlignmentBaseline: 'center',
|
|
11405
|
+
getPixelOffset: d => d.pixelOffset,
|
|
11406
|
+
background: true,
|
|
11407
|
+
getBackgroundColor: [255, 255, 255, Math.round(0.4 * 255)],
|
|
11408
|
+
backgroundPadding: [labelFontPx * 0.25, labelFontPx * 0.1],
|
|
11409
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
11410
|
+
}));
|
|
11411
|
+
}
|
|
11412
|
+
}
|
|
11413
|
+
|
|
11053
11414
|
return layers;
|
|
11054
11415
|
}
|
|
11055
11416
|
|
|
@@ -11083,12 +11444,25 @@
|
|
|
11083
11444
|
// - VALUES: each part's value (COUNT: its records; valuefield: the class name)
|
|
11084
11445
|
// - align / offsets as the SYMBOL branch: half size the normal radius
|
|
11085
11446
|
// A part's pick object carries its chart's properties (tooltip).
|
|
11447
|
+
// The same for a per-item chart over several value fields
|
|
11448
|
+
// (_isMultiFieldSequence): a part per field, max value the biggest
|
|
11449
|
+
// field value of any record (normalsizevalue overrides), the part
|
|
11450
|
+
// radius value-scaled as above; a colorscheme "none" part is only its
|
|
11451
|
+
// outline. RINGS nests the parts (ringsLayout); CENTERVALUES prints
|
|
11452
|
+
// only the first part's value (flat: nClass 0, maptheme.js 22216).
|
|
11086
11453
|
_buildSequenceChartLayers(charts, zoom, liveZoom) {
|
|
11087
11454
|
const st = this.style;
|
|
11088
|
-
const
|
|
11089
|
-
const
|
|
11455
|
+
const multi = !!this._multiFields && this._isMultiFieldSequence();
|
|
11456
|
+
const seqMax = multi ? (styleNum(st.normalsizevalue) || this._multiFieldPartMax) : this._sequenceMaxValue(zoom);
|
|
11090
11457
|
const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(st.scale) || 1);
|
|
11091
11458
|
const normalR = NORMAL_RADIUS_PX * unit;
|
|
11459
|
+
const sizePow = resolveSizePow(st, this.flags);
|
|
11460
|
+
const partRadius = multi
|
|
11461
|
+
? v => Math.max(VALUE_RADIUS_MIN, normalR * Math.pow(Math.abs(v) / seqMax, 1 / sizePow))
|
|
11462
|
+
: v => valueRadius(v, liveZoom, st, this.mapOptions, this.flags, seqMax);
|
|
11463
|
+
const rings = flatFlag(this.flags, 'RINGS');
|
|
11464
|
+
const centerValues = flatFlag(this.flags, 'CENTERVALUES');
|
|
11465
|
+
const noFill = this._partNoFill || new Set();
|
|
11092
11466
|
const nCats = this.categoryLabels ? this.categoryLabels.length : 1;
|
|
11093
11467
|
const marking = this._markedClasses.size && !this._onMarksChanged;
|
|
11094
11468
|
const gray = marking && String(st.evidence || 'isolate') === 'isolate_gray';
|
|
@@ -11097,11 +11471,13 @@
|
|
|
11097
11471
|
const borderRgb = st.linecolor && styleLineColor(st.linecolor) !== 'none' ? hexOrNamedToRgb(styleLineColor(st.linecolor)) : null;
|
|
11098
11472
|
const lineWidth = styleNum(st.linewidth) || 1;
|
|
11099
11473
|
const symbols = Array.isArray(st.symbols) ? st.symbols : null;
|
|
11100
|
-
const shapeOf = i => { const sh = symbols && (symbols[i] || symbols[0]); return
|
|
11474
|
+
const shapeOf = i => { const sh = symbols && (symbols[i] || symbols[0]); return normalizeSymbolShape(sh); };
|
|
11101
11475
|
const colorOf = i => this.categoryColorsRgb[i] || [128, 128, 128];
|
|
11102
11476
|
const parts = [], glows = [];
|
|
11103
11477
|
for (const d of charts) {
|
|
11104
|
-
|
|
11478
|
+
const counts = d.properties.parts || d.properties.counts;
|
|
11479
|
+
let layout = rings ? ringsLayout(counts, partRadius)
|
|
11480
|
+
: sequenceLayout(counts, this.flags, partRadius, { maxRadius: normalR, maxValue: seqMax, nParts: nCats, rangeScale: styleNum(st.rangescale) || 0 });
|
|
11105
11481
|
if (!layout.length) continue;
|
|
11106
11482
|
// GLOW belongs to the first part (sorted) or to every part, and
|
|
11107
11483
|
// hides with it when its class is not marked
|
|
@@ -11155,7 +11531,7 @@
|
|
|
11155
11531
|
data: parts, pickable: true,
|
|
11156
11532
|
getPosition: d => d.geometry.coordinates,
|
|
11157
11533
|
// 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,
|
|
11534
|
+
getIcon: d => this._buildSingleIcon(colorOf(d.part.i), noFill.has(d.part.i) ? 0 : fillOpacity, shapeOf(d.part.i), borderRgb,
|
|
11159
11535
|
borderRgb ? Math.round(lineWidth * unit * Math.min(2 * d.part.r / normalR, 0.2) * BUBBLE_ICON_SIZE / Math.max(1, 2 * d.part.r) * 4) / 4 : 0),
|
|
11160
11536
|
getSize: d => d.part.r * 2,
|
|
11161
11537
|
getPixelOffset: offsetOf,
|
|
@@ -11168,20 +11544,22 @@
|
|
|
11168
11544
|
// class name — formatted with valuedecimals (else none) plus the unit;
|
|
11169
11545
|
// font min(1, 3.2 / L) · r · valuescale, L the length of the theme's
|
|
11170
11546
|
// biggest value text (szMaxText), normal weight, in valuecolor /
|
|
11171
|
-
// textcolor, else ChartColors.textColor of the part color
|
|
11172
|
-
|
|
11547
|
+
// textcolor, else ChartColors.textColor of the part color.
|
|
11548
|
+
// CENTERVALUES: only on the first part (class 0), its own value
|
|
11549
|
+
if ((this.flags.has('VALUES') || centerValues) && !valuesHiddenByScale(st, zoom)) {
|
|
11173
11550
|
const valueScale = styleNum(st.valuescale) || 1;
|
|
11174
11551
|
const { noBreaks } = this._valueTextOpts();
|
|
11175
11552
|
const units = st.units ? String(st.units) : '';
|
|
11176
11553
|
const unit = units ? (units[0] === '.' ? '' : ' ') + units : '';
|
|
11177
11554
|
const unitText = unit.length <= 5 ? unit : '';
|
|
11178
|
-
const dec =
|
|
11555
|
+
const dec = explicitValueDecimals(st) || 0;
|
|
11179
11556
|
const fmt = v => flatGroupedValue(v, dec, noBreaks) + unitText;
|
|
11180
11557
|
const maxLen = fmt(seqMax).length;
|
|
11181
11558
|
const textOverride = st.valuecolor || st.textcolor;
|
|
11182
11559
|
const counting = flatFlag(this.flags, 'COUNT');
|
|
11183
11560
|
const labels = [];
|
|
11184
11561
|
parts.forEach(d => {
|
|
11562
|
+
if (centerValues && d.part.i !== 0) return;
|
|
11185
11563
|
const rec = this._categoryValueRecord(d.part.i);
|
|
11186
11564
|
const name = rec ? String(rec[st.valuefield]) : null;
|
|
11187
11565
|
const t = counting && d.properties.recordCounts ? d.properties.recordCounts[d.part.i] : d.part.v;
|
|
@@ -11241,6 +11619,69 @@
|
|
|
11241
11619
|
// the class name (the records' own valuefield value) — otherwise none
|
|
11242
11620
|
_categoryValueRecord(i) { return categoryValueRecord(this.style, this.binding, this.flags, this.categoryLabels, i); }
|
|
11243
11621
|
|
|
11622
|
+
// A single chart's VALUES text: (item, its radius px) → { text,
|
|
11623
|
+
// fontSize, extent } — extent [x0, y0, x1, y1] in px around the chart
|
|
11624
|
+
// center (y down), as the box encloses it. The font by flat's branch:
|
|
11625
|
+
// - SYMBOL (maptheme.js 22200-22202, 21705-21708): min(1, 3.2 / L)
|
|
11626
|
+
// (hexagon 2.7) · r · valuescale, L the length of the theme's biggest
|
|
11627
|
+
// value text (szMaxText: the max value formatted, plus the unit up to
|
|
11628
|
+
// 5 characters, 21479) — normal weight, baseline 0.33 font below the
|
|
11629
|
+
// center (fire areas SIF chart: r 60, valuescale 3, "7 242 ha" → 72)
|
|
11630
|
+
// - BUBBLE: valuesFontSizePx, bold, centered
|
|
11631
|
+
_chartValueTextFn() {
|
|
11632
|
+
const valueScale = styleNum(this.style.valuescale) || 1;
|
|
11633
|
+
const textOpts = this._valueTextOpts();
|
|
11634
|
+
const symbol = flatChartBranch(this.flags) === 'symbol';
|
|
11635
|
+
let maxLen = 1;
|
|
11636
|
+
if (symbol) {
|
|
11637
|
+
const vMax = this._valueMax !== undefined ? this._valueMax : (this._valueTextRange || [])[1];
|
|
11638
|
+
if (isFinite(vMax)) maxLen = Math.max(1, flatValueText(null, null, vMax, this.style, this.flags, textOpts).length);
|
|
11639
|
+
}
|
|
11640
|
+
return (d, r) => {
|
|
11641
|
+
const p = d.properties;
|
|
11642
|
+
const text = flatValueText(p.raw, this.binding.title && p.raw ? p.raw[this.binding.title] : undefined,
|
|
11643
|
+
p.classValue !== undefined ? p.classValue : p.value, this.style, this.flags, textOpts);
|
|
11644
|
+
if (!symbol) {
|
|
11645
|
+
const fontSize = valuesFontSizePx(r, text, valueScale);
|
|
11646
|
+
const tw = titleTextWidth(text, fontSize);
|
|
11647
|
+
return { text, fontSize, bold: true, extent: [-tw / 2, -fontSize / 2, tw / 2, fontSize / 2] };
|
|
11648
|
+
}
|
|
11649
|
+
const k = this._resolveSymbolShape(p) === 'hexagon' ? 2.7 : 3.2;
|
|
11650
|
+
const fontSize = Math.min(1, k / maxLen) * r * valueScale;
|
|
11651
|
+
const tw = titleTextWidth(text, fontSize);
|
|
11652
|
+
return { text, fontSize, bold: false, baseline: 0.33 * fontSize,
|
|
11653
|
+
extent: [-tw / 2, (0.33 - 0.905) * fontSize, tw / 2, (0.33 + 0.212) * fontSize] };
|
|
11654
|
+
};
|
|
11655
|
+
}
|
|
11656
|
+
|
|
11657
|
+
// A single chart's LABEL (.style({labelfield}), maptheme.js 24533-24590):
|
|
11658
|
+
// (item, its radius px) → { text, fontSize, offset, extent } or null.
|
|
11659
|
+
// Font 5u · textscale; a number formatted like a value — valuedecimals
|
|
11660
|
+
// when set (flat's nValueDecimals is the raw style string, so "0"
|
|
11661
|
+
// counts), else 2 — plus labelunits. The background box is the wider of
|
|
11662
|
+
// the text and (length + 2) · font / 2, 1.2 font high from 0.95 font
|
|
11663
|
+
// above the baseline; the label sits at (width / 3, font / 3 + the
|
|
11664
|
+
// chart's anchor y: r + 5u). Fire areas page: "2 010" at (116.7, 193.3)
|
|
11665
|
+
// units, box 350 × 120.
|
|
11666
|
+
_chartLabelFn(unitPx) {
|
|
11667
|
+
const f = 5 * unitPx * (styleNum(this.style.textscale) || 1);
|
|
11668
|
+
const dec = explicitValueDecimals(this.style);
|
|
11669
|
+
const { noBreaks } = this._valueTextOpts();
|
|
11670
|
+
const units = String(this.style.labelunits || '');
|
|
11671
|
+
return (d, r) => {
|
|
11672
|
+
const raw = d.properties && d.properties.raw;
|
|
11673
|
+
const v = raw && raw[this.style.labelfield];
|
|
11674
|
+
let text = v == null || v === '' ? '' : String(v);
|
|
11675
|
+
if (!text) return null;
|
|
11676
|
+
const num = parseFloat(text.replace(',', '.'));
|
|
11677
|
+
text = (!isNaN(num) ? flatGroupedValue(num, dec != null ? dec : 2, noBreaks) : text) + units;
|
|
11678
|
+
const width = Math.max(titleTextWidth(text, f), (text.length + 2) * f * 0.5);
|
|
11679
|
+
const x = width / 3, y = f / 3 + r + 5 * unitPx;
|
|
11680
|
+
return { text, fontSize: f, offset: [x, y], width,
|
|
11681
|
+
extent: [x - width / 2, y - 0.95 * f, x + width / 2, y + 0.25 * f] };
|
|
11682
|
+
};
|
|
11683
|
+
}
|
|
11684
|
+
|
|
11244
11685
|
// value-text options (flatValueText): flat's formatValue NOBREAKS for a
|
|
11245
11686
|
// theme of values within 1000-3000, the max value for the decimals
|
|
11246
11687
|
_valueTextOpts() {
|
|
@@ -11431,6 +11872,14 @@
|
|
|
11431
11872
|
// DOWN, confirmed against a real ported page's own comment ("il motore
|
|
11432
11873
|
// disegna il triangolo con la punta in basso" — "the engine draws the
|
|
11433
11874
|
// triangle with the tip at the bottom").
|
|
11875
|
+
// flat's symbol shapes (maptheme.js 21625-21680). Geometry follows flat
|
|
11876
|
+
// where GL didn't have an established shape already (square, diamond,
|
|
11877
|
+
// triangle keep GL's own proportions — pages already shipped with them):
|
|
11878
|
+
// roundrect — rect 1.6r, rounded 0.3r; label — wide flat rect 1.6r×0.8r;
|
|
11879
|
+
// carot — flat's alias of diamond; hexagon — vertices (±r,0),(±r/2,±r·sin60°);
|
|
11880
|
+
// cross — flat's 8-arm path (A=0.4r, B=0.8r); empty — the circle
|
|
11881
|
+
// geometry, but _buildSingleIcon gives it flat's transparent fill +
|
|
11882
|
+
// gray stroke (fill-opacity:0, stroke #888888)
|
|
11434
11883
|
function drawSymbolPath(ctx, shape, cx, cy, r) {
|
|
11435
11884
|
switch (shape) {
|
|
11436
11885
|
case 'square': {
|
|
@@ -11439,6 +11888,7 @@
|
|
|
11439
11888
|
break;
|
|
11440
11889
|
}
|
|
11441
11890
|
case 'diamond':
|
|
11891
|
+
case 'carot': // flat: carot and diamond are the same rotated rect
|
|
11442
11892
|
ctx.moveTo(cx, cy - r);
|
|
11443
11893
|
ctx.lineTo(cx + r, cy);
|
|
11444
11894
|
ctx.lineTo(cx, cy + r);
|
|
@@ -11449,10 +11899,58 @@
|
|
|
11449
11899
|
ctx.lineTo(cx + r, cy - r * 0.6);
|
|
11450
11900
|
ctx.lineTo(cx, cy + r);
|
|
11451
11901
|
break;
|
|
11902
|
+
case 'roundrect': {
|
|
11903
|
+
const s = r * 0.8;
|
|
11904
|
+
if (ctx.roundRect) ctx.roundRect(cx - s, cy - s, s * 2, s * 2, r * 0.3);
|
|
11905
|
+
else ctx.rect(cx - s, cy - s, s * 2, s * 2);
|
|
11906
|
+
break;
|
|
11907
|
+
}
|
|
11908
|
+
case 'label': {
|
|
11909
|
+
const w = r * 0.8, h = r * 0.4;
|
|
11910
|
+
ctx.rect(cx - w, cy - h, w * 2, h * 2);
|
|
11911
|
+
break;
|
|
11912
|
+
}
|
|
11913
|
+
case 'hexagon': {
|
|
11914
|
+
const A = r / 2, B = r * Math.sin(Math.PI / 3);
|
|
11915
|
+
ctx.moveTo(cx - r, cy);
|
|
11916
|
+
ctx.lineTo(cx - A, cy - B);
|
|
11917
|
+
ctx.lineTo(cx + A, cy - B);
|
|
11918
|
+
ctx.lineTo(cx + r, cy);
|
|
11919
|
+
ctx.lineTo(cx + A, cy + B);
|
|
11920
|
+
ctx.lineTo(cx - A, cy + B);
|
|
11921
|
+
break;
|
|
11922
|
+
}
|
|
11923
|
+
case 'cross': {
|
|
11924
|
+
// flat's own path, vertex for vertex: a plus of arm thickness A
|
|
11925
|
+
// reaching to ±(B + A/2) (maptheme.js 21652-21655)
|
|
11926
|
+
const A = r * 0.4, B = r * 0.8, E = B + A / 2;
|
|
11927
|
+
ctx.moveTo(cx, cy - E);
|
|
11928
|
+
ctx.lineTo(cx + A / 2, cy - E);
|
|
11929
|
+
ctx.lineTo(cx + A / 2, cy - A / 2);
|
|
11930
|
+
ctx.lineTo(cx + E, cy - A / 2);
|
|
11931
|
+
ctx.lineTo(cx + E, cy + A / 2);
|
|
11932
|
+
ctx.lineTo(cx + A / 2, cy + A / 2);
|
|
11933
|
+
ctx.lineTo(cx + A / 2, cy + E);
|
|
11934
|
+
ctx.lineTo(cx - A / 2, cy + E);
|
|
11935
|
+
ctx.lineTo(cx - A / 2, cy + A / 2);
|
|
11936
|
+
ctx.lineTo(cx - E, cy + A / 2);
|
|
11937
|
+
ctx.lineTo(cx - E, cy - A / 2);
|
|
11938
|
+
ctx.lineTo(cx - A / 2, cy - A / 2);
|
|
11939
|
+
ctx.lineTo(cx - A / 2, cy - E);
|
|
11940
|
+
break;
|
|
11941
|
+
}
|
|
11942
|
+
case 'empty':
|
|
11452
11943
|
default: // circle
|
|
11453
11944
|
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
11454
11945
|
}
|
|
11455
11946
|
}
|
|
11947
|
+
// the shape names flat's szSymbol switch knows (maptheme.js 21625) —
|
|
11948
|
+
// anything else falls back to circle, never silently dropped
|
|
11949
|
+
const SYMBOL_SHAPE_RE = /^(circle|square|diamond|triangle|roundrect|carot|hexagon|cross|empty|label)$/;
|
|
11950
|
+
function normalizeSymbolShape(name) {
|
|
11951
|
+
const n = String(name || '').toLowerCase();
|
|
11952
|
+
return SYMBOL_SHAPE_RE.test(n) ? n : 'circle';
|
|
11953
|
+
}
|
|
11456
11954
|
|
|
11457
11955
|
// a color's own opacity, 0..1, as SVG paints it in flat: "none" (SVG's
|
|
11458
11956
|
// no paint — a polygon with only its outline, or a class not painted)
|
|
@@ -12277,7 +12775,7 @@
|
|
|
12277
12775
|
// test-only: lets test/unit/*.test.mjs call pure internals directly (the
|
|
12278
12776
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
12279
12777
|
global.__ixmapsGlInternals = {
|
|
12280
|
-
IXMAPS_GL_VERSION, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout, multiQuadOffsets, pixelOffsetLngLat,
|
|
12778
|
+
IXMAPS_GL_VERSION, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout, ringsLayout, multiQuadOffsets, pixelOffsetLngLat,
|
|
12281
12779
|
normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
12282
12780
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
12283
12781
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|
|
@@ -12287,7 +12785,8 @@
|
|
|
12287
12785
|
flatToMapLibreZoom, mapLibreToFlatZoom, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
|
|
12288
12786
|
fetchLayerData, parseCsvText, dataTableRows, geometryRowsToFeatureCollection, filterFlatValues, applyField100, field100Binding, rangeClassLegendTotals, resolveAggregateValue, classValueSeparate, cellAggregatedValues, oneHot, groupCoLocated, aggregateOnGrid, GridAggregateIndex, flatRangeParts, aggregateField100, valueRadius, itemPlotGeometry, itemAnchor, objectZoomFactor, resolveZoomReference, defaultNormalSizeValue,
|
|
12289
12787
|
applyWhereFilter, joinChartPositions, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
12290
|
-
|
|
12788
|
+
drawSymbolPath, normalizeSymbolShape,
|
|
12789
|
+
flatFormatValue, themeStyleArgs, explicitValueDecimals, legendDecimals,
|
|
12291
12790
|
dataCacheKey, dataCacheDisabled, cachedLayerData, featuresBounds, wantsZoomToExtent, legendIsOn,
|
|
12292
12791
|
};
|
|
12293
12792
|
global.__setFilter = __setFilter;
|