ixmaps-gl 0.2.25 → 0.2.30
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 +21 -8
- package/ixmaps-gl.js +253 -84
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,7 +124,7 @@ circle/square/diamond/triangle. See the top-of-file comment in
|
|
|
124
124
|
A page's `.require(url)` scripts (page code, e.g. a user chart function) load in order before the
|
|
125
125
|
layers are built. `ixmaps.map()` is the map handle as in flat — its calls wait until the map is ready —
|
|
126
126
|
with `add(theme, flags)` / `replace` / `replaceTheme` / `remove`, `changeThemeStyle` for any style key,
|
|
127
|
-
`getZoom()` (flat's zoom), `resize()`, `attribution(text)` (the page's attribution, bottom left on the map as in flat, in the style of the basemap credit and on its line — also
|
|
127
|
+
`getZoom()` (flat's zoom), `resize()`, `attribution(text)` (the page's attribution, bottom left on the map as in flat, in the style of the basemap credit and on its line, dark on a dark map like the zoom buttons — also
|
|
128
128
|
`.attribution()` on the builder, the `attribution` map option and `ixmaps.setAttribution(text)`; the basemap's credit stays bottom right), `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
|
|
129
129
|
`htmlgui_onZoomAndPan()` (also once on load) are called (as in flat, a theme swapped in with `replace` comes last
|
|
130
130
|
in the legend, while FEATURE and CHOROPLETH shapes always draw under the charts), and `map`, `getZoom` and flat's
|
|
@@ -224,10 +224,23 @@ but unused. It never mutates the page's own objects. Unit tests: `cd test && npm
|
|
|
224
224
|
|
|
225
225
|
### World copies
|
|
226
226
|
|
|
227
|
-
At an extreme zoom-out MapLibre repeats the world side by side
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
227
|
+
At an extreme zoom-out MapLibre repeats the world side by side. The themes are drawn once, as in
|
|
228
|
+
flat (which repeats only its basemap tiles): the map pans freely past the world's edge, with empty
|
|
229
|
+
space beside it. `.options({themecopies: true})` (also at runtime) repeats the themes on every world
|
|
230
|
+
copy. `.options({worldcopies: false})` draws the world once for the map itself, but MapLibre then
|
|
231
|
+
also stops the zoom-out where the world gets narrower than the window.
|
|
232
|
+
|
|
233
|
+
### Click to action
|
|
234
|
+
|
|
235
|
+
flat's click-to-action (`ui/js/tools/tooltip_to_action.js`, e.g. a click on a country filters every
|
|
236
|
+
theme of the page to it) works as on flat: on a click on an item the engine calls the page's
|
|
237
|
+
`ixmaps.onItemAction(szId, item, themeObj)`, `item.szSelectionId` being the value of the theme's
|
|
238
|
+
selection field, and `ixmaps.onItemAction(null, null)` on a click on the map background. The
|
|
239
|
+
selection field, `getThemeObj(id).szSelectionField`, is the `id` binding, else the position (or
|
|
240
|
+
lookup) field; an aggregated item gives its first record's value. The flat script's own
|
|
241
|
+
`htmlgui_onItemClick`, which reads flat's SVG, is not called — the page only needs
|
|
242
|
+
`.require(".../tooltip_to_action.js")` (or its own `ixmaps.onItemAction`). The click still pins the
|
|
243
|
+
tooltip. See [`ports/index_embed_migration - Zeit online data - 2015-2020 - click2filter.html`](./ports/index_embed_migration%20-%20Zeit%20online%20data%20-%202015-2020%20-%20click2filter.html).
|
|
231
244
|
|
|
232
245
|
### Data formats and data.js
|
|
233
246
|
|
|
@@ -289,7 +302,7 @@ of your own project files gl can render.
|
|
|
289
302
|
Load the engine from jsDelivr, pinned to a release tag (or `@main` for the latest commit):
|
|
290
303
|
|
|
291
304
|
```html
|
|
292
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
305
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.30/ixmaps-gl.js"></script>
|
|
293
306
|
```
|
|
294
307
|
|
|
295
308
|
or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmaps-gl)
|
|
@@ -297,13 +310,13 @@ or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmap
|
|
|
297
310
|
browser script, which defines the global `ixmaps`):
|
|
298
311
|
|
|
299
312
|
```html
|
|
300
|
-
<script src="https://unpkg.com/ixmaps-gl@0.2.
|
|
313
|
+
<script src="https://unpkg.com/ixmaps-gl@0.2.30/ixmaps-gl.js"></script>
|
|
301
314
|
```
|
|
302
315
|
|
|
303
316
|
or use a local copy (`<script src="ixmaps-gl.js"></script>`). A minimal page:
|
|
304
317
|
|
|
305
318
|
```html
|
|
306
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
319
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.30/ixmaps-gl.js"></script>
|
|
307
320
|
<div id="map_div" style="position:absolute;inset:0;"></div>
|
|
308
321
|
|
|
309
322
|
<script>
|
package/ixmaps-gl.js
CHANGED
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
// ---------------------------------------------------------------
|
|
101
101
|
// this engine's release (= package.json "version", checked by
|
|
102
102
|
// test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
|
|
103
|
-
const IXMAPS_GL_VERSION = '0.2.
|
|
103
|
+
const IXMAPS_GL_VERSION = '0.2.30';
|
|
104
104
|
|
|
105
105
|
const LIB_URLS = {
|
|
106
106
|
// Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
|
|
@@ -1201,7 +1201,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1201
1201
|
if (!table || !Array.isArray(table.records) || !Array.isArray(table.fields)) {
|
|
1202
1202
|
return table && typeof table.json === 'function' ? table.json() : [];
|
|
1203
1203
|
}
|
|
1204
|
-
|
|
1204
|
+
// a header cell that spans lines ("Sezione (*)\nSektion", bilingual
|
|
1205
|
+
// exports) reads as one line, as in flat: "Sezione (*) Sektion"
|
|
1206
|
+
const names = table.fields.map(f => String(f.id).replace(/[\r\n]+/g, ' '));
|
|
1205
1207
|
const rows = new Array(table.records.length);
|
|
1206
1208
|
for (let i = 0; i < table.records.length; i++) {
|
|
1207
1209
|
const rec = table.records[i], o = {};
|
|
@@ -1210,6 +1212,26 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1210
1212
|
}
|
|
1211
1213
|
return rows;
|
|
1212
1214
|
}
|
|
1215
|
+
// .data({process}) on topojson/geojson, as flat: its data.js hands the
|
|
1216
|
+
// process function the features' properties as a Table (column(...).map,
|
|
1217
|
+
// addColumn, ...); the result goes back onto the same features, by row
|
|
1218
|
+
function processFeatureCollection(fc, dataConfig) {
|
|
1219
|
+
if (!dataConfig.process || !fc || !Array.isArray(fc.features) || !fc.features.length) return fc;
|
|
1220
|
+
requireDataJs('process');
|
|
1221
|
+
const headers = [];
|
|
1222
|
+
const seen = new Set();
|
|
1223
|
+
for (const f of fc.features) {
|
|
1224
|
+
for (const k of Object.keys(f.properties || {})) if (!seen.has(k)) { seen.add(k); headers.push(k); }
|
|
1225
|
+
}
|
|
1226
|
+
const table = new global.Data.Table().setArray([headers, ...fc.features.map(f => headers.map(h => (f.properties || {})[h]))]);
|
|
1227
|
+
const rows = dataTableRows(runDataProcess(table, dataConfig));
|
|
1228
|
+
if (rows.length !== fc.features.length) {
|
|
1229
|
+
console.warn(`[ixmaps-gl] .data({process}) changed the row count of ${dataConfig.type} features (${fc.features.length} → ${rows.length}); process result ignored`);
|
|
1230
|
+
return fc;
|
|
1231
|
+
}
|
|
1232
|
+
fc.features.forEach((f, i) => { f.properties = rows[i]; });
|
|
1233
|
+
return fc;
|
|
1234
|
+
}
|
|
1213
1235
|
// parse CSV text with data.js (as flat) → a Table
|
|
1214
1236
|
function dataJsCsvTable(text) {
|
|
1215
1237
|
return new Promise((resolve, reject) => {
|
|
@@ -1531,8 +1553,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1531
1553
|
// genuinely CSV-shaped .obj with a missing binding.position would
|
|
1532
1554
|
// throw, even though this .obj was never row data in the first place.
|
|
1533
1555
|
if (dataConfig.obj) {
|
|
1534
|
-
if (dataConfig.type === 'topojson') return topojsonToFeatureCollection(dataConfig.obj, dataConfig.object);
|
|
1535
|
-
if (dataConfig.type === 'geojson') return sanitizeGeoJSON(dataConfig.obj);
|
|
1556
|
+
if (dataConfig.type === 'topojson') return processFeatureCollection(topojsonToFeatureCollection(dataConfig.obj, dataConfig.object), dataConfig);
|
|
1557
|
+
if (dataConfig.type === 'geojson') return processFeatureCollection(sanitizeGeoJSON(dataConfig.obj), dataConfig);
|
|
1536
1558
|
let obj = dataConfig.obj;
|
|
1537
1559
|
if (dataConfig.process) {
|
|
1538
1560
|
requireDataJs('process');
|
|
@@ -1624,8 +1646,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1624
1646
|
// them into a table with a geometry column, and back)
|
|
1625
1647
|
const resp = await fetch(dataConfig.url);
|
|
1626
1648
|
if (!resp.ok) throw new Error(`[ixmaps-gl] failed to fetch ${dataConfig.url}: ${resp.status}`);
|
|
1627
|
-
if (dataConfig.type === 'topojson') return topojsonToFeatureCollection(await resp.json(), dataConfig.object);
|
|
1628
|
-
return sanitizeGeoJSON(await resp.json());
|
|
1649
|
+
if (dataConfig.type === 'topojson') return processFeatureCollection(topojsonToFeatureCollection(await resp.json(), dataConfig.object), dataConfig);
|
|
1650
|
+
return processFeatureCollection(sanitizeGeoJSON(await resp.json()), dataConfig);
|
|
1629
1651
|
}
|
|
1630
1652
|
// every other format (json, jsonstat, ndjson, rss, kml, gml, geobuf,
|
|
1631
1653
|
// flatgeobuf, geopackage, parquet, ...) through data.js, as flat
|
|
@@ -3100,7 +3122,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3100
3122
|
'sizepow', 'rangescale', 'minvalue', 'maxvalue', 'markersize', 'boxopacity', 'outlierscale', 'valuescale',
|
|
3101
3123
|
'brightness', 'fractionscale', 'dopacityscale', 'dopacitypow', 'gridwidthpx', 'textscale', 'rangecentervalue',
|
|
3102
3124
|
'shadowblur', 'shadowdx', 'shadowdy', 'maxshadow', 'offsetx', 'offsety', 'gridx', 'boxmargin', 'borderwidth',
|
|
3103
|
-
'borderradius', 'maxcharts', 'archeight'];
|
|
3125
|
+
'borderradius', 'maxcharts', 'archeight', 'alignmargin'];
|
|
3104
3126
|
function toNumberIfNumeric(v) {
|
|
3105
3127
|
if (typeof v !== 'string' || !v.trim()) return v;
|
|
3106
3128
|
const n = Number(v);
|
|
@@ -3592,6 +3614,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3592
3614
|
// The ocean of flat's world / orthographic maps is a {type:"Sphere"} feature: the whole map disc,
|
|
3593
3615
|
// filled with the layer's color. Here it is the surface of the map (the globe, or the flat
|
|
3594
3616
|
// map): a background layer under everything, in that color and opacity.
|
|
3617
|
+
// the themes are drawn once, as in flat, even where the map repeats its world (the themecopies map option:
|
|
3618
|
+
// true repeats them on every world copy)
|
|
3619
|
+
function applyThemeCopies(map, value) {
|
|
3620
|
+
if (value !== true && value !== 'true') {
|
|
3621
|
+
if (!map.__ixOrigGetRenderWorldCopies) map.__ixOrigGetRenderWorldCopies = map.getRenderWorldCopies;
|
|
3622
|
+
map.getRenderWorldCopies = () => false;
|
|
3623
|
+
} else if (map.__ixOrigGetRenderWorldCopies) {
|
|
3624
|
+
map.getRenderWorldCopies = map.__ixOrigGetRenderWorldCopies;
|
|
3625
|
+
delete map.__ixOrigGetRenderWorldCopies;
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3595
3628
|
const SPHERE_BACKDROP_LAYER_ID = '__ixmaps_gl_sphere_backdrop';
|
|
3596
3629
|
function applySphereBackdrop(map, style) {
|
|
3597
3630
|
const cs = style && style.colorscheme;
|
|
@@ -3631,6 +3664,23 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3631
3664
|
// basemap credit, bottom right — same line, same pill: white, 12px
|
|
3632
3665
|
// radius, 12px/20px Helvetica Neue, links rgba(0,0,0,0.75)).
|
|
3633
3666
|
// ---------------------------------------------------------------
|
|
3667
|
+
// On a dark map (the effective darkness, as the legend and the tooltip use it) the MapLibre control buttons
|
|
3668
|
+
// (zoom, compass) and the page's attribution are dark too: the class ixgl-dark on the map element switches them
|
|
3669
|
+
let _controlLookCss = false;
|
|
3670
|
+
function applyControlLook(el, dark) {
|
|
3671
|
+
if (!_controlLookCss && typeof document !== 'undefined') {
|
|
3672
|
+
_controlLookCss = true;
|
|
3673
|
+
const css = document.createElement('style');
|
|
3674
|
+
css.textContent = '.ixgl-dark .maplibregl-ctrl-group{background:#222!important;box-shadow:0 0 0 1px rgba(255,255,255,.15)!important}'
|
|
3675
|
+
+ '.ixgl-dark .maplibregl-ctrl-group button+button{border-top:1px solid #444!important}'
|
|
3676
|
+
+ '.ixgl-dark .maplibregl-ctrl-group button:not(:disabled):hover{background-color:#3a3a3a!important}'
|
|
3677
|
+
+ '.ixgl-dark .maplibregl-ctrl-group .maplibregl-ctrl-icon{filter:invert(1)}'
|
|
3678
|
+
+ '.ixgl-dark .ixmaps-gl-attribution{background:#222!important;color:#aaa!important}'
|
|
3679
|
+
+ '.ixgl-dark .ixmaps-gl-attribution a{color:#ddd!important}';
|
|
3680
|
+
document.head.appendChild(css);
|
|
3681
|
+
}
|
|
3682
|
+
if (el && el.classList) el.classList.toggle('ixgl-dark', !!dark);
|
|
3683
|
+
}
|
|
3634
3684
|
let _attributionCss = false;
|
|
3635
3685
|
function createAttribution(map) {
|
|
3636
3686
|
if (!_attributionCss && typeof document !== 'undefined') {
|
|
@@ -3937,11 +3987,29 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3937
3987
|
// pane, the shared look (fonts included) on the container
|
|
3938
3988
|
return html ? { html: tooltipPaneHtml(html), style: Object.assign({ padding: '0.5em 0.7em', maxWidth: '440px' }, tooltipLook()) } : null;
|
|
3939
3989
|
}
|
|
3990
|
+
// flat's click-to-action hook (ui/js/tools/tooltip_to_action.js): ixmaps.onItemAction(szId, item, themeObj) on a
|
|
3991
|
+
// click on an item — item.szSelectionId is the value of the theme's selection field — and onItemAction(null, null)
|
|
3992
|
+
// on a click on the map background
|
|
3993
|
+
function callItemAction(rt, object) {
|
|
3994
|
+
const hook = pageIxmaps() && pageIxmaps().onItemAction;
|
|
3995
|
+
if (typeof hook !== 'function') return;
|
|
3996
|
+
try {
|
|
3997
|
+
if (!rt) { hook.call(global.ixmaps, null, null); return; }
|
|
3998
|
+
const field = selectionFieldOf(rt);
|
|
3999
|
+
const props = object.properties || {};
|
|
4000
|
+
// a single record, or an aggregated item's first one
|
|
4001
|
+
const raw = props.raw || props.firstRaw || props.titleRaw || (props.raws && props.raws[0]) || props;
|
|
4002
|
+
const value = field && raw ? raw[field] : null;
|
|
4003
|
+
if (value == null) return;
|
|
4004
|
+
hook.call(global.ixmaps, themeIdOf(rt) + '::' + value, { szSelectionId: String(value) }, getThemeObj(themeIdOf(rt)));
|
|
4005
|
+
} catch (err) { console.error('[ixmaps-gl] onItemAction failed:', err); }
|
|
4006
|
+
}
|
|
3940
4007
|
function clickFor(picked) {
|
|
3941
4008
|
const info = picked && Object.assign({}, picked, { object: pickedFeature(picked.object) });
|
|
3942
4009
|
if (info && info.object && info.layer) {
|
|
3943
4010
|
const rt = findRuntimeForLayerId(info.layer.id);
|
|
3944
4011
|
if (rt) {
|
|
4012
|
+
callItemAction(rt, info.object);
|
|
3945
4013
|
// Only a Point geometry's own coordinates ARE an
|
|
3946
4014
|
// [lng,lat] pair (every point/bubble/dot/chart-cluster
|
|
3947
4015
|
// layer) — a CHOROPLETH polygon's geometry.coordinates is
|
|
@@ -3972,6 +4040,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3972
4040
|
}
|
|
3973
4041
|
}
|
|
3974
4042
|
// clicked empty map space (or a non-tooltippable layer) -> unpin
|
|
4043
|
+
callItemAction(null, null);
|
|
3975
4044
|
pinned = null;
|
|
3976
4045
|
pinnedTooltipEl.style.display = 'none';
|
|
3977
4046
|
return false;
|
|
@@ -5313,11 +5382,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5313
5382
|
// world map can't open fully zoomed out. The deck.gl overlay takes
|
|
5314
5383
|
// its `repeat` from map.getRenderWorldCopies(), so the themes are
|
|
5315
5384
|
// drawn on every world copy too (flat draws them once; only its
|
|
5316
|
-
// basemap tiles wrap)
|
|
5385
|
+
// basemap tiles wrap) — unless themecopies is off, which is the default
|
|
5386
|
+
// (see applyThemeCopies). .options({worldcopies: false}) draws the
|
|
5317
5387
|
// world once, at the cost of that zoom limit.
|
|
5318
5388
|
renderWorldCopies: this._engineOptions.worldcopies !== false
|
|
5319
5389
|
});
|
|
5320
5390
|
engineOptionsByMap.set(map, this._engineOptions);
|
|
5391
|
+
// themecopies (default: off): the map keeps its world copies (free panning past the world's edge, empty
|
|
5392
|
+
// space beside it) but the deck.gl overlay, which asks map.getRenderWorldCopies() for its `repeat`, draws the
|
|
5393
|
+
// themes once, as flat does. .options({themecopies: true}) repeats them. MapLibre's own transform does not
|
|
5394
|
+
// use this method.
|
|
5395
|
+
applyThemeCopies(map, this._engineOptions.themecopies);
|
|
5321
5396
|
sphereBackdrops.forEach(style => applySphereBackdrop(map, style));
|
|
5322
5397
|
// the live-SVG pane USER charts render into (see chartHostFor) —
|
|
5323
5398
|
// registered here, keyed by this map's options object, so every
|
|
@@ -5446,6 +5521,16 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5446
5521
|
// the page's attribution (.attribution(), Map option attribution)
|
|
5447
5522
|
const attributionBox = createAttribution(map);
|
|
5448
5523
|
attributionBox.set(this._attributionText != null ? this._attributionText : this.mapOptions.attribution);
|
|
5524
|
+
// dark controls on a dark map; re-read when a style (basemap) loads and when basemapopacity is set
|
|
5525
|
+
const refreshControlLook = () => {
|
|
5526
|
+
const o = parseFloat(this._engineOptions.basemapopacity);
|
|
5527
|
+
const legendBackground = this.mapOptions.legendBackground || this.mapOptions.legendbackground;
|
|
5528
|
+
applyControlLook(el, typeof this.__effectiveDark === 'function'
|
|
5529
|
+
? this.__effectiveDark(isNaN(o) ? 1 : o, this.mapOptions.mapType, legendBackground) : false);
|
|
5530
|
+
};
|
|
5531
|
+
this._refreshControlLook = refreshControlLook;
|
|
5532
|
+
refreshControlLook();
|
|
5533
|
+
map.on('style.load', refreshControlLook);
|
|
5449
5534
|
this._setAttribution = text => attributionBox.set(text);
|
|
5450
5535
|
|
|
5451
5536
|
if (this._legendHtml) {
|
|
@@ -5586,18 +5671,19 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5586
5671
|
add: (layerBuilder, flag) => {
|
|
5587
5672
|
layerBuilder = unwrapLayer(layerBuilder);
|
|
5588
5673
|
_chainedLayers = _chainedLayers.then(() => {
|
|
5674
|
+
let slot = null;
|
|
5589
5675
|
if (/replace/i.test(flag || '')) {
|
|
5590
5676
|
const def = layerBuilder.definition();
|
|
5591
5677
|
const id = (def.style && def.style.name) || (def.meta && def.meta.name);
|
|
5592
|
-
if (id) removeThemeById(id);
|
|
5678
|
+
if (id) { slot = captureSlot(id); removeThemeById(id); }
|
|
5593
5679
|
}
|
|
5594
|
-
return engineApi.defineLayer(layerBuilder);
|
|
5680
|
+
return engineApi.defineLayer(layerBuilder, slot);
|
|
5595
5681
|
}).catch(err => console.error('[ixmaps-gl] map.add():', err));
|
|
5596
5682
|
return engineApi;
|
|
5597
5683
|
},
|
|
5598
5684
|
replace: (id, layerBuilder, flag) => {
|
|
5599
5685
|
layerBuilder = unwrapLayer(layerBuilder);
|
|
5600
|
-
_chainedLayers = _chainedLayers.then(() => { removeThemeById(id); return engineApi.defineLayer(layerBuilder); })
|
|
5686
|
+
_chainedLayers = _chainedLayers.then(() => { const slot = captureSlot(id); removeThemeById(id); return engineApi.defineLayer(layerBuilder, slot); })
|
|
5601
5687
|
.catch(err => console.error('[ixmaps-gl] map.replace():', err));
|
|
5602
5688
|
return engineApi;
|
|
5603
5689
|
},
|
|
@@ -5612,11 +5698,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5612
5698
|
layerBuilder = unwrapLayer(layerBuilder);
|
|
5613
5699
|
const f = String(flag || '');
|
|
5614
5700
|
_chainedLayers = _chainedLayers.then(() => {
|
|
5701
|
+
let slot = null;
|
|
5615
5702
|
if (!/force/i.test(f)) {
|
|
5616
5703
|
if (/replace/i.test(f)) {
|
|
5617
5704
|
const def = layerBuilder.definition();
|
|
5618
5705
|
const id = (def.style && def.style.name) || (def.meta && def.meta.name);
|
|
5619
|
-
if (id) removeThemeById(id);
|
|
5706
|
+
if (id) { slot = captureSlot(id); removeThemeById(id); }
|
|
5620
5707
|
} else if (/clearcharts|clear|clearall/i.test(f)) {
|
|
5621
5708
|
const charts = /clearcharts/i.test(f);
|
|
5622
5709
|
const doomed = runtimes.filter(rt => !flatFlag(rt.flags, 'LOCKED') && (!charts || rt.flags.has('CHART')));
|
|
@@ -5625,7 +5712,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5625
5712
|
byName.forEach((list, name) => removeRuntimes(name, list));
|
|
5626
5713
|
}
|
|
5627
5714
|
}
|
|
5628
|
-
return engineApi.defineLayer(layerBuilder);
|
|
5715
|
+
return engineApi.defineLayer(layerBuilder, slot);
|
|
5629
5716
|
}).catch(err => console.error('[ixmaps-gl] map.newTheme():', err));
|
|
5630
5717
|
return engineApi;
|
|
5631
5718
|
},
|
|
@@ -5857,7 +5944,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5857
5944
|
// exactly how this engine's own facet-filter/style setters below
|
|
5858
5945
|
// already push responsibility for "what should happen" to the
|
|
5859
5946
|
// caller rather than guessing.
|
|
5860
|
-
defineLayer: (layerBuilder) => defineFromDefinition(unwrapLayer(layerBuilder).definition()),
|
|
5947
|
+
defineLayer: (layerBuilder, slot) => defineFromDefinition(unwrapLayer(layerBuilder).definition(), slot),
|
|
5861
5948
|
// GL-PORT COMPAT: real ixmaps-flat's map handle, as a page gets it
|
|
5862
5949
|
// from `myMap.then(map => map.view(...).options(...).layer(a)
|
|
5863
5950
|
// .layer(b))` — the common multi-layer idiom — is chainable on the
|
|
@@ -5879,6 +5966,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5879
5966
|
if (o && typeof o === 'object') {
|
|
5880
5967
|
Object.assign(builder._engineOptions, o);
|
|
5881
5968
|
// read only when the MapLibre map is created — switched here at runtime
|
|
5969
|
+
if ('basemapopacity' in o && builder._refreshControlLook) builder._refreshControlLook();
|
|
5970
|
+
if ('themecopies' in o) { applyThemeCopies(map, o.themecopies); map.triggerRepaint(); }
|
|
5882
5971
|
if ('worldcopies' in o) map.setRenderWorldCopies(o.worldcopies !== false && o.worldcopies !== 'false');
|
|
5883
5972
|
refresh();
|
|
5884
5973
|
}
|
|
@@ -6017,6 +6106,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6017
6106
|
if (removed) { refresh(); notifyRedraw(); }
|
|
6018
6107
|
return removed;
|
|
6019
6108
|
}
|
|
6109
|
+
// where the theme(s) removeThemeById(id) would remove stand in the stack: the first remaining
|
|
6110
|
+
// theme after them (null = they were on top). Taken BEFORE the removal, handed to defineLayer.
|
|
6111
|
+
function captureSlot(id) {
|
|
6112
|
+
const named = runtimes.filter(r => (r.style && r.style.name === id) || (r.meta && r.meta.name === id));
|
|
6113
|
+
const doomed = named.length ? named : runtimes.filter(r => r.name === id);
|
|
6114
|
+
if (!doomed.length) return null;
|
|
6115
|
+
const last = Math.max(...doomed.map(r => runtimes.indexOf(r)));
|
|
6116
|
+
const anchor = runtimes.slice(last + 1).find(r => !doomed.includes(r) && !doomed.some(d => d._subTheme === r)) || null;
|
|
6117
|
+
return { anchor };
|
|
6118
|
+
}
|
|
6119
|
+
|
|
6020
6120
|
// flat addresses a theme by its id — style.name / meta.name (several
|
|
6021
6121
|
// themes may share one layer), else the layer name
|
|
6022
6122
|
function removeThemeById(id) {
|
|
@@ -6187,11 +6287,16 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6187
6287
|
// flat itself does ("defining a theme with the same name replaces it
|
|
6188
6288
|
// in place"; a page's live-refresh loop relies on it, e.g. the NYC
|
|
6189
6289
|
// status page re-defining its two themes every 10s).
|
|
6190
|
-
|
|
6290
|
+
// `slot` (from captureSlot) is where a replaced theme stood in the stack: the replace paths
|
|
6291
|
+
// remove the old theme first and pass it on, so the new one is not appended on top
|
|
6292
|
+
async function defineFromDefinition(def, slot) {
|
|
6191
6293
|
if (validation) validation.definition(def);
|
|
6192
6294
|
const pdef = projectThemeToDefinition(def);
|
|
6193
6295
|
const replacedId = (pdef.meta && pdef.meta.name) || (pdef.style && pdef.style.name);
|
|
6194
|
-
if (replacedId)
|
|
6296
|
+
if (replacedId) {
|
|
6297
|
+
if (!slot) slot = captureSlot(replacedId);
|
|
6298
|
+
removeThemeById(replacedId);
|
|
6299
|
+
}
|
|
6195
6300
|
let spec = normalizeTheme(pdef);
|
|
6196
6301
|
let raw;
|
|
6197
6302
|
const deferred = deferredFeatureLoad(spec, map.getZoom());
|
|
@@ -6218,7 +6323,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6218
6323
|
rt._zoomToPending = wantsZoomToExtent(spec.flags);
|
|
6219
6324
|
rt._dataSourceKey = named ? 'name:' + spec.data.name
|
|
6220
6325
|
: 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) });
|
|
6221
|
-
|
|
6326
|
+
// a replacement keeps the stacking position (draw order, legend order) of the theme it replaces:
|
|
6327
|
+
// it goes in front of the theme that followed the old one, still on the map after the await above
|
|
6328
|
+
const slotAt = slot && slot.anchor ? runtimes.indexOf(slot.anchor) : -1;
|
|
6329
|
+
if (slotAt >= 0) runtimes.splice(slotAt, 0, rt); else runtimes.push(rt);
|
|
6222
6330
|
if (fc._sphere) applySphereBackdrop(map, rt.style);
|
|
6223
6331
|
_globalThemeRegistry.set(rt.name, rt);
|
|
6224
6332
|
if (rt.meta && rt.meta.name && rt.meta.name !== rt.name) _globalThemeRegistry.set(rt.meta.name, rt);
|
|
@@ -6268,10 +6376,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6268
6376
|
for (let i = 0; i < themes.length; i++) {
|
|
6269
6377
|
const t = themes[i] || {};
|
|
6270
6378
|
const name = (t.style && t.style.name) || (t.meta && t.meta.name);
|
|
6271
|
-
|
|
6379
|
+
let slot = null;
|
|
6380
|
+
if (/replace/i.test(f) && name) { slot = captureSlot(name); removeThemesNamed(name); }
|
|
6272
6381
|
else if (i === 0 && !/add/i.test(f)) [...new Set(runtimes.map(r => r.name))].forEach(n => engineApi.removeTheme(n));
|
|
6273
6382
|
try {
|
|
6274
|
-
report.themes.push(await defineFromDefinition(withoutProjectCode(t, report)));
|
|
6383
|
+
report.themes.push(await defineFromDefinition(withoutProjectCode(t, report), slot));
|
|
6275
6384
|
} catch (e) {
|
|
6276
6385
|
report.skipped.push({ layer: t.layer, reason: e.message });
|
|
6277
6386
|
}
|
|
@@ -7409,7 +7518,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7409
7518
|
: 1;
|
|
7410
7519
|
}
|
|
7411
7520
|
|
|
7412
|
-
function valueRadius(value, zoom, style, mapOptions, flags, maxSizeValue) {
|
|
7521
|
+
function valueRadius(value, zoom, style, mapOptions, flags, maxSizeValue, sizeBound) {
|
|
7413
7522
|
const zoomFactor = objectZoomFactor(zoom, mapOptions);
|
|
7414
7523
|
// FIXSIZE (a symbol chart; PLOT / GRIDSIZE size their markers
|
|
7415
7524
|
// themselves): one radius for every item, whatever its value — the
|
|
@@ -7452,7 +7561,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7452
7561
|
if (flags && flags.has('SYMBOL') && !flags.has('BUBBLE') && !flags.has('USER')
|
|
7453
7562
|
&& !flags.has('FIXSIZE') && !flags.has('PLOT') && !flags.has('GRIDSIZE')
|
|
7454
7563
|
&& flags.has('CATEGORICAL') && !flags.has('AGGREGATE') && !flatFlag(flags, 'GROUP')
|
|
7455
|
-
&& !style.sizefield) {
|
|
7564
|
+
&& !style.sizefield && !sizeBound) {
|
|
7456
7565
|
const fixed = NORMAL_RADIUS_PX * (styleNum(style.scale) || 1);
|
|
7457
7566
|
return Math.max(VALUE_RADIUS_MIN, Math.min(VALUE_RADIUS_MAX, fixed * zoomFactor));
|
|
7458
7567
|
}
|
|
@@ -7825,6 +7934,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7825
7934
|
return out;
|
|
7826
7935
|
}
|
|
7827
7936
|
|
|
7937
|
+
// the outline colour of a flat bubble icon: none for NOLINES / linecolor "none", else linecolor, else the
|
|
7938
|
+
// class colour's darker derivate — and none when there is neither (an unfilled "none" class has no colour
|
|
7939
|
+
// to derive from; with a linecolor its outline must still draw)
|
|
7940
|
+
function bubbleStrokeRgb(look, rgb) {
|
|
7941
|
+
return look.noStroke ? null : (look.stroke || (rgb ? flatDerivateRgb(rgb, 0.7) : null));
|
|
7942
|
+
}
|
|
7943
|
+
|
|
7828
7944
|
// ChartColors.textColor (colorscheme.js 104-116): darker on light colors
|
|
7829
7945
|
function flatChartTextRgb(rgb) {
|
|
7830
7946
|
return flatDerivateRgb(rgb, rgb[0] + rgb[1] + rgb[2] > 450 ? 0.6 : 3);
|
|
@@ -12103,6 +12219,91 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12103
12219
|
return Array.isArray(cs) && cs.length === 1 && String(cs[0]).trim().toLowerCase() === 'none';
|
|
12104
12220
|
}
|
|
12105
12221
|
|
|
12222
|
+
// an external symbol file (.svg/.png/.jpeg) named by `symbols` — flat draws it as an
|
|
12223
|
+
// <image> (maptheme.js 21743, 22168), not as the chart's own shape. null for a shape name
|
|
12224
|
+
_urlSymbolOf(sym) {
|
|
12225
|
+
if (typeof sym !== 'string') return null;
|
|
12226
|
+
const s = sym.trim();
|
|
12227
|
+
// a data: URI is a symbol file too, and the only kind fetch() can read on a file:// page
|
|
12228
|
+
return /\.(svg|png|jpe?g)([?#]|$)/i.test(s) || /^data:image\/(svg\+xml|png|jpe?g)[;,]/i.test(s) ? s : null;
|
|
12229
|
+
}
|
|
12230
|
+
|
|
12231
|
+
// the symbol file as an IconLayer icon descriptor, NOT tinted (flat doesn't recolor an
|
|
12232
|
+
// <image> either); fetched and rasterized once per URL, shared by SEQUENCE and plain symbols
|
|
12233
|
+
_urlSymbolIcon(u) {
|
|
12234
|
+
if (!this._urlIconCache) this._urlIconCache = new Map();
|
|
12235
|
+
if (!this._urlIconCache.has(u)) {
|
|
12236
|
+
// the placeholder is a transparent 1x1 PNG, NOT the raw symbol
|
|
12237
|
+
// URL: deck.gl would immediately try to load that itself, and an
|
|
12238
|
+
// SVG without natural dimensions (a viewBox only, the bus
|
|
12239
|
+
// pictogram of the City-moving page) makes Chrome's
|
|
12240
|
+
// createImageBitmap throw — a console error and a part that
|
|
12241
|
+
// never renders. The real raster replaces it below
|
|
12242
|
+
const entry = { url: TRANSPARENT_1PX_PNG, width: 256, height: 256, anchorX: 128, anchorY: 128, mask: false, id: 'seq-sym-pending:' + u };
|
|
12243
|
+
this._urlIconCache.set(u, entry);
|
|
12244
|
+
// fetch the symbol ONCE and rasterize it into the descriptor's
|
|
12245
|
+
// own 256px cell here: deck then always sees exactly the declared
|
|
12246
|
+
// dimensions, and dimension-less SVGs work like any other. The
|
|
12247
|
+
// redraw lets the next getIcon see the patched descriptor
|
|
12248
|
+
fetch(u).then(r => r.ok ? r.text() : null).then(text => new Promise((resolve, reject) => {
|
|
12249
|
+
if (typeof text !== 'string' || !/<svg[\s>]/i.test(text)) { reject(new Error('not svg')); return; }
|
|
12250
|
+
if (!/\swidth\s*=/i.test(text) || !/\sheight\s*=/i.test(text)) {
|
|
12251
|
+
const vb = text.match(/viewBox\s*=\s*["']([^"']*)["']/i);
|
|
12252
|
+
const p = vb && vb[1].trim().split(/[\s,]+/).map(Number);
|
|
12253
|
+
if (p && p.length === 4 && isFinite(p[2]) && p[2] > 0 && isFinite(p[3]) && p[3] > 0) {
|
|
12254
|
+
text = text.replace(/<svg/i, '<svg width="' + p[2] + '" height="' + p[3] + '"');
|
|
12255
|
+
}
|
|
12256
|
+
}
|
|
12257
|
+
const img = new Image();
|
|
12258
|
+
img.onload = () => resolve(img);
|
|
12259
|
+
img.onerror = () => reject(new Error('svg decode failed'));
|
|
12260
|
+
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(text);
|
|
12261
|
+
})).then(img => {
|
|
12262
|
+
const size = entry.width;
|
|
12263
|
+
const canvas = document.createElement('canvas');
|
|
12264
|
+
canvas.width = size; canvas.height = size;
|
|
12265
|
+
const ctx = canvas.getContext('2d');
|
|
12266
|
+
const s = Math.min(size / (img.naturalWidth || size), size / (img.naturalHeight || size));
|
|
12267
|
+
const dw = (img.naturalWidth || size) * s, dh = (img.naturalHeight || size) * s;
|
|
12268
|
+
ctx.drawImage(img, (size - dw) / 2, (size - dh) / 2, dw, dh);
|
|
12269
|
+
entry.url = canvas.toDataURL('image/png');
|
|
12270
|
+
entry.id = 'seq-sym-raster:' + u;
|
|
12271
|
+
if (this._triggerRedraw) this._triggerRedraw();
|
|
12272
|
+
}).catch(() => {
|
|
12273
|
+
// a symbol FILE of a file:// page can't be fetched (nor loaded by deck): say so once
|
|
12274
|
+
let onDisk = false;
|
|
12275
|
+
try { onDisk = new URL(u, location.href).protocol === 'file:'; } catch (e) { /* not a URL */ }
|
|
12276
|
+
if (onDisk && !this._warnedFileSymbol) {
|
|
12277
|
+
this._warnedFileSymbol = true;
|
|
12278
|
+
console.warn('[ixmaps-gl] symbol file "' + u + '" cannot be loaded from a file:// page — serve the page over http(s) or give the symbol as a data: URI');
|
|
12279
|
+
}
|
|
12280
|
+
// not an SVG (a PNG/JPEG symbol): deck's own URL loading takes
|
|
12281
|
+
// it from here — and a pre-sized SVG that failed to rasterize
|
|
12282
|
+
// gets that same second chance
|
|
12283
|
+
entry.url = u;
|
|
12284
|
+
entry.id = 'seq-sym-natural:' + u;
|
|
12285
|
+
if (this._triggerRedraw) this._triggerRedraw();
|
|
12286
|
+
});
|
|
12287
|
+
}
|
|
12288
|
+
return this._urlIconCache.get(u);
|
|
12289
|
+
}
|
|
12290
|
+
|
|
12291
|
+
// a size field is bound (flat's szSizeField): .binding({size}) or style.sizefield, live —
|
|
12292
|
+
// setSizeField() rebinds at runtime. '$item$' is not a field
|
|
12293
|
+
_sizeBound() { return !!(this.binding.size && this.binding.size !== '$item$'); }
|
|
12294
|
+
|
|
12295
|
+
// the symbol file of a plain (non-SEQUENCE) item, or null: the same class→symbol /
|
|
12296
|
+
// symbolfield mapping as _resolveSymbolShape, before it folds unknown names to 'circle'
|
|
12297
|
+
_resolveSymbolUrl(props) {
|
|
12298
|
+
const s = this.style.symbols;
|
|
12299
|
+
if (!Array.isArray(s)) return null;
|
|
12300
|
+
if (!this.style.symbolfield) {
|
|
12301
|
+
const cat = props && props.cat != null ? props.cat : 0;
|
|
12302
|
+
return this._urlSymbolOf(s[Math.min(cat, s.length - 1)]);
|
|
12303
|
+
}
|
|
12304
|
+
return this._urlSymbolOf(this._resolveSymbolShape(props));
|
|
12305
|
+
}
|
|
12306
|
+
|
|
12106
12307
|
_buildSingleIcon(colorRgb, opacity, shape, borderColorRgb, borderWidth, borderOpacity = 1) {
|
|
12107
12308
|
if (!colorRgb) { colorRgb = [128, 128, 128]; opacity = 0; } // no class colour (["none"]): unfilled
|
|
12108
12309
|
shape = shape || 'circle';
|
|
@@ -13681,7 +13882,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13681
13882
|
// the item's class (range classes set it for groups too — a negative
|
|
13682
13883
|
// group total would make dominant() pick the wrong class)
|
|
13683
13884
|
const cat = d.properties.cat != null ? d.properties.cat : (d.properties.counts ? dominant(d.properties.counts) : 0);
|
|
13684
|
-
const radiusPx = valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue);
|
|
13885
|
+
const radiusPx = valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue, this._sizeBound());
|
|
13685
13886
|
const size = radiusPx / pxPerUnit;
|
|
13686
13887
|
const chart = this._userChartSvg(draw, name, theme, flag, d, value, cat, size, sizeValueOf(d));
|
|
13687
13888
|
if (!chart) continue;
|
|
@@ -14321,7 +14522,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14321
14522
|
// GRIDSIZE: every chart the size of its grid cell, whatever its value
|
|
14322
14523
|
// (_gridChartRadiusPx); otherwise valueRadius
|
|
14323
14524
|
const gridR = this._gridChartRadiusPx(zoom, liveZoom);
|
|
14324
|
-
const vRadius = v => (gridR != null ? gridR : valueRadius(v, liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue));
|
|
14525
|
+
const vRadius = v => (gridR != null ? gridR : valueRadius(v, liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue, this._sizeBound()));
|
|
14325
14526
|
// AGGREGATE sums without a normalsizevalue: flat's default is the
|
|
14326
14527
|
// largest aggregated value (defaultNormalSizeValue leaves it unset, the
|
|
14327
14528
|
// sizes depend on the aggregation)
|
|
@@ -14641,7 +14842,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14641
14842
|
if (!flatLook) return this._buildSingleIcon(rgb, fillOpacity, shapeOf(d.properties), singleBorderColorRgb, singleBorderWidthPx);
|
|
14642
14843
|
const look = flatLook(d);
|
|
14643
14844
|
return this._buildSingleIcon(rgb, look.fillOpacity, shapeOf(d.properties),
|
|
14644
|
-
|
|
14845
|
+
bubbleStrokeRgb(look, rgb), look.width, look.strokeOpacity);
|
|
14645
14846
|
};
|
|
14646
14847
|
// flat's symbol outline (maptheme.js 21595-21610 and 21950-21997,
|
|
14647
14848
|
// drawn 21625-21634 / 21995-22035): every SYMBOL shape is stroked,
|
|
@@ -14676,7 +14877,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14676
14877
|
// routing goes by the final id (layerRuntimeById). A symbol's outline
|
|
14677
14878
|
// is centered on its radius (flat); BUBBLE's border keeps the
|
|
14678
14879
|
// icon's look, inside the radius
|
|
14679
|
-
if (combined.length && !alignOf.active && combined.every(d => singleSymbol(d) && shapeOf(d.properties) === 'circle')) {
|
|
14880
|
+
if (combined.length && !alignOf.active && combined.every(d => singleSymbol(d) && shapeOf(d.properties) === 'circle' && !this._resolveSymbolUrl(d.properties))) {
|
|
14680
14881
|
const radiusPx = d => vRadius(sizeValueOf(d));
|
|
14681
14882
|
// a CATEGORICAL BUBBLE: flat's look (flatBubbleLook) in screen px,
|
|
14682
14883
|
// its stroke centered on the radius as flat's SVG circle
|
|
@@ -14705,6 +14906,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14705
14906
|
getPosition: d => d.geometry.coordinates,
|
|
14706
14907
|
getIcon: d => {
|
|
14707
14908
|
if (!singleSymbol(d)) return this._buildBubbleIcon(d.properties.counts, this.categoryColorsRgb, this._groupIsolation(d), Array.isArray(this.style.symbols) ? this.style.symbols : null, flatLook && flatLook(d));
|
|
14909
|
+
const symbolUrl = this._resolveSymbolUrl(d.properties);
|
|
14910
|
+
if (symbolUrl) return this._urlSymbolIcon(symbolUrl);
|
|
14708
14911
|
if (flatLook) return singleIcon(d);
|
|
14709
14912
|
const shape = shapeOf(d.properties), rgb = outlineRgb(d);
|
|
14710
14913
|
if (!symbolOutline || !rgb) return this._buildSingleIcon((this.categoryColorsRgb || [])[d.properties.cat], fillOpacity, shape, rgb, rgb ? singleBorderWidthPx : 0);
|
|
@@ -14994,60 +15197,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14994
15197
|
// it itself (its ImageBitmap loader falls back to <img>, which
|
|
14995
15198
|
// rasterizes SVG) — NOT tinted: these files carry their own colors,
|
|
14996
15199
|
// and flat doesn't recolor an <image> either
|
|
14997
|
-
const urlSymbolOf = i =>
|
|
14998
|
-
|
|
14999
|
-
return typeof s === 'string' && /\.(svg|png|jpe?g)([?#]|$)/i.test(s.trim()) ? s.trim() : null;
|
|
15000
|
-
};
|
|
15001
|
-
if (!this._urlIconCache) this._urlIconCache = new Map();
|
|
15002
|
-
const urlIcon = u => {
|
|
15003
|
-
if (!this._urlIconCache.has(u)) {
|
|
15004
|
-
// the placeholder is a transparent 1x1 PNG, NOT the raw symbol
|
|
15005
|
-
// URL: deck.gl would immediately try to load that itself, and an
|
|
15006
|
-
// SVG without natural dimensions (a viewBox only, the bus
|
|
15007
|
-
// pictogram of the City-moving page) makes Chrome's
|
|
15008
|
-
// createImageBitmap throw — a console error and a part that
|
|
15009
|
-
// never renders. The real raster replaces it below
|
|
15010
|
-
const entry = { url: TRANSPARENT_1PX_PNG, width: 256, height: 256, anchorX: 128, anchorY: 128, mask: false, id: 'seq-sym-pending:' + u };
|
|
15011
|
-
this._urlIconCache.set(u, entry);
|
|
15012
|
-
// fetch the symbol ONCE and rasterize it into the descriptor's
|
|
15013
|
-
// own 256px cell here: deck then always sees exactly the declared
|
|
15014
|
-
// dimensions, and dimension-less SVGs work like any other. The
|
|
15015
|
-
// redraw lets the next getIcon see the patched descriptor
|
|
15016
|
-
fetch(u).then(r => r.ok ? r.text() : null).then(text => new Promise((resolve, reject) => {
|
|
15017
|
-
if (typeof text !== 'string' || !/<svg[\s>]/i.test(text)) { reject(new Error('not svg')); return; }
|
|
15018
|
-
if (!/\swidth\s*=/i.test(text) || !/\sheight\s*=/i.test(text)) {
|
|
15019
|
-
const vb = text.match(/viewBox\s*=\s*["']([^"']*)["']/i);
|
|
15020
|
-
const p = vb && vb[1].trim().split(/[\s,]+/).map(Number);
|
|
15021
|
-
if (p && p.length === 4 && isFinite(p[2]) && p[2] > 0 && isFinite(p[3]) && p[3] > 0) {
|
|
15022
|
-
text = text.replace(/<svg/i, '<svg width="' + p[2] + '" height="' + p[3] + '"');
|
|
15023
|
-
}
|
|
15024
|
-
}
|
|
15025
|
-
const img = new Image();
|
|
15026
|
-
img.onload = () => resolve(img);
|
|
15027
|
-
img.onerror = () => reject(new Error('svg decode failed'));
|
|
15028
|
-
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(text);
|
|
15029
|
-
})).then(img => {
|
|
15030
|
-
const size = entry.width;
|
|
15031
|
-
const canvas = document.createElement('canvas');
|
|
15032
|
-
canvas.width = size; canvas.height = size;
|
|
15033
|
-
const ctx = canvas.getContext('2d');
|
|
15034
|
-
const s = Math.min(size / (img.naturalWidth || size), size / (img.naturalHeight || size));
|
|
15035
|
-
const dw = (img.naturalWidth || size) * s, dh = (img.naturalHeight || size) * s;
|
|
15036
|
-
ctx.drawImage(img, (size - dw) / 2, (size - dh) / 2, dw, dh);
|
|
15037
|
-
entry.url = canvas.toDataURL('image/png');
|
|
15038
|
-
entry.id = 'seq-sym-raster:' + u;
|
|
15039
|
-
if (this._triggerRedraw) this._triggerRedraw();
|
|
15040
|
-
}).catch(() => {
|
|
15041
|
-
// not an SVG (a PNG/JPEG symbol): deck's own URL loading takes
|
|
15042
|
-
// it from here — and a pre-sized SVG that failed to rasterize
|
|
15043
|
-
// gets that same second chance
|
|
15044
|
-
entry.url = u;
|
|
15045
|
-
entry.id = 'seq-sym-natural:' + u;
|
|
15046
|
-
if (this._triggerRedraw) this._triggerRedraw();
|
|
15047
|
-
});
|
|
15048
|
-
}
|
|
15049
|
-
return this._urlIconCache.get(u);
|
|
15050
|
-
};
|
|
15200
|
+
const urlSymbolOf = i => this._urlSymbolOf(symbols && (symbols[i] || symbols[0]));
|
|
15201
|
+
const urlIcon = u => this._urlSymbolIcon(u);
|
|
15051
15202
|
const shapeOf = i => { const sh = symbols && (symbols[i] || symbols[0]); return normalizeSymbolShape(sh); };
|
|
15052
15203
|
const colorOf = i => this.classRgb(i);
|
|
15053
15204
|
const allParts = [], allGlows = [];
|
|
@@ -15077,7 +15228,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15077
15228
|
const sizeFactor = itemSizeFactor(d);
|
|
15078
15229
|
const partRadius = multi
|
|
15079
15230
|
? v => Math.max(VALUE_RADIUS_MIN, normalR * Math.pow(Math.abs(v) / seqMax, 1 / sizePow) * sizeFactor)
|
|
15080
|
-
: v => valueRadius(v, liveZoom, st, this.mapOptions, this.flags, seqMax);
|
|
15231
|
+
: v => valueRadius(v, liveZoom, st, this.mapOptions, this.flags, seqMax, this._sizeBound());
|
|
15081
15232
|
let layout = rings ? ringsLayout(counts, partRadius)
|
|
15082
15233
|
: sequenceLayout(counts, this.flags, partRadius, { maxRadius: normalR, maxValue: seqMax, nParts: nCats, rangeScale: styleNum(st.rangescale) || 0 });
|
|
15083
15234
|
if (!layout.length) continue;
|
|
@@ -16631,7 +16782,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16631
16782
|
// flat's chart position for this build (flatChartAlignOffset): a
|
|
16632
16783
|
// function item → pixel offset, the same for every part of one chart
|
|
16633
16784
|
// (symbol, glow, shadow, box, title, value text). unit is flat's
|
|
16634
|
-
// normalX(1) on screen, the object zoom × style.scale.
|
|
16785
|
+
// normalX(1) on screen, the object zoom × style.scale. flat keeps a margin of 5 units between the
|
|
16786
|
+
// position and the chart's facing edge for left/right/top/bottom; the gl-only style.alignmargin
|
|
16787
|
+
// changes it (0: the edge touches the position, so a left and a right chart touch each other)
|
|
16635
16788
|
_chartAlignFn(liveZoom, radiusOf, textOnly = false) {
|
|
16636
16789
|
const st = this.style;
|
|
16637
16790
|
const none = () => [0, 0];
|
|
@@ -16640,12 +16793,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16640
16793
|
const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(st.scale) || 1);
|
|
16641
16794
|
const symbol = flatChartBranch(this.flags) === 'symbol';
|
|
16642
16795
|
const normalR = NORMAL_RADIUS_PX * unit; // flat's normalX(chart size / 2)
|
|
16796
|
+
const margin = Number.isFinite(styleNum(st.alignmargin)) ? styleNum(st.alignmargin) : 5;
|
|
16643
16797
|
const cache = new Map();
|
|
16644
16798
|
const fn = d => {
|
|
16645
16799
|
let v = cache.get(d);
|
|
16646
16800
|
if (v) return v;
|
|
16647
16801
|
const r = radiusOf(d);
|
|
16648
|
-
v = flatChartAlignOffset(st, { unit, symbolScale: symbol ? r / (2 * normalR) : 1, half: symbol ? normalR : r, orig: r + (textOnly ? 0 :
|
|
16802
|
+
v = flatChartAlignOffset(st, { unit, symbolScale: symbol ? r / (2 * normalR) : 1, half: symbol ? normalR : r, orig: r + (textOnly ? 0 : margin * unit) });
|
|
16649
16803
|
cache.set(d, v);
|
|
16650
16804
|
return v;
|
|
16651
16805
|
};
|
|
@@ -16824,7 +16978,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16824
16978
|
const textOnly = this.flags.has('TEXTONLY');
|
|
16825
16979
|
const showValues = this.flags.has('VALUES') && !valuesHiddenByScale(this.style, zoom);
|
|
16826
16980
|
if (textOnly && !showValues) return [];
|
|
16827
|
-
const radiusOf = d => valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue);
|
|
16981
|
+
const radiusOf = d => valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue, this._sizeBound());
|
|
16828
16982
|
const alignOf = this._chartAlignFn(liveZoom, radiusOf, textOnly);
|
|
16829
16983
|
const valueScale = styleNum(this.style.valuescale) || 1;
|
|
16830
16984
|
const textOverride = this.style.valuecolor || this.style.textcolor;
|
|
@@ -17385,6 +17539,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17385
17539
|
// several themes of a page
|
|
17386
17540
|
szId: themeIdOf(rt),
|
|
17387
17541
|
szName: rt.name,
|
|
17542
|
+
// flat's szSelectionField: the field a click on an item selects by (the id binding, else the field the
|
|
17543
|
+
// item is positioned by)
|
|
17544
|
+
szSelectionField: selectionFieldOf(rt),
|
|
17388
17545
|
// the filter in force: a runtime one (changeThemeStyle "filter")
|
|
17389
17546
|
// replaces the definition's
|
|
17390
17547
|
szFilter: rt._runtimeFilterExpr || rt._filterExpr || '',
|
|
@@ -17421,6 +17578,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17421
17578
|
get itemA() { return onMap().itemA; }
|
|
17422
17579
|
};
|
|
17423
17580
|
}
|
|
17581
|
+
// the field whose value identifies an item on a click (flat's szSelectionField): binding.id, else the position field
|
|
17582
|
+
// (which a join to a geometry layer turns into binding.lookup)
|
|
17583
|
+
function selectionFieldOf(rt) {
|
|
17584
|
+
const b = rt.binding || {};
|
|
17585
|
+
const f = b.id || b.position || b.lookup;
|
|
17586
|
+
return typeof f === 'string' && f && f !== 'geometry' && !/^geometry$/i.test(f) ? f : null;
|
|
17587
|
+
}
|
|
17424
17588
|
const _warnedOnce = new Set();
|
|
17425
17589
|
function warnOnce(key, msg) { if (!_warnedOnce.has(key)) { _warnedOnce.add(key); console.info(msg); } }
|
|
17426
17590
|
// the theme's records as flat's dbFields/dbRecords (cached per data)
|
|
@@ -17852,6 +18016,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17852
18016
|
function compatRemoveTheme(...a) {
|
|
17853
18017
|
return _mapHandle.remove(a.length >= 2 ? a[1] : a[0]);
|
|
17854
18018
|
}
|
|
18019
|
+
// flat's ixmaps.toggleTheme(szMap, szTheme) (htmlgui.js): show / hide a theme by its id, e.g. a page's on / off buttons
|
|
18020
|
+
function compatToggleTheme(...a) {
|
|
18021
|
+
new themeApi(null, a.length >= 2 ? a[1] : a[0]).toggle();
|
|
18022
|
+
}
|
|
17855
18023
|
function compatSetBasemapOpacity(...a) {
|
|
17856
18024
|
if (a.length >= 3) {
|
|
17857
18025
|
const api = (a[0] && _mapApis[a[0]]) || _lastMapApi;
|
|
@@ -17887,6 +18055,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17887
18055
|
htmlgui_getAttributionString: () => (_lastMapApi && _lastMapApi.getAttribution ? _lastMapApi.getAttribution() : ''),
|
|
17888
18056
|
changeThemeStyle: compatChangeThemeStyle,
|
|
17889
18057
|
removeTheme: compatRemoveTheme,
|
|
18058
|
+
toggleTheme: compatToggleTheme,
|
|
17890
18059
|
// flat's ixmaps.setLocalString(orig, local) (htmlgui.js 3814 →
|
|
17891
18060
|
// Api.setLocalString → Dictionary.replace): a page-wide translation,
|
|
17892
18061
|
// e.g. the migration page's ISO code → country name; usable before
|
|
@@ -17935,7 +18104,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17935
18104
|
flatToMapLibreZoom, mapLibreToFlatZoom, flatViewToMapLibreZoom, mapLibreToFlatViewZoom, flatSvgProjectionOf, flatSvgProjectionByOptions, normalSizeScaleOf, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
|
|
17936
18105
|
fetchLayerData, QUERY_HANDOFF, parseCsvText, dataTableRows, geometryRowsToFeatureCollection, filterFlatValues, applyField100, field100Binding, rangeClassLegendTotals, resolveAggregateValue, classValueSeparate, cellAggregatedValues, oneHot, groupCoLocated, aggregateOnGrid, GridAggregateIndex, flatRangeParts, explicitRangeBreaks, explicitRangeParts, resolvePartsClass, isNoneColor, aggregateField100, valueRadius, itemPlotGeometry, itemAnchor, objectZoomFactor, resolveZoomReference, defaultNormalSizeValue,
|
|
17937
18106
|
applyWhereFilter, joinChartPositions, joinChoroplethFeatures, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatBubbleLook, bubbleIconLook, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
17938
|
-
drawSymbolPath, normalizeSymbolShape,
|
|
18107
|
+
drawSymbolPath, normalizeSymbolShape, bubbleStrokeRgb,
|
|
17939
18108
|
flatFormatValue, themeStyleArgs, flatChangedStyleValue, explicitValueDecimals, legendDecimals,
|
|
17940
18109
|
dataCacheKey, dataCacheDisabled, cachedLayerData, featuresBounds, wantsZoomToExtent, legendIsOn, applySphereBackdrop,
|
|
17941
18110
|
};
|