ixmaps-gl 0.2.1 → 0.2.4
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 +8 -7
- package/ixmaps-gl.js +228 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ circle/square/diamond/triangle. See the top-of-file comment in
|
|
|
72
72
|
A page's `.require(url)` scripts (page code, e.g. a user chart function) load in order before the
|
|
73
73
|
layers are built. `ixmaps.map()` is the map handle as in flat — its calls wait until the map is ready —
|
|
74
74
|
with `add(theme, flags)` / `replace` / `replaceTheme` / `remove`, `changeThemeStyle` for any style key,
|
|
75
|
-
`getZoom()` (flat's zoom), `resize()` and `
|
|
75
|
+
`getZoom()` (flat's zoom), `resize()`, `setBasemapOpacity` and `setMapType(id)` (also `setMapTypeId`, `mapType`: a basemap name reading dark/black/night/matter loads CARTO Dark Matter, any other name CARTO Positron, a color a plain background — the same rule the `mapType` option follows at start); the page's `htmlgui_onNewTheme(id)` and
|
|
76
76
|
`htmlgui_onZoomAndPan()` (also once on load) are called (as in flat, a theme swapped in with `replace` comes last
|
|
77
77
|
in the legend, while FEATURE and CHOROPLETH shapes always draw under the charts), and `map`, `getZoom` and flat's
|
|
78
78
|
`formatValue` exist as globals. `.data({name})` without a URL waits for data of that name: from
|
|
@@ -155,8 +155,9 @@ ones, `replace` swaps themes by `style.name`/`meta.name`, `themeonly`/`maponly`/
|
|
|
155
155
|
work as in flat. Themes in flat's older shape (data source in `style.dbtable*`, value field at
|
|
156
156
|
the top level) are translated first. It resolves to `{ themes, skipped, notes }`: a theme
|
|
157
157
|
that can't load is skipped with a warning, the rest still load. **Code a project names is not
|
|
158
|
-
run by default** — `required` scripts, `ext` data scripts and a project's `process`
|
|
159
|
-
reported, not executed (only a page's own
|
|
158
|
+
run by default** — `required` scripts, `ext` data scripts and a project's `process` and `query`
|
|
159
|
+
functions and function-string `colorscheme`s are reported, not executed (only a page's own
|
|
160
|
+
`.data({process, query})` and colorscheme functions run) — and the basemap isn't switched.
|
|
160
161
|
|
|
161
162
|
A page can opt in to a project's **scripts** (`data.ext`) for script URLs under prefixes it lists —
|
|
162
163
|
no built-in prefixes, and a project file's own `options.trustedscripts` is ignored. Both of flat's
|
|
@@ -192,7 +193,7 @@ of your own project files gl can render.
|
|
|
192
193
|
Load the engine from jsDelivr, pinned to a release tag (or `@main` for the latest commit):
|
|
193
194
|
|
|
194
195
|
```html
|
|
195
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
196
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.4/ixmaps-gl.js"></script>
|
|
196
197
|
```
|
|
197
198
|
|
|
198
199
|
or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmaps-gl)
|
|
@@ -200,13 +201,13 @@ or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmap
|
|
|
200
201
|
browser script, which defines the global `ixmaps`):
|
|
201
202
|
|
|
202
203
|
```html
|
|
203
|
-
<script src="https://unpkg.com/ixmaps-gl@0.2.
|
|
204
|
+
<script src="https://unpkg.com/ixmaps-gl@0.2.4/ixmaps-gl.js"></script>
|
|
204
205
|
```
|
|
205
206
|
|
|
206
207
|
or use a local copy (`<script src="ixmaps-gl.js"></script>`). A minimal page:
|
|
207
208
|
|
|
208
209
|
```html
|
|
209
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
210
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.4/ixmaps-gl.js"></script>
|
|
210
211
|
<div id="map_div" style="position:absolute;inset:0;"></div>
|
|
211
212
|
|
|
212
213
|
<script>
|
|
@@ -272,7 +273,7 @@ For checking page files before they run, use the static checker in ixmaps-gramma
|
|
|
272
273
|
| [`mappa_stranieri_30.html`](./stage/mappa_stranieri_30.html) | Symbol shapes, city picker, runtime `changeThemeStyle` filtering, light legend theme |
|
|
273
274
|
| [`roma_incidenti_pericolosita_sidebar_gl.html`](./stage/roma_incidenti_pericolosita_sidebar_gl.html) | AGGREGATE on a gridwidth grid, facets sidebar |
|
|
274
275
|
| [`roma_incidenti_pericolosita_sidebar.html`](./stage/roma_incidenti_pericolosita_sidebar.html) | The same page on the **real** ixmaps-flat engine, kept for side-by-side reference |
|
|
275
|
-
| [`uk_collisions_2023.html`](./stage/uk_collisions_2023.html), [`ixmaps-loader_70.html`](./
|
|
276
|
+
| [`uk_collisions_2023.html`](./stage/uk_collisions_2023.html), [`app/ixmaps-loader_70.html`](./app/ixmaps-loader_70.html) | Additional real-page compatibility tests |
|
|
276
277
|
| [`examples/`](./examples) | Smaller feature-by-feature test pages (choropleth classing modes, bubble ranges, etc.) |
|
|
277
278
|
|
|
278
279
|
Serve any of these with a static file server (they fetch remote data over HTTPS, so
|
package/ixmaps-gl.js
CHANGED
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
// ---------------------------------------------------------------
|
|
81
81
|
// this engine's release (= package.json "version", checked by
|
|
82
82
|
// test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
|
|
83
|
-
const IXMAPS_GL_VERSION = '0.2.
|
|
83
|
+
const IXMAPS_GL_VERSION = '0.2.4';
|
|
84
84
|
|
|
85
85
|
const LIB_URLS = {
|
|
86
86
|
// Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
|
|
@@ -306,7 +306,7 @@
|
|
|
306
306
|
// crafted link must not be able to load arbitrary code into the page.
|
|
307
307
|
// ---------------------------------------------------------------
|
|
308
308
|
// v0.1.6: knows this engine's aliases, lookupfield rule, meta↔style, builder methods, project loading
|
|
309
|
-
const VALIDATOR_URL_DEFAULT = 'https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-grammar@v0.1.
|
|
309
|
+
const VALIDATOR_URL_DEFAULT = 'https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-grammar@v0.1.21/dist/validate.mjs';
|
|
310
310
|
// a page may set ixmaps.validate BEFORE loading this script — the
|
|
311
311
|
// `global.ixmaps = {...}` export below would otherwise overwrite it
|
|
312
312
|
const _preloadValidate = global.ixmaps && global.ixmaps.validate;
|
|
@@ -587,6 +587,78 @@
|
|
|
587
587
|
}
|
|
588
588
|
return null;
|
|
589
589
|
}
|
|
590
|
+
// ---------------------------------------------------------------
|
|
591
|
+
// flat's data cache (maptheme.js 2457-2500, themeDataCacheA): data once
|
|
592
|
+
// loaded from a source is reused by every later theme of the map with the
|
|
593
|
+
// same source — data.cache (also style.datacache, see the dbtable* table)
|
|
594
|
+
// defaults to true (fDataCache, maptheme.js 7736); cache: false reloads.
|
|
595
|
+
// The key is the fetch identity PLUS the shape the binding asks for:
|
|
596
|
+
// rowsResult() turns the same CSV rows into a Table (lookup only) or a
|
|
597
|
+
// FeatureCollection (position), so two themes reading one URL differently
|
|
598
|
+
// must not share one result.
|
|
599
|
+
// ---------------------------------------------------------------
|
|
600
|
+
// the data source itself (no shape) — also rt._dataSourceKey, which tells the
|
|
601
|
+
// facet filters which themes share one dataset (setFacetFilter)
|
|
602
|
+
function dataSourceKey(data) {
|
|
603
|
+
const d = data || {};
|
|
604
|
+
return JSON.stringify({ url: d.url, urls: d.urls, type: d.type, query: d.query,
|
|
605
|
+
process: d.process && String(d.process), ext: d.ext, name: d.ext ? d.name : undefined });
|
|
606
|
+
}
|
|
607
|
+
function dataCacheKey(data, binding) {
|
|
608
|
+
const b = binding || {};
|
|
609
|
+
return dataSourceKey(data) + '|' + (b.lookup && !b.position ? 'table' : 'pos:' + (b.position || ''));
|
|
610
|
+
}
|
|
611
|
+
function dataCacheDisabled(dataConfig) {
|
|
612
|
+
const c = dataConfig && dataConfig.cache;
|
|
613
|
+
return c === false || (typeof c === 'string' && c.trim().toLowerCase() === 'false');
|
|
614
|
+
}
|
|
615
|
+
// cache: Map key → Promise; a failed load is not kept (the next theme tries again)
|
|
616
|
+
function cachedLayerData(cache, data, binding, engineOptions, { reload = false } = {}) {
|
|
617
|
+
const key = dataCacheKey(data, binding);
|
|
618
|
+
if (reload || dataCacheDisabled(data) || !cache.has(key)) {
|
|
619
|
+
const p = fetchLayerData(data, binding, engineOptions);
|
|
620
|
+
cache.set(key, p);
|
|
621
|
+
p.catch(() => { if (cache.get(key) === p) cache.delete(key); });
|
|
622
|
+
}
|
|
623
|
+
return { key, sourceKey: dataSourceKey(data), promise: cache.get(key) };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ---------------------------------------------------------------
|
|
627
|
+
// SHOW / ZOOMTO (flat maptheme.js 8714-8740, MapTheme.zoomTo 15664): once
|
|
628
|
+
// a theme is drawn, the map zooms to the extent of its items, then the
|
|
629
|
+
// flag is dropped — once per theme definition, not on every redraw.
|
|
630
|
+
// featuresBounds: [[west, south], [east, north]] of every coordinate, or
|
|
631
|
+
// null when there is none.
|
|
632
|
+
// ---------------------------------------------------------------
|
|
633
|
+
function featuresBounds(features) {
|
|
634
|
+
let w = Infinity, s = Infinity, e = -Infinity, n = -Infinity;
|
|
635
|
+
const visit = c => {
|
|
636
|
+
if (!Array.isArray(c)) return;
|
|
637
|
+
if (typeof c[0] === 'number' && typeof c[1] === 'number') {
|
|
638
|
+
if (!isFinite(c[0]) || !isFinite(c[1]) || Math.abs(c[1]) > 90) return;
|
|
639
|
+
if (c[0] < w) w = c[0]; if (c[0] > e) e = c[0];
|
|
640
|
+
if (c[1] < s) s = c[1]; if (c[1] > n) n = c[1];
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
for (const x of c) visit(x);
|
|
644
|
+
};
|
|
645
|
+
const geom = g => {
|
|
646
|
+
if (!g) return;
|
|
647
|
+
if (g.type === 'GeometryCollection') (g.geometries || []).forEach(geom);
|
|
648
|
+
else visit(g.coordinates);
|
|
649
|
+
};
|
|
650
|
+
for (const f of features || []) geom(f && f.geometry);
|
|
651
|
+
return w === Infinity ? null : [[w, s], [e, n]];
|
|
652
|
+
}
|
|
653
|
+
// the `legend` map option as flat reads it (htmlgui_flat.js 748-794): on whenever given
|
|
654
|
+
// and not false/"false"/0
|
|
655
|
+
function legendIsOn(opt) {
|
|
656
|
+
return opt !== undefined && opt !== null && opt !== false && opt !== 'false' && opt !== 0 && opt !== '0';
|
|
657
|
+
}
|
|
658
|
+
function wantsZoomToExtent(flags) {
|
|
659
|
+
return !!flags && (flags.has('SHOW') || flags.has('ZOOMTO'));
|
|
660
|
+
}
|
|
661
|
+
|
|
590
662
|
function rowsResult(rows, binding) {
|
|
591
663
|
if (binding && binding.lookup && !binding.position) return { type: 'Table', rows };
|
|
592
664
|
const position = (binding && binding.position) || geometryField(rows);
|
|
@@ -1720,7 +1792,7 @@
|
|
|
1720
1792
|
// htmlgui_flat.js). Its methods act on the last built map; called while
|
|
1721
1793
|
// the map is still building, they run once it is ready.
|
|
1722
1794
|
const MAP_HANDLE_METHODS = ['replace', 'add', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
1723
|
-
'refreshTheme', 'setBasemapOpacity', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible'];
|
|
1795
|
+
'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible'];
|
|
1724
1796
|
const _mapHandle = {};
|
|
1725
1797
|
for (const m of MAP_HANDLE_METHODS) {
|
|
1726
1798
|
_mapHandle[m] = (...args) => {
|
|
@@ -1994,7 +2066,7 @@
|
|
|
1994
2066
|
// edit it by hand; `npm run unit` fails when it drifts.
|
|
1995
2067
|
// ---------------------------------------------------------------
|
|
1996
2068
|
// <grammar:binding-aliases>
|
|
1997
|
-
// generated from ixmaps-grammar 0.1.
|
|
2069
|
+
// generated from ixmaps-grammar 0.1.21 (ixmaps-flat 1.0.41, eaaf2b7 2026-09-20) — 52 aliases
|
|
1998
2070
|
const FLAT_BINDING_ALIASES = {
|
|
1999
2071
|
"aggregation": "style.aggregationfield",
|
|
2000
2072
|
"aggregationfield": "style.aggregationfield",
|
|
@@ -2056,7 +2128,7 @@
|
|
|
2056
2128
|
// both — so .style({tooltip}) is a tooltip and .meta({fillopacity}) a style
|
|
2057
2129
|
// property. GENERATED by test/sync-grammar.mjs — do not edit.
|
|
2058
2130
|
// <grammar:meta-keys>
|
|
2059
|
-
// generated from ixmaps-grammar 0.1.
|
|
2131
|
+
// generated from ixmaps-grammar 0.1.21 — 5 meta keys
|
|
2060
2132
|
const FLAT_META_KEYS = ["description","name","snippet","title","tooltip"];
|
|
2061
2133
|
// </grammar:meta-keys>
|
|
2062
2134
|
|
|
@@ -2148,6 +2220,36 @@
|
|
|
2148
2220
|
dbtable: 'name', dbtableUrl: 'url', dbtableType: 'type', dbtableExt: 'ext',
|
|
2149
2221
|
dbtableProcess: 'process', dbtableQuery: 'query', dbtableObj: 'obj', datacache: 'cache',
|
|
2150
2222
|
};
|
|
2223
|
+
// Every project key the engine would turn into code (new Function)
|
|
2224
|
+
// is kept out, never run — a page's own .data({process, query}) and
|
|
2225
|
+
// .style({colorscheme: fn.toString()}) still run. Project keys of
|
|
2226
|
+
// that kind: the data processing function (data.process /
|
|
2227
|
+
// style.dbtableProcess), the broker query (data.query /
|
|
2228
|
+
// style.dbtableQuery — without it a data.name still reaches a
|
|
2229
|
+
// provider the PAGE defined as ixmaps[name]) and a colorscheme given
|
|
2230
|
+
// as a string, which resolveColorScheme evaluates as a function
|
|
2231
|
+
// (an array colorscheme is data and stays).
|
|
2232
|
+
const PROJECT_CODE_KEYS = [
|
|
2233
|
+
['data', 'process', 'data.process function'],
|
|
2234
|
+
['style', 'dbtableProcess', 'data.process function'],
|
|
2235
|
+
['data', 'query', 'data.query function'],
|
|
2236
|
+
['style', 'dbtableQuery', 'data.query function'],
|
|
2237
|
+
['style', 'colorscheme', 'colorscheme function', v => typeof v === 'string'],
|
|
2238
|
+
];
|
|
2239
|
+
function withoutProjectCode(t, report) {
|
|
2240
|
+
const found = PROJECT_CODE_KEYS.filter(([part, key, , isCode]) =>
|
|
2241
|
+
t[part] && t[part][key] != null && (!isCode || isCode(t[part][key])));
|
|
2242
|
+
if (!found.length) return t;
|
|
2243
|
+
const copy = Object.assign({}, t);
|
|
2244
|
+
found.forEach(([part, key]) => {
|
|
2245
|
+
if (copy[part] === t[part]) copy[part] = Object.assign({}, t[part]);
|
|
2246
|
+
delete copy[part][key];
|
|
2247
|
+
});
|
|
2248
|
+
const what = [...new Set(found.map(f => f[2]))].join(', ');
|
|
2249
|
+
report.notes.push(`theme "${t.layer}": the project's ${what} is never run by ixmaps-gl`);
|
|
2250
|
+
return copy;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2151
2253
|
function projectThemeToDefinition(theme) {
|
|
2152
2254
|
const t = theme || {};
|
|
2153
2255
|
const style = Object.assign({}, t.style);
|
|
@@ -2418,6 +2520,19 @@
|
|
|
2418
2520
|
const named = NAMED_MAPTYPE_COLORS[t.toLowerCase()];
|
|
2419
2521
|
return named || null;
|
|
2420
2522
|
}
|
|
2523
|
+
// A basemap NAME (flat's mapType: "VT_TONER_LITE", "VT_DATAVIZ_DARK",
|
|
2524
|
+
// "CartoDB - Dark matter", ...) picks one of two keyless CARTO vector
|
|
2525
|
+
// styles: dark for a name reading dark/black/night/matter, otherwise
|
|
2526
|
+
// light (Positron, gl's default). A color mapType (resolveMapTypeColor)
|
|
2527
|
+
// is a plain background instead and wins, so "dark" alone stays the
|
|
2528
|
+
// #1a1a1a background it always was.
|
|
2529
|
+
const BASEMAP_STYLE_URLS = {
|
|
2530
|
+
light: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
|
|
2531
|
+
dark: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
|
|
2532
|
+
};
|
|
2533
|
+
function resolveBasemapStyleUrl(mapType) {
|
|
2534
|
+
return typeof mapType === 'string' && /dark|black|night|matter/i.test(mapType) ? BASEMAP_STYLE_URLS.dark : BASEMAP_STYLE_URLS.light;
|
|
2535
|
+
}
|
|
2421
2536
|
// Real ixmaps-flat's legend look (tools/legend.js 3773-3845, via
|
|
2422
2537
|
// tools/background_theme.js): DARK when the basemap is more than half
|
|
2423
2538
|
// opaque and the map background reads as dark — a map type naming
|
|
@@ -2610,7 +2725,9 @@
|
|
|
2610
2725
|
// ones. Confirmed live as a real, not just theoretical, problem:
|
|
2611
2726
|
// without this, a 3-layer page sharing one >1.2M-row multi-file CSV
|
|
2612
2727
|
// source fetched all 5 files 3 SEPARATE times.
|
|
2613
|
-
|
|
2728
|
+
// one data cache for the map's whole lifetime (flat's themeDataCacheA):
|
|
2729
|
+
// build() and every later defineLayer()/refreshTheme() share it
|
|
2730
|
+
const dataCache = this._dataCache = this._dataCache || new Map();
|
|
2614
2731
|
const runtimes = [];
|
|
2615
2732
|
// themes whose data comes by name (see namedDataTheme) start empty and
|
|
2616
2733
|
// load on their own once the map is built, as in flat, where every
|
|
@@ -2650,9 +2767,9 @@
|
|
|
2650
2767
|
raw = await fetchLayerData(spec.data, spec.binding, this._engineOptions);
|
|
2651
2768
|
cacheKey = 'obj:' + spec.name;
|
|
2652
2769
|
} else {
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
raw = await
|
|
2770
|
+
const cached = cachedLayerData(dataCache, spec.data, spec.binding, this._engineOptions);
|
|
2771
|
+
cacheKey = cached.sourceKey;
|
|
2772
|
+
raw = await cached.promise;
|
|
2656
2773
|
}
|
|
2657
2774
|
const filtered = applyWhereFilter(raw, spec.filter);
|
|
2658
2775
|
const fc = filtered.type === 'Table' ? joinTableFeatures(spec, filtered, runtimes) : filtered;
|
|
@@ -2666,6 +2783,7 @@
|
|
|
2666
2783
|
rt._definition = def; // for refreshTheme
|
|
2667
2784
|
rt._deferredLoad = deferred;
|
|
2668
2785
|
rt._named = named;
|
|
2786
|
+
rt._zoomToPending = wantsZoomToExtent(spec.flags);
|
|
2669
2787
|
runtimes.push(rt);
|
|
2670
2788
|
notifyNewTheme(rt);
|
|
2671
2789
|
_globalThemeRegistry.set(rt.name, rt);
|
|
@@ -2688,7 +2806,7 @@
|
|
|
2688
2806
|
const map = new maplibregl.Map({
|
|
2689
2807
|
container: this.containerId,
|
|
2690
2808
|
style: mapTypeColor ? buildBlankBackgroundStyle(mapTypeColor)
|
|
2691
|
-
:
|
|
2809
|
+
: resolveBasemapStyleUrl(this.mapOptions.mapType),
|
|
2692
2810
|
center: [lon, lat],
|
|
2693
2811
|
zoom: this._viewZoom != null && this._viewZoom !== '' ? flatToMapLibreZoom(Number(this._viewZoom)) : 8,
|
|
2694
2812
|
// .attribution(a) (MapBuilder, above) was stored but never read —
|
|
@@ -2722,6 +2840,10 @@
|
|
|
2722
2840
|
const b = map.getBounds();
|
|
2723
2841
|
return [{ lat: b.getSouth(), lng: b.getWest() }, { lat: b.getNorth(), lng: b.getEast() }];
|
|
2724
2842
|
};
|
|
2843
|
+
// set once by the 'load' event — NOT map.loaded(), which MapLibre turns false again
|
|
2844
|
+
// whenever tiles are loading (after every pan/zoom), so a theme defined then would
|
|
2845
|
+
// wait for a 'load' that never fires again (SHOW / ZOOMTO, see maybeZoomToTheme)
|
|
2846
|
+
let mapHasLoaded = false;
|
|
2725
2847
|
|
|
2726
2848
|
// flat's page hook ixmaps.htmlgui_onZoomAndPan(nZoom), called after
|
|
2727
2849
|
// a zoom or a pan of more than 10 px (mapscript2.js 6445-6454) — a
|
|
@@ -3104,6 +3226,7 @@
|
|
|
3104
3226
|
else map.once('idle', () => map.fire('move'));
|
|
3105
3227
|
};
|
|
3106
3228
|
let addLegendPanel = () => {}; // set up with the legend, below
|
|
3229
|
+
let setLegendOption = () => {}; // map.setLegend(value) — set up with the legend, below
|
|
3107
3230
|
let updateSubTheme = () => {};
|
|
3108
3231
|
let layoutLegends = () => {}; // set up with the legend, below
|
|
3109
3232
|
let setDataLoading = () => {}; // set up with the loads, below
|
|
@@ -3140,6 +3263,31 @@
|
|
|
3140
3263
|
},
|
|
3141
3264
|
replaceTheme: (id, layerBuilder, flag) => engineApi.replace(id, layerBuilder, flag),
|
|
3142
3265
|
remove: (id) => { removeThemeById(id); return engineApi; },
|
|
3266
|
+
// flat's map.setMapType(id) / setMapTypeId / mapType (htmlgui_flat.js
|
|
3267
|
+
// mapApi): swap the basemap at runtime. MapLibre's setStyle replaces
|
|
3268
|
+
// the whole style; deck.gl's interleaved overlay re-adds its own
|
|
3269
|
+
// layers on 'styledata' by itself. What the new style resets is put
|
|
3270
|
+
// back once it has loaded: the projection (globe), the basemap
|
|
3271
|
+
// opacity (it is written into the style's paint properties).
|
|
3272
|
+
setMapType: (id) => {
|
|
3273
|
+
const color = resolveMapTypeColor(id);
|
|
3274
|
+
const style = color ? buildBlankBackgroundStyle(color) : resolveBasemapStyleUrl(id);
|
|
3275
|
+
const current = color ? null : resolveBasemapStyleUrl(builder.mapOptions.mapType);
|
|
3276
|
+
const wasColor = !!resolveMapTypeColor(builder.mapOptions.mapType);
|
|
3277
|
+
builder.mapOptions.mapType = id;
|
|
3278
|
+
if (!color && !wasColor && style === current) return engineApi; // same style: nothing to reload
|
|
3279
|
+
const projection = typeof map.getProjection === 'function' ? map.getProjection() : null;
|
|
3280
|
+
map.once('style.load', () => {
|
|
3281
|
+
if (projection && projection.type && typeof map.setProjection === 'function') {
|
|
3282
|
+
try { map.setProjection({ type: projection.type }); } catch (e) { /* projection unsupported by this build */ }
|
|
3283
|
+
}
|
|
3284
|
+
applyBasemapOpacity();
|
|
3285
|
+
});
|
|
3286
|
+
map.setStyle(style);
|
|
3287
|
+
return engineApi;
|
|
3288
|
+
},
|
|
3289
|
+
setMapTypeId: (id) => engineApi.setMapType(id),
|
|
3290
|
+
mapType: (id) => engineApi.setMapType(id),
|
|
3143
3291
|
setBasemapOpacity: (delta, mode) => {
|
|
3144
3292
|
_basemapOpacity = mode === 'relative'
|
|
3145
3293
|
? Math.max(0, Math.min(1, _basemapOpacity + (parseFloat(delta) || 0)))
|
|
@@ -3290,10 +3438,14 @@
|
|
|
3290
3438
|
options: (o) => {
|
|
3291
3439
|
if (o && typeof o === 'object') {
|
|
3292
3440
|
Object.assign(builder._engineOptions, o);
|
|
3441
|
+
// read only when the MapLibre map is created — switched here at runtime
|
|
3442
|
+
if ('worldcopies' in o) map.setRenderWorldCopies(o.worldcopies !== false && o.worldcopies !== 'false');
|
|
3293
3443
|
refresh();
|
|
3294
3444
|
}
|
|
3295
3445
|
return engineApi;
|
|
3296
3446
|
},
|
|
3447
|
+
// the `legend` map option at runtime (see setLegendOption)
|
|
3448
|
+
setLegend: (opt) => { setLegendOption(opt); return engineApi; },
|
|
3297
3449
|
layer: (nameOrBuilder) => {
|
|
3298
3450
|
if (typeof nameOrBuilder !== 'string') {
|
|
3299
3451
|
const lb = nameOrBuilder;
|
|
@@ -3345,9 +3497,14 @@
|
|
|
3345
3497
|
for (const rt of targets) {
|
|
3346
3498
|
if (rt._named) { await loadNamedTheme(rt, true); continue; }
|
|
3347
3499
|
const spec = normalizeTheme(rt._definition);
|
|
3348
|
-
|
|
3500
|
+
// flat's refresh reloads, cache or not (maptheme.js 2366: datacache = false);
|
|
3501
|
+
// the fresh result also replaces the cached one for later themes
|
|
3502
|
+
const raw = spec.data && spec.data.obj
|
|
3503
|
+
? await fetchLayerData(spec.data, rt._specBinding, builder._engineOptions)
|
|
3504
|
+
: await cachedLayerData(builder._dataCache, spec.data, rt._specBinding, builder._engineOptions, { reload: true }).promise;
|
|
3349
3505
|
const filtered = applyWhereFilter(raw, spec.filter);
|
|
3350
3506
|
rt.replaceFeatures(filtered.type === 'Table' ? joinTableFeatures(spec, filtered, runtimes) : filtered);
|
|
3507
|
+
maybeZoomToTheme(rt); // a scale-deferred theme gets its first data here
|
|
3351
3508
|
}
|
|
3352
3509
|
if (targets.length) { refresh(); notifyRedraw(); }
|
|
3353
3510
|
}
|
|
@@ -3438,6 +3595,19 @@
|
|
|
3438
3595
|
loadingEl.style.display = 'none';
|
|
3439
3596
|
}
|
|
3440
3597
|
};
|
|
3598
|
+
// SHOW / ZOOMTO: zoom to the theme's extent the first time it has
|
|
3599
|
+
// features on a loaded map (see featuresBounds), then never again for
|
|
3600
|
+
// this definition — as flat drops the flag after its zoomTo()
|
|
3601
|
+
function maybeZoomToTheme(rt) {
|
|
3602
|
+
if (!rt || !rt._zoomToPending || !mapHasLoaded || !runtimes.includes(rt)) return;
|
|
3603
|
+
const bounds = featuresBounds(rt.features);
|
|
3604
|
+
if (!bounds) return; // no data yet (named / deferred): wait for it
|
|
3605
|
+
rt._zoomToPending = false;
|
|
3606
|
+
const [[w, s], [e, n]] = bounds;
|
|
3607
|
+
if (w === e && s === n) map.jumpTo({ center: [w, s], zoom: Math.max(map.getZoom(), 12) });
|
|
3608
|
+
else map.fitBounds(bounds, { padding: 30, animate: false, maxZoom: 16 });
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3441
3611
|
async function loadNamedTheme(rt, force) {
|
|
3442
3612
|
const spec = normalizeTheme(rt._definition);
|
|
3443
3613
|
const name = String(spec.data.name);
|
|
@@ -3455,7 +3625,7 @@
|
|
|
3455
3625
|
table = await w.promise;
|
|
3456
3626
|
}
|
|
3457
3627
|
await built;
|
|
3458
|
-
if (runtimes.includes(rt)) fillNamedTheme(rt, spec, table, patch);
|
|
3628
|
+
if (runtimes.includes(rt)) { fillNamedTheme(rt, spec, table, patch); maybeZoomToTheme(rt); }
|
|
3459
3629
|
} catch (err) {
|
|
3460
3630
|
// a broker that hands nothing over yet is normal in flat (it may
|
|
3461
3631
|
// wait for other data); the theme stays empty until a refresh
|
|
@@ -3536,10 +3706,16 @@
|
|
|
3536
3706
|
let raw;
|
|
3537
3707
|
const deferred = deferredFeatureLoad(spec, map.getZoom());
|
|
3538
3708
|
const named = namedDataTheme(spec);
|
|
3709
|
+
let sourceKey = null;
|
|
3539
3710
|
if (deferred || named) {
|
|
3540
3711
|
raw = { type: 'FeatureCollection', features: [] };
|
|
3541
|
-
} else {
|
|
3712
|
+
} else if (spec.data && spec.data.obj) {
|
|
3713
|
+
// in memory already — nothing to cache (see build())
|
|
3542
3714
|
raw = await fetchLayerData(spec.data, spec.binding, builder._engineOptions);
|
|
3715
|
+
} else {
|
|
3716
|
+
const cached = cachedLayerData(builder._dataCache, spec.data, spec.binding, builder._engineOptions);
|
|
3717
|
+
sourceKey = cached.sourceKey;
|
|
3718
|
+
raw = await cached.promise;
|
|
3543
3719
|
}
|
|
3544
3720
|
const filtered = applyWhereFilter(raw, spec.filter);
|
|
3545
3721
|
const fc = filtered.type === 'Table' ? joinTableFeatures(spec, filtered, runtimes) : filtered;
|
|
@@ -3547,8 +3723,9 @@
|
|
|
3547
3723
|
rt._definition = pdef; // for refreshTheme
|
|
3548
3724
|
rt._deferredLoad = deferred;
|
|
3549
3725
|
rt._named = named;
|
|
3726
|
+
rt._zoomToPending = wantsZoomToExtent(spec.flags);
|
|
3550
3727
|
rt._dataSourceKey = named ? 'name:' + spec.data.name
|
|
3551
|
-
: JSON.stringify({ url: spec.data && spec.data.url, urls: spec.data && spec.data.urls, type: spec.data && spec.data.type, query: spec.data && spec.data.query, obj: !!(spec.data && spec.data.obj) });
|
|
3728
|
+
: sourceKey || JSON.stringify({ url: spec.data && spec.data.url, urls: spec.data && spec.data.urls, type: spec.data && spec.data.type, query: spec.data && spec.data.query, obj: !!(spec.data && spec.data.obj) });
|
|
3552
3729
|
runtimes.push(rt);
|
|
3553
3730
|
_globalThemeRegistry.set(rt.name, rt);
|
|
3554
3731
|
if (rt.meta && rt.meta.name && rt.meta.name !== rt.name) _globalThemeRegistry.set(rt.meta.name, rt);
|
|
@@ -3556,6 +3733,7 @@
|
|
|
3556
3733
|
notifyNewTheme(rt);
|
|
3557
3734
|
refresh();
|
|
3558
3735
|
notifyRedraw();
|
|
3736
|
+
maybeZoomToTheme(rt); // before the map's 'load': its handler zooms pending themes
|
|
3559
3737
|
if (named) { listenNamedData(rt); if (!deferred) loadNamedTheme(rt, false); }
|
|
3560
3738
|
return rt.name;
|
|
3561
3739
|
}
|
|
@@ -3611,19 +3789,6 @@
|
|
|
3611
3789
|
return report;
|
|
3612
3790
|
}
|
|
3613
3791
|
|
|
3614
|
-
// a project file's data processing function is code the project
|
|
3615
|
-
// names — kept out, never run (a page's own .data({process}) runs)
|
|
3616
|
-
function withoutProjectCode(t, report) {
|
|
3617
|
-
const hasData = t.data && t.data.process != null;
|
|
3618
|
-
const hasStyle = t.style && t.style.dbtableProcess != null;
|
|
3619
|
-
if (!hasData && !hasStyle) return t;
|
|
3620
|
-
report.notes.push(`theme "${t.layer}": the project's data.process function is never run by ixmaps-gl`);
|
|
3621
|
-
const copy = Object.assign({}, t);
|
|
3622
|
-
if (hasData) { copy.data = Object.assign({}, t.data); delete copy.data.process; }
|
|
3623
|
-
if (hasStyle) { copy.style = Object.assign({}, t.style); delete copy.style.dbtableProcess; }
|
|
3624
|
-
return copy;
|
|
3625
|
-
}
|
|
3626
|
-
|
|
3627
3792
|
function applyProjectMap(m, f, report) {
|
|
3628
3793
|
const svg = String(m.map || '');
|
|
3629
3794
|
if (svg && !/generic\/(mercator|orthographic)\.svg$/i.test(svg)) report.notes.push(`flat SVG map "${svg.split('/').pop()}" is not available in ixmaps-gl — mercator used`);
|
|
@@ -3875,8 +4040,10 @@
|
|
|
3875
4040
|
map.on('moveend', loadDeferredThemes);
|
|
3876
4041
|
|
|
3877
4042
|
map.on('load', () => {
|
|
4043
|
+
mapHasLoaded = true;
|
|
3878
4044
|
loadDeferredThemes();
|
|
3879
4045
|
refresh();
|
|
4046
|
+
runtimes.forEach(maybeZoomToTheme);
|
|
3880
4047
|
hideSplash(splash);
|
|
3881
4048
|
const initialOpacity = parseFloat(this._engineOptions.basemapopacity);
|
|
3882
4049
|
if (!isNaN(initialOpacity)) { _basemapOpacity = Math.max(0, Math.min(1, initialOpacity)); applyBasemapOpacity(); }
|
|
@@ -3986,8 +4153,9 @@
|
|
|
3986
4153
|
// isn't false/"false" ("true", "open", "", 1, …), folded for
|
|
3987
4154
|
// "closed", off when not given at all.
|
|
3988
4155
|
const legendOpt = this.mapOptions.legend;
|
|
3989
|
-
|
|
3990
|
-
|
|
4156
|
+
// let, not const: map.setLegend(value) switches them at runtime (see setLegendOption)
|
|
4157
|
+
let legendOn = legendIsOn(legendOpt);
|
|
4158
|
+
let legendFolded = legendOpt === 'closed';
|
|
3991
4159
|
{
|
|
3992
4160
|
// Map-level `align` option positions the legend panel — a map-
|
|
3993
4161
|
// wide placement choice, not per-theme (unlike legendtheme/
|
|
@@ -4104,6 +4272,23 @@
|
|
|
4104
4272
|
el.parentElement.appendChild(legendStack);
|
|
4105
4273
|
return legendStack;
|
|
4106
4274
|
};
|
|
4275
|
+
// map.setLegend(value): the `legend` map option at runtime, same values as
|
|
4276
|
+
// ixmaps.Map(id, {legend}) — on ("true", "open", …), folded ("closed"), off
|
|
4277
|
+
// (false/"false"/0/undefined). Panels are made for themes that lack one,
|
|
4278
|
+
// removed when off, folded/unfolded otherwise.
|
|
4279
|
+
setLegendOption = (opt) => {
|
|
4280
|
+
builder.mapOptions.legend = opt;
|
|
4281
|
+
legendFolded = opt === 'closed';
|
|
4282
|
+
legendOn = legendIsOn(opt);
|
|
4283
|
+
runtimes.forEach(rt => {
|
|
4284
|
+
if (!legendOn) {
|
|
4285
|
+
if (rt._legendPanel && rt._legendPanel.parentNode) rt._legendPanel.parentNode.removeChild(rt._legendPanel);
|
|
4286
|
+
rt._legendPanel = null;
|
|
4287
|
+
} else if (!rt._legendPanel) addLegendPanel(rt);
|
|
4288
|
+
else if (rt._setLegendCollapsed) rt._setLegendCollapsed(legendFolded);
|
|
4289
|
+
});
|
|
4290
|
+
layoutLegends();
|
|
4291
|
+
};
|
|
4107
4292
|
layoutLegends = () => {
|
|
4108
4293
|
if (!legendStack) return;
|
|
4109
4294
|
const z = map.getZoom();
|
|
@@ -4498,6 +4683,7 @@
|
|
|
4498
4683
|
}
|
|
4499
4684
|
collapseBtn.addEventListener('click', () => { collapsed = !collapsed; applyCollapsed(); });
|
|
4500
4685
|
applyCollapsed();
|
|
4686
|
+
rt._setLegendCollapsed = c => { collapsed = !!c; applyCollapsed(); }; // setLegendOption
|
|
4501
4687
|
|
|
4502
4688
|
// Selection/filter by field — opt-in via .style({legendfilter:
|
|
4503
4689
|
// "<field>"}), e.g. "country_long" on the power-plants sample.
|
|
@@ -5266,6 +5452,16 @@
|
|
|
5266
5452
|
return text;
|
|
5267
5453
|
}
|
|
5268
5454
|
|
|
5455
|
+
// Records behind one drawn chart item: a group's per-category record
|
|
5456
|
+
// counts summed, else a Supercluster cluster's point_count, else 1.
|
|
5457
|
+
// recordCounts only exists on RELOCATE groups (groupCoLocated); a plain
|
|
5458
|
+
// grid group (no RELOCATE) carries counts only — read unguarded, BOX|TITLE
|
|
5459
|
+
// without RELOCATE threw on the first cluster and the theme stopped drawing.
|
|
5460
|
+
function groupRecordCount(props) {
|
|
5461
|
+
const perCat = props.recordCounts || props.counts;
|
|
5462
|
+
return perCat ? perCat.reduce((x, c) => x + c, 0) : (props.point_count || 1);
|
|
5463
|
+
}
|
|
5464
|
+
|
|
5269
5465
|
function valuesFontSizePx(radiusPx, text, valueScale) {
|
|
5270
5466
|
return Math.min(radiusPx * 0.8, radiusPx * (3.3 / Math.max(1, text.length))) * (valueScale || 1);
|
|
5271
5467
|
}
|
|
@@ -9364,7 +9560,7 @@
|
|
|
9364
9560
|
let title = null;
|
|
9365
9561
|
if (titleShown) {
|
|
9366
9562
|
const raw = d.properties.raw;
|
|
9367
|
-
const n =
|
|
9563
|
+
const n = groupRecordCount(d.properties);
|
|
9368
9564
|
const text = n > 3 ? '(' + n + ')' : (raw && raw[this.binding.title] != null ? String(raw[this.binding.title]) : '');
|
|
9369
9565
|
if (text) {
|
|
9370
9566
|
const f = titleFontPx;
|
|
@@ -9987,7 +10183,7 @@
|
|
|
9987
10183
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
9988
10184
|
global.__ixmapsGlInternals = {
|
|
9989
10185
|
IXMAPS_GL_VERSION,
|
|
9990
|
-
normalizeTheme, projectThemeToDefinition, LayerBuilder, LayerRuntime, typeStyleNumbers, styleNum,
|
|
10186
|
+
normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, typeStyleNumbers, styleNum,
|
|
9991
10187
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
9992
10188
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|
|
9993
10189
|
computeAlphaStats, computeMultiFieldClasses, computeDominantStats, resolveDominantClass, computeComposeColorStats,
|
|
@@ -9997,6 +10193,7 @@
|
|
|
9997
10193
|
fetchLayerData, parseCsvText, dataTableRows, geometryRowsToFeatureCollection, filterFlatValues, applyField100, field100Binding, rangeClassLegendTotals, resolveAggregateValue, classValueSeparate, cellAggregatedValues, oneHot, groupCoLocated, aggregateOnGrid, GridAggregateIndex, flatRangeParts, aggregateField100, formatBubbleValue, valueRadius, itemPlotGeometry, itemAnchor, objectZoomFactor, resolveZoomReference, defaultNormalSizeValue,
|
|
9998
10194
|
applyWhereFilter, joinChartPositions, flatLookupKey, flatShapeCenter, chartHiddenByScale, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
9999
10195
|
flatFormatValue,
|
|
10196
|
+
dataCacheKey, dataCacheDisabled, cachedLayerData, featuresBounds, wantsZoomToExtent, legendIsOn,
|
|
10000
10197
|
};
|
|
10001
10198
|
global.__setFilter = __setFilter;
|
|
10002
10199
|
global.__removeFacets = __removeFacets;
|