ixmaps-gl 0.2.17 → 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 +430 -66
- 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.18/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.18/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.18/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.18';
|
|
86
86
|
|
|
87
87
|
const LIB_URLS = {
|
|
88
88
|
// Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
|
|
@@ -2479,6 +2479,44 @@
|
|
|
2479
2479
|
return typeof v === 'number' ? v : NaN;
|
|
2480
2480
|
}
|
|
2481
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
|
+
|
|
2482
2520
|
function normalizeTheme(def) {
|
|
2483
2521
|
// real ixmaps-flat (htmlgui.js newTheme) merges .meta() INTO style first
|
|
2484
2522
|
// — meta wins — and only then reads type, bindings and style properties
|
|
@@ -3444,28 +3482,36 @@
|
|
|
3444
3482
|
// sync — no separate light/dark branch anywhere else.
|
|
3445
3483
|
// flat's look (flatLegendLook) unless the page picks one with
|
|
3446
3484
|
// this engine's own legendtheme
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
//
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
:
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
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();
|
|
3469
3515
|
// max-height:66.6% resolves against el's own height (the map
|
|
3470
3516
|
// container, which always has a definite height for the map
|
|
3471
3517
|
// itself to render into) since this panel sits in the stack,
|
|
@@ -3886,6 +3932,7 @@
|
|
|
3886
3932
|
// sharing the same data source and calls refresh()), so this
|
|
3887
3933
|
// is UI wiring only, no new filtering logic.
|
|
3888
3934
|
const filterField = rt.style.legendfilter;
|
|
3935
|
+
let filterSelect = null;
|
|
3889
3936
|
if (filterField) {
|
|
3890
3937
|
const facet = engineApi.getFacets(rt.name, [filterField], { bbox: [-180, -85, 180, 85] })[0];
|
|
3891
3938
|
const values = (facet && facet.type === 'textual')
|
|
@@ -3901,8 +3948,27 @@
|
|
|
3901
3948
|
else engineApi.clearFacetFilter(rt.name, filterField);
|
|
3902
3949
|
});
|
|
3903
3950
|
bodyEl.appendChild(filterEl);
|
|
3951
|
+
filterSelect = filterEl;
|
|
3904
3952
|
}
|
|
3905
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
|
+
|
|
3906
3972
|
if (!isTextOnly) { bodyEl.appendChild(rowsScroll); renderRows(); }
|
|
3907
3973
|
|
|
3908
3974
|
if (rt.meta.description) {
|
|
@@ -3959,6 +4025,9 @@
|
|
|
3959
4025
|
}
|
|
3960
4026
|
};
|
|
3961
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(); }));
|
|
3962
4031
|
}
|
|
3963
4032
|
return { addLegendPanel, setLegendOption, updateSubTheme, layoutLegends };
|
|
3964
4033
|
}
|
|
@@ -4495,6 +4564,33 @@
|
|
|
4495
4564
|
return engineApi;
|
|
4496
4565
|
},
|
|
4497
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
|
+
},
|
|
4498
4594
|
remove: (id) => { removeThemeById(id); return engineApi; },
|
|
4499
4595
|
// flat's map.setMapType(id) / setMapTypeId / mapType (htmlgui_flat.js
|
|
4500
4596
|
// mapApi): swap the basemap at runtime. MapLibre's setStyle replaces
|
|
@@ -4616,23 +4712,40 @@
|
|
|
4616
4712
|
// "filter:WHERE ..." value reuses rt.setRuntimeFilter, the SAME
|
|
4617
4713
|
// applyWhereFilter grammar the layer's own load-time .filter()
|
|
4618
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').
|
|
4619
4722
|
changeThemeStyle: (themeId, styleKeyValue, action) => {
|
|
4620
|
-
const
|
|
4621
|
-
|
|
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;
|
|
4622
4727
|
const colonIdx = String(styleKeyValue).indexOf(':');
|
|
4623
4728
|
const key = colonIdx === -1 ? String(styleKeyValue) : styleKeyValue.slice(0, colonIdx);
|
|
4624
4729
|
const value = colonIdx === -1 ? '' : styleKeyValue.slice(colonIdx + 1);
|
|
4625
|
-
if (
|
|
4626
|
-
if (key === 'filter') {
|
|
4627
|
-
rt.setRuntimeFilter(action === 'remove' ? '' : value);
|
|
4628
|
-
refresh();
|
|
4629
|
-
} else if (key === 'type') {
|
|
4730
|
+
if (key === 'type') {
|
|
4630
4731
|
console.warn(`[ixmaps-gl] changeThemeStyle: changing the type ("${value}") is not supported`);
|
|
4631
|
-
|
|
4632
|
-
// any other style key, as flat: "set" writes it, "remove" drops it
|
|
4633
|
-
rt.setStyle({ [key]: /remove/.test(action || '') ? undefined : value });
|
|
4634
|
-
refresh();
|
|
4732
|
+
return;
|
|
4635
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();
|
|
4636
4749
|
},
|
|
4637
4750
|
// flat's ixmaps.setThemeTimeFrame(szId, min, max): show only the
|
|
4638
4751
|
// records whose timefield lies in [min, max) (ms or Date-
|
|
@@ -5191,10 +5304,26 @@
|
|
|
5191
5304
|
// never by size, so resizing the SAME cached icon every frame
|
|
5192
5305
|
// creates no new atlas entries — none of the reclustering risk
|
|
5193
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
|
+
}
|
|
5194
5323
|
function refreshLayers() {
|
|
5195
5324
|
const liveZoom = map.getZoom();
|
|
5196
5325
|
// frozen only while MapLibre really is zooming (see 'moveend')
|
|
5197
|
-
const zoom = isZooming && map.isZooming() ? stableGridZoom :
|
|
5326
|
+
const zoom = isZooming && map.isZooming() ? stableGridZoom : gridZoomOf();
|
|
5198
5327
|
const viewMoving = map.isMoving();
|
|
5199
5328
|
const bounds = map.getBounds();
|
|
5200
5329
|
const bbox = [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()];
|
|
@@ -5277,11 +5406,11 @@
|
|
|
5277
5406
|
// zoom too) picks up the hide/show within one 150ms tick — still
|
|
5278
5407
|
// effectively instant, without the unthrottled rebuild storm.
|
|
5279
5408
|
let isZooming = false;
|
|
5280
|
-
let stableGridZoom =
|
|
5409
|
+
let stableGridZoom = gridZoomOf();
|
|
5281
5410
|
map.on('zoomstart', () => { isZooming = true; });
|
|
5282
5411
|
map.on('zoomend', () => {
|
|
5283
5412
|
isZooming = false;
|
|
5284
|
-
stableGridZoom =
|
|
5413
|
+
stableGridZoom = gridZoomOf();
|
|
5285
5414
|
// Was: clearTimeout(refreshTimer); refreshLayers(); — a DIRECT,
|
|
5286
5415
|
// un-throttled rebuild on every single zoomend. That was fine
|
|
5287
5416
|
// while every runtime's per-tick cost was cheap (viewport-scoped
|
|
@@ -5331,7 +5460,7 @@
|
|
|
5331
5460
|
// the same arrows at every zoom, while the scale gates (liveZoom)
|
|
5332
5461
|
// still switched.
|
|
5333
5462
|
isZooming = false;
|
|
5334
|
-
stableGridZoom =
|
|
5463
|
+
stableGridZoom = gridZoomOf();
|
|
5335
5464
|
refresh();
|
|
5336
5465
|
});
|
|
5337
5466
|
|
|
@@ -5550,7 +5679,7 @@
|
|
|
5550
5679
|
// as ixmaps.map() does). The builder's own methods with different
|
|
5551
5680
|
// semantics — layer, view, options, require, attribution, on — keep
|
|
5552
5681
|
// theirs and are not overridden.
|
|
5553
|
-
for (const m of ['replace', 'add', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
5682
|
+
for (const m of ['replace', 'add', 'newTheme', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
5554
5683
|
'refreshTheme', 'redrawTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'setProjection', 'resize',
|
|
5555
5684
|
'loadProject', 'project', 'setThemeVisible', 'setThemeTimeFrame', 'show', 'hide', 'getZoom', 'getThemeObj',
|
|
5556
5685
|
'getThemes', 'theme', 'setView']) {
|
|
@@ -6916,6 +7045,83 @@
|
|
|
6916
7045
|
return { offset, index, extent };
|
|
6917
7046
|
}
|
|
6918
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
|
+
|
|
6919
7125
|
// A pixel offset [dx, dy] (y down) from lngLat at MapLibre zoom `zoom`,
|
|
6920
7126
|
// as lng/lat (Web Mercator, 512 px world tiles) — for shapes too large
|
|
6921
7127
|
// for an icon, drawn in map coordinates (rebuilt with every refresh).
|
|
@@ -8743,7 +8949,11 @@
|
|
|
8743
8949
|
}
|
|
8744
8950
|
|
|
8745
8951
|
_usesAggregationIndex() {
|
|
8746
|
-
|
|
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()));
|
|
8747
8957
|
}
|
|
8748
8958
|
|
|
8749
8959
|
_rebuildActiveFeatures() {
|
|
@@ -9487,16 +9697,19 @@
|
|
|
9487
9697
|
// GRIDSIZE (spatial grid binning) is checked before the generic
|
|
9488
9698
|
// CHART|SYMBOL bubble pipeline, same "more specific base type wins"
|
|
9489
9699
|
// precedence as DOT: PLOT+GRIDSIZE is the per-year curves mini-chart
|
|
9490
|
-
// (_buildPlotLayers); GRIDSIZE
|
|
9491
|
-
//
|
|
9492
|
-
//
|
|
9493
|
-
//
|
|
9494
|
-
//
|
|
9495
|
-
// 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.
|
|
9496
9705
|
if (this.flags.has('GRIDSIZE') && this.flags.has('PLOT')) return this._buildPlotLayers(zoom, bbox);
|
|
9497
9706
|
// PLOT without GRIDSIZE: one line chart per item over its value fields
|
|
9498
9707
|
if (this.flags.has('PLOT') && this._isItemPlot()) return this._buildItemPlotLayers(zoom);
|
|
9499
|
-
|
|
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);
|
|
9500
9713
|
// CHOROPLETH is checked before the generic FEATURE dispatch — a
|
|
9501
9714
|
// CHOROPLETH layer's own type string doesn't carry FEATURE/FEATURES
|
|
9502
9715
|
// (only its geometry-donor base layer does, see
|
|
@@ -9852,6 +10065,27 @@
|
|
|
9852
10065
|
// style.values/xaxis (explicit, see _prepare) if set, else whatever
|
|
9853
10066
|
// CATEGORICAL auto-discovered.
|
|
9854
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
|
+
}
|
|
9855
10089
|
_isItemPlot() {
|
|
9856
10090
|
return this.flags.has('PLOT') && !this.flags.has('GRIDSIZE') && String(this.binding.value || '').includes('|');
|
|
9857
10091
|
}
|
|
@@ -10715,6 +10949,20 @@
|
|
|
10715
10949
|
...(f.properties.titleRaw ? { titleRaw: f.properties.titleRaw } : {}) }
|
|
10716
10950
|
};
|
|
10717
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
|
+
}
|
|
10718
10966
|
return { individual, groups };
|
|
10719
10967
|
}
|
|
10720
10968
|
|
|
@@ -10842,6 +11090,10 @@
|
|
|
10842
11090
|
// _computeAggregatedItems below — a plain (non-AGGREGATE)
|
|
10843
11091
|
// BUBBLE/CHART theme never reads them.
|
|
10844
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));
|
|
10845
11097
|
// AGGREGATE sums without a normalsizevalue: flat's default is the
|
|
10846
11098
|
// largest aggregated value (defaultNormalSizeValue leaves it unset, the
|
|
10847
11099
|
// sizes depend on the aggregation)
|
|
@@ -10999,7 +11251,7 @@
|
|
|
10999
11251
|
const singleBorderWidthPx = styleNum(this.style.linewidth) || 0;
|
|
11000
11252
|
|
|
11001
11253
|
const itemRgb = d => this.categoryColorsRgb[d.properties.counts ? dominant(d.properties.counts) : d.properties.cat] || [128, 128, 128];
|
|
11002
|
-
const radiusOf = d =>
|
|
11254
|
+
const radiusOf = d => vRadius(sizeValueOf(d));
|
|
11003
11255
|
// align / offsetx / offsety: the whole chart moves (flatChartAlignOffset)
|
|
11004
11256
|
// MULTIQUAD / MULTISQUARE: the items at one position side by side
|
|
11005
11257
|
// (multiQuadOffsets), added to the chart position
|
|
@@ -11048,7 +11300,7 @@
|
|
|
11048
11300
|
data: combined, pickable: false,
|
|
11049
11301
|
getPosition: d => d.geometry.coordinates,
|
|
11050
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]),
|
|
11051
|
-
getSize: d =>
|
|
11303
|
+
getSize: d => vRadius(sizeValueOf(d)) * (d.properties.counts ? 9 : 11),
|
|
11052
11304
|
getColor: d => [255, 255, 255, this._iconAlpha(d)],
|
|
11053
11305
|
...(alignOf.active ? { getPixelOffset: alignOf } : {}),
|
|
11054
11306
|
sizeUnits: 'pixels',
|
|
@@ -11124,7 +11376,7 @@
|
|
|
11124
11376
|
const alignRight = /right/.test(String(this.style.align || ''));
|
|
11125
11377
|
// with MULTIQUAD only the first item of a position gets its box/title
|
|
11126
11378
|
const boxLayout = boxShown ? combined.filter(d => !multi || multi.index.get(d) === 0).map(d => {
|
|
11127
|
-
const r =
|
|
11379
|
+
const r = vRadius(sizeValueOf(d));
|
|
11128
11380
|
const ext = multi ? multi.extent.get(d) : [0, 0, 0, 0];
|
|
11129
11381
|
const chart = [ext[0] - r, ext[1] - r, ext[2] + r, ext[3] + r];
|
|
11130
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]); };
|
|
@@ -11249,17 +11501,27 @@
|
|
|
11249
11501
|
}));
|
|
11250
11502
|
}
|
|
11251
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);
|
|
11252
11514
|
layers.push(new IconLayer({
|
|
11253
11515
|
id: `ix-bubbles-${this.name}-g${this._iconGeneration}`,
|
|
11254
11516
|
data: combined, pickable: true,
|
|
11255
11517
|
getPosition: d => d.geometry.coordinates,
|
|
11256
|
-
getIcon: d => d
|
|
11518
|
+
getIcon: d => !singleSymbol(d)
|
|
11257
11519
|
? 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,
|
|
11259
|
-
getSize: d =>
|
|
11520
|
+
: this._buildSingleIcon((this.categoryColorsRgb || [])[d.properties.cat], fillOpacity, shapeOf(d.properties), singleBorderColorRgb, singleBorderWidthPx),
|
|
11521
|
+
getSize: d => vRadius(sizeValueOf(d)) * 2,
|
|
11260
11522
|
// group icons bake a fixed 0.9 alpha; an explicit fillopacity scales it
|
|
11261
11523
|
// so a near-transparent theme doesn't show opaque 1-px-cell clusters
|
|
11262
|
-
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))],
|
|
11263
11525
|
...(alignOf.active ? { getPixelOffset: alignOf } : {}),
|
|
11264
11526
|
sizeUnits: 'pixels',
|
|
11265
11527
|
billboard: true,
|
|
@@ -11293,8 +11555,8 @@
|
|
|
11293
11555
|
// VALUES: bold value label centered on each bubble (see
|
|
11294
11556
|
// flatValueText/valuesFontSizePx above). Labels are their own
|
|
11295
11557
|
// layers, added last in the array so they draw on top of every icon
|
|
11296
|
-
// layer
|
|
11297
|
-
//
|
|
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
|
|
11298
11560
|
// computed font size would be sub-pixel are dropped rather than
|
|
11299
11561
|
// rendered at getSize: 0, matching the real engine's own "too
|
|
11300
11562
|
// small to bother" gate.
|
|
@@ -11303,15 +11565,19 @@
|
|
|
11303
11565
|
const textOpts = this._valueTextOpts();
|
|
11304
11566
|
const pointValueOf = valueTextOf || this._chartValueTextFn();
|
|
11305
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]));
|
|
11306
11571
|
const pointLabels = individual.reduce((out, d) => {
|
|
11307
|
-
|
|
11572
|
+
if (!orderOf.has(d)) return out;
|
|
11573
|
+
const radius = vRadius(d.properties.value);
|
|
11308
11574
|
// flat's VALUES label prints the value (nValuesA), not the size;
|
|
11309
11575
|
// its font by flat's chart branch (_chartValueTextFn)
|
|
11310
11576
|
const { text, fontSize, bold, baseline } = pointValueOf(d, radius);
|
|
11311
11577
|
if (fontSize > VALUES_MIN_FONT_PX) {
|
|
11312
11578
|
const o = alignOf(d);
|
|
11313
11579
|
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 });
|
|
11580
|
+
pixelOffset: baseline ? [o[0], o[1] + baseline - 0.35 * fontSize] : o, order: orderOf.get(d) });
|
|
11315
11581
|
}
|
|
11316
11582
|
return out;
|
|
11317
11583
|
}, []);
|
|
@@ -11327,11 +11593,25 @@
|
|
|
11327
11593
|
// would misrepresent every category's own count as one summed
|
|
11328
11594
|
// number nobody's individual bubble actually shows.
|
|
11329
11595
|
const groupLabels = groups.reduce((out, d) => {
|
|
11596
|
+
if (!orderOf.has(d)) return out;
|
|
11330
11597
|
const counts = d.properties.counts;
|
|
11331
|
-
const outerRadiusPx =
|
|
11598
|
+
const outerRadiusPx = vRadius(d.properties.total);
|
|
11332
11599
|
const iconSizePx = outerRadiusPx * 2; // matches the cluster IconLayer's own getSize (*2) below
|
|
11333
11600
|
const pxPerCanvasUnit = iconSizePx / BUBBLE_ICON_SIZE;
|
|
11334
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
|
+
}
|
|
11335
11615
|
const { present, radii, offsets, fitScale } = (iso && isolatedBubblePackLayout(counts, BUBBLE_ICON_SIZE, iso)) || computeBubblePackLayout(counts, BUBBLE_ICON_SIZE);
|
|
11336
11616
|
|
|
11337
11617
|
present.forEach((p, i) => {
|
|
@@ -11343,13 +11623,24 @@
|
|
|
11343
11623
|
out.push({
|
|
11344
11624
|
geometry: d.geometry, text, fontSize,
|
|
11345
11625
|
color: resolveTextColor(this.style, contrastTextColor((this.categoryColorsRgb || [])[p.i])),
|
|
11346
|
-
pixelOffset: [offsets[i].x * fitScale * pxPerCanvasUnit + alignOf(d)[0], offsets[i].y * fitScale * pxPerCanvasUnit + alignOf(d)[1]]
|
|
11626
|
+
pixelOffset: [offsets[i].x * fitScale * pxPerCanvasUnit + alignOf(d)[0], offsets[i].y * fitScale * pxPerCanvasUnit + alignOf(d)[1]],
|
|
11627
|
+
order: orderOf.get(d)
|
|
11347
11628
|
});
|
|
11348
11629
|
}
|
|
11349
11630
|
});
|
|
11350
11631
|
return out;
|
|
11351
11632
|
}, []);
|
|
11352
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
|
+
|
|
11353
11644
|
const textLayerCommonProps = {
|
|
11354
11645
|
pickable: false,
|
|
11355
11646
|
getPosition: d => d.geometry.coordinates,
|
|
@@ -11359,13 +11650,18 @@
|
|
|
11359
11650
|
sizeUnits: 'pixels',
|
|
11360
11651
|
fontFamily: 'arial',
|
|
11361
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',
|
|
11362
11657
|
getTextAnchor: 'middle',
|
|
11363
11658
|
getAlignmentBaseline: 'center'
|
|
11364
11659
|
};
|
|
11365
11660
|
// flat's SYMBOL branch writes its values in normal weight
|
|
11366
|
-
if (
|
|
11367
|
-
fontWeight:
|
|
11368
|
-
if (
|
|
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' }));
|
|
11369
11665
|
}
|
|
11370
11666
|
|
|
11371
11667
|
// LABEL: flat's chart label (maptheme.js 24533-24590) — a small text
|
|
@@ -11726,6 +12022,67 @@
|
|
|
11726
12022
|
// the item color (stroke font/7, opacity 0.5).
|
|
11727
12023
|
// Both moved by the chart position (flatChartAlignOffset). The text is
|
|
11728
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
|
+
|
|
11729
12086
|
_buildLabelChartLayers(combined, zoom, liveZoom, sizeValueOf) {
|
|
11730
12087
|
const textOnly = this.flags.has('TEXTONLY');
|
|
11731
12088
|
const showValues = this.flags.has('VALUES') && !valuesHiddenByScale(this.style, zoom);
|
|
@@ -11882,6 +12239,11 @@
|
|
|
11882
12239
|
// gray stroke (fill-opacity:0, stroke #888888)
|
|
11883
12240
|
function drawSymbolPath(ctx, shape, cx, cy, r) {
|
|
11884
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;
|
|
11885
12247
|
case 'square': {
|
|
11886
12248
|
const s = r * Math.SQRT2;
|
|
11887
12249
|
ctx.rect(cx - s / 2, cy - s / 2, s, s);
|
|
@@ -12084,7 +12446,7 @@
|
|
|
12084
12446
|
// map named by szMap (its Map() options name) or, without a name, the
|
|
12085
12447
|
// last built map; called while the map is still building, they run once
|
|
12086
12448
|
// it is ready.
|
|
12087
|
-
const MAP_HANDLE_METHODS = ['replace', 'add', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
12449
|
+
const MAP_HANDLE_METHODS = ['replace', 'add', 'newTheme', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
12088
12450
|
'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'setProjection', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible', 'setThemeTimeFrame', 'attribution'];
|
|
12089
12451
|
const _mapHandles = Object.create(null);
|
|
12090
12452
|
function makeMapHandle(szMap) {
|
|
@@ -12749,6 +13111,8 @@
|
|
|
12749
13111
|
htmlgui_getAttributionString: () => (_lastMapApi && _lastMapApi.getAttribution ? _lastMapApi.getAttribution() : ''),
|
|
12750
13112
|
changeThemeStyle: compatChangeThemeStyle,
|
|
12751
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),
|
|
12752
13116
|
// flat's time slider: show the records of a theme (null = all themes) whose timefield lies in [min, max)
|
|
12753
13117
|
setThemeTimeFrame: (szId, nMin, nMax) => _mapHandle.setThemeTimeFrame(szId, nMin, nMax),
|
|
12754
13118
|
setBasemapOpacity: compatSetBasemapOpacity,
|
|
@@ -12775,7 +13139,7 @@
|
|
|
12775
13139
|
// test-only: lets test/unit/*.test.mjs call pure internals directly (the
|
|
12776
13140
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
12777
13141
|
global.__ixmapsGlInternals = {
|
|
12778
|
-
IXMAPS_GL_VERSION, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout, ringsLayout, multiQuadOffsets, pixelOffsetLngLat,
|
|
13142
|
+
IXMAPS_GL_VERSION, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, chartTextTransmittance, glowProfileAlpha, insideSymbolShape, sequenceLayout, ringsLayout, multiQuadOffsets, pixelOffsetLngLat,
|
|
12779
13143
|
normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
12780
13144
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
12781
13145
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|
|
@@ -12786,7 +13150,7 @@
|
|
|
12786
13150
|
fetchLayerData, parseCsvText, dataTableRows, geometryRowsToFeatureCollection, filterFlatValues, applyField100, field100Binding, rangeClassLegendTotals, resolveAggregateValue, classValueSeparate, cellAggregatedValues, oneHot, groupCoLocated, aggregateOnGrid, GridAggregateIndex, flatRangeParts, aggregateField100, valueRadius, itemPlotGeometry, itemAnchor, objectZoomFactor, resolveZoomReference, defaultNormalSizeValue,
|
|
12787
13151
|
applyWhereFilter, joinChartPositions, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
12788
13152
|
drawSymbolPath, normalizeSymbolShape,
|
|
12789
|
-
flatFormatValue, themeStyleArgs, explicitValueDecimals, legendDecimals,
|
|
13153
|
+
flatFormatValue, themeStyleArgs, flatChangedStyleValue, explicitValueDecimals, legendDecimals,
|
|
12790
13154
|
dataCacheKey, dataCacheDisabled, cachedLayerData, featuresBounds, wantsZoomToExtent, legendIsOn,
|
|
12791
13155
|
};
|
|
12792
13156
|
global.__setFilter = __setFilter;
|