ixmaps-gl 0.2.24 → 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 +1595 -748
- package/package.json +1 -1
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
|
|
@@ -220,7 +220,7 @@
|
|
|
220
220
|
let IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer, PathLayer, PathStyleExtension;
|
|
221
221
|
// PieSliceLayer (see makePieSliceLayer) and BarShapeLayer
|
|
222
222
|
// (makeBarShapeLayer) are derived from ScatterplotLayer once deck.gl is there
|
|
223
|
-
let PieSliceLayer, BarShapeLayer;
|
|
223
|
+
let PieSliceLayer, BarShapeLayer, FlowPathLayer;
|
|
224
224
|
|
|
225
225
|
// CHART|PIE / DONUT slices (_buildPieLayers): one instance per slice — a
|
|
226
226
|
// ScatterplotLayer circle (billboarded, pixel radius, pixel offset, the
|
|
@@ -242,6 +242,111 @@
|
|
|
242
242
|
// without anti-aliasing between them (a hard partition: no background
|
|
243
243
|
// seam shows between two slices); the circle's rim keeps
|
|
244
244
|
// ScatterplotLayer's own anti-aliasing.
|
|
245
|
+
// flat's VECTOR|DASH (maptheme.js 18240-18256, the bezier branch):
|
|
246
|
+
// the flow lines are dashed — one long dash, a small gap — and the
|
|
247
|
+
// dash pattern MOVES toward the target: flat animates each SVG line's
|
|
248
|
+
// stroke-dashoffset from d1+d2+random down to random (1-2s, infinite),
|
|
249
|
+
// the offset decreasing walking the pattern along the path direction.
|
|
250
|
+
// Here the same in the fragment shader — but deck.gl tessellates a
|
|
251
|
+
// path into per-SEGMENT quads whose vPathPosition.y restarts at 0 at
|
|
252
|
+
// every segment vertex (and is in half-stroke-width units, not
|
|
253
|
+
// pixels), so the pattern is NOT continuous from the segment
|
|
254
|
+
// coordinate alone. deck's own PathStyleExtension with dashMode
|
|
255
|
+
// 'path' (HIGH_PRECISION_DASH) supplies the missing cumulative term:
|
|
256
|
+
// its calculateDashMetrics fills per-segment [distance so far, path
|
|
257
|
+
// length] and its vertex shader turns it into vDashOffset, making
|
|
258
|
+
// alongPath = vPathPosition.y + vDashOffset
|
|
259
|
+
// continuous across segments (deck's own dash shader uses exactly
|
|
260
|
+
// this; the pattern is periodic, so the mod() in vDashOffset is
|
|
261
|
+
// seamless). The extension's own gap stays 0 (getDashArray's second
|
|
262
|
+
// component — it renders the line solid); THIS layer's
|
|
263
|
+
// DECKGL_FILTER_COLOR discards the moving gap: the solid length and
|
|
264
|
+
// flat's random per-arc phase as an instanced attribute, a time
|
|
265
|
+
// uniform advanced by the wall clock (one full dash cycle every 2s),
|
|
266
|
+
// and fragments outside the moving dash window discarded — the layers
|
|
267
|
+
// themselves are NOT rebuilt per frame. The discard skips the picking
|
|
268
|
+
// pass (picking.isActive): flat's dashed SVG line is hover-tested
|
|
269
|
+
// across its gaps too, so the arcs keep their tooltips while the
|
|
270
|
+
// dash runs
|
|
271
|
+
function makeFlowPathLayer(Base) {
|
|
272
|
+
// deck.gl 9: a layer's custom uniforms live in shader MODULES — an
|
|
273
|
+
// std140 block named <module>Uniforms, its layout declared by the
|
|
274
|
+
// module's own GLSL and uniformTypes, values pushed per draw via
|
|
275
|
+
// setShaderModuleProps (the picking module's picking.isActive works
|
|
276
|
+
// exactly the same way). A plain GLSL uniform would have no backing
|
|
277
|
+
// buffer, and deck 8's state.model.setUniforms() no longer exists.
|
|
278
|
+
const FLOW_MODULE = {
|
|
279
|
+
name: 'flow',
|
|
280
|
+
uniformTypes: { uFlowTime: 'f32' },
|
|
281
|
+
defaultUniforms: { uFlowTime: 0 },
|
|
282
|
+
vs: 'layout(std140) uniform flowUniforms { float uFlowTime; } flow;',
|
|
283
|
+
fs: 'layout(std140) uniform flowUniforms { float uFlowTime; } flow;'
|
|
284
|
+
};
|
|
285
|
+
class Layer extends Base {
|
|
286
|
+
getShaders() {
|
|
287
|
+
const shaders = super.getShaders();
|
|
288
|
+
shaders.modules = (shaders.modules || []).concat(FLOW_MODULE);
|
|
289
|
+
const inject = Object.assign({}, shaders.inject || {});
|
|
290
|
+
inject['vs:#decl'] = (inject['vs:#decl'] || '') + `
|
|
291
|
+
in vec2 instanceFlowPhase;
|
|
292
|
+
out vec2 vFlowPhase;
|
|
293
|
+
`;
|
|
294
|
+
inject['vs:#main-end'] = (inject['vs:#main-end'] || '') + `
|
|
295
|
+
vFlowPhase = instanceFlowPhase;
|
|
296
|
+
`;
|
|
297
|
+
inject['fs:#decl'] = (inject['fs:#decl'] || '') + `
|
|
298
|
+
in vec2 vFlowPhase;
|
|
299
|
+
`;
|
|
300
|
+
// before the picking module's own filter; the dash is skipped
|
|
301
|
+
// there (see the class comment). vDashArray/vDashOffset come
|
|
302
|
+
// from the PathStyleExtension (see the layer's extensions),
|
|
303
|
+
// the position along the path is geometry.uv (the path
|
|
304
|
+
// layer's main sets it to vPathPosition before the filter
|
|
305
|
+
// runs; the varying itself is NOT redeclared here — the base
|
|
306
|
+
// shader already declares it, and the injected block lands
|
|
307
|
+
// before the base source, so a redeclaration is a
|
|
308
|
+
// redefinition error). uFlowTime comes from the flow module's
|
|
309
|
+
// block (see FLOW_MODULE)
|
|
310
|
+
inject['fs:DECKGL_FILTER_COLOR'] = (inject['fs:DECKGL_FILTER_COLOR'] || '') + `
|
|
311
|
+
{
|
|
312
|
+
float period = vDashArray.x;
|
|
313
|
+
float pickingActive = 0.0;
|
|
314
|
+
#ifdef MODULE_PICKING
|
|
315
|
+
pickingActive = picking.isActive;
|
|
316
|
+
#endif
|
|
317
|
+
if (period > 0.0 && pickingActive < 0.5) {
|
|
318
|
+
float alongPath = geometry.uv.y + vDashOffset;
|
|
319
|
+
float pos = fract((alongPath - flow.uFlowTime * period + vFlowPhase.y) / period);
|
|
320
|
+
if (pos * period > vFlowPhase.x) discard;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
`;
|
|
324
|
+
shaders.inject = inject;
|
|
325
|
+
return shaders;
|
|
326
|
+
}
|
|
327
|
+
initializeState() {
|
|
328
|
+
super.initializeState();
|
|
329
|
+
this.getAttributeManager().addInstanced({
|
|
330
|
+
instanceFlowPhase: { size: 2, accessor: 'getFlowPhase', defaultValue: [0, 0] }
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
draw(opts) {
|
|
334
|
+
// one full dash cycle every 2s — flat's animate dur (the bezier
|
|
335
|
+
// branch); the offset decreases, so the pattern walks toward
|
|
336
|
+
// the target (alongPath grows along the path). setProps on
|
|
337
|
+
// the shader inputs; the model's predraw flushes the uniform
|
|
338
|
+
// buffer when super.draw renders
|
|
339
|
+
this.setShaderModuleProps({ flow: { uFlowTime: (Date.now() % 2000) / 2000 } });
|
|
340
|
+
super.draw(opts);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
Layer.layerName = 'FlowPathLayer';
|
|
344
|
+
Layer.defaultProps = Object.assign({}, Base.defaultProps, {
|
|
345
|
+
getFlowPhase: { type: 'accessor', value: [0, 0] }
|
|
346
|
+
});
|
|
347
|
+
return Layer;
|
|
348
|
+
}
|
|
349
|
+
|
|
245
350
|
function makePieSliceLayer(Base) {
|
|
246
351
|
class Layer extends Base {
|
|
247
352
|
getShaders() {
|
|
@@ -293,12 +398,15 @@ in vec4 vPieLineColor;
|
|
|
293
398
|
}
|
|
294
399
|
// in device pixels (fwidth: CSS px per device pixel), so a hairline
|
|
295
400
|
// stays one at any device pixel ratio. Half the outline lies inside
|
|
296
|
-
// each slice (the other half in its neighbour)
|
|
297
|
-
//
|
|
401
|
+
// each slice (the other half in its neighbour). No fade-in by the
|
|
402
|
+
// width: the browser rasterizes flat's sub-pixel SVG strokes as
|
|
403
|
+
// hairlines — one device pixel at FULL opacity (Skia's stroke floor,
|
|
404
|
+
// measured on the ISTAT story: 0.17 CSS px strokes render gray 32) —
|
|
405
|
+
// so the line keeps full alpha and only its extent grows past 1px
|
|
298
406
|
float pieAA = max(fwidth(pieR), 1e-4);
|
|
299
407
|
float pieH = vPieLineWidth * 0.5 / pieAA;
|
|
300
408
|
float pieDD = pieD / pieAA;
|
|
301
|
-
float pieLine = pieH > 0.0 ? clamp(
|
|
409
|
+
float pieLine = pieH > 0.0 ? clamp(pieH + 0.5 - pieDD, 0.0, 1.0) : 0.0;
|
|
302
410
|
float pieRim = clamp((vPieRadius - pieR) / pieAA + 0.5, 0.0, 1.0);
|
|
303
411
|
color = vec4(mix(color.rgb, vPieLineColor.rgb, pieLine), mix(color.a, vPieLineColor.a * pieRim, pieLine));
|
|
304
412
|
if (pieInner > 0.0) color.a *= clamp((pieR - pieInner) / pieAA + 0.5, 0.0, 1.0);
|
|
@@ -503,6 +611,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
503
611
|
({ IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer, PathLayer, PathStyleExtension } = global.deck);
|
|
504
612
|
PieSliceLayer = makePieSliceLayer(ScatterplotLayer);
|
|
505
613
|
BarShapeLayer = makeBarShapeLayer(ScatterplotLayer);
|
|
614
|
+
FlowPathLayer = makeFlowPathLayer(PathLayer);
|
|
506
615
|
// which engine and libraries this page really runs (a page may
|
|
507
616
|
// bring its own MapLibre/deck.gl — the loaded ones are named)
|
|
508
617
|
const ml = global.maplibregl && typeof global.maplibregl.getVersion === 'function' ? global.maplibregl.getVersion() : '?';
|
|
@@ -662,10 +771,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
662
771
|
return ring;
|
|
663
772
|
}
|
|
664
773
|
|
|
774
|
+
// a quantized topology's point positions are absolute (not delta-encoded
|
|
775
|
+
// like arcs) quantized integers: x * scale + translate
|
|
776
|
+
function decodePoint(p, transform) {
|
|
777
|
+
if (!transform) return p;
|
|
778
|
+
return [p[0] * transform.scale[0] + transform.translate[0], p[1] * transform.scale[1] + transform.translate[1]];
|
|
779
|
+
}
|
|
780
|
+
|
|
665
781
|
function topoGeometryToGeoJSON(geom, arcs, transform) {
|
|
666
782
|
switch (geom.type) {
|
|
667
|
-
case 'Point': return { type: 'Point', coordinates: geom.coordinates };
|
|
668
|
-
case 'MultiPoint': return { type: 'MultiPoint', coordinates: geom.coordinates };
|
|
783
|
+
case 'Point': return { type: 'Point', coordinates: decodePoint(geom.coordinates, transform) };
|
|
784
|
+
case 'MultiPoint': return { type: 'MultiPoint', coordinates: geom.coordinates.map(p => decodePoint(p, transform)) };
|
|
669
785
|
case 'LineString': return { type: 'LineString', coordinates: ringCoords(geom.arcs, arcs, transform) };
|
|
670
786
|
case 'MultiLineString': return { type: 'MultiLineString', coordinates: geom.arcs.map(a => ringCoords(a, arcs, transform)) };
|
|
671
787
|
case 'Polygon': return { type: 'Polygon', coordinates: geom.arcs.map(ring => ringCoords(ring, arcs, transform)) };
|
|
@@ -941,11 +1057,40 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
941
1057
|
// ixmaps-flat dev tree, Themes.prototype.setExternalData). This engine
|
|
942
1058
|
// has no such pipeline to resume, so the bridge only needs to do the
|
|
943
1059
|
// ONE thing fetchLayerData actually needs: resolve the promise a
|
|
944
|
-
// query()-mode fetch is awaiting.
|
|
945
|
-
//
|
|
946
|
-
//
|
|
947
|
-
//
|
|
948
|
-
|
|
1060
|
+
// query()-mode fetch is awaiting. Queries run in parallel (several
|
|
1061
|
+
// layers, a later map.layer()), so every pending one has its own entry,
|
|
1062
|
+
// keyed by the options object its query function received — real pages
|
|
1063
|
+
// hand that same object back: query(themeObj, options) …
|
|
1064
|
+
// ixmaps.setExternalData(data, options). In data order: oldest first.
|
|
1065
|
+
const _pendingQueries = new Map(); // options object → { name, resolve, reject, timer }
|
|
1066
|
+
// a pending query's timeout (0: DATAJS_LOAD_TIMEOUT_MS) — a field so the
|
|
1067
|
+
// unit tests can shorten it
|
|
1068
|
+
const QUERY_HANDOFF = { timeoutMs: 0 };
|
|
1069
|
+
function settleQuery(opts, err, data) {
|
|
1070
|
+
const q = _pendingQueries.get(opts);
|
|
1071
|
+
if (!q) return;
|
|
1072
|
+
_pendingQueries.delete(opts);
|
|
1073
|
+
clearTimeout(q.timer);
|
|
1074
|
+
if (err) q.reject(err); else q.resolve(data);
|
|
1075
|
+
}
|
|
1076
|
+
// the pending query a setExternalData(data, opt) belongs to: the one
|
|
1077
|
+
// given that very options object, else the one of that data name, else —
|
|
1078
|
+
// no name — the oldest (exact while only one is pending)
|
|
1079
|
+
function pendingQueryOf(opt) {
|
|
1080
|
+
if (opt && _pendingQueries.has(opt)) return opt;
|
|
1081
|
+
const name = opt && opt.name;
|
|
1082
|
+
if (name != null && name !== '') {
|
|
1083
|
+
for (const [o, q] of _pendingQueries) if (q.name === String(name)) return o;
|
|
1084
|
+
return null;
|
|
1085
|
+
}
|
|
1086
|
+
const first = _pendingQueries.keys().next();
|
|
1087
|
+
if (first.done) return null;
|
|
1088
|
+
if (_pendingQueries.size > 1) {
|
|
1089
|
+
console.warn('[ixmaps-gl] ixmaps.setExternalData without its query\'s options object or a name, while ' + _pendingQueries.size +
|
|
1090
|
+
' .data({query}) fetches are pending — handed to the oldest; pass the options the query function received');
|
|
1091
|
+
}
|
|
1092
|
+
return first.value;
|
|
1093
|
+
}
|
|
949
1094
|
// Data by name, as flat (maptheme.js 2570-2593, 8286-8310):
|
|
950
1095
|
// setExternalData(data, {type, name}) makes a data.js Table of the data
|
|
951
1096
|
// and keeps it as window[name]; whoever waits for that name gets it, and
|
|
@@ -991,6 +1136,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
991
1136
|
// globals a data name must not overwrite
|
|
992
1137
|
const _engineGlobalsGuard = { ixmaps: 1, Data: 1, deck: 1, maplibregl: 1, window: 1, document: 1, $: 1, jQuery: 1 };
|
|
993
1138
|
function setExternalDataBridge(dataObj, opt) {
|
|
1139
|
+
const query = pendingQueryOf(opt);
|
|
1140
|
+
if (query) {
|
|
1141
|
+
if (dataObj == null) settleQuery(query, new Error('[ixmaps-gl] .data({query}): the query handed over no data (ixmaps.setExternalData(null, …))'));
|
|
1142
|
+
else settleQuery(query, null, dataObj);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
994
1145
|
const name = opt && opt.name;
|
|
995
1146
|
if (name != null && name !== '') {
|
|
996
1147
|
(dataObj == null ? Promise.resolve(null) : externalDataToTable(dataObj, opt))
|
|
@@ -998,13 +1149,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
998
1149
|
.catch(err => { console.error(err); publishNamedData(String(name), null); });
|
|
999
1150
|
return;
|
|
1000
1151
|
}
|
|
1001
|
-
|
|
1002
|
-
console.warn('[ixmaps-gl] ixmaps.setExternalData called with no pending .data({query}) fetch — ignored');
|
|
1003
|
-
return;
|
|
1004
|
-
}
|
|
1005
|
-
const resolve = _pendingQueryResolve;
|
|
1006
|
-
_pendingQueryResolve = null;
|
|
1007
|
-
resolve(dataObj);
|
|
1152
|
+
console.warn('[ixmaps-gl] ixmaps.setExternalData called with no pending .data({query}) fetch — ignored');
|
|
1008
1153
|
}
|
|
1009
1154
|
|
|
1010
1155
|
// ---------------------------------------------------------------
|
|
@@ -1056,7 +1201,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1056
1201
|
if (!table || !Array.isArray(table.records) || !Array.isArray(table.fields)) {
|
|
1057
1202
|
return table && typeof table.json === 'function' ? table.json() : [];
|
|
1058
1203
|
}
|
|
1059
|
-
|
|
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, ' '));
|
|
1060
1207
|
const rows = new Array(table.records.length);
|
|
1061
1208
|
for (let i = 0; i < table.records.length; i++) {
|
|
1062
1209
|
const rec = table.records[i], o = {};
|
|
@@ -1065,6 +1212,26 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1065
1212
|
}
|
|
1066
1213
|
return rows;
|
|
1067
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
|
+
}
|
|
1068
1235
|
// parse CSV text with data.js (as flat) → a Table
|
|
1069
1236
|
function dataJsCsvTable(text) {
|
|
1070
1237
|
return new Promise((resolve, reject) => {
|
|
@@ -1386,8 +1553,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1386
1553
|
// genuinely CSV-shaped .obj with a missing binding.position would
|
|
1387
1554
|
// throw, even though this .obj was never row data in the first place.
|
|
1388
1555
|
if (dataConfig.obj) {
|
|
1389
|
-
if (dataConfig.type === 'topojson') return topojsonToFeatureCollection(dataConfig.obj, dataConfig.object);
|
|
1390
|
-
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);
|
|
1391
1558
|
let obj = dataConfig.obj;
|
|
1392
1559
|
if (dataConfig.process) {
|
|
1393
1560
|
requireDataJs('process');
|
|
@@ -1402,7 +1569,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1402
1569
|
}
|
|
1403
1570
|
obj = runDataProcess(obj, dataConfig);
|
|
1404
1571
|
}
|
|
1405
|
-
|
|
1572
|
+
// a raw jsonDB object ({fields, records}) → rows, with or without
|
|
1573
|
+
// process (and without needing data.js)
|
|
1574
|
+
const isJsonDb = !Array.isArray(obj) && Array.isArray(obj.fields) && Array.isArray(obj.records);
|
|
1575
|
+
const rows = typeof obj.json === 'function' ? obj.json() : isJsonDb ? dataTableRows(obj) : obj;
|
|
1406
1576
|
return rowsResult(rows, binding);
|
|
1407
1577
|
}
|
|
1408
1578
|
|
|
@@ -1412,9 +1582,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1412
1582
|
'<script src="https://cdn.jsdelivr.net/gh/gjrichter/data.js@master/data.js"> before ixmaps-gl.js');
|
|
1413
1583
|
}
|
|
1414
1584
|
const queryFn = typeof dataConfig.query === 'function' ? dataConfig.query : new Function(`return (${dataConfig.query});`)();
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1585
|
+
// its own options object: the query hands it back with the data
|
|
1586
|
+
const opts = { type: dataConfig.type, name: dataConfig.name };
|
|
1587
|
+
const timeoutMs = QUERY_HANDOFF.timeoutMs || DATAJS_LOAD_TIMEOUT_MS;
|
|
1588
|
+
const dataObj = await new Promise((resolve, reject) => {
|
|
1589
|
+
const timer = setTimeout(() => settleQuery(opts, new Error(`[ixmaps-gl] .data({query}): no data handed over (ixmaps.setExternalData) within ${timeoutMs / 1000}s`)), timeoutMs);
|
|
1590
|
+
_pendingQueries.set(opts, { name: dataConfig.name != null ? String(dataConfig.name) : null, resolve, reject, timer });
|
|
1591
|
+
try { queryFn({}, opts); } catch (err) { settleQuery(opts, err); }
|
|
1418
1592
|
});
|
|
1419
1593
|
return rowsResult((dataConfig.process ? runDataProcess(dataObj, dataConfig) : dataObj).json(), binding);
|
|
1420
1594
|
}
|
|
@@ -1436,8 +1610,21 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1436
1610
|
// parser (PapaParse), so the Table arrives via the callback
|
|
1437
1611
|
const tables = await Promise.all(texts.map(dataJsCsvTable));
|
|
1438
1612
|
let table = tables[0];
|
|
1439
|
-
|
|
1440
|
-
|
|
1613
|
+
// records are positional: align each later file's columns to the
|
|
1614
|
+
// first file's by name (a column only a later file has is added),
|
|
1615
|
+
// as the no-data.js path below gets from each file's own header
|
|
1616
|
+
for (const t of tables.slice(1)) {
|
|
1617
|
+
const ids = table.fields.map(f => f.id);
|
|
1618
|
+
const tIds = t.fields.map(f => f.id);
|
|
1619
|
+
if (tIds.length === ids.length && tIds.every((id, c) => id === ids[c])) {
|
|
1620
|
+
table.records = table.records.concat(t.records);
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
t.fields.forEach(f => { if (!ids.includes(f.id)) { ids.push(f.id); table.fields.push(f); } });
|
|
1624
|
+
const src = ids.map(id => tIds.indexOf(id));
|
|
1625
|
+
table.records = table.records.concat(t.records.map(rec => src.map(c => (c < 0 ? undefined : rec[c]))));
|
|
1626
|
+
}
|
|
1627
|
+
if (table.table) { table.table.records = table.records.length; table.table.fields = table.fields.length; }
|
|
1441
1628
|
if (scriptProcess) table = scriptProcess(table);
|
|
1442
1629
|
else if (dataConfig.process) table = runDataProcess(table, dataConfig);
|
|
1443
1630
|
rows = dataTableRows(table);
|
|
@@ -1459,8 +1646,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1459
1646
|
// them into a table with a geometry column, and back)
|
|
1460
1647
|
const resp = await fetch(dataConfig.url);
|
|
1461
1648
|
if (!resp.ok) throw new Error(`[ixmaps-gl] failed to fetch ${dataConfig.url}: ${resp.status}`);
|
|
1462
|
-
if (dataConfig.type === 'topojson') return topojsonToFeatureCollection(await resp.json(), dataConfig.object);
|
|
1463
|
-
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);
|
|
1464
1651
|
}
|
|
1465
1652
|
// every other format (json, jsonstat, ndjson, rss, kml, gml, geobuf,
|
|
1466
1653
|
// flatgeobuf, geopackage, parquet, ...) through data.js, as flat
|
|
@@ -1858,21 +2045,25 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1858
2045
|
if (isAggregatedCategoricalChoropleth(spec)) {
|
|
1859
2046
|
const groups = new Map();
|
|
1860
2047
|
for (const row of table.rows) {
|
|
1861
|
-
|
|
2048
|
+
if (row[lookupField] == null) continue;
|
|
2049
|
+
const key = flatLookupKey(row[lookupField], spec.style);
|
|
1862
2050
|
if (!groups.has(key)) groups.set(key, []);
|
|
1863
2051
|
groups.get(key).push(row);
|
|
1864
2052
|
}
|
|
1865
2053
|
return {
|
|
1866
2054
|
type: 'FeatureCollection',
|
|
1867
2055
|
features: geomRt.features.map(f => {
|
|
1868
|
-
const rows = groups.get(String(f.properties[idField]));
|
|
2056
|
+
const rows = f.properties[idField] == null ? null : groups.get(String(f.properties[idField]));
|
|
1869
2057
|
const properties = rows ? Object.assign({}, rows[0]) : {};
|
|
1870
2058
|
if (rows) Object.defineProperty(properties, AGGREGATED_ROWS, { value: rows });
|
|
1871
2059
|
return { type: 'Feature', geometry: f.geometry, properties, _flatAreaKm2: flatShapeAreaKm2(geomRt, f) };
|
|
1872
2060
|
})
|
|
1873
2061
|
};
|
|
1874
2062
|
}
|
|
1875
|
-
|
|
2063
|
+
// the data side's key as flat makes it (lookupdigits/toupper/tonumber),
|
|
2064
|
+
// the geometry id as is — the same rule as joinChartPositions
|
|
2065
|
+
// a missing key on either side joins nothing (not "undefined" to "undefined")
|
|
2066
|
+
const rowsByKey = new Map(table.rows.filter(row => row[lookupField] != null).map(row => [flatLookupKey(row[lookupField], spec.style), row]));
|
|
1876
2067
|
// Unmatched polygons (no CSV row for that id) keep their geometry with
|
|
1877
2068
|
// empty properties — real-world data: not every comune necessarily has
|
|
1878
2069
|
// a row in every demographic source. Renders as "no data" (see
|
|
@@ -1881,7 +2072,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1881
2072
|
const features = geomRt.features.map(f => ({
|
|
1882
2073
|
type: 'Feature',
|
|
1883
2074
|
geometry: f.geometry,
|
|
1884
|
-
properties: rowsByKey.get(String(f.properties[idField])) || {},
|
|
2075
|
+
properties: (f.properties[idField] != null && rowsByKey.get(String(f.properties[idField]))) || {},
|
|
1885
2076
|
_flatAreaKm2: flatShapeAreaKm2(geomRt, f)
|
|
1886
2077
|
}));
|
|
1887
2078
|
return { type: 'FeatureCollection', features };
|
|
@@ -2294,13 +2485,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2294
2485
|
// this list; the rest is still recognized-but-inert
|
|
2295
2486
|
const KNOWN_INERT_FLAGS = ['CLIPTOGEOBOUNDS', 'OUTLIER',
|
|
2296
2487
|
'POW2', 'POW3', 'NOSORT'];
|
|
2297
|
-
// flat's PIE / DONUT variants with no rendering here (see _buildPieLayers)
|
|
2298
|
-
|
|
2488
|
+
// flat's PIE / DONUT variants with no rendering here (see _buildPieLayers).
|
|
2489
|
+
// STARBURST / FLOWER / RAYS (the starburst rays, with the XLRAYS & kin
|
|
2490
|
+
// angle/gap variants) and POLAR (its reference circles) ARE drawn — see
|
|
2491
|
+
// pieSliceLayout; the rest is still recognized-but-inert
|
|
2492
|
+
const PIE_INERT_FLAGS = [/3D/, /VOLUME/, /HEIGHT/, /BOW/, /ZOOM/, /DIRECTION/, /DIRECTED/];
|
|
2299
2493
|
// ... and of AGGREGATE / CATEGORICAL pies: GRIDSIZE's pie the size of its
|
|
2300
|
-
// grid cell (drawn by the SIZE rule instead), AUTOSIZE, DOPACITY,
|
|
2301
|
-
//
|
|
2302
|
-
// value arithmetic (DIFFERENCE, FRACTION, PERCENT …, field100)
|
|
2303
|
-
|
|
2494
|
+
// grid cell (drawn by the SIZE rule instead), AUTOSIZE, DOPACITY,
|
|
2495
|
+
// GAUGE, MULTIPLE / DIFFUSE, the post-aggregation
|
|
2496
|
+
// value arithmetic (DIFFERENCE, FRACTION, PERCENT …, field100).
|
|
2497
|
+
// The chart BOX and TITLE ARE drawn (see _buildPieLayers)
|
|
2498
|
+
const PIE_AGGREGATE_INERT_FLAGS = [/^GRIDSIZE$/, /AUTOSIZE/, /DOPACITY/, /^GAUGE$/, /^MULTIPLE$/, /^DIFFUSE$/,
|
|
2304
2499
|
/^DIFFERENCE$/, /^FRACTION$/, /^PERCENT$/, /^PERMILLE$/, /^PRODUCT$/, /^CALCVAL$/, /^RELATIVE$/];
|
|
2305
2500
|
// flat's BAR / BARS variants with no rendering here (see flatBarLayout):
|
|
2306
2501
|
// the bar is drawn plain (SMALLARROW as ARROW) — tested as substrings,
|
|
@@ -2310,11 +2505,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2310
2505
|
/^DIFFERENCE$/, /^FRACTION$/, /^PERCENT$/, /^PERMILLE$/, /^PRODUCT$/, /^CALCVAL$/, /^RELATIVE$/];
|
|
2311
2506
|
// flat's VECTOR / BEZIER modifiers with no rendering here (see
|
|
2312
2507
|
// _buildVectorLayers), the word → what happens instead
|
|
2508
|
+
// (DASH IS drawn — the animated flow, see makeFlowPathLayer)
|
|
2313
2509
|
const VECTOR_INERT_FLAGS = {
|
|
2314
2510
|
VALUES: 'the value text along the curve is not drawn yet',
|
|
2315
2511
|
DYNAMICWIDTH: 'not drawn yet (flat widens by the log of the length in its own map units) — the plain width instead',
|
|
2316
|
-
LONG: 'not drawn yet (its bow depends on flat\'s own map units) — the default bow instead'
|
|
2317
|
-
DASH: 'drawn as static dashes — flat\'s dash animation is not ported'
|
|
2512
|
+
LONG: 'not drawn yet (its bow depends on flat\'s own map units) — the default bow instead'
|
|
2318
2513
|
};
|
|
2319
2514
|
const _warnedFlags = new Set();
|
|
2320
2515
|
|
|
@@ -2359,6 +2554,18 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2359
2554
|
// makes while the map is still building wait for it
|
|
2360
2555
|
let _resolveMapReady;
|
|
2361
2556
|
const _mapReady = new Promise(r => { _resolveMapReady = r; });
|
|
2557
|
+
// a map declared with an options name (Map(id, {name})) → the deferred of
|
|
2558
|
+
// ITS build: ixmaps.map("name") calls made while it is still building wait
|
|
2559
|
+
// for that map, not whichever map is built first
|
|
2560
|
+
const _namedMapReady = Object.create(null);
|
|
2561
|
+
function namedMapReady(name) {
|
|
2562
|
+
if (!_namedMapReady[name]) {
|
|
2563
|
+
let resolve;
|
|
2564
|
+
const promise = new Promise(r => { resolve = r; });
|
|
2565
|
+
_namedMapReady[name] = { promise, resolve };
|
|
2566
|
+
}
|
|
2567
|
+
return _namedMapReady[name];
|
|
2568
|
+
}
|
|
2362
2569
|
|
|
2363
2570
|
// flat's ixmaps.require(url) fetches the script and evals it
|
|
2364
2571
|
// (htmlgui.js 3911-3934); here each one runs globally, in order, before
|
|
@@ -2478,7 +2685,16 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2478
2685
|
// key wins. Always into a new object — the page's own objects are never
|
|
2479
2686
|
// mutated. Aliases (geo → position, ...) are resolved in normalizeTheme.
|
|
2480
2687
|
binding(b) {
|
|
2481
|
-
|
|
2688
|
+
// flat's toString joins an array with '|' (maptheme.js 1014): a
|
|
2689
|
+
// binding given as an ARRAY — .binding({value: ["out","in"]}, the
|
|
2690
|
+
// pendolari page's saldo pie) is a MULTI-FIELD binding, not one
|
|
2691
|
+
// field named "out,in" (which would find no column and draw
|
|
2692
|
+
// nothing). Normalized once, here, for every consumer that
|
|
2693
|
+
// splits binding.value on '|'
|
|
2694
|
+
const norm = {};
|
|
2695
|
+
// (an empty or missing argument keeps what earlier calls set)
|
|
2696
|
+
for (const [k, v] of Object.entries(b || {})) norm[k] = Array.isArray(v) ? v.join('|') : v;
|
|
2697
|
+
this._binding = Object.assign({}, this._binding, norm);
|
|
2482
2698
|
return this;
|
|
2483
2699
|
}
|
|
2484
2700
|
filter(expr) { this._filterExpr = expr; return this; }
|
|
@@ -2768,27 +2984,75 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2768
2984
|
// that kind: the data processing function (data.process /
|
|
2769
2985
|
// style.dbtableProcess), the broker query (data.query /
|
|
2770
2986
|
// style.dbtableQuery — without it a data.name still reaches a
|
|
2771
|
-
// provider the PAGE defined as ixmaps[name]) and a colorscheme
|
|
2772
|
-
//
|
|
2773
|
-
// (
|
|
2987
|
+
// provider the PAGE defined as ixmaps[name]) and a colorscheme
|
|
2988
|
+
// resolveColorScheme would evaluate: a string, or ["byvalue", "<fn>"]
|
|
2989
|
+
// (any other array colorscheme is data and stays). normalizeTheme
|
|
2990
|
+
// merges meta INTO style, so every style key is checked under meta too.
|
|
2991
|
+
const isColorSchemeCode = v => typeof v === 'string'
|
|
2992
|
+
|| (Array.isArray(v) && v[0] === 'byvalue' && typeof v[1] === 'string');
|
|
2774
2993
|
const PROJECT_CODE_KEYS = [
|
|
2775
2994
|
['data', 'process', 'data.process function'],
|
|
2776
2995
|
['style', 'dbtableProcess', 'data.process function'],
|
|
2777
2996
|
['data', 'query', 'data.query function'],
|
|
2778
2997
|
['style', 'dbtableQuery', 'data.query function'],
|
|
2779
|
-
['style', 'colorscheme', 'colorscheme function',
|
|
2780
|
-
];
|
|
2998
|
+
['style', 'colorscheme', 'colorscheme function', isColorSchemeCode],
|
|
2999
|
+
].flatMap(k => (k[0] === 'style' ? [k, ['meta', ...k.slice(1)]] : [k]));
|
|
3000
|
+
// A project's legend texts (title, snippet, description — in style or
|
|
3001
|
+
// meta, normalizeTheme fills meta from either) are shown as HTML, as on
|
|
3002
|
+
// flat: citation links in a description are real. A project's markup is
|
|
3003
|
+
// cleaned to formatting tags and plain attributes so nothing in it runs;
|
|
3004
|
+
// a page's own .meta()/.style() HTML stays as given.
|
|
3005
|
+
const PROJECT_HTML_KEYS = ['title', 'snippet', 'description'];
|
|
3006
|
+
const PROJECT_HTML_DROP = new Set(['SCRIPT', 'STYLE', 'IFRAME', 'FRAME', 'FRAMESET', 'OBJECT', 'EMBED', 'SVG', 'MATH',
|
|
3007
|
+
'TEMPLATE', 'NOSCRIPT', 'LINK', 'META', 'BASE', 'FORM', 'INPUT', 'BUTTON', 'SELECT', 'TEXTAREA', 'AUDIO', 'VIDEO', 'SOURCE']);
|
|
3008
|
+
const PROJECT_HTML_TAGS = new Set(['A', 'B', 'I', 'EM', 'STRONG', 'U', 'S', 'SMALL', 'SUB', 'SUP', 'BR', 'P', 'DIV', 'SPAN',
|
|
3009
|
+
'UL', 'OL', 'LI', 'HR', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'IMG', 'TABLE', 'THEAD', 'TBODY', 'TR', 'TD', 'TH', 'CODE', 'BLOCKQUOTE']);
|
|
3010
|
+
const PROJECT_HTML_ATTRS = new Set(['href', 'src', 'alt', 'title', 'target', 'rel', 'class', 'style', 'width', 'height', 'colspan', 'rowspan']);
|
|
3011
|
+
function sanitizeProjectHtml(html) {
|
|
3012
|
+
const s = String(html);
|
|
3013
|
+
if (!/[<&]/.test(s)) return s;
|
|
3014
|
+
// no DOM to parse with (Node): the text, escaped
|
|
3015
|
+
if (typeof global.DOMParser !== 'function') return escapeHtml(s);
|
|
3016
|
+
// an inert document: nothing in it runs or loads while it is cleaned
|
|
3017
|
+
const body = new global.DOMParser().parseFromString('<!doctype html><body>' + s, 'text/html').body;
|
|
3018
|
+
for (const el of Array.from(body.querySelectorAll('*'))) {
|
|
3019
|
+
if (PROJECT_HTML_DROP.has(el.tagName.toUpperCase())) { el.remove(); continue; }
|
|
3020
|
+
if (!PROJECT_HTML_TAGS.has(el.tagName.toUpperCase())) { el.replaceWith(...el.childNodes); continue; }
|
|
3021
|
+
for (const a of Array.from(el.attributes)) {
|
|
3022
|
+
const name = a.name.toLowerCase();
|
|
3023
|
+
if (!PROJECT_HTML_ATTRS.has(name)
|
|
3024
|
+
|| ((name === 'href' || name === 'src') && /^(javascript|vbscript|data):/i.test(a.value.replace(/[\s\u0000-\u001f]/g, '')))) {
|
|
3025
|
+
el.removeAttribute(a.name);
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
return body.innerHTML;
|
|
3030
|
+
}
|
|
3031
|
+
|
|
2781
3032
|
function withoutProjectCode(t, report) {
|
|
2782
3033
|
const found = PROJECT_CODE_KEYS.filter(([part, key, , isCode]) =>
|
|
2783
3034
|
t[part] && t[part][key] != null && (!isCode || isCode(t[part][key])));
|
|
2784
|
-
|
|
3035
|
+
const html = [];
|
|
3036
|
+
for (const part of ['style', 'meta']) {
|
|
3037
|
+
for (const key of PROJECT_HTML_KEYS) {
|
|
3038
|
+
const v = t[part] && t[part][key];
|
|
3039
|
+
if (typeof v !== 'string') continue;
|
|
3040
|
+
const clean = sanitizeProjectHtml(v);
|
|
3041
|
+
if (clean !== v) html.push([part, key, clean]);
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
if (!found.length && !html.length) return t;
|
|
2785
3045
|
const copy = Object.assign({}, t);
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
3046
|
+
const own = part => { if (copy[part] === t[part]) copy[part] = Object.assign({}, t[part]); return copy[part]; };
|
|
3047
|
+
found.forEach(([part, key]) => { delete own(part)[key]; });
|
|
3048
|
+
html.forEach(([part, key, clean]) => { own(part)[key] = clean; });
|
|
3049
|
+
if (found.length) {
|
|
3050
|
+
const what = [...new Set(found.map(f => f[2]))].join(', ');
|
|
3051
|
+
report.notes.push(`theme "${t.layer}": the project's ${what} is never run by ixmaps-gl`);
|
|
3052
|
+
}
|
|
3053
|
+
if (html.length) {
|
|
3054
|
+
report.notes.push(`theme "${t.layer}": the project's ${[...new Set(html.map(h => h[1]))].join(', ')} HTML is cleaned to formatting markup by ixmaps-gl`);
|
|
3055
|
+
}
|
|
2792
3056
|
return copy;
|
|
2793
3057
|
}
|
|
2794
3058
|
|
|
@@ -2858,7 +3122,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2858
3122
|
'sizepow', 'rangescale', 'minvalue', 'maxvalue', 'markersize', 'boxopacity', 'outlierscale', 'valuescale',
|
|
2859
3123
|
'brightness', 'fractionscale', 'dopacityscale', 'dopacitypow', 'gridwidthpx', 'textscale', 'rangecentervalue',
|
|
2860
3124
|
'shadowblur', 'shadowdx', 'shadowdy', 'maxshadow', 'offsetx', 'offsety', 'gridx', 'boxmargin', 'borderwidth',
|
|
2861
|
-
'borderradius', 'maxcharts', 'archeight'];
|
|
3125
|
+
'borderradius', 'maxcharts', 'archeight', 'alignmargin'];
|
|
2862
3126
|
function toNumberIfNumeric(v) {
|
|
2863
3127
|
if (typeof v !== 'string' || !v.trim()) return v;
|
|
2864
3128
|
const n = Number(v);
|
|
@@ -3350,6 +3614,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3350
3614
|
// The ocean of flat's world / orthographic maps is a {type:"Sphere"} feature: the whole map disc,
|
|
3351
3615
|
// filled with the layer's color. Here it is the surface of the map (the globe, or the flat
|
|
3352
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
|
+
}
|
|
3353
3628
|
const SPHERE_BACKDROP_LAYER_ID = '__ixmaps_gl_sphere_backdrop';
|
|
3354
3629
|
function applySphereBackdrop(map, style) {
|
|
3355
3630
|
const cs = style && style.colorscheme;
|
|
@@ -3367,7 +3642,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3367
3642
|
map.setPaintProperty(id, 'background-opacity', opacity);
|
|
3368
3643
|
} catch (e) { console.warn('[ixmaps-gl] the {type:"Sphere"} ocean backdrop could not be set:', e); }
|
|
3369
3644
|
};
|
|
3370
|
-
|
|
3645
|
+
// isStyleLoaded() is also false AFTER the style loaded while tiles or
|
|
3646
|
+
// images load (every pan/zoom) — 'style.load' never fires again then, so
|
|
3647
|
+
// a Sphere theme defined at such a moment would never get its color
|
|
3648
|
+
if (map.isStyleLoaded() || (map.style && map.style._loaded)) paint(); else map.once('style.load', paint);
|
|
3371
3649
|
}
|
|
3372
3650
|
|
|
3373
3651
|
function buildBlankBackgroundStyle(color) {
|
|
@@ -3386,6 +3664,23 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3386
3664
|
// basemap credit, bottom right — same line, same pill: white, 12px
|
|
3387
3665
|
// radius, 12px/20px Helvetica Neue, links rgba(0,0,0,0.75)).
|
|
3388
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
|
+
}
|
|
3389
3684
|
let _attributionCss = false;
|
|
3390
3685
|
function createAttribution(map) {
|
|
3391
3686
|
if (!_attributionCss && typeof document !== 'undefined') {
|
|
@@ -3516,7 +3811,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3516
3811
|
// visible (an element larger than the viewport ends up at its edge)
|
|
3517
3812
|
function keepTooltipVisible(tipEl, x, y) {
|
|
3518
3813
|
const w = tipEl.offsetWidth, h = tipEl.offsetHeight;
|
|
3519
|
-
|
|
3814
|
+
// x/y and the translate are relative to the map container (it clips,
|
|
3815
|
+
// overflow:hidden): its size is the room, not the window's — they
|
|
3816
|
+
// differ on an embedded map
|
|
3817
|
+
const vw = el.clientWidth || global.innerWidth || 1200, vh = el.clientHeight || global.innerHeight || 800;
|
|
3520
3818
|
let tx = x > vw / 2 ? x - w - 30 : x + 30;
|
|
3521
3819
|
let ty = y > vh / 2 ? y - h - 20 : y + 20;
|
|
3522
3820
|
tx = Math.min(Math.max(8, tx), Math.max(8, vw - w - 8));
|
|
@@ -3689,11 +3987,29 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3689
3987
|
// pane, the shared look (fonts included) on the container
|
|
3690
3988
|
return html ? { html: tooltipPaneHtml(html), style: Object.assign({ padding: '0.5em 0.7em', maxWidth: '440px' }, tooltipLook()) } : null;
|
|
3691
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
|
+
}
|
|
3692
4007
|
function clickFor(picked) {
|
|
3693
4008
|
const info = picked && Object.assign({}, picked, { object: pickedFeature(picked.object) });
|
|
3694
4009
|
if (info && info.object && info.layer) {
|
|
3695
4010
|
const rt = findRuntimeForLayerId(info.layer.id);
|
|
3696
4011
|
if (rt) {
|
|
4012
|
+
callItemAction(rt, info.object);
|
|
3697
4013
|
// Only a Point geometry's own coordinates ARE an
|
|
3698
4014
|
// [lng,lat] pair (every point/bubble/dot/chart-cluster
|
|
3699
4015
|
// layer) — a CHOROPLETH polygon's geometry.coordinates is
|
|
@@ -3724,6 +4040,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3724
4040
|
}
|
|
3725
4041
|
}
|
|
3726
4042
|
// clicked empty map space (or a non-tooltippable layer) -> unpin
|
|
4043
|
+
callItemAction(null, null);
|
|
3727
4044
|
pinned = null;
|
|
3728
4045
|
pinnedTooltipEl.style.display = 'none';
|
|
3729
4046
|
return false;
|
|
@@ -4116,13 +4433,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4116
4433
|
// twin pages)
|
|
4117
4434
|
const flatUnitLegend = rt._isVectorChart() || rt._isAggregatedPie();
|
|
4118
4435
|
const rawLegendUnit = rt.style.legendunits || rt.style.units || '';
|
|
4119
|
-
|
|
4436
|
+
// COUNT: the rows are record counts — flat prints them as whole numbers
|
|
4437
|
+
// with no unit (legend.js 705, 810: __formatValue(count, nDecimals,
|
|
4438
|
+
// "BLANK") + (COUNT ? "" : " " + unit))
|
|
4439
|
+
const countLegend = flatFlag(rt.flags, 'COUNT') && !useSum;
|
|
4440
|
+
const legendUnit = countLegend ? '' : flatUnitLegend && rawLegendUnit && rawLegendUnit[0] !== '.' && rawLegendUnit[0] !== ' ' ? ' ' + rawLegendUnit : rawLegendUnit;
|
|
4120
4441
|
// a per-item chart over several value fields: the row values as
|
|
4121
4442
|
// flat's legend prints them (legend.js 610, 810: __formatValue
|
|
4122
4443
|
// with valuedecimals, else 2, "BLANK" — whole numbers without
|
|
4123
4444
|
// decimals), a colorscheme "none" part as an outline in linecolor
|
|
4124
4445
|
const multiFieldLegend = rt._isMultiFieldChart();
|
|
4125
|
-
const legendTotalText = v => ((multiFieldLegend || flatUnitLegend)
|
|
4446
|
+
const legendTotalText = v => ((multiFieldLegend || flatUnitLegend || countLegend)
|
|
4126
4447
|
? flatFormatValue(v, legendDecimals(rt.style), 'BLANK')
|
|
4127
4448
|
: rt._formatTooltipValue(v));
|
|
4128
4449
|
const outlineRgb = rt.style.linecolor && styleLineColor(rt.style.linecolor) !== 'none' ? hexOrNamedToRgb(styleLineColor(rt.style.linecolor)) : [128, 128, 128];
|
|
@@ -4425,7 +4746,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4425
4746
|
// same 400ms as the facets sidebar's own onRedraw use), same
|
|
4426
4747
|
// signal already used elsewhere in this file for "the visible
|
|
4427
4748
|
// data just changed, recompute".
|
|
4428
|
-
|
|
4749
|
+
// a panel replaced (rebuild, legend off) or a removed theme: the
|
|
4750
|
+
// listener goes with it — else one per rebuild piles up, each
|
|
4751
|
+
// recomputing a stale runtime on every redraw
|
|
4752
|
+
const offRedraw = engineApi.onRedraw(() => {
|
|
4753
|
+
if (rt._legendPanel !== panel || !runtimes.includes(rt)) { offRedraw(); return; }
|
|
4754
|
+
computeTotals(); renderRows();
|
|
4755
|
+
});
|
|
4429
4756
|
}
|
|
4430
4757
|
|
|
4431
4758
|
// rt._triggerRedraw: see its own doc comment on LayerRuntime
|
|
@@ -4740,7 +5067,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4740
5067
|
}
|
|
4741
5068
|
return this;
|
|
4742
5069
|
}
|
|
4743
|
-
options(
|
|
5070
|
+
// merged, as flat's options() sets each key on its own (ixmaps.setOptions)
|
|
5071
|
+
// and the live map's options() below does — .options({a}).options({b})
|
|
5072
|
+
// keeps a
|
|
5073
|
+
options(o) { if (o && typeof o === 'object') Object.assign(this._engineOptions, o); return this; }
|
|
4744
5074
|
local(...args) { this._locals.push(args); return this; }
|
|
4745
5075
|
// flat's .require(url) (htmlgui.js ixmaps.require): a script the page
|
|
4746
5076
|
// needs (a chart library, a user chart) — page code, like a <script>
|
|
@@ -4793,6 +5123,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4793
5123
|
// the Map(id, opts, map => map.options(...)) callback form worked).
|
|
4794
5124
|
// One microtask lets the caller's whole synchronous chain land first.
|
|
4795
5125
|
await null;
|
|
5126
|
+
// the DASH flow animation's repaint loop (see refreshLayers): MapLibre
|
|
5127
|
+
// renders on demand only, so the shader's wall-clock dash (see
|
|
5128
|
+
// makeFlowPathLayer) needs a continuous frame request while any flow
|
|
5129
|
+
// layer is on screen — one handle per map, or another map's refresh
|
|
5130
|
+
// cancels (or blocks) this map's loop
|
|
5131
|
+
let _flowRepaintRAF = null;
|
|
4796
5132
|
let el = document.getElementById(this.containerId);
|
|
4797
5133
|
if (!el) {
|
|
4798
5134
|
// flat's own fallback for a missing target div (htmlgui_flat.js
|
|
@@ -4812,7 +5148,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4812
5148
|
el = document.createElement('div');
|
|
4813
5149
|
el.id = this.containerId;
|
|
4814
5150
|
el.style.width = (this.mapOptions && this.mapOptions.width) ? String(this.mapOptions.width) : '100%';
|
|
4815
|
-
|
|
5151
|
+
// a percentage height would collapse against a body without a
|
|
5152
|
+
// height of its own (the blank-screen case): the window's
|
|
5153
|
+
// innerHeight instead, like flat's fullscreen fallback
|
|
5154
|
+
el.style.height = (this.mapOptions && this.mapOptions.height) ? String(this.mapOptions.height)
|
|
5155
|
+
: (global.innerHeight || 800) + 'px';
|
|
4816
5156
|
document.body.appendChild(el);
|
|
4817
5157
|
}
|
|
4818
5158
|
// flat's init replaces the container's content with its own map page
|
|
@@ -4836,13 +5176,22 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4836
5176
|
else if (typeof v === 'string' && /^\s*\d+(\.\d+)?\s*%\s*$/.test(v)
|
|
4837
5177
|
&& !(dim === 'width' ? el.clientWidth : el.clientHeight)) el.style[dim] = v.trim();
|
|
4838
5178
|
}
|
|
4839
|
-
// without
|
|
5179
|
+
// without height flat's map is "fullscreen" (htmlgui_flat.js
|
|
4840
5180
|
// 847-876): it fills the window, whatever the page's CSS gives the
|
|
4841
|
-
// container —
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
5181
|
+
// container. PER SIDE — also when only a width is given (the World
|
|
5182
|
+
// Dataviz Prize pages: width 100%, no height), where the old
|
|
5183
|
+
// both-sides gate left the container collapsed at 0 (a blank
|
|
5184
|
+
// screen): no height option and no effective CSS height gets the
|
|
5185
|
+
// window's innerHeight, kept current on window resizes (MapLibre
|
|
5186
|
+
// follows its container's own size)
|
|
5187
|
+
if (!(this.mapOptions && this.mapOptions.height) && !el.clientHeight) {
|
|
5188
|
+
this._heightFallbackToWindow = true;
|
|
5189
|
+
el.style.height = (global.innerHeight || 800) + 'px';
|
|
5190
|
+
global.addEventListener('resize', () => {
|
|
5191
|
+
if (this._heightFallbackToWindow) el.style.height = (global.innerHeight || 800) + 'px';
|
|
5192
|
+
});
|
|
4845
5193
|
}
|
|
5194
|
+
if (!(this.mapOptions && this.mapOptions.width) && !el.clientWidth) el.style.width = '100%';
|
|
4846
5195
|
|
|
4847
5196
|
// shown for the whole build() (library lazy-load + every layer's own
|
|
4848
5197
|
// data fetch, real-world verified as the two slowest phases) through
|
|
@@ -4869,8 +5218,6 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4869
5218
|
const svgProjection = flatSvgProjectionOf(this.mapOptions.map)
|
|
4870
5219
|
|| (isFlatOrthographicName(this.mapOptions.mapProjection) ? FLAT_ORTHOGRAPHIC_PROJECTION : null);
|
|
4871
5220
|
if (svgProjection) flatSvgProjectionByOptions.set(this._engineOptions, svgProjection);
|
|
4872
|
-
const planeProjection = flatPlaneProjectionOf(this.mapOptions);
|
|
4873
|
-
if (planeProjection) flatPlaneProjectionByOptions.set(this._engineOptions, planeProjection);
|
|
4874
5221
|
if (isFlatOrthographicName(this.mapOptions.mapProjection)) {
|
|
4875
5222
|
flatOrthographicByOptions.set(this._engineOptions, true);
|
|
4876
5223
|
if (this._viewCenter) flatViewCenterByOptions.set(this._engineOptions, { lat: this._viewCenter[0], lng: this._viewCenter[1] });
|
|
@@ -5035,11 +5382,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5035
5382
|
// world map can't open fully zoomed out. The deck.gl overlay takes
|
|
5036
5383
|
// its `repeat` from map.getRenderWorldCopies(), so the themes are
|
|
5037
5384
|
// drawn on every world copy too (flat draws them once; only its
|
|
5038
|
-
// basemap tiles wrap)
|
|
5385
|
+
// basemap tiles wrap) — unless themecopies is off, which is the default
|
|
5386
|
+
// (see applyThemeCopies). .options({worldcopies: false}) draws the
|
|
5039
5387
|
// world once, at the cost of that zoom limit.
|
|
5040
5388
|
renderWorldCopies: this._engineOptions.worldcopies !== false
|
|
5041
5389
|
});
|
|
5042
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);
|
|
5043
5396
|
sphereBackdrops.forEach(style => applySphereBackdrop(map, style));
|
|
5044
5397
|
// the live-SVG pane USER charts render into (see chartHostFor) —
|
|
5045
5398
|
// registered here, keyed by this map's options object, so every
|
|
@@ -5168,6 +5521,16 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5168
5521
|
// the page's attribution (.attribution(), Map option attribution)
|
|
5169
5522
|
const attributionBox = createAttribution(map);
|
|
5170
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);
|
|
5171
5534
|
this._setAttribution = text => attributionBox.set(text);
|
|
5172
5535
|
|
|
5173
5536
|
if (this._legendHtml) {
|
|
@@ -5175,7 +5538,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5175
5538
|
// a page legend (map.legend(html)) is positioned by the map's
|
|
5176
5539
|
// own align option — the same rule that places the native stack
|
|
5177
5540
|
legendEl.style.cssText = 'position:absolute;' + legendAlignCssFor(this.mapOptions) + 'z-index:5;pointer-events:none';
|
|
5178
|
-
|
|
5541
|
+
// flat's legend takes the pane's html OR a URL to it (htmlgui):
|
|
5542
|
+
// a value without a tag is fetched, its html shown
|
|
5543
|
+
if (/<[a-z!]/i.test(this._legendHtml)) legendEl.innerHTML = this._legendHtml;
|
|
5544
|
+
else fetch(this._legendHtml).then(r => r.ok ? r.text() : '').then(t => { legendEl.innerHTML = t; }).catch(() => {});
|
|
5179
5545
|
// inside the map container, never the page's body (see the pinned
|
|
5180
5546
|
// tooltip mount) — styling body breaks page-positioned elements
|
|
5181
5547
|
anchorInside(el);
|
|
@@ -5305,18 +5671,19 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5305
5671
|
add: (layerBuilder, flag) => {
|
|
5306
5672
|
layerBuilder = unwrapLayer(layerBuilder);
|
|
5307
5673
|
_chainedLayers = _chainedLayers.then(() => {
|
|
5674
|
+
let slot = null;
|
|
5308
5675
|
if (/replace/i.test(flag || '')) {
|
|
5309
5676
|
const def = layerBuilder.definition();
|
|
5310
5677
|
const id = (def.style && def.style.name) || (def.meta && def.meta.name);
|
|
5311
|
-
if (id) removeThemeById(id);
|
|
5678
|
+
if (id) { slot = captureSlot(id); removeThemeById(id); }
|
|
5312
5679
|
}
|
|
5313
|
-
return engineApi.defineLayer(layerBuilder);
|
|
5680
|
+
return engineApi.defineLayer(layerBuilder, slot);
|
|
5314
5681
|
}).catch(err => console.error('[ixmaps-gl] map.add():', err));
|
|
5315
5682
|
return engineApi;
|
|
5316
5683
|
},
|
|
5317
5684
|
replace: (id, layerBuilder, flag) => {
|
|
5318
5685
|
layerBuilder = unwrapLayer(layerBuilder);
|
|
5319
|
-
_chainedLayers = _chainedLayers.then(() => { removeThemeById(id); return engineApi.defineLayer(layerBuilder); })
|
|
5686
|
+
_chainedLayers = _chainedLayers.then(() => { const slot = captureSlot(id); removeThemeById(id); return engineApi.defineLayer(layerBuilder, slot); })
|
|
5320
5687
|
.catch(err => console.error('[ixmaps-gl] map.replace():', err));
|
|
5321
5688
|
return engineApi;
|
|
5322
5689
|
},
|
|
@@ -5331,11 +5698,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5331
5698
|
layerBuilder = unwrapLayer(layerBuilder);
|
|
5332
5699
|
const f = String(flag || '');
|
|
5333
5700
|
_chainedLayers = _chainedLayers.then(() => {
|
|
5701
|
+
let slot = null;
|
|
5334
5702
|
if (!/force/i.test(f)) {
|
|
5335
5703
|
if (/replace/i.test(f)) {
|
|
5336
5704
|
const def = layerBuilder.definition();
|
|
5337
5705
|
const id = (def.style && def.style.name) || (def.meta && def.meta.name);
|
|
5338
|
-
if (id) removeThemeById(id);
|
|
5706
|
+
if (id) { slot = captureSlot(id); removeThemeById(id); }
|
|
5339
5707
|
} else if (/clearcharts|clear|clearall/i.test(f)) {
|
|
5340
5708
|
const charts = /clearcharts/i.test(f);
|
|
5341
5709
|
const doomed = runtimes.filter(rt => !flatFlag(rt.flags, 'LOCKED') && (!charts || rt.flags.has('CHART')));
|
|
@@ -5344,7 +5712,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5344
5712
|
byName.forEach((list, name) => removeRuntimes(name, list));
|
|
5345
5713
|
}
|
|
5346
5714
|
}
|
|
5347
|
-
return engineApi.defineLayer(layerBuilder);
|
|
5715
|
+
return engineApi.defineLayer(layerBuilder, slot);
|
|
5348
5716
|
}).catch(err => console.error('[ixmaps-gl] map.newTheme():', err));
|
|
5349
5717
|
return engineApi;
|
|
5350
5718
|
},
|
|
@@ -5550,7 +5918,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5550
5918
|
// called after every redraw (pan/zoom, or any of the setters above)
|
|
5551
5919
|
// so a page can keep a facets sidebar in sync, mirroring the real
|
|
5552
5920
|
// engine's htmlgui_onDrawTheme hook
|
|
5553
|
-
|
|
5921
|
+
// returns an unsubscribe (the legend drops its listener with its panel)
|
|
5922
|
+
onRedraw: (cb) => {
|
|
5923
|
+
redrawListeners.push(cb);
|
|
5924
|
+
return () => { const i = redrawListeners.indexOf(cb); if (i >= 0) redrawListeners.splice(i, 1); };
|
|
5925
|
+
},
|
|
5554
5926
|
refresh: () => refresh(),
|
|
5555
5927
|
// Adds ONE new layer/theme to an ALREADY-BUILT map — build()'s own
|
|
5556
5928
|
// loop above only ever runs once, at construction, so a caller
|
|
@@ -5572,7 +5944,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5572
5944
|
// exactly how this engine's own facet-filter/style setters below
|
|
5573
5945
|
// already push responsibility for "what should happen" to the
|
|
5574
5946
|
// caller rather than guessing.
|
|
5575
|
-
defineLayer: (layerBuilder) => defineFromDefinition(unwrapLayer(layerBuilder).definition()),
|
|
5947
|
+
defineLayer: (layerBuilder, slot) => defineFromDefinition(unwrapLayer(layerBuilder).definition(), slot),
|
|
5576
5948
|
// GL-PORT COMPAT: real ixmaps-flat's map handle, as a page gets it
|
|
5577
5949
|
// from `myMap.then(map => map.view(...).options(...).layer(a)
|
|
5578
5950
|
// .layer(b))` — the common multi-layer idiom — is chainable on the
|
|
@@ -5594,6 +5966,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5594
5966
|
if (o && typeof o === 'object') {
|
|
5595
5967
|
Object.assign(builder._engineOptions, o);
|
|
5596
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(); }
|
|
5597
5971
|
if ('worldcopies' in o) map.setRenderWorldCopies(o.worldcopies !== false && o.worldcopies !== 'false');
|
|
5598
5972
|
refresh();
|
|
5599
5973
|
}
|
|
@@ -5700,7 +6074,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5700
6074
|
// layers — leaving its groups mounted keeps the charts on the
|
|
5701
6075
|
// map after map.remove()/replace() (censimenti's sidebar)
|
|
5702
6076
|
if (rt._clearUserChartPane) rt._clearUserChartPane();
|
|
6077
|
+
if (rt._chartPaneDefs) { rt._chartPaneDefs.remove(); rt._chartPaneDefs = null; }
|
|
5703
6078
|
if (rt._chartHost && rt._chartHost.runtimes) rt._chartHost.runtimes.delete(rt);
|
|
6079
|
+
// and its hidden <name>_init group (userChartHost, on the body)
|
|
6080
|
+
if (rt._userChartGroup) { rt._userChartGroup.remove(); rt._userChartGroup = null; }
|
|
5704
6081
|
});
|
|
5705
6082
|
const removed = removedRuntimes.length > 0;
|
|
5706
6083
|
// stale-registry guard: _globalThemeRegistry is a bare name->
|
|
@@ -5729,6 +6106,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5729
6106
|
if (removed) { refresh(); notifyRedraw(); }
|
|
5730
6107
|
return removed;
|
|
5731
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
|
+
|
|
5732
6120
|
// flat addresses a theme by its id — style.name / meta.name (several
|
|
5733
6121
|
// themes may share one layer), else the layer name
|
|
5734
6122
|
function removeThemeById(id) {
|
|
@@ -5899,11 +6287,16 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5899
6287
|
// flat itself does ("defining a theme with the same name replaces it
|
|
5900
6288
|
// in place"; a page's live-refresh loop relies on it, e.g. the NYC
|
|
5901
6289
|
// status page re-defining its two themes every 10s).
|
|
5902
|
-
|
|
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) {
|
|
5903
6293
|
if (validation) validation.definition(def);
|
|
5904
6294
|
const pdef = projectThemeToDefinition(def);
|
|
5905
6295
|
const replacedId = (pdef.meta && pdef.meta.name) || (pdef.style && pdef.style.name);
|
|
5906
|
-
if (replacedId)
|
|
6296
|
+
if (replacedId) {
|
|
6297
|
+
if (!slot) slot = captureSlot(replacedId);
|
|
6298
|
+
removeThemeById(replacedId);
|
|
6299
|
+
}
|
|
5907
6300
|
let spec = normalizeTheme(pdef);
|
|
5908
6301
|
let raw;
|
|
5909
6302
|
const deferred = deferredFeatureLoad(spec, map.getZoom());
|
|
@@ -5930,7 +6323,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5930
6323
|
rt._zoomToPending = wantsZoomToExtent(spec.flags);
|
|
5931
6324
|
rt._dataSourceKey = named ? 'name:' + spec.data.name
|
|
5932
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) });
|
|
5933
|
-
|
|
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);
|
|
5934
6330
|
if (fc._sphere) applySphereBackdrop(map, rt.style);
|
|
5935
6331
|
_globalThemeRegistry.set(rt.name, rt);
|
|
5936
6332
|
if (rt.meta && rt.meta.name && rt.meta.name !== rt.name) _globalThemeRegistry.set(rt.meta.name, rt);
|
|
@@ -5980,10 +6376,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5980
6376
|
for (let i = 0; i < themes.length; i++) {
|
|
5981
6377
|
const t = themes[i] || {};
|
|
5982
6378
|
const name = (t.style && t.style.name) || (t.meta && t.meta.name);
|
|
5983
|
-
|
|
6379
|
+
let slot = null;
|
|
6380
|
+
if (/replace/i.test(f) && name) { slot = captureSlot(name); removeThemesNamed(name); }
|
|
5984
6381
|
else if (i === 0 && !/add/i.test(f)) [...new Set(runtimes.map(r => r.name))].forEach(n => engineApi.removeTheme(n));
|
|
5985
6382
|
try {
|
|
5986
|
-
report.themes.push(await defineFromDefinition(withoutProjectCode(t, report)));
|
|
6383
|
+
report.themes.push(await defineFromDefinition(withoutProjectCode(t, report), slot));
|
|
5987
6384
|
} catch (e) {
|
|
5988
6385
|
report.skipped.push({ layer: t.layer, reason: e.message });
|
|
5989
6386
|
}
|
|
@@ -6002,8 +6399,6 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6002
6399
|
const svgProjection = flatSvgProjectionOf(svg) || (/orthographic\.svg$/i.test(svg) ? FLAT_ORTHOGRAPHIC_PROJECTION : null);
|
|
6003
6400
|
if (svgProjection) flatSvgProjectionByOptions.set(builder._engineOptions, svgProjection);
|
|
6004
6401
|
else flatSvgProjectionByOptions.delete(builder._engineOptions);
|
|
6005
|
-
if (svgProjection && svgProjection.invert) flatPlaneProjectionByOptions.set(builder._engineOptions, svgProjection);
|
|
6006
|
-
else flatPlaneProjectionByOptions.delete(builder._engineOptions);
|
|
6007
6402
|
if (/orthographic\.svg$/i.test(svg)) flatOrthographicByOptions.set(builder._engineOptions, true);
|
|
6008
6403
|
else { flatOrthographicByOptions.delete(builder._engineOptions); flatViewCenterByOptions.delete(builder._engineOptions); }
|
|
6009
6404
|
}
|
|
@@ -6174,6 +6569,22 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6174
6569
|
if (order !== refreshLayers._lastOrder) { refreshLayers._lastOrder = order; console.info('[ixmaps-gl debug] draw order (bottom → top): ' + order); }
|
|
6175
6570
|
}
|
|
6176
6571
|
overlay.setProps({ layers });
|
|
6572
|
+
// start/stop the flow repaint loop: any layer whose paths carry
|
|
6573
|
+
// dash offsets animates, and only then (a static map renders
|
|
6574
|
+
// nothing new; the loop is one rAF, one triggerRepaint each)
|
|
6575
|
+
const flowOn = layers.some(l => l.constructor.layerName === 'FlowPathLayer'
|
|
6576
|
+
&& l.props.data && l.props.data.some(d => d.dash && d.dash[0] + d.dash[1] > 0));
|
|
6577
|
+
if (glDebug()) console.info('[ixmaps-gl debug] flow loop on: ' + flowOn + ' (' + layers.filter(l => l.constructor.layerName === 'FlowPathLayer').length + ' flow layers, '
|
|
6578
|
+
+ (layers.find(l => l.constructor.layerName === 'FlowPathLayer') || {props: {data: []}}).props.data.length + ' paths)');
|
|
6579
|
+
if (flowOn) {
|
|
6580
|
+
if (_flowRepaintRAF == null) {
|
|
6581
|
+
const loop = () => { if (map.triggerRepaint) map.triggerRepaint(); _flowRepaintRAF = global.requestAnimationFrame(loop); };
|
|
6582
|
+
_flowRepaintRAF = global.requestAnimationFrame(loop);
|
|
6583
|
+
}
|
|
6584
|
+
} else if (_flowRepaintRAF != null) {
|
|
6585
|
+
global.cancelAnimationFrame(_flowRepaintRAF);
|
|
6586
|
+
_flowRepaintRAF = null;
|
|
6587
|
+
}
|
|
6177
6588
|
// flat's WMS themes render as native raster layers, not deck.gl —
|
|
6178
6589
|
// reconcile them with the current runtimes on every rebuild
|
|
6179
6590
|
syncWmsLayers();
|
|
@@ -6334,7 +6745,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6334
6745
|
} finally { inDrawThemeHook = false; }
|
|
6335
6746
|
}
|
|
6336
6747
|
function notifyRedraw() {
|
|
6337
|
-
redrawListeners.forEach(cb => { try { cb(); } catch (err) { console.error('[ixmaps-gl] onRedraw callback failed:', err); } });
|
|
6748
|
+
redrawListeners.slice().forEach(cb => { try { cb(); } catch (err) { console.error('[ixmaps-gl] onRedraw callback failed:', err); } });
|
|
6338
6749
|
callDrawThemeHooks();
|
|
6339
6750
|
}
|
|
6340
6751
|
|
|
@@ -6516,6 +6927,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6516
6927
|
};
|
|
6517
6928
|
}
|
|
6518
6929
|
_resolveMapReady(engineApi);
|
|
6930
|
+
if (this.mapOptions && this.mapOptions.name) namedMapReady(this.mapOptions.name).resolve(engineApi);
|
|
6519
6931
|
resolveBuilt();
|
|
6520
6932
|
namedLoads.forEach(rt => { listenNamedData(rt); if (!rt._deferredLoad) loadNamedTheme(rt, false); });
|
|
6521
6933
|
return engineApi;
|
|
@@ -6547,6 +6959,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6547
6959
|
// class used throughout this file — a real bug caught during testing
|
|
6548
6960
|
// (every `new Map()` below silently resolved to *this* function instead)
|
|
6549
6961
|
function createMap(containerId, options, mapFn) {
|
|
6962
|
+
if (options && options.name) namedMapReady(options.name);
|
|
6550
6963
|
const builder = new MapBuilder(containerId, options);
|
|
6551
6964
|
if (mapFn) mapFn(builder);
|
|
6552
6965
|
// If build() rejects (bad data URL, misconfigured CHOROPLETH join,
|
|
@@ -6614,7 +7027,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6614
7027
|
// flat's mapApi.setView(center, zoom) (htmlgui_flat.js 1327) — the
|
|
6615
7028
|
// same live-or-build-time split as .view() above
|
|
6616
7029
|
promise.setView = (...args) => promise.view(...args);
|
|
6617
|
-
|
|
7030
|
+
// post-build: the live map's options() (merged into the options
|
|
7031
|
+
// object the running themes hold), as .view() above — the builder's
|
|
7032
|
+
// own is read once, by build()
|
|
7033
|
+
promise.options = (...args) => { if (resolvedApi) resolvedApi.options(...args); else builder.options(...args); return promise; };
|
|
6618
7034
|
promise.attribution = (...args) => { builder.attribution(...args); return promise; };
|
|
6619
7035
|
promise.legend = (...args) => { builder.legend(...args); return promise; };
|
|
6620
7036
|
promise.local = (...args) => { builder.local(...args); return promise; };
|
|
@@ -6838,11 +7254,6 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6838
7254
|
// the flat SVG projection a map's options object belongs to (set by the
|
|
6839
7255
|
// map's init and loadProject; the runtimes only see the options object)
|
|
6840
7256
|
const flatSvgProjectionByOptions = new WeakMap();
|
|
6841
|
-
// the projection plane a map's VECTOR / BEZIER flows are drawn in: flat's
|
|
6842
|
-
// map file, or the Map() option mapProjection ("equalearth") — flat computes
|
|
6843
|
-
// the curves in its map's own coordinates, so the same flow bows around the
|
|
6844
|
-
// pole there. (Unlike flatSvgProjectionByOptions it leaves the view framing alone.)
|
|
6845
|
-
const flatPlaneProjectionByOptions = new WeakMap();
|
|
6846
7257
|
// a map that is flat's orthographic one (drawn as MapLibre's globe): its object scale is
|
|
6847
7258
|
// flat's (scale × the globe's pixels per degree at its center, measured on flat 2026-10-10
|
|
6848
7259
|
// on the mechanical-goods globe page, the same at view zooms 2.5 and 3.5: 197.44e6; the
|
|
@@ -6885,12 +7296,6 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6885
7296
|
const radius = 512 * Math.pow(2, zoom) / (2 * Math.PI * c);
|
|
6886
7297
|
return Math.log2(globeDiscRadius(radius, height > 0 ? height : 768) * 2 * Math.PI / 512);
|
|
6887
7298
|
}
|
|
6888
|
-
function flatPlaneProjectionOf(mapOptions) {
|
|
6889
|
-
const byUrl = flatSvgProjectionOf(mapOptions.map);
|
|
6890
|
-
if (byUrl) return byUrl;
|
|
6891
|
-
const named = FLAT_SVG_PROJECTIONS[String(mapOptions.mapProjection || '').trim().toLowerCase()];
|
|
6892
|
-
return named || null;
|
|
6893
|
-
}
|
|
6894
7299
|
// flat SVG map scale ÷ the Mercator one at the same on-screen size (1
|
|
6895
7300
|
// without a flat SVG projection)
|
|
6896
7301
|
function flatMapScaleRatio(mapOptions) {
|
|
@@ -7113,7 +7518,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7113
7518
|
: 1;
|
|
7114
7519
|
}
|
|
7115
7520
|
|
|
7116
|
-
function valueRadius(value, zoom, style, mapOptions, flags, maxSizeValue) {
|
|
7521
|
+
function valueRadius(value, zoom, style, mapOptions, flags, maxSizeValue, sizeBound) {
|
|
7117
7522
|
const zoomFactor = objectZoomFactor(zoom, mapOptions);
|
|
7118
7523
|
// FIXSIZE (a symbol chart; PLOT / GRIDSIZE size their markers
|
|
7119
7524
|
// themselves): one radius for every item, whatever its value — the
|
|
@@ -7156,7 +7561,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7156
7561
|
if (flags && flags.has('SYMBOL') && !flags.has('BUBBLE') && !flags.has('USER')
|
|
7157
7562
|
&& !flags.has('FIXSIZE') && !flags.has('PLOT') && !flags.has('GRIDSIZE')
|
|
7158
7563
|
&& flags.has('CATEGORICAL') && !flags.has('AGGREGATE') && !flatFlag(flags, 'GROUP')
|
|
7159
|
-
&& !style.sizefield) {
|
|
7564
|
+
&& !style.sizefield && !sizeBound) {
|
|
7160
7565
|
const fixed = NORMAL_RADIUS_PX * (styleNum(style.scale) || 1);
|
|
7161
7566
|
return Math.max(VALUE_RADIUS_MIN, Math.min(VALUE_RADIUS_MAX, fixed * zoomFactor));
|
|
7162
7567
|
}
|
|
@@ -7529,6 +7934,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7529
7934
|
return out;
|
|
7530
7935
|
}
|
|
7531
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
|
+
|
|
7532
7944
|
// ChartColors.textColor (colorscheme.js 104-116): darker on light colors
|
|
7533
7945
|
function flatChartTextRgb(rgb) {
|
|
7534
7946
|
return flatDerivateRgb(rgb, rgb[0] + rgb[1] + rgb[2] > 450 ? 0.6 : 3);
|
|
@@ -7678,8 +8090,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7678
8090
|
_measureCtx.font = fontPx + 'px arial';
|
|
7679
8091
|
return _measureCtx.measureText(text).width;
|
|
7680
8092
|
}
|
|
7681
|
-
function wrapTitleLines(text, fontPx, widthPx) {
|
|
7682
|
-
const width = w =>
|
|
8093
|
+
function wrapTitleLines(text, fontPx, widthPx, measure = titleTextWidth) {
|
|
8094
|
+
const width = w => measure(w, fontPx);
|
|
7683
8095
|
const lines = [];
|
|
7684
8096
|
for (const word of String(text).split(/\s+/).filter(Boolean)) {
|
|
7685
8097
|
const last = lines.length ? lines[lines.length - 1] + ' ' + word : null;
|
|
@@ -8629,39 +9041,55 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8629
9041
|
return { bars, texts, lines, extent: ext[0] <= ext[2] ? ext : [0, 0, 0, 0], width: W, sizer };
|
|
8630
9042
|
}
|
|
8631
9043
|
|
|
8632
|
-
//
|
|
8633
|
-
//
|
|
8634
|
-
//
|
|
8635
|
-
//
|
|
8636
|
-
//
|
|
8637
|
-
//
|
|
8638
|
-
//
|
|
8639
|
-
//
|
|
8640
|
-
|
|
9044
|
+
// THE box-and-title layout of every chart type (flat's BOX block,
|
|
9045
|
+
// maptheme.js 18757-18925): single symbol, SEQUENCE, PIE and BAR all call
|
|
9046
|
+
// this. `extent`: the chart's drawn [x0, y0, x1, y1] around its anchor (px;
|
|
9047
|
+
// chart units with unit 1). o: { unit (px per flat unit, default 1), margin
|
|
9048
|
+
// (boxmargin, flat's 2), circular (CIRCULARBOX), radius / center (the
|
|
9049
|
+
// circle's base radius and center; default the extent's half longer side
|
|
9050
|
+
// and its center), titleText (with the titleunits suffix, '' / null: no
|
|
9051
|
+
// title), titleFont (px, or w => px for DTEXT), bottomTitle, alignRight,
|
|
9052
|
+
// wrap, measure }. Returns { rect (rectangular box), boxR / cx / cy (the
|
|
9053
|
+
// circular box: radius, center), w (the extent's width), title { text,
|
|
9054
|
+
// lines, x, anchor, top, baseline, f, extent } }.
|
|
9055
|
+
// The margin is min(2 m u, m w / 30); the title's first baseline 0.7 font
|
|
9056
|
+
// above the box (BOTTOMTITLE: one font below), wrapped to the chart's width
|
|
9057
|
+
// (the circle's diameter); a rectangular box grows to enclose the title,
|
|
9058
|
+
// its top trimmed by 0.1 font. arial: ascent 0.905, descent 0.212.
|
|
9059
|
+
function chartBoxLayout(extent, o) {
|
|
8641
9060
|
const e = extent.slice();
|
|
8642
|
-
const w = e[2] - e[0];
|
|
8643
|
-
const
|
|
9061
|
+
const w = e[2] - e[0], h = e[3] - e[1];
|
|
9062
|
+
const unit = o.unit == null ? 1 : o.unit;
|
|
9063
|
+
const circular = !!o.circular;
|
|
9064
|
+
const margin = Math.min(2 * o.margin * unit, o.margin * w / 30);
|
|
9065
|
+
const center = o.center || [(e[0] + e[2]) / 2, (e[1] + e[3]) / 2];
|
|
9066
|
+
const boxR = circular ? (o.radius != null ? o.radius : Math.max(w, h) / 2) * 1.1 + margin : 0;
|
|
8644
9067
|
let title = null;
|
|
8645
|
-
if (o.
|
|
8646
|
-
const f = o.titleFont;
|
|
8647
|
-
const measure = o.measure ||
|
|
8648
|
-
const lines =
|
|
8649
|
-
for (const word of String(o.title).split(/\s+/).filter(Boolean)) {
|
|
8650
|
-
const last = lines.length ? lines[lines.length - 1] + ' ' + word : null;
|
|
8651
|
-
if (last !== null && measure(last, f) <= w) lines[lines.length - 1] = last;
|
|
8652
|
-
else lines.push(word);
|
|
8653
|
-
}
|
|
9068
|
+
if (o.titleText) {
|
|
9069
|
+
const f = typeof o.titleFont === 'function' ? o.titleFont(w) : o.titleFont;
|
|
9070
|
+
const measure = o.measure || titleTextWidth;
|
|
9071
|
+
const lines = wrapTitleLines(o.titleText, f, circular ? boxR * 2 : w, measure).split('\n');
|
|
8654
9072
|
const width = Math.max(...lines.map(l => measure(l, f)));
|
|
8655
|
-
|
|
8656
|
-
const
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
9073
|
+
// first baseline; lines go down one font each
|
|
9074
|
+
const base = circular
|
|
9075
|
+
? (o.bottomTitle ? center[1] + boxR + f : center[1] - boxR - 0.7 * f - (lines.length - 1) * f)
|
|
9076
|
+
: (o.bottomTitle ? e[3] + f : e[1] - 0.7 * f - (lines.length - 1) * f);
|
|
9077
|
+
const x = circular ? center[0] + unit / 2 : (o.alignRight ? e[2] : e[0]);
|
|
9078
|
+
const anchor = circular ? 'middle' : (o.alignRight ? 'end' : 'start');
|
|
9079
|
+
const x0 = anchor === 'middle' ? x - width / 2 : anchor === 'end' ? x - width : x;
|
|
9080
|
+
title = { text: lines.join('\n'), lines, x, anchor, top: base - 0.8 * f, baseline: base, f,
|
|
9081
|
+
extent: [x0, base - 0.905 * f, x0 + width, base + (lines.length - 1) * f + 0.212 * f] };
|
|
8663
9082
|
}
|
|
8664
|
-
|
|
9083
|
+
let rect = null;
|
|
9084
|
+
if (!circular) {
|
|
9085
|
+
const b = e.slice();
|
|
9086
|
+
if (title) {
|
|
9087
|
+
b[0] = Math.min(b[0], title.extent[0]); b[1] = Math.min(b[1], title.extent[1]) + 0.1 * title.f;
|
|
9088
|
+
b[2] = Math.max(b[2], title.extent[2]); b[3] = Math.max(b[3], title.extent[3]);
|
|
9089
|
+
}
|
|
9090
|
+
rect = [b[0] - margin, b[1] - margin, b[2] + margin, b[3] + margin];
|
|
9091
|
+
}
|
|
9092
|
+
return { rect, boxR, cx: center[0], cy: center[1], w, margin, title };
|
|
8665
9093
|
}
|
|
8666
9094
|
|
|
8667
9095
|
// CHART|PIE / DONUT: one pie's slices as real ixmaps-flat lays them out —
|
|
@@ -8693,10 +9121,38 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8693
9121
|
// centered on 12 o'clock), HALF over 180° from 270° (HALFPLUS10 200°
|
|
8694
9122
|
// from 260°, HALFMINUS10 160° from 280°, HALFPLUS20 220° from 250°,
|
|
8695
9123
|
// HALFMINUS20 140° from 290°)
|
|
9124
|
+
// - STARBURST (piechart.js 246, 324-355, 415-420): every part >= 0, the
|
|
9125
|
+
// angles EQUAL (the value is the ray's radius, not its sweep) and the
|
|
9126
|
+
// percents NOT normalized — raw values. The FLOWER/RAYS variants
|
|
9127
|
+
// (XLFLOWER/XLRAYS, LFLOWER/LRAYS, XSFLOWER/XSRAYS, SFLOWER/SRAYS,
|
|
9128
|
+
// FLOWER/RAYS) cap the ray angle (maxAngle / 4 / 12 / 10 / none) and
|
|
9129
|
+
// inset a gap (10 / 20 / 40 / 33 / 35 % of it) on both sides of each
|
|
9130
|
+
// ray. The ray RADIUS is
|
|
9131
|
+
// computed in _buildPieLayers (it needs the theme's maxValue)
|
|
8696
9132
|
function pieSliceLayout(values, t, opts = {}) {
|
|
8697
9133
|
t = String(t || '');
|
|
8698
9134
|
const has = re => re.test(t);
|
|
8699
|
-
|
|
9135
|
+
// flat's STARBURST (piechart.js realize 246, 324-355, 415-420): the
|
|
9136
|
+
// parts get EQUAL angles — the value is the ray's radius, not its
|
|
9137
|
+
// sweep — and every part is clamped to >= 0. The FLOWER/RAYS
|
|
9138
|
+
// variants (flat maps XLFLOWER to XLRAYS & kin, maptheme.js
|
|
9139
|
+
// 20169-20186) cap the ray angle and leave a gap between the rays.
|
|
9140
|
+
// flat's `nRadInner = max(5, nRadInner)` for the ray variants is 5
|
|
9141
|
+
// units of the chart's INTERNAL coordinate system — measured on the
|
|
9142
|
+
// ISTAT story: 5 of the disc's 197 internal units, 0.28 screen px,
|
|
9143
|
+
// sub-pixel and invisible (the petals go to the center; an inner
|
|
9144
|
+
// radius shows only with DONUT) — so starInner here is 0 for the
|
|
9145
|
+
// ray variants too, and only DONUT+STARBURST starts its rays at
|
|
9146
|
+
// the donut's inner radius (see _buildPieLayers)
|
|
9147
|
+
const star = has(/STARBURST/);
|
|
9148
|
+
const starRay = !star ? null
|
|
9149
|
+
: /XLFLOWER|XLRAYS/.test(t) ? { cap: 4, gap: 0.10 }
|
|
9150
|
+
: /LFLOWER|LRAYS/.test(t) ? { cap: 4, gap: 0.20 }
|
|
9151
|
+
: /XSFLOWER|XSRAYS/.test(t) ? { cap: 12, gap: 0.40 }
|
|
9152
|
+
: /SFLOWER|SRAYS/.test(t) ? { cap: 10, gap: 0.33 }
|
|
9153
|
+
: /FLOWER|RAYS/.test(t) ? { cap: 1, gap: 0.35 }
|
|
9154
|
+
: { cap: 1, gap: 0 };
|
|
9155
|
+
const parts = values.map(v => (isNaN(v) ? 0 : star ? Math.max(0, v) : v));
|
|
8700
9156
|
const sum = parts.reduce((a, v) => a + v, 0);
|
|
8701
9157
|
const autocomplete = has(/AUTOCOMPLETE/);
|
|
8702
9158
|
if (sum === 0 && !autocomplete) return null;
|
|
@@ -8749,13 +9205,18 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8749
9205
|
// flat draws its "Pie error: no values !" circle here
|
|
8750
9206
|
if (!any && !autocomplete) return null;
|
|
8751
9207
|
const pctSum = slices.reduce((a, sl) => a + sl.percent, 0);
|
|
8752
|
-
|
|
9208
|
+
// flat's STARBURST keeps the parts' raw values (piechart.js 246-258:
|
|
9209
|
+
// no 100% normalization, no AUTOCOMPLETE) — the value is the radius
|
|
9210
|
+
if (!star && pctSum !== 100) {
|
|
8753
9211
|
if (autocomplete && pctSum < 100) slices.push({ i: -1, value: null, percent: Math.floor((100 - pctSum) * 100000) / 100000, complement: true });
|
|
8754
9212
|
else slices.forEach(sl => { sl.percent = sl.percent / pctSum * 100; });
|
|
8755
9213
|
}
|
|
8756
9214
|
let end = 0;
|
|
8757
9215
|
if (has(/SYMMETRIC/) && slices.length) {
|
|
8758
|
-
if (
|
|
9216
|
+
if (star) {
|
|
9217
|
+
// flat piechart.js 296-299: the first ray centered on 12 o'clock
|
|
9218
|
+
end = 360 - Math.floor(360 / parts.length / 2);
|
|
9219
|
+
} else if (has(/BIGTOTOP/)) {
|
|
8759
9220
|
end = 360;
|
|
8760
9221
|
let x = 0;
|
|
8761
9222
|
for (; x < slices.length && slices[x] !== biggest; x++) end -= slices[x].percent < 100 ? Math.floor(360 / 100 * slices[x].percent) : 0;
|
|
@@ -8776,6 +9237,20 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8776
9237
|
if (has(/HALFMINUS20/)) maxAngle = 140;
|
|
8777
9238
|
const out = [];
|
|
8778
9239
|
slices.forEach((sl, k) => {
|
|
9240
|
+
if (star) {
|
|
9241
|
+
// flat piechart.js 324-355, 395-403: the angles advance by the
|
|
9242
|
+
// FULL equal angle (the ray variants capped), the gap inset on
|
|
9243
|
+
// both sides of a ray; a ray's own sweep is that reduced angle
|
|
9244
|
+
const angle = Math.min(maxAngle / starRay.cap, maxAngle / parts.length);
|
|
9245
|
+
const base = end % 360;
|
|
9246
|
+
end = base + angle;
|
|
9247
|
+
if (end > 360) end = end % 360;
|
|
9248
|
+
const sweep = angle * (1 - 2 * starRay.gap);
|
|
9249
|
+
const start = base + angle * starRay.gap;
|
|
9250
|
+
if (sweep <= 0 || start < 0) return;
|
|
9251
|
+
out.push(Object.assign(sl, { start, sweep, k }));
|
|
9252
|
+
return;
|
|
9253
|
+
}
|
|
8779
9254
|
const sweep = sl.percent / 100 * maxAngle;
|
|
8780
9255
|
const start = end % 360;
|
|
8781
9256
|
end = start + sweep;
|
|
@@ -8902,6 +9377,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8902
9377
|
const parts = [], zeroNot = [];
|
|
8903
9378
|
for (let k = 0; k < o.fields.length; k++) {
|
|
8904
9379
|
let v = parseFloat(raw[o.fields[k]]);
|
|
9380
|
+
// flat's weights multiply every record's per-field value as it is
|
|
9381
|
+
// accumulated (maptheme.js 10506-10508) — the SIZE (nValueSize)
|
|
9382
|
+
// stays raw, exactly as there
|
|
9383
|
+
if (o.weights) v = v * o.weights(k);
|
|
8905
9384
|
zeroNot[k] = false;
|
|
8906
9385
|
if (isNaN(v)) {
|
|
8907
9386
|
if (!r.undefinedOk) return null;
|
|
@@ -9111,6 +9590,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9111
9590
|
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
|
9112
9591
|
return (h >>> 0) / 4294967296;
|
|
9113
9592
|
}
|
|
9593
|
+
// a flow's dash phase in [0, 1): flat's random one, the same for the
|
|
9594
|
+
// same arc on every rebuild
|
|
9595
|
+
function flowDashPhase(p1, p2) {
|
|
9596
|
+
return hashUnit('dash:' + p1[0] + ',' + p1[1] + '&' + p2[0] + ',' + p2[1]);
|
|
9597
|
+
}
|
|
9114
9598
|
|
|
9115
9599
|
// points of a cubic Bézier, n segments (n + 1 points)
|
|
9116
9600
|
function cubicBezierPoints(p0, c1, c2, p3, n) {
|
|
@@ -9273,14 +9757,21 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9273
9757
|
const up = fl.up;
|
|
9274
9758
|
const maxR = opts.maxRadius || 0;
|
|
9275
9759
|
const nStarParts = opts.nParts || counts.length;
|
|
9276
|
-
|
|
9760
|
+
// flat's star radius (nStarRadius, 0 per chart) is set ONLY by the FIRST
|
|
9761
|
+
// part of the sorted order (maptheme.js 22301-22316: `if (i === 0)`), and a
|
|
9762
|
+
// part of value 0 is skipped before it (22222): a chart whose first part
|
|
9763
|
+
// (with SORT|UP the smallest) is 0 keeps star radius 0, so with UP every
|
|
9764
|
+
// part sits on the center — only a chart with ALL parts non-zero spreads
|
|
9765
|
+
// into its star. (A cell of a CATEGORICAL AGGREGATE theme has a zero part
|
|
9766
|
+
// for every category it lacks.)
|
|
9767
|
+
let angle = 0, starR = 0, x = 0;
|
|
9277
9768
|
order.forEach((p, k) => {
|
|
9278
9769
|
if (!p.v) return;
|
|
9279
9770
|
const r = radiusOf(p.v);
|
|
9280
9771
|
if (!(r > 0)) return;
|
|
9281
9772
|
let px = 0, py = 0;
|
|
9282
9773
|
if (star) {
|
|
9283
|
-
if (
|
|
9774
|
+
if (k === 0) {
|
|
9284
9775
|
starR = r;
|
|
9285
9776
|
if (fl.expand && fl.sort && up) {
|
|
9286
9777
|
const big = order[order.length - 1].v, mv = opts.maxValue || big || 1;
|
|
@@ -9288,7 +9779,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9288
9779
|
}
|
|
9289
9780
|
if (fl.compress) starR *= fl.compressmax ? 0.25 : 0.5;
|
|
9290
9781
|
}
|
|
9291
|
-
|
|
9782
|
+
// flat: `(i > 0 || UP) && nRadius` — i the part's index in the sorted order
|
|
9783
|
+
if (k > 0 || up) {
|
|
9292
9784
|
const c = starR + r;
|
|
9293
9785
|
const b = Math.sqrt(Math.max(0, c * c - r * r));
|
|
9294
9786
|
let f = 90 - Math.asin(Math.min(1, b / c)) / Math.PI * 180;
|
|
@@ -9837,7 +10329,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9837
10329
|
// 7740-7801): a value that is no number counts as 0 — unless
|
|
9838
10330
|
// UNDEFINEDISNOTVALUE —, and 0 is a value — unless ZEROISNOTVALUE
|
|
9839
10331
|
function flatNumber(v, flags) {
|
|
9840
|
-
|
|
10332
|
+
// flat's own value scan (maptheme.js __scanValue 9265-9272): a
|
|
10333
|
+
// value with a comma is EUROPEAN format — dots stripped as
|
|
10334
|
+
// thousands separators, the comma is the decimal ("78,8" -> 78.8);
|
|
10335
|
+
// a value without one is parsed plain, blanks stripped. Applied
|
|
10336
|
+
// to every theme value in flat, so also here
|
|
10337
|
+
const s = String(v);
|
|
10338
|
+
let n = s.includes(',') ? parseFloat(s.replace(/\./g, '').replace(/,/g, '.')) : parseFloat(s.replace(/ /g, ''));
|
|
9841
10339
|
if (isNaN(n)) n = flags && flags.has('UNDEFINEDISNOTVALUE') ? NaN : 0;
|
|
9842
10340
|
if (n === 0 && flags && flags.has('ZEROISNOTVALUE')) return NaN;
|
|
9843
10341
|
return n;
|
|
@@ -9848,6 +10346,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9848
10346
|
const bins = [arr.reduce((a, v) => (v < a ? v : a), Infinity), m];
|
|
9849
10347
|
while (arr.length > 1) {
|
|
9850
10348
|
arr = arr.filter(d => d > m);
|
|
10349
|
+
// every value left equals the last mean (a tie at the top): there is
|
|
10350
|
+
// no head above it — mean([]) would add a NaN break (flat's own bug)
|
|
10351
|
+
if (!arr.length) break;
|
|
9851
10352
|
m = mean(arr);
|
|
9852
10353
|
bins.push(m);
|
|
9853
10354
|
}
|
|
@@ -9898,6 +10399,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9898
10399
|
// values above the last mean, while more than one is left
|
|
9899
10400
|
if (flags.has('HEADTAIL')) {
|
|
9900
10401
|
const b = headTailBreaks(values);
|
|
10402
|
+
// fewer breaks than classes (small or low-variance data): the surplus
|
|
10403
|
+
// classes padded at the top with the last break, as naturalBreaks
|
|
10404
|
+
// does — flat reads past the end (undefined bounds, records in no class)
|
|
10405
|
+
while (b.length < nParts + 1) b.push(b[b.length - 1]);
|
|
9901
10406
|
parts = new Array(nParts).fill(null).map((_, i) => ({ min: b[i], max: b[i + 1] - 0.000001 }));
|
|
9902
10407
|
} else if (flags.has('QUANTILE')) parts = quantileBreaks(values, nParts);
|
|
9903
10408
|
else if (flags.has('NATURAL')) parts = naturalBreaks(values, nParts);
|
|
@@ -10010,7 +10515,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10010
10515
|
function fieldWeightOf(style, nFields, repeatLast) {
|
|
10011
10516
|
const w = style && style.weights;
|
|
10012
10517
|
if (w == null || w === '') return null;
|
|
10013
|
-
|
|
10518
|
+
// flat's toArray (maptheme.js 999-1014): split on '|' when the
|
|
10519
|
+
// string has one, else on ',' — "1,10,10" is a THREE-weight list
|
|
10520
|
+
// (the Happy Planet page), not one weight of 1
|
|
10521
|
+
const s = Array.isArray(w) ? w.join('|') : String(w);
|
|
10522
|
+
const list = (s.includes('|') ? s.split('|') : s.split(',')).map(v => parseFloat(v));
|
|
10014
10523
|
if (!list.some(v => !isNaN(v))) return null;
|
|
10015
10524
|
if (repeatLast) { const last = list[list.length - 1]; while (list.length < nFields) list.push(last); }
|
|
10016
10525
|
return i => list[i] || list[0] || 1;
|
|
@@ -10019,9 +10528,16 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10019
10528
|
// the value fields' own values weighted — the class value and the field100
|
|
10020
10529
|
// value stay raw, exactly as flat reads them
|
|
10021
10530
|
function weightedRecord(f, weightOf) {
|
|
10531
|
+
// a pie record's parts were weighted in flatPieRecord, and its size
|
|
10532
|
+
// (nValueSize) stays raw — nothing to do here
|
|
10533
|
+
if (f.properties && f.properties.pie) return f;
|
|
10022
10534
|
const p = Object.assign({}, f.properties);
|
|
10023
10535
|
if (p.value !== undefined && !isNaN(p.value)) p.value = p.value * weightOf(0);
|
|
10024
10536
|
if (Array.isArray(p.series)) p.series = p.series.map((v, i) => (isNaN(v) ? v : v * weightOf(i)));
|
|
10537
|
+
// the multi-field pie's parts — the SAME per-field values as series,
|
|
10538
|
+
// but the grid cell's pie accumulates THESE (flatPieAccumulate's
|
|
10539
|
+
// rec.parts), so they carry the weights too
|
|
10540
|
+
if (Array.isArray(p.parts)) p.parts = p.parts.map((v, i) => (isNaN(v) ? v : v * weightOf(i)));
|
|
10025
10541
|
return Object.assign({}, f, { properties: p });
|
|
10026
10542
|
}
|
|
10027
10543
|
function applyField100(features, binding, flags, style) {
|
|
@@ -10044,7 +10560,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10044
10560
|
return features.map(f => {
|
|
10045
10561
|
if (!f || !f.properties) return f;
|
|
10046
10562
|
const props = Object.assign({}, f.properties);
|
|
10047
|
-
let values = names.map(name =>
|
|
10563
|
+
let values = names.map(name => flatScanValue(f.properties[name]));
|
|
10048
10564
|
// the weights run BEFORE the field100 arithmetic, as in flat's
|
|
10049
10565
|
// addItemValues — the FRACTION of a weighted value is the weighted
|
|
10050
10566
|
// per-unit value
|
|
@@ -10053,7 +10569,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10053
10569
|
const sum = auto100 ? values.reduce((a, v) => a + (isNaN(v) ? 0 : v), 0) : 0;
|
|
10054
10570
|
values = values.map((v, i) => {
|
|
10055
10571
|
if (isNaN(v)) return v;
|
|
10056
|
-
const v100 = auto100 ? sum :
|
|
10572
|
+
const v100 = auto100 ? sum : flatScanValue(f.properties[f100A[Math.min(i, f100A.length - 1)]]);
|
|
10057
10573
|
if (v === 0 && !flags.has('ZEROISVALUE')) return 0;
|
|
10058
10574
|
if (fields[i][0] === '!') return (isNaN(v100) ? 0 : v100) - v;
|
|
10059
10575
|
if (flags.has('CALCVAL') || flags.has('CALC100')) return v100 ? Math.round(v * v100 / 100) : v;
|
|
@@ -10099,7 +10615,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10099
10615
|
// DIFFERENCE over several value fields: the difference is the value
|
|
10100
10616
|
const fields = String(binding.value).split('|');
|
|
10101
10617
|
const valueOf = flags && flags.has('DIFFERENCE') && fields.length > 1
|
|
10102
|
-
|
|
10618
|
+
// without AGGREGATE applyField100 already differenced every record
|
|
10619
|
+
// (flat's nValuesA, popped): the first field IS the value
|
|
10620
|
+
? (!flags.has('AGGREGATE') ? p => flatNumber(p[fields[0]], flags)
|
|
10621
|
+
: p => cellAggregatedValues({ sums: fields.map(k => parseFloat(p[k]) || 0), counts: fields.map(() => 1) }, flags, style)[0])
|
|
10103
10622
|
// "$item$" is flat's record count: 1 per record
|
|
10104
10623
|
: binding.value === '$item$' ? () => 1
|
|
10105
10624
|
: p => (p && Object.keys(p).length ? flatNumber(p[binding.value], flags) : NaN);
|
|
@@ -10395,13 +10914,18 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10395
10914
|
if (cell.titleMax === undefined || (maxAgg ? m > cell.titleMax : m < cell.titleMax)) {
|
|
10396
10915
|
cell.titleMax = m;
|
|
10397
10916
|
cell.titleFrom = f;
|
|
10917
|
+
cell.titleX = p.x; cell.titleY = p.y;
|
|
10398
10918
|
}
|
|
10399
10919
|
}
|
|
10400
10920
|
}
|
|
10401
10921
|
const out = [];
|
|
10402
10922
|
for (const cell of cells.values()) {
|
|
10403
|
-
|
|
10404
|
-
|
|
10923
|
+
// RELOCATE: at the records' mean position — with MAX / MIN at the
|
|
10924
|
+
// record holding the max (min), the one the cell IS (and its title's);
|
|
10925
|
+
// only the position follows it, never a value
|
|
10926
|
+
const atTitle = relocate && cell.titleFrom;
|
|
10927
|
+
const x = atTitle ? cell.titleX : relocate ? cell.sumX / cell.n : cell.snapped.x;
|
|
10928
|
+
const y = atTitle ? cell.titleY : relocate ? cell.sumY / cell.n : cell.snapped.y;
|
|
10405
10929
|
const ll = worldPixelToLngLat(x, y, zoom);
|
|
10406
10930
|
const geometry = { type: 'Point', coordinates: [ll.lng, ll.lat] };
|
|
10407
10931
|
const hasClass = cell.first.properties.classValue !== undefined;
|
|
@@ -10702,17 +11226,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10702
11226
|
// could ever need it (a plain "px" gridwidth, or an aggregation
|
|
10703
11227
|
// array with only "px" entries, never reads this field).
|
|
10704
11228
|
const gridwidthIsMeters = typeof this.style.gridwidth === 'string' ? !/px\s*$/i.test(this.style.gridwidth) : this.style.gridwidth != null;
|
|
10705
|
-
|
|
10706
|
-
|
|
10707
|
-
|
|
10708
|
-
const lat = f.geometry && f.geometry.coordinates && f.geometry.coordinates[1];
|
|
10709
|
-
if (typeof lat === 'number' && !isNaN(lat)) {
|
|
10710
|
-
if (lat < minLat) minLat = lat;
|
|
10711
|
-
if (lat > maxLat) maxLat = lat;
|
|
10712
|
-
}
|
|
10713
|
-
});
|
|
10714
|
-
this._gridRefLat = (minLat <= maxLat) ? (minLat + maxLat) / 2 : 0;
|
|
10715
|
-
}
|
|
11229
|
+
// aggregationscale: flat's alias of aggregation, read the same way in
|
|
11230
|
+
// _ensureClusterIndices
|
|
11231
|
+
if (gridwidthIsMeters || aggregationHasMetersEntry(this.style.aggregation || this.style.aggregationscale)) this._scanGridRefLat();
|
|
10716
11232
|
|
|
10717
11233
|
// a bar chart's parts are a pie's (its value fields or categories)
|
|
10718
11234
|
if ((this._isPieChart() || this._isBarChart()) && this.binding.value) {
|
|
@@ -10799,6 +11315,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10799
11315
|
let max = 0;
|
|
10800
11316
|
for (const f of this.features) for (const field of this._multiFields) { const v = Math.abs(parseFloat(f.properties[field])); if (v > max) max = v; }
|
|
10801
11317
|
this._multiFieldPartMax = max || 1;
|
|
11318
|
+
// AUTO100's scale, measured on these features by _buildSequenceChartLayers
|
|
11319
|
+
this._seqAuto100Max = null;
|
|
10802
11320
|
} else if (this.binding.value) {
|
|
10803
11321
|
// real engine's OTHER coloring mode (maptheme.js distributeValues,
|
|
10804
11322
|
// partsA): a NUMERIC bound value, not CATEGORICAL, is classed into
|
|
@@ -10988,6 +11506,14 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10988
11506
|
_resolveAggregateValue(props) { return resolveAggregateValue(this.binding, this.flags, props); }
|
|
10989
11507
|
|
|
10990
11508
|
_buildAggregationIndex(sourceFeatures) {
|
|
11509
|
+
// only records with a point position are drawn: a GeoJSON feature may
|
|
11510
|
+
// have geometry null (RFC 7946 — open-data exports of unlocated
|
|
11511
|
+
// records), or a shape chartAnchoredFeatures could not anchor; they
|
|
11512
|
+
// stay in the data (counts, facets), as on flat, but have no chart
|
|
11513
|
+
sourceFeatures = sourceFeatures.filter(f => {
|
|
11514
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
11515
|
+
return !!c && typeof c[0] === 'number' && isFinite(c[0]) && isFinite(c[1]);
|
|
11516
|
+
});
|
|
10991
11517
|
// an AGGREGATE / CATEGORICAL pie: one bucket, each record carrying its
|
|
10992
11518
|
// contribution to its item (flatPieRecord), added up per position or
|
|
10993
11519
|
// grid cell by aggregateOnGrid (flatPieAccumulate)
|
|
@@ -10997,9 +11523,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10997
11523
|
const rules = flatValueRules(t, fields ? fields.length : 1);
|
|
10998
11524
|
const sizeField = this.binding.size && this.binding.size !== '$item$' ? this.binding.size : null;
|
|
10999
11525
|
const feats = [];
|
|
11526
|
+
// flat's per-field weights (maptheme.js 10506-10508), applied to
|
|
11527
|
+
// every record's parts inside flatPieRecord
|
|
11528
|
+
const pieWeightOf = fieldWeightOf(this.style, fields ? fields.length : 1, true);
|
|
11000
11529
|
for (const f of sourceFeatures) {
|
|
11001
|
-
const c = f.geometry && f.geometry.coordinates;
|
|
11002
|
-
if (!c || typeof c[0] !== 'number' || !isFinite(c[0]) || !isFinite(c[1])) continue;
|
|
11003
11530
|
const p = f.properties || {};
|
|
11004
11531
|
let cat;
|
|
11005
11532
|
if (this._pieCategorical) {
|
|
@@ -11007,7 +11534,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11007
11534
|
const i = k == null ? undefined : this._pieCategoryIndex.get(k);
|
|
11008
11535
|
cat = i == null ? -1 : i;
|
|
11009
11536
|
}
|
|
11010
|
-
const pie = flatPieRecord(p, { rules, sizeField, cat, fields });
|
|
11537
|
+
const pie = flatPieRecord(p, { rules, sizeField, cat, fields, weights: pieWeightOf });
|
|
11011
11538
|
if (!pie) continue;
|
|
11012
11539
|
feats.push({ type: 'Feature', geometry: f.geometry, properties: { value: pie.size, pie, raw: p } });
|
|
11013
11540
|
}
|
|
@@ -11066,7 +11593,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11066
11593
|
const seriesFields = String(this.binding.value || '').split('|');
|
|
11067
11594
|
this._seriesDifference = this.flags.has('DIFFERENCE') && seriesFields.length > 1 && !this._aggregateField100;
|
|
11068
11595
|
const seriesOf = props => seriesFields.map(k => parseFloat(props[k]) || 0);
|
|
11069
|
-
|
|
11596
|
+
// without AGGREGATE the records are already differenced (applyField100)
|
|
11597
|
+
const seriesValue = !this.flags.has('AGGREGATE') ? props => parseFloat(props[seriesFields[0]]) || 0 : props => cellAggregatedValues({ sums: seriesOf(props), counts: seriesFields.map(() => 1) }, this.flags, this.style)[0];
|
|
11070
11598
|
this._featuresByCategory = [sourceFeatures.map(f => ({
|
|
11071
11599
|
type: 'Feature',
|
|
11072
11600
|
geometry: f.geometry,
|
|
@@ -11424,6 +11952,20 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11424
11952
|
});
|
|
11425
11953
|
}
|
|
11426
11954
|
|
|
11955
|
+
// the meters grid's reference latitude: the middle of the data's own
|
|
11956
|
+
// latitude extent (see _prepare)
|
|
11957
|
+
_scanGridRefLat() {
|
|
11958
|
+
let minLat = Infinity, maxLat = -Infinity;
|
|
11959
|
+
this.features.forEach(f => {
|
|
11960
|
+
const lat = f.geometry && f.geometry.coordinates && f.geometry.coordinates[1];
|
|
11961
|
+
if (typeof lat === 'number' && !isNaN(lat)) {
|
|
11962
|
+
if (lat < minLat) minLat = lat;
|
|
11963
|
+
if (lat > maxLat) maxLat = lat;
|
|
11964
|
+
}
|
|
11965
|
+
});
|
|
11966
|
+
this._gridRefLat = (minLat <= maxLat) ? (minLat + maxLat) / 2 : 0;
|
|
11967
|
+
}
|
|
11968
|
+
|
|
11427
11969
|
// (re)builds the per-category GridAggregateIndex for the grid width
|
|
11428
11970
|
// resolved at the given zoom, only when that width changed. Grid width,
|
|
11429
11971
|
// as flat reads it (maptheme.js:1892-1908, 7990-8008): gridwidthpx and
|
|
@@ -11434,6 +11976,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11434
11976
|
// style.aggregation bracket; none → no grid (exact positions).
|
|
11435
11977
|
_ensureClusterIndices(zoom) {
|
|
11436
11978
|
let radiusPx;
|
|
11979
|
+
// a meters grid set later by setStyle (gridwidth / aggregation) never
|
|
11980
|
+
// went through _prepare's scan — not the equator, the data's latitude
|
|
11981
|
+
if (this._gridRefLat === undefined) this._scanGridRefLat();
|
|
11437
11982
|
// GL-PORT COMPAT: a real-ixmaps-flat page's own gridwidth is
|
|
11438
11983
|
// sometimes a fixed SCREEN-PIXEL size (e.g. style.gridwidth:"3px",
|
|
11439
11984
|
// confirmed against a real ported page) rather than this engine's
|
|
@@ -11480,6 +12025,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11480
12025
|
this._clusterUsesFixedZoom = false;
|
|
11481
12026
|
}
|
|
11482
12027
|
if (this._clusterIndices && this._clusterRadiusPx === radiusPx && this._clusterField === aggField) return;
|
|
12028
|
+
// new cells (e.g. a runtime gridwidth): the zoom-keyed stats of the
|
|
12029
|
+
// old ones (breaks, NORMALIZE, NOOUTLIER, sizeMax) no longer apply
|
|
12030
|
+
this._aggregateStatsCache = null;
|
|
11483
12031
|
this._clusterRadiusPx = radiusPx;
|
|
11484
12032
|
this._clusterField = aggField;
|
|
11485
12033
|
// after aggregation, on the cell sums: field100 of a value-based
|
|
@@ -11671,6 +12219,91 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11671
12219
|
return Array.isArray(cs) && cs.length === 1 && String(cs[0]).trim().toLowerCase() === 'none';
|
|
11672
12220
|
}
|
|
11673
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
|
+
|
|
11674
12307
|
_buildSingleIcon(colorRgb, opacity, shape, borderColorRgb, borderWidth, borderOpacity = 1) {
|
|
11675
12308
|
if (!colorRgb) { colorRgb = [128, 128, 128]; opacity = 0; } // no class colour (["none"]): unfilled
|
|
11676
12309
|
shape = shape || 'circle';
|
|
@@ -12080,7 +12713,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12080
12713
|
// companion layer) takes the uniform mesh shortcut.
|
|
12081
12714
|
if (this.flags.has('GRIDSIZE') && this.flags.has('PLOT')) return this._buildPlotLayers(zoom, bbox);
|
|
12082
12715
|
// PLOT without GRIDSIZE: one line chart per item over its value fields
|
|
12083
|
-
if (this.flags.has('PLOT') && this._isItemPlot()) return this._buildItemPlotLayers(zoom);
|
|
12716
|
+
if (this.flags.has('PLOT') && this._isItemPlot()) return this._buildItemPlotLayers(zoom, liveZoom);
|
|
12084
12717
|
// a plain grid (one color, no values or glow — the Germany page's
|
|
12085
12718
|
// companion "grid" layer) as one square per cell; a classed GRIDSIZE
|
|
12086
12719
|
// chart (the fuel-price page) is a symbol chart sized to the grid,
|
|
@@ -12666,16 +13299,20 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12666
13299
|
return explicit ? fields.map((f, i) => String(explicit[i] != null ? explicit[i] : f)) : fields;
|
|
12667
13300
|
}
|
|
12668
13301
|
|
|
12669
|
-
_buildItemPlotLayers(zoom) {
|
|
13302
|
+
_buildItemPlotLayers(zoom, liveZoom = zoom) {
|
|
12670
13303
|
const fields = this._itemPlotFields();
|
|
12671
13304
|
const items = [];
|
|
12672
13305
|
let dMin = Infinity, dMax = -Infinity;
|
|
13306
|
+
// facet / runtime WHERE / time frame narrow the drawn charts, as in
|
|
13307
|
+
// every other renderer; the value range stays the full data's (as
|
|
13308
|
+
// the bubbles' _maxSizeValue), so a filter does not rescale them
|
|
13309
|
+
const active = this._activeFeatures ? new Set(this._activeFeatures) : null;
|
|
12673
13310
|
for (const f of this.features) {
|
|
12674
13311
|
const pos = itemAnchor(f.geometry);
|
|
12675
13312
|
if (!pos) continue;
|
|
12676
13313
|
const values = fields.map(k => parseFloat(f.properties[k]));
|
|
12677
13314
|
for (const v of values) if (!isNaN(v)) { dMin = Math.min(dMin, v); dMax = Math.max(dMax, v); }
|
|
12678
|
-
items.push({ geometry: { type: 'Point', coordinates: pos }, properties: { values, raw: f.properties } });
|
|
13315
|
+
if (!active || active.has(f)) items.push({ geometry: { type: 'Point', coordinates: pos }, properties: { values, raw: f.properties } });
|
|
12679
13316
|
}
|
|
12680
13317
|
if (!items.length) return [];
|
|
12681
13318
|
const sMin = styleNum(this.style.minvalue), sMaxRaw = this.style.maxvalue;
|
|
@@ -12694,7 +13331,18 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12694
13331
|
const S = 2 * NORMAL_RADIUS_PX * (styleNum(this.style.scale) || 1) * objectZoomFactor(zoom, this.mapOptions);
|
|
12695
13332
|
const color = parseCssColor(Array.isArray(this.style.colorscheme) ? this.style.colorscheme[0] : this.style.colorscheme) || [0, 102, 204];
|
|
12696
13333
|
const fillOpacity = styleNum(this.style.fillopacity);
|
|
12697
|
-
|
|
13334
|
+
// BOX / TITLE: flat's chart-group box around every chart; the chart's
|
|
13335
|
+
// drawn extent is its plot geometry's bounding box (chart units × S px,
|
|
13336
|
+
// y up in the geometry, down on screen)
|
|
13337
|
+
const boxRows = this._boxRows(items, { unit: S / (2 * NORMAL_RADIUS_PX), liveZoom, extentOf: d => {
|
|
13338
|
+
const b = itemPlotGeometry(d.properties.values, rangeFor(d.properties.values), this.style, this.flags, sizeMax).bbox;
|
|
13339
|
+
return [b.x0 * S, -b.y1 * S, b.x1 * S, -b.y0 * S];
|
|
13340
|
+
} });
|
|
13341
|
+
const boxLayers = boxRows ? this._boxTitleLayers(boxRows, {
|
|
13342
|
+
unit: S / (2 * NORMAL_RADIUS_PX), liveZoom, positionOf: b => b.d.geometry.coordinates, offsetOf: () => [0, 0],
|
|
13343
|
+
ids: { circle: `ix-itemplot-box-${this.name}`, icon: `ix-itemplot-box-${this.name}`, polygons: `ix-itemplot-box-polygons-${this.name}`, titles: `ix-itemplot-titles-${this.name}` }
|
|
13344
|
+
}) : { box: [], titles: [] };
|
|
13345
|
+
return [...boxLayers.box, new IconLayer({
|
|
12698
13346
|
id: `ix-itemplot-${this.name}-g${this._iconGeneration}`,
|
|
12699
13347
|
data: items, pickable: true,
|
|
12700
13348
|
getPosition: d => d.geometry.coordinates,
|
|
@@ -12704,7 +13352,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12704
13352
|
getSize: d => this._buildItemPlotIcon(d.properties.values, rangeFor(d.properties.values), sizeMax, color, fillOpacity).heightUnits * S,
|
|
12705
13353
|
sizeUnits: 'pixels',
|
|
12706
13354
|
updateTriggers: { getSize: [S] }
|
|
12707
|
-
})];
|
|
13355
|
+
}), ...boxLayers.titles];
|
|
12708
13356
|
}
|
|
12709
13357
|
|
|
12710
13358
|
// canvas for one per-item PLOT chart (itemPlotGeometry), RASTER px per
|
|
@@ -12842,14 +13490,22 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12842
13490
|
datasetMax = datasetMax || 1;
|
|
12843
13491
|
if (isNaN(normalSizeValue)) normalSizeValue = datasetMax;
|
|
12844
13492
|
|
|
12845
|
-
|
|
13493
|
+
// BOX / TITLE: flat's chart-group box around every chart; the chart is
|
|
13494
|
+
// the iconSize square on the cell's center (flat's chart size is 30 units)
|
|
13495
|
+
const boxRows = this._boxRows(data, { unit: iconSize / 30, liveZoom: zoom,
|
|
13496
|
+
extentOf: () => [-iconSize / 2, -iconSize / 2, iconSize / 2, iconSize / 2] });
|
|
13497
|
+
const boxLayers = boxRows ? this._boxTitleLayers(boxRows, {
|
|
13498
|
+
unit: iconSize / 30, liveZoom: zoom, positionOf: b => b.d.geometry.coordinates, offsetOf: () => [0, 0],
|
|
13499
|
+
ids: { circle: `ix-plot-box-${this.name}`, icon: `ix-plot-box-${this.name}`, polygons: `ix-plot-box-polygons-${this.name}`, titles: `ix-plot-titles-${this.name}` }
|
|
13500
|
+
}) : { box: [], titles: [] };
|
|
13501
|
+
return [...boxLayers.box, new IconLayer({
|
|
12846
13502
|
id: `ix-plot-${this.name}-g${this._iconGeneration}`,
|
|
12847
13503
|
data, pickable: true,
|
|
12848
13504
|
getPosition: d => d.geometry.coordinates,
|
|
12849
13505
|
getIcon: d => this._buildPlotIcon(d.properties.values, categories, labels, normalSizeValue, datasetMax, datasetMin),
|
|
12850
13506
|
getSize: iconSize,
|
|
12851
13507
|
sizeUnits: 'pixels'
|
|
12852
|
-
})];
|
|
13508
|
+
}), ...boxLayers.titles];
|
|
12853
13509
|
}
|
|
12854
13510
|
|
|
12855
13511
|
// Canvas rendering for one cell's mini-chart — BOX background, AREA
|
|
@@ -12967,9 +13623,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12967
13623
|
canvas.width = W; canvas.height = H;
|
|
12968
13624
|
const ctx = canvas.getContext('2d');
|
|
12969
13625
|
|
|
12970
|
-
// BOX
|
|
13626
|
+
// BOX (a plot without the BOX flag keeps its own canvas box; with it the
|
|
13627
|
+
// shared box layer draws it — see _buildPlotLayers)
|
|
12971
13628
|
const boxOpacity = styleNum(this.style.boxopacity);
|
|
12972
|
-
if (!isNaN(boxOpacity) && boxOpacity > 0) {
|
|
13629
|
+
if (!flatFlag(this.flags, 'BOX') && !isNaN(boxOpacity) && boxOpacity > 0) {
|
|
12973
13630
|
ctx.fillStyle = `rgba(255,255,255,${boxOpacity})`;
|
|
12974
13631
|
ctx.fillRect(0, 0, W, H);
|
|
12975
13632
|
}
|
|
@@ -13215,13 +13872,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13215
13872
|
// async image decode, the whole drawing is synchronous
|
|
13216
13873
|
const items = [];
|
|
13217
13874
|
const defsSeen = new Set();
|
|
13875
|
+
// this drawing's defs, swapped in for the previous drawing's below —
|
|
13876
|
+
// appended to the shared pane <defs> on every draw they grew without
|
|
13877
|
+
// bound (and getElementById then found a stale earlier copy)
|
|
13878
|
+
const paneDefs = document.createElementNS(SVG_NS, 'g');
|
|
13218
13879
|
let html = '';
|
|
13219
13880
|
for (const d of combined) {
|
|
13220
13881
|
const value = valueOf(d);
|
|
13221
13882
|
// the item's class (range classes set it for groups too — a negative
|
|
13222
13883
|
// group total would make dominant() pick the wrong class)
|
|
13223
13884
|
const cat = d.properties.cat != null ? d.properties.cat : (d.properties.counts ? dominant(d.properties.counts) : 0);
|
|
13224
|
-
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());
|
|
13225
13886
|
const size = radiusPx / pxPerUnit;
|
|
13226
13887
|
const chart = this._userChartSvg(draw, name, theme, flag, d, value, cat, size, sizeValueOf(d));
|
|
13227
13888
|
if (!chart) continue;
|
|
@@ -13231,11 +13892,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13231
13892
|
for (const id of chart.ids) {
|
|
13232
13893
|
if (defsSeen.has(id)) continue;
|
|
13233
13894
|
defsSeen.add(id);
|
|
13234
|
-
|
|
13235
|
-
|
|
13895
|
+
// drawn now (fresh), else a cached chart's: the previous drawing's copy
|
|
13896
|
+
const def = this._userChartGroup.querySelector('#' + CSS.escape(id)) || this._userChartGroup.ownerDocument.getElementById(id);
|
|
13897
|
+
if (def) paneDefs.appendChild(def.cloneNode(true));
|
|
13236
13898
|
}
|
|
13237
13899
|
items.push({ d, coords: d.geometry.coordinates, ox: chart.ox, oy: chart.oy, s: pxPerUnit,
|
|
13238
|
-
r: Math.max(chart.vw, chart.vh) * pxPerUnit / 2
|
|
13900
|
+
r: Math.max(chart.vw, chart.vh) * pxPerUnit / 2,
|
|
13901
|
+
// the chart's measured box (flat's getBox of the chart group) in px
|
|
13902
|
+
// around the map position: the pane draws a chart point at
|
|
13903
|
+
// p + s · (point − origin)
|
|
13904
|
+
ext: [(chart.bb.x - chart.ox) * pxPerUnit, (chart.bb.y - chart.oy) * pxPerUnit,
|
|
13905
|
+
(chart.bb.x + chart.bb.width - chart.ox) * pxPerUnit, (chart.bb.y + chart.bb.height - chart.oy) * pxPerUnit] });
|
|
13239
13906
|
}
|
|
13240
13907
|
if (glDebug()) console.info(`[ixmaps-gl debug] ${themeIdOf(this)}: zoom ${mapLibreToFlatZoom(liveZoom).toFixed(2)}, ${combined.length} charts, ${items.length} drawn${this._viewMoving ? ' (moving)' : ''}`);
|
|
13241
13908
|
if (!items.length) {
|
|
@@ -13244,6 +13911,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13244
13911
|
this._chartItems = null;
|
|
13245
13912
|
return (this._userChartLastLayers = []);
|
|
13246
13913
|
}
|
|
13914
|
+
chartHost.defs.appendChild(paneDefs);
|
|
13915
|
+
if (this._chartPaneDefs) this._chartPaneDefs.remove();
|
|
13916
|
+
this._chartPaneDefs = paneDefs;
|
|
13247
13917
|
if (this._chartPaneGroup) this._chartPaneGroup.remove();
|
|
13248
13918
|
this._chartPaneGroup = document.createElementNS(SVG_NS, 'g');
|
|
13249
13919
|
chartHost.pane.appendChild(this._chartPaneGroup);
|
|
@@ -13252,11 +13922,24 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13252
13922
|
for (let i = 0; i < items.length; i++) items[i].el = els[i];
|
|
13253
13923
|
this._chartItems = items;
|
|
13254
13924
|
chartHost.runtimes.add(this);
|
|
13925
|
+
this._chartHost = chartHost; // removeRuntimes takes it out again
|
|
13255
13926
|
chartHost.update();
|
|
13256
13927
|
// picking: a transparent circle per chart — the VISIBLE chart is the
|
|
13257
13928
|
// live SVG in the pane; this deck layer only serves hover, click and
|
|
13258
13929
|
// the tooltips (pickedFeature unwraps the {d, icon} entry shape)
|
|
13259
|
-
|
|
13930
|
+
// BOX / TITLE: flat's chart-group box around the chart as the page's
|
|
13931
|
+
// function drew it (its measured box). The chart itself is a live SVG in
|
|
13932
|
+
// the pane above the deck canvas, so the box lies under it and the title
|
|
13933
|
+
// (outside the box) beside it
|
|
13934
|
+
const extOf = new Map(items.map(it => [it.d, it.ext]));
|
|
13935
|
+
const boxRows = this._boxRows(items.map(it => it.d), {
|
|
13936
|
+
unit: objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1), liveZoom, extentOf: d => extOf.get(d) });
|
|
13937
|
+
const boxLayers = boxRows ? this._boxTitleLayers(boxRows, {
|
|
13938
|
+
unit: objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1), liveZoom,
|
|
13939
|
+
positionOf: b => b.d.geometry.coordinates, offsetOf: () => [0, 0],
|
|
13940
|
+
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}` }
|
|
13941
|
+
}) : { box: [], titles: [] };
|
|
13942
|
+
this._userChartLastLayers = [...boxLayers.box, new ScatterplotLayer({
|
|
13260
13943
|
id: `ix-bubbles-${this.name}`,
|
|
13261
13944
|
data: items.map(it => ({ d: it.d, icon: true, r: it.r })),
|
|
13262
13945
|
pickable: true,
|
|
@@ -13265,7 +13948,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13265
13948
|
radiusUnits: 'pixels', radiusMinPixels: 1,
|
|
13266
13949
|
stroked: false, filled: true, getFillColor: [0, 0, 0, 0],
|
|
13267
13950
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
13268
|
-
})];
|
|
13951
|
+
}), ...boxLayers.titles];
|
|
13269
13952
|
return this._userChartLastLayers;
|
|
13270
13953
|
}
|
|
13271
13954
|
// the chart's SVG, cached by its draw parameters — a failing draw
|
|
@@ -13320,7 +14003,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13320
14003
|
const vx = bb.x - pad, vy = bb.y - pad, vw = bb.width + 2 * pad, vh = bb.height + 2 * pad;
|
|
13321
14004
|
// the chart's origin (0,0 — or the point flat moves it to, ret) sits at the map position
|
|
13322
14005
|
const ox = (ret && ret.x) || 0, oy = (ret && ret.y) || 0;
|
|
13323
|
-
chart = { vx, vy, vw, vh, ox, oy, markup, ids: [...ids],
|
|
14006
|
+
chart = { vx, vy, vw, vh, ox, oy, markup, ids: [...ids], bb: { x: bb.x, y: bb.y, width: bb.width, height: bb.height },
|
|
13324
14007
|
svg: `<svg xmlns="${SVG_NS}" width="__W__" height="__H__" viewBox="${vx} ${vy} ${vw} ${vh}"><defs>${defs}</defs>${markup}</svg>` };
|
|
13325
14008
|
}
|
|
13326
14009
|
} catch (e) {
|
|
@@ -13333,6 +14016,187 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13333
14016
|
return chart;
|
|
13334
14017
|
}
|
|
13335
14018
|
|
|
14019
|
+
// flat's BOX block belongs to the box: the box owns its TITLE — no BOX, no
|
|
14020
|
+
// title — so everything about both is decided here and in _boxRows, for
|
|
14021
|
+
// every chart type (single symbol, SEQUENCE, PIE, BAR): the gates (BOX /
|
|
14022
|
+
// TITLE flags, boxupper / boxlower / titleupper scale), the margin, the
|
|
14023
|
+
// title font (DTEXT follows the chart), BOTTOMTITLE, align, CIRCULARBOX.
|
|
14024
|
+
// null when there is no box at this zoom.
|
|
14025
|
+
_boxSpec(liveZoom) {
|
|
14026
|
+
const st = this.style;
|
|
14027
|
+
if (!(flatFlag(this.flags, 'BOX') && !boxHiddenByScale(st, liveZoom))) return null;
|
|
14028
|
+
const titleUpper = scaleDenom(st.titleupper);
|
|
14029
|
+
const titleScale = styleNum(st.textscale) || styleNum(st.valuescale) || 1;
|
|
14030
|
+
return {
|
|
14031
|
+
circular: flatFlag(this.flags, 'CIRCULAR'),
|
|
14032
|
+
margin: styleNum(st.boxmargin) || 2,
|
|
14033
|
+
bottomTitle: flatFlag(this.flags, 'BOTTOMTITLE'),
|
|
14034
|
+
alignRight: /right/.test(String(st.align || '')),
|
|
14035
|
+
titleShown: flatFlag(this.flags, 'TITLE') && !!this.binding.title
|
|
14036
|
+
&& !(titleUpper && scaleDenominatorAt(liveZoom) > titleUpper),
|
|
14037
|
+
// DTEXT: the title font follows the chart, max(0.9u, min(W/5, 14u /
|
|
14038
|
+
// scale)) (18815) — measured on the fire areas page's SIF chart
|
|
14039
|
+
// (W 362 units → font 72.35)
|
|
14040
|
+
titleFontOf: (unit, w) => (flatFlag(this.flags, 'DTEXT')
|
|
14041
|
+
? Math.max(0.9 * unit, Math.min(w / 5, 14 * unit / (styleNum(st.scale) || 1)))
|
|
14042
|
+
: 5 * unit) * titleScale
|
|
14043
|
+
};
|
|
14044
|
+
}
|
|
14045
|
+
|
|
14046
|
+
// the layout row of every item's box and title (chartBoxLayout + the spec
|
|
14047
|
+
// above); null without a box (items: the array, or a function building it
|
|
14048
|
+
// only when there is a box). o: { unit (px per flat unit), liveZoom,
|
|
14049
|
+
// extentOf (item -> its drawn [x0, y0, x1, y1], px around the anchor),
|
|
14050
|
+
// radiusOf / centerOf (the circular box's base, default the extent's) }.
|
|
14051
|
+
// A row: { d: item, geometry, properties, w (the chart's width — flat's
|
|
14052
|
+
// size measure W, the stroke and corner basis), boxR / cx / cy (circle),
|
|
14053
|
+
// rect (rectangle), title }
|
|
14054
|
+
_boxRows(items, o) {
|
|
14055
|
+
const spec = this._boxSpec(o.liveZoom);
|
|
14056
|
+
if (!spec) return null;
|
|
14057
|
+
// flat appends style.titleunits (its parse adds the leading space,
|
|
14058
|
+
// maptheme.js 1470; szTitle += at 18827)
|
|
14059
|
+
const units = this.style.titleunits ? ' ' + this.style.titleunits : '';
|
|
14060
|
+
return (typeof items === 'function' ? items() : items).map(d => {
|
|
14061
|
+
const itemTitle = spec.titleShown ? this._itemTitle(d.properties) : null;
|
|
14062
|
+
const L = chartBoxLayout(o.extentOf(d), { unit: o.unit, margin: spec.margin, circular: spec.circular,
|
|
14063
|
+
radius: o.radiusOf ? o.radiusOf(d) : undefined, center: o.centerOf ? o.centerOf(d) : undefined,
|
|
14064
|
+
titleText: itemTitle ? itemTitle + units : null, titleFont: w => spec.titleFontOf(o.unit, w),
|
|
14065
|
+
bottomTitle: spec.bottomTitle, alignRight: spec.alignRight });
|
|
14066
|
+
// geometry/properties of the chart's feature on the row: the circular
|
|
14067
|
+
// box is pickable, and the tooltip (buildTooltipHtml, the click-pin)
|
|
14068
|
+
// works on the feature's own properties
|
|
14069
|
+
return { d, geometry: d.geometry, properties: d.properties, w: L.w, boxR: L.boxR, cx: L.cx, cy: L.cy,
|
|
14070
|
+
rect: L.rect, title: L.title };
|
|
14071
|
+
});
|
|
14072
|
+
}
|
|
14073
|
+
|
|
14074
|
+
// THE box-and-title layers of every chart type: rows are _boxRows results.
|
|
14075
|
+
// o: { unit, liveZoom, positionOf (row -> [lng, lat]), offsetOf (row ->
|
|
14076
|
+
// the chart's [dx, dy] px move: align / offsetx / offsety), box (false:
|
|
14077
|
+
// the title layer only — a BAR draws its box as a shader shape), ids:
|
|
14078
|
+
// { circle, icon, polygons, titles } } — the layer ids stay each chart
|
|
14079
|
+
// type's own. Returns { box: [layers], titles: [layers] }: the caller
|
|
14080
|
+
// pushes them where its draw order wants them.
|
|
14081
|
+
_boxTitleLayers(rows, o) {
|
|
14082
|
+
const st = this.style;
|
|
14083
|
+
const { unit, liveZoom, positionOf, offsetOf, ids } = o;
|
|
14084
|
+
const circular = flatFlag(this.flags, 'CIRCULAR');
|
|
14085
|
+
const box = [], titles = [];
|
|
14086
|
+
// flat: fill-opacity (boxopacity || 1) on the raw style value — the usual
|
|
14087
|
+
// string "0" is transparent; typed here, a given 0 is too
|
|
14088
|
+
const boxOpacity = isNaN(styleNum(st.boxopacity)) ? 1 : styleNum(st.boxopacity);
|
|
14089
|
+
if (o.box === false) {
|
|
14090
|
+
// the title layer only
|
|
14091
|
+
} else if (circular) {
|
|
14092
|
+
const boxNone = isNoneColor(st.boxcolor), borderNone = isNoneColor(st.bordercolor);
|
|
14093
|
+
const boxRgb = hexOrNamedToRgb(boxNone ? '#eeeeee' : st.boxcolor || '#eeeeee');
|
|
14094
|
+
const boxAlpha = boxNone ? 0 : Math.round(255 * boxOpacity);
|
|
14095
|
+
const borderRgb = borderNone ? null : hexOrNamedToRgb(st.bordercolor || '#dddddd');
|
|
14096
|
+
// Always a ScatterplotLayer: its shader circle is smooth at any
|
|
14097
|
+
// size, where an icon — a 48px canvas texture deck.gl scales up —
|
|
14098
|
+
// pixelated with large boxes. ScatterplotLayer has no pixel offset,
|
|
14099
|
+
// so the chart's align/offsetx/offsety move and flat's circular box
|
|
14100
|
+
// position (the chart's bounding-box center, 18793) are baked into
|
|
14101
|
+
// map coordinates (pixelOffsetLngLat). Pickable: flat's box is part
|
|
14102
|
+
// of the chart group, so hovering it fires the chart's tooltip — the
|
|
14103
|
+
// symbols sit above it, so they keep their own
|
|
14104
|
+
box.push(new ScatterplotLayer({
|
|
14105
|
+
id: ids.circle,
|
|
14106
|
+
data: rows, pickable: true,
|
|
14107
|
+
getPosition: b => { const f = offsetOf(b); return pixelOffsetLngLat(positionOf(b), [f[0] + b.cx, f[1] + b.cy], liveZoom); },
|
|
14108
|
+
getRadius: b => b.boxR,
|
|
14109
|
+
radiusUnits: 'pixels',
|
|
14110
|
+
stroked: !borderNone,
|
|
14111
|
+
getFillColor: [...boxRgb.slice(0, 3), boxAlpha],
|
|
14112
|
+
getLineColor: [...(borderRgb || boxRgb).slice(0, 3), 255],
|
|
14113
|
+
getLineWidth: unit / 20,
|
|
14114
|
+
lineWidthUnits: 'pixels',
|
|
14115
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14116
|
+
}));
|
|
14117
|
+
} else {
|
|
14118
|
+
// flat's rectangular box (the chart and its title plus the margin)
|
|
14119
|
+
const borderStyle = String(st.borderstyle || '');
|
|
14120
|
+
const boxLook = {
|
|
14121
|
+
fill: st.boxcolor || 'white',
|
|
14122
|
+
fillOpacity: boxOpacity,
|
|
14123
|
+
stroke: borderStyle === 'none' || isNoneColor(styleLineColor(st.bordercolor)) ? null : styleLineColor(st.bordercolor) || '#bbbbbb',
|
|
14124
|
+
strokeOpacity: /^(solid|dotted|dashed)$/.test(borderStyle) ? 1 : 0.3,
|
|
14125
|
+
};
|
|
14126
|
+
const borderWidth = styleNum(st.borderwidth) || 1;
|
|
14127
|
+
const borderRadius = styleNum(st.borderradius);
|
|
14128
|
+
const strokeOf = b => borderWidth * 0.2 * b.w / 30;
|
|
14129
|
+
// a box larger than BOX_ICON_MAX_PX (a MULTIQUAD grid of thousands
|
|
14130
|
+
// of items) would need a huge canvas: it is drawn as a polygon in
|
|
14131
|
+
// map coordinates instead (square corners)
|
|
14132
|
+
const bigBox = b => Math.max(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1]) > BOX_ICON_MAX_PX;
|
|
14133
|
+
const iconBoxes = rows.filter(b => !bigBox(b)), polygonBoxes = rows.filter(bigBox);
|
|
14134
|
+
iconBoxes.forEach(b => {
|
|
14135
|
+
b.icon = this._buildBoxIcon(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1],
|
|
14136
|
+
isNaN(borderRadius) ? 0 : borderRadius * Math.min(unit, b.w / 30),
|
|
14137
|
+
strokeOf(b), boxLook);
|
|
14138
|
+
});
|
|
14139
|
+
if (polygonBoxes.length) {
|
|
14140
|
+
const fillRgb = hexOrNamedToRgb(boxLook.fill);
|
|
14141
|
+
const strokeRgb = boxLook.stroke ? hexOrNamedToRgb(boxLook.stroke) : null;
|
|
14142
|
+
box.push(new GeoJsonLayer({
|
|
14143
|
+
id: ids.polygons,
|
|
14144
|
+
data: polygonBoxes.map(b => {
|
|
14145
|
+
const f = offsetOf(b), c = positionOf(b), r = b.rect;
|
|
14146
|
+
const at = (x, y) => pixelOffsetLngLat(c, [x + f[0], y + f[1]], liveZoom);
|
|
14147
|
+
return { type: 'Feature', properties: { stroke: strokeOf(b) },
|
|
14148
|
+
geometry: { type: 'Polygon', coordinates: [[at(r[0], r[1]), at(r[2], r[1]), at(r[2], r[3]), at(r[0], r[3]), at(r[0], r[1])]] } };
|
|
14149
|
+
}),
|
|
14150
|
+
pickable: false,
|
|
14151
|
+
filled: boxLook.fillOpacity > 0,
|
|
14152
|
+
stroked: !!strokeRgb,
|
|
14153
|
+
getFillColor: [...fillRgb.slice(0, 3), Math.round(255 * boxLook.fillOpacity)],
|
|
14154
|
+
getLineColor: strokeRgb ? [...strokeRgb.slice(0, 3), Math.round(255 * boxLook.strokeOpacity)] : [0, 0, 0, 0],
|
|
14155
|
+
getLineWidth: f => f.properties.stroke,
|
|
14156
|
+
lineWidthUnits: 'pixels',
|
|
14157
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14158
|
+
}));
|
|
14159
|
+
}
|
|
14160
|
+
if (iconBoxes.length) box.push(new IconLayer({
|
|
14161
|
+
// the generation suffix (like every icon-atlas layer id, see
|
|
14162
|
+
// ICON_ATLAS_RESET_AFTER): every zoom creates a new set of box
|
|
14163
|
+
// rasters, and deck.gl's atlas never evicts
|
|
14164
|
+
id: `${ids.icon}-g${this._iconGeneration}`,
|
|
14165
|
+
data: iconBoxes, pickable: false,
|
|
14166
|
+
getPosition: positionOf,
|
|
14167
|
+
getIcon: b => b.icon,
|
|
14168
|
+
// the icon is drawn at 2× with a stroke padding around the box
|
|
14169
|
+
getSize: b => b.icon.height / 2,
|
|
14170
|
+
getPixelOffset: b => { const f = offsetOf(b); return [(b.rect[0] + b.rect[2]) / 2 + f[0], (b.rect[1] + b.rect[3]) / 2 + f[1]]; },
|
|
14171
|
+
sizeUnits: 'pixels',
|
|
14172
|
+
billboard: true,
|
|
14173
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14174
|
+
}));
|
|
14175
|
+
}
|
|
14176
|
+
const titleData = rows.filter(b => b.title);
|
|
14177
|
+
if (titleData.length) titles.push(new TextLayer({
|
|
14178
|
+
id: ids.titles,
|
|
14179
|
+
data: titleData, pickable: false,
|
|
14180
|
+
getPosition: positionOf,
|
|
14181
|
+
getText: b => b.title.text,
|
|
14182
|
+
getSize: b => b.title.f,
|
|
14183
|
+
sizeUnits: 'pixels',
|
|
14184
|
+
fontFamily: 'arial',
|
|
14185
|
+
characterSet: 'auto',
|
|
14186
|
+
lineHeight: 1,
|
|
14187
|
+
getColor: [...hexOrNamedToRgb(st.textcolor || '#888888').slice(0, 3), 255],
|
|
14188
|
+
fontSettings: { sdf: true },
|
|
14189
|
+
outlineWidth: 1 / 7,
|
|
14190
|
+
outlineColor: [255, 255, 255, 128],
|
|
14191
|
+
getTextAnchor: b => b.title.anchor,
|
|
14192
|
+
// top of the text ≈ first baseline − 0.8 font
|
|
14193
|
+
getAlignmentBaseline: 'top',
|
|
14194
|
+
getPixelOffset: b => { const f = offsetOf(b); return [b.title.x + f[0], b.title.top + f[1]]; },
|
|
14195
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14196
|
+
}));
|
|
14197
|
+
return { box, titles };
|
|
14198
|
+
}
|
|
14199
|
+
|
|
13336
14200
|
// BOX (see the BOX block in _buildChartLayers): a rectangle icon
|
|
13337
14201
|
// w×h px, rounded corners rx, stroke sw px; drawn at twice the size
|
|
13338
14202
|
_buildBoxIcon(w, h, rx, sw, look) {
|
|
@@ -13503,7 +14367,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13503
14367
|
const groups = doRelocate ? this._groupCoLocated(clusterFeatures, effectiveZoom) : clusterFeatures.map(f => {
|
|
13504
14368
|
return {
|
|
13505
14369
|
geometry: f.geometry,
|
|
13506
|
-
|
|
14370
|
+
// counts is the summed bound VALUE and recordCounts the record
|
|
14371
|
+
// count, as groupCoLocated builds them (RELOCATE) — RELOCATE
|
|
14372
|
+
// moves the item, it never changes a value
|
|
14373
|
+
properties: { counts: this._oneHot(f.properties.cat, f.properties.value),
|
|
14374
|
+
recordCounts: this._oneHot(f.properties.cat, f.properties.point_count), total: f.properties.value,
|
|
13507
14375
|
...(f.properties.classValue !== undefined ? { classTotal: f.properties.classValue } : {}),
|
|
13508
14376
|
...(f.properties.titleRaw ? { titleRaw: f.properties.titleRaw } : {}) }
|
|
13509
14377
|
};
|
|
@@ -13654,7 +14522,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13654
14522
|
// GRIDSIZE: every chart the size of its grid cell, whatever its value
|
|
13655
14523
|
// (_gridChartRadiusPx); otherwise valueRadius
|
|
13656
14524
|
const gridR = this._gridChartRadiusPx(zoom, liveZoom);
|
|
13657
|
-
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()));
|
|
13658
14526
|
// AGGREGATE sums without a normalsizevalue: flat's default is the
|
|
13659
14527
|
// largest aggregated value (defaultNormalSizeValue leaves it unset, the
|
|
13660
14528
|
// sizes depend on the aggregation)
|
|
@@ -13922,15 +14790,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13922
14790
|
// DTEXT: the title font follows the chart, max(0.9u, min(W/5,
|
|
13923
14791
|
// 14u / scale)) (18815) — measured on the fire areas page's SIF chart
|
|
13924
14792
|
// (W 362 units → font 72.35).
|
|
13925
|
-
const boxShown = flatFlag(this.flags, 'BOX') && !boxHiddenByScale(this.style, liveZoom);
|
|
13926
|
-
const circular = flatFlag(this.flags, 'CIRCULAR');
|
|
13927
14793
|
const unitPx = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1);
|
|
13928
|
-
const boxMarginU = styleNum(this.style.boxmargin) || 2;
|
|
13929
|
-
const titleShown = boxShown && flatFlag(this.flags, 'TITLE') && !!this.binding.title;
|
|
13930
|
-
const titleScale = styleNum(this.style.textscale) || styleNum(this.style.valuescale) || 1;
|
|
13931
|
-
const titleFontOf = W => (flatFlag(this.flags, 'DTEXT')
|
|
13932
|
-
? Math.max(0.9 * unitPx, Math.min(W / 5, 14 * unitPx / (styleNum(this.style.scale) || 1)))
|
|
13933
|
-
: 5 * unitPx) * titleScale;
|
|
13934
14794
|
// the chart's value text and label (see VALUES and LABEL below), shared
|
|
13935
14795
|
// with the box: flat's box encloses whatever the chart group draws
|
|
13936
14796
|
const valuesShown = this.flags.has('VALUES') && !valuesHiddenByScale(this.style, zoom);
|
|
@@ -13938,141 +14798,28 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13938
14798
|
const labelShown = !!this.style.labelfield && !flatFlag(this.flags, 'NORMSIZE') && !flatFlag(this.flags, 'ZOOM')
|
|
13939
14799
|
&& !labelHiddenByScale(this.style, zoom);
|
|
13940
14800
|
const labelOf = labelShown ? this._chartLabelFn(unitPx) : null;
|
|
13941
|
-
//
|
|
13942
|
-
//
|
|
13943
|
-
const
|
|
13944
|
-
|
|
13945
|
-
|
|
13946
|
-
|
|
13947
|
-
|
|
13948
|
-
|
|
13949
|
-
|
|
13950
|
-
|
|
13951
|
-
|
|
13952
|
-
|
|
13953
|
-
|
|
13954
|
-
const w = chart[2] - chart[0];
|
|
13955
|
-
const margin = Math.min(2 * boxMarginU * unitPx, boxMarginU * w / 30);
|
|
13956
|
-
const boxR = circular ? r * 1.1 + margin : r;
|
|
13957
|
-
const titleFontPx = titleFontOf(w);
|
|
13958
|
-
let title = null;
|
|
13959
|
-
if (titleShown) {
|
|
13960
|
-
// flat appends style.titleunits (its parse adds the leading
|
|
13961
|
-
// space, maptheme.js 1470; szTitle += at 18827)
|
|
13962
|
-
const units = this.style.titleunits ? ' ' + this.style.titleunits : '';
|
|
13963
|
-
const text = this._itemTitle(d.properties) ? this._itemTitle(d.properties) + units : null;
|
|
13964
|
-
if (text) {
|
|
13965
|
-
const f = titleFontPx;
|
|
13966
|
-
const lines = wrapTitleLines(text, f, circular ? boxR * 2 : w).split('\n');
|
|
13967
|
-
const width = Math.max(...lines.map(l => titleTextWidth(l, f)));
|
|
13968
|
-
// first baseline; lines go down one font each
|
|
13969
|
-
const base = circular
|
|
13970
|
-
? (bottomTitle ? boxR + f : -boxR - 0.7 * f - (lines.length - 1) * f)
|
|
13971
|
-
: (bottomTitle ? chart[3] + f : chart[1] - 0.7 * f - (lines.length - 1) * f);
|
|
13972
|
-
const x = circular ? unitPx / 2 : (alignRight ? chart[2] : chart[0]);
|
|
13973
|
-
const anchor = circular ? 'middle' : (alignRight ? 'end' : 'start');
|
|
13974
|
-
const x0 = anchor === 'middle' ? x - width / 2 : anchor === 'end' ? x - width : x;
|
|
13975
|
-
// arial: ascent 0.905, descent 0.212 of the font size
|
|
13976
|
-
title = { text: lines.join('\n'), x, anchor, top: base - 0.8 * f, f,
|
|
13977
|
-
extent: [x0, base - 0.905 * f, x0 + width, base + (lines.length - 1) * f + 0.212 * f] };
|
|
13978
|
-
}
|
|
13979
|
-
}
|
|
13980
|
-
let rect = null;
|
|
13981
|
-
if (!circular) {
|
|
13982
|
-
const e = chart.slice();
|
|
13983
|
-
// with a title flat trims 0.1 font off the top of chart + title
|
|
13984
|
-
if (title) { e[0] = Math.min(e[0], title.extent[0]); e[1] = Math.min(e[1], title.extent[1]) + 0.1 * titleFontPx; e[2] = Math.max(e[2], title.extent[2]); e[3] = Math.max(e[3], title.extent[3]); }
|
|
13985
|
-
rect = [e[0] - margin, e[1] - margin, e[2] + margin, e[3] + margin];
|
|
14801
|
+
// with MULTIQUAD only the first item of a position gets its box/title;
|
|
14802
|
+
// the circle sits on the anchor (0, 0), its base radius the symbol's
|
|
14803
|
+
const boxLayout = this._boxRows(() => combined.filter(d => !multi || multi.index.get(d) === 0), {
|
|
14804
|
+
unit: unitPx, liveZoom,
|
|
14805
|
+
radiusOf: d => vRadius(sizeValueOf(d)), centerOf: () => [0, 0],
|
|
14806
|
+
extentOf: d => {
|
|
14807
|
+
const r = vRadius(sizeValueOf(d));
|
|
14808
|
+
const ext = multi ? multi.extent.get(d) : [0, 0, 0, 0];
|
|
14809
|
+
const chart = [ext[0] - r, ext[1] - r, ext[2] + r, ext[3] + r];
|
|
14810
|
+
const grow = e => { if (!e) return; chart[0] = Math.min(chart[0], e[0]); chart[1] = Math.min(chart[1], e[1]); chart[2] = Math.max(chart[2], e[2]); chart[3] = Math.max(chart[3], e[3]); };
|
|
14811
|
+
if (valueTextOf && !d.properties.counts) { const v = valueTextOf(d, r); if (v) grow(v.extent); }
|
|
14812
|
+
if (labelOf) { const l = labelOf(d, r); if (l) grow(l.extent); }
|
|
14813
|
+
return chart;
|
|
13986
14814
|
}
|
|
13987
|
-
|
|
14815
|
+
});
|
|
14816
|
+
// (the title layers are pushed after the symbols, below)
|
|
14817
|
+
const boxLayers = boxLayout ? this._boxTitleLayers(boxLayout, {
|
|
14818
|
+
unit: unitPx, liveZoom,
|
|
14819
|
+
positionOf: b => b.d.geometry.coordinates, offsetOf: b => alignOf(b.d),
|
|
14820
|
+
ids: { circle: `ix-box-${this.name}`, icon: `ix-box-${this.name}`, polygons: `ix-box-polygons-${this.name}`, titles: `ix-titles-${this.name}` }
|
|
13988
14821
|
}) : null;
|
|
13989
|
-
if (
|
|
13990
|
-
const boxNone = isNoneColor(this.style.boxcolor), borderNone = isNoneColor(this.style.bordercolor);
|
|
13991
|
-
const boxRgb = hexOrNamedToRgb(boxNone ? '#eeeeee' : this.style.boxcolor || '#eeeeee');
|
|
13992
|
-
const boxAlpha = boxNone ? 0 : Math.round(255 * boxOpacity);
|
|
13993
|
-
const borderRgb = borderNone ? null : hexOrNamedToRgb(this.style.bordercolor || '#dddddd');
|
|
13994
|
-
// ScatterplotLayer has no pixel offset: a moved chart (align,
|
|
13995
|
-
// offsetx/offsety) gets its circle as an icon
|
|
13996
|
-
if (alignOf.active) layers.push(new IconLayer({
|
|
13997
|
-
id: `ix-box-${this.name}-g${this._iconGeneration}`,
|
|
13998
|
-
data: boxLayout, pickable: false,
|
|
13999
|
-
getPosition: b => b.d.geometry.coordinates,
|
|
14000
|
-
getIcon: b => this._buildSingleIcon(boxRgb, boxAlpha / 255, 'circle', borderRgb,
|
|
14001
|
-
borderNone ? 0 : Math.round(unitPx / 20 * BUBBLE_ICON_SIZE / Math.max(1, 2 * b.boxR) * 4) / 4),
|
|
14002
|
-
getSize: b => b.boxR * 2,
|
|
14003
|
-
getPixelOffset: b => alignOf(b.d),
|
|
14004
|
-
sizeUnits: 'pixels',
|
|
14005
|
-
billboard: true,
|
|
14006
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14007
|
-
}));
|
|
14008
|
-
else layers.push(new ScatterplotLayer({
|
|
14009
|
-
id: `ix-box-${this.name}`,
|
|
14010
|
-
data: boxLayout, pickable: false,
|
|
14011
|
-
getPosition: b => b.d.geometry.coordinates,
|
|
14012
|
-
getRadius: b => b.boxR,
|
|
14013
|
-
radiusUnits: 'pixels',
|
|
14014
|
-
stroked: !borderNone,
|
|
14015
|
-
getFillColor: [...boxRgb.slice(0, 3), boxAlpha],
|
|
14016
|
-
getLineColor: [...(borderRgb || boxRgb).slice(0, 3), 255],
|
|
14017
|
-
getLineWidth: unitPx / 20,
|
|
14018
|
-
lineWidthUnits: 'pixels',
|
|
14019
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14020
|
-
}));
|
|
14021
|
-
} else if (boxShown) {
|
|
14022
|
-
const borderStyle = String(this.style.borderstyle || '');
|
|
14023
|
-
const boxLook = {
|
|
14024
|
-
fill: this.style.boxcolor || 'white',
|
|
14025
|
-
fillOpacity: boxOpacity,
|
|
14026
|
-
stroke: borderStyle === 'none' || isNoneColor(styleLineColor(this.style.bordercolor)) ? null : styleLineColor(this.style.bordercolor) || '#bbbbbb',
|
|
14027
|
-
strokeOpacity: /^(solid|dotted|dashed)$/.test(borderStyle) ? 1 : 0.3,
|
|
14028
|
-
};
|
|
14029
|
-
const borderWidth = styleNum(this.style.borderwidth) || 1;
|
|
14030
|
-
const borderRadius = styleNum(this.style.borderradius);
|
|
14031
|
-
const strokeOf = b => borderWidth * 0.2 * b.w / 30;
|
|
14032
|
-
// a box larger than BOX_ICON_MAX_PX (a MULTIQUAD grid of thousands
|
|
14033
|
-
// of items) would need a huge canvas: it is drawn as a polygon in
|
|
14034
|
-
// map coordinates instead (square corners)
|
|
14035
|
-
const bigBox = b => Math.max(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1]) > BOX_ICON_MAX_PX;
|
|
14036
|
-
const iconBoxes = boxLayout.filter(b => !bigBox(b)), polygonBoxes = boxLayout.filter(bigBox);
|
|
14037
|
-
iconBoxes.forEach(b => {
|
|
14038
|
-
b.icon = this._buildBoxIcon(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1],
|
|
14039
|
-
isNaN(borderRadius) ? 0 : borderRadius * Math.min(unitPx, b.w / 30),
|
|
14040
|
-
strokeOf(b), boxLook);
|
|
14041
|
-
});
|
|
14042
|
-
if (polygonBoxes.length) {
|
|
14043
|
-
const fillRgb = hexOrNamedToRgb(boxLook.fill);
|
|
14044
|
-
const strokeRgb = boxLook.stroke ? hexOrNamedToRgb(boxLook.stroke) : null;
|
|
14045
|
-
layers.push(new GeoJsonLayer({
|
|
14046
|
-
id: `ix-box-polygons-${this.name}`,
|
|
14047
|
-
data: polygonBoxes.map(b => {
|
|
14048
|
-
const o = alignOf(b.d), c = b.d.geometry.coordinates, r = b.rect;
|
|
14049
|
-
const at = (x, y) => pixelOffsetLngLat(c, [x + o[0], y + o[1]], liveZoom);
|
|
14050
|
-
return { type: 'Feature', properties: { stroke: strokeOf(b) },
|
|
14051
|
-
geometry: { type: 'Polygon', coordinates: [[at(r[0], r[1]), at(r[2], r[1]), at(r[2], r[3]), at(r[0], r[3]), at(r[0], r[1])]] } };
|
|
14052
|
-
}),
|
|
14053
|
-
pickable: false,
|
|
14054
|
-
filled: boxLook.fillOpacity > 0,
|
|
14055
|
-
stroked: !!strokeRgb,
|
|
14056
|
-
getFillColor: [...fillRgb.slice(0, 3), Math.round(255 * boxLook.fillOpacity)],
|
|
14057
|
-
getLineColor: strokeRgb ? [...strokeRgb.slice(0, 3), Math.round(255 * boxLook.strokeOpacity)] : [0, 0, 0, 0],
|
|
14058
|
-
getLineWidth: f => f.properties.stroke,
|
|
14059
|
-
lineWidthUnits: 'pixels',
|
|
14060
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14061
|
-
}));
|
|
14062
|
-
}
|
|
14063
|
-
if (iconBoxes.length) layers.push(new IconLayer({
|
|
14064
|
-
id: `ix-box-${this.name}-g${this._iconGeneration}`,
|
|
14065
|
-
data: iconBoxes, pickable: false,
|
|
14066
|
-
getPosition: b => b.d.geometry.coordinates,
|
|
14067
|
-
getIcon: b => b.icon,
|
|
14068
|
-
// the icon is drawn at 2× with a stroke padding around the box
|
|
14069
|
-
getSize: b => b.icon.height / 2,
|
|
14070
|
-
getPixelOffset: b => { const o = alignOf(b.d); return [(b.rect[0] + b.rect[2]) / 2 + o[0], (b.rect[1] + b.rect[3]) / 2 + o[1]]; },
|
|
14071
|
-
sizeUnits: 'pixels',
|
|
14072
|
-
billboard: true,
|
|
14073
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14074
|
-
}));
|
|
14075
|
-
}
|
|
14822
|
+
if (boxLayers) layers.push(...boxLayers.box);
|
|
14076
14823
|
|
|
14077
14824
|
// flat's per-part SYMBOL branch (everything but CATEGORICAL without
|
|
14078
14825
|
// AGGREGATE/GROUP) draws a square or roundrect of side 2r, the other
|
|
@@ -14095,7 +14842,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14095
14842
|
if (!flatLook) return this._buildSingleIcon(rgb, fillOpacity, shapeOf(d.properties), singleBorderColorRgb, singleBorderWidthPx);
|
|
14096
14843
|
const look = flatLook(d);
|
|
14097
14844
|
return this._buildSingleIcon(rgb, look.fillOpacity, shapeOf(d.properties),
|
|
14098
|
-
|
|
14845
|
+
bubbleStrokeRgb(look, rgb), look.width, look.strokeOpacity);
|
|
14099
14846
|
};
|
|
14100
14847
|
// flat's symbol outline (maptheme.js 21595-21610 and 21950-21997,
|
|
14101
14848
|
// drawn 21625-21634 / 21995-22035): every SYMBOL shape is stroked,
|
|
@@ -14130,7 +14877,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14130
14877
|
// routing goes by the final id (layerRuntimeById). A symbol's outline
|
|
14131
14878
|
// is centered on its radius (flat); BUBBLE's border keeps the
|
|
14132
14879
|
// icon's look, inside the radius
|
|
14133
|
-
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))) {
|
|
14134
14881
|
const radiusPx = d => vRadius(sizeValueOf(d));
|
|
14135
14882
|
// a CATEGORICAL BUBBLE: flat's look (flatBubbleLook) in screen px,
|
|
14136
14883
|
// its stroke centered on the radius as flat's SVG circle
|
|
@@ -14159,6 +14906,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14159
14906
|
getPosition: d => d.geometry.coordinates,
|
|
14160
14907
|
getIcon: d => {
|
|
14161
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);
|
|
14162
14911
|
if (flatLook) return singleIcon(d);
|
|
14163
14912
|
const shape = shapeOf(d.properties), rgb = outlineRgb(d);
|
|
14164
14913
|
if (!symbolOutline || !rgb) return this._buildSingleIcon((this.categoryColorsRgb || [])[d.properties.cat], fillOpacity, shape, rgb, rgb ? singleBorderWidthPx : 0);
|
|
@@ -14180,29 +14929,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14180
14929
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14181
14930
|
}));
|
|
14182
14931
|
|
|
14183
|
-
|
|
14184
|
-
if (titleData.length) {
|
|
14185
|
-
layers.push(new TextLayer({
|
|
14186
|
-
id: `ix-titles-${this.name}`,
|
|
14187
|
-
data: titleData, pickable: false,
|
|
14188
|
-
getPosition: b => b.d.geometry.coordinates,
|
|
14189
|
-
getText: b => b.title.text,
|
|
14190
|
-
getSize: b => b.title.f,
|
|
14191
|
-
sizeUnits: 'pixels',
|
|
14192
|
-
fontFamily: 'arial',
|
|
14193
|
-
characterSet: 'auto',
|
|
14194
|
-
lineHeight: 1,
|
|
14195
|
-
getColor: [...hexOrNamedToRgb(this.style.textcolor || '#888888').slice(0, 3), 255],
|
|
14196
|
-
fontSettings: { sdf: true },
|
|
14197
|
-
outlineWidth: 1 / 7,
|
|
14198
|
-
outlineColor: [255, 255, 255, 128],
|
|
14199
|
-
getTextAnchor: b => b.title.anchor,
|
|
14200
|
-
// top of the text ≈ first baseline − 0.8 font
|
|
14201
|
-
getAlignmentBaseline: 'top',
|
|
14202
|
-
getPixelOffset: b => { const o = alignOf(b.d); return [b.title.x + o[0], b.title.top + o[1]]; },
|
|
14203
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14204
|
-
}));
|
|
14205
|
-
}
|
|
14932
|
+
if (boxLayers) layers.push(...boxLayers.titles);
|
|
14206
14933
|
|
|
14207
14934
|
// VALUES: bold value label centered on each bubble (see
|
|
14208
14935
|
// flatValueText/valuesFontSizePx above). Labels are their own
|
|
@@ -14470,63 +15197,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14470
15197
|
// it itself (its ImageBitmap loader falls back to <img>, which
|
|
14471
15198
|
// rasterizes SVG) — NOT tinted: these files carry their own colors,
|
|
14472
15199
|
// and flat doesn't recolor an <image> either
|
|
14473
|
-
const urlSymbolOf = i =>
|
|
14474
|
-
|
|
14475
|
-
return typeof s === 'string' && /\.(svg|png|jpe?g)([?#]|$)/i.test(s.trim()) ? s.trim() : null;
|
|
14476
|
-
};
|
|
14477
|
-
if (!this._urlIconCache) this._urlIconCache = new Map();
|
|
14478
|
-
const urlIcon = u => {
|
|
14479
|
-
if (!this._urlIconCache.has(u)) {
|
|
14480
|
-
// the placeholder is a transparent 1x1 PNG, NOT the raw symbol
|
|
14481
|
-
// URL: deck.gl would immediately try to load that itself, and an
|
|
14482
|
-
// SVG without natural dimensions (a viewBox only, the bus
|
|
14483
|
-
// pictogram of the City-moving page) makes Chrome's
|
|
14484
|
-
// createImageBitmap throw — a console error and a part that
|
|
14485
|
-
// never renders. The real raster replaces it below
|
|
14486
|
-
const entry = { url: TRANSPARENT_1PX_PNG, width: 256, height: 256, anchorX: 128, anchorY: 128, mask: false, id: 'seq-sym-pending:' + u };
|
|
14487
|
-
this._urlIconCache.set(u, entry);
|
|
14488
|
-
// fetch the symbol ONCE and rasterize it into the descriptor's
|
|
14489
|
-
// own 256px cell here: deck then always sees exactly the declared
|
|
14490
|
-
// dimensions, and dimension-less SVGs work like any other. The
|
|
14491
|
-
// redraw lets the next getIcon see the patched descriptor
|
|
14492
|
-
fetch(u).then(r => r.ok ? r.text() : null).then(text => new Promise((resolve, reject) => {
|
|
14493
|
-
if (typeof text !== 'string' || !/<svg[\s>]/i.test(text)) { reject(new Error('not svg')); return; }
|
|
14494
|
-
if (!/\swidth\s*=/i.test(text) || !/\sheight\s*=/i.test(text)) {
|
|
14495
|
-
const vb = text.match(/viewBox\s*=\s*["']([^"']*)["']/i);
|
|
14496
|
-
const p = vb && vb[1].trim().split(/[\s,]+/).map(Number);
|
|
14497
|
-
if (p && p.length === 4 && isFinite(p[2]) && p[2] > 0 && isFinite(p[3]) && p[3] > 0) {
|
|
14498
|
-
text = text.replace(/<svg/i, '<svg width="' + p[2] + '" height="' + p[3] + '"');
|
|
14499
|
-
}
|
|
14500
|
-
}
|
|
14501
|
-
const img = new Image();
|
|
14502
|
-
img.onload = () => resolve(img);
|
|
14503
|
-
img.onerror = () => reject(new Error('svg decode failed'));
|
|
14504
|
-
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(text);
|
|
14505
|
-
})).then(img => {
|
|
14506
|
-
const size = entry.width;
|
|
14507
|
-
const canvas = document.createElement('canvas');
|
|
14508
|
-
canvas.width = size; canvas.height = size;
|
|
14509
|
-
const ctx = canvas.getContext('2d');
|
|
14510
|
-
const s = Math.min(size / (img.naturalWidth || size), size / (img.naturalHeight || size));
|
|
14511
|
-
const dw = (img.naturalWidth || size) * s, dh = (img.naturalHeight || size) * s;
|
|
14512
|
-
ctx.drawImage(img, (size - dw) / 2, (size - dh) / 2, dw, dh);
|
|
14513
|
-
entry.url = canvas.toDataURL('image/png');
|
|
14514
|
-
entry.id = 'seq-sym-raster:' + u;
|
|
14515
|
-
if (this._triggerRedraw) this._triggerRedraw();
|
|
14516
|
-
}).catch(() => {
|
|
14517
|
-
// not an SVG (a PNG/JPEG symbol): deck's own URL loading takes
|
|
14518
|
-
// it from here — and a pre-sized SVG that failed to rasterize
|
|
14519
|
-
// gets that same second chance
|
|
14520
|
-
entry.url = u;
|
|
14521
|
-
entry.id = 'seq-sym-natural:' + u;
|
|
14522
|
-
if (this._triggerRedraw) this._triggerRedraw();
|
|
14523
|
-
});
|
|
14524
|
-
}
|
|
14525
|
-
return this._urlIconCache.get(u);
|
|
14526
|
-
};
|
|
15200
|
+
const urlSymbolOf = i => this._urlSymbolOf(symbols && (symbols[i] || symbols[0]));
|
|
15201
|
+
const urlIcon = u => this._urlSymbolIcon(u);
|
|
14527
15202
|
const shapeOf = i => { const sh = symbols && (symbols[i] || symbols[0]); return normalizeSymbolShape(sh); };
|
|
14528
15203
|
const colorOf = i => this.classRgb(i);
|
|
14529
|
-
const
|
|
15204
|
+
const allParts = [], allGlows = [];
|
|
14530
15205
|
for (const d of charts) {
|
|
14531
15206
|
// parts: the record's own field values (a per-item chart, or the
|
|
14532
15207
|
// single-record cell of an aggregated one); series: a multi-record
|
|
@@ -14538,16 +15213,34 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14538
15213
|
const sum = counts.reduce((a, v) => a + (v || 0), 0);
|
|
14539
15214
|
counts = counts.map(v => sum > 0 ? v / (sum / 100) : 0);
|
|
14540
15215
|
}
|
|
15216
|
+
// flat's aggregated CATEGORICAL item holds a value per category only up
|
|
15217
|
+
// to the highest category it has (measured on flat: the chart's star and
|
|
15218
|
+
// glow follow the categories present as 0..k, 985 of 988 charts of the
|
|
15219
|
+
// concessioni page): trailing zeros are no parts, so with SORT|UP the
|
|
15220
|
+
// smallest part — order index 0, the one that sets the star radius and
|
|
15221
|
+
// carries the glow — is a zero only when a category BELOW the highest
|
|
15222
|
+
// one is missing
|
|
15223
|
+
if (!d.properties.parts && !d.properties.series) {
|
|
15224
|
+
let n = counts.length;
|
|
15225
|
+
while (n > 0 && !counts[n - 1]) n--;
|
|
15226
|
+
if (n < counts.length) counts = counts.slice(0, n);
|
|
15227
|
+
}
|
|
14541
15228
|
const sizeFactor = itemSizeFactor(d);
|
|
14542
15229
|
const partRadius = multi
|
|
14543
15230
|
? v => Math.max(VALUE_RADIUS_MIN, normalR * Math.pow(Math.abs(v) / seqMax, 1 / sizePow) * sizeFactor)
|
|
14544
|
-
: v => valueRadius(v, liveZoom, st, this.mapOptions, this.flags, seqMax);
|
|
15231
|
+
: v => valueRadius(v, liveZoom, st, this.mapOptions, this.flags, seqMax, this._sizeBound());
|
|
14545
15232
|
let layout = rings ? ringsLayout(counts, partRadius)
|
|
14546
15233
|
: sequenceLayout(counts, this.flags, partRadius, { maxRadius: normalR, maxValue: seqMax, nParts: nCats, rangeScale: styleNum(st.rangescale) || 0 });
|
|
14547
15234
|
if (!layout.length) continue;
|
|
14548
15235
|
// GLOW belongs to the first part (sorted) or to every part, and
|
|
14549
15236
|
// hides with it when its class is not marked
|
|
14550
|
-
|
|
15237
|
+
// every chart glows (deliberately NOT flat's rule, which gives the glow to
|
|
15238
|
+
// the part at sorted order index 0 — a zero, skipped, whenever a category
|
|
15239
|
+
// below the highest is missing — so only about a quarter of flat's charts
|
|
15240
|
+
// glow): a sorted chart's glow belongs to its biggest part, an unsorted
|
|
15241
|
+
// one glows with every part as in flat
|
|
15242
|
+
const biggest = layout.reduce((m, p) => (p.v > m.v ? p : m), layout[0]);
|
|
15243
|
+
const glowParts = new Set(sorted ? [biggest.i] : layout.map(p => p.i));
|
|
14551
15244
|
// the chart's drawn extent (flat: map.Dom.getBox of its shape
|
|
14552
15245
|
// group) — the parts' star cluster — for BOX / CIRCULARBOX sizing
|
|
14553
15246
|
let ex0 = Infinity, ey0 = Infinity, ex1 = -Infinity, ey1 = -Infinity;
|
|
@@ -14564,12 +15257,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14564
15257
|
d._seqLast = layout[layout.length - 1].r;
|
|
14565
15258
|
const items = layout.map(p => {
|
|
14566
15259
|
const item = { geometry: d.geometry, properties: d.properties, chart: d, part: p };
|
|
14567
|
-
if (glowParts.has(p.i))
|
|
15260
|
+
if (glowParts.has(p.i)) allGlows.push(item);
|
|
14568
15261
|
return item;
|
|
14569
15262
|
});
|
|
14570
15263
|
// STAR: the first part (its center) is put on top at the end (23145)
|
|
14571
15264
|
if (star && items.length > 1) items.push(items.shift());
|
|
14572
|
-
|
|
15265
|
+
allParts.push(...items);
|
|
14573
15266
|
}
|
|
14574
15267
|
const alignOf = this._chartAlignFn(liveZoom, d => (d.chart || d)._seqLast || 0);
|
|
14575
15268
|
const offsetOf = item => { const o = alignOf(item.chart); return [o[0] + item.part.x, o[1] + item.part.y]; };
|
|
@@ -14587,287 +15280,227 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14587
15280
|
// max(0.9u, min(w/5, 14u/scale)) · textscale, wrapped to the
|
|
14588
15281
|
// circle's width — gated by titleupper (shown while the map
|
|
14589
15282
|
// scale is that bound or coarser, 18810) and boxupper/boxlower
|
|
14590
|
-
|
|
15283
|
+
// (flat centers the circular box and its title on the chart's
|
|
15284
|
+
// bounding-box CENTER — maptheme.js 18793-18798 — not on its anchor: a
|
|
15285
|
+
// star cluster's satellites spread asymmetrically around it; the
|
|
15286
|
+
// circle's title baselines are measured on the resized box, 18798-18830)
|
|
14591
15287
|
const circular = flatFlag(this.flags, 'CIRCULAR');
|
|
14592
|
-
const
|
|
14593
|
-
|
|
14594
|
-
|
|
14595
|
-
|
|
14596
|
-
|
|
14597
|
-
|
|
14598
|
-
|
|
14599
|
-
|
|
14600
|
-
|
|
14601
|
-
|
|
14602
|
-
|
|
14603
|
-
|
|
14604
|
-
|
|
14605
|
-
|
|
14606
|
-
|
|
14607
|
-
|
|
14608
|
-
|
|
14609
|
-
|
|
14610
|
-
|
|
14611
|
-
|
|
14612
|
-
|
|
14613
|
-
|
|
14614
|
-
|
|
14615
|
-
|
|
14616
|
-
|
|
14617
|
-
|
|
14618
|
-
|
|
14619
|
-
|
|
14620
|
-
|
|
14621
|
-
|
|
14622
|
-
|
|
14623
|
-
|
|
14624
|
-
|
|
14625
|
-
|
|
14626
|
-
|
|
14627
|
-
|
|
14628
|
-
|
|
14629
|
-
|
|
14630
|
-
|
|
14631
|
-
|
|
14632
|
-
|
|
14633
|
-
|
|
14634
|
-
|
|
14635
|
-
|
|
14636
|
-
|
|
14637
|
-
|
|
14638
|
-
|
|
14639
|
-
|
|
14640
|
-
|
|
14641
|
-
|
|
14642
|
-
|
|
14643
|
-
|
|
14644
|
-
|
|
14645
|
-
|
|
14646
|
-
|
|
14647
|
-
|
|
14648
|
-
|
|
14649
|
-
|
|
14650
|
-
const
|
|
14651
|
-
|
|
14652
|
-
|
|
14653
|
-
|
|
14654
|
-
|
|
14655
|
-
id: `ix-seqbox-${this.name}-g${this._iconGeneration}`,
|
|
14656
|
-
data: boxLayout, pickable: false,
|
|
14657
|
-
getPosition: b => b.d.geometry.coordinates,
|
|
14658
|
-
getIcon: b => this._buildSingleIcon(boxRgb, b.boxOpacity, 'circle', borderRgb,
|
|
14659
|
-
borderNone ? 0 : Math.round(unit / 20 * BUBBLE_ICON_SIZE / Math.max(1, 2 * b.boxR) * 4) / 4),
|
|
14660
|
-
getSize: b => b.boxR * 2,
|
|
14661
|
-
getPixelOffset: b => { const o = alignOf(b.d); return [o[0] + b.cx, o[1] + b.cy]; },
|
|
14662
|
-
sizeUnits: 'pixels', billboard: true,
|
|
14663
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14664
|
-
}));
|
|
14665
|
-
else layers.push(new ScatterplotLayer({
|
|
14666
|
-
id: `ix-seqbox-${this.name}`,
|
|
14667
|
-
data: boxLayout, pickable: false,
|
|
14668
|
-
getPosition: b => {
|
|
14669
|
-
// the circle at the chart's bbox center: with no align
|
|
14670
|
-
// offset it moves in map coordinates
|
|
14671
|
-
const o = pixelOffsetLngLat(b.d.geometry.coordinates, [b.cx, b.cy], liveZoom);
|
|
14672
|
-
return o;
|
|
14673
|
-
},
|
|
14674
|
-
getRadius: b => b.boxR,
|
|
14675
|
-
radiusUnits: 'pixels',
|
|
14676
|
-
stroked: !borderNone,
|
|
14677
|
-
getFillColor: b => [...boxRgb.slice(0, 3), Math.round(255 * b.boxOpacity)],
|
|
14678
|
-
getLineColor: [...(borderRgb || boxRgb).slice(0, 3), 255],
|
|
14679
|
-
getLineWidth: unit / 20,
|
|
14680
|
-
lineWidthUnits: 'pixels',
|
|
14681
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14682
|
-
}));
|
|
14683
|
-
} else {
|
|
14684
|
-
// flat's rectangular box (the chart and its title plus the
|
|
14685
|
-
// margin), the single-symbol path's own _buildBoxIcon drawing
|
|
14686
|
-
const borderStyle = String(st.borderstyle || '');
|
|
14687
|
-
const boxLook = {
|
|
14688
|
-
fill: st.boxcolor || 'white',
|
|
14689
|
-
fillOpacity: isNaN(styleNum(st.boxopacity)) ? 1 : styleNum(st.boxopacity),
|
|
14690
|
-
stroke: borderStyle === 'none' || isNoneColor(styleLineColor(st.bordercolor)) ? null : styleLineColor(st.bordercolor) || '#bbbbbb',
|
|
14691
|
-
strokeOpacity: /^(solid|dotted|dashed)$/.test(borderStyle) ? 1 : 0.3
|
|
14692
|
-
};
|
|
14693
|
-
const borderWidth = styleNum(st.borderwidth) || 1;
|
|
14694
|
-
const borderRadius = styleNum(st.borderradius);
|
|
14695
|
-
const strokeOf = b => borderWidth * 0.2 * b.w / 30;
|
|
14696
|
-
boxLayout.forEach(b => {
|
|
14697
|
-
b.icon = this._buildBoxIcon(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1],
|
|
14698
|
-
isNaN(borderRadius) ? 0 : borderRadius * Math.min(unit, b.w / 30),
|
|
14699
|
-
strokeOf(b), boxLook);
|
|
15288
|
+
const boxLayoutAll = this._boxRows(() => charts.filter(d => d._seqExtent), { unit, liveZoom, extentOf: d => d._seqExtent });
|
|
15289
|
+
// per-chart overlap bands (see the band computation in the
|
|
15290
|
+
// box block): a Map chart → band index, filled only when
|
|
15291
|
+
// boxes are drawn; empty (single band) otherwise
|
|
15292
|
+
const bandOfChart = new Map();
|
|
15293
|
+
let nBands = 1;
|
|
15294
|
+
if (boxLayoutAll) {
|
|
15295
|
+
// flat draws every chart as ONE group — its box, its symbols,
|
|
15296
|
+
// its title, its values together — and the groups are sorted by
|
|
15297
|
+
// size, so a bigger chart covers a smaller one ENTIRELY
|
|
15298
|
+
// (chartMap's ascending sort, the biggest drawn last). deck.gl
|
|
15299
|
+
// stacks whole layers, so with every box in one layer below
|
|
15300
|
+
// every symbol, a bigger chart's box covered a smaller chart's
|
|
15301
|
+
// box while that chart's symbols floated on top of it (the
|
|
15302
|
+
// City-moving page's overlapping circles). Charts are grouped
|
|
15303
|
+
// into overlap "bands" instead — an interval coloring assigned
|
|
15304
|
+
// biggest-chart-first: within a band no two charts overlap
|
|
15305
|
+
// (their box + title extents, in map pixel space, disjoint),
|
|
15306
|
+
// and a chart is always in a band above every smaller chart it
|
|
15307
|
+
// overlaps. Each band then draws its charts' complete group
|
|
15308
|
+
// (box, title, glow, symbols, values), the smallest band first
|
|
15309
|
+
// — deck.gl draws the first layer at the bottom — so between
|
|
15310
|
+
// two overlapping charts the bigger one is wholly on top, as
|
|
15311
|
+
// flat draws it. Overlap-free themes keep one band and today's
|
|
15312
|
+
// exact single layers
|
|
15313
|
+
const world = 512 * Math.pow(2, liveZoom);
|
|
15314
|
+
const anchorPx = d => {
|
|
15315
|
+
const o = alignOf(d), c = d.geometry.coordinates;
|
|
15316
|
+
const lat = c[1] * Math.PI / 180;
|
|
15317
|
+
return [(c[0] + 180) / 360 * world + o[0],
|
|
15318
|
+
(0.5 - Math.log(Math.tan(Math.PI / 4 + lat / 2)) / (2 * Math.PI)) * world + o[1]];
|
|
15319
|
+
};
|
|
15320
|
+
const sizeOfBand = b => circular ? b.boxR
|
|
15321
|
+
: b.rect ? Math.max(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1]) / 2 : 0;
|
|
15322
|
+
const bandRects = [];
|
|
15323
|
+
for (const b of boxLayoutAll.slice().sort((a, c) => sizeOfBand(c) - sizeOfBand(a))) {
|
|
15324
|
+
// the chart's whole drawn extent: parts cluster, box and
|
|
15325
|
+
// title, as px around the (align-moved) anchor
|
|
15326
|
+
const e = b.d._seqExtent || [0, 0, 0, 0];
|
|
15327
|
+
let x0 = e[0], y0 = e[1], x1 = e[2], y1 = e[3];
|
|
15328
|
+
if (circular) { x0 = Math.min(x0, b.cx - b.boxR); y0 = Math.min(y0, b.cy - b.boxR); x1 = Math.max(x1, b.cx + b.boxR); y1 = Math.max(y1, b.cy + b.boxR); }
|
|
15329
|
+
else if (b.rect) { x0 = Math.min(x0, b.rect[0]); y0 = Math.min(y0, b.rect[1]); x1 = Math.max(x1, b.rect[2]); y1 = Math.max(y1, b.rect[3]); }
|
|
15330
|
+
if (b.title) { x0 = Math.min(x0, b.title.extent[0]); y0 = Math.min(y0, b.title.extent[1]); x1 = Math.max(x1, b.title.extent[2]); y1 = Math.max(y1, b.title.extent[3]); }
|
|
15331
|
+
const p = anchorPx(b.d);
|
|
15332
|
+
const r = [p[0] + x0, p[1] + y0, p[0] + x1, p[1] + y1];
|
|
15333
|
+
let k = 0;
|
|
15334
|
+
while (bandRects[k] && bandRects[k].some(q => r[0] < q[2] && q[0] < r[2] && r[1] < q[3] && q[1] < r[3])) k++;
|
|
15335
|
+
(bandRects[k] = bandRects[k] || []).push(r);
|
|
15336
|
+
bandOfChart.set(b.d, k);
|
|
15337
|
+
}
|
|
15338
|
+
nBands = bandRects.length;
|
|
15339
|
+
}
|
|
15340
|
+
// one band's whole chart group — box, title, glow, symbols and
|
|
15341
|
+
// values of ITS charts only (the closure's boxLayout, glows and
|
|
15342
|
+
// parts are the band's slices) — pushed as one deck.gl layer
|
|
15343
|
+
// sequence, in today's order. bs: the band's id suffix
|
|
15344
|
+
const drawBand = (boxLayout, glows, parts, bs) => {
|
|
15345
|
+
if (boxLayout) {
|
|
15346
|
+
const L = this._boxTitleLayers(boxLayout, {
|
|
15347
|
+
unit, liveZoom,
|
|
15348
|
+
positionOf: b => b.d.geometry.coordinates, offsetOf: b => alignOf(b.d),
|
|
15349
|
+
ids: { circle: `ix-seqbox-${this.name}${bs}`, icon: `ix-seqbox-${this.name}${bs}`,
|
|
15350
|
+
polygons: `ix-seqbox-polygons-${this.name}${bs}`, titles: `ix-seqtitles-${this.name}${bs}` }
|
|
14700
15351
|
});
|
|
14701
|
-
|
|
14702
|
-
|
|
14703
|
-
|
|
14704
|
-
|
|
14705
|
-
|
|
14706
|
-
|
|
14707
|
-
|
|
15352
|
+
layers.push(...L.box, ...L.titles);
|
|
15353
|
+
}
|
|
15354
|
+
if (glows.length && this.flags.has('GLOW') && !glowHiddenByScale(st, liveZoom)) {
|
|
15355
|
+
layers.push(new IconLayer({
|
|
15356
|
+
id: `ix-glow-${this.name}${bs}-g${this._iconGeneration}`,
|
|
15357
|
+
data: glows, pickable: false,
|
|
15358
|
+
getPosition: d => d.geometry.coordinates,
|
|
15359
|
+
getIcon: d => this._getGlowIcon(colorOf(d.part.i)),
|
|
15360
|
+
getSize: d => d.part.r * 11,
|
|
15361
|
+
getPixelOffset: offsetOf,
|
|
15362
|
+
getColor: d => [255, 255, 255, alphaOf(d)],
|
|
14708
15363
|
sizeUnits: 'pixels', billboard: true,
|
|
14709
15364
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14710
15365
|
}));
|
|
14711
|
-
}
|
|
14712
|
-
|
|
14713
|
-
|
|
14714
|
-
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
|
|
14718
|
-
|
|
14719
|
-
|
|
14720
|
-
sizeUnits: 'pixels',
|
|
14721
|
-
characterSet: 'auto',
|
|
14722
|
-
lineHeight: 1,
|
|
14723
|
-
getColor: [...hexOrNamedToRgb(st.textcolor || '#888888').slice(0, 3), 255],
|
|
14724
|
-
fontSettings: { sdf: true },
|
|
14725
|
-
outlineWidth: 1 / 7,
|
|
14726
|
-
outlineColor: [255, 255, 255, 128],
|
|
14727
|
-
getTextAnchor: b => b.title.anchor,
|
|
14728
|
-
getAlignmentBaseline: 'top',
|
|
14729
|
-
getPixelOffset: b => { const o = alignOf(b.d); return [b.title.x + o[0], b.title.top + o[1]]; },
|
|
15366
|
+
} else if (glows.length && this.flags.has('AURA') && !glowHiddenByScale(st, liveZoom)) {
|
|
15367
|
+
layers.push(new IconLayer({
|
|
15368
|
+
id: `ix-aura-${this.name}${bs}-g${this._iconGeneration}`,
|
|
15369
|
+
data: glows, pickable: false,
|
|
15370
|
+
getPosition: d => d.geometry.coordinates,
|
|
15371
|
+
getIcon: d => this._buildSingleIcon(borderRgb || colorOf(d.part.i), 0.3, 'circle', null, 0),
|
|
15372
|
+
getSize: d => d.part.r * 2 * 1.3,
|
|
15373
|
+
getPixelOffset: offsetOf,
|
|
15374
|
+
getColor: d => [255, 255, 255, alphaOf(d)],
|
|
15375
|
+
sizeUnits: 'pixels', billboard: true,
|
|
14730
15376
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14731
15377
|
}));
|
|
14732
15378
|
}
|
|
14733
|
-
}
|
|
14734
|
-
if (glows.length && this.flags.has('GLOW') && !glowHiddenByScale(st, liveZoom)) {
|
|
14735
15379
|
layers.push(new IconLayer({
|
|
14736
|
-
id: `ix-
|
|
14737
|
-
data:
|
|
15380
|
+
id: `ix-bubbles-${this.name}${bs}-g${this._iconGeneration}`,
|
|
15381
|
+
data: parts, pickable: true,
|
|
14738
15382
|
getPosition: d => d.geometry.coordinates,
|
|
14739
|
-
|
|
14740
|
-
|
|
14741
|
-
|
|
14742
|
-
|
|
14743
|
-
|
|
14744
|
-
|
|
14745
|
-
|
|
14746
|
-
|
|
14747
|
-
|
|
14748
|
-
|
|
14749
|
-
|
|
14750
|
-
getPosition: d => d.geometry.coordinates,
|
|
14751
|
-
getIcon: d => this._buildSingleIcon(borderRgb || colorOf(d.part.i), 0.3, 'circle', null, 0),
|
|
14752
|
-
getSize: d => d.part.r * 2 * 1.3,
|
|
15383
|
+
// the stroke, in the icon raster's pixels (the icon is drawn at 2r)
|
|
15384
|
+
getIcon: d => {
|
|
15385
|
+
// an external symbol file: the URL icon as flat's <image> (see
|
|
15386
|
+
// urlSymbolOf above), scaled to the part's diameter like flat's
|
|
15387
|
+
// fu.scale(nRadius / nMaxRadius) on a nChartSize image
|
|
15388
|
+
const u = urlSymbolOf(d.part.i);
|
|
15389
|
+
if (u) return urlIcon(u);
|
|
15390
|
+
return this._buildSingleIcon(colorOf(d.part.i), noFill.has(d.part.i) ? 0 : fillOpacity, shapeOf(d.part.i), borderRgb,
|
|
15391
|
+
borderRgb ? Math.round(lineWidth * unit * Math.min(2 * d.part.r / normalR, 0.2) * BUBBLE_ICON_SIZE / Math.max(1, 2 * d.part.r) * 4) / 4 : 0);
|
|
15392
|
+
},
|
|
15393
|
+
getSize: d => d.part.r * 2,
|
|
14753
15394
|
getPixelOffset: offsetOf,
|
|
14754
15395
|
getColor: d => [255, 255, 255, alphaOf(d)],
|
|
14755
15396
|
sizeUnits: 'pixels', billboard: true,
|
|
14756
15397
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14757
15398
|
}));
|
|
14758
|
-
|
|
14759
|
-
|
|
14760
|
-
|
|
14761
|
-
|
|
14762
|
-
|
|
14763
|
-
//
|
|
14764
|
-
|
|
14765
|
-
|
|
14766
|
-
|
|
14767
|
-
|
|
14768
|
-
|
|
14769
|
-
|
|
14770
|
-
|
|
14771
|
-
|
|
14772
|
-
|
|
14773
|
-
|
|
14774
|
-
|
|
14775
|
-
|
|
14776
|
-
|
|
14777
|
-
|
|
14778
|
-
|
|
14779
|
-
|
|
14780
|
-
|
|
14781
|
-
|
|
14782
|
-
|
|
14783
|
-
|
|
14784
|
-
|
|
14785
|
-
|
|
14786
|
-
|
|
14787
|
-
|
|
14788
|
-
|
|
14789
|
-
|
|
14790
|
-
|
|
14791
|
-
|
|
14792
|
-
|
|
14793
|
-
|
|
14794
|
-
|
|
14795
|
-
|
|
14796
|
-
|
|
14797
|
-
|
|
14798
|
-
|
|
14799
|
-
|
|
14800
|
-
|
|
14801
|
-
|
|
14802
|
-
|
|
14803
|
-
|
|
14804
|
-
|
|
14805
|
-
|
|
14806
|
-
|
|
14807
|
-
|
|
14808
|
-
|
|
14809
|
-
|
|
14810
|
-
|
|
14811
|
-
|
|
14812
|
-
|
|
14813
|
-
|
|
14814
|
-
|
|
14815
|
-
|
|
14816
|
-
|
|
14817
|
-
|
|
14818
|
-
|
|
14819
|
-
|
|
14820
|
-
|
|
14821
|
-
|
|
14822
|
-
|
|
14823
|
-
|
|
14824
|
-
|
|
14825
|
-
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
|
|
14831
|
-
|
|
14832
|
-
|
|
14833
|
-
const plainLabels = labels.filter(l => !l.image);
|
|
14834
|
-
if (haloLabels.length) {
|
|
14835
|
-
// a real outline around the characters: the label's own
|
|
14836
|
-
// glyphs re-drawn in white on rings of pixel offsets — a
|
|
14837
|
-
// stroke of copies that hugs the character shapes (deck's
|
|
14838
|
-
// SDF outline is only a faint, buffer-clipped rim). The
|
|
14839
|
-
// base radius is a small fraction of the font size (the
|
|
14840
|
-
// 1px floor keeps it tiny until labels get big), and the
|
|
14841
|
-
// ring alphas fall off, so the border fades to nothing
|
|
14842
|
-
const DIRS = 16, RINGS = [[1, 210], [2, 120], [3.2, 65]];
|
|
14843
|
-
const haloCopies = [];
|
|
14844
|
-
for (const l of haloLabels) {
|
|
14845
|
-
const rb = Math.max(0.75, l.fontSize * 0.015);
|
|
14846
|
-
for (let ring = 0; ring < RINGS.length; ring++) {
|
|
14847
|
-
for (let dir = 0; dir < DIRS; dir++) {
|
|
14848
|
-
const a = (dir / DIRS) * 2 * Math.PI;
|
|
14849
|
-
haloCopies.push({ ...l,
|
|
14850
|
-
dx: Math.cos(a), dy: Math.sin(a),
|
|
14851
|
-
radius: rb * RINGS[ring][0],
|
|
14852
|
-
haloAlpha: Math.round(RINGS[ring][1] * l.alpha / 255) });
|
|
15399
|
+
// VALUES on each part (maptheme.js 22188-22260): the part's value — with
|
|
15400
|
+
// COUNT its record count, with valuefield on the categorical field the
|
|
15401
|
+
// class name — formatted with valuedecimals (else none) plus the unit.
|
|
15402
|
+
// - a SHAPE part (circle & kin): font min(1, 3.2 / L) · r · valuescale,
|
|
15403
|
+
// L the length of the theme's biggest value text (szMaxText), normal
|
|
15404
|
+
// weight, in valuecolor / textcolor, else ChartColors.textColor of
|
|
15405
|
+
// the part color
|
|
15406
|
+
// - an IMAGE-symbol part (an external .svg, the TELRAAM pictograms):
|
|
15407
|
+
// flat's own other branch (22253-22256) — font max(r · 0.8, 1) ·
|
|
15408
|
+
// valuescale, the text over createTextLabel's background box (white,
|
|
15409
|
+
// its rgba(…,0.3) fill × the 0.5 fill-opacity) and in
|
|
15410
|
+
// ChartColors.lowColor — the 0.7 derivate of the part color —
|
|
15411
|
+
// regardless of valuecolor/textcolor (flat passes cColor.lowColor
|
|
15412
|
+
// straight into createTextLabel)
|
|
15413
|
+
// CENTERVALUES: only on the first part (class 0), its own value
|
|
15414
|
+
if ((this.flags.has('VALUES') || centerValues) && !valuesHiddenByScale(st, zoom)) {
|
|
15415
|
+
const valueScale = styleNum(st.valuescale) || 1;
|
|
15416
|
+
const { noBreaks } = this._valueTextOpts();
|
|
15417
|
+
const units = st.units ? String(st.units) : '';
|
|
15418
|
+
const unit = units ? (units[0] === '.' ? '' : ' ') + units : '';
|
|
15419
|
+
const unitText = unit.length <= 5 ? unit : '';
|
|
15420
|
+
const dec = explicitValueDecimals(st) || 0;
|
|
15421
|
+
const fmt = v => flatGroupedValue(v, dec, noBreaks) + unitText;
|
|
15422
|
+
const maxLen = fmt(seqMax).length;
|
|
15423
|
+
const textOverride = st.valuecolor || st.textcolor;
|
|
15424
|
+
const counting = flatFlag(this.flags, 'COUNT');
|
|
15425
|
+
const labels = [];
|
|
15426
|
+
parts.forEach(d => {
|
|
15427
|
+
if (centerValues && d.part.i !== 0) return;
|
|
15428
|
+
const rec = this._categoryValueRecord(d.part.i);
|
|
15429
|
+
const name = rec ? String(rec[st.valuefield]) : null;
|
|
15430
|
+
const t = counting && d.properties.recordCounts ? d.properties.recordCounts[d.part.i] : d.part.v;
|
|
15431
|
+
const text = name != null ? name : fmt(t);
|
|
15432
|
+
const image = !!urlSymbolOf(d.part.i);
|
|
15433
|
+
const L = Math.max((name != null ? name.length : 0) + 1, maxLen);
|
|
15434
|
+
const fontSize = image
|
|
15435
|
+
? Math.max(d.part.r * 0.8, 1) * valueScale
|
|
15436
|
+
: Math.min(1, 3.2 / L) * d.part.r * valueScale;
|
|
15437
|
+
if (fontSize > VALUES_MIN_FONT_PX) {
|
|
15438
|
+
labels.push({ geometry: d.geometry, text, fontSize, pixelOffset: offsetOf(d), alpha: alphaOf(d),
|
|
15439
|
+
image,
|
|
15440
|
+
color: image ? flatDerivateRgb(colorOf(d.part.i), 0.7)
|
|
15441
|
+
: textOverride ? hexOrNamedToRgb(textOverride) : flatChartTextRgb(colorOf(d.part.i)) });
|
|
15442
|
+
}
|
|
15443
|
+
});
|
|
15444
|
+
if (labels.length) {
|
|
15445
|
+
// VALUEBACKGROUND on URL-symbol parts: a fading white border
|
|
15446
|
+
// around the value's own CHARACTER SHAPES — the glyphs
|
|
15447
|
+
// re-drawn in white on rings of pixel offsets (a stroke of
|
|
15448
|
+
// copies, no box): the width is a fraction of the font size,
|
|
15449
|
+
// so the halo scales with every label; the shape-part values
|
|
15450
|
+
// keep flat's plain text (no outline)
|
|
15451
|
+
if (flatFlag(this.flags, 'VALUEBACKGROUND')) {
|
|
15452
|
+
const haloLabels = labels.filter(l => l.image);
|
|
15453
|
+
const plainLabels = labels.filter(l => !l.image);
|
|
15454
|
+
if (haloLabels.length) {
|
|
15455
|
+
// a real outline around the characters: the label's own
|
|
15456
|
+
// glyphs re-drawn in white on rings of pixel offsets — a
|
|
15457
|
+
// stroke of copies that hugs the character shapes (deck's
|
|
15458
|
+
// SDF outline is only a faint, buffer-clipped rim). The
|
|
15459
|
+
// base radius is a small fraction of the font size (the
|
|
15460
|
+
// 1px floor keeps it tiny until labels get big), and the
|
|
15461
|
+
// ring alphas fall off, so the border fades to nothing
|
|
15462
|
+
const DIRS = 16, RINGS = [[1, 210], [2, 120], [3.2, 65]];
|
|
15463
|
+
const haloCopies = [];
|
|
15464
|
+
for (const l of haloLabels) {
|
|
15465
|
+
const rb = Math.max(0.75, l.fontSize * 0.015);
|
|
15466
|
+
for (let ring = 0; ring < RINGS.length; ring++) {
|
|
15467
|
+
for (let dir = 0; dir < DIRS; dir++) {
|
|
15468
|
+
const a = (dir / DIRS) * 2 * Math.PI;
|
|
15469
|
+
haloCopies.push({ ...l,
|
|
15470
|
+
dx: Math.cos(a), dy: Math.sin(a),
|
|
15471
|
+
radius: rb * RINGS[ring][0],
|
|
15472
|
+
haloAlpha: Math.round(RINGS[ring][1] * l.alpha / 255) });
|
|
15473
|
+
}
|
|
14853
15474
|
}
|
|
14854
15475
|
}
|
|
15476
|
+
layers.push(new TextLayer({
|
|
15477
|
+
id: `ix-cluster-values-halo-${this.name}${bs}`,
|
|
15478
|
+
data: haloCopies, pickable: false,
|
|
15479
|
+
getPosition: d => d.geometry.coordinates,
|
|
15480
|
+
getText: d => d.text,
|
|
15481
|
+
getSize: d => d.fontSize,
|
|
15482
|
+
getColor: d => [255, 255, 255, d.haloAlpha],
|
|
15483
|
+
getPixelOffset: d => [d.pixelOffset[0] + d.dx * d.radius, d.pixelOffset[1] + d.dy * d.radius],
|
|
15484
|
+
sizeUnits: 'pixels', fontFamily: 'arial', fontWeight: 'normal',
|
|
15485
|
+
characterSet: 'auto',
|
|
15486
|
+
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
15487
|
+
}));
|
|
15488
|
+
layers.push(new TextLayer({
|
|
15489
|
+
id: `ix-cluster-values-${this.name}${bs}`,
|
|
15490
|
+
data: haloLabels, pickable: false,
|
|
15491
|
+
getPosition: d => d.geometry.coordinates,
|
|
15492
|
+
getText: d => d.text,
|
|
15493
|
+
getSize: d => d.fontSize,
|
|
15494
|
+
getColor: d => [...d.color.slice(0, 3), d.alpha],
|
|
15495
|
+
getPixelOffset: d => d.pixelOffset,
|
|
15496
|
+
sizeUnits: 'pixels', fontFamily: 'arial', fontWeight: 'normal',
|
|
15497
|
+
characterSet: 'auto',
|
|
15498
|
+
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
15499
|
+
}));
|
|
14855
15500
|
}
|
|
14856
|
-
layers.push(new TextLayer({
|
|
14857
|
-
id: `ix-cluster-values-
|
|
14858
|
-
data:
|
|
14859
|
-
getPosition: d => d.geometry.coordinates,
|
|
14860
|
-
getText: d => d.text,
|
|
14861
|
-
getSize: d => d.fontSize,
|
|
14862
|
-
getColor: d => [255, 255, 255, d.haloAlpha],
|
|
14863
|
-
getPixelOffset: d => [d.pixelOffset[0] + d.dx * d.radius, d.pixelOffset[1] + d.dy * d.radius],
|
|
14864
|
-
sizeUnits: 'pixels', fontFamily: 'arial', fontWeight: 'normal',
|
|
14865
|
-
characterSet: 'auto',
|
|
14866
|
-
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
14867
|
-
}));
|
|
14868
|
-
layers.push(new TextLayer({
|
|
14869
|
-
id: `ix-cluster-values-${this.name}`,
|
|
14870
|
-
data: haloLabels, pickable: false,
|
|
15501
|
+
if (plainLabels.length) layers.push(new TextLayer({
|
|
15502
|
+
id: `ix-cluster-values-plain-${this.name}${bs}`,
|
|
15503
|
+
data: plainLabels, pickable: false,
|
|
14871
15504
|
getPosition: d => d.geometry.coordinates,
|
|
14872
15505
|
getText: d => d.text,
|
|
14873
15506
|
getSize: d => d.fontSize,
|
|
@@ -14877,10 +15510,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14877
15510
|
characterSet: 'auto',
|
|
14878
15511
|
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
14879
15512
|
}));
|
|
14880
|
-
}
|
|
14881
|
-
|
|
14882
|
-
id: `ix-cluster-values
|
|
14883
|
-
data:
|
|
15513
|
+
} else {
|
|
15514
|
+
layers.push(new TextLayer({
|
|
15515
|
+
id: `ix-cluster-values-${this.name}${bs}`,
|
|
15516
|
+
data: labels, pickable: false,
|
|
14884
15517
|
getPosition: d => d.geometry.coordinates,
|
|
14885
15518
|
getText: d => d.text,
|
|
14886
15519
|
getSize: d => d.fontSize,
|
|
@@ -14890,21 +15523,20 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14890
15523
|
characterSet: 'auto',
|
|
14891
15524
|
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
14892
15525
|
}));
|
|
14893
|
-
|
|
14894
|
-
layers.push(new TextLayer({
|
|
14895
|
-
id: `ix-cluster-values-${this.name}`,
|
|
14896
|
-
data: labels, pickable: false,
|
|
14897
|
-
getPosition: d => d.geometry.coordinates,
|
|
14898
|
-
getText: d => d.text,
|
|
14899
|
-
getSize: d => d.fontSize,
|
|
14900
|
-
getColor: d => [...d.color.slice(0, 3), d.alpha],
|
|
14901
|
-
getPixelOffset: d => d.pixelOffset,
|
|
14902
|
-
sizeUnits: 'pixels', fontFamily: 'arial', fontWeight: 'normal',
|
|
14903
|
-
characterSet: 'auto',
|
|
14904
|
-
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
14905
|
-
}));
|
|
15526
|
+
}
|
|
14906
15527
|
}
|
|
14907
15528
|
}
|
|
15529
|
+
};
|
|
15530
|
+
const chartBand = c => bandOfChart.get(c) || 0;
|
|
15531
|
+
if (nBands > 1) {
|
|
15532
|
+
// deck.gl draws the FIRST layer at the bottom: the smallest
|
|
15533
|
+
// charts' band first, the biggest (band 0) last, on top
|
|
15534
|
+
for (let k = nBands - 1; k >= 0; k--) drawBand(
|
|
15535
|
+
boxLayoutAll ? boxLayoutAll.filter(b => chartBand(b.d) === k) : null,
|
|
15536
|
+
allGlows.filter(g => chartBand(g.chart) === k),
|
|
15537
|
+
allParts.filter(p => chartBand(p.chart) === k), '-b' + k);
|
|
15538
|
+
} else {
|
|
15539
|
+
drawBand(boxLayoutAll, allGlows, allParts, '');
|
|
14908
15540
|
}
|
|
14909
15541
|
return layers;
|
|
14910
15542
|
}
|
|
@@ -14964,8 +15596,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14964
15596
|
// CENTERVALUE the item's record count
|
|
14965
15597
|
// Not drawn here (a console note at type parsing, PIE_INERT_FLAGS,
|
|
14966
15598
|
// PIE_AGGREGATE_INERT_FLAGS):
|
|
14967
|
-
// 3D, VOLUME, HEIGHT,
|
|
14968
|
-
//
|
|
15599
|
+
// 3D, VOLUME, HEIGHT, BOW, ZOOM, DIRECTION / DIRECTED — the pie is
|
|
15600
|
+
// drawn flat and plain instead; POLAR without STARBURST has no
|
|
15601
|
+
// effect in flat either. STARBURST (with the FLOWER/RAYS angle/gap
|
|
15602
|
+
// variants) and its POLAR circles ARE drawn (pieSliceLayout). Nor
|
|
14969
15603
|
// style.clipparts, showparts, gridx (flat's several pies per item).
|
|
14970
15604
|
// Known divergence: flat draws a chart's value labels in the chart's
|
|
14971
15605
|
// own group, so a bigger pie drawn later covers a smaller neighbour's
|
|
@@ -15029,6 +15663,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15029
15663
|
const lineRgb = line === 'none' ? null : hexOrNamedToRgb(line);
|
|
15030
15664
|
const lineWidthOf = r => (lineRgb ? (styleNum(st.linewidth) || 0.1) * r / 15 : 0);
|
|
15031
15665
|
const lineRgba = lineRgb ? [...lineRgb.slice(0, 3), Math.round(255 * lineAlpha)] : [0, 0, 0, 0];
|
|
15666
|
+
// flat's STARBURST (maptheme.js 20262-20275, piechart.js 281-287,
|
|
15667
|
+
// 415-420): the parts get equal angles and a ray's radius carries
|
|
15668
|
+
// its value — inner + span · sqrt(v / maxValue) · scale, scale =
|
|
15669
|
+
// maxValue / (normalsizevalue || maxValue) · 2.14 (with SIZE flat
|
|
15670
|
+
// sets only maxValue, so a part at the reference value reaches
|
|
15671
|
+
// 2.14 R). maxValue: style.maxvalue (the reference — the ISTAT
|
|
15672
|
+
// story's national mean), else the biggest part value over the
|
|
15673
|
+
// items (flat's max of the per-part maxima nMaxA). The star's
|
|
15674
|
+
// inner px (the ray variants' 5px) comes off the layout
|
|
15675
|
+
const star = has(/STARBURST/);
|
|
15676
|
+
let starMaxPart = 0;
|
|
15032
15677
|
|
|
15033
15678
|
const charts = [];
|
|
15034
15679
|
if (agg) {
|
|
@@ -15043,6 +15688,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15043
15688
|
if (!layout) continue;
|
|
15044
15689
|
if (cat) layout.slices.forEach(sl => { sl.single = values.length === 1; });
|
|
15045
15690
|
const sum = values.reduce((a, v) => a + (isNaN(v) ? 0 : v), 0);
|
|
15691
|
+
if (star) for (const v of values) if (v > starMaxPart) starMaxPart = v;
|
|
15046
15692
|
const r = nSizeOf(sum, size) * unit;
|
|
15047
15693
|
if (!(r > 0) || !isFinite(r)) continue;
|
|
15048
15694
|
// flat's draw-order key (maptheme.js 16812-16860): with a size
|
|
@@ -15067,6 +15713,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15067
15713
|
const layout = pieSliceLayout(values, t, { centerPart: st.centerpart });
|
|
15068
15714
|
if (!layout) continue;
|
|
15069
15715
|
const sum = values.reduce((a, v) => a + v, 0);
|
|
15716
|
+
if (star) for (const v of values) if (v > starMaxPart) starMaxPart = v;
|
|
15070
15717
|
const size = this.binding.size ? parseFloat(raw[this.binding.size]) : undefined;
|
|
15071
15718
|
const r = nSizeOf(sum, size) * unit;
|
|
15072
15719
|
if (!(r > 0) || !isFinite(r)) continue;
|
|
@@ -15099,14 +15746,80 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15099
15746
|
});
|
|
15100
15747
|
|
|
15101
15748
|
const TAU = 2 * Math.PI;
|
|
15102
|
-
const slices = [], centers = [], glows = [];
|
|
15749
|
+
const slices = [], centers = [], glows = [], polar = [], polarDashes = [];
|
|
15103
15750
|
const glowOn = has(/GLOW/) && !has(/ZOOM/) && !glowHiddenByScale(st, liveZoom);
|
|
15751
|
+
// the STARBURST ray scale (see the star comment above): style
|
|
15752
|
+
// maxvalue, else the biggest part value over the items — flat's
|
|
15753
|
+
// `this.nMaxValue || max(nMaxA)`. Without SIZE flat also sets the
|
|
15754
|
+
// donut's normalSizeValue (normalsizevalue, else maxValue), which
|
|
15755
|
+
// dampens the scale
|
|
15756
|
+
const starMax = styleNum(st.maxvalue);
|
|
15757
|
+
const starRef = !isNaN(starMax) && starMax > 0 ? starMax : (starMaxPart > 0 ? starMaxPart : 1);
|
|
15758
|
+
const starNsv = has(/\bSIZE\b/) ? starRef : (nsv || starRef);
|
|
15759
|
+
const starScale = starRef / starNsv * 2.14;
|
|
15760
|
+
// a chart's rays start at the CENTER — flat's `max(5, nRadInner)`
|
|
15761
|
+
// for the ray variants is 5 units of the chart's internal
|
|
15762
|
+
// coordinate system, sub-pixel and invisible on screen (see
|
|
15763
|
+
// pieSliceLayout) — and only with DONUT at the donut's inner
|
|
15764
|
+
// radius (flat piechart.js 326-327: nRadOuter += nRadInner, the
|
|
15765
|
+
// span stays R)
|
|
15766
|
+
const starInnerOf = ch => (ch.layout.inner || 0) * ch.r;
|
|
15767
|
+
const starRadius = (v, R, inner) => Math.max(1, inner + (R - inner) * Math.sqrt(Math.max(0, v) / starRef) * starScale);
|
|
15104
15768
|
for (const ch of charts) {
|
|
15769
|
+
const starInner = star ? starInnerOf(ch) : 0;
|
|
15770
|
+
if (star && has(/POLAR/)) {
|
|
15771
|
+
// flat piechart.js 435-452: the STARBURST's reference circles —
|
|
15772
|
+
// a white disc at the reference value (fill-opacity 0.25,
|
|
15773
|
+
// #444444 border, solid) and range rings at 75 / 50 / 25 % of
|
|
15774
|
+
// it, the 75% and 25% rings DASHED (stroke-dasharray 50 50 in
|
|
15775
|
+
// the chart group's units, on for half of each cycle). All of
|
|
15776
|
+
// them under every ray. The stroke widths are flat's 1px /
|
|
15777
|
+
// 3px — in the CHART GROUP's coordinate system, whose scale
|
|
15778
|
+
// is ~0.017 of the screen, so on screen they are sub-pixel
|
|
15779
|
+
// hairlines (measured on the ISTAT story: the disc's 197
|
|
15780
|
+
// group-units radius is 11.2px on screen); 1px ≈ 1.1% and
|
|
15781
|
+
// 3px ≈ 3.2% of the base radius R, taken here as fractions
|
|
15782
|
+
// of ch.r so they stay hairlines at every zoom, as in flat
|
|
15783
|
+
const lw1 = ch.r * 0.0108, lw3 = ch.r * 0.0325;
|
|
15784
|
+
const w = ch.r - starInner;
|
|
15785
|
+
polar.push({ position: ch.c, r: starInner + w * starScale, off: ch.off,
|
|
15786
|
+
fill: [255, 255, 255, 64], line: [68, 68, 68, 255], lw: lw1 });
|
|
15787
|
+
polar.push({ position: ch.c, r: starInner + w * Math.sqrt(0.5) * starScale, off: ch.off,
|
|
15788
|
+
fill: [0, 0, 0, 0], line: [68, 68, 68, 255], lw: lw1 });
|
|
15789
|
+
// the dashed rings: one chord per dash — flat's own arcs are
|
|
15790
|
+
// octant polylines, so straight chords are as faithful. The
|
|
15791
|
+
// dash count is flat's geometry: a 50-unit dash on the ring's
|
|
15792
|
+
// group radius — the ring in group units is ~17.7 × its
|
|
15793
|
+
// screen radius (the chart group scale measured on the
|
|
15794
|
+
// story), so a cycle is 100 group units = 100/(17.7 · 2π ·
|
|
15795
|
+
// ringR_screen) of the circle: ~11 dashes around the 75%,
|
|
15796
|
+
// ~6 around the 25% ring, at every zoom
|
|
15797
|
+
for (const q of [0.75, 0.25]) {
|
|
15798
|
+
const rr = starInner + w * Math.sqrt(q) * starScale;
|
|
15799
|
+
const cycles = Math.max(q > 0.5 ? 4 : 3, Math.round((q > 0.5 ? 2.06 : 1.19) * ch.r));
|
|
15800
|
+
const seg = 2 * Math.PI / cycles;
|
|
15801
|
+
for (let i = 0; i < cycles; i++) {
|
|
15802
|
+
const a0 = i * seg, a1 = a0 + seg / 2;
|
|
15803
|
+
// 0 at 12 o'clock, clockwise (pieSliceLayout's convention);
|
|
15804
|
+
// pixelOffsetLngLat's y grows down
|
|
15805
|
+
polarDashes.push({
|
|
15806
|
+
path: [
|
|
15807
|
+
pixelOffsetLngLat(ch.c, [Math.sin(a0) * rr, -Math.cos(a0) * rr], liveZoom),
|
|
15808
|
+
pixelOffsetLngLat(ch.c, [Math.sin(a1) * rr, -Math.cos(a1) * rr], liveZoom)
|
|
15809
|
+
], lw: lw3
|
|
15810
|
+
});
|
|
15811
|
+
}
|
|
15812
|
+
}
|
|
15813
|
+
}
|
|
15105
15814
|
for (const sl of ch.layout.slices) {
|
|
15106
15815
|
const rgb = sliceRgb(sl);
|
|
15816
|
+
// STARBURST: this ray's own radius (the value), its inner the
|
|
15817
|
+
// px starInner as a fraction of that radius — the shader takes
|
|
15818
|
+
// a fraction of the instance radius, which varies per ray here
|
|
15819
|
+
const rayR = star ? starRadius(sl.value, ch.r, starInner) : ch.r;
|
|
15107
15820
|
slices.push({
|
|
15108
|
-
position: ch.c, r:
|
|
15109
|
-
slice: [sl.start * Math.PI / 180, sl.sweep >= 360 ? TAU : sl.sweep * Math.PI / 180, ch.layout.inner],
|
|
15821
|
+
position: ch.c, r: rayR, off: ch.off, properties: ch.properties, chart: ch, part: sl,
|
|
15822
|
+
slice: [sl.start * Math.PI / 180, sl.sweep >= 360 ? TAU : sl.sweep * Math.PI / 180, star ? starInner / rayR : ch.layout.inner],
|
|
15110
15823
|
fill: [...rgb.slice(0, 3), !sl.complement && noFill.has(sl.i) ? 0 : Math.round(255 * fillAlpha)]
|
|
15111
15824
|
});
|
|
15112
15825
|
}
|
|
@@ -15130,13 +15843,80 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15130
15843
|
radiusUnits: 'pixels', billboard: true, stroked: false, filled: true, antialiasing: true,
|
|
15131
15844
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
15132
15845
|
};
|
|
15133
|
-
|
|
15846
|
+
// flat's BOX / TITLE for a pie (maptheme.js 18446-18925, the same
|
|
15847
|
+
// block the sequence builder draws): a rectangle around the
|
|
15848
|
+
// chart's drawn extent — on a STARBURST the longest ray, the
|
|
15849
|
+
// disc otherwise — plus, with TITLE, the item's title (an
|
|
15850
|
+
// aggregated grid cell of more than 3 records without its own:
|
|
15851
|
+
// "(n)", see _itemTitle) at the chart's left edge, wrapped to
|
|
15852
|
+
// the chart's width, in textcolor (#888888) over a half-opaque
|
|
15853
|
+
// white halo, its first baseline 0.7 font above the box — the
|
|
15854
|
+
// box grows to enclose it, its top trimmed by 0.1 font. The
|
|
15855
|
+
// box: white at boxopacity, #bbbbbb border at 0.3 opacity
|
|
15856
|
+
// (borderstyle solid/dotted/dashed: 1, none: no border),
|
|
15857
|
+
// borderwidth · 0.2 · width/chartSize px wide. Both are removed
|
|
15858
|
+
// outside boxupper / titleupper (maptheme.js 18758-18760, the
|
|
15859
|
+
// TITLE gate 18809-18811)
|
|
15860
|
+
// the chart's drawn extent, px around the anchor (flat's getBox of the
|
|
15861
|
+
// shape group — the longest ray on a STARBURST)
|
|
15862
|
+
const boxRows = this._boxRows(charts, { unit, liveZoom, extentOf: ch => {
|
|
15863
|
+
let e = ch.r;
|
|
15864
|
+
if (star) { const in0 = starInnerOf(ch); for (const sl of ch.layout.slices) e = Math.max(e, starRadius(sl.value, ch.r, in0)); }
|
|
15865
|
+
return [-e, -e, e, e];
|
|
15866
|
+
} });
|
|
15867
|
+
const layers = [];
|
|
15868
|
+
if (boxRows && boxRows.length) {
|
|
15869
|
+
const L = this._boxTitleLayers(boxRows, {
|
|
15870
|
+
unit, liveZoom,
|
|
15871
|
+
positionOf: b => b.d.c, offsetOf: b => b.d.off,
|
|
15872
|
+
ids: { circle: `ix-pie-box-${this.name}`, icon: `ix-pie-box-${this.name}`, polygons: `ix-pie-box-polygons-${this.name}`, titles: `ix-pie-titles-${this.name}` }
|
|
15873
|
+
});
|
|
15874
|
+
layers.push(...L.box, ...L.titles);
|
|
15875
|
+
}
|
|
15876
|
+
if (polar.length) {
|
|
15877
|
+
layers.push(new ScatterplotLayer({
|
|
15878
|
+
id: `ix-pie-polar-${this.name}`,
|
|
15879
|
+
data: polar, pickable: false,
|
|
15880
|
+
getPosition: d => d.position,
|
|
15881
|
+
getRadius: d => d.r,
|
|
15882
|
+
getPixelOffset: d => d.off,
|
|
15883
|
+
radiusUnits: 'pixels', billboard: true,
|
|
15884
|
+
filled: true, stroked: true,
|
|
15885
|
+
getFillColor: d => d.fill,
|
|
15886
|
+
getLineColor: d => d.line,
|
|
15887
|
+
getLineWidth: d => d.lw,
|
|
15888
|
+
lineWidthUnits: 'pixels',
|
|
15889
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
15890
|
+
}));
|
|
15891
|
+
// flat's dashed range rings (see the polar block): one chord per
|
|
15892
|
+
// dash, drawn in map coordinates at the current zoom — the
|
|
15893
|
+
// chart anchors move on zoom, and the layers rebuild with them
|
|
15894
|
+
if (polarDashes.length) {
|
|
15895
|
+
layers.push(new PathLayer({
|
|
15896
|
+
id: `ix-pie-polar-dashes-${this.name}`,
|
|
15897
|
+
data: polarDashes, pickable: false,
|
|
15898
|
+
getPath: d => d.path,
|
|
15899
|
+
getColor: [68, 68, 68, 255],
|
|
15900
|
+
getWidth: d => d.lw,
|
|
15901
|
+
widthUnits: 'pixels',
|
|
15902
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
15903
|
+
}));
|
|
15904
|
+
}
|
|
15905
|
+
}
|
|
15906
|
+
layers.push(new PieSliceLayer(Object.assign({}, discProps, {
|
|
15134
15907
|
id: `ix-pie-${this.name}`,
|
|
15135
15908
|
data: slices, pickable: true,
|
|
15136
15909
|
getLineColor: lineRgba,
|
|
15137
15910
|
getSlice: d => d.slice,
|
|
15138
|
-
|
|
15139
|
-
|
|
15911
|
+
// flat's pie outline (piechart.js: normalX(linewidth) · R / 300)
|
|
15912
|
+
// is 2% of the radius — sub-pixel for every small chart, and the
|
|
15913
|
+
// browser rasterizes it as a HAIRLINE: one device pixel at full
|
|
15914
|
+
// opacity (Skia's stroke floor). The 1 device-px floor here
|
|
15915
|
+
// matches that: the width is in the shader's device-pixel
|
|
15916
|
+
// space (pieAA = fwidth), so the CSS width is scaled by the
|
|
15917
|
+
// device pixel ratio first
|
|
15918
|
+
getSliceLineWidth: d => (lineWidthOf(d.r) > 0 ? Math.max(lineWidthOf(d.r) * (global.devicePixelRatio || 1), 1) : 0)
|
|
15919
|
+
})));
|
|
15140
15920
|
if (centers.length) {
|
|
15141
15921
|
layers.push(new PieSliceLayer(Object.assign({}, discProps, { id: `ix-pie-center-${this.name}`, data: centers, pickable: true })));
|
|
15142
15922
|
}
|
|
@@ -15350,7 +16130,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15350
16130
|
// STACKED beside the column on leader lines; flat's values color
|
|
15351
16131
|
// #798697 (textcolor), negative red (valuecolor); DTEXT and a sized
|
|
15352
16132
|
// POINTER fade small values
|
|
15353
|
-
// - BOX / TITLE (
|
|
16133
|
+
// - BOX / TITLE (chartBoxLayout): a box around chart and title, the
|
|
15354
16134
|
// item's title above it — boxcolor (white) at boxopacity, bordercolor
|
|
15355
16135
|
// (#bbbbbb) at 0.3, borderwidth · 0.2 · w / 30; title 5 u ·
|
|
15356
16136
|
// textscale in textcolor (#888888) over a white halo
|
|
@@ -15446,9 +16226,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15446
16226
|
}
|
|
15447
16227
|
applyFlatMaxCharts(charts, st);
|
|
15448
16228
|
|
|
15449
|
-
|
|
15450
|
-
|
|
15451
|
-
|
|
16229
|
+
// a BAR draws its box as a shader shape (rectangle only, no CIRCULARBOX):
|
|
16230
|
+
// the gates, margin, title font and placement are the box's, shared with
|
|
16231
|
+
// every chart type (_boxSpec / chartBoxLayout, here in chart units)
|
|
16232
|
+
const boxSpec = this._boxSpec(liveZoom);
|
|
16233
|
+
const boxUnits = st.titleunits ? ' ' + st.titleunits : '';
|
|
15452
16234
|
const boxOpacity = isNaN(styleNum(st.boxopacity)) ? 1 : styleNum(st.boxopacity);
|
|
15453
16235
|
const borderStyle = String(st.borderstyle || '');
|
|
15454
16236
|
const boxFill = [...hexOrNamedToRgb(isNoneColor(st.boxcolor) ? 'white' : st.boxcolor || 'white').slice(0, 3), isNoneColor(st.boxcolor) ? 0 : Math.round(255 * boxOpacity)];
|
|
@@ -15477,10 +16259,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15477
16259
|
for (const ch of charts) {
|
|
15478
16260
|
const L = ch.layout;
|
|
15479
16261
|
let box = null;
|
|
15480
|
-
if (
|
|
15481
|
-
const title = titleShown ? this._itemTitle(ch.properties) : '';
|
|
15482
|
-
box =
|
|
15483
|
-
|
|
16262
|
+
if (boxSpec) {
|
|
16263
|
+
const title = boxSpec.titleShown ? this._itemTitle(ch.properties) : '';
|
|
16264
|
+
box = chartBoxLayout(L.extent, { unit: 1, margin: boxSpec.margin, titleText: title ? title + boxUnits : '',
|
|
16265
|
+
titleFont: w => boxSpec.titleFontOf(1, w), bottomTitle: boxSpec.bottomTitle, alignRight: boxSpec.alignRight, measure });
|
|
15484
16266
|
}
|
|
15485
16267
|
const ext = box ? box.rect : L.extent;
|
|
15486
16268
|
// align / offsetx / offsety (flatChartAlignOffset), the chart's
|
|
@@ -15489,9 +16271,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15489
16271
|
if (box) {
|
|
15490
16272
|
const r = box.rect;
|
|
15491
16273
|
shape(ch, { base: [(r[0] + r[2]) / 2, r[3]], dir: 0, len: r[3] - r[1], w2: (r[2] - r[0]) / 2, sw: (r[2] - r[0]) / 2, band: 0, head: 0 },
|
|
15492
|
-
boxFill, boxLine, borderWidth * 0.2 * box.
|
|
15493
|
-
|
|
15494
|
-
|
|
16274
|
+
boxFill, boxLine, borderWidth * 0.2 * box.w / 30, null, true);
|
|
16275
|
+
// the title in px, rendered by the shared title layer
|
|
16276
|
+
if (box.title) titles.push({ d: ch, title: { text: box.title.text, x: box.title.x * unit, anchor: box.title.anchor,
|
|
16277
|
+
top: box.title.top * unit, f: box.title.f * unit } });
|
|
15495
16278
|
}
|
|
15496
16279
|
for (const b of L.bars) {
|
|
15497
16280
|
const rgb = this.classRgb(b.cls);
|
|
@@ -15559,18 +16342,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15559
16342
|
backgroundPadding: [0.3, 0, 0.4, 0.1].map(k => k * boxed.reduce((a, d) => a + d.font, 0) / boxed.length)
|
|
15560
16343
|
})));
|
|
15561
16344
|
}
|
|
15562
|
-
if (titles.length) {
|
|
15563
|
-
|
|
15564
|
-
|
|
15565
|
-
|
|
15566
|
-
sizeUnits: 'pixels', fontFamily: 'arial', characterSet: 'auto', lineHeight: 1,
|
|
15567
|
-
getColor: [...hexOrNamedToRgb(st.textcolor || '#888888').slice(0, 3), 255],
|
|
15568
|
-
fontSettings: { sdf: true }, outlineWidth: 1 / 7, outlineColor: [255, 255, 255, 128],
|
|
15569
|
-
getTextAnchor: d => d.anchor, getAlignmentBaseline: 'top',
|
|
15570
|
-
getPixelOffset: d => d.off,
|
|
15571
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
15572
|
-
}));
|
|
15573
|
-
}
|
|
16345
|
+
if (titles.length) layers.push(...this._boxTitleLayers(titles, {
|
|
16346
|
+
box: false, unit, liveZoom, positionOf: b => b.d.c, offsetOf: b => b.d.off,
|
|
16347
|
+
ids: { titles: `ix-bar-titles-${this.name}` }
|
|
16348
|
+
}).titles);
|
|
15574
16349
|
return layers;
|
|
15575
16350
|
}
|
|
15576
16351
|
|
|
@@ -15757,22 +16532,28 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15757
16532
|
return [(lng + 180) / 360 * world, (0.5 - Math.log((1 + s) / (1 - s)) / (4 * Math.PI)) * world];
|
|
15758
16533
|
};
|
|
15759
16534
|
const mercToLngLat = ([x, y]) => [x / world * 360 - 180, (2 * Math.atan(Math.exp((0.5 - y / world) * 2 * Math.PI)) - Math.PI / 2) * 180 / Math.PI];
|
|
15760
|
-
// the curves are computed in the plane
|
|
15761
|
-
//
|
|
15762
|
-
//
|
|
15763
|
-
//
|
|
15764
|
-
//
|
|
16535
|
+
// the curves are computed in the plane the map is DISPLAYED in: Web
|
|
16536
|
+
// Mercator under MapLibre's mercator projection; on the globe (flat's
|
|
16537
|
+
// orthographic map) the orthographic plane of the view center, for
|
|
16538
|
+
// the flows whose two ends face the viewer (the others keep
|
|
16539
|
+
// Mercator's). flat's own Equal Earth / Winkel plane (e1cb264) is NOT
|
|
16540
|
+
// used: this engine displays those maps in Web Mercator too (a flat
|
|
16541
|
+
// SVG map in another projection is reported "not available — mercator
|
|
16542
|
+
// used"), and a curve bowed in the Equal Earth plane swings far
|
|
16543
|
+
// toward the pole once Mercator renders it — measured on
|
|
16544
|
+
// mechanical_goods_flows.html: Japan→USA at 82°N, its bow almost half
|
|
16545
|
+
// the chord, where the Mercator curve stays at 63°N — the "deviated
|
|
16546
|
+
// near the north pole" vectors. Flat bows its curves in the plane of
|
|
16547
|
+
// ITS map; here the plane of the display.
|
|
15765
16548
|
const planeFns = (proj, k) => ({
|
|
15766
16549
|
toPx: ([lng, lat]) => { const q = proj.project(lat, lng); return [q[0] * k, -q[1] * k]; },
|
|
15767
16550
|
toLngLat: ([x, y]) => { const q = proj.invert(x / k, -y / k); return [q[1], q[0]]; }
|
|
15768
16551
|
});
|
|
15769
16552
|
const mercFns = { toPx: mercToPx, toLngLat: mercToLngLat };
|
|
15770
|
-
const fixedPlane = flatPlaneProjectionByOptions.get(this.mapOptions);
|
|
15771
|
-
const fixedFns = fixedPlane && fixedPlane.invert ? planeFns(fixedPlane, (world / 360) / fixedPlane.project(0, 1)[0]) : null;
|
|
15772
16553
|
const planeCenter = flatViewCenterByOptions.get(this.mapOptions) || globeCenter;
|
|
15773
|
-
const ortho =
|
|
16554
|
+
const ortho = globeCenter ? orthographicPlane(planeCenter.lat, planeCenter.lng) : null;
|
|
15774
16555
|
const orthoFns = ortho ? planeFns(ortho, world / (2 * Math.PI)) : null;
|
|
15775
|
-
const fnsOf = it => (
|
|
16556
|
+
const fnsOf = it => (orthoFns && ortho.inFront(it.p1) && ortho.inFront(it.p2) ? orthoFns : mercFns);
|
|
15776
16557
|
const inBox = c => c && c[0] >= bbox[0] && c[0] <= bbox[2] && c[1] >= bbox[1] && c[1] <= bbox[3];
|
|
15777
16558
|
|
|
15778
16559
|
const sizePow = resolveSizePow(st, this.flags);
|
|
@@ -15871,7 +16652,6 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15871
16652
|
// — every size equal — SVG ignores: the opacity stays)
|
|
15872
16653
|
if (dop || dopMin) { const d = dopacity(it); if (isFinite(d)) { lineAlpha = d; headAlpha = d; } }
|
|
15873
16654
|
if (gradient) stops = fadeGradientStops(pts, lay.vx, lay.vy, fade);
|
|
15874
|
-
if (dash) dashArr = [50 * oz, 5 * oz + sw];
|
|
15875
16655
|
pts = pts.map(p => [p[0] + a[0], p[1] + a[1]]);
|
|
15876
16656
|
end = [end[0] + a[0], end[1] + a[1]];
|
|
15877
16657
|
} else {
|
|
@@ -15885,7 +16665,25 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15885
16665
|
headS = 4; refX = 4; refY = 2;
|
|
15886
16666
|
lineAlpha = fo || 0.3;
|
|
15887
16667
|
headAlpha = Math.min(1, (fo || 0.3) * 2);
|
|
15888
|
-
|
|
16668
|
+
}
|
|
16669
|
+
if (dash) {
|
|
16670
|
+
// flat's dash pattern: one long dash (50 px at the object scale)
|
|
16671
|
+
// and a small gap, its phase walking toward the target — a flow
|
|
16672
|
+
// of any length gets its own count of dashes that way: long ones
|
|
16673
|
+
// many, short ones none at all (a solid line). style.dashcount
|
|
16674
|
+
// instead divides the flow into that many dash+gap periods:
|
|
16675
|
+
// every flow, long or short, shows the same pattern and moves at
|
|
16676
|
+
// the same relative speed (one full dash cycle every 2s), the
|
|
16677
|
+
// arrows as on the region-migration page
|
|
16678
|
+
const nDashes = Math.round(styleNum(st.dashcount) || 0);
|
|
16679
|
+
if (nDashes >= 1) {
|
|
16680
|
+
let flowPx = 0;
|
|
16681
|
+
for (let i = 1; i < pts.length; i++) flowPx += Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]);
|
|
16682
|
+
const period = flowPx / nDashes;
|
|
16683
|
+
dashArr = [period * 0.85, period * 0.15];
|
|
16684
|
+
} else {
|
|
16685
|
+
dashArr = bezier ? [50 * oz, 5 * oz + sw] : [50 * oz, 15 * oz];
|
|
16686
|
+
}
|
|
15889
16687
|
}
|
|
15890
16688
|
// a line under a pixel: one pixel at its width as opacity
|
|
15891
16689
|
const drawW = Math.max(1, sw);
|
|
@@ -15911,7 +16709,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15911
16709
|
let run = [], runColors = [];
|
|
15912
16710
|
const flush = () => {
|
|
15913
16711
|
if (run.length > 1) {
|
|
15914
|
-
|
|
16712
|
+
// the flow triple: [dash, gap] in px (the shader's path
|
|
16713
|
+
// distance unit) + flat's random per-arc dashoffset phase
|
|
16714
|
+
// (maptheme.js 18249), fixed per arc: the layers are rebuilt on
|
|
16715
|
+
// every pan/zoom, a new random phase each time made dashes jump
|
|
16716
|
+
const dash = dashArr ? [dashArr[0], dashArr[1], flowDashPhase(p1, p2)] : [0, 0, 0];
|
|
15915
16717
|
if (globeCenter && !dashArr) {
|
|
15916
16718
|
// on the globe deck.gl draws only the first three quarters of a path that has a
|
|
15917
16719
|
// color per vertex (also an equal one): one color per path, a gradient as one
|
|
@@ -15946,12 +16748,22 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15946
16748
|
fill: [...rgb.slice(0, 3), Math.round(255 * Math.min(1, headAlpha) * groupAlpha)] });
|
|
15947
16749
|
}
|
|
15948
16750
|
}
|
|
15949
|
-
|
|
16751
|
+
// always FlowPathLayer: without dash offsets its flow hook no-ops
|
|
16752
|
+
// (solid lines), with them the pattern walks toward the target
|
|
16753
|
+
// (see makeFlowPathLayer). The dash triple is px; deck's path
|
|
16754
|
+
// shader measures along-path positions in HALF-STROKE-WIDTH
|
|
16755
|
+
// units (see makeFlowPathLayer), so the accessors convert:
|
|
16756
|
+
// getDashArray carries the pattern period (its gap stays 0 — the
|
|
16757
|
+
// extension renders solid, the moving gap is the flow hook),
|
|
16758
|
+
// getFlowPhase the solid length and flat's random per-arc phase
|
|
16759
|
+
const layers = [new FlowPathLayer({
|
|
15950
16760
|
id: `ix-vector-${this.name}`, data: paths, pickable: true,
|
|
15951
16761
|
getPath: d => d.path, getColor: d => d.colors, getWidth: d => d.width,
|
|
16762
|
+
getDashArray: d => [(d.dash[0] + d.dash[1]) * 2 / Math.max(d.width, 1), 0],
|
|
16763
|
+
getFlowPhase: d => [d.dash[0] * 2 / Math.max(d.width, 1), d.dash[2]],
|
|
15952
16764
|
widthUnits: 'pixels', capRounded: dash && bezier, jointRounded: false, billboard: false,
|
|
15953
|
-
|
|
15954
|
-
|
|
16765
|
+
extensions: [new PathStyleExtension({ dash: true, dashMode: 'path' })],
|
|
16766
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
15955
16767
|
})];
|
|
15956
16768
|
if (heads.length) {
|
|
15957
16769
|
// the head count in the id: deck kept a same-id polygon layer's
|
|
@@ -15970,7 +16782,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15970
16782
|
// flat's chart position for this build (flatChartAlignOffset): a
|
|
15971
16783
|
// function item → pixel offset, the same for every part of one chart
|
|
15972
16784
|
// (symbol, glow, shadow, box, title, value text). unit is flat's
|
|
15973
|
-
// 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)
|
|
15974
16788
|
_chartAlignFn(liveZoom, radiusOf, textOnly = false) {
|
|
15975
16789
|
const st = this.style;
|
|
15976
16790
|
const none = () => [0, 0];
|
|
@@ -15979,12 +16793,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15979
16793
|
const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(st.scale) || 1);
|
|
15980
16794
|
const symbol = flatChartBranch(this.flags) === 'symbol';
|
|
15981
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;
|
|
15982
16797
|
const cache = new Map();
|
|
15983
16798
|
const fn = d => {
|
|
15984
16799
|
let v = cache.get(d);
|
|
15985
16800
|
if (v) return v;
|
|
15986
16801
|
const r = radiusOf(d);
|
|
15987
|
-
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) });
|
|
15988
16803
|
cache.set(d, v);
|
|
15989
16804
|
return v;
|
|
15990
16805
|
};
|
|
@@ -16051,7 +16866,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16051
16866
|
const v = raw && raw[this.style.labelfield];
|
|
16052
16867
|
let text = v == null || v === '' ? '' : String(v);
|
|
16053
16868
|
if (!text) return null;
|
|
16054
|
-
|
|
16869
|
+
// flat reads it with __scanValue: "1.234,5", "12 500"
|
|
16870
|
+
const num = flatScanValue(text);
|
|
16055
16871
|
text = (!isNaN(num) ? flatGroupedValue(num, dec != null ? dec : 2, noBreaks) : text) + units;
|
|
16056
16872
|
const width = Math.max(titleTextWidth(text, f), (text.length + 2) * f * 0.5);
|
|
16057
16873
|
const x = width / 3, y = f / 3 + r + 5 * unitPx;
|
|
@@ -16162,7 +16978,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16162
16978
|
const textOnly = this.flags.has('TEXTONLY');
|
|
16163
16979
|
const showValues = this.flags.has('VALUES') && !valuesHiddenByScale(this.style, zoom);
|
|
16164
16980
|
if (textOnly && !showValues) return [];
|
|
16165
|
-
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());
|
|
16166
16982
|
const alignOf = this._chartAlignFn(liveZoom, radiusOf, textOnly);
|
|
16167
16983
|
const valueScale = styleNum(this.style.valuescale) || 1;
|
|
16168
16984
|
const textOverride = this.style.valuecolor || this.style.textcolor;
|
|
@@ -16223,6 +17039,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16223
17039
|
// the box is sized by the value's own text, not the valuefield's
|
|
16224
17040
|
const boxStyle = Object.assign({}, this.style, { valuefield: undefined });
|
|
16225
17041
|
const boxes = [], texts = [];
|
|
17042
|
+
// the label's drawn extent for the chart-group BOX (below): x from -r
|
|
17043
|
+
// to 1.05r, y from -0.83f to 0.77f around the chart origin
|
|
17044
|
+
const labelExtent = new Map();
|
|
16226
17045
|
for (const d of combined) {
|
|
16227
17046
|
const r = radiusOf(d);
|
|
16228
17047
|
if (!(r > 0)) continue;
|
|
@@ -16237,6 +17056,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16237
17056
|
const o = alignOf(d);
|
|
16238
17057
|
// the box's center relative to the chart's: x from −r to 1.05r, y from −0.83f to 0.77f
|
|
16239
17058
|
boxes.push({ ...d, icon, pixelOffset: [o[0] + 0.025 * r, o[1] - 0.03 * f] });
|
|
17059
|
+
labelExtent.set(d, [-r, -0.83 * f, 1.05 * r, 0.77 * f]);
|
|
16240
17060
|
if (showValues) {
|
|
16241
17061
|
const text = textOf(props);
|
|
16242
17062
|
const fontSize = Math.min(r * 0.8, r * 3.3 / Math.max(1, text.length)) * valueScale;
|
|
@@ -16244,7 +17064,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16244
17064
|
if (fontSize > 0) texts.push({ ...d, text, fontSize, color, pixelOffset: o });
|
|
16245
17065
|
}
|
|
16246
17066
|
}
|
|
16247
|
-
|
|
17067
|
+
// BOX / TITLE: flat's chart-group box around the label chart
|
|
17068
|
+
const boxRows = this._boxRows(() => combined.filter(d => labelExtent.has(d)), { unit, liveZoom, extentOf: d => labelExtent.get(d) });
|
|
17069
|
+
const boxLayers = boxRows ? this._boxTitleLayers(boxRows, {
|
|
17070
|
+
unit, liveZoom, positionOf: b => b.d.geometry.coordinates, offsetOf: b => alignOf(b.d),
|
|
17071
|
+
ids: { circle: `ix-label-box-${this.name}`, icon: `ix-label-box-${this.name}`, polygons: `ix-label-box-polygons-${this.name}`, titles: `ix-label-titles-${this.name}` }
|
|
17072
|
+
}) : null;
|
|
17073
|
+
const layers = boxLayers ? [...boxLayers.box] : [];
|
|
16248
17074
|
if (boxes.length) {
|
|
16249
17075
|
layers.push(new IconLayer({
|
|
16250
17076
|
id: `ix-bubbles-${this.name}-g${this._iconGeneration}`,
|
|
@@ -16278,6 +17104,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16278
17104
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
16279
17105
|
}));
|
|
16280
17106
|
}
|
|
17107
|
+
if (boxLayers) layers.push(...boxLayers.titles);
|
|
16281
17108
|
return layers;
|
|
16282
17109
|
}
|
|
16283
17110
|
|
|
@@ -16535,12 +17362,16 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16535
17362
|
const _mapHandles = Object.create(null);
|
|
16536
17363
|
function makeMapHandle(szMap) {
|
|
16537
17364
|
const h = {};
|
|
16538
|
-
|
|
17365
|
+
// a name a Map() declared waits for that map; any other name (or none)
|
|
17366
|
+
// is the last built map, as before
|
|
17367
|
+
const declared = () => szMap && _namedMapReady[szMap];
|
|
17368
|
+
const api = () => (szMap && _mapApis[szMap]) || (declared() ? null : _lastMapApi);
|
|
16539
17369
|
for (const m of MAP_HANDLE_METHODS) {
|
|
16540
17370
|
h[m] = (...args) => {
|
|
16541
17371
|
const a = api();
|
|
16542
17372
|
if (a) { const r = a[m](...args); return r === a ? h : r; }
|
|
16543
|
-
_mapReady.then(ready => ready[m](...args))
|
|
17373
|
+
(declared() ? declared().promise : _mapReady).then(ready => ready[m](...args))
|
|
17374
|
+
.catch(err => console.error(`[ixmaps-gl] map(${szMap ? '"' + szMap + '"' : ''}).${m}:`, err));
|
|
16544
17375
|
return h;
|
|
16545
17376
|
};
|
|
16546
17377
|
}
|
|
@@ -16708,6 +17539,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16708
17539
|
// several themes of a page
|
|
16709
17540
|
szId: themeIdOf(rt),
|
|
16710
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),
|
|
16711
17545
|
// the filter in force: a runtime one (changeThemeStyle "filter")
|
|
16712
17546
|
// replaces the definition's
|
|
16713
17547
|
szFilter: rt._runtimeFilterExpr || rt._filterExpr || '',
|
|
@@ -16744,6 +17578,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16744
17578
|
get itemA() { return onMap().itemA; }
|
|
16745
17579
|
};
|
|
16746
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
|
+
}
|
|
16747
17588
|
const _warnedOnce = new Set();
|
|
16748
17589
|
function warnOnce(key, msg) { if (!_warnedOnce.has(key)) { _warnedOnce.add(key); console.info(msg); } }
|
|
16749
17590
|
// the theme's records as flat's dbFields/dbRecords (cached per data)
|
|
@@ -17005,9 +17846,14 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17005
17846
|
// their own comments) are still real, directly-callable globals a
|
|
17006
17847
|
// page's OWN custom UI can use too — only THIS generated markup
|
|
17007
17848
|
// happens to reach them via delegation instead of inline HTML.
|
|
17849
|
+
// the handlers below outlive this call: they read the theme id of
|
|
17850
|
+
// the LATEST call into this container (a theme switch re-renders
|
|
17851
|
+
// the same div for another theme)
|
|
17852
|
+
container._ixFacetsId = szId;
|
|
17008
17853
|
if (!container.dataset.ixFacetsWired) {
|
|
17009
17854
|
container.dataset.ixFacetsWired = '1';
|
|
17010
17855
|
container.addEventListener('click', e => {
|
|
17856
|
+
const szId = container._ixFacetsId;
|
|
17011
17857
|
const moreBtn = e.target.closest('[data-role="more"]');
|
|
17012
17858
|
if (moreBtn) { moreBtn.hidden = true; moreBtn.nextElementSibling.hidden = false; return; }
|
|
17013
17859
|
|
|
@@ -17033,7 +17879,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17033
17879
|
container.addEventListener('keyup', e => {
|
|
17034
17880
|
if (e.key !== 'Enter') return;
|
|
17035
17881
|
const input = e.target.closest('[data-role="search"]');
|
|
17036
|
-
if (input) global.__setFilter(
|
|
17882
|
+
if (input) global.__setFilter(container._ixFacetsId, input.closest('[data-field]').dataset.field, input.value);
|
|
17037
17883
|
});
|
|
17038
17884
|
}
|
|
17039
17885
|
}
|
|
@@ -17123,11 +17969,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17123
17969
|
// known real page's globe toggle actually uses (see
|
|
17124
17970
|
// example-world-bubble-projection-toggle.md) — so `ixmaps.szResourceBase`
|
|
17125
17971
|
// only needs to be SOME string, not a real, working resource base URL.
|
|
17126
|
-
|
|
17127
|
-
// page (tooltip_mustache.js) resolve their dependencies through it —
|
|
17128
|
-
// $.getScript(ixmaps.szResourceBase + "ui/js/tools/format.js") — which
|
|
17129
|
-
// with the empty string 404s against the page's own directory
|
|
17130
|
-
const ixmapsSzResourceBase = 'https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-flat@1/';
|
|
17972
|
+
const ixmapsSzResourceBase = '';
|
|
17131
17973
|
function getProjectString() {
|
|
17132
17974
|
if (!_lastMapApi || !_lastMapApi.map) return JSON.stringify({ map: {} });
|
|
17133
17975
|
const map = _lastMapApi.map;
|
|
@@ -17174,6 +18016,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17174
18016
|
function compatRemoveTheme(...a) {
|
|
17175
18017
|
return _mapHandle.remove(a.length >= 2 ? a[1] : a[0]);
|
|
17176
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
|
+
}
|
|
17177
18023
|
function compatSetBasemapOpacity(...a) {
|
|
17178
18024
|
if (a.length >= 3) {
|
|
17179
18025
|
const api = (a[0] && _mapApis[a[0]]) || _lastMapApi;
|
|
@@ -17209,6 +18055,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17209
18055
|
htmlgui_getAttributionString: () => (_lastMapApi && _lastMapApi.getAttribution ? _lastMapApi.getAttribution() : ''),
|
|
17210
18056
|
changeThemeStyle: compatChangeThemeStyle,
|
|
17211
18057
|
removeTheme: compatRemoveTheme,
|
|
18058
|
+
toggleTheme: compatToggleTheme,
|
|
17212
18059
|
// flat's ixmaps.setLocalString(orig, local) (htmlgui.js 3814 →
|
|
17213
18060
|
// Api.setLocalString → Dictionary.replace): a page-wide translation,
|
|
17214
18061
|
// e.g. the migration page's ISO code → country name; usable before
|
|
@@ -17247,19 +18094,19 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17247
18094
|
// test-only: lets test/unit/*.test.mjs call pure internals directly (the
|
|
17248
18095
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
17249
18096
|
global.__ixmapsGlInternals = {
|
|
17250
|
-
IXMAPS_GL_VERSION, applyFlatMaxCharts, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout, ringsLayout, pieSliceLayout, pieValueLabelLayout, isBarChart, flatBarLayout,
|
|
17251
|
-
choroplethOutlineColor, normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
18097
|
+
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,
|
|
18098
|
+
chartBoxLayout, choroplethOutlineColor, normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
17252
18099
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
17253
18100
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|
|
17254
18101
|
computeAlphaStats, computeMultiFieldClasses, computeDominantStats, resolveDominantClass, computeComposeColorStats,
|
|
17255
18102
|
resolveComposedColor, computeRangeClasses, colorSchemeClassCount, resolveDopacityAlpha,
|
|
17256
18103
|
flatColorSweep, applyClassesToColorScheme, resolveClassColors, flatOutlierStats, parseCssColor, flatLegendLook,
|
|
17257
18104
|
flatToMapLibreZoom, mapLibreToFlatZoom, flatViewToMapLibreZoom, mapLibreToFlatViewZoom, flatSvgProjectionOf, flatSvgProjectionByOptions, normalSizeScaleOf, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
|
|
17258
|
-
fetchLayerData, 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,
|
|
17259
|
-
applyWhereFilter, joinChartPositions, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatBubbleLook, bubbleIconLook, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
17260
|
-
drawSymbolPath, normalizeSymbolShape,
|
|
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,
|
|
18106
|
+
applyWhereFilter, joinChartPositions, joinChoroplethFeatures, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatBubbleLook, bubbleIconLook, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
18107
|
+
drawSymbolPath, normalizeSymbolShape, bubbleStrokeRgb,
|
|
17261
18108
|
flatFormatValue, themeStyleArgs, flatChangedStyleValue, explicitValueDecimals, legendDecimals,
|
|
17262
|
-
dataCacheKey, dataCacheDisabled, cachedLayerData, featuresBounds, wantsZoomToExtent, legendIsOn,
|
|
18109
|
+
dataCacheKey, dataCacheDisabled, cachedLayerData, featuresBounds, wantsZoomToExtent, legendIsOn, applySphereBackdrop,
|
|
17263
18110
|
};
|
|
17264
18111
|
global.__setFilter = __setFilter;
|
|
17265
18112
|
global.__removeFacets = __removeFacets;
|