ixmaps-gl 0.2.8 → 0.2.9
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 +9 -5
- package/ixmaps-gl.js +613 -55
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,8 @@ page. It currently implements:
|
|
|
36
36
|
(equal-interval, QUANTILE, NATURAL/Jenks breaks) or multi-field DOMINANT
|
|
37
37
|
(per-polygon argmax, plain/`PERCENTOFMEAN`/`DEVIATION`; with a `value100` the field means are
|
|
38
38
|
flat's pooled Σ field ÷ Σ value100) or COMPOSECOLOR (additive or
|
|
39
|
-
`SUBTRACTIVE` color blend)
|
|
39
|
+
`SUBTRACTIVE` color blend) or, with `CATEGORICAL|AGGREGATE` on one field, the dominant category of
|
|
40
|
+
each polygon's records (sum of the size field per category); `HEADTAIL` breaks, `DENSITY` (value per km²), flat's
|
|
40
41
|
`ZEROISNOTVALUE`/`UNDEFINEDISNOTVALUE`; plus DOPACITY/DOPACITYMIN/DOPACITYMAX/DOPACITYMINMAX for
|
|
41
42
|
value- or density-driven fill opacity
|
|
42
43
|
- **CHART\|SYMBOL\|GLOW\|CATEGORICAL\|AGGREGATE\|COUNT\|RELOCATE\|VALUES** — the
|
|
@@ -55,6 +56,9 @@ page. It currently implements:
|
|
|
55
56
|
chart — symbol, glow, shadow, box, title and value — as flat does
|
|
56
57
|
- A range theme of at most two classes gets flat's one-row legend (the value range, or for a
|
|
57
58
|
`$item$` count the label or field name)
|
|
59
|
+
- **CHART\|SYMBOL\|SEQUENCE** on AGGREGATE CATEGORICAL data — one chart per cell (or aggregation
|
|
60
|
+
field value) with a symbol per category, laid out as flat does (`STAR`, `HORZ`, centered), with
|
|
61
|
+
flat's part sizes, glow and value labels
|
|
58
62
|
- **CHART\|USER** — a page's own chart function (`style.userdraw`: `ixmaps.<name>(SVGDocument, opt)`
|
|
59
63
|
and its `_init`, as flat calls them), drawn in flat's chart units and shown as an icon at flat's size
|
|
60
64
|
- **DOT** — the simplest base symbol (fixed-radius, unclustered points)
|
|
@@ -72,7 +76,7 @@ page. It currently implements:
|
|
|
72
76
|
`window.__setFacetFilter`) for building filterable sidebars
|
|
73
77
|
- Globe (orthographic) projection alongside flat Mercator
|
|
74
78
|
|
|
75
|
-
Not yet implemented: `CATEGORICAL` choropleths, and the `QUAD`/`BEZIER`/`VECTOR`/
|
|
79
|
+
Not yet implemented: plain (non-AGGREGATE) `CATEGORICAL` choropleths, and the `QUAD`/`BEZIER`/`VECTOR`/
|
|
76
80
|
`PIE`/`DONUT`/`WAFFLE`/`BAR` base types and SYMBOL shape variants beyond
|
|
77
81
|
circle/square/diamond/triangle. See the top-of-file comment in
|
|
78
82
|
[`ixmaps-gl.js`](./ixmaps-gl.js) for the exact, currently-accurate scope note.
|
|
@@ -203,7 +207,7 @@ of your own project files gl can render.
|
|
|
203
207
|
Load the engine from jsDelivr, pinned to a release tag (or `@main` for the latest commit):
|
|
204
208
|
|
|
205
209
|
```html
|
|
206
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
210
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.9/ixmaps-gl.js"></script>
|
|
207
211
|
```
|
|
208
212
|
|
|
209
213
|
or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmaps-gl)
|
|
@@ -211,13 +215,13 @@ or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmap
|
|
|
211
215
|
browser script, which defines the global `ixmaps`):
|
|
212
216
|
|
|
213
217
|
```html
|
|
214
|
-
<script src="https://unpkg.com/ixmaps-gl@0.2.
|
|
218
|
+
<script src="https://unpkg.com/ixmaps-gl@0.2.9/ixmaps-gl.js"></script>
|
|
215
219
|
```
|
|
216
220
|
|
|
217
221
|
or use a local copy (`<script src="ixmaps-gl.js"></script>`). A minimal page:
|
|
218
222
|
|
|
219
223
|
```html
|
|
220
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
224
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.9/ixmaps-gl.js"></script>
|
|
221
225
|
<div id="map_div" style="position:absolute;inset:0;"></div>
|
|
222
226
|
|
|
223
227
|
<script>
|
package/ixmaps-gl.js
CHANGED
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
// from the same bound value by default, or from .binding({alpha,
|
|
21
21
|
// alpha100}) when set (alpha100:"$density$" divides by the polygon's
|
|
22
22
|
// own geodesic area — see _prepareAlphaField/_resolveDopacityAlpha).
|
|
23
|
-
// CATEGORICAL
|
|
24
|
-
//
|
|
23
|
+
// AGGREGATE CATEGORICAL on one field paints each polygon in its records'
|
|
24
|
+
// dominant category; a plain CATEGORICAL choropleth is not implemented,
|
|
25
|
+
// see _buildChoroplethLayers), and the CHART|SYMBOL|
|
|
25
26
|
// GLOW|CATEGORICAL|AGGREGATE|COUNT|RELOCATE|VALUES pipeline (categorical
|
|
26
27
|
// clustering + sizing + glow + multi-bubble grouping + on-bubble value
|
|
27
28
|
// labels, generalized to however many distinct category values the DATA
|
|
@@ -81,7 +82,7 @@
|
|
|
81
82
|
// ---------------------------------------------------------------
|
|
82
83
|
// this engine's release (= package.json "version", checked by
|
|
83
84
|
// test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
|
|
84
|
-
const IXMAPS_GL_VERSION = '0.2.
|
|
85
|
+
const IXMAPS_GL_VERSION = '0.2.9';
|
|
85
86
|
|
|
86
87
|
const LIB_URLS = {
|
|
87
88
|
// Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
|
|
@@ -307,7 +308,7 @@
|
|
|
307
308
|
// crafted link must not be able to load arbitrary code into the page.
|
|
308
309
|
// ---------------------------------------------------------------
|
|
309
310
|
// v0.1.6: knows this engine's aliases, lookupfield rule, meta↔style, builder methods, project loading
|
|
310
|
-
const VALIDATOR_URL_DEFAULT = 'https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-grammar@v0.1.
|
|
311
|
+
const VALIDATOR_URL_DEFAULT = 'https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-grammar@v0.1.24/dist/validate.mjs';
|
|
311
312
|
// a page may set ixmaps.validate BEFORE loading this script — the
|
|
312
313
|
// `global.ixmaps = {...}` export below would otherwise overwrite it
|
|
313
314
|
const _preloadValidate = global.ixmaps && global.ixmaps.validate;
|
|
@@ -1464,8 +1465,53 @@
|
|
|
1464
1465
|
function featureAreaKm2(f) {
|
|
1465
1466
|
return f._flatAreaKm2 !== undefined ? f._flatAreaKm2 : geodesicPolygonAreaKm2(f.geometry);
|
|
1466
1467
|
}
|
|
1468
|
+
// .type("CHOROPLETH|CATEGORICAL|AGGREGATE") on ONE value field (e.g.
|
|
1469
|
+
// DOMINANT over the categories of "missione"): flat aggregates every
|
|
1470
|
+
// record of a polygon into a part per category — the sum of the size
|
|
1471
|
+
// field (else the record count, resolveAggregateValue) — and paints the
|
|
1472
|
+
// dominant part's class (maptheme.js 13520-13565)
|
|
1473
|
+
const AGGREGATED_ROWS = Symbol('ixmaps-gl aggregated rows');
|
|
1474
|
+
function isAggregatedCategoricalChoropleth(spec) {
|
|
1475
|
+
const flags = spec.flags;
|
|
1476
|
+
return !!(flags && flags.has('CHOROPLETH') && flags.has('CATEGORICAL') && flags.has('AGGREGATE')
|
|
1477
|
+
&& spec.binding && spec.binding.value && !String(spec.binding.value).includes('|'));
|
|
1478
|
+
}
|
|
1479
|
+
// a polygon's parts (sum per category) and its dominant one: the biggest
|
|
1480
|
+
// part above 0, the first of equal ones; null without any
|
|
1481
|
+
function aggregatedCategoricalClass(rows, binding, flags, indexByLabel, nCats) {
|
|
1482
|
+
const parts = new Array(nCats).fill(0);
|
|
1483
|
+
for (const row of rows) {
|
|
1484
|
+
const i = indexByLabel.get(String(row[binding.value]));
|
|
1485
|
+
if (i == null) continue;
|
|
1486
|
+
parts[i] += resolveAggregateValue(binding, flags, row);
|
|
1487
|
+
}
|
|
1488
|
+
let index = -1;
|
|
1489
|
+
parts.forEach((v, i) => { if (v > 0 && (index < 0 || v > parts[index])) index = i; });
|
|
1490
|
+
return index < 0 ? null : { parts, index, value: parts[index] };
|
|
1491
|
+
}
|
|
1492
|
+
// flat's DOMINANT opacity (maptheme.js 13567-13628): from the dominant
|
|
1493
|
+
// part v and the biggest part of that class over all polygons, max —
|
|
1494
|
+
// DOPACITYMAX dopacityscale · (v / max)^(1/dopacitypow), DOPACITYLOGMAX
|
|
1495
|
+
// log v / log max, plain DOPACITY log(dopacityscale · v) / log max;
|
|
1496
|
+
// below 0.0001 → 0, at most 0.9
|
|
1497
|
+
function dominantDopacityAlpha(style, flags, v, max) {
|
|
1498
|
+
const scale = styleNum(style.dopacityscale) || 1;
|
|
1499
|
+
const pow = 1 / (styleNum(style.dopacitypow) || 1);
|
|
1500
|
+
let o;
|
|
1501
|
+
if (flags.has('DOPACITYLOGMAX')) o = Math.log(v) / Math.log(max);
|
|
1502
|
+
else if (flags.has('DOPACITYMAX') || flags.has('DOPACITYPOWMAX')) o = scale * Math.pow(v, pow) / Math.pow(max, pow);
|
|
1503
|
+
else o = Math.log(scale * v) / Math.log(max);
|
|
1504
|
+
if (!(o >= 0.0001)) o = 0;
|
|
1505
|
+
return Math.min(0.9, o);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1467
1508
|
function joinChoroplethFeatures(spec, table, runtimes) {
|
|
1468
|
-
|
|
1509
|
+
// a page can define several FEATURE layers of one name — e.g. the
|
|
1510
|
+
// comuni as polygons and as center points (for chart positions); flat
|
|
1511
|
+
// paints every shape of the layer with a matching id, so the polygons
|
|
1512
|
+
// are the ones that show: prefer a base with polygon geometry
|
|
1513
|
+
const bases = runtimes.filter(r => r.name === spec.name && (r.flags.has('FEATURE') || r.flags.has('FEATURES')));
|
|
1514
|
+
const geomRt = bases.find(r => (r.features || []).some(f => f.geometry && /Polygon/.test(f.geometry.type))) || bases[0];
|
|
1469
1515
|
if (!geomRt) {
|
|
1470
1516
|
throw new Error(`[ixmaps-gl] CHOROPLETH layer "${spec.name}" needs a FEATURE base layer with the same name, .layer()'d earlier on the map`);
|
|
1471
1517
|
}
|
|
@@ -1488,6 +1534,27 @@
|
|
|
1488
1534
|
if (!idField || !lookupField) {
|
|
1489
1535
|
throw new Error(`[ixmaps-gl] CHOROPLETH layer "${spec.name}" needs .binding({lookup}), and its FEATURE base needs .binding({id})`);
|
|
1490
1536
|
}
|
|
1537
|
+
// an AGGREGATE CATEGORICAL choropleth sums ALL the rows of a polygon
|
|
1538
|
+
// per category (aggregatedCategoricalClass) — every row is kept, under
|
|
1539
|
+
// AGGREGATED_ROWS (not enumerable: the first row stays the polygon's
|
|
1540
|
+
// properties for the tooltip and filters)
|
|
1541
|
+
if (isAggregatedCategoricalChoropleth(spec)) {
|
|
1542
|
+
const groups = new Map();
|
|
1543
|
+
for (const row of table.rows) {
|
|
1544
|
+
const key = String(row[lookupField]);
|
|
1545
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
1546
|
+
groups.get(key).push(row);
|
|
1547
|
+
}
|
|
1548
|
+
return {
|
|
1549
|
+
type: 'FeatureCollection',
|
|
1550
|
+
features: geomRt.features.map(f => {
|
|
1551
|
+
const rows = groups.get(String(f.properties[idField]));
|
|
1552
|
+
const properties = rows ? Object.assign({}, rows[0]) : {};
|
|
1553
|
+
if (rows) Object.defineProperty(properties, AGGREGATED_ROWS, { value: rows });
|
|
1554
|
+
return { type: 'Feature', geometry: f.geometry, properties, _flatAreaKm2: flatShapeAreaKm2(geomRt, f) };
|
|
1555
|
+
})
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1491
1558
|
const rowsByKey = new Map(table.rows.map(row => [String(row[lookupField]), row]));
|
|
1492
1559
|
// Unmatched polygons (no CSV row for that id) keep their geometry with
|
|
1493
1560
|
// empty properties — real-world data: not every comune necessarily has
|
|
@@ -2194,7 +2261,7 @@
|
|
|
2194
2261
|
// edit it by hand; `npm run unit` fails when it drifts.
|
|
2195
2262
|
// ---------------------------------------------------------------
|
|
2196
2263
|
// <grammar:binding-aliases>
|
|
2197
|
-
// generated from ixmaps-grammar 0.1.
|
|
2264
|
+
// generated from ixmaps-grammar 0.1.24 (ixmaps-flat 1.0.42, 2b978d5 2026-10-02) — 52 aliases
|
|
2198
2265
|
const FLAT_BINDING_ALIASES = {
|
|
2199
2266
|
"aggregation": "style.aggregationfield",
|
|
2200
2267
|
"aggregationfield": "style.aggregationfield",
|
|
@@ -2256,7 +2323,7 @@
|
|
|
2256
2323
|
// both — so .style({tooltip}) is a tooltip and .meta({fillopacity}) a style
|
|
2257
2324
|
// property. GENERATED by test/sync-grammar.mjs — do not edit.
|
|
2258
2325
|
// <grammar:meta-keys>
|
|
2259
|
-
// generated from ixmaps-grammar 0.1.
|
|
2326
|
+
// generated from ixmaps-grammar 0.1.24 — 5 meta keys
|
|
2260
2327
|
const FLAT_META_KEYS = ["description","name","snippet","title","tooltip"];
|
|
2261
2328
|
// </grammar:meta-keys>
|
|
2262
2329
|
|
|
@@ -2923,6 +2990,9 @@
|
|
|
2923
2990
|
// theme addressed only by its meta.name, same bug different
|
|
2924
2991
|
// resolution path.
|
|
2925
2992
|
if (rt.meta && rt.meta.name && rt.meta.name !== rt.name) _globalThemeRegistry.set(rt.meta.name, rt);
|
|
2993
|
+
// flat's theme id is style.name (maptheme szId) — several themes
|
|
2994
|
+
// can share one .layer() name (a page's features + charts + texts)
|
|
2995
|
+
if (themeIdOf(rt) !== rt.name) _globalThemeRegistry.set(themeIdOf(rt), rt);
|
|
2926
2996
|
if (named) namedLoads.push(rt);
|
|
2927
2997
|
} catch (err) {
|
|
2928
2998
|
console.error(`[ixmaps-gl] theme "${lb.name}" skipped:`, err);
|
|
@@ -3416,6 +3486,8 @@
|
|
|
3416
3486
|
},
|
|
3417
3487
|
setMapTypeId: (id) => engineApi.setMapType(id),
|
|
3418
3488
|
mapType: (id) => engineApi.setMapType(id),
|
|
3489
|
+
// flat's getMapTypeId(): the basemap type name in use
|
|
3490
|
+
getMapTypeId: () => String(builder.mapOptions.mapType || ''),
|
|
3419
3491
|
setBasemapOpacity: (delta, mode) => {
|
|
3420
3492
|
_basemapOpacity = mode === 'relative'
|
|
3421
3493
|
? Math.max(0, Math.min(1, _basemapOpacity + (parseFloat(delta) || 0)))
|
|
@@ -3857,6 +3929,7 @@
|
|
|
3857
3929
|
runtimes.push(rt);
|
|
3858
3930
|
_globalThemeRegistry.set(rt.name, rt);
|
|
3859
3931
|
if (rt.meta && rt.meta.name && rt.meta.name !== rt.name) _globalThemeRegistry.set(rt.meta.name, rt);
|
|
3932
|
+
if (themeIdOf(rt) !== rt.name) _globalThemeRegistry.set(themeIdOf(rt), rt);
|
|
3860
3933
|
addLegendPanel(rt);
|
|
3861
3934
|
notifyNewTheme(rt);
|
|
3862
3935
|
refresh();
|
|
@@ -4139,8 +4212,25 @@
|
|
|
4139
4212
|
refresh();
|
|
4140
4213
|
});
|
|
4141
4214
|
|
|
4215
|
+
// flat's page hook ixmaps.htmlgui_onDrawTheme(szId), called when a
|
|
4216
|
+
// theme has been drawn: here once per settled redraw, for every
|
|
4217
|
+
// shown theme, with its id. Not re-entered when the hook itself
|
|
4218
|
+
// changes a theme (changeThemeStyle → refresh → here again).
|
|
4219
|
+
let inDrawThemeHook = false;
|
|
4220
|
+
function callDrawThemeHooks() {
|
|
4221
|
+
const hook = pageIxmaps() && pageIxmaps().htmlgui_onDrawTheme;
|
|
4222
|
+
if (typeof hook !== 'function' || inDrawThemeHook) return;
|
|
4223
|
+
inDrawThemeHook = true;
|
|
4224
|
+
try {
|
|
4225
|
+
for (const rt of runtimes.slice()) {
|
|
4226
|
+
if (rt._hidden) continue;
|
|
4227
|
+
try { hook.call(global.ixmaps, themeIdOf(rt)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onDrawTheme:', e); }
|
|
4228
|
+
}
|
|
4229
|
+
} finally { inDrawThemeHook = false; }
|
|
4230
|
+
}
|
|
4142
4231
|
function notifyRedraw() {
|
|
4143
4232
|
redrawListeners.forEach(cb => { try { cb(); } catch (err) { console.error('[ixmaps-gl] onRedraw callback failed:', err); } });
|
|
4233
|
+
callDrawThemeHooks();
|
|
4144
4234
|
}
|
|
4145
4235
|
|
|
4146
4236
|
// For explicit, discrete API calls (setFacetFilter, setSizeField,
|
|
@@ -4514,7 +4604,10 @@
|
|
|
4514
4604
|
// themselves authored with their own leading space (e.g.
|
|
4515
4605
|
// " MW") in real pages.
|
|
4516
4606
|
const legendUnit = rt.style.legendunits || rt.style.units || '';
|
|
4517
|
-
|
|
4607
|
+
// row labels go into innerHTML: the page's own .style({label})
|
|
4608
|
+
// stays HTML, as in flat; categories read from the data are
|
|
4609
|
+
// escaped (a value could carry markup)
|
|
4610
|
+
const labels = legendRowLabels(rt);
|
|
4518
4611
|
// .type("...|TEXTLEGEND") — the FIFTH real legend-related
|
|
4519
4612
|
// type() token: title/snippet/description text only, no
|
|
4520
4613
|
// category rows at all (swatches, bars, chips — none of it),
|
|
@@ -4661,8 +4754,8 @@
|
|
|
4661
4754
|
+ '<span>' + text + '</span></div>';
|
|
4662
4755
|
rowsEl.querySelectorAll('.ix-legend-row').forEach(row => {
|
|
4663
4756
|
row.addEventListener('click', () => {
|
|
4664
|
-
if (rt._markedClasses.has(0))
|
|
4665
|
-
else
|
|
4757
|
+
if (rt._markedClasses.has(0)) unmarkRuntimeClass(rt, 0);
|
|
4758
|
+
else markRuntimeClass(rt, 0);
|
|
4666
4759
|
});
|
|
4667
4760
|
});
|
|
4668
4761
|
};
|
|
@@ -4701,8 +4794,8 @@
|
|
|
4701
4794
|
rowsEl.querySelectorAll('.ix-legend-row').forEach(cell => {
|
|
4702
4795
|
cell.addEventListener('click', () => {
|
|
4703
4796
|
const idx = parseInt(cell.dataset.idx, 10);
|
|
4704
|
-
if (rt._markedClasses.has(idx))
|
|
4705
|
-
else
|
|
4797
|
+
if (rt._markedClasses.has(idx)) unmarkRuntimeClass(rt, idx);
|
|
4798
|
+
else markRuntimeClass(rt, idx);
|
|
4706
4799
|
});
|
|
4707
4800
|
});
|
|
4708
4801
|
};
|
|
@@ -4759,15 +4852,16 @@
|
|
|
4759
4852
|
// — legend.js:817), toggle decided by the caller. Same
|
|
4760
4853
|
// split here: the toggle check lives in this click
|
|
4761
4854
|
// handler, the actual add/remove-and-redraw primitive is
|
|
4762
|
-
// the
|
|
4763
|
-
//
|
|
4764
|
-
//
|
|
4765
|
-
//
|
|
4855
|
+
// markRuntimeClass/unmarkRuntimeClass, which the globals
|
|
4856
|
+
// ixmaps.markThemeClass/unmarkThemeClass also use — so a
|
|
4857
|
+
// page's OWN custom UI drives this exact legend too. The
|
|
4858
|
+
// rows pass their runtime itself: its .layer() name may be
|
|
4859
|
+
// shared with other themes of the page.
|
|
4766
4860
|
rowsEl.querySelectorAll('.ix-legend-row').forEach(rowEl => {
|
|
4767
4861
|
rowEl.addEventListener('click', () => {
|
|
4768
4862
|
const idx = parseInt(rowEl.dataset.idx, 10);
|
|
4769
|
-
if (rt._markedClasses.has(idx))
|
|
4770
|
-
else
|
|
4863
|
+
if (rt._markedClasses.has(idx)) unmarkRuntimeClass(rt, idx);
|
|
4864
|
+
else markRuntimeClass(rt, idx);
|
|
4771
4865
|
});
|
|
4772
4866
|
});
|
|
4773
4867
|
};
|
|
@@ -5627,7 +5721,7 @@
|
|
|
5627
5721
|
}
|
|
5628
5722
|
// maptheme.js formatValue's NOBREAKS switch (26368): theme min > 999 and max < 3000
|
|
5629
5723
|
function flatNoBreaks(vMin, vMax) {
|
|
5630
|
-
return vMin > 999 && vMax < 3000;
|
|
5724
|
+
return Number.isFinite(vMin) && Number.isFinite(vMax) && vMin > 999 && vMax < 3000;
|
|
5631
5725
|
}
|
|
5632
5726
|
|
|
5633
5727
|
// flat's chart position (maptheme.js 24441-24530 and 24603, the non-PLOT,
|
|
@@ -5737,9 +5831,13 @@
|
|
|
5737
5831
|
// scale gate (maptheme.js 2609-2611, 8250-8260): the theme is hidden at
|
|
5738
5832
|
// scales above featureupper and at or below featurelower ("1:200000" or
|
|
5739
5833
|
// a bare denominator, __scanScaleValue 2021)
|
|
5834
|
+
// a scale style value ("1:200000" or a bare denominator, flat's
|
|
5835
|
+
// __scanScaleValue, maptheme.js 2021) as its denominator; 0 when unset
|
|
5836
|
+
function scaleDenom(v) {
|
|
5837
|
+
return (v == null || v === '' ? 0 : Number(String(v).includes(':') ? String(v).split(':')[1] : v)) || 0;
|
|
5838
|
+
}
|
|
5740
5839
|
function featuresHiddenByScale(style, zoom) {
|
|
5741
|
-
const
|
|
5742
|
-
const upper = denom(style.featureupper), lower = denom(style.featurelower);
|
|
5840
|
+
const upper = scaleDenom(style.featureupper), lower = scaleDenom(style.featurelower);
|
|
5743
5841
|
if (!upper && !lower) return false;
|
|
5744
5842
|
const scale = scaleDenominatorAt(zoom);
|
|
5745
5843
|
return (lower && scale <= lower) || (upper && scale > upper);
|
|
@@ -5750,9 +5848,8 @@
|
|
|
5750
5848
|
// chartupper and at or below chartlower; layerupper / layerlower stand in
|
|
5751
5849
|
// for them when unset (1774-1779)
|
|
5752
5850
|
function chartHiddenByScale(style, zoom) {
|
|
5753
|
-
const
|
|
5754
|
-
const
|
|
5755
|
-
const lower = denom(style.chartlower != null ? style.chartlower : style.layerlower);
|
|
5851
|
+
const upper = scaleDenom(style.chartupper != null ? style.chartupper : style.layerupper);
|
|
5852
|
+
const lower = scaleDenom(style.chartlower != null ? style.chartlower : style.layerlower);
|
|
5756
5853
|
if (!upper && !lower) return false;
|
|
5757
5854
|
const scale = scaleDenominatorAt(zoom);
|
|
5758
5855
|
return !!((upper && scale > upper) || (lower && scale <= lower));
|
|
@@ -5761,8 +5858,7 @@
|
|
|
5761
5858
|
// 1726-1734, 16986-16990): the GLOW halo is drawn at scales at or below
|
|
5762
5859
|
// glowupper and at or above glowlower (both inclusive)
|
|
5763
5860
|
function glowHiddenByScale(style, zoom) {
|
|
5764
|
-
const
|
|
5765
|
-
const upper = denom(style.glowupper), lower = denom(style.glowlower);
|
|
5861
|
+
const upper = scaleDenom(style.glowupper), lower = scaleDenom(style.glowlower);
|
|
5766
5862
|
if (!upper && !lower) return false;
|
|
5767
5863
|
const scale = scaleDenominatorAt(zoom);
|
|
5768
5864
|
return !!((upper && scale > upper) || (lower && scale < lower));
|
|
@@ -5784,8 +5880,7 @@
|
|
|
5784
5880
|
const on = style.shadow === true || style.shadow === 'true' || style.shadow === 1 || style.shadow === '1';
|
|
5785
5881
|
if (!on) return false;
|
|
5786
5882
|
if (count > (isNaN(styleNum(style.maxshadow)) ? 1000 : styleNum(style.maxshadow))) return false;
|
|
5787
|
-
const
|
|
5788
|
-
const upper = denom(style.shadowupper), lower = denom(style.shadowlower);
|
|
5883
|
+
const upper = scaleDenom(style.shadowupper), lower = scaleDenom(style.shadowlower);
|
|
5789
5884
|
const scale = scaleDenominatorAt(zoom);
|
|
5790
5885
|
return !((upper && scale > upper) || (lower && scale < lower));
|
|
5791
5886
|
}
|
|
@@ -5799,8 +5894,7 @@
|
|
|
5799
5894
|
// flat's BOX block (maptheme.js 18758-18760): the box — and its title —
|
|
5800
5895
|
// is removed at scales above boxupper or below boxlower
|
|
5801
5896
|
function boxHiddenByScale(style, zoom) {
|
|
5802
|
-
const
|
|
5803
|
-
const upper = denom(style.boxupper), lower = denom(style.boxlower);
|
|
5897
|
+
const upper = scaleDenom(style.boxupper), lower = scaleDenom(style.boxlower);
|
|
5804
5898
|
const scale = scaleDenominatorAt(zoom);
|
|
5805
5899
|
return !!((upper && scale > upper) || (lower && scale < lower));
|
|
5806
5900
|
}
|
|
@@ -6133,8 +6227,9 @@
|
|
|
6133
6227
|
// functionally deleted.
|
|
6134
6228
|
const DIMMED_ICON_ALPHA = 8;
|
|
6135
6229
|
|
|
6136
|
-
// deck.gl
|
|
6137
|
-
//
|
|
6230
|
+
// deck.gl's IconLayer auto-packing atlas (icon-manager.ts, read and
|
|
6231
|
+
// measured on 8.9.35; the guards below are kept for 9.x, not re-measured
|
|
6232
|
+
// there) has NO eviction and NO cap of its own: its texture is 1024px wide (fixed) but
|
|
6138
6233
|
// grows TALLER via power-of-two resize forever, for every distinct icon
|
|
6139
6234
|
// id ever seen, for the lifetime of the layer — confirmed by reading
|
|
6140
6235
|
// that file directly. There is no "atlas full" code path; it just keeps
|
|
@@ -6226,6 +6321,74 @@
|
|
|
6226
6321
|
};
|
|
6227
6322
|
}
|
|
6228
6323
|
|
|
6324
|
+
// CHART|SYMBOL|SEQUENCE (flat maptheme.js 21184-21300, 21790-21925,
|
|
6325
|
+
// 22296-22395): one symbol per category part of a chart, in category
|
|
6326
|
+
// order or sorted (SORT: DOWN biggest first, UP smallest first), parts
|
|
6327
|
+
// of value 0 left out. radiusOf(v) is the part's radius (flat: max radius
|
|
6328
|
+
// · (v / max value)^(1/sizepow)). Placement:
|
|
6329
|
+
// - STAR: the first part is the center (star radius R = its radius); each
|
|
6330
|
+
// further part (with UP every part) sits on a circle around it, at
|
|
6331
|
+
// R + r (×1.5 EXPAND, ×2 EXPANDMAX, ×rangescale), stepping the angle
|
|
6332
|
+
// by f = 90° − asin(√((R+r)² − r²) / (R+r)) before and after it, so
|
|
6333
|
+
// neighbours touch; UP: radius R·(1 + parts/5), the angle step of the
|
|
6334
|
+
// 7th part on divided by 1 + 2r/maxRadius; with EXPAND|SORT|UP,
|
|
6335
|
+
// R = (EXPANDMAX ? 2 : 0.5) · maxRadius · √(biggest / max value)
|
|
6336
|
+
// (LINEAR/SIZEP1: linear), COMPRESS halves it (COMPRESSMAX quarter)
|
|
6337
|
+
// - HORZ: side by side from the center to the right
|
|
6338
|
+
// - otherwise (or CENTER): all on the center
|
|
6339
|
+
// Returns the parts in drawing order: { i, v, r, x, y } (pixels, y down).
|
|
6340
|
+
function sequenceLayout(counts, flags, radiusOf, opts = {}) {
|
|
6341
|
+
const has = f => flatFlag(flags, f);
|
|
6342
|
+
const word = w => [...flags].some(f => new RegExp('\\b' + w + '\\b').test(f));
|
|
6343
|
+
let order = counts.map((v, i) => ({ i, v }));
|
|
6344
|
+
if (word('SORT')) {
|
|
6345
|
+
if (word('UP')) order.sort((a, b) => a.v - b.v);
|
|
6346
|
+
else if (word('DOWN')) order.sort((a, b) => b.v - a.v);
|
|
6347
|
+
}
|
|
6348
|
+
const parts = [];
|
|
6349
|
+
const star = has('STAR') && !has('CENTER'), horz = has('HORZ') && !has('CENTER');
|
|
6350
|
+
const up = word('UP');
|
|
6351
|
+
const maxR = opts.maxRadius || 0;
|
|
6352
|
+
const nStarParts = opts.nParts || counts.length;
|
|
6353
|
+
let angle = 0, starR = null, x = 0;
|
|
6354
|
+
order.forEach((p, k) => {
|
|
6355
|
+
if (!p.v) return;
|
|
6356
|
+
const r = radiusOf(p.v);
|
|
6357
|
+
if (!(r > 0)) return;
|
|
6358
|
+
let px = 0, py = 0;
|
|
6359
|
+
if (star) {
|
|
6360
|
+
if (starR === null) {
|
|
6361
|
+
starR = r;
|
|
6362
|
+
if (has('EXPAND') && word('SORT') && up) {
|
|
6363
|
+
const big = order[order.length - 1].v, mv = opts.maxValue || big || 1;
|
|
6364
|
+
starR = (has('EXPANDMAX') ? 2 : 0.5) * maxR * (has('LINEAR') || has('SIZEP1') ? big / mv : Math.sqrt(big / mv));
|
|
6365
|
+
}
|
|
6366
|
+
if (has('COMPRESS')) starR *= has('COMPRESSMAX') ? 0.25 : 0.5;
|
|
6367
|
+
}
|
|
6368
|
+
if (parts.length > 0 || up) {
|
|
6369
|
+
const c = starR + r;
|
|
6370
|
+
const b = Math.sqrt(Math.max(0, c * c - r * r));
|
|
6371
|
+
let f = 90 - Math.asin(Math.min(1, b / c)) / Math.PI * 180;
|
|
6372
|
+
let dist = starR + r;
|
|
6373
|
+
if (up) {
|
|
6374
|
+
if (k > 5) f /= (1 + 2 * (r / (maxR || r)));
|
|
6375
|
+
dist = starR * (1 + nStarParts / 5);
|
|
6376
|
+
}
|
|
6377
|
+
if (opts.rangeScale) dist *= opts.rangeScale;
|
|
6378
|
+
else dist = has('EXPANDMAX') ? dist * 2 : (has('EXPAND') ? dist * 1.5 : dist);
|
|
6379
|
+
angle += f;
|
|
6380
|
+
px = Math.cos(angle / 180 * Math.PI) * dist;
|
|
6381
|
+
py = Math.sin(angle / 180 * Math.PI) * dist;
|
|
6382
|
+
angle += f;
|
|
6383
|
+
}
|
|
6384
|
+
} else if (horz) {
|
|
6385
|
+
x += r; px = x; x += r;
|
|
6386
|
+
}
|
|
6387
|
+
parts.push({ i: p.i, v: p.v, r, x: px, y: py });
|
|
6388
|
+
});
|
|
6389
|
+
return parts;
|
|
6390
|
+
}
|
|
6391
|
+
|
|
6229
6392
|
// ---------------------------------------------------------------
|
|
6230
6393
|
// Classification — pure functions (no LayerRuntime state): class
|
|
6231
6394
|
// breaks for a numeric field, and which class a value falls in.
|
|
@@ -7180,7 +7343,7 @@
|
|
|
7180
7343
|
key = `${snapped.x}:${snapped.y}`;
|
|
7181
7344
|
}
|
|
7182
7345
|
let cell = cells.get(key);
|
|
7183
|
-
if (!cell) { cell = { snapped, sumX: 0, sumY: 0, n: 0, value: 0, classValue: 0, value100: 0, series: null, first: f }; cells.set(key, cell); }
|
|
7346
|
+
if (!cell) { cell = { key, snapped, sumX: 0, sumY: 0, n: 0, value: 0, classValue: 0, value100: 0, series: null, first: f }; cells.set(key, cell); }
|
|
7184
7347
|
cell.sumX += p.x; cell.sumY += p.y; cell.n++;
|
|
7185
7348
|
cell.value += f.properties.value;
|
|
7186
7349
|
if (f.properties.classValue !== undefined) cell.classValue += f.properties.classValue;
|
|
@@ -7202,6 +7365,9 @@
|
|
|
7202
7365
|
? { cluster: true, point_count: cell.n, value: cell.value, ...(hasClass ? { classValue: cell.classValue } : {}) }
|
|
7203
7366
|
: cell.first.properties;
|
|
7204
7367
|
if (cell.series && cell.n > 1) props = Object.assign({}, props, { series: cell.series });
|
|
7368
|
+
// a field aggregation's cell key (the field value): the categories of
|
|
7369
|
+
// one cell are merged by it (groupCoLocated), not by their positions
|
|
7370
|
+
if (field) props = Object.assign({}, props, { aggKey: cell.key });
|
|
7205
7371
|
if (post) {
|
|
7206
7372
|
props = Object.assign({}, props);
|
|
7207
7373
|
if (hasClass) props.classValue = post(cell.n > 1 ? cell.classValue : props.classValue, cell.value100, props.series);
|
|
@@ -7234,7 +7400,7 @@
|
|
|
7234
7400
|
}
|
|
7235
7401
|
}
|
|
7236
7402
|
|
|
7237
|
-
function groupCoLocated(clusterFeatures, zoom, clusterRadiusPx, nCategories, flags) {
|
|
7403
|
+
function groupCoLocated(clusterFeatures, zoom, clusterRadiusPx, nCategories, flags, atCellCenter = false) {
|
|
7238
7404
|
// Merge tolerance MUST track the same theme-driven, scale-dependent
|
|
7239
7405
|
// aggregation width the clustering itself just used (clusterRadiusPx,
|
|
7240
7406
|
// set by _ensureClusterIndices right before this runs) — not an
|
|
@@ -7256,9 +7422,11 @@
|
|
|
7256
7422
|
// recomputes ptPos as the mean of ptPosA regardless of which
|
|
7257
7423
|
// grid produced the grouping key).
|
|
7258
7424
|
const snapped = cellPx ? snapToAggregationGrid(p.x, p.y, cellPx, flags, zoom) : p;
|
|
7259
|
-
|
|
7425
|
+
// a field aggregation's categories each sit at the mean of their own
|
|
7426
|
+
// records — merged by the field value, not by (float) position
|
|
7427
|
+
const key = f.properties.aggKey != null ? f.properties.aggKey : `${snapped.x}:${snapped.y}`;
|
|
7260
7428
|
let cell = cells.get(key);
|
|
7261
|
-
if (!cell) { cell = { sumX: 0, sumY: 0, n: 0, counts: new Array(n).fill(0), recordCounts: new Array(n).fill(0), classTotal: undefined }; cells.set(key, cell); }
|
|
7429
|
+
if (!cell) { cell = { sumX: 0, sumY: 0, n: 0, cx: snapped.x, cy: snapped.y, counts: new Array(n).fill(0), recordCounts: new Array(n).fill(0), classTotal: undefined }; cells.set(key, cell); }
|
|
7262
7430
|
cell.sumX += p.x; cell.sumY += p.y; cell.n++;
|
|
7263
7431
|
cell.counts[f.properties.cat] += f.properties.value;
|
|
7264
7432
|
if (f.properties.classValue !== undefined) cell.classTotal = (cell.classTotal || 0) + f.properties.classValue;
|
|
@@ -7269,7 +7437,8 @@
|
|
|
7269
7437
|
cell.recordCounts[f.properties.cat] += (f.properties.point_count || 1);
|
|
7270
7438
|
});
|
|
7271
7439
|
return Array.from(cells.values()).map(cell => {
|
|
7272
|
-
|
|
7440
|
+
// without RELOCATE flat's cell item sits at the cell center
|
|
7441
|
+
const ll = atCellCenter && cellPx ? worldPixelToLngLat(cell.cx, cell.cy, zoom) : worldPixelToLngLat(cell.sumX / cell.n, cell.sumY / cell.n, zoom);
|
|
7273
7442
|
return {
|
|
7274
7443
|
geometry: { type: 'Point', coordinates: [ll.lng, ll.lat] },
|
|
7275
7444
|
properties: { counts: cell.counts, total: cell.counts.reduce((a, c) => a + c, 0), recordCounts: cell.recordCounts, ...(cell.classTotal !== undefined ? { classTotal: cell.classTotal } : {}) }
|
|
@@ -7424,7 +7593,7 @@
|
|
|
7424
7593
|
this._gridRefLat = (minLat <= maxLat) ? (minLat + maxLat) / 2 : 0;
|
|
7425
7594
|
}
|
|
7426
7595
|
|
|
7427
|
-
if (this.flags.has('DOMINANT') && this.binding.value) {
|
|
7596
|
+
if (this.flags.has('DOMINANT') && this.binding.value && !isAggregatedCategoricalChoropleth(this)) {
|
|
7428
7597
|
// .type("CHOROPLETH|DOMINANT") — a MULTI-field bound value
|
|
7429
7598
|
// (binding.value is a pipe-joined field list, e.g. one CSV column
|
|
7430
7599
|
// per age band for the same year/sex — same pipe convention
|
|
@@ -7774,6 +7943,26 @@
|
|
|
7774
7943
|
}
|
|
7775
7944
|
|
|
7776
7945
|
_rebuildActiveFeatures() {
|
|
7946
|
+
// An AGGREGATE CATEGORICAL choropleth filters the RECORDS of each
|
|
7947
|
+
// polygon (as its .filter() does before the join), then sums them —
|
|
7948
|
+
// not the polygons by their first record
|
|
7949
|
+
if (isAggregatedCategoricalChoropleth(this)) {
|
|
7950
|
+
const expr = this._runtimeFilterExpr;
|
|
7951
|
+
const clauses = Array.from(this.facetFilters.entries());
|
|
7952
|
+
if (!expr && !clauses.length) { this._activeFeatures = null; return; }
|
|
7953
|
+
const pred = expr ? flatFilterPredicate(String(expr)) : null;
|
|
7954
|
+
this._activeFeatures = this.features.map(f => {
|
|
7955
|
+
const rows = f.properties && f.properties[AGGREGATED_ROWS];
|
|
7956
|
+
if (!rows) return f;
|
|
7957
|
+
const kept = rows.filter(r => (!pred || pred(r)) && clauses.every(([field, clause]) => matchesFacetClause(r[field], clause)));
|
|
7958
|
+
const properties = kept.length ? Object.assign({}, kept[0]) : {};
|
|
7959
|
+
if (kept.length) Object.defineProperty(properties, AGGREGATED_ROWS, { value: kept });
|
|
7960
|
+
const out = { type: 'Feature', geometry: f.geometry, properties };
|
|
7961
|
+
if (f._flatAreaKm2 !== undefined) out._flatAreaKm2 = f._flatAreaKm2;
|
|
7962
|
+
return out;
|
|
7963
|
+
});
|
|
7964
|
+
return;
|
|
7965
|
+
}
|
|
7777
7966
|
// Base set: this.features narrowed by the runtime WHERE-filter
|
|
7778
7967
|
// (changeThemeStyle's own mechanism, see _runtimeFilterExpr's own
|
|
7779
7968
|
// comment) if one is active — reusing applyWhereFilter, the SAME
|
|
@@ -8470,8 +8659,11 @@
|
|
|
8470
8659
|
// unlike CHOROPLETH, there's no per-feature classification here.
|
|
8471
8660
|
// flat puts fillopacity into the shape's fill-opacity only
|
|
8472
8661
|
// (maptheme.js 13407, 14071-14078): the outline stays opaque
|
|
8473
|
-
|
|
8474
|
-
|
|
8662
|
+
// (a fill color's own alpha — rgba(…, 0.5) — times fillopacity)
|
|
8663
|
+
getFillColor: filled ? [...hexOrNamedToRgb(raw).slice(0, 3), Math.round(255 * cssColorAlpha(raw) * (styleNum(this.style.fillopacity) || 1))] : [0, 0, 0, 0],
|
|
8664
|
+
getLineColor: this.style.linecolor
|
|
8665
|
+
? withAlpha(hexOrNamedToRgb(styleLineColor(this.style.linecolor)), cssColorAlpha(styleLineColor(this.style.linecolor)))
|
|
8666
|
+
: [130, 130, 130],
|
|
8475
8667
|
lineWidthMinPixels: styleNum(this.style.linewidth) || 1,
|
|
8476
8668
|
// Confirmed live (2026-09-21, deck.gl v9.4/MapLibre-globe upgrade):
|
|
8477
8669
|
// this layer's fill rendered with a moire/hatching pattern of
|
|
@@ -8540,9 +8732,10 @@
|
|
|
8540
8732
|
// — plain/PERCENTOFMEAN/DEVIATION); or COMPOSECOLOR's per-polygon
|
|
8541
8733
|
// BLEND across the same piped fields (_resolveComposedColor — no
|
|
8542
8734
|
// single winning class, so no `cat`/tooltip chart line, just the
|
|
8543
|
-
// blended color directly)
|
|
8544
|
-
//
|
|
8545
|
-
//
|
|
8735
|
+
// blended color directly); AGGREGATE CATEGORICAL on one field — the
|
|
8736
|
+
// dominant category of each polygon's records (aggregatedCategoricalClass).
|
|
8737
|
+
// A plain CATEGORICAL choropleth (exact match of one row's value) is not
|
|
8738
|
+
// implemented. Geometry + properties are already the joined
|
|
8546
8739
|
// FeatureCollection from joinChoroplethFeatures. `zoom` is only used
|
|
8547
8740
|
// for style.fillopacity:"auto" (see resolveAutoFillOpacity) — every
|
|
8548
8741
|
// other branch here is zoom-independent.
|
|
@@ -8565,7 +8758,37 @@
|
|
|
8565
8758
|
// DOPACITY/COMPOSECOLOR branches.
|
|
8566
8759
|
const dopacityActive = !isComposeColor &&
|
|
8567
8760
|
(this.flags.has('DOPACITY') || this.flags.has('DOPACITYMIN') || this.flags.has('DOPACITYMAX') || this.flags.has('DOPACITYMINMAX'));
|
|
8761
|
+
// AGGREGATE CATEGORICAL: every polygon's parts and dominant class
|
|
8762
|
+
// (aggregatedCategoricalClass), the biggest part per class over all
|
|
8763
|
+
// polygons for the opacity (flat's nMaxA)
|
|
8764
|
+
const aggCat = isAggregatedCategoricalChoropleth(this) && this.categoryIndexByLabel;
|
|
8765
|
+
let aggOf = null, aggMaxA = null;
|
|
8766
|
+
if (aggCat) {
|
|
8767
|
+
const nCats = this.categoryLabels.length;
|
|
8768
|
+
aggOf = new Map();
|
|
8769
|
+
aggMaxA = new Array(nCats).fill(0);
|
|
8770
|
+
source.forEach(f => {
|
|
8771
|
+
const rows = f.properties && f.properties[AGGREGATED_ROWS];
|
|
8772
|
+
if (!rows) return;
|
|
8773
|
+
const agg = aggregatedCategoricalClass(rows, this.binding, this.flags, this.categoryIndexByLabel, nCats);
|
|
8774
|
+
if (!agg) return;
|
|
8775
|
+
aggOf.set(f, agg);
|
|
8776
|
+
agg.parts.forEach((v, i) => { if (v > aggMaxA[i]) aggMaxA[i] = v; });
|
|
8777
|
+
});
|
|
8778
|
+
}
|
|
8568
8779
|
const data = source.map(f => {
|
|
8780
|
+
if (aggCat) {
|
|
8781
|
+
const agg = aggOf.get(f);
|
|
8782
|
+
return {
|
|
8783
|
+
type: 'Feature',
|
|
8784
|
+
geometry: f.geometry,
|
|
8785
|
+
properties: {
|
|
8786
|
+
// no part above 0: no item in flat, not painted
|
|
8787
|
+
value: agg ? agg.value : undefined, raw: agg ? f.properties : {}, cat: agg ? agg.index : null,
|
|
8788
|
+
dopacityAlpha: agg && dopacityActive ? dominantDopacityAlpha(this.style, this.flags, agg.value, aggMaxA[agg.index]) : null
|
|
8789
|
+
}
|
|
8790
|
+
};
|
|
8791
|
+
}
|
|
8569
8792
|
if (isComposeColor) {
|
|
8570
8793
|
return {
|
|
8571
8794
|
type: 'Feature',
|
|
@@ -8604,7 +8827,14 @@
|
|
|
8604
8827
|
// evidence mode "isolate", the default (7715) — or grayed with
|
|
8605
8828
|
// style.evidence "isolate_gray". ("highlight", flat's highlight list,
|
|
8606
8829
|
// is shown grayed too here.) COMPOSECOLOR has no classes.
|
|
8607
|
-
|
|
8830
|
+
// linecolor "none": no outline; a class color "none" (an explicit
|
|
8831
|
+
// colorscheme list) leaves its polygons unpainted, an rgba() class
|
|
8832
|
+
// color keeps its alpha — as flat's SVG fill does
|
|
8833
|
+
const lineColorRaw = this.style.linecolor ? styleLineColor(this.style.linecolor) : null;
|
|
8834
|
+
const lineRgb = lineColorRaw ? withAlpha(hexOrNamedToRgb(lineColorRaw), cssColorAlpha(lineColorRaw)) : [255, 255, 255];
|
|
8835
|
+
const cs = this.style.colorscheme;
|
|
8836
|
+
const classAlpha = Array.isArray(cs) && cs.length && !/^\d+$/.test(String(cs[0])) ? cs.map(cssColorAlpha) : null;
|
|
8837
|
+
const alphaOfClass = i => (classAlpha && i != null && classAlpha[i] != null ? classAlpha[i] : 1);
|
|
8608
8838
|
const evidenceMode = String(this.style.evidence || 'isolate');
|
|
8609
8839
|
const unmarkedEvidence = d => {
|
|
8610
8840
|
if (!this._markedClasses.size || this._onMarksChanged || this._markedClasses.has(d.properties.cat)) return null;
|
|
@@ -8624,7 +8854,8 @@
|
|
|
8624
8854
|
const evidence = unmarkedEvidence(d);
|
|
8625
8855
|
if (evidence === 'hide') return [0, 0, 0, 0];
|
|
8626
8856
|
if (evidence === 'gray') return [...grayOf(rgb), 90];
|
|
8627
|
-
|
|
8857
|
+
const a = alphaOfClass(d.properties.cat);
|
|
8858
|
+
return d.properties.dopacityAlpha != null ? [...rgb.slice(0, 3), Math.round(d.properties.dopacityAlpha * a * 255)] : withAlpha(rgb, a);
|
|
8628
8859
|
},
|
|
8629
8860
|
// a per-polygon accessor only while a class is isolated
|
|
8630
8861
|
getLineColor: this._markedClasses.size && !this._onMarksChanged && evidenceMode === 'isolate'
|
|
@@ -9568,6 +9799,14 @@
|
|
|
9568
9799
|
});
|
|
9569
9800
|
|
|
9570
9801
|
const doRelocate = this.flags.has('RELOCATE');
|
|
9802
|
+
// SEQUENCE: flat's AGGREGATE cell is ONE chart with a part per
|
|
9803
|
+
// category (RELOCATE only moves it to the records' mean position) —
|
|
9804
|
+
// every category of a cell, single records included, in one group
|
|
9805
|
+
if (flatFlag(this.flags, 'SEQUENCE') && this.flags.has('CATEGORICAL')) {
|
|
9806
|
+
const groups = groupCoLocated(clusterFeatures.concat(individual), effectiveZoom, this._clusterRadiusPx,
|
|
9807
|
+
this.categoryLabels ? this.categoryLabels.length : 1, this.flags, !doRelocate);
|
|
9808
|
+
return { individual: [], groups };
|
|
9809
|
+
}
|
|
9571
9810
|
// without RELOCATE a cell's item already sits at the cell center
|
|
9572
9811
|
// (aggregateOnGrid)
|
|
9573
9812
|
const groups = doRelocate ? this._groupCoLocated(clusterFeatures, effectiveZoom) : clusterFeatures.map(f => {
|
|
@@ -9832,6 +10071,9 @@
|
|
|
9832
10071
|
if (userLayers) return layers.concat(userLayers);
|
|
9833
10072
|
}
|
|
9834
10073
|
if (this.flags.has('LABEL')) return layers.concat(this._buildLabelChartLayers(combined, zoom, liveZoom, sizeValueOf));
|
|
10074
|
+
if (flatFlag(this.flags, 'SEQUENCE') && this.flags.has('CATEGORICAL') && this.flags.has('AGGREGATE')) {
|
|
10075
|
+
return layers.concat(this._buildSequenceChartLayers(combined, zoom, liveZoom));
|
|
10076
|
+
}
|
|
9835
10077
|
const fillOpacity = styleNum(this.style.fillopacity) || 0.85;
|
|
9836
10078
|
// .style({linecolor, linewidth}) — an individual icon's own border,
|
|
9837
10079
|
// see _buildSingleIcon's own comment. Real ixmaps-flat symbol
|
|
@@ -10144,6 +10386,164 @@
|
|
|
10144
10386
|
return layers;
|
|
10145
10387
|
}
|
|
10146
10388
|
|
|
10389
|
+
// flat's nAllMaxValue for a SEQUENCE chart (maptheme.js 13211-13215):
|
|
10390
|
+
// the biggest category part of any chart at this aggregation — over
|
|
10391
|
+
// the whole dataset, not the view; cached per zoom
|
|
10392
|
+
_sequenceMaxValue(zoom) {
|
|
10393
|
+
if (this._seqMaxFor !== this._clusterIndices) { this._seqMaxFor = this._clusterIndices; this._seqMax = new Map(); }
|
|
10394
|
+
if (!this._seqMax.has(zoom)) {
|
|
10395
|
+
const { groups } = this._computeAggregatedItems([-180, -90, 180, 90], zoom);
|
|
10396
|
+
let max = 0;
|
|
10397
|
+
groups.forEach(g => g.properties.counts.forEach(c => { if (c > max) max = c; }));
|
|
10398
|
+
this._seqMax.set(zoom, max || 1);
|
|
10399
|
+
}
|
|
10400
|
+
return this._seqMax.get(zoom);
|
|
10401
|
+
}
|
|
10402
|
+
|
|
10403
|
+
// CHART|SYMBOL|SEQUENCE on an AGGREGATE CATEGORICAL theme: each cell is
|
|
10404
|
+
// one chart of a symbol per category part (sequenceLayout), as flat's
|
|
10405
|
+
// SYMBOL branch draws it (maptheme.js 21122-22400):
|
|
10406
|
+
// - part radius: max radius · (v / max value)^(1/sizepow), max value =
|
|
10407
|
+
// normalsizevalue, else the biggest part of any chart (21793)
|
|
10408
|
+
// - circle in the category color at fillopacity, stroked in linecolor
|
|
10409
|
+
// linewidth · unit · min(2r / max radius, 0.2) wide (21966)
|
|
10410
|
+
// - GLOW (and AURA): with SORT only around the first part, else around
|
|
10411
|
+
// every part (22009)
|
|
10412
|
+
// - marked legend classes: the other parts hidden, the marked ones
|
|
10413
|
+
// back on the center (14339-14420, evidence "isolate"); with
|
|
10414
|
+
// "isolate_gray" the others dimmed
|
|
10415
|
+
// - STAR: the center part drawn last, on top of its satellites (23145)
|
|
10416
|
+
// - VALUES: each part's value (COUNT: its records; valuefield: the class name)
|
|
10417
|
+
// - align / offsets as the SYMBOL branch: half size the normal radius
|
|
10418
|
+
// A part's pick object carries its chart's properties (tooltip).
|
|
10419
|
+
_buildSequenceChartLayers(charts, zoom, liveZoom) {
|
|
10420
|
+
const st = this.style;
|
|
10421
|
+
const seqMax = this._sequenceMaxValue(zoom);
|
|
10422
|
+
const partRadius = v => valueRadius(v, liveZoom, st, this.mapOptions, this.flags, seqMax);
|
|
10423
|
+
const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(st.scale) || 1);
|
|
10424
|
+
const normalR = NORMAL_RADIUS_PX * unit;
|
|
10425
|
+
const nCats = this.categoryLabels ? this.categoryLabels.length : 1;
|
|
10426
|
+
const marking = this._markedClasses.size && !this._onMarksChanged;
|
|
10427
|
+
const gray = marking && String(st.evidence || 'isolate') === 'isolate_gray';
|
|
10428
|
+
const sorted = [...this.flags].some(f => /\bSORT\b/.test(f));
|
|
10429
|
+
const fillOpacity = styleNum(st.fillopacity) || 0.85;
|
|
10430
|
+
const borderRgb = st.linecolor && styleLineColor(st.linecolor) !== 'none' ? hexOrNamedToRgb(styleLineColor(st.linecolor)) : null;
|
|
10431
|
+
const lineWidth = styleNum(st.linewidth) || 1;
|
|
10432
|
+
const symbols = Array.isArray(st.symbols) ? st.symbols : null;
|
|
10433
|
+
const shapeOf = i => { const sh = symbols && (symbols[i] || symbols[0]); return /^(circle|square|diamond|triangle)$/.test(sh) ? sh : 'circle'; };
|
|
10434
|
+
const colorOf = i => this.categoryColorsRgb[i] || [128, 128, 128];
|
|
10435
|
+
const parts = [], glows = [];
|
|
10436
|
+
for (const d of charts) {
|
|
10437
|
+
let layout = sequenceLayout(d.properties.counts, this.flags, partRadius, { maxRadius: normalR, maxValue: seqMax, nParts: nCats, rangeScale: styleNum(st.rangescale) || 0 });
|
|
10438
|
+
if (!layout.length) continue;
|
|
10439
|
+
// GLOW belongs to the first part (sorted) or to every part, and
|
|
10440
|
+
// hides with it when its class is not marked
|
|
10441
|
+
const glowParts = new Set(sorted ? [layout[0].i] : layout.map(p => p.i));
|
|
10442
|
+
if (marking && !gray) {
|
|
10443
|
+
layout = layout.filter(p => this._markedClasses.has(p.i)).map(p => ({ ...p, x: 0, y: 0 }));
|
|
10444
|
+
if (!layout.length) continue;
|
|
10445
|
+
}
|
|
10446
|
+
// the chart's own ptNull.y: the last part's radius + 5 units (23204)
|
|
10447
|
+
d._seqLast = layout[layout.length - 1].r;
|
|
10448
|
+
const items = layout.map(p => {
|
|
10449
|
+
const item = { geometry: d.geometry, properties: d.properties, chart: d, part: p };
|
|
10450
|
+
if (glowParts.has(p.i)) glows.push(item);
|
|
10451
|
+
return item;
|
|
10452
|
+
});
|
|
10453
|
+
// STAR: the first part (its center) is put on top at the end (23145)
|
|
10454
|
+
if (flatFlag(this.flags, 'STAR') && items.length > 1) items.push(items.shift());
|
|
10455
|
+
parts.push(...items);
|
|
10456
|
+
}
|
|
10457
|
+
const alignOf = this._chartAlignFn(liveZoom, d => (d.chart || d)._seqLast || 0);
|
|
10458
|
+
const offsetOf = item => { const o = alignOf(item.chart); return [o[0] + item.part.x, o[1] + item.part.y]; };
|
|
10459
|
+
const alphaOf = item => (gray && !this._markedClasses.has(item.part.i) ? DIMMED_ICON_ALPHA : 255);
|
|
10460
|
+
const layers = [];
|
|
10461
|
+
if (glows.length && this.flags.has('GLOW') && !glowHiddenByScale(st, liveZoom)) {
|
|
10462
|
+
layers.push(new IconLayer({
|
|
10463
|
+
id: `ix-glow-${this.name}-g${this._iconGeneration}`,
|
|
10464
|
+
data: glows, pickable: false,
|
|
10465
|
+
getPosition: d => d.geometry.coordinates,
|
|
10466
|
+
getIcon: d => this._getGlowIcon(colorOf(d.part.i)),
|
|
10467
|
+
getSize: d => d.part.r * 11,
|
|
10468
|
+
getPixelOffset: offsetOf,
|
|
10469
|
+
getColor: d => [255, 255, 255, alphaOf(d)],
|
|
10470
|
+
sizeUnits: 'pixels', billboard: true,
|
|
10471
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
10472
|
+
}));
|
|
10473
|
+
} else if (glows.length && this.flags.has('AURA') && !glowHiddenByScale(st, liveZoom)) {
|
|
10474
|
+
layers.push(new IconLayer({
|
|
10475
|
+
id: `ix-aura-${this.name}-g${this._iconGeneration}`,
|
|
10476
|
+
data: glows, pickable: false,
|
|
10477
|
+
getPosition: d => d.geometry.coordinates,
|
|
10478
|
+
getIcon: d => this._buildSingleIcon(borderRgb || colorOf(d.part.i), 0.3, 'circle', null, 0),
|
|
10479
|
+
getSize: d => d.part.r * 2 * 1.3,
|
|
10480
|
+
getPixelOffset: offsetOf,
|
|
10481
|
+
getColor: d => [255, 255, 255, alphaOf(d)],
|
|
10482
|
+
sizeUnits: 'pixels', billboard: true,
|
|
10483
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
10484
|
+
}));
|
|
10485
|
+
}
|
|
10486
|
+
layers.push(new IconLayer({
|
|
10487
|
+
id: `ix-bubbles-${this.name}-g${this._iconGeneration}`,
|
|
10488
|
+
data: parts, pickable: true,
|
|
10489
|
+
getPosition: d => d.geometry.coordinates,
|
|
10490
|
+
// the stroke, in the icon raster's pixels (the icon is drawn at 2r)
|
|
10491
|
+
getIcon: d => this._buildSingleIcon(colorOf(d.part.i), fillOpacity, shapeOf(d.part.i), borderRgb,
|
|
10492
|
+
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),
|
|
10493
|
+
getSize: d => d.part.r * 2,
|
|
10494
|
+
getPixelOffset: offsetOf,
|
|
10495
|
+
getColor: d => [255, 255, 255, alphaOf(d)],
|
|
10496
|
+
sizeUnits: 'pixels', billboard: true,
|
|
10497
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
10498
|
+
}));
|
|
10499
|
+
// VALUES on each part (maptheme.js 22188-22250): the part's value — with
|
|
10500
|
+
// COUNT its record count, with valuefield on the categorical field the
|
|
10501
|
+
// class name — formatted with valuedecimals (else none) plus the unit;
|
|
10502
|
+
// font min(1, 3.2 / L) · r · valuescale, L the length of the theme's
|
|
10503
|
+
// biggest value text (szMaxText), normal weight, in valuecolor /
|
|
10504
|
+
// textcolor, else ChartColors.textColor of the part color
|
|
10505
|
+
if (this.flags.has('VALUES') && !valuesHiddenByScale(st, zoom)) {
|
|
10506
|
+
const valueScale = styleNum(st.valuescale) || 1;
|
|
10507
|
+
const { noBreaks } = this._valueTextOpts();
|
|
10508
|
+
const units = st.units ? String(st.units) : '';
|
|
10509
|
+
const unit = units ? (units[0] === '.' ? '' : ' ') + units : '';
|
|
10510
|
+
const unitText = unit.length <= 5 ? unit : '';
|
|
10511
|
+
const dec = styleNum(st.valuedecimals) || 0;
|
|
10512
|
+
const fmt = v => flatGroupedValue(v, dec, noBreaks) + unitText;
|
|
10513
|
+
const maxLen = fmt(seqMax).length;
|
|
10514
|
+
const textOverride = st.valuecolor || st.textcolor;
|
|
10515
|
+
const counting = flatFlag(this.flags, 'COUNT');
|
|
10516
|
+
const labels = [];
|
|
10517
|
+
parts.forEach(d => {
|
|
10518
|
+
const rec = this._categoryValueRecord(d.part.i);
|
|
10519
|
+
const name = rec ? String(rec[st.valuefield]) : null;
|
|
10520
|
+
const t = counting && d.properties.recordCounts ? d.properties.recordCounts[d.part.i] : d.part.v;
|
|
10521
|
+
const text = name != null ? name : fmt(t);
|
|
10522
|
+
const L = Math.max((name != null ? name.length : 0) + 1, maxLen);
|
|
10523
|
+
const fontSize = Math.min(1, 3.2 / L) * d.part.r * valueScale;
|
|
10524
|
+
if (fontSize > VALUES_MIN_FONT_PX) {
|
|
10525
|
+
labels.push({ geometry: d.geometry, text, fontSize, pixelOffset: offsetOf(d), alpha: alphaOf(d),
|
|
10526
|
+
color: textOverride ? hexOrNamedToRgb(textOverride) : flatChartTextRgb(colorOf(d.part.i)) });
|
|
10527
|
+
}
|
|
10528
|
+
});
|
|
10529
|
+
if (labels.length) {
|
|
10530
|
+
layers.push(new TextLayer({
|
|
10531
|
+
id: `ix-cluster-values-${this.name}`,
|
|
10532
|
+
data: labels, pickable: false,
|
|
10533
|
+
getPosition: d => d.geometry.coordinates,
|
|
10534
|
+
getText: d => d.text,
|
|
10535
|
+
getSize: d => d.fontSize,
|
|
10536
|
+
getColor: d => [...d.color.slice(0, 3), d.alpha],
|
|
10537
|
+
getPixelOffset: d => d.pixelOffset,
|
|
10538
|
+
sizeUnits: 'pixels', fontFamily: 'arial', fontWeight: 'normal',
|
|
10539
|
+
characterSet: 'auto',
|
|
10540
|
+
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
10541
|
+
}));
|
|
10542
|
+
}
|
|
10543
|
+
}
|
|
10544
|
+
return layers;
|
|
10545
|
+
}
|
|
10546
|
+
|
|
10147
10547
|
// flat's chart position for this build (flatChartAlignOffset): a
|
|
10148
10548
|
// function item → pixel offset, the same for every part of one chart
|
|
10149
10549
|
// (symbol, glow, shadow, box, title, value text). unit is flat's
|
|
@@ -10387,6 +10787,23 @@
|
|
|
10387
10787
|
}
|
|
10388
10788
|
}
|
|
10389
10789
|
|
|
10790
|
+
// a color's own opacity, 0..1, as SVG paints it in flat: "none" (SVG's
|
|
10791
|
+
// no paint — a polygon with only its outline, or a class not painted)
|
|
10792
|
+
// 0, rgba()/hsla() its alpha, #rrggbbaa / #rgba its alpha digits, else 1
|
|
10793
|
+
function cssColorAlpha(v) {
|
|
10794
|
+
if (typeof v !== 'string') return 1;
|
|
10795
|
+
const c = v.trim().toLowerCase();
|
|
10796
|
+
if (c === 'none' || c === 'transparent') return 0;
|
|
10797
|
+
const m = c.match(/^(?:rgba|hsla)\([^)]*,\s*([\d.]+%?)\s*\)$/) || c.match(/^(?:rgb|hsl)a?\([^)]*\/\s*([\d.]+%?)\s*\)$/);
|
|
10798
|
+
if (m) return Math.max(0, Math.min(1, m[1].endsWith('%') ? parseFloat(m[1]) / 100 : parseFloat(m[1])));
|
|
10799
|
+
const h = c.match(/^#(?:[0-9a-f]{3}([0-9a-f])|[0-9a-f]{6}([0-9a-f]{2}))$/);
|
|
10800
|
+
if (h) return parseInt(h[1] ? h[1] + h[1] : h[2], 16) / 255;
|
|
10801
|
+
return 1;
|
|
10802
|
+
}
|
|
10803
|
+
// [r, g, b] with an alpha 0..1 — the plain [r, g, b] (opaque) when it is 1
|
|
10804
|
+
function withAlpha(rgb, a) {
|
|
10805
|
+
return a >= 1 ? rgb.slice(0, 3) : [...rgb.slice(0, 3), Math.round(255 * Math.max(0, a))];
|
|
10806
|
+
}
|
|
10390
10807
|
function hexOrNamedToRgb(v) {
|
|
10391
10808
|
const NAMED = { gray: [128, 128, 128], grey: [128, 128, 128], black: [0, 0, 0], white: [255, 255, 255] };
|
|
10392
10809
|
return parseCssColor(v) || NAMED[v] || [130, 130, 130];
|
|
@@ -10405,6 +10822,13 @@
|
|
|
10405
10822
|
// nothing here authored that string. Escaped before going into
|
|
10406
10823
|
// innerHTML so a stray `<`/`&`/quote in a source field can't break the
|
|
10407
10824
|
// option markup (or worse).
|
|
10825
|
+
// a legend's row labels as HTML: the page's .style({label}) as given,
|
|
10826
|
+
// values from the data escaped
|
|
10827
|
+
function legendRowLabels(rt) {
|
|
10828
|
+
const display = rt.categoryDisplayLabels;
|
|
10829
|
+
if (display && display !== rt.categoryLabels) return display;
|
|
10830
|
+
return (rt.categoryLabels || []).map(v => escapeHtml(v));
|
|
10831
|
+
}
|
|
10408
10832
|
function escapeHtml(s) {
|
|
10409
10833
|
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
10410
10834
|
}
|
|
@@ -10415,19 +10839,130 @@
|
|
|
10415
10839
|
// definition wins" caveat). Returns null for an unknown id, same as the
|
|
10416
10840
|
// real engine, rather than throwing.
|
|
10417
10841
|
function getThemeObj(szId) {
|
|
10418
|
-
|
|
10842
|
+
// no id: flat's current theme — here the one a page's facets were
|
|
10843
|
+
// built for (its own facet script sets ixmaps.filterThemeId), else the
|
|
10844
|
+
// last chart / choropleth theme
|
|
10845
|
+
let rt = szId != null ? _globalThemeRegistry.get(szId) : null;
|
|
10846
|
+
if (szId == null) {
|
|
10847
|
+
const pid = pageIxmaps() && pageIxmaps().filterThemeId;
|
|
10848
|
+
rt = (pid != null && _globalThemeRegistry.get(pid))
|
|
10849
|
+
|| [..._globalThemeRegistry.values()].reverse().find(r => r.flags.has('CHART') || r.flags.has('CHOROPLETH')) || null;
|
|
10850
|
+
}
|
|
10419
10851
|
if (!rt) return null;
|
|
10852
|
+
const rgbHex = c => '#' + c.slice(0, 3).map(v => Math.round(v).toString(16).padStart(2, '0')).join('');
|
|
10853
|
+
let items = null;
|
|
10854
|
+
const onMap = () => items || (items = themeItemsOnMap(rt));
|
|
10420
10855
|
return {
|
|
10421
|
-
|
|
10856
|
+
// flat's theme id (style.name) — a .layer() name may be shared by
|
|
10857
|
+
// several themes of a page
|
|
10858
|
+
szId: themeIdOf(rt),
|
|
10422
10859
|
szName: rt.name,
|
|
10423
|
-
|
|
10860
|
+
// the filter in force: a runtime one (changeThemeStyle "filter")
|
|
10861
|
+
// replaces the definition's
|
|
10862
|
+
szFilter: rt._runtimeFilterExpr || rt._filterExpr || '',
|
|
10424
10863
|
szFlag: Array.from(rt.flags).join('|'),
|
|
10425
10864
|
// flat's fVisible is false for a FEATURE theme outside its
|
|
10426
10865
|
// featureupper/featurelower scales too (maptheme.js 2609-2611) — a
|
|
10427
10866
|
// broker reads it to skip refreshing a hidden theme
|
|
10428
|
-
fVisible: !rt._hidden && !(_lastMapApi && _lastMapApi.map && themeHiddenByScale(rt.flags, rt.style, _lastMapApi.map.getZoom()))
|
|
10867
|
+
fVisible: !rt._hidden && !(_lastMapApi && _lastMapApi.map && themeHiddenByScale(rt.flags, rt.style, _lastMapApi.map.getZoom())),
|
|
10868
|
+
nScale: styleNum(rt.style.scale) || 1,
|
|
10869
|
+
szUnits: rt.style.units || '',
|
|
10870
|
+
// flat stores the unit with a leading space unless it starts with "."
|
|
10871
|
+
szUnit: rt.style.units ? (String(rt.style.units)[0] === '.' ? '' : ' ') + rt.style.units : '',
|
|
10872
|
+
szTitleField: rt.binding.title || null,
|
|
10873
|
+
szFieldsA: rt.binding.value ? String(rt.binding.value).split('|') : [],
|
|
10874
|
+
szSizeField: rt.binding.size || null,
|
|
10875
|
+
// the classes, as flat's facet / legend scripts read them:
|
|
10876
|
+
// colorScheme by class, nStringToValueA value → class + 1,
|
|
10877
|
+
// szLabelA / szValuesA the style's label / values lists
|
|
10878
|
+
colorScheme: (rt.categoryColorsRgb || []).map(rgbHex),
|
|
10879
|
+
nStringToValueA: rt.categoryLabels ? Object.fromEntries(rt.categoryLabels.map((v, i) => [v, i + 1])) : {},
|
|
10880
|
+
szLabelA: Array.isArray(rt.style.label) ? rt.style.label.map(String) : (rt.categoryDisplayLabels || null),
|
|
10881
|
+
szValuesA: Array.isArray(rt.style.values) ? rt.style.values.map(String) : null,
|
|
10882
|
+
partsA: rt.partsA || [],
|
|
10883
|
+
// flat's own data surface, as a page's own facet / list scripts
|
|
10884
|
+
// (flat's ui/js/tools facet.js, list.js) read it: the theme's data
|
|
10885
|
+
// table (objTheme.dbTable/dbFields/dbRecords — records as arrays in
|
|
10886
|
+
// field order) and the items on the map (indexA → itemA[i].dbIndexA,
|
|
10887
|
+
// the records of each item). Here one item holds every record of
|
|
10888
|
+
// the theme within the current view (shown, in scale, facet filters
|
|
10889
|
+
// applied) — computed when read
|
|
10890
|
+
get objTheme() { return themeDbTable(rt); },
|
|
10891
|
+
// computed once per theme object (a page's loop reads itemA per record)
|
|
10892
|
+
get indexA() { return onMap().indexA; },
|
|
10893
|
+
get itemA() { return onMap().itemA; }
|
|
10429
10894
|
};
|
|
10430
10895
|
}
|
|
10896
|
+
const _warnedOnce = new Set();
|
|
10897
|
+
function warnOnce(key, msg) { if (!_warnedOnce.has(key)) { _warnedOnce.add(key); console.info(msg); } }
|
|
10898
|
+
// the theme's records as flat's dbFields/dbRecords (cached per data)
|
|
10899
|
+
function themeDbTable(rt) {
|
|
10900
|
+
const features = rt.features || [];
|
|
10901
|
+
if (rt._dbTableFor === features) return rt._dbTable;
|
|
10902
|
+
const rowOf = f => (f.properties && f.properties.raw) || f.properties || {};
|
|
10903
|
+
const names = [];
|
|
10904
|
+
const seen = new Set();
|
|
10905
|
+
for (const f of features.slice(0, 1000)) for (const k of Object.keys(rowOf(f))) if (!seen.has(k)) { seen.add(k); names.push(k); }
|
|
10906
|
+
const indexOf = new Map();
|
|
10907
|
+
const dbRecords = features.map((f, i) => { indexOf.set(f, i); const row = rowOf(f); return names.map(k => (row[k] == null ? '' : row[k])); });
|
|
10908
|
+
rt._dbTableFor = features;
|
|
10909
|
+
rt._dbIndexOf = indexOf;
|
|
10910
|
+
const dbFields = names.map(id => ({ id, typ: 0, width: 16, decimals: 0 }));
|
|
10911
|
+
// dbTable: data.js Table's own table header ({records, fields} counts)
|
|
10912
|
+
rt._dbTable = { dbTable: { records: dbRecords.length, fields: dbFields.length }, dbFields, dbRecords };
|
|
10913
|
+
return rt._dbTable;
|
|
10914
|
+
}
|
|
10915
|
+
// the records on the map now: none for a hidden / out of scale theme,
|
|
10916
|
+
// else the (facet-filtered) records within the view
|
|
10917
|
+
function themeItemsOnMap(rt) {
|
|
10918
|
+
themeDbTable(rt);
|
|
10919
|
+
const map = _lastMapApi && _lastMapApi.map;
|
|
10920
|
+
const shown = map && !rt._hidden && !themeHiddenByScale(rt.flags, rt.style, map.getZoom());
|
|
10921
|
+
if (!shown) return { indexA: [], itemA: {} };
|
|
10922
|
+
const b = map.getBounds();
|
|
10923
|
+
const bbox = [b.getWest(), b.getSouth(), b.getEast(), b.getNorth()];
|
|
10924
|
+
const dbIndexA = [];
|
|
10925
|
+
for (const f of (rt._activeFeatures || rt.features || [])) {
|
|
10926
|
+
const g = f.geometry;
|
|
10927
|
+
if (g && g.type === 'Point' && !pointInBbox(g, bbox)) continue;
|
|
10928
|
+
const i = rt._dbIndexOf.get(f);
|
|
10929
|
+
if (i != null) dbIndexA.push(i);
|
|
10930
|
+
}
|
|
10931
|
+
return { indexA: ['view'], itemA: { view: { dbIndexA } } };
|
|
10932
|
+
}
|
|
10933
|
+
// flat's ixmaps.getThemeDefinitionObj(szId): the theme's definition —
|
|
10934
|
+
// type, style (sizefield/valuefield as flat names them), meta
|
|
10935
|
+
function getThemeDefinitionObj(szId) {
|
|
10936
|
+
const rt = _globalThemeRegistry.get(szId);
|
|
10937
|
+
if (!rt) return null;
|
|
10938
|
+
const style = Object.assign({}, rt.style);
|
|
10939
|
+
if (rt.binding.size && !style.sizefield) style.sizefield = rt.binding.size;
|
|
10940
|
+
if (rt.binding.title && !style.titlefield) style.titlefield = rt.binding.title;
|
|
10941
|
+
style.type = rt.flags.typeString != null ? rt.flags.typeString : [...rt.flags].join('|');
|
|
10942
|
+
const data = Object.assign({}, rt._definition && rt._definition.data);
|
|
10943
|
+
if (!data.name) data.name = themeDataName(rt);
|
|
10944
|
+
return { layer: rt.name, type: style.type, field: rt.binding.value, data, style, meta: Object.assign({}, rt.meta) };
|
|
10945
|
+
}
|
|
10946
|
+
// a theme's data table name — flat keeps named data tables in its map
|
|
10947
|
+
// window (ixmaps.embeddedSVG.window[name], read by a page's own list
|
|
10948
|
+
// scripts): the .data({name}) given, else one made from the theme id
|
|
10949
|
+
function themeDataName(rt) {
|
|
10950
|
+
const d = rt._definition && rt._definition.data;
|
|
10951
|
+
return (d && d.name) || ('themeData_' + String(themeIdOf(rt)).replace(/\W/g, '_'));
|
|
10952
|
+
}
|
|
10953
|
+
// flat's ixmaps.embeddedSVG.window: here only its named data tables, as
|
|
10954
|
+
// data.js Table objects {table, fields, records} of the themes' data
|
|
10955
|
+
const embeddedSVG = {
|
|
10956
|
+
window: new Proxy({}, {
|
|
10957
|
+
get(target, key) {
|
|
10958
|
+
if (typeof key !== 'string') return undefined;
|
|
10959
|
+
const rt = [...new Set(_globalThemeRegistry.values())].find(r => themeDataName(r) === key);
|
|
10960
|
+
if (!rt) return undefined;
|
|
10961
|
+
const t = themeDbTable(rt);
|
|
10962
|
+
return { table: t.dbTable, fields: t.dbFields, records: t.dbRecords };
|
|
10963
|
+
}
|
|
10964
|
+
})
|
|
10965
|
+
};
|
|
10431
10966
|
|
|
10432
10967
|
// GL-PORT COMPAT: real ixmaps-flat's global ixmaps.markThemeClass(szId,
|
|
10433
10968
|
// index)/unmarkThemeClass(szId, index) (htmlgui.js:2196/2208 — a bare
|
|
@@ -10444,7 +10979,15 @@
|
|
|
10444
10979
|
// here instead of redrawing.
|
|
10445
10980
|
function markThemeClass(szId, index) {
|
|
10446
10981
|
const rt = _globalThemeRegistry.get(szId);
|
|
10447
|
-
if (
|
|
10982
|
+
if (rt) markRuntimeClass(rt, index);
|
|
10983
|
+
}
|
|
10984
|
+
function unmarkThemeClass(szId, index) {
|
|
10985
|
+
const rt = _globalThemeRegistry.get(szId);
|
|
10986
|
+
if (rt) unmarkRuntimeClass(rt, index);
|
|
10987
|
+
}
|
|
10988
|
+
// the legend's own rows mark their theme directly: its .layer() name
|
|
10989
|
+
// may be shared with other themes of the page
|
|
10990
|
+
function markRuntimeClass(rt, index) {
|
|
10448
10991
|
// flat: a non-CHART theme (choropleth) marks ONE class at a time —
|
|
10449
10992
|
// MapTheme.markClass unmarks the previous class first (maptheme.js
|
|
10450
10993
|
// 14301-14305); chart themes can mark several
|
|
@@ -10454,9 +10997,7 @@
|
|
|
10454
10997
|
if (rt._onMarksChanged) rt._onMarksChanged();
|
|
10455
10998
|
else if (rt._triggerRedraw) rt._triggerRedraw();
|
|
10456
10999
|
}
|
|
10457
|
-
function
|
|
10458
|
-
const rt = _globalThemeRegistry.get(szId);
|
|
10459
|
-
if (!rt) return;
|
|
11000
|
+
function unmarkRuntimeClass(rt, index) {
|
|
10460
11001
|
rt._markedClasses.delete(index);
|
|
10461
11002
|
if (rt._onMarksChanged) rt._onMarksChanged();
|
|
10462
11003
|
else if (rt._triggerRedraw) rt._triggerRedraw();
|
|
@@ -10759,13 +11300,30 @@
|
|
|
10759
11300
|
layer, Map: createMap, setExternalData: setExternalDataBridge, getThemeObj, data: ixmapsData,
|
|
10760
11301
|
szResourceBase: ixmapsSzResourceBase, getProjectString, setProjectJSON, loadProject,
|
|
10761
11302
|
markThemeClass, unmarkThemeClass, getBoundingBox, setTitle, setTitleBox, refreshTheme, formatValue: flatFormatValue,
|
|
10762
|
-
map: mapHandle, getZoom,
|
|
11303
|
+
map: mapHandle, getZoom, getThemeDefinitionObj, embeddedSVG,
|
|
11304
|
+
// flat's theme list: the theme objects of every theme
|
|
11305
|
+
getThemes: () => [...new Set(_globalThemeRegistry.values())].map(rt => getThemeObj(themeIdOf(rt))).filter(Boolean),
|
|
11306
|
+
// flat shows a short status message on the map; not ported
|
|
11307
|
+
message: () => {},
|
|
11308
|
+
// flat's item highlight / item filter (facet histogram hovers): not ported
|
|
11309
|
+
filterThemeItems: () => warnOnce('filterThemeItems', '[ixmaps-gl] ixmaps.filterThemeItems is not supported yet'),
|
|
11310
|
+
highlightThemeItems: () => warnOnce('highlightThemeItems', '[ixmaps-gl] ixmaps.highlightThemeItems is not supported yet'),
|
|
11311
|
+
// flat's global forms with the map name first (htmlgui.js) — the map
|
|
11312
|
+
// handle's methods on the last built map
|
|
11313
|
+
getCenter: () => (_lastMapApi && _lastMapApi.map ? _lastMapApi.map.getCenter() : null),
|
|
11314
|
+
getMapTypeId: () => (_lastMapApi && _lastMapApi.getMapTypeId ? _lastMapApi.getMapTypeId() : ''),
|
|
11315
|
+
changeThemeStyle: (szMap, szId, szStyle, szFlag) => _mapHandle.changeThemeStyle(szId, szStyle, szFlag),
|
|
11316
|
+
removeTheme: (szMap, szId) => _mapHandle.remove(szId),
|
|
11317
|
+
setBasemapOpacity: (szMap, nOpacity, szMode) => _mapHandle.setBasemapOpacity(nOpacity, szMode),
|
|
10763
11318
|
// this engine's version (flat's own ixmaps.version numbers flat)
|
|
10764
11319
|
glVersion: IXMAPS_GL_VERSION,
|
|
10765
11320
|
// flat always has this page hook (its layer legend tool defines it,
|
|
10766
11321
|
// ui/js/tools/layer_legend.js 702-712): pages wrap it and call the one
|
|
10767
11322
|
// they replaced — the chain ends here
|
|
10768
11323
|
htmlgui_onZoomAndPan: function (nZoom) {},
|
|
11324
|
+
// flat calls it when a theme is drawn (htmlgui.js) — pages wrap it
|
|
11325
|
+
// (statistics, facets); called after each settled redraw, notifyRedraw
|
|
11326
|
+
htmlgui_onDrawTheme: function (szId) {},
|
|
10769
11327
|
// carried over from a page's own pre-load `ixmaps.validate = ...`
|
|
10770
11328
|
validate: _preloadValidate
|
|
10771
11329
|
};
|
|
@@ -10780,7 +11338,7 @@
|
|
|
10780
11338
|
// test-only: lets test/unit/*.test.mjs call pure internals directly (the
|
|
10781
11339
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
10782
11340
|
global.__ixmapsGlInternals = {
|
|
10783
|
-
IXMAPS_GL_VERSION, computeBubblePackLayout, isolatedBubblePackLayout,
|
|
11341
|
+
IXMAPS_GL_VERSION, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout,
|
|
10784
11342
|
normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, typeStyleNumbers, styleNum,
|
|
10785
11343
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
10786
11344
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|