ixmaps-gl 0.2.17 → 0.2.19
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 +269 -59
- 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.19/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.19/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.19/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.19';
|
|
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']) {
|
|
@@ -8743,7 +8872,11 @@
|
|
|
8743
8872
|
}
|
|
8744
8873
|
|
|
8745
8874
|
_usesAggregationIndex() {
|
|
8746
|
-
|
|
8875
|
+
// GRIDSIZE bins its own grid (_ensureGridIndex) — PLOT curves and the
|
|
8876
|
+
// plain mesh; a classed GRIDSIZE symbol chart is aggregated like any
|
|
8877
|
+
// other (see _isPlainGridMesh)
|
|
8878
|
+
return (this.flags.has('AGGREGATE') || isSymbolChart(this.flags))
|
|
8879
|
+
&& (!this.flags.has('GRIDSIZE') || (!this.flags.has('PLOT') && !this._isPlainGridMesh()));
|
|
8747
8880
|
}
|
|
8748
8881
|
|
|
8749
8882
|
_rebuildActiveFeatures() {
|
|
@@ -9487,16 +9620,19 @@
|
|
|
9487
9620
|
// GRIDSIZE (spatial grid binning) is checked before the generic
|
|
9488
9621
|
// CHART|SYMBOL bubble pipeline, same "more specific base type wins"
|
|
9489
9622
|
// 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).
|
|
9623
|
+
// (_buildPlotLayers); GRIDSIZE without PLOT is flat's symbol chart
|
|
9624
|
+
// with `nRadius = nGridSize/2` (maptheme.js 21843) — every chart the
|
|
9625
|
+
// size of its cell, colored by its class (QUANTILE of the MEAN, the
|
|
9626
|
+
// fuel-price page). Only a plain one-color grid (the "grid"
|
|
9627
|
+
// companion layer) takes the uniform mesh shortcut.
|
|
9496
9628
|
if (this.flags.has('GRIDSIZE') && this.flags.has('PLOT')) return this._buildPlotLayers(zoom, bbox);
|
|
9497
9629
|
// PLOT without GRIDSIZE: one line chart per item over its value fields
|
|
9498
9630
|
if (this.flags.has('PLOT') && this._isItemPlot()) return this._buildItemPlotLayers(zoom);
|
|
9499
|
-
|
|
9631
|
+
// a plain grid (one color, no values or glow — the Germany page's
|
|
9632
|
+
// companion "grid" layer) as one square per cell; a classed GRIDSIZE
|
|
9633
|
+
// chart (the fuel-price page) is a symbol chart sized to the grid,
|
|
9634
|
+
// below in _buildChartLayers (_gridChartRadiusPx)
|
|
9635
|
+
if (this.flags.has('GRIDSIZE') && this._isPlainGridMesh()) return this._buildGridMeshLayers(zoom, bbox);
|
|
9500
9636
|
// CHOROPLETH is checked before the generic FEATURE dispatch — a
|
|
9501
9637
|
// CHOROPLETH layer's own type string doesn't carry FEATURE/FEATURES
|
|
9502
9638
|
// (only its geometry-donor base layer does, see
|
|
@@ -9852,6 +9988,27 @@
|
|
|
9852
9988
|
// style.values/xaxis (explicit, see _prepare) if set, else whatever
|
|
9853
9989
|
// CATEGORICAL auto-discovered.
|
|
9854
9990
|
// a per-item PLOT: no GRIDSIZE grid, several value fields per item
|
|
9991
|
+
// GRIDSIZE without PLOT drawn as one uniform square per cell: only a
|
|
9992
|
+
// plain grid — a single color, no VALUES, no GLOW
|
|
9993
|
+
_isPlainGridMesh() {
|
|
9994
|
+
return !this.flags.has('PLOT') && (this.categoryColorsRgb || []).length <= 1
|
|
9995
|
+
&& !this.flags.has('VALUES') && !this.flags.has('GLOW');
|
|
9996
|
+
}
|
|
9997
|
+
// GRIDSIZE (no PLOT) symbol chart radius, px: flat's nGridSize / 2
|
|
9998
|
+
// (maptheme.js 21843), nGridSize the aggregation cell pitch / nAutoSize
|
|
9999
|
+
// (16944-16952: RECT or no AGGREGATE 1.0, with GAP 1.15; else 0.75,
|
|
10000
|
+
// with GAP 0.8) · style.scale. Measured on the fuel-price page: 5 px
|
|
10001
|
+
// cells, RECT|GAP → nGridSize 4.35 px, circle r 2.17 px. null when not
|
|
10002
|
+
// a GRIDSIZE chart or no grid at this scale.
|
|
10003
|
+
_gridChartRadiusPx(zoom, liveZoom) {
|
|
10004
|
+
if (!this.flags.has('GRIDSIZE') || this.flags.has('PLOT') || !this._clusterRadiusPx) return null;
|
|
10005
|
+
const fromZoom = this._clusterUsesFixedZoom ? GRIDWIDTH_METERS_REFERENCE_ZOOM : zoom;
|
|
10006
|
+
const cellPx = this._clusterRadiusPx * Math.pow(2, liveZoom - fromZoom);
|
|
10007
|
+
const rect = flatFlag(this.flags, 'RECT') || !this.flags.has('AGGREGATE');
|
|
10008
|
+
const gap = flatFlag(this.flags, 'GAP');
|
|
10009
|
+
const autoSize = rect ? (gap ? 1.15 : 1.0) : (gap ? 0.8 : 0.75);
|
|
10010
|
+
return cellPx / autoSize * (styleNum(this.style.scale) || 1) / 2;
|
|
10011
|
+
}
|
|
9855
10012
|
_isItemPlot() {
|
|
9856
10013
|
return this.flags.has('PLOT') && !this.flags.has('GRIDSIZE') && String(this.binding.value || '').includes('|');
|
|
9857
10014
|
}
|
|
@@ -10715,6 +10872,20 @@
|
|
|
10715
10872
|
...(f.properties.titleRaw ? { titleRaw: f.properties.titleRaw } : {}) }
|
|
10716
10873
|
};
|
|
10717
10874
|
});
|
|
10875
|
+
// MEAN (no CATEGORICAL): a cell's value is the mean of its records,
|
|
10876
|
+
// not their sum (flat's MEAN aggregation) — what its label prints,
|
|
10877
|
+
// its size and its class follow. The fuel-price page: 119 stations,
|
|
10878
|
+
// sum 270.0 → 2.27 €
|
|
10879
|
+
if (this.flags.has('MEAN') && !this.flags.has('CATEGORICAL')) {
|
|
10880
|
+
groups.forEach(g => {
|
|
10881
|
+
const p = g.properties;
|
|
10882
|
+
const n = groupRecordCount(p);
|
|
10883
|
+
if (!(n > 0)) return;
|
|
10884
|
+
if (p.recordCounts) p.counts = p.counts.map((c, i) => (p.recordCounts[i] ? c / p.recordCounts[i] : c));
|
|
10885
|
+
p.total = p.total / n;
|
|
10886
|
+
if (p.classTotal !== undefined) p.classTotal = p.classTotal / n;
|
|
10887
|
+
});
|
|
10888
|
+
}
|
|
10718
10889
|
return { individual, groups };
|
|
10719
10890
|
}
|
|
10720
10891
|
|
|
@@ -10842,6 +11013,10 @@
|
|
|
10842
11013
|
// _computeAggregatedItems below — a plain (non-AGGREGATE)
|
|
10843
11014
|
// BUBBLE/CHART theme never reads them.
|
|
10844
11015
|
if (this.flags.has('AGGREGATE')) this._ensureClusterIndices(zoom);
|
|
11016
|
+
// GRIDSIZE: every chart the size of its grid cell, whatever its value
|
|
11017
|
+
// (_gridChartRadiusPx); otherwise valueRadius
|
|
11018
|
+
const gridR = this._gridChartRadiusPx(zoom, liveZoom);
|
|
11019
|
+
const vRadius = v => (gridR != null ? gridR : valueRadius(v, liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue));
|
|
10845
11020
|
// AGGREGATE sums without a normalsizevalue: flat's default is the
|
|
10846
11021
|
// largest aggregated value (defaultNormalSizeValue leaves it unset, the
|
|
10847
11022
|
// sizes depend on the aggregation)
|
|
@@ -10999,7 +11174,7 @@
|
|
|
10999
11174
|
const singleBorderWidthPx = styleNum(this.style.linewidth) || 0;
|
|
11000
11175
|
|
|
11001
11176
|
const itemRgb = d => this.categoryColorsRgb[d.properties.counts ? dominant(d.properties.counts) : d.properties.cat] || [128, 128, 128];
|
|
11002
|
-
const radiusOf = d =>
|
|
11177
|
+
const radiusOf = d => vRadius(sizeValueOf(d));
|
|
11003
11178
|
// align / offsetx / offsety: the whole chart moves (flatChartAlignOffset)
|
|
11004
11179
|
// MULTIQUAD / MULTISQUARE: the items at one position side by side
|
|
11005
11180
|
// (multiQuadOffsets), added to the chart position
|
|
@@ -11048,7 +11223,7 @@
|
|
|
11048
11223
|
data: combined, pickable: false,
|
|
11049
11224
|
getPosition: d => d.geometry.coordinates,
|
|
11050
11225
|
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 =>
|
|
11226
|
+
getSize: d => vRadius(sizeValueOf(d)) * (d.properties.counts ? 9 : 11),
|
|
11052
11227
|
getColor: d => [255, 255, 255, this._iconAlpha(d)],
|
|
11053
11228
|
...(alignOf.active ? { getPixelOffset: alignOf } : {}),
|
|
11054
11229
|
sizeUnits: 'pixels',
|
|
@@ -11124,7 +11299,7 @@
|
|
|
11124
11299
|
const alignRight = /right/.test(String(this.style.align || ''));
|
|
11125
11300
|
// with MULTIQUAD only the first item of a position gets its box/title
|
|
11126
11301
|
const boxLayout = boxShown ? combined.filter(d => !multi || multi.index.get(d) === 0).map(d => {
|
|
11127
|
-
const r =
|
|
11302
|
+
const r = vRadius(sizeValueOf(d));
|
|
11128
11303
|
const ext = multi ? multi.extent.get(d) : [0, 0, 0, 0];
|
|
11129
11304
|
const chart = [ext[0] - r, ext[1] - r, ext[2] + r, ext[3] + r];
|
|
11130
11305
|
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 +11424,27 @@
|
|
|
11249
11424
|
}));
|
|
11250
11425
|
}
|
|
11251
11426
|
|
|
11427
|
+
// flat's per-part SYMBOL branch (everything but CATEGORICAL without
|
|
11428
|
+
// AGGREGATE/GROUP) draws a square of side 2r, the other one 1.6r
|
|
11429
|
+
// (drawSymbolPath's own square)
|
|
11430
|
+
const perPartBranch = flatChartBranch(this.flags) === 'symbol'
|
|
11431
|
+
&& !(this.flags.has('CATEGORICAL') && !this.flags.has('AGGREGATE') && !flatFlag(this.flags, 'GROUP'));
|
|
11432
|
+
const shapeOf = props => { const sh = this._resolveSymbolShape(props); return sh === 'square' && perPartBranch ? 'squarefull' : sh; };
|
|
11433
|
+
// a range-classed cell is ONE class: one symbol in its class color,
|
|
11434
|
+
// fillopacity and linecolor, as a single item (flat draws one
|
|
11435
|
+
// symbol) — the packed multi-category icon is for CATEGORICAL cells
|
|
11436
|
+
const singleSymbol = d => !d.properties.counts || (this._rangeClassed && d.properties.cat != null);
|
|
11252
11437
|
layers.push(new IconLayer({
|
|
11253
11438
|
id: `ix-bubbles-${this.name}-g${this._iconGeneration}`,
|
|
11254
11439
|
data: combined, pickable: true,
|
|
11255
11440
|
getPosition: d => d.geometry.coordinates,
|
|
11256
|
-
getIcon: d => d
|
|
11441
|
+
getIcon: d => !singleSymbol(d)
|
|
11257
11442
|
? 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 =>
|
|
11443
|
+
: this._buildSingleIcon((this.categoryColorsRgb || [])[d.properties.cat], fillOpacity, shapeOf(d.properties), singleBorderColorRgb, singleBorderWidthPx),
|
|
11444
|
+
getSize: d => vRadius(sizeValueOf(d)) * 2,
|
|
11260
11445
|
// group icons bake a fixed 0.9 alpha; an explicit fillopacity scales it
|
|
11261
11446
|
// 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
|
|
11447
|
+
getColor: d => [255, 255, 255, Math.round(this._iconAlpha(d) * (!singleSymbol(d) && this.style.fillopacity != null ? fillOpacity : 1))],
|
|
11263
11448
|
...(alignOf.active ? { getPixelOffset: alignOf } : {}),
|
|
11264
11449
|
sizeUnits: 'pixels',
|
|
11265
11450
|
billboard: true,
|
|
@@ -11304,7 +11489,7 @@
|
|
|
11304
11489
|
const pointValueOf = valueTextOf || this._chartValueTextFn();
|
|
11305
11490
|
|
|
11306
11491
|
const pointLabels = individual.reduce((out, d) => {
|
|
11307
|
-
const radius =
|
|
11492
|
+
const radius = vRadius(d.properties.value);
|
|
11308
11493
|
// flat's VALUES label prints the value (nValuesA), not the size;
|
|
11309
11494
|
// its font by flat's chart branch (_chartValueTextFn)
|
|
11310
11495
|
const { text, fontSize, bold, baseline } = pointValueOf(d, radius);
|
|
@@ -11328,10 +11513,23 @@
|
|
|
11328
11513
|
// number nobody's individual bubble actually shows.
|
|
11329
11514
|
const groupLabels = groups.reduce((out, d) => {
|
|
11330
11515
|
const counts = d.properties.counts;
|
|
11331
|
-
const outerRadiusPx =
|
|
11516
|
+
const outerRadiusPx = vRadius(d.properties.total);
|
|
11332
11517
|
const iconSizePx = outerRadiusPx * 2; // matches the cluster IconLayer's own getSize (*2) below
|
|
11333
11518
|
const pxPerCanvasUnit = iconSizePx / BUBBLE_ICON_SIZE;
|
|
11334
11519
|
const iso = this._groupIsolation(d);
|
|
11520
|
+
// a range-classed cell is one symbol (see the bubbles layer): its
|
|
11521
|
+
// value text as a single chart's (_chartValueTextFn)
|
|
11522
|
+
if (this._rangeClassed && d.properties.cat != null) {
|
|
11523
|
+
const value = d.properties.classTotal !== undefined ? d.properties.classTotal : d.properties.total;
|
|
11524
|
+
const v = pointValueOf({ properties: { value, cat: d.properties.cat } }, outerRadiusPx);
|
|
11525
|
+
if (v.fontSize > VALUES_MIN_FONT_PX) {
|
|
11526
|
+
const o = alignOf(d);
|
|
11527
|
+
out.push({ geometry: d.geometry, text: v.text, fontSize: v.fontSize, bold: v.bold,
|
|
11528
|
+
color: resolveTextColor(this.style, contrastTextColor((this.categoryColorsRgb || [])[d.properties.cat])),
|
|
11529
|
+
pixelOffset: v.baseline ? [o[0], o[1] + v.baseline - 0.35 * v.fontSize] : o });
|
|
11530
|
+
}
|
|
11531
|
+
return out;
|
|
11532
|
+
}
|
|
11335
11533
|
const { present, radii, offsets, fitScale } = (iso && isolatedBubblePackLayout(counts, BUBBLE_ICON_SIZE, iso)) || computeBubblePackLayout(counts, BUBBLE_ICON_SIZE);
|
|
11336
11534
|
|
|
11337
11535
|
present.forEach((p, i) => {
|
|
@@ -11359,13 +11557,18 @@
|
|
|
11359
11557
|
sizeUnits: 'pixels',
|
|
11360
11558
|
fontFamily: 'arial',
|
|
11361
11559
|
fontWeight: 'bold',
|
|
11560
|
+
// the glyphs of the actual texts, not deck's ASCII default — a
|
|
11561
|
+
// unit like " €" (the fuel-price page) is outside it ("Missing
|
|
11562
|
+
// character: €" and no glyph)
|
|
11563
|
+
characterSet: 'auto',
|
|
11362
11564
|
getTextAnchor: 'middle',
|
|
11363
11565
|
getAlignmentBaseline: 'center'
|
|
11364
11566
|
};
|
|
11365
11567
|
// flat's SYMBOL branch writes its values in normal weight
|
|
11366
11568
|
if (pointLabels.length) layers.push(new TextLayer({ id: `ix-points-values-${this.name}`, data: pointLabels, getPixelOffset: d => d.pixelOffset, ...textLayerCommonProps,
|
|
11367
11569
|
fontWeight: pointLabels[0].bold === false ? 'normal' : 'bold' }));
|
|
11368
|
-
if (groupLabels.length) layers.push(new TextLayer({ id: `ix-cluster-values-${this.name}`, data: groupLabels, getPixelOffset: d => d.pixelOffset, ...textLayerCommonProps
|
|
11570
|
+
if (groupLabels.length) layers.push(new TextLayer({ id: `ix-cluster-values-${this.name}`, data: groupLabels, getPixelOffset: d => d.pixelOffset, ...textLayerCommonProps,
|
|
11571
|
+
fontWeight: groupLabels[0].bold === false ? 'normal' : 'bold' }));
|
|
11369
11572
|
}
|
|
11370
11573
|
|
|
11371
11574
|
// LABEL: flat's chart label (maptheme.js 24533-24590) — a small text
|
|
@@ -11882,6 +12085,11 @@
|
|
|
11882
12085
|
// gray stroke (fill-opacity:0, stroke #888888)
|
|
11883
12086
|
function drawSymbolPath(ctx, shape, cx, cy, r) {
|
|
11884
12087
|
switch (shape) {
|
|
12088
|
+
// flat's per-part SYMBOL square (maptheme.js 22037): side 2r — with
|
|
12089
|
+
// GRIDSIZE the whole cell (the fuel-price grid, contiguous squares)
|
|
12090
|
+
case 'squarefull':
|
|
12091
|
+
ctx.rect(cx - r, cy - r, 2 * r, 2 * r);
|
|
12092
|
+
break;
|
|
11885
12093
|
case 'square': {
|
|
11886
12094
|
const s = r * Math.SQRT2;
|
|
11887
12095
|
ctx.rect(cx - s / 2, cy - s / 2, s, s);
|
|
@@ -12084,7 +12292,7 @@
|
|
|
12084
12292
|
// map named by szMap (its Map() options name) or, without a name, the
|
|
12085
12293
|
// last built map; called while the map is still building, they run once
|
|
12086
12294
|
// it is ready.
|
|
12087
|
-
const MAP_HANDLE_METHODS = ['replace', 'add', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
12295
|
+
const MAP_HANDLE_METHODS = ['replace', 'add', 'newTheme', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
12088
12296
|
'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'setProjection', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible', 'setThemeTimeFrame', 'attribution'];
|
|
12089
12297
|
const _mapHandles = Object.create(null);
|
|
12090
12298
|
function makeMapHandle(szMap) {
|
|
@@ -12749,6 +12957,8 @@
|
|
|
12749
12957
|
htmlgui_getAttributionString: () => (_lastMapApi && _lastMapApi.getAttribution ? _lastMapApi.getAttribution() : ''),
|
|
12750
12958
|
changeThemeStyle: compatChangeThemeStyle,
|
|
12751
12959
|
removeTheme: compatRemoveTheme,
|
|
12960
|
+
// flat's ixmaps.newTheme(title, theme, flag) — on the last built map
|
|
12961
|
+
newTheme: (title, theme, flag) => _mapHandle.newTheme(title, theme, flag),
|
|
12752
12962
|
// flat's time slider: show the records of a theme (null = all themes) whose timefield lies in [min, max)
|
|
12753
12963
|
setThemeTimeFrame: (szId, nMin, nMax) => _mapHandle.setThemeTimeFrame(szId, nMin, nMax),
|
|
12754
12964
|
setBasemapOpacity: compatSetBasemapOpacity,
|
|
@@ -12786,7 +12996,7 @@
|
|
|
12786
12996
|
fetchLayerData, parseCsvText, dataTableRows, geometryRowsToFeatureCollection, filterFlatValues, applyField100, field100Binding, rangeClassLegendTotals, resolveAggregateValue, classValueSeparate, cellAggregatedValues, oneHot, groupCoLocated, aggregateOnGrid, GridAggregateIndex, flatRangeParts, aggregateField100, valueRadius, itemPlotGeometry, itemAnchor, objectZoomFactor, resolveZoomReference, defaultNormalSizeValue,
|
|
12787
12997
|
applyWhereFilter, joinChartPositions, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
12788
12998
|
drawSymbolPath, normalizeSymbolShape,
|
|
12789
|
-
flatFormatValue, themeStyleArgs, explicitValueDecimals, legendDecimals,
|
|
12999
|
+
flatFormatValue, themeStyleArgs, flatChangedStyleValue, explicitValueDecimals, legendDecimals,
|
|
12790
13000
|
dataCacheKey, dataCacheDisabled, cachedLayerData, featuresBounds, wantsZoomToExtent, legendIsOn,
|
|
12791
13001
|
};
|
|
12792
13002
|
global.__setFilter = __setFilter;
|