ixmaps-gl 0.2.31 → 0.2.34
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 +179 -7
- package/ixmaps-gl.js +367 -61
- 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. SYMBOL shapes: the built-in ones and image-URL
|
|
118
|
+
symbols are described under [Symbols](#symbols).
|
|
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
|
|
@@ -297,12 +329,152 @@ so zoom-dependent page state (e.g. a basemap opacity ramp) changes in step with
|
|
|
297
329
|
hooks) aren't supported. `test/project-report.mjs` shows which themes
|
|
298
330
|
of your own project files gl can render.
|
|
299
331
|
|
|
332
|
+
### Symbols
|
|
333
|
+
|
|
334
|
+
`CHART|SYMBOL` draws one symbol per item (per category with `CATEGORICAL`, per part with `SEQUENCE`).
|
|
335
|
+
`style.symbols` names them, by color class: class `i` (the `i`-th entry of `values`, else the range
|
|
336
|
+
class) takes `symbols[i]`, and the last entry repeats when the array is shorter than the classes.
|
|
337
|
+
|
|
338
|
+
- **Built-in shapes:** `circle` (default), `square`, `diamond` (= `carot`), `triangle`, `roundrect`,
|
|
339
|
+
`hexagon`, `cross`, `label`, `empty`. An unknown name draws a circle.
|
|
340
|
+
- **Image symbols:** a URL ending in `.svg`, `.png`, `.jpg` or `.jpeg` (a query or `#` may follow), or a
|
|
341
|
+
`data:image/svg+xml|png|jpeg` URI. The image is drawn as is, not tinted by the class color. An SVG is
|
|
342
|
+
fetched and rasterized once per URL (a `viewBox` is enough, it needs no width/height); a PNG/JPEG is
|
|
343
|
+
loaded by deck.gl. A page opened from `file://` cannot fetch symbol files: serve it over http(s) or use
|
|
344
|
+
`data:` URIs.
|
|
345
|
+
- **By record field:** `symbolfield: "type"` with `symbolvalues: ["a", "b"]` maps the value of that field
|
|
346
|
+
to `symbols[i]` by position, independent of the color class (for individual items, not aggregated cells).
|
|
347
|
+
|
|
348
|
+
```js
|
|
349
|
+
.type("CHART|SYMBOL|CATEGORICAL")
|
|
350
|
+
.style({ values: ["school", "shop"], colorscheme: ["#d9534f", "#428bca"],
|
|
351
|
+
symbols: ["icons/school.svg", "icons/shop.svg"] })
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Zoom gates: `chartupper` and `chartlower`
|
|
355
|
+
|
|
356
|
+
`style.chartupper` / `style.chartlower` (`layerupper` / `layerlower` when unset) take a scale `"1:N"`. The
|
|
357
|
+
theme is **hidden when zoomed out beyond `chartupper`** (the scale denominator is above N) and **hidden when
|
|
358
|
+
zoomed in to `chartlower` or closer** (the scale denominator is at or below N). So `chartlower: "1:20000"`
|
|
359
|
+
shows the layer only while the view is zoomed out past 1:20000, and `chartupper: "1:20000"` only from there on
|
|
360
|
+
in. The scale is flat's, about `442913385 / 2^z` for the zoom `z` you give `.view()` on a Mercator map:
|
|
361
|
+
|
|
362
|
+
| zoom | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
|
|
363
|
+
|---|---|---|---|---|---|---|---|---|
|
|
364
|
+
| scale 1: | 432533 | 216266 | 108133 | 54067 | 27033 | 13517 | 6758 | 3379 |
|
|
365
|
+
|
|
366
|
+
`featureupper`/`featurelower` gate FEATURE themes the same way, `glowupper`/`glowlower` the glow halo.
|
|
367
|
+
|
|
368
|
+
### Aggregation grid and symbol size
|
|
369
|
+
|
|
370
|
+
`AGGREGATE` groups items into grid cells. The cell size is read, first match wins, from:
|
|
371
|
+
|
|
372
|
+
1. `aggregation` (flat's alias `aggregationscale`): scale/width pairs, e.g. `["1:1", "3px", "1:500000", "1px"]`,
|
|
373
|
+
the width of the last pair for which the view is zoomed out past `1:N` (the example gives 3 px, and 1 px once
|
|
374
|
+
the view is zoomed out past 1:500000); `"Npx"` is screen pixels, a bare number meters
|
|
375
|
+
2. `gridwidthpx`: screen pixels, constant over zoom (the recommended spelling for a fixed cell)
|
|
376
|
+
3. `gridwidth: "Npx"`: the same, as a string
|
|
377
|
+
4. `gridwidth: N` (a bare number): meters, the same cells at every zoom
|
|
378
|
+
5. none of them: no grid, items stay at their positions
|
|
379
|
+
|
|
380
|
+
With `CATEGORICAL|COUNT` and no size field the size of a symbol follows the number of records in its cell:
|
|
381
|
+
radius = `15 px × scale × (count / normalsizevalue)^(1/sizepow)`, scaled with the zoom (`objectscaling`) and
|
|
382
|
+
clamped to a minimum and maximum radius.
|
|
383
|
+
`normalsizevalue` is the count drawn at normal size, the dataset's maximum when unset. A `normalsizevalue` of
|
|
384
|
+
`30` therefore draws a single-record cell at 1/30 of the normal radius: pick it near the biggest cell you expect.
|
|
385
|
+
|
|
386
|
+
### Basemaps (`mapType`)
|
|
387
|
+
|
|
388
|
+
`mapType` is resolved in this order; a name that matches nothing falls back to the last two rules, so an
|
|
389
|
+
unknown flat name such as `VT_TONER_LITE` draws CARTO Positron:
|
|
390
|
+
|
|
391
|
+
| `mapType` | style |
|
|
392
|
+
|---|---|
|
|
393
|
+
| `ArcGIS - Topo` (`terrain`), `ArcGIS - Light Gray Base`, `ArcGIS - Ocean Basemap`, `ArcGIS - Hillshade` | Esri raster tiles |
|
|
394
|
+
| `CARTO - Voyager` (`VT_VOYAGER`), `CARTO - Voyager (no labels)` (`VT_VOYAGER_LIGHT`), `CARTO - Positron (no labels)`, `CARTO - Dark Matter (no labels)` | CARTO vector styles |
|
|
395
|
+
| `OpenFreeMap - Liberty`, `OpenFreeMap - Bright`, `OpenFreeMap - Positron` | OpenFreeMap vector styles |
|
|
396
|
+
| any name containing `dark`, `black`, `night` or `matter` | CARTO Dark Matter |
|
|
397
|
+
| everything else | CARTO Positron |
|
|
398
|
+
|
|
399
|
+
### Adding and replacing themes at runtime
|
|
400
|
+
|
|
401
|
+
`ixmaps.map().add(theme, "replace")` adds the theme the first time and swaps it on later calls, matched by
|
|
402
|
+
the theme id, `style.name` or `meta.name`. Give the theme a `name` in `.meta()`: the name passed to
|
|
403
|
+
`ixmaps.layer("name", …)` is not the id, and without one `"replace"` cannot find the old theme and adds a second. `ixmaps.map().remove(id)` removes it.
|
|
404
|
+
|
|
405
|
+
### Text on labels
|
|
406
|
+
|
|
407
|
+
`CHART|LABEL` (with `TEXTONLY` for the text alone) writes the item's value. A text field bound as the *value*
|
|
408
|
+
reads as 0, as in flat, and shows nothing visible; the console says so once. To show a text, as in flat, keep a
|
|
409
|
+
numeric `value` (or `size`) binding and name the text field in `style.valuefield`, or `"$title$"` for the
|
|
410
|
+
`title` binding:
|
|
411
|
+
|
|
412
|
+
```js
|
|
413
|
+
.binding({ geo: "geometry", value: "count" })
|
|
414
|
+
.type("CHART|LABEL|TEXTONLY|VALUES")
|
|
415
|
+
.style({ valuefield: "name" })
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
With `CATEGORICAL` the value field holds the categories and `valuefield` names the text. The text is drawn as a
|
|
419
|
+
single-color glyph with a halo, so an emoji comes out as a monochrome silhouette (seen in a headless Chromium
|
|
420
|
+
test): use image symbols (see Symbols) for colored icons.
|
|
421
|
+
|
|
422
|
+
### Runtime filtering
|
|
423
|
+
|
|
424
|
+
A theme's records can be filtered in place, without redefining it, with flat's `changeThemeStyle`:
|
|
425
|
+
|
|
426
|
+
```js
|
|
427
|
+
ixmaps.changeThemeStyle("mytheme", 'filter:WHERE "type" = "school"', "set");
|
|
428
|
+
ixmaps.changeThemeStyle("mytheme", "filter", "remove");
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
The same through the handles, `ixmaps.map().changeThemeStyle(id, style, flag)` and
|
|
432
|
+
`ixmaps.map().theme(id).changeStyle(style, flag)`; with `null` as the id every theme but the FEATURE ones is
|
|
433
|
+
changed. The expression is flat's filter grammar: `WHERE field op value` clauses joined by `AND` with `=`, `!=`,
|
|
434
|
+
`<>`, `<`, `>`, `<=`, `>=`, `BETWEEN … AND …`, `IN (…)`, `LIKE` and `NOT` (see `test/unit/filter.test.mjs`).
|
|
435
|
+
(`npm run runtime` in `test/` checks these calls in a browser.)
|
|
436
|
+
|
|
437
|
+
### Porting a flat page
|
|
438
|
+
|
|
439
|
+
How the calls of a flat page map to gl; "tested" means `test/runtime.mjs` runs it in a browser.
|
|
440
|
+
|
|
441
|
+
| flat | gl |
|
|
442
|
+
|---|---|
|
|
443
|
+
| `ixmaps.Map(div, opts, function (map) { … })` | the same; the callback gets the map builder (`view`, `options`, `layer`, `on`, …) |
|
|
444
|
+
| `var myMap = ixmaps.Map(div, opts).view(…)`, `myMap.then(map => …)` | the same, `myMap` is the builder and a thenable |
|
|
445
|
+
| `myMap.layer("name").data(…)…define()` | the same, before and after the map is built (tested) |
|
|
446
|
+
| `myMap.layer(theme)`, `myMap.layer(theme, "direct")` after the map is built | the same (tested); up to v0.2.33 these silently added nothing |
|
|
447
|
+
| `ixmaps.map().layer(theme)`, `ixmaps.map().add(theme, "replace")` | the same (tested) |
|
|
448
|
+
| `ixmaps.changeThemeStyle(id, 'filter:…', "set")` | the same (tested), see Runtime filtering |
|
|
449
|
+
| `map.getBounds()` | `ixmaps.map().getBounds()`, from v0.2.33 |
|
|
450
|
+
| `map.on(event, fn)` | `map.on` / `map.off`, see Map events |
|
|
451
|
+
| `ixmaps.hideTheme(id)` / `ixmaps.showTheme(id)` | not provided: `ixmaps.map().theme(id).hide()` / `.show()` or `ixmaps.map().setThemeVisible(id, bool)` |
|
|
452
|
+
| `ixmaps.flyTo(…)` | not provided: `ixmaps.map().view([lat, lng], zoom)` |
|
|
453
|
+
|
|
454
|
+
### `CHART|USER` draw function
|
|
455
|
+
|
|
456
|
+
`style.userdraw` names a function (`ixmaps[name]`, or a dotted path on `window`). It is called as
|
|
457
|
+
`fn(document, opt)` for each chart, after an optional `name_init(document, {target, theme})` once per draw.
|
|
458
|
+
`opt` holds `target` (the SVG group to draw into), `theme`, `value`, `values`, `size`, `maxSize`, `color`,
|
|
459
|
+
`class`, `flag`, `ccolor`, `item` (`{szColor, szLabel, szTitle, nCount, nSize}`) and `dbRecord` (the item's data
|
|
460
|
+
record, also the place to read the category). `size` is in flat's chart units, `maxSize` (300) being the
|
|
461
|
+
normal radius: one unit is `15 px × scale × zoom factor / 300`. The chart is drawn around 0,0.
|
|
462
|
+
|
|
463
|
+
### Headless, CI and offline
|
|
464
|
+
|
|
465
|
+
A headless Chromium has no GPU: start it with `--use-gl=swiftshader --enable-unsafe-swiftshader
|
|
466
|
+
--ignore-gpu-blocklist`. deck.gl then logs "GPU stall due to ReadPixels", which is harmless. The engine loads
|
|
467
|
+
`maplibre-gl@6.11.2` (module and css) and `deck.gl@9.4.0` and `mustache@4.2.0` from unpkg, `data.js` from
|
|
468
|
+
jsDelivr (`gh/gjrichter/data.js`), and on demand `d3-array@3.2.4` and `d3-contour@4.0.2` (jsDelivr) and the
|
|
469
|
+
`ixmaps-grammar` validator; mirror these to run offline. Page-level script and data URLs are not
|
|
470
|
+
configurable yet.
|
|
471
|
+
|
|
300
472
|
## Quick start
|
|
301
473
|
|
|
302
474
|
Load the engine from jsDelivr, pinned to a release tag (or `@main` for the latest commit):
|
|
303
475
|
|
|
304
476
|
```html
|
|
305
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
477
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.34/ixmaps-gl.js"></script>
|
|
306
478
|
```
|
|
307
479
|
|
|
308
480
|
or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmaps-gl)
|
|
@@ -310,13 +482,13 @@ or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmap
|
|
|
310
482
|
browser script, which defines the global `ixmaps`):
|
|
311
483
|
|
|
312
484
|
```html
|
|
313
|
-
<script src="https://unpkg.com/ixmaps-gl@0.2.
|
|
485
|
+
<script src="https://unpkg.com/ixmaps-gl@0.2.34/ixmaps-gl.js"></script>
|
|
314
486
|
```
|
|
315
487
|
|
|
316
488
|
or use a local copy (`<script src="ixmaps-gl.js"></script>`). A minimal page:
|
|
317
489
|
|
|
318
490
|
```html
|
|
319
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
491
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.34/ixmaps-gl.js"></script>
|
|
320
492
|
<div id="map_div" style="position:absolute;inset:0;"></div>
|
|
321
493
|
|
|
322
494
|
<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.34';
|
|
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,13 +7619,21 @@ 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
|
|
7477
7629
|
// ixmaps.layer(name, cb) factory): existing semantics, chain
|
|
7478
7630
|
// back on the MAP for further .layer(a).layer(b) calls.
|
|
7479
|
-
|
|
7631
|
+
// Once the map is built build() has consumed its queue, so queuing
|
|
7632
|
+
// here would add nothing, silently: define the theme on the live
|
|
7633
|
+
// map instead (flat's runtime overlay: myMap.layer(theme, "direct"),
|
|
7634
|
+
// the second argument is accepted and not needed)
|
|
7635
|
+
if (resolvedApi) resolvedApi.layer(nameOrBuilder);
|
|
7636
|
+
else builder.layer(nameOrBuilder);
|
|
7480
7637
|
return promise;
|
|
7481
7638
|
}
|
|
7482
7639
|
const lb = new LayerBuilder(nameOrBuilder);
|
|
@@ -11913,7 +12070,19 @@ self.onmessage = e => {
|
|
|
11913
12070
|
// flat's nValueSum is the RAW weighted sum, whatever the flags.
|
|
11914
12071
|
// (RELATIVE/INVERT subtract after the multiply — not reconstructable
|
|
11915
12072
|
// from the parts; flat's own composed branch never pairs them.)
|
|
11916
|
-
|
|
12073
|
+
// With binding.alpha the opacity follows the alpha field instead of the value sum — flat's
|
|
12074
|
+
// `if (this.szAlphaField)` branch of the same DOPACITY block (maptheme.js 13476-13482):
|
|
12075
|
+
// dopacityscale / nAlphaMax^(1/pow) · nAlpha^(1/pow), nAlpha as _prepareAlphaField made it
|
|
12076
|
+
// (alpha100 "$density$": alpha / the shape's area, the FEATURE base's size field when it binds one);
|
|
12077
|
+
// a shape with no alpha is 0. Not capped at 0.9 and not times fillopacity, as flat
|
|
12078
|
+
_composedDopacityAlpha(props, feature) {
|
|
12079
|
+
if (this.binding && this.binding.alpha) {
|
|
12080
|
+
const nAlpha = this._alphaByFeature && feature ? this._alphaByFeature.get(feature) : undefined;
|
|
12081
|
+
if (nAlpha == null) return 0;
|
|
12082
|
+
const pow = 1 / (styleNum(this.style.dopacitypow) || 1);
|
|
12083
|
+
const o = (styleNum(this.style.dopacityscale) || 1) / Math.pow(this._alphaMax || 1, pow) * Math.pow(nAlpha, pow);
|
|
12084
|
+
return isFinite(o) ? Math.max(0, Math.min(1, o)) : 0;
|
|
12085
|
+
}
|
|
11917
12086
|
const fields = this._multiFields || [];
|
|
11918
12087
|
const f100A = this.binding && this.binding.field100 ? String(this.binding.field100).split('|') : null;
|
|
11919
12088
|
const multiplier = this.flags.has('FRACTION') ? (styleNum(this.style.fractionscale) || 1)
|
|
@@ -12932,7 +13101,10 @@ self.onmessage = e => {
|
|
|
12932
13101
|
const labels = this.categoryDisplayLabels || this.categoryLabels || [];
|
|
12933
13102
|
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
13103
|
}
|
|
12935
|
-
|
|
13104
|
+
// (a USER chart over several value fields — the pinnacle peaks of a COMPOSECOLOR theme — lists its fields
|
|
13105
|
+
// the same way: its single item row would show the first class with the size total)
|
|
13106
|
+
const multiFieldUser = this.flags.has('USER') && String(this.binding.value || '').includes('|');
|
|
13107
|
+
if ((this.flags.has('CHOROPLETH') || multiFieldUser) && (this.flags.has('DOMINANT') || this.flags.has('COMPOSECOLOR')) && props.raw) {
|
|
12936
13108
|
return this._renderMultiFieldBarsHtml(props.raw);
|
|
12937
13109
|
}
|
|
12938
13110
|
const unit = unitOf;
|
|
@@ -13148,7 +13320,7 @@ self.onmessage = e => {
|
|
|
13148
13320
|
// early return here must also unmount its groups, or the pane keeps
|
|
13149
13321
|
// showing the last drawing after the theme went out of scale (or was
|
|
13150
13322
|
// hidden via setThemeVisible)
|
|
13151
|
-
if (this._hidden) { this._clearUserChartPane(); return []; }
|
|
13323
|
+
if (this._hidden) { this._kdeShowGate = true; this._clearUserChartPane(); return []; }
|
|
13152
13324
|
if (!this.flags.has('FEATURE') && !this.flags.has('FEATURES') && chartHiddenByScale(this.style, liveZoom)) { this._clearUserChartPane(); return []; }
|
|
13153
13325
|
// DOT is checked FIRST, matching the real engine's own precedence
|
|
13154
13326
|
// (its DOT branch is a loop-level fast-path that bypasses the whole
|
|
@@ -13451,7 +13623,7 @@ self.onmessage = e => {
|
|
|
13451
13623
|
// sum over (field100/100), against the same nMax the color
|
|
13452
13624
|
// blend normalizes by. The raw weighted sum is reconstructed
|
|
13453
13625
|
// from the fractioned part values (raw = part × its field100)
|
|
13454
|
-
dopacityAlpha: dopacityActive ? this._composedDopacityAlpha(f.properties) : null
|
|
13626
|
+
dopacityAlpha: dopacityActive ? this._composedDopacityAlpha(f.properties, f) : null
|
|
13455
13627
|
}
|
|
13456
13628
|
};
|
|
13457
13629
|
}
|
|
@@ -14468,8 +14640,9 @@ self.onmessage = e => {
|
|
|
14468
14640
|
// drawn now (fresh), else a cached chart's: the previous drawing's copy
|
|
14469
14641
|
const def = this._userChartGroup.querySelector('#' + CSS.escape(id)) || this._userChartGroup.ownerDocument.getElementById(id);
|
|
14470
14642
|
if (def) paneDefs.appendChild(def.cloneNode(true));
|
|
14643
|
+
else if (chart.defsById && chart.defsById[id]) paneDefs.insertAdjacentHTML('beforeend', chart.defsById[id]);
|
|
14471
14644
|
}
|
|
14472
|
-
items.push({ d, coords: d.geometry.coordinates, ox: chart.ox, oy: chart.oy, s: pxPerUnit,
|
|
14645
|
+
items.push({ d, coords: d.geometry.coordinates, ox: chart.ox, oy: chart.oy, s: pxPerUnit, pick: chart.pick,
|
|
14473
14646
|
r: Math.max(chart.vw, chart.vh) * pxPerUnit / 2,
|
|
14474
14647
|
// the chart's measured box (flat's getBox of the chart group) in px
|
|
14475
14648
|
// around the map position: the pane draws a chart point at
|
|
@@ -14512,14 +14685,31 @@ self.onmessage = e => {
|
|
|
14512
14685
|
positionOf: b => b.d.geometry.coordinates, offsetOf: () => [0, 0],
|
|
14513
14686
|
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
14687
|
}) : { box: [], titles: [] };
|
|
14515
|
-
|
|
14688
|
+
// each chart answers to its own outline: the polygons sampled from its drawing (else its box), in px
|
|
14689
|
+
// around the map position, here as lng/lat at this zoom (a px is 360/(512·2^z) degrees across,
|
|
14690
|
+
// times cos(lat) up); a few px of transparent stroke keep a thin peak easy to hit
|
|
14691
|
+
const degPerPx = 360 / (512 * Math.pow(2, liveZoom));
|
|
14692
|
+
const pickFeatures = items.map(it => {
|
|
14693
|
+
const [lng, lat] = it.coords;
|
|
14694
|
+
const kx = it.s * degPerPx, ky = it.s * degPerPx * Math.cos(lat * Math.PI / 180);
|
|
14695
|
+
const rings = it.pick && it.pick.length ? it.pick
|
|
14696
|
+
: [[[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]]];
|
|
14697
|
+
const polys = rings.map(r => {
|
|
14698
|
+
const ring = r.map(([x, y]) => [lng + x * kx, lat - y * ky]);
|
|
14699
|
+
ring.push(ring[0]);
|
|
14700
|
+
return [ring];
|
|
14701
|
+
});
|
|
14702
|
+
// icon + d: what pickedFeature unwraps to the item
|
|
14703
|
+
return { type: 'Feature', geometry: { type: 'MultiPolygon', coordinates: polys }, properties: {}, icon: true, d: it.d };
|
|
14704
|
+
});
|
|
14705
|
+
this._userChartLastLayers = [...boxLayers.box, new GeoJsonLayer({
|
|
14516
14706
|
id: `ix-bubbles-${this.name}`,
|
|
14517
|
-
data:
|
|
14707
|
+
data: { type: 'FeatureCollection', features: pickFeatures },
|
|
14518
14708
|
pickable: true,
|
|
14519
|
-
|
|
14520
|
-
|
|
14521
|
-
|
|
14522
|
-
|
|
14709
|
+
filled: true, stroked: true,
|
|
14710
|
+
// alpha 1/255: invisible, but never a fully transparent fragment a renderer might skip
|
|
14711
|
+
getFillColor: [0, 0, 0, 1], getLineColor: [0, 0, 0, 1],
|
|
14712
|
+
lineWidthUnits: 'pixels', getLineWidth: 4,
|
|
14523
14713
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14524
14714
|
}), ...boxLayers.titles];
|
|
14525
14715
|
return this._userChartLastLayers;
|
|
@@ -14532,11 +14722,40 @@ self.onmessage = e => {
|
|
|
14532
14722
|
const raw = d.properties.raw || d.properties;
|
|
14533
14723
|
const titleField = this.style.titlefield || this.binding.title;
|
|
14534
14724
|
const title = titleField && raw && raw[titleField] != null ? String(raw[titleField]) : undefined;
|
|
14535
|
-
|
|
14725
|
+
// flat hands the chart the item's value fields: values = every field of binding.value (after the
|
|
14726
|
+
// field100 arithmetic — the record already carries it) and value = the first of them
|
|
14727
|
+
// (maptheme.js 19667, nValuesA[0]); a theme with one value field keeps the item's value
|
|
14728
|
+
let values = [value];
|
|
14729
|
+
const fieldNames = this.binding.value && !d.properties.counts ? String(this.binding.value).split('|') : [];
|
|
14730
|
+
if (fieldNames.length > 1) {
|
|
14731
|
+
const parts = fieldNames.map(f => scanValue(raw && raw[f[0] === '!' ? f.slice(1) : f]));
|
|
14732
|
+
if (parts.some(v => !isNaN(v))) {
|
|
14733
|
+
values = parts.map(v => (isNaN(v) ? 0 : v));
|
|
14734
|
+
// COMPOSECOLOR blends all the fields into one colour, so the chart's value is the one that wins it —
|
|
14735
|
+
// the highest field (flat itself would hand over the first field here: a deliberate departure,
|
|
14736
|
+
// so a "party with the highest result" map prints that party's share)
|
|
14737
|
+
value = this.flags.has('COMPOSECOLOR') ? Math.max(...values) : values[0];
|
|
14738
|
+
}
|
|
14739
|
+
}
|
|
14740
|
+
// DOMINANT: the chart is the dominant field's — its class colour and its value (flat's USER branch,
|
|
14741
|
+
// maptheme.js ~19700-19770: nClass = the dominant index, nValue = nValuesA[nIndex]); the relevance
|
|
14742
|
+
// (plain value, PERCENTOFMEAN, DEVIATION) is the one the DOMINANT choropleth uses
|
|
14743
|
+
if (this.flags.has('DOMINANT') && !this.flags.has('COMPOSECOLOR') && this._multiFields && this._dominantMins && raw && fieldNames.length > 1) {
|
|
14744
|
+
const dominant = this._resolveDominantClass(raw);
|
|
14745
|
+
if (dominant) { value = dominant.value; cat = dominant.index; }
|
|
14746
|
+
}
|
|
14747
|
+
// COMPOSECOLOR: the chart takes the item's blended colour (flat's itemA[a].szColor, the same
|
|
14748
|
+
// blend the choropleth uses) instead of one class colour
|
|
14749
|
+
let composed;
|
|
14750
|
+
if (this.flags.has('COMPOSECOLOR') && this._multiFields && raw) {
|
|
14751
|
+
const c = this._resolveComposedColor(raw);
|
|
14752
|
+
composed = `rgb(${c[0]},${c[1]},${c[2]})`;
|
|
14753
|
+
}
|
|
14754
|
+
const base = `user|${name}|${cat}|${Math.round(value * 100) / 100}|${values.length > 1 ? values.join(',') : ''}|${composed || ''}|${Math.round(size * 2) / 2}|${flag}|${title || ''}`;
|
|
14536
14755
|
if (!this._userChartSvgs) this._userChartSvgs = new Map();
|
|
14537
14756
|
let chart = this._userChartSvgs.get(base);
|
|
14538
14757
|
if (chart === undefined) {
|
|
14539
|
-
chart = this._drawUserChartSvg(draw, name, theme, flag, d, value, cat, size, nSize);
|
|
14758
|
+
chart = this._drawUserChartSvg(draw, name, theme, flag, d, value, cat, size, nSize, values, composed);
|
|
14540
14759
|
if (chart === undefined) return null;
|
|
14541
14760
|
if (this._userChartSvgs.size >= ICON_CACHE_MAX) this._userChartSvgs.delete(this._userChartSvgs.keys().next().value);
|
|
14542
14761
|
this._userChartSvgs.set(base, chart);
|
|
@@ -14546,10 +14765,10 @@ self.onmessage = e => {
|
|
|
14546
14765
|
// the page's chart function draws one chart into the hidden SVG: its
|
|
14547
14766
|
// markup and box (chart units), null when it draws nothing, undefined
|
|
14548
14767
|
// when it fails
|
|
14549
|
-
_drawUserChartSvg(draw, name, theme, flag, d, value, cat, size, nSize) {
|
|
14768
|
+
_drawUserChartSvg(draw, name, theme, flag, d, value, cat, size, nSize, values, composedColor) {
|
|
14550
14769
|
const g = document.createElementNS(SVG_NS, 'g');
|
|
14551
14770
|
this._userChartGroup.appendChild(g);
|
|
14552
|
-
const color = this._userChartColor(cat);
|
|
14771
|
+
const color = composedColor || this._userChartColor(cat);
|
|
14553
14772
|
const raw = d.properties.raw || d.properties;
|
|
14554
14773
|
const label = this.binding.title && raw && raw[this.binding.title] != null ? String(raw[this.binding.title]) : undefined;
|
|
14555
14774
|
// flat's chart title (style.titlefield, maptheme's item.szTitle) —
|
|
@@ -14559,7 +14778,7 @@ self.onmessage = e => {
|
|
|
14559
14778
|
let chart = null;
|
|
14560
14779
|
try {
|
|
14561
14780
|
const ret = draw.call(global.ixmaps, document, {
|
|
14562
|
-
target: g, theme, value, values: [value], size, maxSize: USER_CHART_MAX_SIZE, color, class: cat, flag,
|
|
14781
|
+
target: g, theme, value, values: values || [value], size, maxSize: USER_CHART_MAX_SIZE, color, class: cat, flag,
|
|
14563
14782
|
// flat hands the chart the item's color CLASS object (maptheme's
|
|
14564
14783
|
// ColorScheme entry) — pinnacleChart reads ccolor.lowColor for its
|
|
14565
14784
|
// VALUES label fill (usercharts/d3/chart.js 199)
|
|
@@ -14570,13 +14789,51 @@ self.onmessage = e => {
|
|
|
14570
14789
|
if (bb && bb.width > 0 && bb.height > 0) {
|
|
14571
14790
|
const markup = new XMLSerializer().serializeToString(g);
|
|
14572
14791
|
const ids = new Set([...markup.matchAll(/url\(#([^)"']+)\)/g)].map(m => m[1]));
|
|
14573
|
-
|
|
14792
|
+
// the gradients the chart uses, kept WITH the chart: a cached chart that was not on the screen in the
|
|
14793
|
+
// last drawing finds no copy of them in the document any more (they are one per colour, and only the
|
|
14794
|
+
// theme's own colours are defined again by _init), and would lose its fill
|
|
14795
|
+
const defsById = {};
|
|
14796
|
+
for (const id of ids) {
|
|
14797
|
+
const el = this._userChartGroup.ownerDocument.getElementById(id);
|
|
14798
|
+
if (el) defsById[id] = new XMLSerializer().serializeToString(el);
|
|
14799
|
+
}
|
|
14800
|
+
const defs = Object.values(defsById).join('');
|
|
14574
14801
|
// strokes and text halos reach past the geometric box
|
|
14575
14802
|
const pad = Math.max(bb.width, bb.height) * 0.06 + 10;
|
|
14576
14803
|
const vx = bb.x - pad, vy = bb.y - pad, vw = bb.width + 2 * pad, vh = bb.height + 2 * pad;
|
|
14577
14804
|
// the chart's origin (0,0 — or the point flat moves it to, ret) sits at the map position
|
|
14578
14805
|
const ox = (ret && ret.x) || 0, oy = (ret && ret.y) || 0;
|
|
14579
|
-
|
|
14806
|
+
// the chart's own outline for hover and click: its shapes (the peak's triangle, a pie's circle ...)
|
|
14807
|
+
// sampled into polygons, in chart units around the origin — not the box around them (a tall peak
|
|
14808
|
+
// would otherwise answer to a rectangle, or a circle, far wider than the peak itself)
|
|
14809
|
+
const pick = [];
|
|
14810
|
+
try {
|
|
14811
|
+
const inv = g.getCTM().inverse();
|
|
14812
|
+
const at = (el, x, y) => { const p = new DOMPoint(x, y).matrixTransform(inv.multiply(el.getCTM())); return [p.x - ox, p.y - oy]; };
|
|
14813
|
+
g.querySelectorAll('path,rect,circle,ellipse,polygon').forEach(el => {
|
|
14814
|
+
if (el.closest('defs,clipPath,mask')) return;
|
|
14815
|
+
let ring = null;
|
|
14816
|
+
if (el.tagName === 'path') {
|
|
14817
|
+
const len = el.getTotalLength();
|
|
14818
|
+
if (!(len > 0)) return;
|
|
14819
|
+
const n = Math.max(8, Math.min(40, Math.ceil(len / 50)));
|
|
14820
|
+
ring = [];
|
|
14821
|
+
for (let i = 0; i <= n; i++) { const q = el.getPointAtLength(len * i / n); ring.push(at(el, q.x, q.y)); }
|
|
14822
|
+
} else if (el.tagName === 'rect') {
|
|
14823
|
+
const x = el.x.baseVal.value, y = el.y.baseVal.value, w = el.width.baseVal.value, h = el.height.baseVal.value;
|
|
14824
|
+
ring = [at(el, x, y), at(el, x + w, y), at(el, x + w, y + h), at(el, x, y + h)];
|
|
14825
|
+
} else if (el.tagName === 'polygon') {
|
|
14826
|
+
ring = [...el.points].map(q => at(el, q.x, q.y));
|
|
14827
|
+
} else {
|
|
14828
|
+
const cx = el.cx.baseVal.value, cy = el.cy.baseVal.value;
|
|
14829
|
+
const rx = el.tagName === 'circle' ? el.r.baseVal.value : el.rx.baseVal.value, ry = el.tagName === 'circle' ? rx : el.ry.baseVal.value;
|
|
14830
|
+
ring = [];
|
|
14831
|
+
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)));
|
|
14832
|
+
}
|
|
14833
|
+
if (ring && ring.length >= 3 && ring.every(q => isFinite(q[0]) && isFinite(q[1]))) pick.push(ring);
|
|
14834
|
+
});
|
|
14835
|
+
} catch (e) { /* the box below stands in */ }
|
|
14836
|
+
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
14837
|
svg: `<svg xmlns="${SVG_NS}" width="__W__" height="__H__" viewBox="${vx} ${vy} ${vw} ${vh}"><defs>${defs}</defs>${markup}</svg>` };
|
|
14581
14838
|
}
|
|
14582
14839
|
} catch (e) {
|
|
@@ -14852,7 +15109,8 @@ self.onmessage = e => {
|
|
|
14852
15109
|
// to kdeGrid in a worker, d3-contour cuts the bands at fractions of the maximum (style.kdelevels,
|
|
14853
15110
|
// default 0.15 / 0.30 / 0.50 / 0.70), each band the area at or above its threshold, so they
|
|
14854
15111
|
// stack, colored from style.colorscheme, drawn with style.fillopacity. The bandwidth is
|
|
14855
|
-
// style.bandwidth (km) or, without it, follows the zoom (kdeBandwidthKm).
|
|
15112
|
+
// style.bandwidth (km) or, without it, follows the zoom (kdeBandwidthKm); style.bandwidthscale
|
|
15113
|
+
// (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
15114
|
// The result belongs to a view: while the map moves the last bands stay, and once it stands still
|
|
14857
15115
|
// (and the view, the records, the binding or the bandwidth changed) the new ones are computed
|
|
14858
15116
|
// after a short pause and drawn by a redraw — never more than one computation wanted at a time.
|
|
@@ -14869,7 +15127,18 @@ self.onmessage = e => {
|
|
|
14869
15127
|
const source = this._activeFeatures || this.features;
|
|
14870
15128
|
if (this._kdeSource !== source) { this._kdeSource = source; this._kdeSourceId = (this._kdeSourceId || 0) + 1; }
|
|
14871
15129
|
const flatZoom = mapLibreToFlatZoom(zoom);
|
|
14872
|
-
const
|
|
15130
|
+
const bwScale = styleNum(this.style.bandwidthscale) > 0 ? styleNum(this.style.bandwidthscale) : 1;
|
|
15131
|
+
// style.bandwidthzoompow (0..1) flattens the zoom dependence: the radius is the table's ratio to its
|
|
15132
|
+
// value at the reference flat zoom (style.bandwidthzoomref, default 12), raised to this power
|
|
15133
|
+
// (0 = constant). Without style.bandwidth the radius is the table's, default power 1 (as the fire
|
|
15134
|
+
// pages); WITH style.bandwidth (km) that is the radius AT the reference zoom, default power 0 (fixed).
|
|
15135
|
+
const explicitKm = styleNum(this.style.bandwidth) > 0 ? styleNum(this.style.bandwidth) : 0;
|
|
15136
|
+
const powStyle = styleNum(this.style.bandwidthzoompow);
|
|
15137
|
+
const zoomPow = powStyle >= 0 ? Math.min(1, powStyle) : (explicitKm ? 0 : 1);
|
|
15138
|
+
const refZoom = styleNum(this.style.bandwidthzoomref) > 0 ? styleNum(this.style.bandwidthzoomref) : 12;
|
|
15139
|
+
const tableKm = kdeBandwidthKm(flatZoom), refKm = kdeBandwidthKm(refZoom);
|
|
15140
|
+
const zoomRatio = zoomPow === 1 ? tableKm / refKm : Math.pow(tableKm / refKm, zoomPow);
|
|
15141
|
+
const bwKm = (explicitKm ? explicitKm * zoomRatio : (zoomPow === 1 ? tableKm : refKm * zoomRatio)) * bwScale;
|
|
14873
15142
|
const maxCells = kdeMaxCells(flatZoom);
|
|
14874
15143
|
const levels = kdeLevels(this.style);
|
|
14875
15144
|
const key = JSON.stringify([this._dataGen, this._kdeSourceId, this.binding, bwKm, maxCells, levels, bbox.map(v => Math.round(v * 1e4) / 1e4)]);
|
|
@@ -14886,6 +15155,7 @@ self.onmessage = e => {
|
|
|
14886
15155
|
this._kdeBands = kdeBands(grid, levels, () => contours());
|
|
14887
15156
|
this._kdeBandsGen = (this._kdeBandsGen || 0) + 1; // every set of bands is a new layer for deck.gl
|
|
14888
15157
|
this._kdeLevelCount = levels.length;
|
|
15158
|
+
this._kdeBandsBw = bwKm; // the radius these bands were computed for
|
|
14889
15159
|
this._kdeKeyDone = key;
|
|
14890
15160
|
if (this._triggerRedraw) this._triggerRedraw();
|
|
14891
15161
|
}).catch(err => {
|
|
@@ -14896,6 +15166,14 @@ self.onmessage = e => {
|
|
|
14896
15166
|
}
|
|
14897
15167
|
const bands = this._kdeBands;
|
|
14898
15168
|
if (!bands || !bands.features.length) return [];
|
|
15169
|
+
// bands kept from before the layer was switched off belong to another view and radius: they are not
|
|
15170
|
+
// shown on switching on until the right ones are computed (a wrong-radius density flashing first
|
|
15171
|
+
// reads as flicker). Once the right ones are there the gate opens, and while the map zooms the
|
|
15172
|
+
// last bands stay as before — a gap on every zoom step would be worse.
|
|
15173
|
+
if (this._kdeShowGate) {
|
|
15174
|
+
if (this._kdeBandsBw !== bwKm) return [];
|
|
15175
|
+
this._kdeShowGate = false;
|
|
15176
|
+
}
|
|
14899
15177
|
const colors = kdeBandColors(this.style.colorscheme, this._kdeLevelCount || levels.length);
|
|
14900
15178
|
return [new GeoJsonLayer({
|
|
14901
15179
|
// a new id for each set of bands: deck.gl keeps a layer's buffers by id, and bands whose polygon
|
|
@@ -17685,6 +17963,24 @@ self.onmessage = e => {
|
|
|
17685
17963
|
// the item color (stroke font/7, opacity 0.5).
|
|
17686
17964
|
// Both moved by the chart position (flatChartAlignOffset). The text is
|
|
17687
17965
|
// the hover target of TEXTONLY, the box of LABEL.
|
|
17966
|
+
// A text field bound as the LABEL's value scans to 0 (flat's __scanValue), so the labels are drawn at the smallest size
|
|
17967
|
+
// with the text "0" — as flat skips such items, nothing visible, and no message. Say how to show a text: flat's
|
|
17968
|
+
// style.valuefield (or "$title$"), with a numeric value or size binding. CATEGORICAL values are categories, not numbers.
|
|
17969
|
+
_warnTextValue(combined) {
|
|
17970
|
+
// looked at once per theme (this runs on every redraw): the first record with a value
|
|
17971
|
+
if (this._checkedTextValue || this.style.valuefield || this.flags.has('CATEGORICAL') || !this.binding.value) return;
|
|
17972
|
+
let sample;
|
|
17973
|
+
for (const d of combined) {
|
|
17974
|
+
const v = d.properties.raw && d.properties.raw[this.binding.value];
|
|
17975
|
+
if (v != null && String(v).trim() !== '') { sample = v; break; }
|
|
17976
|
+
}
|
|
17977
|
+
if (sample === undefined) return;
|
|
17978
|
+
this._checkedTextValue = true;
|
|
17979
|
+
if (!isNaN(scanValue(sample))) return;
|
|
17980
|
+
console.warn(`[ixmaps-gl] theme "${this.name}": LABEL value field "${this.binding.value}" holds text ("${String(sample).slice(0, 20)}"), ` +
|
|
17981
|
+
'which reads as 0 — as in flat the labels are not visible. To show a text keep a numeric value (or size) binding ' +
|
|
17982
|
+
'and name the text field in style.valuefield (or "$title$" for the title).');
|
|
17983
|
+
}
|
|
17688
17984
|
_buildLabelChartLayers(combined, zoom, liveZoom, sizeValueOf) {
|
|
17689
17985
|
const textOnly = this.flags.has('TEXTONLY');
|
|
17690
17986
|
const showValues = this.flags.has('VALUES') && !valuesHiddenByScale(this.style, zoom);
|
|
@@ -17699,6 +17995,7 @@ self.onmessage = e => {
|
|
|
17699
17995
|
const textOf = props => flatValueText(props.counts ? this._categoryValueRecord(dominant(props.counts)) : props.raw,
|
|
17700
17996
|
this.binding.title && props.raw ? props.raw[this.binding.title] : undefined, valueOf(props), this.style, this.flags, opts);
|
|
17701
17997
|
|
|
17998
|
+
this._warnTextValue(combined);
|
|
17702
17999
|
if (textOnly) {
|
|
17703
18000
|
const textRgb = textOverride ? hexOrNamedToRgb(textOverride) : [0, 0, 0];
|
|
17704
18001
|
const labels = [];
|
|
@@ -18069,7 +18366,7 @@ self.onmessage = e => {
|
|
|
18069
18366
|
// last built map; called while the map is still building, they run once
|
|
18070
18367
|
// it is ready.
|
|
18071
18368
|
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'];
|
|
18369
|
+
'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'setProjection', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible', 'switchLayer', 'setThemeTimeFrame', 'attribution', 'on', 'off'];
|
|
18073
18370
|
const _mapHandles = Object.create(null);
|
|
18074
18371
|
function makeMapHandle(szMap) {
|
|
18075
18372
|
const h = {};
|
|
@@ -18087,6 +18384,9 @@ self.onmessage = e => {
|
|
|
18087
18384
|
};
|
|
18088
18385
|
}
|
|
18089
18386
|
h.getZoom = () => { const a = api(); return a ? a.getZoom() : undefined; };
|
|
18387
|
+
h.getBounds = () => getBounds();
|
|
18388
|
+
// the MapLibre map itself (undefined until the map is built)
|
|
18389
|
+
h.getMapLibre = () => { const a = api(); return a ? a.map : undefined; };
|
|
18090
18390
|
h.getThemeObj = id => getThemeObj(id);
|
|
18091
18391
|
// flat's mapApi.message(szText) (htmlgui_flat.js 1381): a short status
|
|
18092
18392
|
// line over the map — the same accepted no-op as the global
|
|
@@ -18198,6 +18498,12 @@ self.onmessage = e => {
|
|
|
18198
18498
|
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
18499
|
}
|
|
18200
18500
|
|
|
18501
|
+
// flat's mapApi.getBounds() (htmlgui_flat.js): [swLat, swLng, neLat, neLng] — the same box as getBoundingBox
|
|
18502
|
+
function getBounds() {
|
|
18503
|
+
const b = getBoundingBox();
|
|
18504
|
+
return b ? [b[0].lat, b[0].lng, b[1].lat, b[1].lng] : null;
|
|
18505
|
+
}
|
|
18506
|
+
|
|
18201
18507
|
// flat's ixmaps.setTitle(html) / setTitleBox(text, color) (htmlgui.js
|
|
18202
18508
|
// 1311-1331): a message line over the last built map, same markup as
|
|
18203
18509
|
// flat's default (legend not aligned left); '' clears it.
|
|
@@ -18743,7 +19049,7 @@ self.onmessage = e => {
|
|
|
18743
19049
|
global.ixmaps = {
|
|
18744
19050
|
layer, Layer: layer, Map: createMap, setExternalData: setExternalDataBridge, getThemeObj, data: ixmapsData,
|
|
18745
19051
|
szResourceBase: ixmapsSzResourceBase, getProjectString, setProjectJSON, loadProject,
|
|
18746
|
-
markThemeClass, unmarkThemeClass, getBoundingBox, setTitle, setTitleBox, refreshTheme, formatValue: flatFormatValue,
|
|
19052
|
+
markThemeClass, unmarkThemeClass, getBoundingBox, getBounds, setTitle, setTitleBox, refreshTheme, formatValue: flatFormatValue,
|
|
18747
19053
|
// flat's ixmaps.__formatValue (ui/js/tools/legend.js 451-553) — pages call
|
|
18748
19054
|
// it for statistics/legend values; legend's own variant differs from
|
|
18749
19055
|
// format.js's only in the SPACE flag, which flatFormatValue covers too
|
|
@@ -18806,7 +19112,7 @@ self.onmessage = e => {
|
|
|
18806
19112
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
18807
19113
|
global.__ixmapsGlInternals = {
|
|
18808
19114
|
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,
|
|
19115
|
+
splitMapEvents, getBounds, chartBoxLayout, choroplethOutlineColor, normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
18810
19116
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
18811
19117
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|
|
18812
19118
|
computeAlphaStats, computeMultiFieldClasses, computeDominantStats, resolveDominantClass, computeComposeColorStats,
|