ixmaps-gl 0.2.31 → 0.2.33
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 +39 -7
- package/ixmaps-gl.js +348 -60
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,10 +114,10 @@ page. It currently implements:
|
|
|
114
114
|
`window.__setFacetFilter`) for building filterable sidebars
|
|
115
115
|
- Globe (orthographic) projection alongside flat Mercator
|
|
116
116
|
|
|
117
|
-
Not yet implemented:
|
|
118
|
-
`
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
Not yet implemented: the `QUAD`/`WAFFLE` base types. Custom SYMBOL shapes are supported
|
|
118
|
+
as image-URL symbols (`symbols: [...]`), not as built-in shape variants.
|
|
119
|
+
The top-of-file comment in [`ixmaps-gl.js`](./ixmaps-gl.js) is the single source of truth
|
|
120
|
+
for scope; this list only summarises it.
|
|
121
121
|
|
|
122
122
|
### Page scripts, named data and the runtime API
|
|
123
123
|
|
|
@@ -230,6 +230,38 @@ space beside it. `.options({themecopies: true})` (also at runtime) repeats the t
|
|
|
230
230
|
copy. `.options({worldcopies: false})` draws the world once for the map itself, but MapLibre then
|
|
231
231
|
also stops the zoom-out where the world gets narrower than the window.
|
|
232
232
|
|
|
233
|
+
### Map events, bounds and the MapLibre map
|
|
234
|
+
|
|
235
|
+
`map.on("zoomend moveend", fn)` (flat's `mapApi.on`; on the builder chain, the callback object or
|
|
236
|
+
`ixmaps.map()`) fires:
|
|
237
|
+
|
|
238
|
+
| event | when | handler receives |
|
|
239
|
+
|---|---|---|
|
|
240
|
+
| `ready` | the map has loaded (called at once if it already has) | `{type, szMap}` |
|
|
241
|
+
| `viewchange` (`zoompan`) | after every pan or zoom | `{type, szMap, nZoom, zoomChanged, panChanged}` |
|
|
242
|
+
| `zoomend` / `moveend` | the zoom changed / the centre moved without a zoom change | as `viewchange` |
|
|
243
|
+
| `click` (`itemclick`) | an item was clicked — every item, also of a theme bound by geometry only | `{type, szMap, szId, id, theme, record, lngLat, point}` |
|
|
244
|
+
| `mapclick` | any click on the map, background included (gl addition) | `{type, szMap, lngLat: {lng, lat}, point: {x, y}}` |
|
|
245
|
+
| `layerdraw` (`drawtheme`) | after every redraw, once per theme | `{type, szMap, szId, id}` |
|
|
246
|
+
|
|
247
|
+
`szId` is `theme::key` and `id` the key: the theme's selection field value (the `id`, position or lookup
|
|
248
|
+
field), else the feature's `id`, else its position `"lng,lat"`; `record` is the item's data record (gl
|
|
249
|
+
addition). `szMap` is the map's `name` option, or `null`. flat's `frozenDynamic` is not provided.
|
|
250
|
+
`map.off("click", fn)` removes a handler, as in flat; several event names may be given, separated by
|
|
251
|
+
spaces, and the names in brackets are flat's aliases.
|
|
252
|
+
|
|
253
|
+
Differences from flat: `layerdraw` fires for every theme on every redraw, panning included (flat: once per
|
|
254
|
+
theme as it is drawn); `ready` means the MapLibre map has loaded, themes may still be loading; view events
|
|
255
|
+
fire when a movement ends, not during it. The other flat event names (`mouseover`/`itemover`,
|
|
256
|
+
`mouseout`/`itemout`, `resize`, `layeradd`/`newtheme`, `layerremove`/`removetheme`) are accepted but never
|
|
257
|
+
fire; the console says so once. `ixmaps.onItemAction(null, null, {lngLat, point})` gets the click position on
|
|
258
|
+
a background click as a third argument; it is still called for items only when the theme has a selection
|
|
259
|
+
field.
|
|
260
|
+
|
|
261
|
+
`ixmaps.map().getBounds()` returns flat's `[swLat, swLng, neLat, neLng]` (`ixmaps.getBoundingBox()`
|
|
262
|
+
the same box as `[{lat, lng} SW, {lat, lng} NE]`); `ixmaps.map().getMapLibre()` returns the MapLibre
|
|
263
|
+
map itself, or `undefined` until it is built.
|
|
264
|
+
|
|
233
265
|
### Click to action
|
|
234
266
|
|
|
235
267
|
flat's click-to-action (`ui/js/tools/tooltip_to_action.js`, e.g. a click on a country filters every
|
|
@@ -302,7 +334,7 @@ of your own project files gl can render.
|
|
|
302
334
|
Load the engine from jsDelivr, pinned to a release tag (or `@main` for the latest commit):
|
|
303
335
|
|
|
304
336
|
```html
|
|
305
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
337
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.33/ixmaps-gl.js"></script>
|
|
306
338
|
```
|
|
307
339
|
|
|
308
340
|
or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmaps-gl)
|
|
@@ -310,13 +342,13 @@ or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmap
|
|
|
310
342
|
browser script, which defines the global `ixmaps`):
|
|
311
343
|
|
|
312
344
|
```html
|
|
313
|
-
<script src="https://unpkg.com/ixmaps-gl@0.2.
|
|
345
|
+
<script src="https://unpkg.com/ixmaps-gl@0.2.33/ixmaps-gl.js"></script>
|
|
314
346
|
```
|
|
315
347
|
|
|
316
348
|
or use a local copy (`<script src="ixmaps-gl.js"></script>`). A minimal page:
|
|
317
349
|
|
|
318
350
|
```html
|
|
319
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
351
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.33/ixmaps-gl.js"></script>
|
|
320
352
|
<div id="map_div" style="position:absolute;inset:0;"></div>
|
|
321
353
|
|
|
322
354
|
<script>
|
package/ixmaps-gl.js
CHANGED
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
// ---------------------------------------------------------------
|
|
101
101
|
// this engine's release (= package.json "version", checked by
|
|
102
102
|
// test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
|
|
103
|
-
const IXMAPS_GL_VERSION = '0.2.
|
|
103
|
+
const IXMAPS_GL_VERSION = '0.2.33';
|
|
104
104
|
|
|
105
105
|
const LIB_URLS = {
|
|
106
106
|
// Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
|
|
@@ -614,6 +614,19 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
614
614
|
[...document.styleSheets].some(s => s.href === LIB_URLS.maplibreCss) ? Promise.resolve() : loadStylesheet(LIB_URLS.maplibreCss)
|
|
615
615
|
]).then(() => {
|
|
616
616
|
({ IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer, PathLayer, PathStyleExtension, HeatmapLayer } = global.deck);
|
|
617
|
+
// deck.gl 9 gamma-encodes the layer-level `opacity` prop in its `layer` shader module
|
|
618
|
+
// (Math.pow(opacity, 1/2.2); deck 8 and flat's SVG fill-opacity are linear), so a declared
|
|
619
|
+
// fillopacity 0.2 would draw as 0.48. Hand deck opacity ** 2.2: it draws exactly the declared alpha.
|
|
620
|
+
// Alpha inside a color array (getFillColor [r,g,b,a]) is linear and stays as it is.
|
|
621
|
+
const linearOpacity = Cls => new Proxy(Cls, {
|
|
622
|
+
construct(target, args, newTarget) {
|
|
623
|
+
const fixed = args.map(p => (p && typeof p === 'object' && typeof p.opacity === 'number' && p.opacity >= 0 && p.opacity < 1)
|
|
624
|
+
? Object.assign({}, p, { opacity: Math.pow(p.opacity, 2.2) }) : p);
|
|
625
|
+
return Reflect.construct(target, fixed, newTarget);
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
[IconLayer, ScatterplotLayer, GeoJsonLayer, TextLayer, PathLayer, HeatmapLayer] =
|
|
629
|
+
[IconLayer, ScatterplotLayer, GeoJsonLayer, TextLayer, PathLayer, HeatmapLayer].map(linearOpacity);
|
|
617
630
|
PieSliceLayer = makePieSliceLayer(ScatterplotLayer);
|
|
618
631
|
BarShapeLayer = makeBarShapeLayer(ScatterplotLayer);
|
|
619
632
|
FlowPathLayer = makeFlowPathLayer(PathLayer);
|
|
@@ -1861,12 +1874,31 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1861
1874
|
})(g && g.coordinates);
|
|
1862
1875
|
return minX <= maxX ? [(minX + maxX) / 2, (minY + maxY) / 2] : null;
|
|
1863
1876
|
}
|
|
1877
|
+
// flat finds a geometry id field whatever its letter case (the comuni topojson has "pro_com_t", the page
|
|
1878
|
+
// binds id "pro_com_T"): the property name of a FEATURE runtime's shapes that matches the binding, else
|
|
1879
|
+
// the binding as written. Cached per runtime and name
|
|
1880
|
+
function geometryIdField(rt, name) {
|
|
1881
|
+
if (!name || !rt) return name;
|
|
1882
|
+
const cache = rt._idFieldCache || (rt._idFieldCache = {});
|
|
1883
|
+
if (cache[name] !== undefined) return cache[name];
|
|
1884
|
+
const feats = rt.features || [];
|
|
1885
|
+
let found = name;
|
|
1886
|
+
const has = f => f.properties && Object.prototype.hasOwnProperty.call(f.properties, name);
|
|
1887
|
+
if (!feats.slice(0, 50).some(has)) {
|
|
1888
|
+
const low = String(name).toLowerCase();
|
|
1889
|
+
for (const f of feats.slice(0, 50)) {
|
|
1890
|
+
const k = f.properties && Object.keys(f.properties).find(key => key.toLowerCase() === low);
|
|
1891
|
+
if (k) { found = k; break; }
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
return (cache[name] = found);
|
|
1895
|
+
}
|
|
1864
1896
|
// id → position of one FEATURE runtime's shapes (flat's getNodePosition,
|
|
1865
1897
|
// maptheme.js 24702-24784): its shape (flatShapeWins) at flat's shape
|
|
1866
1898
|
// center (flatShapeCenter)
|
|
1867
1899
|
function featurePositionIndex(rt) {
|
|
1868
1900
|
if (rt._positionIndex && rt._positionIndexOf === rt.features) return rt._positionIndex;
|
|
1869
|
-
const idField = rt.binding && rt.binding.id;
|
|
1901
|
+
const idField = geometryIdField(rt, rt.binding && rt.binding.id);
|
|
1870
1902
|
const best = new Map();
|
|
1871
1903
|
if (idField) {
|
|
1872
1904
|
for (const f of rt.features) {
|
|
@@ -2450,7 +2482,7 @@ self.onmessage = e => {
|
|
|
2450
2482
|
// the donor's own style (outline) — the layer registry is keyed by name,
|
|
2451
2483
|
// so the same-named CHOROPLETH replaces the donor there and can't find it
|
|
2452
2484
|
Object.defineProperty(spec, '_donorStyle', { value: geomRt.style, configurable: true });
|
|
2453
|
-
const idField = geomRt.binding.id;
|
|
2485
|
+
const idField = geometryIdField(geomRt, geomRt.binding.id);
|
|
2454
2486
|
const lookupField = spec.binding.lookup;
|
|
2455
2487
|
if (!idField || !lookupField) {
|
|
2456
2488
|
throw new Error(`[ixmaps-gl] CHOROPLETH layer "${spec.name}" needs .binding({lookup}), and its FEATURE base needs .binding({id})`);
|
|
@@ -3174,6 +3206,13 @@ self.onmessage = e => {
|
|
|
3174
3206
|
if (this._filterExpr != null) style.filter = this._filterExpr;
|
|
3175
3207
|
if (this._titleField != null) style.title = this._titleField;
|
|
3176
3208
|
if (this._lookupfield != null) style.lookupfield = this._lookupfield;
|
|
3209
|
+
// flat's .data() (htmlgui_flat.js ~1827): a geojson/topojson layer with no type of its own is
|
|
3210
|
+
// a FEATURES base layer ("FEATURES|NOLEGEND", lookup on its geometry) — the base a CHOROPLETH
|
|
3211
|
+
// of the same name needs. Only when neither .type() nor style.type says otherwise
|
|
3212
|
+
if (!style.type && this._data && /geojson|topojson/i.test(String(this._data.type || ''))) {
|
|
3213
|
+
style.type = 'FEATURES|NOLEGEND';
|
|
3214
|
+
if (style.lookupfield == null) style.lookupfield = 'geometry';
|
|
3215
|
+
}
|
|
3177
3216
|
let data = this._data;
|
|
3178
3217
|
if (this._query != null || this._process != null) {
|
|
3179
3218
|
data = Object.assign({}, this._data);
|
|
@@ -3237,7 +3276,7 @@ self.onmessage = e => {
|
|
|
3237
3276
|
// edit it by hand; `npm run unit` fails when it drifts.
|
|
3238
3277
|
// ---------------------------------------------------------------
|
|
3239
3278
|
// <grammar:binding-aliases>
|
|
3240
|
-
// generated from ixmaps-grammar 0.1.
|
|
3279
|
+
// generated from ixmaps-grammar 0.1.30 (ixmaps-flat 1.0.42, 2b978d5 2026-10-02) — 52 aliases
|
|
3241
3280
|
const FLAT_BINDING_ALIASES = {
|
|
3242
3281
|
"aggregation": "style.aggregationfield",
|
|
3243
3282
|
"aggregationfield": "style.aggregationfield",
|
|
@@ -3299,7 +3338,7 @@ self.onmessage = e => {
|
|
|
3299
3338
|
// both — so .style({tooltip}) is a tooltip and .meta({fillopacity}) a style
|
|
3300
3339
|
// property. GENERATED by test/sync-grammar.mjs — do not edit.
|
|
3301
3340
|
// <grammar:meta-keys>
|
|
3302
|
-
// generated from ixmaps-grammar 0.1.
|
|
3341
|
+
// generated from ixmaps-grammar 0.1.30 — 5 meta keys
|
|
3303
3342
|
const FLAT_META_KEYS = ["description","name","snippet","title","tooltip"];
|
|
3304
3343
|
// </grammar:meta-keys>
|
|
3305
3344
|
|
|
@@ -3541,7 +3580,7 @@ self.onmessage = e => {
|
|
|
3541
3580
|
'shadowblur', 'shadowdx', 'shadowdy', 'maxshadow', 'offsetx', 'offsety', 'gridx', 'boxmargin', 'borderwidth',
|
|
3542
3581
|
'borderradius', 'maxcharts', 'archeight', 'alignmargin',
|
|
3543
3582
|
// HEATMAP / MLHEATMAP / KDE (a project JSON writes them as strings)
|
|
3544
|
-
'radiuspx', 'intensity', 'threshold', 'weightmax', 'bandwidth'];
|
|
3583
|
+
'radiuspx', 'intensity', 'threshold', 'weightmax', 'bandwidth', 'bandwidthscale', 'bandwidthzoompow', 'bandwidthzoomref'];
|
|
3545
3584
|
function toNumberIfNumeric(v) {
|
|
3546
3585
|
if (typeof v !== 'string' || !v.trim()) return v;
|
|
3547
3586
|
const n = Number(v);
|
|
@@ -3925,9 +3964,23 @@ self.onmessage = e => {
|
|
|
3925
3964
|
if (t === entry.name || (entry.aliases || []).includes(t)) return entry;
|
|
3926
3965
|
}
|
|
3927
3966
|
}
|
|
3967
|
+
// keyless vector styles by NAME (exact match, before the dark/light rule): flat's own VT_VOYAGER names
|
|
3968
|
+
// resolve to the CARTO Voyager style they are made from; the rest are further keyless MapLibre styles
|
|
3969
|
+
const KEYLESS_VECTOR_BASEMAPS = [
|
|
3970
|
+
{ name: 'CARTO - Voyager', aliases: ['VT_VOYAGER'], url: 'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json' },
|
|
3971
|
+
{ name: 'CARTO - Voyager (no labels)', aliases: ['VT_VOYAGER_LIGHT'], url: 'https://basemaps.cartocdn.com/gl/voyager-nolabels-gl-style/style.json' },
|
|
3972
|
+
{ name: 'CARTO - Positron (no labels)', url: 'https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json' },
|
|
3973
|
+
{ name: 'CARTO - Dark Matter (no labels)', url: 'https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json' },
|
|
3974
|
+
{ name: 'OpenFreeMap - Liberty', url: 'https://tiles.openfreemap.org/styles/liberty' },
|
|
3975
|
+
{ name: 'OpenFreeMap - Bright', url: 'https://tiles.openfreemap.org/styles/bright' },
|
|
3976
|
+
{ name: 'OpenFreeMap - Positron', url: 'https://tiles.openfreemap.org/styles/positron' },
|
|
3977
|
+
];
|
|
3928
3978
|
function resolveBasemapStyleUrl(mapType) {
|
|
3929
3979
|
const raster = flatRasterBasemap(mapType);
|
|
3930
3980
|
if (raster) return buildRasterBasemapStyle(raster);
|
|
3981
|
+
const typeName = typeof mapType === 'string' ? mapType.trim() : '';
|
|
3982
|
+
const vector = KEYLESS_VECTOR_BASEMAPS.find(e => e.name === typeName || (e.aliases || []).includes(typeName));
|
|
3983
|
+
if (vector) return vector.url;
|
|
3931
3984
|
return typeof mapType === 'string' && /dark|black|night|matter/i.test(mapType) ? BASEMAP_STYLE_URLS.dark : BASEMAP_STYLE_URLS.light;
|
|
3932
3985
|
}
|
|
3933
3986
|
// Real ixmaps-flat's legend look (tools/legend.js 3773-3845, via
|
|
@@ -4414,18 +4467,34 @@ self.onmessage = e => {
|
|
|
4414
4467
|
}
|
|
4415
4468
|
// flat's click-to-action hook (ui/js/tools/tooltip_to_action.js): ixmaps.onItemAction(szId, item, themeObj) on a
|
|
4416
4469
|
// click on an item — item.szSelectionId is the value of the theme's selection field — and onItemAction(null, null)
|
|
4417
|
-
// on a click on the map background
|
|
4418
|
-
|
|
4470
|
+
// on a click on the map background — gl adds the click position as third argument there:
|
|
4471
|
+
// onItemAction(null, null, { lngLat: {lng, lat}, point: {x, y} }) (flat passes none)
|
|
4472
|
+
function clickPosition(info) {
|
|
4473
|
+
const c = info && info.coordinate;
|
|
4474
|
+
if (!c || !isFinite(c[0]) || !isFinite(c[1])) return undefined;
|
|
4475
|
+
return { lngLat: { lng: c[0], lat: c[1] }, point: { x: info.x, y: info.y } };
|
|
4476
|
+
}
|
|
4477
|
+
function callItemAction(rt, object, info) {
|
|
4419
4478
|
const hook = pageIxmaps() && pageIxmaps().onItemAction;
|
|
4420
|
-
if (typeof hook !== 'function') return;
|
|
4421
4479
|
try {
|
|
4422
|
-
|
|
4480
|
+
const pos = clickPosition(info);
|
|
4481
|
+
if (!rt) { if (typeof hook === 'function') hook.call(global.ixmaps, null, null, pos); return; }
|
|
4423
4482
|
const field = selectionFieldOf(rt);
|
|
4424
4483
|
const props = object.properties || {};
|
|
4425
4484
|
// a single record, or an aggregated item's first one
|
|
4426
4485
|
const raw = props.raw || props.firstRaw || props.titleRaw || (props.raws && props.raws[0]) || props;
|
|
4427
4486
|
const value = field && raw ? raw[field] : null;
|
|
4428
|
-
|
|
4487
|
+
// the map's "click" event, for EVERY item as in flat (szId = theme::itemKey): the key is the selection
|
|
4488
|
+
// field's value, else the feature's id, else its position "lng,lat" — a theme bound by geometry only
|
|
4489
|
+
// (no id/lookup field) has no selection value. gl adds the record and the click position.
|
|
4490
|
+
if (builder.__emitItemClick) {
|
|
4491
|
+
const g = object.geometry, a = anchorCoordOf(g) || (g && Array.isArray(g.coordinates) && g.coordinates[0]);
|
|
4492
|
+
const key = value != null ? String(value)
|
|
4493
|
+
: (object.id != null ? String(object.id) : (a && typeof a[0] === 'number' ? a[0] + ',' + a[1] : ''));
|
|
4494
|
+
builder.__emitItemClick(Object.assign({ szId: themeIdOf(rt) + '::' + key, id: key, theme: themeIdOf(rt), record: raw }, pos));
|
|
4495
|
+
}
|
|
4496
|
+
// flat's onItemAction needs the theme's selection value
|
|
4497
|
+
if (value == null || typeof hook !== 'function') return;
|
|
4429
4498
|
hook.call(global.ixmaps, themeIdOf(rt) + '::' + value, { szSelectionId: String(value) }, getThemeObj(themeIdOf(rt)));
|
|
4430
4499
|
} catch (err) { console.error('[ixmaps-gl] onItemAction failed:', err); }
|
|
4431
4500
|
}
|
|
@@ -4434,7 +4503,7 @@ self.onmessage = e => {
|
|
|
4434
4503
|
if (info && info.object && info.layer) {
|
|
4435
4504
|
const rt = findRuntimeForLayerId(info.layer.id);
|
|
4436
4505
|
if (rt) {
|
|
4437
|
-
callItemAction(rt, info.object);
|
|
4506
|
+
callItemAction(rt, info.object, info);
|
|
4438
4507
|
// Only a Point geometry's own coordinates ARE an
|
|
4439
4508
|
// [lng,lat] pair (every point/bubble/dot/chart-cluster
|
|
4440
4509
|
// layer) — a CHOROPLETH polygon's geometry.coordinates is
|
|
@@ -4465,7 +4534,7 @@ self.onmessage = e => {
|
|
|
4465
4534
|
}
|
|
4466
4535
|
}
|
|
4467
4536
|
// clicked empty map space (or a non-tooltippable layer) -> unpin
|
|
4468
|
-
callItemAction(null, null);
|
|
4537
|
+
callItemAction(null, null, picked);
|
|
4469
4538
|
pinned = null;
|
|
4470
4539
|
pinnedTooltipEl.style.display = 'none';
|
|
4471
4540
|
return false;
|
|
@@ -5472,6 +5541,23 @@ self.onmessage = e => {
|
|
|
5472
5541
|
return { addLegendPanel, setLegendOption, updateSubTheme, layoutLegends };
|
|
5473
5542
|
}
|
|
5474
5543
|
|
|
5544
|
+
// flat's map event names (htmlgui_flat.js mapApi.on, with its aliases) and the ones this engine fires:
|
|
5545
|
+
// viewchange (every view change end), zoomend / moveend ({ nZoom, zoomChanged, panChanged }, as flat),
|
|
5546
|
+
// ready (the map has loaded), click (an item: { szId, id, theme } + lngLat/point), layerdraw ({ id }) and,
|
|
5547
|
+
// as a gl addition, mapclick (every click on the map, background included: { lngLat, point })
|
|
5548
|
+
const MAP_EVENT_ALIAS = { zoompan: 'viewchange', itemover: 'mouseover', itemclick: 'click', itemout: 'mouseout', mapready: 'ready',
|
|
5549
|
+
drawtheme: 'layerdraw', newtheme: 'layeradd', removetheme: 'layerremove' };
|
|
5550
|
+
const MAP_EVENTS_WIRED = new Set(['viewchange', 'zoomend', 'moveend', 'ready', 'click', 'mapclick', 'layerdraw']);
|
|
5551
|
+
function splitMapEvents(events) {
|
|
5552
|
+
return String(events == null ? '' : events).trim().toLowerCase().split(/\s+/).filter(Boolean).map(e => MAP_EVENT_ALIAS[e] || e);
|
|
5553
|
+
}
|
|
5554
|
+
const _warnedMapEvents = new Set();
|
|
5555
|
+
function warnUnwiredMapEvent(ev) {
|
|
5556
|
+
if (MAP_EVENTS_WIRED.has(ev) || _warnedMapEvents.has(ev)) return;
|
|
5557
|
+
_warnedMapEvents.add(ev);
|
|
5558
|
+
console.warn(`[ixmaps-gl] map event "${ev}" is not implemented — the handler is accepted but never fires (supported: ${[...MAP_EVENTS_WIRED].join(', ')})`);
|
|
5559
|
+
}
|
|
5560
|
+
|
|
5475
5561
|
class MapBuilder {
|
|
5476
5562
|
constructor(containerId, options) {
|
|
5477
5563
|
this.containerId = containerId;
|
|
@@ -5536,15 +5622,21 @@ self.onmessage = e => {
|
|
|
5536
5622
|
}
|
|
5537
5623
|
// .Layer() — alias of .layer()
|
|
5538
5624
|
Layer(nameOrBuilder) { return this.layer(nameOrBuilder); }
|
|
5539
|
-
//
|
|
5540
|
-
//
|
|
5541
|
-
//
|
|
5542
|
-
// OTHER event name is accepted and stored but never fired —
|
|
5543
|
-
// recognized rather than throwing, the same "known but inert" pattern
|
|
5544
|
-
// KNOWN_INERT_FLAGS already uses for unimplemented type flags.
|
|
5625
|
+
// flat's mapApi.on(events, cb) (htmlgui_flat.js 1631), "a b" for several names; see MAP_EVENTS for the
|
|
5626
|
+
// ones wired here. Any other name is accepted (never throws) but never fires, and says so once on the console.
|
|
5627
|
+
// Registered before the map is built, they are attached by build().
|
|
5545
5628
|
on(event, cb) {
|
|
5546
5629
|
this._pendingEvents = this._pendingEvents || [];
|
|
5547
|
-
|
|
5630
|
+
for (const ev of splitMapEvents(event)) {
|
|
5631
|
+
warnUnwiredMapEvent(ev);
|
|
5632
|
+
this._pendingEvents.push([ev, cb]);
|
|
5633
|
+
}
|
|
5634
|
+
return this;
|
|
5635
|
+
}
|
|
5636
|
+
// flat's mapApi.off(events, cb), before the map is built (after it: the handle's off)
|
|
5637
|
+
off(event, cb) {
|
|
5638
|
+
const names = new Set(splitMapEvents(event));
|
|
5639
|
+
this._pendingEvents = (this._pendingEvents || []).filter(([ev, f]) => !(names.has(ev) && f === cb));
|
|
5548
5640
|
return this;
|
|
5549
5641
|
}
|
|
5550
5642
|
|
|
@@ -5966,20 +6058,29 @@ self.onmessage = e => {
|
|
|
5966
6058
|
map.on('style.load', refreshControlLook);
|
|
5967
6059
|
this._setAttribution = text => attributionBox.set(text);
|
|
5968
6060
|
|
|
5969
|
-
|
|
6061
|
+
// the page legend (map.legend(html)), or the legend a loaded project
|
|
6062
|
+
// carries in its map part (applyProjectMap) — flat shows the project's
|
|
6063
|
+
// legend in the same pane, so the project's replaces the page's
|
|
6064
|
+
let pageLegendEl = null;
|
|
6065
|
+
const showPageLegend = html => {
|
|
6066
|
+
if (pageLegendEl) { pageLegendEl.remove(); pageLegendEl = null; }
|
|
6067
|
+
this._legendHtml = html;
|
|
6068
|
+
if (!html) return;
|
|
5970
6069
|
const legendEl = document.createElement('div');
|
|
5971
6070
|
// a page legend (map.legend(html)) is positioned by the map's
|
|
5972
6071
|
// own align option — the same rule that places the native stack
|
|
5973
6072
|
legendEl.style.cssText = 'position:absolute;' + legendAlignCssFor(this.mapOptions) + 'z-index:5;pointer-events:none';
|
|
5974
6073
|
// flat's legend takes the pane's html OR a URL to it (htmlgui):
|
|
5975
6074
|
// a value without a tag is fetched, its html shown
|
|
5976
|
-
if (/<[a-z!]/i.test(
|
|
5977
|
-
else fetch(
|
|
6075
|
+
if (/<[a-z!]/i.test(html)) legendEl.innerHTML = html;
|
|
6076
|
+
else fetch(html).then(r => r.ok ? r.text() : '').then(t => { legendEl.innerHTML = t; }).catch(() => {});
|
|
5978
6077
|
// inside the map container, never the page's body (see the pinned
|
|
5979
6078
|
// tooltip mount) — styling body breaks page-positioned elements
|
|
5980
6079
|
anchorInside(el);
|
|
5981
6080
|
el.appendChild(legendEl);
|
|
5982
|
-
|
|
6081
|
+
pageLegendEl = legendEl;
|
|
6082
|
+
};
|
|
6083
|
+
if (this._legendHtml) showPageLegend(this._legendHtml);
|
|
5983
6084
|
|
|
5984
6085
|
// themes are addressed by either the layer()'s own name (e.g. "AREU")
|
|
5985
6086
|
// or style.name (e.g. "chart") — the real engine's changeThemeStyle
|
|
@@ -6865,6 +6966,12 @@ self.onmessage = e => {
|
|
|
6865
6966
|
Object.assign(builder._engineOptions, o);
|
|
6866
6967
|
}
|
|
6867
6968
|
if (m.scaleParam && m.scaleParam.normalSizeScale != null) builder._engineOptions.normalSizeScale = m.scaleParam.normalSizeScale;
|
|
6969
|
+
// the project's own legend (map.legend: html or a URL to it) replaces the page's, as in flat;
|
|
6970
|
+
// like a page legend it takes the place of the theme-generated legend panels
|
|
6971
|
+
if (typeof m.legend === 'string' && m.legend.trim()) {
|
|
6972
|
+
showPageLegend(m.legend);
|
|
6973
|
+
setLegendOption(builder.mapOptions.legend);
|
|
6974
|
+
}
|
|
6868
6975
|
if (m.basemap) report.notes.push(`basemap "${m.basemap}" not switched (ixmaps-gl keeps the map's own basemap)`);
|
|
6869
6976
|
}
|
|
6870
6977
|
|
|
@@ -7208,8 +7315,61 @@ self.onmessage = e => {
|
|
|
7208
7315
|
}
|
|
7209
7316
|
map.on('moveend', loadDeferredThemes);
|
|
7210
7317
|
|
|
7318
|
+
// map events (flat's mapApi.on): see MAP_EVENTS_WIRED. "layerdraw" is approximated below.
|
|
7319
|
+
const mapListeners = Object.create(null);
|
|
7320
|
+
const drawHooks = new Map(); // layerdraw handler -> its onRedraw unsubscribe functions
|
|
7321
|
+
const mapName = (this.mapOptions && this.mapOptions.name) || null; // flat's szMap
|
|
7322
|
+
let mapReady = false;
|
|
7323
|
+
const emitMapEvent = (type, detail) => (mapListeners[type] || []).slice().forEach(cb => {
|
|
7324
|
+
try { cb(Object.assign({ type, szMap: mapName }, detail)); } catch (err) { console.error(`[ixmaps-gl] "${type}" callback failed:`, err); }
|
|
7325
|
+
});
|
|
7326
|
+
engineApi.on = (events, cb) => {
|
|
7327
|
+
if (typeof cb !== 'function') return engineApi;
|
|
7328
|
+
for (const ev of splitMapEvents(events)) {
|
|
7329
|
+
warnUnwiredMapEvent(ev);
|
|
7330
|
+
if (ev === 'layerdraw') {
|
|
7331
|
+
// flat fires "layerdraw" once per theme as it finishes drawing; here onRedraw fires once per refresh of
|
|
7332
|
+
// the whole map, so the callback is called once per current theme every time — a page that re-reads
|
|
7333
|
+
// that theme's state (getThemeObj/getFacets) gets fresh numbers after every redraw
|
|
7334
|
+
const off = engineApi.onRedraw(() => runtimes.forEach(rt => {
|
|
7335
|
+
const id = themeIdOf(rt);
|
|
7336
|
+
try { cb({ type: 'layerdraw', szId: id, id, szMap: mapName }); } catch (err) { console.error('[ixmaps-gl] "layerdraw" callback failed:', err); }
|
|
7337
|
+
}));
|
|
7338
|
+
drawHooks.set(cb, (drawHooks.get(cb) || []).concat(off));
|
|
7339
|
+
} else {
|
|
7340
|
+
(mapListeners[ev] = mapListeners[ev] || []).push(cb);
|
|
7341
|
+
if (ev === 'ready' && mapReady) setTimeout(() => { try { cb({ type: 'ready', szMap: mapName }); } catch (err) { console.error('[ixmaps-gl] "ready" callback failed:', err); } }, 0);
|
|
7342
|
+
}
|
|
7343
|
+
}
|
|
7344
|
+
return engineApi;
|
|
7345
|
+
};
|
|
7346
|
+
// flat's mapApi.off(events, cb): removes the handler(s) given to on()
|
|
7347
|
+
engineApi.off = (events, cb) => {
|
|
7348
|
+
if (typeof cb !== 'function') return engineApi;
|
|
7349
|
+
for (const ev of splitMapEvents(events)) {
|
|
7350
|
+
if (ev === 'layerdraw') { (drawHooks.get(cb) || []).forEach(off => off()); drawHooks.delete(cb); continue; }
|
|
7351
|
+
const list = mapListeners[ev];
|
|
7352
|
+
if (list) mapListeners[ev] = list.filter(f => f !== cb);
|
|
7353
|
+
}
|
|
7354
|
+
return engineApi;
|
|
7355
|
+
};
|
|
7356
|
+
let lastViewZoom = map.getZoom(), lastViewCenter = map.getCenter();
|
|
7357
|
+
map.on('moveend', () => {
|
|
7358
|
+
const z = map.getZoom(), c = map.getCenter();
|
|
7359
|
+
const zoomChanged = z !== lastViewZoom, panChanged = c.lng !== lastViewCenter.lng || c.lat !== lastViewCenter.lat;
|
|
7360
|
+
lastViewZoom = z; lastViewCenter = c;
|
|
7361
|
+
const detail = { nZoom: flatViewZoomOf(map), zoomChanged, panChanged };
|
|
7362
|
+
emitMapEvent('viewchange', detail);
|
|
7363
|
+
if (zoomChanged) emitMapEvent('zoomend', detail);
|
|
7364
|
+
if (panChanged && !zoomChanged) emitMapEvent('moveend', detail);
|
|
7365
|
+
});
|
|
7366
|
+
map.on('click', e => emitMapEvent('mapclick', { lngLat: { lng: e.lngLat.lng, lat: e.lngLat.lat }, point: { x: e.point.x, y: e.point.y } }));
|
|
7367
|
+
this.__emitItemClick = detail => emitMapEvent('click', detail);
|
|
7368
|
+
|
|
7211
7369
|
map.on('load', () => {
|
|
7212
7370
|
mapHasLoaded = true;
|
|
7371
|
+
mapReady = true;
|
|
7372
|
+
emitMapEvent('ready');
|
|
7213
7373
|
loadDeferredThemes();
|
|
7214
7374
|
refresh();
|
|
7215
7375
|
runtimes.forEach(maybeZoomToTheme);
|
|
@@ -7275,18 +7435,7 @@ self.onmessage = e => {
|
|
|
7275
7435
|
// gets the same practical effect: fresh numbers after every redraw,
|
|
7276
7436
|
// for every theme, without needing the page rewritten around a
|
|
7277
7437
|
// finer-grained event this engine doesn't produce.
|
|
7278
|
-
|
|
7279
|
-
.filter(([event]) => event === 'layerdraw')
|
|
7280
|
-
.map(([, cb]) => cb);
|
|
7281
|
-
if (layerdrawCbs.length) {
|
|
7282
|
-
engineApi.onRedraw(() => {
|
|
7283
|
-
runtimes.forEach(rt => {
|
|
7284
|
-
layerdrawCbs.forEach(cb => {
|
|
7285
|
-
try { cb({ id: rt.name }); } catch (err) { console.error('[ixmaps-gl] "layerdraw" callback failed:', err); }
|
|
7286
|
-
});
|
|
7287
|
-
});
|
|
7288
|
-
});
|
|
7289
|
-
}
|
|
7438
|
+
(this._pendingEvents || []).forEach(([event, cb]) => engineApi.on(event, cb));
|
|
7290
7439
|
|
|
7291
7440
|
// the native legend: createLegend (its four hooks are what the rest
|
|
7292
7441
|
// of the map calls — panels, the legend option, sub-themes, layout)
|
|
@@ -7470,7 +7619,10 @@ self.onmessage = e => {
|
|
|
7470
7619
|
promise.legend = (...args) => { builder.legend(...args); return promise; };
|
|
7471
7620
|
promise.local = (...args) => { builder.local(...args); return promise; };
|
|
7472
7621
|
promise.require = (url) => { if (resolvedApi) resolvedApi.require(url); else builder.require(url); return promise; };
|
|
7473
|
-
promise.on = (...args) => { builder.on(...args); return promise; };
|
|
7622
|
+
promise.on = (...args) => { if (resolvedApi) resolvedApi.on(...args); else builder.on(...args); return promise; };
|
|
7623
|
+
promise.off = (...args) => { if (resolvedApi) resolvedApi.off(...args); else builder.off(...args); return promise; };
|
|
7624
|
+
promise.getBounds = () => getBounds();
|
|
7625
|
+
promise.getMapLibre = () => (resolvedApi ? resolvedApi.map : undefined);
|
|
7474
7626
|
promise.layer = (nameOrBuilder) => {
|
|
7475
7627
|
if (typeof nameOrBuilder !== 'string') {
|
|
7476
7628
|
// object form (a pre-built LayerBuilder, e.g. from the global
|
|
@@ -11913,7 +12065,19 @@ self.onmessage = e => {
|
|
|
11913
12065
|
// flat's nValueSum is the RAW weighted sum, whatever the flags.
|
|
11914
12066
|
// (RELATIVE/INVERT subtract after the multiply — not reconstructable
|
|
11915
12067
|
// from the parts; flat's own composed branch never pairs them.)
|
|
11916
|
-
|
|
12068
|
+
// With binding.alpha the opacity follows the alpha field instead of the value sum — flat's
|
|
12069
|
+
// `if (this.szAlphaField)` branch of the same DOPACITY block (maptheme.js 13476-13482):
|
|
12070
|
+
// dopacityscale / nAlphaMax^(1/pow) · nAlpha^(1/pow), nAlpha as _prepareAlphaField made it
|
|
12071
|
+
// (alpha100 "$density$": alpha / the shape's area, the FEATURE base's size field when it binds one);
|
|
12072
|
+
// a shape with no alpha is 0. Not capped at 0.9 and not times fillopacity, as flat
|
|
12073
|
+
_composedDopacityAlpha(props, feature) {
|
|
12074
|
+
if (this.binding && this.binding.alpha) {
|
|
12075
|
+
const nAlpha = this._alphaByFeature && feature ? this._alphaByFeature.get(feature) : undefined;
|
|
12076
|
+
if (nAlpha == null) return 0;
|
|
12077
|
+
const pow = 1 / (styleNum(this.style.dopacitypow) || 1);
|
|
12078
|
+
const o = (styleNum(this.style.dopacityscale) || 1) / Math.pow(this._alphaMax || 1, pow) * Math.pow(nAlpha, pow);
|
|
12079
|
+
return isFinite(o) ? Math.max(0, Math.min(1, o)) : 0;
|
|
12080
|
+
}
|
|
11917
12081
|
const fields = this._multiFields || [];
|
|
11918
12082
|
const f100A = this.binding && this.binding.field100 ? String(this.binding.field100).split('|') : null;
|
|
11919
12083
|
const multiplier = this.flags.has('FRACTION') ? (styleNum(this.style.fractionscale) || 1)
|
|
@@ -12932,7 +13096,10 @@ self.onmessage = e => {
|
|
|
12932
13096
|
const labels = this.categoryDisplayLabels || this.categoryLabels || [];
|
|
12933
13097
|
return tooltipTable(props.parts.map((v, i) => (v > 0 ? { rgb: this.categoryColorsRgb[i], label: labels[i] || '(n/d)', value: this._formatTooltipValue(v) + unitOf } : null)).filter(Boolean));
|
|
12934
13098
|
}
|
|
12935
|
-
|
|
13099
|
+
// (a USER chart over several value fields — the pinnacle peaks of a COMPOSECOLOR theme — lists its fields
|
|
13100
|
+
// the same way: its single item row would show the first class with the size total)
|
|
13101
|
+
const multiFieldUser = this.flags.has('USER') && String(this.binding.value || '').includes('|');
|
|
13102
|
+
if ((this.flags.has('CHOROPLETH') || multiFieldUser) && (this.flags.has('DOMINANT') || this.flags.has('COMPOSECOLOR')) && props.raw) {
|
|
12936
13103
|
return this._renderMultiFieldBarsHtml(props.raw);
|
|
12937
13104
|
}
|
|
12938
13105
|
const unit = unitOf;
|
|
@@ -13148,7 +13315,7 @@ self.onmessage = e => {
|
|
|
13148
13315
|
// early return here must also unmount its groups, or the pane keeps
|
|
13149
13316
|
// showing the last drawing after the theme went out of scale (or was
|
|
13150
13317
|
// hidden via setThemeVisible)
|
|
13151
|
-
if (this._hidden) { this._clearUserChartPane(); return []; }
|
|
13318
|
+
if (this._hidden) { this._kdeShowGate = true; this._clearUserChartPane(); return []; }
|
|
13152
13319
|
if (!this.flags.has('FEATURE') && !this.flags.has('FEATURES') && chartHiddenByScale(this.style, liveZoom)) { this._clearUserChartPane(); return []; }
|
|
13153
13320
|
// DOT is checked FIRST, matching the real engine's own precedence
|
|
13154
13321
|
// (its DOT branch is a loop-level fast-path that bypasses the whole
|
|
@@ -13451,7 +13618,7 @@ self.onmessage = e => {
|
|
|
13451
13618
|
// sum over (field100/100), against the same nMax the color
|
|
13452
13619
|
// blend normalizes by. The raw weighted sum is reconstructed
|
|
13453
13620
|
// from the fractioned part values (raw = part × its field100)
|
|
13454
|
-
dopacityAlpha: dopacityActive ? this._composedDopacityAlpha(f.properties) : null
|
|
13621
|
+
dopacityAlpha: dopacityActive ? this._composedDopacityAlpha(f.properties, f) : null
|
|
13455
13622
|
}
|
|
13456
13623
|
};
|
|
13457
13624
|
}
|
|
@@ -14468,8 +14635,9 @@ self.onmessage = e => {
|
|
|
14468
14635
|
// drawn now (fresh), else a cached chart's: the previous drawing's copy
|
|
14469
14636
|
const def = this._userChartGroup.querySelector('#' + CSS.escape(id)) || this._userChartGroup.ownerDocument.getElementById(id);
|
|
14470
14637
|
if (def) paneDefs.appendChild(def.cloneNode(true));
|
|
14638
|
+
else if (chart.defsById && chart.defsById[id]) paneDefs.insertAdjacentHTML('beforeend', chart.defsById[id]);
|
|
14471
14639
|
}
|
|
14472
|
-
items.push({ d, coords: d.geometry.coordinates, ox: chart.ox, oy: chart.oy, s: pxPerUnit,
|
|
14640
|
+
items.push({ d, coords: d.geometry.coordinates, ox: chart.ox, oy: chart.oy, s: pxPerUnit, pick: chart.pick,
|
|
14473
14641
|
r: Math.max(chart.vw, chart.vh) * pxPerUnit / 2,
|
|
14474
14642
|
// the chart's measured box (flat's getBox of the chart group) in px
|
|
14475
14643
|
// around the map position: the pane draws a chart point at
|
|
@@ -14512,14 +14680,31 @@ self.onmessage = e => {
|
|
|
14512
14680
|
positionOf: b => b.d.geometry.coordinates, offsetOf: () => [0, 0],
|
|
14513
14681
|
ids: { circle: `ix-user-box-${this.name}`, icon: `ix-user-box-${this.name}`, polygons: `ix-user-box-polygons-${this.name}`, titles: `ix-user-titles-${this.name}` }
|
|
14514
14682
|
}) : { box: [], titles: [] };
|
|
14515
|
-
|
|
14683
|
+
// each chart answers to its own outline: the polygons sampled from its drawing (else its box), in px
|
|
14684
|
+
// around the map position, here as lng/lat at this zoom (a px is 360/(512·2^z) degrees across,
|
|
14685
|
+
// times cos(lat) up); a few px of transparent stroke keep a thin peak easy to hit
|
|
14686
|
+
const degPerPx = 360 / (512 * Math.pow(2, liveZoom));
|
|
14687
|
+
const pickFeatures = items.map(it => {
|
|
14688
|
+
const [lng, lat] = it.coords;
|
|
14689
|
+
const kx = it.s * degPerPx, ky = it.s * degPerPx * Math.cos(lat * Math.PI / 180);
|
|
14690
|
+
const rings = it.pick && it.pick.length ? it.pick
|
|
14691
|
+
: [[[it.ext[0] / it.s, it.ext[1] / it.s], [it.ext[2] / it.s, it.ext[1] / it.s], [it.ext[2] / it.s, it.ext[3] / it.s], [it.ext[0] / it.s, it.ext[3] / it.s]]];
|
|
14692
|
+
const polys = rings.map(r => {
|
|
14693
|
+
const ring = r.map(([x, y]) => [lng + x * kx, lat - y * ky]);
|
|
14694
|
+
ring.push(ring[0]);
|
|
14695
|
+
return [ring];
|
|
14696
|
+
});
|
|
14697
|
+
// icon + d: what pickedFeature unwraps to the item
|
|
14698
|
+
return { type: 'Feature', geometry: { type: 'MultiPolygon', coordinates: polys }, properties: {}, icon: true, d: it.d };
|
|
14699
|
+
});
|
|
14700
|
+
this._userChartLastLayers = [...boxLayers.box, new GeoJsonLayer({
|
|
14516
14701
|
id: `ix-bubbles-${this.name}`,
|
|
14517
|
-
data:
|
|
14702
|
+
data: { type: 'FeatureCollection', features: pickFeatures },
|
|
14518
14703
|
pickable: true,
|
|
14519
|
-
|
|
14520
|
-
|
|
14521
|
-
|
|
14522
|
-
|
|
14704
|
+
filled: true, stroked: true,
|
|
14705
|
+
// alpha 1/255: invisible, but never a fully transparent fragment a renderer might skip
|
|
14706
|
+
getFillColor: [0, 0, 0, 1], getLineColor: [0, 0, 0, 1],
|
|
14707
|
+
lineWidthUnits: 'pixels', getLineWidth: 4,
|
|
14523
14708
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14524
14709
|
}), ...boxLayers.titles];
|
|
14525
14710
|
return this._userChartLastLayers;
|
|
@@ -14532,11 +14717,40 @@ self.onmessage = e => {
|
|
|
14532
14717
|
const raw = d.properties.raw || d.properties;
|
|
14533
14718
|
const titleField = this.style.titlefield || this.binding.title;
|
|
14534
14719
|
const title = titleField && raw && raw[titleField] != null ? String(raw[titleField]) : undefined;
|
|
14535
|
-
|
|
14720
|
+
// flat hands the chart the item's value fields: values = every field of binding.value (after the
|
|
14721
|
+
// field100 arithmetic — the record already carries it) and value = the first of them
|
|
14722
|
+
// (maptheme.js 19667, nValuesA[0]); a theme with one value field keeps the item's value
|
|
14723
|
+
let values = [value];
|
|
14724
|
+
const fieldNames = this.binding.value && !d.properties.counts ? String(this.binding.value).split('|') : [];
|
|
14725
|
+
if (fieldNames.length > 1) {
|
|
14726
|
+
const parts = fieldNames.map(f => scanValue(raw && raw[f[0] === '!' ? f.slice(1) : f]));
|
|
14727
|
+
if (parts.some(v => !isNaN(v))) {
|
|
14728
|
+
values = parts.map(v => (isNaN(v) ? 0 : v));
|
|
14729
|
+
// COMPOSECOLOR blends all the fields into one colour, so the chart's value is the one that wins it —
|
|
14730
|
+
// the highest field (flat itself would hand over the first field here: a deliberate departure,
|
|
14731
|
+
// so a "party with the highest result" map prints that party's share)
|
|
14732
|
+
value = this.flags.has('COMPOSECOLOR') ? Math.max(...values) : values[0];
|
|
14733
|
+
}
|
|
14734
|
+
}
|
|
14735
|
+
// DOMINANT: the chart is the dominant field's — its class colour and its value (flat's USER branch,
|
|
14736
|
+
// maptheme.js ~19700-19770: nClass = the dominant index, nValue = nValuesA[nIndex]); the relevance
|
|
14737
|
+
// (plain value, PERCENTOFMEAN, DEVIATION) is the one the DOMINANT choropleth uses
|
|
14738
|
+
if (this.flags.has('DOMINANT') && !this.flags.has('COMPOSECOLOR') && this._multiFields && this._dominantMins && raw && fieldNames.length > 1) {
|
|
14739
|
+
const dominant = this._resolveDominantClass(raw);
|
|
14740
|
+
if (dominant) { value = dominant.value; cat = dominant.index; }
|
|
14741
|
+
}
|
|
14742
|
+
// COMPOSECOLOR: the chart takes the item's blended colour (flat's itemA[a].szColor, the same
|
|
14743
|
+
// blend the choropleth uses) instead of one class colour
|
|
14744
|
+
let composed;
|
|
14745
|
+
if (this.flags.has('COMPOSECOLOR') && this._multiFields && raw) {
|
|
14746
|
+
const c = this._resolveComposedColor(raw);
|
|
14747
|
+
composed = `rgb(${c[0]},${c[1]},${c[2]})`;
|
|
14748
|
+
}
|
|
14749
|
+
const base = `user|${name}|${cat}|${Math.round(value * 100) / 100}|${values.length > 1 ? values.join(',') : ''}|${composed || ''}|${Math.round(size * 2) / 2}|${flag}|${title || ''}`;
|
|
14536
14750
|
if (!this._userChartSvgs) this._userChartSvgs = new Map();
|
|
14537
14751
|
let chart = this._userChartSvgs.get(base);
|
|
14538
14752
|
if (chart === undefined) {
|
|
14539
|
-
chart = this._drawUserChartSvg(draw, name, theme, flag, d, value, cat, size, nSize);
|
|
14753
|
+
chart = this._drawUserChartSvg(draw, name, theme, flag, d, value, cat, size, nSize, values, composed);
|
|
14540
14754
|
if (chart === undefined) return null;
|
|
14541
14755
|
if (this._userChartSvgs.size >= ICON_CACHE_MAX) this._userChartSvgs.delete(this._userChartSvgs.keys().next().value);
|
|
14542
14756
|
this._userChartSvgs.set(base, chart);
|
|
@@ -14546,10 +14760,10 @@ self.onmessage = e => {
|
|
|
14546
14760
|
// the page's chart function draws one chart into the hidden SVG: its
|
|
14547
14761
|
// markup and box (chart units), null when it draws nothing, undefined
|
|
14548
14762
|
// when it fails
|
|
14549
|
-
_drawUserChartSvg(draw, name, theme, flag, d, value, cat, size, nSize) {
|
|
14763
|
+
_drawUserChartSvg(draw, name, theme, flag, d, value, cat, size, nSize, values, composedColor) {
|
|
14550
14764
|
const g = document.createElementNS(SVG_NS, 'g');
|
|
14551
14765
|
this._userChartGroup.appendChild(g);
|
|
14552
|
-
const color = this._userChartColor(cat);
|
|
14766
|
+
const color = composedColor || this._userChartColor(cat);
|
|
14553
14767
|
const raw = d.properties.raw || d.properties;
|
|
14554
14768
|
const label = this.binding.title && raw && raw[this.binding.title] != null ? String(raw[this.binding.title]) : undefined;
|
|
14555
14769
|
// flat's chart title (style.titlefield, maptheme's item.szTitle) —
|
|
@@ -14559,7 +14773,7 @@ self.onmessage = e => {
|
|
|
14559
14773
|
let chart = null;
|
|
14560
14774
|
try {
|
|
14561
14775
|
const ret = draw.call(global.ixmaps, document, {
|
|
14562
|
-
target: g, theme, value, values: [value], size, maxSize: USER_CHART_MAX_SIZE, color, class: cat, flag,
|
|
14776
|
+
target: g, theme, value, values: values || [value], size, maxSize: USER_CHART_MAX_SIZE, color, class: cat, flag,
|
|
14563
14777
|
// flat hands the chart the item's color CLASS object (maptheme's
|
|
14564
14778
|
// ColorScheme entry) — pinnacleChart reads ccolor.lowColor for its
|
|
14565
14779
|
// VALUES label fill (usercharts/d3/chart.js 199)
|
|
@@ -14570,13 +14784,51 @@ self.onmessage = e => {
|
|
|
14570
14784
|
if (bb && bb.width > 0 && bb.height > 0) {
|
|
14571
14785
|
const markup = new XMLSerializer().serializeToString(g);
|
|
14572
14786
|
const ids = new Set([...markup.matchAll(/url\(#([^)"']+)\)/g)].map(m => m[1]));
|
|
14573
|
-
|
|
14787
|
+
// the gradients the chart uses, kept WITH the chart: a cached chart that was not on the screen in the
|
|
14788
|
+
// last drawing finds no copy of them in the document any more (they are one per colour, and only the
|
|
14789
|
+
// theme's own colours are defined again by _init), and would lose its fill
|
|
14790
|
+
const defsById = {};
|
|
14791
|
+
for (const id of ids) {
|
|
14792
|
+
const el = this._userChartGroup.ownerDocument.getElementById(id);
|
|
14793
|
+
if (el) defsById[id] = new XMLSerializer().serializeToString(el);
|
|
14794
|
+
}
|
|
14795
|
+
const defs = Object.values(defsById).join('');
|
|
14574
14796
|
// strokes and text halos reach past the geometric box
|
|
14575
14797
|
const pad = Math.max(bb.width, bb.height) * 0.06 + 10;
|
|
14576
14798
|
const vx = bb.x - pad, vy = bb.y - pad, vw = bb.width + 2 * pad, vh = bb.height + 2 * pad;
|
|
14577
14799
|
// the chart's origin (0,0 — or the point flat moves it to, ret) sits at the map position
|
|
14578
14800
|
const ox = (ret && ret.x) || 0, oy = (ret && ret.y) || 0;
|
|
14579
|
-
|
|
14801
|
+
// the chart's own outline for hover and click: its shapes (the peak's triangle, a pie's circle ...)
|
|
14802
|
+
// sampled into polygons, in chart units around the origin — not the box around them (a tall peak
|
|
14803
|
+
// would otherwise answer to a rectangle, or a circle, far wider than the peak itself)
|
|
14804
|
+
const pick = [];
|
|
14805
|
+
try {
|
|
14806
|
+
const inv = g.getCTM().inverse();
|
|
14807
|
+
const at = (el, x, y) => { const p = new DOMPoint(x, y).matrixTransform(inv.multiply(el.getCTM())); return [p.x - ox, p.y - oy]; };
|
|
14808
|
+
g.querySelectorAll('path,rect,circle,ellipse,polygon').forEach(el => {
|
|
14809
|
+
if (el.closest('defs,clipPath,mask')) return;
|
|
14810
|
+
let ring = null;
|
|
14811
|
+
if (el.tagName === 'path') {
|
|
14812
|
+
const len = el.getTotalLength();
|
|
14813
|
+
if (!(len > 0)) return;
|
|
14814
|
+
const n = Math.max(8, Math.min(40, Math.ceil(len / 50)));
|
|
14815
|
+
ring = [];
|
|
14816
|
+
for (let i = 0; i <= n; i++) { const q = el.getPointAtLength(len * i / n); ring.push(at(el, q.x, q.y)); }
|
|
14817
|
+
} else if (el.tagName === 'rect') {
|
|
14818
|
+
const x = el.x.baseVal.value, y = el.y.baseVal.value, w = el.width.baseVal.value, h = el.height.baseVal.value;
|
|
14819
|
+
ring = [at(el, x, y), at(el, x + w, y), at(el, x + w, y + h), at(el, x, y + h)];
|
|
14820
|
+
} else if (el.tagName === 'polygon') {
|
|
14821
|
+
ring = [...el.points].map(q => at(el, q.x, q.y));
|
|
14822
|
+
} else {
|
|
14823
|
+
const cx = el.cx.baseVal.value, cy = el.cy.baseVal.value;
|
|
14824
|
+
const rx = el.tagName === 'circle' ? el.r.baseVal.value : el.rx.baseVal.value, ry = el.tagName === 'circle' ? rx : el.ry.baseVal.value;
|
|
14825
|
+
ring = [];
|
|
14826
|
+
for (let i = 0; i < 12; i++) ring.push(at(el, cx + rx * Math.cos(i * Math.PI / 6), cy + ry * Math.sin(i * Math.PI / 6)));
|
|
14827
|
+
}
|
|
14828
|
+
if (ring && ring.length >= 3 && ring.every(q => isFinite(q[0]) && isFinite(q[1]))) pick.push(ring);
|
|
14829
|
+
});
|
|
14830
|
+
} catch (e) { /* the box below stands in */ }
|
|
14831
|
+
chart = { vx, vy, vw, vh, ox, oy, markup, ids: [...ids], defsById, pick, bb: { x: bb.x, y: bb.y, width: bb.width, height: bb.height },
|
|
14580
14832
|
svg: `<svg xmlns="${SVG_NS}" width="__W__" height="__H__" viewBox="${vx} ${vy} ${vw} ${vh}"><defs>${defs}</defs>${markup}</svg>` };
|
|
14581
14833
|
}
|
|
14582
14834
|
} catch (e) {
|
|
@@ -14852,7 +15104,8 @@ self.onmessage = e => {
|
|
|
14852
15104
|
// to kdeGrid in a worker, d3-contour cuts the bands at fractions of the maximum (style.kdelevels,
|
|
14853
15105
|
// default 0.15 / 0.30 / 0.50 / 0.70), each band the area at or above its threshold, so they
|
|
14854
15106
|
// stack, colored from style.colorscheme, drawn with style.fillopacity. The bandwidth is
|
|
14855
|
-
// style.bandwidth (km) or, without it, follows the zoom (kdeBandwidthKm).
|
|
15107
|
+
// style.bandwidth (km) or, without it, follows the zoom (kdeBandwidthKm); style.bandwidthscale
|
|
15108
|
+
// (default 1) multiplies either, style.bandwidthzoompow (default 1) flattens the zoom dependence, so a radius control can stay zoom-aware without lagging a redraw.
|
|
14856
15109
|
// The result belongs to a view: while the map moves the last bands stay, and once it stands still
|
|
14857
15110
|
// (and the view, the records, the binding or the bandwidth changed) the new ones are computed
|
|
14858
15111
|
// after a short pause and drawn by a redraw — never more than one computation wanted at a time.
|
|
@@ -14869,7 +15122,18 @@ self.onmessage = e => {
|
|
|
14869
15122
|
const source = this._activeFeatures || this.features;
|
|
14870
15123
|
if (this._kdeSource !== source) { this._kdeSource = source; this._kdeSourceId = (this._kdeSourceId || 0) + 1; }
|
|
14871
15124
|
const flatZoom = mapLibreToFlatZoom(zoom);
|
|
14872
|
-
const
|
|
15125
|
+
const bwScale = styleNum(this.style.bandwidthscale) > 0 ? styleNum(this.style.bandwidthscale) : 1;
|
|
15126
|
+
// style.bandwidthzoompow (0..1) flattens the zoom dependence: the radius is the table's ratio to its
|
|
15127
|
+
// value at the reference flat zoom (style.bandwidthzoomref, default 12), raised to this power
|
|
15128
|
+
// (0 = constant). Without style.bandwidth the radius is the table's, default power 1 (as the fire
|
|
15129
|
+
// pages); WITH style.bandwidth (km) that is the radius AT the reference zoom, default power 0 (fixed).
|
|
15130
|
+
const explicitKm = styleNum(this.style.bandwidth) > 0 ? styleNum(this.style.bandwidth) : 0;
|
|
15131
|
+
const powStyle = styleNum(this.style.bandwidthzoompow);
|
|
15132
|
+
const zoomPow = powStyle >= 0 ? Math.min(1, powStyle) : (explicitKm ? 0 : 1);
|
|
15133
|
+
const refZoom = styleNum(this.style.bandwidthzoomref) > 0 ? styleNum(this.style.bandwidthzoomref) : 12;
|
|
15134
|
+
const tableKm = kdeBandwidthKm(flatZoom), refKm = kdeBandwidthKm(refZoom);
|
|
15135
|
+
const zoomRatio = zoomPow === 1 ? tableKm / refKm : Math.pow(tableKm / refKm, zoomPow);
|
|
15136
|
+
const bwKm = (explicitKm ? explicitKm * zoomRatio : (zoomPow === 1 ? tableKm : refKm * zoomRatio)) * bwScale;
|
|
14873
15137
|
const maxCells = kdeMaxCells(flatZoom);
|
|
14874
15138
|
const levels = kdeLevels(this.style);
|
|
14875
15139
|
const key = JSON.stringify([this._dataGen, this._kdeSourceId, this.binding, bwKm, maxCells, levels, bbox.map(v => Math.round(v * 1e4) / 1e4)]);
|
|
@@ -14886,6 +15150,7 @@ self.onmessage = e => {
|
|
|
14886
15150
|
this._kdeBands = kdeBands(grid, levels, () => contours());
|
|
14887
15151
|
this._kdeBandsGen = (this._kdeBandsGen || 0) + 1; // every set of bands is a new layer for deck.gl
|
|
14888
15152
|
this._kdeLevelCount = levels.length;
|
|
15153
|
+
this._kdeBandsBw = bwKm; // the radius these bands were computed for
|
|
14889
15154
|
this._kdeKeyDone = key;
|
|
14890
15155
|
if (this._triggerRedraw) this._triggerRedraw();
|
|
14891
15156
|
}).catch(err => {
|
|
@@ -14896,6 +15161,14 @@ self.onmessage = e => {
|
|
|
14896
15161
|
}
|
|
14897
15162
|
const bands = this._kdeBands;
|
|
14898
15163
|
if (!bands || !bands.features.length) return [];
|
|
15164
|
+
// bands kept from before the layer was switched off belong to another view and radius: they are not
|
|
15165
|
+
// shown on switching on until the right ones are computed (a wrong-radius density flashing first
|
|
15166
|
+
// reads as flicker). Once the right ones are there the gate opens, and while the map zooms the
|
|
15167
|
+
// last bands stay as before — a gap on every zoom step would be worse.
|
|
15168
|
+
if (this._kdeShowGate) {
|
|
15169
|
+
if (this._kdeBandsBw !== bwKm) return [];
|
|
15170
|
+
this._kdeShowGate = false;
|
|
15171
|
+
}
|
|
14899
15172
|
const colors = kdeBandColors(this.style.colorscheme, this._kdeLevelCount || levels.length);
|
|
14900
15173
|
return [new GeoJsonLayer({
|
|
14901
15174
|
// a new id for each set of bands: deck.gl keeps a layer's buffers by id, and bands whose polygon
|
|
@@ -17708,6 +17981,12 @@ self.onmessage = e => {
|
|
|
17708
17981
|
if (!(fontSize > 0)) continue;
|
|
17709
17982
|
labels.push({ ...d, text: textOf(d.properties), fontSize, halo: itemRgb(d.properties), pixelOffset: alignOf(d) });
|
|
17710
17983
|
}
|
|
17984
|
+
// a text field read as the value/size scans to 0 (flat's __scanValue), so every label has size 0 and nothing is drawn
|
|
17985
|
+
if (!labels.length && combined.length && !this._warnedNoLabels) {
|
|
17986
|
+
this._warnedNoLabels = true;
|
|
17987
|
+
console.warn(`[ixmaps-gl] theme "${this.name}": LABEL|TEXTONLY drew no labels — the size of all ${combined.length} items is 0. ` +
|
|
17988
|
+
'A non-numeric (text) field bound as value/size reads as 0; bind a numeric size field.');
|
|
17989
|
+
}
|
|
17711
17990
|
// TextLayer's outlineColor is one per layer: a layer per halo color
|
|
17712
17991
|
const byHalo = new Map();
|
|
17713
17992
|
for (const l of labels) {
|
|
@@ -18069,7 +18348,7 @@ self.onmessage = e => {
|
|
|
18069
18348
|
// last built map; called while the map is still building, they run once
|
|
18070
18349
|
// it is ready.
|
|
18071
18350
|
const MAP_HANDLE_METHODS = ['replace', 'add', 'newTheme', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
18072
|
-
'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'setProjection', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible', 'switchLayer', 'setThemeTimeFrame', 'attribution'];
|
|
18351
|
+
'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'setProjection', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible', 'switchLayer', 'setThemeTimeFrame', 'attribution', 'on', 'off'];
|
|
18073
18352
|
const _mapHandles = Object.create(null);
|
|
18074
18353
|
function makeMapHandle(szMap) {
|
|
18075
18354
|
const h = {};
|
|
@@ -18087,6 +18366,9 @@ self.onmessage = e => {
|
|
|
18087
18366
|
};
|
|
18088
18367
|
}
|
|
18089
18368
|
h.getZoom = () => { const a = api(); return a ? a.getZoom() : undefined; };
|
|
18369
|
+
h.getBounds = () => getBounds();
|
|
18370
|
+
// the MapLibre map itself (undefined until the map is built)
|
|
18371
|
+
h.getMapLibre = () => { const a = api(); return a ? a.map : undefined; };
|
|
18090
18372
|
h.getThemeObj = id => getThemeObj(id);
|
|
18091
18373
|
// flat's mapApi.message(szText) (htmlgui_flat.js 1381): a short status
|
|
18092
18374
|
// line over the map — the same accepted no-op as the global
|
|
@@ -18198,6 +18480,12 @@ self.onmessage = e => {
|
|
|
18198
18480
|
return [{ lat: Math.max(b[0].lat, -85.05), lng: b[0].lng }, { lat: Math.min(b[1].lat, 85.05), lng: b[1].lng }];
|
|
18199
18481
|
}
|
|
18200
18482
|
|
|
18483
|
+
// flat's mapApi.getBounds() (htmlgui_flat.js): [swLat, swLng, neLat, neLng] — the same box as getBoundingBox
|
|
18484
|
+
function getBounds() {
|
|
18485
|
+
const b = getBoundingBox();
|
|
18486
|
+
return b ? [b[0].lat, b[0].lng, b[1].lat, b[1].lng] : null;
|
|
18487
|
+
}
|
|
18488
|
+
|
|
18201
18489
|
// flat's ixmaps.setTitle(html) / setTitleBox(text, color) (htmlgui.js
|
|
18202
18490
|
// 1311-1331): a message line over the last built map, same markup as
|
|
18203
18491
|
// flat's default (legend not aligned left); '' clears it.
|
|
@@ -18743,7 +19031,7 @@ self.onmessage = e => {
|
|
|
18743
19031
|
global.ixmaps = {
|
|
18744
19032
|
layer, Layer: layer, Map: createMap, setExternalData: setExternalDataBridge, getThemeObj, data: ixmapsData,
|
|
18745
19033
|
szResourceBase: ixmapsSzResourceBase, getProjectString, setProjectJSON, loadProject,
|
|
18746
|
-
markThemeClass, unmarkThemeClass, getBoundingBox, setTitle, setTitleBox, refreshTheme, formatValue: flatFormatValue,
|
|
19034
|
+
markThemeClass, unmarkThemeClass, getBoundingBox, getBounds, setTitle, setTitleBox, refreshTheme, formatValue: flatFormatValue,
|
|
18747
19035
|
// flat's ixmaps.__formatValue (ui/js/tools/legend.js 451-553) — pages call
|
|
18748
19036
|
// it for statistics/legend values; legend's own variant differs from
|
|
18749
19037
|
// format.js's only in the SPACE flag, which flatFormatValue covers too
|
|
@@ -18806,7 +19094,7 @@ self.onmessage = e => {
|
|
|
18806
19094
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
18807
19095
|
global.__ixmapsGlInternals = {
|
|
18808
19096
|
IXMAPS_GL_VERSION, applyFlatMaxCharts, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout, ringsLayout, pieSliceLayout, pieValueLabelLayout, isBarChart, flatBarLayout, flatValueRules, flatPieRecord, flatPieAccumulate, flatPieItemValues, flatVectorItems, orthographicPlane, vectorArcHeight, pathBackIndex, greatCircleMeters, cameraOptions, bezierVectorLayout, cubicBezierPoints, arrowMarkerTriangle, fadeGradientStops, flatToArray, hashUnit, flowDashPhase, multiQuadOffsets, pixelOffsetLngLat, chartTextTransmittance, insideSymbolShape,
|
|
18809
|
-
chartBoxLayout, choroplethOutlineColor, normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
19097
|
+
splitMapEvents, getBounds, chartBoxLayout, choroplethOutlineColor, normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
18810
19098
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
18811
19099
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|
|
18812
19100
|
computeAlphaStats, computeMultiFieldClasses, computeDominantStats, resolveDominantClass, computeComposeColorStats,
|