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.
Files changed (3) hide show
  1. package/README.md +179 -7
  2. package/ixmaps-gl.js +367 -61
  3. 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: plain (non-AGGREGATE) `CATEGORICAL` choropleths, and the `QUAD`/
118
- `WAFFLE` base types, and SYMBOL shape variants beyond
119
- circle/square/diamond/triangle. See the top-of-file comment in
120
- [`ixmaps-gl.js`](./ixmaps-gl.js) for the exact, currently-accurate scope note.
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.31/ixmaps-gl.js"></script>
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.31/ixmaps-gl.js"></script>
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.31/ixmaps-gl.js"></script>
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.31';
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.29 (ixmaps-flat 1.0.42, 2b978d5 2026-10-02) — 52 aliases
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.29 — 5 meta keys
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
- function callItemAction(rt, object) {
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
- if (!rt) { hook.call(global.ixmaps, null, null); return; }
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
- if (value == null) return;
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
- // GL-PORT COMPAT: real ixmaps-flat's map.on("layerdraw", cb) — only
5540
- // "layerdraw" is actually wired up (see build()'s own note on how
5541
- // it's translated into onRedraw + a synthetic per-runtime event); any
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
- this._pendingEvents.push([event, cb]);
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
- if (this._legendHtml) {
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(this._legendHtml)) legendEl.innerHTML = this._legendHtml;
5977
- else fetch(this._legendHtml).then(r => r.ok ? r.text() : '').then(t => { legendEl.innerHTML = t; }).catch(() => {});
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
- const layerdrawCbs = (this._pendingEvents || [])
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
- builder.layer(nameOrBuilder);
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
- _composedDopacityAlpha(props) {
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
- if (this.flags.has('CHOROPLETH') && (this.flags.has('DOMINANT') || this.flags.has('COMPOSECOLOR')) && props.raw) {
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
- this._userChartLastLayers = [...boxLayers.box, new ScatterplotLayer({
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: items.map(it => ({ d: it.d, icon: true, r: it.r })),
14707
+ data: { type: 'FeatureCollection', features: pickFeatures },
14518
14708
  pickable: true,
14519
- getPosition: e => e.d.geometry.coordinates,
14520
- getRadius: e => Math.max(2, e.r),
14521
- radiusUnits: 'pixels', radiusMinPixels: 1,
14522
- stroked: false, filled: true, getFillColor: [0, 0, 0, 0],
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
- const base = `user|${name}|${cat}|${Math.round(value * 100) / 100}|${Math.round(size * 2) / 2}|${flag}|${title || ''}`;
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
- const defs = [...ids].map(id => { const el = this._userChartGroup.ownerDocument.getElementById(id); return el ? new XMLSerializer().serializeToString(el) : ''; }).join('');
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
- chart = { vx, vy, vw, vh, ox, oy, markup, ids: [...ids], bb: { x: bb.x, y: bb.y, width: bb.width, height: bb.height },
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 bwKm = styleNum(this.style.bandwidth) > 0 ? styleNum(this.style.bandwidth) : kdeBandwidthKm(flatZoom);
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ixmaps-gl",
3
- "version": "0.2.31",
3
+ "version": "0.2.34",
4
4
  "description": "MapLibre GL + deck.gl reimplementation of the ixmaps-flat declarative mapping engine",
5
5
  "main": "ixmaps-gl.js",
6
6
  "browser": "ixmaps-gl.js",