ixmaps-gl 0.2.23 → 0.2.25
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 +3 -3
- package/ixmaps-gl.js +2285 -541
- package/package.json +1 -1
package/ixmaps-gl.js
CHANGED
|
@@ -45,7 +45,11 @@
|
|
|
45
45
|
// range adjustments; see flatRangeParts). A CHOROPLETH
|
|
46
46
|
// layer's geometry is borrowed from a FEATURE base layer sharing the same
|
|
47
47
|
// .layer() name (the real engine's own convention), joined via
|
|
48
|
-
// binding.lookup — see joinChoroplethFeatures.
|
|
48
|
+
// binding.lookup — see joinChoroplethFeatures. POSITION (maptheme.js
|
|
49
|
+
// 8328-8353) is a theme that renders nothing itself: it loads a table of
|
|
50
|
+
// id + position pairs and provides the id→position join that other themes
|
|
51
|
+
// on the same .layer() name consume via binding.lookup — see
|
|
52
|
+
// joinChartPositions. HEADTAIL breaks are flat's
|
|
49
53
|
// (headTailBreaks); LOG/POW2/POW3 are recognized type flags but not
|
|
50
54
|
// implemented, see KNOWN_INERT_FLAGS. CHART|USER draws a page's own chart
|
|
51
55
|
// function (style.userdraw, _buildUserChartLayers), CHART|LABEL flat's
|
|
@@ -96,7 +100,7 @@
|
|
|
96
100
|
// ---------------------------------------------------------------
|
|
97
101
|
// this engine's release (= package.json "version", checked by
|
|
98
102
|
// test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
|
|
99
|
-
const IXMAPS_GL_VERSION = '0.2.
|
|
103
|
+
const IXMAPS_GL_VERSION = '0.2.25';
|
|
100
104
|
|
|
101
105
|
const LIB_URLS = {
|
|
102
106
|
// Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
|
|
@@ -216,7 +220,7 @@
|
|
|
216
220
|
let IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer, PathLayer, PathStyleExtension;
|
|
217
221
|
// PieSliceLayer (see makePieSliceLayer) and BarShapeLayer
|
|
218
222
|
// (makeBarShapeLayer) are derived from ScatterplotLayer once deck.gl is there
|
|
219
|
-
let PieSliceLayer, BarShapeLayer;
|
|
223
|
+
let PieSliceLayer, BarShapeLayer, FlowPathLayer;
|
|
220
224
|
|
|
221
225
|
// CHART|PIE / DONUT slices (_buildPieLayers): one instance per slice — a
|
|
222
226
|
// ScatterplotLayer circle (billboarded, pixel radius, pixel offset, the
|
|
@@ -238,6 +242,111 @@
|
|
|
238
242
|
// without anti-aliasing between them (a hard partition: no background
|
|
239
243
|
// seam shows between two slices); the circle's rim keeps
|
|
240
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
|
+
|
|
241
350
|
function makePieSliceLayer(Base) {
|
|
242
351
|
class Layer extends Base {
|
|
243
352
|
getShaders() {
|
|
@@ -289,12 +398,15 @@ in vec4 vPieLineColor;
|
|
|
289
398
|
}
|
|
290
399
|
// in device pixels (fwidth: CSS px per device pixel), so a hairline
|
|
291
400
|
// stays one at any device pixel ratio. Half the outline lies inside
|
|
292
|
-
// each slice (the other half in its neighbour)
|
|
293
|
-
//
|
|
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
|
|
294
406
|
float pieAA = max(fwidth(pieR), 1e-4);
|
|
295
407
|
float pieH = vPieLineWidth * 0.5 / pieAA;
|
|
296
408
|
float pieDD = pieD / pieAA;
|
|
297
|
-
float pieLine = pieH > 0.0 ? clamp(
|
|
409
|
+
float pieLine = pieH > 0.0 ? clamp(pieH + 0.5 - pieDD, 0.0, 1.0) : 0.0;
|
|
298
410
|
float pieRim = clamp((vPieRadius - pieR) / pieAA + 0.5, 0.0, 1.0);
|
|
299
411
|
color = vec4(mix(color.rgb, vPieLineColor.rgb, pieLine), mix(color.a, vPieLineColor.a * pieRim, pieLine));
|
|
300
412
|
if (pieInner > 0.0) color.a *= clamp((pieR - pieInner) / pieAA + 0.5, 0.0, 1.0);
|
|
@@ -499,6 +611,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
499
611
|
({ IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer, PathLayer, PathStyleExtension } = global.deck);
|
|
500
612
|
PieSliceLayer = makePieSliceLayer(ScatterplotLayer);
|
|
501
613
|
BarShapeLayer = makeBarShapeLayer(ScatterplotLayer);
|
|
614
|
+
FlowPathLayer = makeFlowPathLayer(PathLayer);
|
|
502
615
|
// which engine and libraries this page really runs (a page may
|
|
503
616
|
// bring its own MapLibre/deck.gl — the loaded ones are named)
|
|
504
617
|
const ml = global.maplibregl && typeof global.maplibregl.getVersion === 'function' ? global.maplibregl.getVersion() : '?';
|
|
@@ -530,7 +643,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
530
643
|
// crafted link must not be able to load arbitrary code into the page.
|
|
531
644
|
// ---------------------------------------------------------------
|
|
532
645
|
// v0.1.6: knows this engine's aliases, lookupfield rule, meta↔style, builder methods, project loading
|
|
533
|
-
const VALIDATOR_URL_DEFAULT = 'https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-grammar@v0.1.
|
|
646
|
+
const VALIDATOR_URL_DEFAULT = 'https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-grammar@v0.1.28/dist/validate.mjs';
|
|
534
647
|
// a page may set ixmaps.validate BEFORE loading this script — the
|
|
535
648
|
// `global.ixmaps = {...}` export below would otherwise overwrite it
|
|
536
649
|
const _preloadValidate = global.ixmaps && global.ixmaps.validate;
|
|
@@ -658,10 +771,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
658
771
|
return ring;
|
|
659
772
|
}
|
|
660
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
|
+
|
|
661
781
|
function topoGeometryToGeoJSON(geom, arcs, transform) {
|
|
662
782
|
switch (geom.type) {
|
|
663
|
-
case 'Point': return { type: 'Point', coordinates: geom.coordinates };
|
|
664
|
-
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)) };
|
|
665
785
|
case 'LineString': return { type: 'LineString', coordinates: ringCoords(geom.arcs, arcs, transform) };
|
|
666
786
|
case 'MultiLineString': return { type: 'MultiLineString', coordinates: geom.arcs.map(a => ringCoords(a, arcs, transform)) };
|
|
667
787
|
case 'Polygon': return { type: 'Polygon', coordinates: geom.arcs.map(ring => ringCoords(ring, arcs, transform)) };
|
|
@@ -670,8 +790,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
670
790
|
}
|
|
671
791
|
}
|
|
672
792
|
|
|
673
|
-
function topojsonToFeatureCollection(topo) {
|
|
674
|
-
|
|
793
|
+
function topojsonToFeatureCollection(topo, objectName) {
|
|
794
|
+
// .data({object: "NAME"}) selects the topojson object to show; without
|
|
795
|
+
// one (or with an unknown name) it stays the file's FIRST object, the
|
|
796
|
+
// previous behaviour
|
|
797
|
+
const objName = (objectName && topo.objects[objectName]) ? objectName : Object.keys(topo.objects)[0];
|
|
675
798
|
const obj = topo.objects[objName];
|
|
676
799
|
const geoms = obj.type === 'GeometryCollection' ? obj.geometries : [obj];
|
|
677
800
|
const features = geoms.map(g => ({
|
|
@@ -787,7 +910,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
787
910
|
}
|
|
788
911
|
return false;
|
|
789
912
|
});
|
|
790
|
-
|
|
913
|
+
const out = features.length === fc.features.length ? fc : Object.assign({}, fc, { features });
|
|
914
|
+
// a {type:"Sphere"} feature is flat's ocean backdrop: not drawn as a shape, the globe's own
|
|
915
|
+
// surface takes its color (see applySphereBackdrop)
|
|
916
|
+
if (fc.features.some(f => f && f.geometry && f.geometry.type === 'Sphere')) Object.defineProperty(out, '_sphere', { value: true, enumerable: false });
|
|
917
|
+
return out;
|
|
791
918
|
}
|
|
792
919
|
|
|
793
920
|
// CHOROPLETH's own .data() (e.g. the comuni demographics CSV in
|
|
@@ -826,7 +953,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
826
953
|
function dataSourceKey(data) {
|
|
827
954
|
const d = data || {};
|
|
828
955
|
return JSON.stringify({ url: d.url, urls: d.urls, type: d.type, query: d.query,
|
|
829
|
-
process: d.process && String(d.process), ext: d.ext, name: d.ext ? d.name : undefined
|
|
956
|
+
process: d.process && String(d.process), ext: d.ext, name: d.ext ? d.name : undefined,
|
|
957
|
+
// .data({object}) selects a topojson object — two themes on the same
|
|
958
|
+
// URL with different objects must NOT share one cache entry (the
|
|
959
|
+
// DISS page's 5 layers of one file)
|
|
960
|
+
object: d.object });
|
|
830
961
|
}
|
|
831
962
|
function dataCacheKey(data, binding) {
|
|
832
963
|
const b = binding || {};
|
|
@@ -926,11 +1057,40 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
926
1057
|
// ixmaps-flat dev tree, Themes.prototype.setExternalData). This engine
|
|
927
1058
|
// has no such pipeline to resume, so the bridge only needs to do the
|
|
928
1059
|
// ONE thing fetchLayerData actually needs: resolve the promise a
|
|
929
|
-
// query()-mode fetch is awaiting.
|
|
930
|
-
//
|
|
931
|
-
//
|
|
932
|
-
//
|
|
933
|
-
|
|
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
|
+
}
|
|
934
1094
|
// Data by name, as flat (maptheme.js 2570-2593, 8286-8310):
|
|
935
1095
|
// setExternalData(data, {type, name}) makes a data.js Table of the data
|
|
936
1096
|
// and keeps it as window[name]; whoever waits for that name gets it, and
|
|
@@ -976,6 +1136,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
976
1136
|
// globals a data name must not overwrite
|
|
977
1137
|
const _engineGlobalsGuard = { ixmaps: 1, Data: 1, deck: 1, maplibregl: 1, window: 1, document: 1, $: 1, jQuery: 1 };
|
|
978
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
|
+
}
|
|
979
1145
|
const name = opt && opt.name;
|
|
980
1146
|
if (name != null && name !== '') {
|
|
981
1147
|
(dataObj == null ? Promise.resolve(null) : externalDataToTable(dataObj, opt))
|
|
@@ -983,13 +1149,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
983
1149
|
.catch(err => { console.error(err); publishNamedData(String(name), null); });
|
|
984
1150
|
return;
|
|
985
1151
|
}
|
|
986
|
-
|
|
987
|
-
console.warn('[ixmaps-gl] ixmaps.setExternalData called with no pending .data({query}) fetch — ignored');
|
|
988
|
-
return;
|
|
989
|
-
}
|
|
990
|
-
const resolve = _pendingQueryResolve;
|
|
991
|
-
_pendingQueryResolve = null;
|
|
992
|
-
resolve(dataObj);
|
|
1152
|
+
console.warn('[ixmaps-gl] ixmaps.setExternalData called with no pending .data({query}) fetch — ignored');
|
|
993
1153
|
}
|
|
994
1154
|
|
|
995
1155
|
// ---------------------------------------------------------------
|
|
@@ -1371,7 +1531,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1371
1531
|
// genuinely CSV-shaped .obj with a missing binding.position would
|
|
1372
1532
|
// throw, even though this .obj was never row data in the first place.
|
|
1373
1533
|
if (dataConfig.obj) {
|
|
1374
|
-
if (dataConfig.type === 'topojson') return topojsonToFeatureCollection(dataConfig.obj);
|
|
1534
|
+
if (dataConfig.type === 'topojson') return topojsonToFeatureCollection(dataConfig.obj, dataConfig.object);
|
|
1375
1535
|
if (dataConfig.type === 'geojson') return sanitizeGeoJSON(dataConfig.obj);
|
|
1376
1536
|
let obj = dataConfig.obj;
|
|
1377
1537
|
if (dataConfig.process) {
|
|
@@ -1387,7 +1547,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1387
1547
|
}
|
|
1388
1548
|
obj = runDataProcess(obj, dataConfig);
|
|
1389
1549
|
}
|
|
1390
|
-
|
|
1550
|
+
// a raw jsonDB object ({fields, records}) → rows, with or without
|
|
1551
|
+
// process (and without needing data.js)
|
|
1552
|
+
const isJsonDb = !Array.isArray(obj) && Array.isArray(obj.fields) && Array.isArray(obj.records);
|
|
1553
|
+
const rows = typeof obj.json === 'function' ? obj.json() : isJsonDb ? dataTableRows(obj) : obj;
|
|
1391
1554
|
return rowsResult(rows, binding);
|
|
1392
1555
|
}
|
|
1393
1556
|
|
|
@@ -1397,9 +1560,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1397
1560
|
'<script src="https://cdn.jsdelivr.net/gh/gjrichter/data.js@master/data.js"> before ixmaps-gl.js');
|
|
1398
1561
|
}
|
|
1399
1562
|
const queryFn = typeof dataConfig.query === 'function' ? dataConfig.query : new Function(`return (${dataConfig.query});`)();
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1563
|
+
// its own options object: the query hands it back with the data
|
|
1564
|
+
const opts = { type: dataConfig.type, name: dataConfig.name };
|
|
1565
|
+
const timeoutMs = QUERY_HANDOFF.timeoutMs || DATAJS_LOAD_TIMEOUT_MS;
|
|
1566
|
+
const dataObj = await new Promise((resolve, reject) => {
|
|
1567
|
+
const timer = setTimeout(() => settleQuery(opts, new Error(`[ixmaps-gl] .data({query}): no data handed over (ixmaps.setExternalData) within ${timeoutMs / 1000}s`)), timeoutMs);
|
|
1568
|
+
_pendingQueries.set(opts, { name: dataConfig.name != null ? String(dataConfig.name) : null, resolve, reject, timer });
|
|
1569
|
+
try { queryFn({}, opts); } catch (err) { settleQuery(opts, err); }
|
|
1403
1570
|
});
|
|
1404
1571
|
return rowsResult((dataConfig.process ? runDataProcess(dataObj, dataConfig) : dataObj).json(), binding);
|
|
1405
1572
|
}
|
|
@@ -1421,8 +1588,21 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1421
1588
|
// parser (PapaParse), so the Table arrives via the callback
|
|
1422
1589
|
const tables = await Promise.all(texts.map(dataJsCsvTable));
|
|
1423
1590
|
let table = tables[0];
|
|
1424
|
-
|
|
1425
|
-
|
|
1591
|
+
// records are positional: align each later file's columns to the
|
|
1592
|
+
// first file's by name (a column only a later file has is added),
|
|
1593
|
+
// as the no-data.js path below gets from each file's own header
|
|
1594
|
+
for (const t of tables.slice(1)) {
|
|
1595
|
+
const ids = table.fields.map(f => f.id);
|
|
1596
|
+
const tIds = t.fields.map(f => f.id);
|
|
1597
|
+
if (tIds.length === ids.length && tIds.every((id, c) => id === ids[c])) {
|
|
1598
|
+
table.records = table.records.concat(t.records);
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
t.fields.forEach(f => { if (!ids.includes(f.id)) { ids.push(f.id); table.fields.push(f); } });
|
|
1602
|
+
const src = ids.map(id => tIds.indexOf(id));
|
|
1603
|
+
table.records = table.records.concat(t.records.map(rec => src.map(c => (c < 0 ? undefined : rec[c]))));
|
|
1604
|
+
}
|
|
1605
|
+
if (table.table) { table.table.records = table.records.length; table.table.fields = table.fields.length; }
|
|
1426
1606
|
if (scriptProcess) table = scriptProcess(table);
|
|
1427
1607
|
else if (dataConfig.process) table = runDataProcess(table, dataConfig);
|
|
1428
1608
|
rows = dataTableRows(table);
|
|
@@ -1444,7 +1624,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1444
1624
|
// them into a table with a geometry column, and back)
|
|
1445
1625
|
const resp = await fetch(dataConfig.url);
|
|
1446
1626
|
if (!resp.ok) throw new Error(`[ixmaps-gl] failed to fetch ${dataConfig.url}: ${resp.status}`);
|
|
1447
|
-
if (dataConfig.type === 'topojson') return topojsonToFeatureCollection(await resp.json());
|
|
1627
|
+
if (dataConfig.type === 'topojson') return topojsonToFeatureCollection(await resp.json(), dataConfig.object);
|
|
1448
1628
|
return sanitizeGeoJSON(await resp.json());
|
|
1449
1629
|
}
|
|
1450
1630
|
// every other format (json, jsonstat, ndjson, rss, kml, gml, geobuf,
|
|
@@ -1549,7 +1729,29 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1549
1729
|
// first filter left.
|
|
1550
1730
|
function filterAndJoin(raw, spec, runtimes, expr = spec.filter) {
|
|
1551
1731
|
const f = applyWhereFilter(raw, expr);
|
|
1552
|
-
return f.type === 'Table' ? joinTableFeatures(spec, f, runtimes) : f;
|
|
1732
|
+
return f.type === 'Table' ? joinTableFeatures(spec, f, runtimes) : chartAnchoredFeatures(f, spec);
|
|
1733
|
+
}
|
|
1734
|
+
// a CHART theme over geojson SHAPES (the TELRAAM page binds
|
|
1735
|
+
// lookup:"geometry" — each chart at its own feature's shape): lines and
|
|
1736
|
+
// polygons carry no [lng, lat] position, so every chart pipeline test
|
|
1737
|
+
// (typeof c[0] === 'number', inViewport, ...) drops them silently and
|
|
1738
|
+
// nothing renders. flat positions such a chart at the shape's center
|
|
1739
|
+
// (its ptPos, flatShapeCenter) — normalize the features to Point at
|
|
1740
|
+
// that anchor up front. FEATURE(S)/CHOROPLETH keep their real geometry,
|
|
1741
|
+
// VECTOR/BEZIER keep their lines (their own renderers)
|
|
1742
|
+
function chartAnchoredFeatures(fc, spec) {
|
|
1743
|
+
const chart = fc && fc.type === 'FeatureCollection' && spec.flags && spec.flags.has('CHART')
|
|
1744
|
+
&& !spec.flags.has('FEATURE') && !spec.flags.has('FEATURES')
|
|
1745
|
+
&& !spec.flags.has('CHOROPLETH') && !isVectorChart(spec.flags)
|
|
1746
|
+
&& fc.features.some(f => f && f.geometry && f.geometry.type !== 'Point');
|
|
1747
|
+
if (!chart) return fc;
|
|
1748
|
+
const features = fc.features.map(f => {
|
|
1749
|
+
const t = f && f.geometry && f.geometry.type;
|
|
1750
|
+
if (!t || t === 'Point') return f;
|
|
1751
|
+
const anchor = flatShapeCenter(f.geometry);
|
|
1752
|
+
return anchor ? { type: 'Feature', geometry: { type: 'Point', coordinates: anchor }, properties: f.properties } : f;
|
|
1753
|
+
});
|
|
1754
|
+
return { type: 'FeatureCollection', features };
|
|
1553
1755
|
}
|
|
1554
1756
|
function applyWhereFilter(fc, filterExpr) {
|
|
1555
1757
|
if (!filterExpr) return fc;
|
|
@@ -1658,6 +1860,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1658
1860
|
// each layer where its shape is found (maptheme.js 9324, 10376); records
|
|
1659
1861
|
// with no shape are left out (10387-10392)
|
|
1660
1862
|
//
|
|
1863
|
+
// A POSITION theme is a layer's id→position PROVIDER: flat loads it, draws
|
|
1864
|
+
// nothing, and only registers its records as themeNodesPosA entries
|
|
1865
|
+
// (maptheme.js 8328-8353 — "don't create SVG objects but only entries in
|
|
1866
|
+
// map.Themes.themeNodesPosA[]"), the same registry getNodePosition reads
|
|
1867
|
+
// — so here it takes part in the join index exactly like a FEATURE layer
|
|
1868
|
+
// of center points, its binding.id against the consumer's lookup key
|
|
1869
|
+
//
|
|
1661
1870
|
// flat runs the after-aggregation steps after EACH layer's pass, over all
|
|
1662
1871
|
// items; DIFFERENCE turns a record's value fields into their difference
|
|
1663
1872
|
// and drops the last, so a second pass leaves the earlier layers' items
|
|
@@ -1672,7 +1881,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1672
1881
|
const features = [];
|
|
1673
1882
|
const indexes = layers.map(layer => {
|
|
1674
1883
|
const index = new Map();
|
|
1675
|
-
runtimes.filter(r => r.name === layer && (r.flags.has('FEATURE') || r.flags.has('FEATURES'))).forEach(r => {
|
|
1884
|
+
runtimes.filter(r => r.name === layer && (r.flags.has('FEATURE') || r.flags.has('FEATURES') || r.flags.has('POSITION'))).forEach(r => {
|
|
1676
1885
|
// in theme order, as flat's layer holds the themes' shapes
|
|
1677
1886
|
for (const [k, v] of featurePositionIndex(r)) { if (flatShapeWins(index.get(k), v.area)) index.set(k, v); }
|
|
1678
1887
|
});
|
|
@@ -1759,6 +1968,25 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1759
1968
|
return Math.min(0.9, o);
|
|
1760
1969
|
}
|
|
1761
1970
|
|
|
1971
|
+
// flat's DOPACITY formula inside the COMPOSECOLOR branch (maptheme.js
|
|
1972
|
+
// 13489-13505): (dopacityscale || 1) · (v/max)^(1/dopacitypow) — no 0.9
|
|
1973
|
+
// cap and no 0.0001 clip here, unlike the DOMINANT branch: flat writes
|
|
1974
|
+
// the raw SVG stroke-/fill-opacity property and the browser clamps at 1
|
|
1975
|
+
function composedDopacityAlpha(style, flags, v, max) {
|
|
1976
|
+
const scale = styleNum(style.dopacityscale) || 1;
|
|
1977
|
+
const pow = 1 / (styleNum(style.dopacitypow) || 1);
|
|
1978
|
+
const o = scale * Math.pow(v, pow) / Math.pow(max, pow);
|
|
1979
|
+
return isFinite(o) ? Math.max(0, Math.min(1, o)) : 0;
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
// a choropleth shape's outline color: a LINE shape strokes with its value
|
|
1983
|
+
// color; any other shape keeps the theme's outline, hidden while a class
|
|
1984
|
+
// is isolated and the shape is not in it
|
|
1985
|
+
function choroplethOutlineColor(isLine, valueColorOf, d, isolating, unmarkedEvidence, lineRgb) {
|
|
1986
|
+
if (isLine) return valueColorOf(d);
|
|
1987
|
+
return isolating && unmarkedEvidence(d) === 'hide' ? [0, 0, 0, 0] : lineRgb;
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1762
1990
|
function joinChoroplethFeatures(spec, table, runtimes) {
|
|
1763
1991
|
// a page can define several FEATURE layers of one name — e.g. the
|
|
1764
1992
|
// comuni as polygons and as center points (for chart positions); flat
|
|
@@ -1795,21 +2023,25 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1795
2023
|
if (isAggregatedCategoricalChoropleth(spec)) {
|
|
1796
2024
|
const groups = new Map();
|
|
1797
2025
|
for (const row of table.rows) {
|
|
1798
|
-
|
|
2026
|
+
if (row[lookupField] == null) continue;
|
|
2027
|
+
const key = flatLookupKey(row[lookupField], spec.style);
|
|
1799
2028
|
if (!groups.has(key)) groups.set(key, []);
|
|
1800
2029
|
groups.get(key).push(row);
|
|
1801
2030
|
}
|
|
1802
2031
|
return {
|
|
1803
2032
|
type: 'FeatureCollection',
|
|
1804
2033
|
features: geomRt.features.map(f => {
|
|
1805
|
-
const rows = groups.get(String(f.properties[idField]));
|
|
2034
|
+
const rows = f.properties[idField] == null ? null : groups.get(String(f.properties[idField]));
|
|
1806
2035
|
const properties = rows ? Object.assign({}, rows[0]) : {};
|
|
1807
2036
|
if (rows) Object.defineProperty(properties, AGGREGATED_ROWS, { value: rows });
|
|
1808
2037
|
return { type: 'Feature', geometry: f.geometry, properties, _flatAreaKm2: flatShapeAreaKm2(geomRt, f) };
|
|
1809
2038
|
})
|
|
1810
2039
|
};
|
|
1811
2040
|
}
|
|
1812
|
-
|
|
2041
|
+
// the data side's key as flat makes it (lookupdigits/toupper/tonumber),
|
|
2042
|
+
// the geometry id as is — the same rule as joinChartPositions
|
|
2043
|
+
// a missing key on either side joins nothing (not "undefined" to "undefined")
|
|
2044
|
+
const rowsByKey = new Map(table.rows.filter(row => row[lookupField] != null).map(row => [flatLookupKey(row[lookupField], spec.style), row]));
|
|
1813
2045
|
// Unmatched polygons (no CSV row for that id) keep their geometry with
|
|
1814
2046
|
// empty properties — real-world data: not every comune necessarily has
|
|
1815
2047
|
// a row in every demographic source. Renders as "no data" (see
|
|
@@ -1818,7 +2050,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1818
2050
|
const features = geomRt.features.map(f => ({
|
|
1819
2051
|
type: 'Feature',
|
|
1820
2052
|
geometry: f.geometry,
|
|
1821
|
-
properties: rowsByKey.get(String(f.properties[idField])) || {},
|
|
2053
|
+
properties: (f.properties[idField] != null && rowsByKey.get(String(f.properties[idField]))) || {},
|
|
1822
2054
|
_flatAreaKm2: flatShapeAreaKm2(geomRt, f)
|
|
1823
2055
|
}));
|
|
1824
2056
|
return { type: 'FeatureCollection', features };
|
|
@@ -2091,6 +2323,36 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2091
2323
|
// colorscheme (isGeneratedColorScheme above), never as the step count.
|
|
2092
2324
|
function resolveColorScheme(colorscheme, labels, classes) {
|
|
2093
2325
|
if (!colorscheme) return labels.map((_, i) => FALLBACK_PALETTE[i % FALLBACK_PALETTE.length]);
|
|
2326
|
+
// flat's custom per-value color function (maptheme.js 12677-12705):
|
|
2327
|
+
// colorscheme: ["byvalue", function(value){ return color; }] — every
|
|
2328
|
+
// known category value gets its color from the function and the
|
|
2329
|
+
// result REPLACES the whole scheme (so neither classes nor the
|
|
2330
|
+
// generated-sweep machinery below ever see it, as in flat). A
|
|
2331
|
+
// function that throws, or one a non-categorical theme hands no
|
|
2332
|
+
// values to iterate, falls back to the default palette — exactly
|
|
2333
|
+
// flat's own two fallbacks. The function may also arrive serialized
|
|
2334
|
+
// (a project-JSON string), like the whole-scheme string function
|
|
2335
|
+
// below evaluates one.
|
|
2336
|
+
if (Array.isArray(colorscheme) && colorscheme.length > 1 && colorscheme[0] === 'byvalue') {
|
|
2337
|
+
let fnByValue = null;
|
|
2338
|
+
if (typeof colorscheme[1] === 'function') fnByValue = colorscheme[1];
|
|
2339
|
+
else if (typeof colorscheme[1] === 'string') {
|
|
2340
|
+
try {
|
|
2341
|
+
const f = new Function('return (' + colorscheme[1] + ')')();
|
|
2342
|
+
if (typeof f === 'function') fnByValue = f;
|
|
2343
|
+
} catch (err) { /* not code */ }
|
|
2344
|
+
}
|
|
2345
|
+
if (fnByValue) {
|
|
2346
|
+
try {
|
|
2347
|
+
const byValueA = labels.map(l => fnByValue(l));
|
|
2348
|
+
if (byValueA.length) return byValueA;
|
|
2349
|
+
console.warn('[ixmaps-gl] colorscheme "byvalue" function needs a categorical theme with known values — using the fallback palette');
|
|
2350
|
+
} catch (err) {
|
|
2351
|
+
console.warn('[ixmaps-gl] colorscheme "byvalue" function threw "' + err + '" — using the fallback palette');
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
return labels.map((_, i) => FALLBACK_PALETTE[i % FALLBACK_PALETTE.length]);
|
|
2355
|
+
}
|
|
2094
2356
|
colorscheme = applyClassesToColorScheme(colorscheme, classes);
|
|
2095
2357
|
if (isGeneratedColorScheme(colorscheme)) {
|
|
2096
2358
|
const [, cc1, cc2, nParam1, nParam2] = colorscheme;
|
|
@@ -2196,15 +2458,22 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2196
2458
|
// mean — see the NOOUTLIER block in _buildChartLayers) has real
|
|
2197
2459
|
// behavior now; bare OUTLIER (the real source's "keep ONLY the
|
|
2198
2460
|
// outliers" inverse mode) stays unimplemented — no config here uses it.
|
|
2199
|
-
|
|
2461
|
+
// SEQUENCE/STAR/SORT/DOWN/RECT are implemented now (the sequence
|
|
2462
|
+
// symbol chart, sequenceLayout + snapToAggregationGrid) — removed from
|
|
2463
|
+
// this list; the rest is still recognized-but-inert
|
|
2464
|
+
const KNOWN_INERT_FLAGS = ['CLIPTOGEOBOUNDS', 'OUTLIER',
|
|
2200
2465
|
'POW2', 'POW3', 'NOSORT'];
|
|
2201
|
-
// flat's PIE / DONUT variants with no rendering here (see _buildPieLayers)
|
|
2202
|
-
|
|
2466
|
+
// flat's PIE / DONUT variants with no rendering here (see _buildPieLayers).
|
|
2467
|
+
// STARBURST / FLOWER / RAYS (the starburst rays, with the XLRAYS & kin
|
|
2468
|
+
// angle/gap variants) and POLAR (its reference circles) ARE drawn — see
|
|
2469
|
+
// pieSliceLayout; the rest is still recognized-but-inert
|
|
2470
|
+
const PIE_INERT_FLAGS = [/3D/, /VOLUME/, /HEIGHT/, /BOW/, /ZOOM/, /DIRECTION/, /DIRECTED/];
|
|
2203
2471
|
// ... and of AGGREGATE / CATEGORICAL pies: GRIDSIZE's pie the size of its
|
|
2204
|
-
// grid cell (drawn by the SIZE rule instead), AUTOSIZE, DOPACITY,
|
|
2205
|
-
//
|
|
2206
|
-
// value arithmetic (DIFFERENCE, FRACTION, PERCENT …, field100)
|
|
2207
|
-
|
|
2472
|
+
// grid cell (drawn by the SIZE rule instead), AUTOSIZE, DOPACITY,
|
|
2473
|
+
// GAUGE, MULTIPLE / DIFFUSE, the post-aggregation
|
|
2474
|
+
// value arithmetic (DIFFERENCE, FRACTION, PERCENT …, field100).
|
|
2475
|
+
// The chart BOX and TITLE ARE drawn (see _buildPieLayers)
|
|
2476
|
+
const PIE_AGGREGATE_INERT_FLAGS = [/^GRIDSIZE$/, /AUTOSIZE/, /DOPACITY/, /^GAUGE$/, /^MULTIPLE$/, /^DIFFUSE$/,
|
|
2208
2477
|
/^DIFFERENCE$/, /^FRACTION$/, /^PERCENT$/, /^PERMILLE$/, /^PRODUCT$/, /^CALCVAL$/, /^RELATIVE$/];
|
|
2209
2478
|
// flat's BAR / BARS variants with no rendering here (see flatBarLayout):
|
|
2210
2479
|
// the bar is drawn plain (SMALLARROW as ARROW) — tested as substrings,
|
|
@@ -2214,11 +2483,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2214
2483
|
/^DIFFERENCE$/, /^FRACTION$/, /^PERCENT$/, /^PERMILLE$/, /^PRODUCT$/, /^CALCVAL$/, /^RELATIVE$/];
|
|
2215
2484
|
// flat's VECTOR / BEZIER modifiers with no rendering here (see
|
|
2216
2485
|
// _buildVectorLayers), the word → what happens instead
|
|
2486
|
+
// (DASH IS drawn — the animated flow, see makeFlowPathLayer)
|
|
2217
2487
|
const VECTOR_INERT_FLAGS = {
|
|
2218
2488
|
VALUES: 'the value text along the curve is not drawn yet',
|
|
2219
2489
|
DYNAMICWIDTH: 'not drawn yet (flat widens by the log of the length in its own map units) — the plain width instead',
|
|
2220
|
-
LONG: 'not drawn yet (its bow depends on flat\'s own map units) — the default bow instead'
|
|
2221
|
-
DASH: 'drawn as static dashes — flat\'s dash animation is not ported'
|
|
2490
|
+
LONG: 'not drawn yet (its bow depends on flat\'s own map units) — the default bow instead'
|
|
2222
2491
|
};
|
|
2223
2492
|
const _warnedFlags = new Set();
|
|
2224
2493
|
|
|
@@ -2263,6 +2532,18 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2263
2532
|
// makes while the map is still building wait for it
|
|
2264
2533
|
let _resolveMapReady;
|
|
2265
2534
|
const _mapReady = new Promise(r => { _resolveMapReady = r; });
|
|
2535
|
+
// a map declared with an options name (Map(id, {name})) → the deferred of
|
|
2536
|
+
// ITS build: ixmaps.map("name") calls made while it is still building wait
|
|
2537
|
+
// for that map, not whichever map is built first
|
|
2538
|
+
const _namedMapReady = Object.create(null);
|
|
2539
|
+
function namedMapReady(name) {
|
|
2540
|
+
if (!_namedMapReady[name]) {
|
|
2541
|
+
let resolve;
|
|
2542
|
+
const promise = new Promise(r => { resolve = r; });
|
|
2543
|
+
_namedMapReady[name] = { promise, resolve };
|
|
2544
|
+
}
|
|
2545
|
+
return _namedMapReady[name];
|
|
2546
|
+
}
|
|
2266
2547
|
|
|
2267
2548
|
// flat's ixmaps.require(url) fetches the script and evals it
|
|
2268
2549
|
// (htmlgui.js 3911-3934); here each one runs globally, in order, before
|
|
@@ -2382,7 +2663,16 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2382
2663
|
// key wins. Always into a new object — the page's own objects are never
|
|
2383
2664
|
// mutated. Aliases (geo → position, ...) are resolved in normalizeTheme.
|
|
2384
2665
|
binding(b) {
|
|
2385
|
-
|
|
2666
|
+
// flat's toString joins an array with '|' (maptheme.js 1014): a
|
|
2667
|
+
// binding given as an ARRAY — .binding({value: ["out","in"]}, the
|
|
2668
|
+
// pendolari page's saldo pie) is a MULTI-FIELD binding, not one
|
|
2669
|
+
// field named "out,in" (which would find no column and draw
|
|
2670
|
+
// nothing). Normalized once, here, for every consumer that
|
|
2671
|
+
// splits binding.value on '|'
|
|
2672
|
+
const norm = {};
|
|
2673
|
+
// (an empty or missing argument keeps what earlier calls set)
|
|
2674
|
+
for (const [k, v] of Object.entries(b || {})) norm[k] = Array.isArray(v) ? v.join('|') : v;
|
|
2675
|
+
this._binding = Object.assign({}, this._binding, norm);
|
|
2386
2676
|
return this;
|
|
2387
2677
|
}
|
|
2388
2678
|
filter(expr) { this._filterExpr = expr; return this; }
|
|
@@ -2477,6 +2767,27 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2477
2767
|
return construct;
|
|
2478
2768
|
}
|
|
2479
2769
|
const unwrapLayer = x => (typeof x === 'function' && x.__ixLayer) ? x.__ixLayer : x;
|
|
2770
|
+
// flat's layer builder finalizers — definition(), define() AND json()
|
|
2771
|
+
// (htmlgui_flat.js 2056-2083) — all return the plain DEFINITION object,
|
|
2772
|
+
// so a real page's .layer(ixmaps.layer("x").data()...json()) hands
|
|
2773
|
+
// map.layer() that object rather than the builder. Wrap it back into a
|
|
2774
|
+
// LayerBuilder whose definition() reproduces it (the def's own slots;
|
|
2775
|
+
// its type/filter/title live INSIDE style, where definition() put them)
|
|
2776
|
+
function asLayerBuilder(x) {
|
|
2777
|
+
const lb = unwrapLayer(x);
|
|
2778
|
+
if (lb instanceof LayerBuilder) return lb;
|
|
2779
|
+
if (lb && typeof lb === 'object' && (lb.layer || lb.data || lb.style)) {
|
|
2780
|
+
const b = new LayerBuilder(lb.layer || 'layer');
|
|
2781
|
+
b._data = lb.data || null;
|
|
2782
|
+
b._binding = lb.binding || {};
|
|
2783
|
+
b._style = lb.style || {};
|
|
2784
|
+
b._meta = lb.meta || {};
|
|
2785
|
+
if (lb.field != null) b._field = lb.field;
|
|
2786
|
+
if (lb.field100 != null) b._field100 = lb.field100;
|
|
2787
|
+
return b;
|
|
2788
|
+
}
|
|
2789
|
+
return lb;
|
|
2790
|
+
}
|
|
2480
2791
|
|
|
2481
2792
|
// ---------------------------------------------------------------
|
|
2482
2793
|
// Binding aliases. Real ixmaps-flat accepts ~50 .binding() keys and maps
|
|
@@ -2487,7 +2798,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2487
2798
|
// edit it by hand; `npm run unit` fails when it drifts.
|
|
2488
2799
|
// ---------------------------------------------------------------
|
|
2489
2800
|
// <grammar:binding-aliases>
|
|
2490
|
-
// generated from ixmaps-grammar 0.1.
|
|
2801
|
+
// generated from ixmaps-grammar 0.1.28 (ixmaps-flat 1.0.42, 2b978d5 2026-10-02) — 52 aliases
|
|
2491
2802
|
const FLAT_BINDING_ALIASES = {
|
|
2492
2803
|
"aggregation": "style.aggregationfield",
|
|
2493
2804
|
"aggregationfield": "style.aggregationfield",
|
|
@@ -2549,7 +2860,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2549
2860
|
// both — so .style({tooltip}) is a tooltip and .meta({fillopacity}) a style
|
|
2550
2861
|
// property. GENERATED by test/sync-grammar.mjs — do not edit.
|
|
2551
2862
|
// <grammar:meta-keys>
|
|
2552
|
-
// generated from ixmaps-grammar 0.1.
|
|
2863
|
+
// generated from ixmaps-grammar 0.1.28 — 5 meta keys
|
|
2553
2864
|
const FLAT_META_KEYS = ["description","name","snippet","title","tooltip"];
|
|
2554
2865
|
// </grammar:meta-keys>
|
|
2555
2866
|
|
|
@@ -2651,27 +2962,75 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2651
2962
|
// that kind: the data processing function (data.process /
|
|
2652
2963
|
// style.dbtableProcess), the broker query (data.query /
|
|
2653
2964
|
// style.dbtableQuery — without it a data.name still reaches a
|
|
2654
|
-
// provider the PAGE defined as ixmaps[name]) and a colorscheme
|
|
2655
|
-
//
|
|
2656
|
-
// (
|
|
2965
|
+
// provider the PAGE defined as ixmaps[name]) and a colorscheme
|
|
2966
|
+
// resolveColorScheme would evaluate: a string, or ["byvalue", "<fn>"]
|
|
2967
|
+
// (any other array colorscheme is data and stays). normalizeTheme
|
|
2968
|
+
// merges meta INTO style, so every style key is checked under meta too.
|
|
2969
|
+
const isColorSchemeCode = v => typeof v === 'string'
|
|
2970
|
+
|| (Array.isArray(v) && v[0] === 'byvalue' && typeof v[1] === 'string');
|
|
2657
2971
|
const PROJECT_CODE_KEYS = [
|
|
2658
2972
|
['data', 'process', 'data.process function'],
|
|
2659
2973
|
['style', 'dbtableProcess', 'data.process function'],
|
|
2660
2974
|
['data', 'query', 'data.query function'],
|
|
2661
2975
|
['style', 'dbtableQuery', 'data.query function'],
|
|
2662
|
-
['style', 'colorscheme', 'colorscheme function',
|
|
2663
|
-
];
|
|
2976
|
+
['style', 'colorscheme', 'colorscheme function', isColorSchemeCode],
|
|
2977
|
+
].flatMap(k => (k[0] === 'style' ? [k, ['meta', ...k.slice(1)]] : [k]));
|
|
2978
|
+
// A project's legend texts (title, snippet, description — in style or
|
|
2979
|
+
// meta, normalizeTheme fills meta from either) are shown as HTML, as on
|
|
2980
|
+
// flat: citation links in a description are real. A project's markup is
|
|
2981
|
+
// cleaned to formatting tags and plain attributes so nothing in it runs;
|
|
2982
|
+
// a page's own .meta()/.style() HTML stays as given.
|
|
2983
|
+
const PROJECT_HTML_KEYS = ['title', 'snippet', 'description'];
|
|
2984
|
+
const PROJECT_HTML_DROP = new Set(['SCRIPT', 'STYLE', 'IFRAME', 'FRAME', 'FRAMESET', 'OBJECT', 'EMBED', 'SVG', 'MATH',
|
|
2985
|
+
'TEMPLATE', 'NOSCRIPT', 'LINK', 'META', 'BASE', 'FORM', 'INPUT', 'BUTTON', 'SELECT', 'TEXTAREA', 'AUDIO', 'VIDEO', 'SOURCE']);
|
|
2986
|
+
const PROJECT_HTML_TAGS = new Set(['A', 'B', 'I', 'EM', 'STRONG', 'U', 'S', 'SMALL', 'SUB', 'SUP', 'BR', 'P', 'DIV', 'SPAN',
|
|
2987
|
+
'UL', 'OL', 'LI', 'HR', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'IMG', 'TABLE', 'THEAD', 'TBODY', 'TR', 'TD', 'TH', 'CODE', 'BLOCKQUOTE']);
|
|
2988
|
+
const PROJECT_HTML_ATTRS = new Set(['href', 'src', 'alt', 'title', 'target', 'rel', 'class', 'style', 'width', 'height', 'colspan', 'rowspan']);
|
|
2989
|
+
function sanitizeProjectHtml(html) {
|
|
2990
|
+
const s = String(html);
|
|
2991
|
+
if (!/[<&]/.test(s)) return s;
|
|
2992
|
+
// no DOM to parse with (Node): the text, escaped
|
|
2993
|
+
if (typeof global.DOMParser !== 'function') return escapeHtml(s);
|
|
2994
|
+
// an inert document: nothing in it runs or loads while it is cleaned
|
|
2995
|
+
const body = new global.DOMParser().parseFromString('<!doctype html><body>' + s, 'text/html').body;
|
|
2996
|
+
for (const el of Array.from(body.querySelectorAll('*'))) {
|
|
2997
|
+
if (PROJECT_HTML_DROP.has(el.tagName.toUpperCase())) { el.remove(); continue; }
|
|
2998
|
+
if (!PROJECT_HTML_TAGS.has(el.tagName.toUpperCase())) { el.replaceWith(...el.childNodes); continue; }
|
|
2999
|
+
for (const a of Array.from(el.attributes)) {
|
|
3000
|
+
const name = a.name.toLowerCase();
|
|
3001
|
+
if (!PROJECT_HTML_ATTRS.has(name)
|
|
3002
|
+
|| ((name === 'href' || name === 'src') && /^(javascript|vbscript|data):/i.test(a.value.replace(/[\s\u0000-\u001f]/g, '')))) {
|
|
3003
|
+
el.removeAttribute(a.name);
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
return body.innerHTML;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
2664
3010
|
function withoutProjectCode(t, report) {
|
|
2665
3011
|
const found = PROJECT_CODE_KEYS.filter(([part, key, , isCode]) =>
|
|
2666
3012
|
t[part] && t[part][key] != null && (!isCode || isCode(t[part][key])));
|
|
2667
|
-
|
|
3013
|
+
const html = [];
|
|
3014
|
+
for (const part of ['style', 'meta']) {
|
|
3015
|
+
for (const key of PROJECT_HTML_KEYS) {
|
|
3016
|
+
const v = t[part] && t[part][key];
|
|
3017
|
+
if (typeof v !== 'string') continue;
|
|
3018
|
+
const clean = sanitizeProjectHtml(v);
|
|
3019
|
+
if (clean !== v) html.push([part, key, clean]);
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
if (!found.length && !html.length) return t;
|
|
2668
3023
|
const copy = Object.assign({}, t);
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
3024
|
+
const own = part => { if (copy[part] === t[part]) copy[part] = Object.assign({}, t[part]); return copy[part]; };
|
|
3025
|
+
found.forEach(([part, key]) => { delete own(part)[key]; });
|
|
3026
|
+
html.forEach(([part, key, clean]) => { own(part)[key] = clean; });
|
|
3027
|
+
if (found.length) {
|
|
3028
|
+
const what = [...new Set(found.map(f => f[2]))].join(', ');
|
|
3029
|
+
report.notes.push(`theme "${t.layer}": the project's ${what} is never run by ixmaps-gl`);
|
|
3030
|
+
}
|
|
3031
|
+
if (html.length) {
|
|
3032
|
+
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`);
|
|
3033
|
+
}
|
|
2675
3034
|
return copy;
|
|
2676
3035
|
}
|
|
2677
3036
|
|
|
@@ -2905,6 +3264,15 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2905
3264
|
}
|
|
2906
3265
|
|
|
2907
3266
|
typeStyleNumbers(style);
|
|
3267
|
+
// flat's own fill-opacity fallback (maptheme.js 13406:
|
|
3268
|
+
// `this.fillOpacity = this.fillOpacity ? this.fillOpacity : this.nOpacity`)
|
|
3269
|
+
// — an "opacity" style is the fill opacity whenever "fillopacity" is
|
|
3270
|
+
// not set, as flat realizes every theme
|
|
3271
|
+
// (Number("") and Number(null) are 0: an empty opacity must not hide the theme)
|
|
3272
|
+
if (!styleNum(style.fillopacity) && style.opacity != null && String(style.opacity).trim() !== ''
|
|
3273
|
+
&& Number.isFinite(Number(style.opacity))) {
|
|
3274
|
+
style.fillopacity = Number(style.opacity);
|
|
3275
|
+
}
|
|
2908
3276
|
return { name: def.layer, data: def.data, binding, targets, geometry, flags, typeStr, style, meta, filter };
|
|
2909
3277
|
}
|
|
2910
3278
|
|
|
@@ -3061,7 +3429,58 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3061
3429
|
light: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
|
|
3062
3430
|
dark: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
|
|
3063
3431
|
};
|
|
3432
|
+
// flat's own raster-tile basemaps (htmlgui_sync_Leaflet.js __addTileLayer
|
|
3433
|
+
// list, ArcGIS block): a mapType NAME flat resolves to real raster tiles
|
|
3434
|
+
// resolves to the SAME tiles here, as a built MapLibre raster style —
|
|
3435
|
+
// "ArcGIS - Topo" was falling through to CARTO Positron before, a
|
|
3436
|
+
// non-topographic look nothing like flat's. Styles are built once per
|
|
3437
|
+
// name, so the same name is the SAME style object and setMapType's
|
|
3438
|
+
// "same style: nothing to reload" identity test keeps working.
|
|
3439
|
+
const ESRI_ATTRIBUTION = 'Tiles © Esri — Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community';
|
|
3440
|
+
const FLAT_RASTER_BASEMAPS = [
|
|
3441
|
+
{ name: 'ArcGIS - Topo', aliases: ['terrain'], maxzoom: 19,
|
|
3442
|
+
tiles: 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}.png' },
|
|
3443
|
+
{ name: 'ArcGIS - Light Gray Base', maxzoom: 19,
|
|
3444
|
+
tiles: 'https://services.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}.png' },
|
|
3445
|
+
{ name: 'ArcGIS - Ocean Basemap', maxzoom: 13,
|
|
3446
|
+
tiles: 'https://services.arcgisonline.com/ArcGIS/rest/services/Ocean_Basemap/MapServer/tile/{z}/{y}/{x}.png' },
|
|
3447
|
+
{ name: 'ArcGIS - Hillshade', maxzoom: 16,
|
|
3448
|
+
tiles: 'https://server.arcgisonline.com/arcgis/rest/services/Elevation/World_Hillshade/MapServer/tile/{z}/{y}/{x}.png' },
|
|
3449
|
+
];
|
|
3450
|
+
const _rasterBasemapStyles = new Map();
|
|
3451
|
+
function buildRasterBasemapStyle(entry) {
|
|
3452
|
+
if (_rasterBasemapStyles.has(entry.name)) return _rasterBasemapStyles.get(entry.name);
|
|
3453
|
+
const sourceId = 'ixmaps-raster-basemap';
|
|
3454
|
+
const style = {
|
|
3455
|
+
version: 8,
|
|
3456
|
+
sources: {
|
|
3457
|
+
[sourceId]: {
|
|
3458
|
+
type: 'raster',
|
|
3459
|
+
tiles: [entry.tiles],
|
|
3460
|
+
tileSize: 256,
|
|
3461
|
+
maxzoom: entry.maxzoom,
|
|
3462
|
+
attribution: ESRI_ATTRIBUTION
|
|
3463
|
+
}
|
|
3464
|
+
},
|
|
3465
|
+
// raster-opacity (applyBasemapOpacity's raster entry) fades this
|
|
3466
|
+
// layer like any other basemap
|
|
3467
|
+
layers: [{ id: sourceId, type: 'raster', source: sourceId, paint: {} }]
|
|
3468
|
+
};
|
|
3469
|
+
_rasterBasemapStyles.set(entry.name, style);
|
|
3470
|
+
return style;
|
|
3471
|
+
}
|
|
3472
|
+
// flat's mapTypeTranslate ("terrain" → "ArcGIS - Topo", ...) applies to
|
|
3473
|
+
// the raster names too
|
|
3474
|
+
function flatRasterBasemap(mapType) {
|
|
3475
|
+
if (typeof mapType !== 'string') return null;
|
|
3476
|
+
const t = mapType.trim();
|
|
3477
|
+
for (const entry of FLAT_RASTER_BASEMAPS) {
|
|
3478
|
+
if (t === entry.name || (entry.aliases || []).includes(t)) return entry;
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3064
3481
|
function resolveBasemapStyleUrl(mapType) {
|
|
3482
|
+
const raster = flatRasterBasemap(mapType);
|
|
3483
|
+
if (raster) return buildRasterBasemapStyle(raster);
|
|
3065
3484
|
return typeof mapType === 'string' && /dark|black|night|matter/i.test(mapType) ? BASEMAP_STYLE_URLS.dark : BASEMAP_STYLE_URLS.light;
|
|
3066
3485
|
}
|
|
3067
3486
|
// Real ixmaps-flat's legend look (tools/legend.js 3773-3845, via
|
|
@@ -3170,6 +3589,32 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3170
3589
|
return false;
|
|
3171
3590
|
}
|
|
3172
3591
|
|
|
3592
|
+
// The ocean of flat's world / orthographic maps is a {type:"Sphere"} feature: the whole map disc,
|
|
3593
|
+
// filled with the layer's color. Here it is the surface of the map (the globe, or the flat
|
|
3594
|
+
// map): a background layer under everything, in that color and opacity.
|
|
3595
|
+
const SPHERE_BACKDROP_LAYER_ID = '__ixmaps_gl_sphere_backdrop';
|
|
3596
|
+
function applySphereBackdrop(map, style) {
|
|
3597
|
+
const cs = style && style.colorscheme;
|
|
3598
|
+
const color = Array.isArray(cs) ? cs[0] : cs;
|
|
3599
|
+
if (!color || color === 'none') return;
|
|
3600
|
+
const opacity = Number.isFinite(styleNum(style.fillopacity)) ? styleNum(style.fillopacity) : 1;
|
|
3601
|
+
const paint = () => {
|
|
3602
|
+
try {
|
|
3603
|
+
const id = map.getLayer(BLANK_BACKGROUND_LAYER_ID) ? BLANK_BACKGROUND_LAYER_ID : SPHERE_BACKDROP_LAYER_ID;
|
|
3604
|
+
if (id === SPHERE_BACKDROP_LAYER_ID && !map.getLayer(id)) {
|
|
3605
|
+
const first = (map.getStyle().layers || [])[0];
|
|
3606
|
+
map.addLayer({ id, type: 'background', paint: {} }, first && first.id);
|
|
3607
|
+
}
|
|
3608
|
+
map.setPaintProperty(id, 'background-color', color);
|
|
3609
|
+
map.setPaintProperty(id, 'background-opacity', opacity);
|
|
3610
|
+
} catch (e) { console.warn('[ixmaps-gl] the {type:"Sphere"} ocean backdrop could not be set:', e); }
|
|
3611
|
+
};
|
|
3612
|
+
// isStyleLoaded() is also false AFTER the style loaded while tiles or
|
|
3613
|
+
// images load (every pan/zoom) — 'style.load' never fires again then, so
|
|
3614
|
+
// a Sphere theme defined at such a moment would never get its color
|
|
3615
|
+
if (map.isStyleLoaded() || (map.style && map.style._loaded)) paint(); else map.once('style.load', paint);
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3173
3618
|
function buildBlankBackgroundStyle(color) {
|
|
3174
3619
|
return {
|
|
3175
3620
|
version: 8,
|
|
@@ -3316,7 +3761,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3316
3761
|
// visible (an element larger than the viewport ends up at its edge)
|
|
3317
3762
|
function keepTooltipVisible(tipEl, x, y) {
|
|
3318
3763
|
const w = tipEl.offsetWidth, h = tipEl.offsetHeight;
|
|
3319
|
-
|
|
3764
|
+
// x/y and the translate are relative to the map container (it clips,
|
|
3765
|
+
// overflow:hidden): its size is the room, not the window's — they
|
|
3766
|
+
// differ on an embedded map
|
|
3767
|
+
const vw = el.clientWidth || global.innerWidth || 1200, vh = el.clientHeight || global.innerHeight || 800;
|
|
3320
3768
|
let tx = x > vw / 2 ? x - w - 30 : x + 30;
|
|
3321
3769
|
let ty = y > vh / 2 ? y - h - 20 : y + 20;
|
|
3322
3770
|
tx = Math.min(Math.max(8, tx), Math.max(8, vw - w - 8));
|
|
@@ -3659,6 +4107,14 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3659
4107
|
// explicit request (real engine's own fallback is right-anchored
|
|
3660
4108
|
// too, just without an opinion on vertical placement).
|
|
3661
4109
|
const legendAlignCss = legendAlignCssFor(builder.mapOptions);
|
|
4110
|
+
// flat's align:"center"/"top" legend realizing (htmlgui_flat.js
|
|
4111
|
+
// 445-480, legend.css 516-550): not a floating corner panel but a
|
|
4112
|
+
// full-width bar over the map's top edge — width 100% capped at
|
|
4113
|
+
// legendMaxWidth (1200px), legendBackground (default
|
|
4114
|
+
// rgba(255,255,255,0.9)), rounded BOTTOM corners only, the title
|
|
4115
|
+
// centered above, the class chips inline-block side by side and
|
|
4116
|
+
// the chart-size slider inline in that same row
|
|
4117
|
+
const legendCenter = /^(center|top)$/i.test(String(builder.mapOptions.align || '').trim());
|
|
3662
4118
|
// Real ixmaps-flat's SUB-THEME (MapTheme.createSubTheme,
|
|
3663
4119
|
// maptheme.js 14284-14292 / 15010-15160; map.Themes.enableSubThemes
|
|
3664
4120
|
// defaults to true): marking fields of a multi-field choropleth
|
|
@@ -3728,7 +4184,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3728
4184
|
if (legendStack && legendStack.parentNode) return legendStack;
|
|
3729
4185
|
legendStack = document.createElement('div');
|
|
3730
4186
|
legendStack.className = 'ix-legend-stack';
|
|
3731
|
-
legendStack.style.cssText =
|
|
4187
|
+
legendStack.style.cssText = legendCenter
|
|
4188
|
+
// flat's center legend: the full-width bar, the panel carries
|
|
4189
|
+
// the background (no stack-level radius or shadow)
|
|
4190
|
+
? 'position:absolute;left:50%;transform:translateX(-50%);top:0;z-index:6;width:100%;max-width:1200px;'
|
|
4191
|
+
+ 'display:flex;flex-direction:column;pointer-events:none;'
|
|
4192
|
+
: 'position:absolute;' + legendAlignCss + 'z-index:6;width:280px;'
|
|
3732
4193
|
+ 'display:flex;flex-direction:column;max-height:66.6%;overflow-y:auto;'
|
|
3733
4194
|
+ 'border-radius:6px;box-shadow:' + shadow + ';pointer-events:auto;';
|
|
3734
4195
|
if (!document.getElementById('ix-legend-stack-css')) {
|
|
@@ -3838,7 +4299,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3838
4299
|
const looksDark = typeof builder.__effectiveDark === 'function'
|
|
3839
4300
|
? builder.__effectiveDark(isNaN(opacityOpt) ? 1 : opacityOpt, builder.mapOptions.mapType, legendBackground)
|
|
3840
4301
|
: flatLook.dark;
|
|
3841
|
-
const isLightLegend = rt.style.legendtheme === 'light' || (rt.style.legendtheme !== 'dark' && !looksDark);
|
|
4302
|
+
const isLightLegend = legendCenter || rt.style.legendtheme === 'light' || (rt.style.legendtheme !== 'dark' && !looksDark);
|
|
3842
4303
|
const colors = isLightLegend
|
|
3843
4304
|
? { bg: 'rgba(255,255,255,0.92)', fg: '#1a1a1a', shadow: '0 2px 10px rgba(0,0,0,0.18)',
|
|
3844
4305
|
rowMarked: 'rgba(0,0,0,0.08)', track: 'rgba(0,0,0,0.08)',
|
|
@@ -3849,7 +4310,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3849
4310
|
// the panel bg follows the same effective decision (flatLegendLook
|
|
3850
4311
|
// would hand the dark look a light bg here: its dark branch never
|
|
3851
4312
|
// ran for this map, its light bg would clash with dark rows/text)
|
|
3852
|
-
if (!rt.style.legendtheme) colors.bg = looksDark ? '#111' : flatLook.bg;
|
|
4313
|
+
if (!rt.style.legendtheme && !legendCenter) colors.bg = looksDark ? '#111' : flatLook.bg;
|
|
3853
4314
|
return colors;
|
|
3854
4315
|
};
|
|
3855
4316
|
const legendColors = lookColors();
|
|
@@ -3867,7 +4328,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3867
4328
|
// one box for every theme, as flat's #map-legend (legend.js
|
|
3868
4329
|
// 2200-2460): the panels stack in the legend stack in theme
|
|
3869
4330
|
// order, a line between them (see layoutLegends)
|
|
3870
|
-
panel.style.cssText =
|
|
4331
|
+
panel.style.cssText = legendCenter
|
|
4332
|
+
// flat's center legend bar: full width, centered text, the
|
|
4333
|
+
// light legendBackground, rounded bottom corners only
|
|
4334
|
+
? 'position:relative;display:flex;flex-direction:column;text-align:center;'
|
|
4335
|
+
+ 'background:' + legendColors.bg + ';color:' + legendColors.fg + ';font:12px/1.4 -apple-system,Arial,sans-serif;'
|
|
4336
|
+
+ 'padding:6px 12px 10px;pointer-events:auto;border-radius:0 0 6px 6px;'
|
|
4337
|
+
: 'position:relative;flex:0 1 auto;min-height:0;overflow:hidden;'
|
|
3871
4338
|
+ 'display:flex;flex-direction:column;text-align:left;'
|
|
3872
4339
|
+ 'background:' + legendColors.bg + ';color:' + legendColors.fg + ';font:12px/1.4 -apple-system,Arial,sans-serif;'
|
|
3873
4340
|
+ 'padding:10px 12px 12px;pointer-events:auto;';
|
|
@@ -3897,13 +4364,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3897
4364
|
// twin pages)
|
|
3898
4365
|
const flatUnitLegend = rt._isVectorChart() || rt._isAggregatedPie();
|
|
3899
4366
|
const rawLegendUnit = rt.style.legendunits || rt.style.units || '';
|
|
3900
|
-
|
|
4367
|
+
// COUNT: the rows are record counts — flat prints them as whole numbers
|
|
4368
|
+
// with no unit (legend.js 705, 810: __formatValue(count, nDecimals,
|
|
4369
|
+
// "BLANK") + (COUNT ? "" : " " + unit))
|
|
4370
|
+
const countLegend = flatFlag(rt.flags, 'COUNT') && !useSum;
|
|
4371
|
+
const legendUnit = countLegend ? '' : flatUnitLegend && rawLegendUnit && rawLegendUnit[0] !== '.' && rawLegendUnit[0] !== ' ' ? ' ' + rawLegendUnit : rawLegendUnit;
|
|
3901
4372
|
// a per-item chart over several value fields: the row values as
|
|
3902
4373
|
// flat's legend prints them (legend.js 610, 810: __formatValue
|
|
3903
4374
|
// with valuedecimals, else 2, "BLANK" — whole numbers without
|
|
3904
4375
|
// decimals), a colorscheme "none" part as an outline in linecolor
|
|
3905
4376
|
const multiFieldLegend = rt._isMultiFieldChart();
|
|
3906
|
-
const legendTotalText = v => ((multiFieldLegend || flatUnitLegend)
|
|
4377
|
+
const legendTotalText = v => ((multiFieldLegend || flatUnitLegend || countLegend)
|
|
3907
4378
|
? flatFormatValue(v, legendDecimals(rt.style), 'BLANK')
|
|
3908
4379
|
: rt._formatTooltipValue(v));
|
|
3909
4380
|
const outlineRgb = rt.style.linecolor && styleLineColor(rt.style.linecolor) !== 'none' ? hexOrNamedToRgb(styleLineColor(rt.style.linecolor)) : [128, 128, 128];
|
|
@@ -3937,6 +4408,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3937
4408
|
// branch uses (no antimeridian wraparound handling — matches
|
|
3938
4409
|
// that existing convention, not a new gap this introduces).
|
|
3939
4410
|
let totals, maxTotal, order, rowsScroll;
|
|
4411
|
+
// hoisted so the chart-size slider below can join the chips row
|
|
4412
|
+
// in flat's center legend (legend-top-align's inline slider)
|
|
4413
|
+
let rowsEl = null;
|
|
3940
4414
|
// No-op default: TEXTLEGEND never reassigns this (see below),
|
|
3941
4415
|
// so the LATER unconditional-looking `renderRows()` call
|
|
3942
4416
|
// safely does nothing rather than needing its own isTextOnly
|
|
@@ -3993,8 +4467,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3993
4467
|
// share, then overflow-y:auto scrolls just the row list) —
|
|
3994
4468
|
// the header above and description/slider below stay put.
|
|
3995
4469
|
rowsScroll = document.createElement('div');
|
|
3996
|
-
rowsScroll.style.cssText =
|
|
3997
|
-
|
|
4470
|
+
rowsScroll.style.cssText = legendCenter
|
|
4471
|
+
// flat's center bar: one wrapped, centered row holding the
|
|
4472
|
+
// chips AND the inline chart-size slider (legend.css 521-550)
|
|
4473
|
+
? 'flex:0 1 auto;display:flex;flex-wrap:wrap;justify-content:center;align-items:center;'
|
|
4474
|
+
: 'flex:1 1 auto;min-height:0;overflow-y:auto;';
|
|
4475
|
+
rowsEl = document.createElement('div');
|
|
3998
4476
|
rowsEl.className = 'ix-legend-rows';
|
|
3999
4477
|
// .type("...|SIMPLELEGEND") — real ixmaps-flat's OTHER legend
|
|
4000
4478
|
// variant (confirmed against a real-engine screenshot, per
|
|
@@ -4019,7 +4497,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4019
4497
|
// smaller categories packed 3-4 per row) rather than a fixed
|
|
4020
4498
|
// column count.
|
|
4021
4499
|
if (rt.flags.has('SIMPLELEGEND') || rt.flags.has('COMPACTLEGEND')) {
|
|
4022
|
-
|
|
4500
|
+
// centered: inline-flex so the chips shrink to content inside
|
|
4501
|
+
// the wrapping centered row (the slider joins as a sibling)
|
|
4502
|
+
rowsEl.style.cssText = legendCenter ? 'display:inline-flex;flex-wrap:wrap;gap:4px;'
|
|
4503
|
+
: 'display:flex;flex-wrap:wrap;gap:4px;';
|
|
4023
4504
|
}
|
|
4024
4505
|
rowsScroll.appendChild(rowsEl);
|
|
4025
4506
|
|
|
@@ -4196,7 +4677,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4196
4677
|
// same 400ms as the facets sidebar's own onRedraw use), same
|
|
4197
4678
|
// signal already used elsewhere in this file for "the visible
|
|
4198
4679
|
// data just changed, recompute".
|
|
4199
|
-
|
|
4680
|
+
// a panel replaced (rebuild, legend off) or a removed theme: the
|
|
4681
|
+
// listener goes with it — else one per rebuild piles up, each
|
|
4682
|
+
// recomputing a stale runtime on every redraw
|
|
4683
|
+
const offRedraw = engineApi.onRedraw(() => {
|
|
4684
|
+
if (rt._legendPanel !== panel || !runtimes.includes(rt)) { offRedraw(); return; }
|
|
4685
|
+
computeTotals(); renderRows();
|
|
4686
|
+
});
|
|
4200
4687
|
}
|
|
4201
4688
|
|
|
4202
4689
|
// rt._triggerRedraw: see its own doc comment on LayerRuntime
|
|
@@ -4329,6 +4816,101 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4329
4816
|
bodyEl.appendChild(desc);
|
|
4330
4817
|
}
|
|
4331
4818
|
|
|
4819
|
+
// flat's TIME SLIDER (legend.js 2488-2530, the szTimeField
|
|
4820
|
+
// branch of makeColorLegendHTMLLong): a theme with a timefield
|
|
4821
|
+
// (style.timefield, resolved into binding.time) gets a slider
|
|
4822
|
+
// over its records' time span — at the span's start every
|
|
4823
|
+
// record shows (setTimeFrame's own start-of-frame rule),
|
|
4824
|
+
// elsewhere a [value, value+range) window. The window size is
|
|
4825
|
+
// flat's own default (a day; 28 days for a span over 120 days,
|
|
4826
|
+
// an hour under 7 — legend.js 2722-2729) and the range buttons
|
|
4827
|
+
// below (hour/day/week/month/year, only the ones the span
|
|
4828
|
+
// supports, and only when there's more than one — legend.js
|
|
4829
|
+
// 2512-2543) replace it, as flat's ixmaps.setSliderRange.
|
|
4830
|
+
if (rt.binding && rt.binding.time) {
|
|
4831
|
+
const timeField = rt.binding.time;
|
|
4832
|
+
const toMs = v => (typeof v === 'number' ? v : new Date(v).getTime());
|
|
4833
|
+
let uMin = Infinity, uMax = -Infinity;
|
|
4834
|
+
rt.features.forEach(f => {
|
|
4835
|
+
const t = toMs((f.properties || {})[timeField]);
|
|
4836
|
+
if (!isNaN(t)) { if (t < uMin) uMin = t; if (t > uMax) uMax = t; }
|
|
4837
|
+
});
|
|
4838
|
+
// one step guard: no span (no/broken time values) — no slider
|
|
4839
|
+
if (Number.isFinite(uMin) && Number.isFinite(uMax) && uMax > uMin) {
|
|
4840
|
+
const uDay = 1000 * 60 * 60 * 24;
|
|
4841
|
+
const days = (uMax - uMin) / uDay;
|
|
4842
|
+
let range = days > 120 ? uDay * 28 : days < 7 ? 1000 * 60 * 60 : uDay;
|
|
4843
|
+
const timeRow = document.createElement('div');
|
|
4844
|
+
timeRow.style.cssText = 'margin-top:10px;font-size:11px;opacity:0.8;';
|
|
4845
|
+
// textContent: the field name comes from the page / project JSON
|
|
4846
|
+
const timeLabel = document.createElement('span');
|
|
4847
|
+
timeLabel.style.cssText = 'font-weight:600;';
|
|
4848
|
+
timeLabel.textContent = String(rt.style.timefield || timeField);
|
|
4849
|
+
const timeSpan = document.createElement('span');
|
|
4850
|
+
timeSpan.className = 'ix-legend-time-span';
|
|
4851
|
+
timeRow.append(timeLabel, ': ', timeSpan);
|
|
4852
|
+
bodyEl.appendChild(timeRow);
|
|
4853
|
+
const timeSlider = document.createElement('input');
|
|
4854
|
+
timeSlider.type = 'range';
|
|
4855
|
+
timeSlider.min = String(uMin);
|
|
4856
|
+
timeSlider.max = String(uMax);
|
|
4857
|
+
timeSlider.value = String(uMin);
|
|
4858
|
+
timeSlider.style.cssText = 'width:100%;margin-top:0.4em;'; // flat's own slider width (legend.js 2503)
|
|
4859
|
+
timeRow.appendChild(timeSlider);
|
|
4860
|
+
const spanEl = () => timeRow.querySelector('.ix-legend-time-span');
|
|
4861
|
+
const applyTimeFrame = () => {
|
|
4862
|
+
const v = Number(timeSlider.value);
|
|
4863
|
+
if (v <= uMin) {
|
|
4864
|
+
// flat: slider at its start position = every record
|
|
4865
|
+
rt.setTimeFrame(null, null);
|
|
4866
|
+
spanEl().textContent = '';
|
|
4867
|
+
} else {
|
|
4868
|
+
rt.setTimeFrame(v, v + range);
|
|
4869
|
+
// flat's span text (legend.js 2737-2745): date - date
|
|
4870
|
+
// for a day-and-up window, date-time under it
|
|
4871
|
+
spanEl().textContent = range < uDay
|
|
4872
|
+
? new Date(v).toLocaleDateString() + ' ' + new Date(v).toLocaleTimeString()
|
|
4873
|
+
: new Date(v).toLocaleDateString() + ' - ' + new Date(v + range).toLocaleDateString();
|
|
4874
|
+
}
|
|
4875
|
+
// a discrete control, not a pan/zoom stream: full refresh,
|
|
4876
|
+
// so the rows/totals (onRedraw → computeTotals) follow live
|
|
4877
|
+
refresh();
|
|
4878
|
+
};
|
|
4879
|
+
timeSlider.addEventListener('input', applyTimeFrame);
|
|
4880
|
+
const rangeChoices = [
|
|
4881
|
+
['hour', 1000 * 60 * 60, days < 2],
|
|
4882
|
+
['day', uDay, days > 1],
|
|
4883
|
+
['week', uDay * 7, days > 13],
|
|
4884
|
+
['month', uDay * 28, days > 55],
|
|
4885
|
+
['year', uDay * 356, days > 365]
|
|
4886
|
+
].filter(c => c[2]);
|
|
4887
|
+
if (rangeChoices.length > 1) {
|
|
4888
|
+
const btnRow = document.createElement('div');
|
|
4889
|
+
btnRow.style.cssText = 'display:flex;gap:4px;margin-top:4px;';
|
|
4890
|
+
rangeChoices.forEach(([name, ms]) => {
|
|
4891
|
+
const b = document.createElement('button');
|
|
4892
|
+
b.type = 'button';
|
|
4893
|
+
b.textContent = name;
|
|
4894
|
+
const paint = active => {
|
|
4895
|
+
b.style.cssText = 'background:transparent;border:1px solid '
|
|
4896
|
+
+ (active ? 'currentColor' : 'rgba(128,128,128,0.5)')
|
|
4897
|
+
+ ';color:inherit;font:inherit;font-size:10px;padding:1px 7px;'
|
|
4898
|
+
+ 'border-radius:4px;cursor:pointer;opacity:' + (active ? 1 : 0.55) + ';'
|
|
4899
|
+
+ (active ? 'font-weight:600;' : '');
|
|
4900
|
+
};
|
|
4901
|
+
paint(ms === range);
|
|
4902
|
+
b.addEventListener('click', () => {
|
|
4903
|
+
range = ms;
|
|
4904
|
+
[...btnRow.children].forEach(x => paint(x === b));
|
|
4905
|
+
applyTimeFrame();
|
|
4906
|
+
});
|
|
4907
|
+
btnRow.appendChild(b);
|
|
4908
|
+
});
|
|
4909
|
+
timeRow.appendChild(btnRow);
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
}
|
|
4913
|
+
|
|
4332
4914
|
// Chart-size slider — real engine's own range (legend.js:
|
|
4333
4915
|
// 3196-3202), 100% == scale:1, wired to changeThemeStyle(
|
|
4334
4916
|
// themeId,"scale:"+pct/100,"set")+redrawTheme there; this
|
|
@@ -4353,20 +4935,30 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4353
4935
|
: Math.round((styleNum(rt.style.scale) || 1) * 100);
|
|
4354
4936
|
const sliderLabel = isOpacitySlider ? 'Opacity' : 'Chart size';
|
|
4355
4937
|
sliderRow.innerHTML = sliderLabel + ': <span class="ix-legend-scale-val">' + initialPct + '</span>%';
|
|
4356
|
-
|
|
4938
|
+
// flat's center legend: the slider is INLINE in the chips row
|
|
4939
|
+
// (legend.css 541-550: inline-block, one item width, no wrap,
|
|
4940
|
+
// its input 80px) — a sibling of the chips container so the
|
|
4941
|
+
// row re-render can't wipe it
|
|
4942
|
+
const sliderHost = legendCenter && rowsScroll ? rowsScroll : bodyEl;
|
|
4943
|
+
sliderRow.style.cssText = legendCenter && rowsScroll
|
|
4944
|
+
? 'display:inline-block;margin:0 0 0.5em 1.5em;font-size:11px;opacity:0.8;white-space:nowrap;'
|
|
4945
|
+
: sliderRow.style.cssText;
|
|
4946
|
+
sliderHost.appendChild(sliderRow);
|
|
4357
4947
|
const slider = document.createElement('input');
|
|
4358
4948
|
slider.type = 'range';
|
|
4359
4949
|
slider.min = isOpacitySlider ? '0' : '25';
|
|
4360
4950
|
slider.max = isOpacitySlider ? '100' : '200';
|
|
4361
4951
|
slider.value = String(initialPct);
|
|
4362
|
-
slider.style.cssText =
|
|
4952
|
+
slider.style.cssText = legendCenter && rowsScroll
|
|
4953
|
+
? 'width:80px;vertical-align:middle;margin:0 0 0 0.5em;' // flat's inline slider width (legend.css 548)
|
|
4954
|
+
: 'width:50%;margin-top:0.4em;'; // flat's own inline slider style (legend.js 2392)
|
|
4363
4955
|
slider.addEventListener('input', () => {
|
|
4364
4956
|
const pct = parseInt(slider.value, 10);
|
|
4365
4957
|
sliderRow.querySelector('.ix-legend-scale-val').textContent = pct;
|
|
4366
4958
|
rt.setStyle(isOpacitySlider ? { fillopacity: pct / 100 } : { scale: pct / 100 });
|
|
4367
4959
|
quietRefresh();
|
|
4368
4960
|
});
|
|
4369
|
-
|
|
4961
|
+
sliderHost.appendChild(slider);
|
|
4370
4962
|
}
|
|
4371
4963
|
}
|
|
4372
4964
|
};
|
|
@@ -4406,7 +4998,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4406
4998
|
}
|
|
4407
4999
|
return this;
|
|
4408
5000
|
}
|
|
4409
|
-
options(
|
|
5001
|
+
// merged, as flat's options() sets each key on its own (ixmaps.setOptions)
|
|
5002
|
+
// and the live map's options() below does — .options({a}).options({b})
|
|
5003
|
+
// keeps a
|
|
5004
|
+
options(o) { if (o && typeof o === 'object') Object.assign(this._engineOptions, o); return this; }
|
|
4410
5005
|
local(...args) { this._locals.push(args); return this; }
|
|
4411
5006
|
// flat's .require(url) (htmlgui.js ixmaps.require): a script the page
|
|
4412
5007
|
// needs (a chart library, a user chart) — page code, like a <script>
|
|
@@ -4434,7 +5029,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4434
5029
|
this._layerBuilders.push(lb);
|
|
4435
5030
|
return lb;
|
|
4436
5031
|
}
|
|
4437
|
-
this._layerBuilders.push(
|
|
5032
|
+
this._layerBuilders.push(asLayerBuilder(nameOrBuilder));
|
|
4438
5033
|
return this;
|
|
4439
5034
|
}
|
|
4440
5035
|
// .Layer() — alias of .layer()
|
|
@@ -4459,8 +5054,38 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4459
5054
|
// the Map(id, opts, map => map.options(...)) callback form worked).
|
|
4460
5055
|
// One microtask lets the caller's whole synchronous chain land first.
|
|
4461
5056
|
await null;
|
|
4462
|
-
|
|
4463
|
-
|
|
5057
|
+
// the DASH flow animation's repaint loop (see refreshLayers): MapLibre
|
|
5058
|
+
// renders on demand only, so the shader's wall-clock dash (see
|
|
5059
|
+
// makeFlowPathLayer) needs a continuous frame request while any flow
|
|
5060
|
+
// layer is on screen — one handle per map, or another map's refresh
|
|
5061
|
+
// cancels (or blocks) this map's loop
|
|
5062
|
+
let _flowRepaintRAF = null;
|
|
5063
|
+
let el = document.getElementById(this.containerId);
|
|
5064
|
+
if (!el) {
|
|
5065
|
+
// flat's own fallback for a missing target div (htmlgui_flat.js
|
|
5066
|
+
// 1055-1082): auto-create it instead of hard-failing, with a
|
|
5067
|
+
// RANDOMIZED id suffix — two calls that both forgot their div
|
|
5068
|
+
// and both name the same id must not collide on one element —
|
|
5069
|
+
// opt.width/height (else 100%), appended to document.body, and a
|
|
5070
|
+
// loud warning; reassigning this.containerId keeps every
|
|
5071
|
+
// downstream lookup (the MapLibre container, the splash cleanup
|
|
5072
|
+
// in createMap) consistent automatically
|
|
5073
|
+
const szOriginalDiv = this.containerId;
|
|
5074
|
+
do {
|
|
5075
|
+
this.containerId = szOriginalDiv + '-' + Math.random().toString(36).slice(2, 8);
|
|
5076
|
+
} while (document.getElementById(this.containerId));
|
|
5077
|
+
console.warn('[ixmaps-gl] container #' + szOriginalDiv + ' not found in the document — creating #' + this.containerId
|
|
5078
|
+
+ ' automatically. Add <div id="' + szOriginalDiv + '"></div> to the page to avoid this warning.');
|
|
5079
|
+
el = document.createElement('div');
|
|
5080
|
+
el.id = this.containerId;
|
|
5081
|
+
el.style.width = (this.mapOptions && this.mapOptions.width) ? String(this.mapOptions.width) : '100%';
|
|
5082
|
+
// a percentage height would collapse against a body without a
|
|
5083
|
+
// height of its own (the blank-screen case): the window's
|
|
5084
|
+
// innerHeight instead, like flat's fullscreen fallback
|
|
5085
|
+
el.style.height = (this.mapOptions && this.mapOptions.height) ? String(this.mapOptions.height)
|
|
5086
|
+
: (global.innerHeight || 800) + 'px';
|
|
5087
|
+
document.body.appendChild(el);
|
|
5088
|
+
}
|
|
4464
5089
|
// flat's init replaces the container's content with its own map page
|
|
4465
5090
|
// (mappage.html via innerHTML), so a page's placeholder inside the
|
|
4466
5091
|
// map div ("connecting to Internet ...", the fuel-price page) goes
|
|
@@ -4482,13 +5107,22 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4482
5107
|
else if (typeof v === 'string' && /^\s*\d+(\.\d+)?\s*%\s*$/.test(v)
|
|
4483
5108
|
&& !(dim === 'width' ? el.clientWidth : el.clientHeight)) el.style[dim] = v.trim();
|
|
4484
5109
|
}
|
|
4485
|
-
// without
|
|
5110
|
+
// without height flat's map is "fullscreen" (htmlgui_flat.js
|
|
4486
5111
|
// 847-876): it fills the window, whatever the page's CSS gives the
|
|
4487
|
-
// container —
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
5112
|
+
// container. PER SIDE — also when only a width is given (the World
|
|
5113
|
+
// Dataviz Prize pages: width 100%, no height), where the old
|
|
5114
|
+
// both-sides gate left the container collapsed at 0 (a blank
|
|
5115
|
+
// screen): no height option and no effective CSS height gets the
|
|
5116
|
+
// window's innerHeight, kept current on window resizes (MapLibre
|
|
5117
|
+
// follows its container's own size)
|
|
5118
|
+
if (!(this.mapOptions && this.mapOptions.height) && !el.clientHeight) {
|
|
5119
|
+
this._heightFallbackToWindow = true;
|
|
5120
|
+
el.style.height = (global.innerHeight || 800) + 'px';
|
|
5121
|
+
global.addEventListener('resize', () => {
|
|
5122
|
+
if (this._heightFallbackToWindow) el.style.height = (global.innerHeight || 800) + 'px';
|
|
5123
|
+
});
|
|
4491
5124
|
}
|
|
5125
|
+
if (!(this.mapOptions && this.mapOptions.width) && !el.clientWidth) el.style.width = '100%';
|
|
4492
5126
|
|
|
4493
5127
|
// shown for the whole build() (library lazy-load + every layer's own
|
|
4494
5128
|
// data fetch, real-world verified as the two slowest phases) through
|
|
@@ -4515,8 +5149,6 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4515
5149
|
const svgProjection = flatSvgProjectionOf(this.mapOptions.map)
|
|
4516
5150
|
|| (isFlatOrthographicName(this.mapOptions.mapProjection) ? FLAT_ORTHOGRAPHIC_PROJECTION : null);
|
|
4517
5151
|
if (svgProjection) flatSvgProjectionByOptions.set(this._engineOptions, svgProjection);
|
|
4518
|
-
const planeProjection = flatPlaneProjectionOf(this.mapOptions);
|
|
4519
|
-
if (planeProjection) flatPlaneProjectionByOptions.set(this._engineOptions, planeProjection);
|
|
4520
5152
|
if (isFlatOrthographicName(this.mapOptions.mapProjection)) {
|
|
4521
5153
|
flatOrthographicByOptions.set(this._engineOptions, true);
|
|
4522
5154
|
if (this._viewCenter) flatViewCenterByOptions.set(this._engineOptions, { lat: this._viewCenter[0], lng: this._viewCenter[1] });
|
|
@@ -4562,6 +5194,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4562
5194
|
// build() and every later defineLayer()/refreshTheme() share it
|
|
4563
5195
|
const dataCache = this._dataCache = this._dataCache || new Map();
|
|
4564
5196
|
const runtimes = [];
|
|
5197
|
+
const sphereBackdrops = []; // {type:"Sphere"} layers read before the map exists
|
|
4565
5198
|
// themes whose data comes by name (see namedDataTheme) start empty and
|
|
4566
5199
|
// load on their own once the map is built, as in flat, where every
|
|
4567
5200
|
// theme loads independently: a broker that answers late — or only
|
|
@@ -4593,9 +5226,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4593
5226
|
// instead of colliding with each other under one 'null' entry.
|
|
4594
5227
|
let raw, cacheKey;
|
|
4595
5228
|
const deferred = deferredFeatureLoad(spec, initialZoom);
|
|
4596
|
-
|
|
5229
|
+
// flat's WMS theme type (maptheme.js 8207-8247, the szFlag /\bWMS|IMAGE\b/
|
|
5230
|
+
// realize branch) has NO data of its own — .data({server}) is an
|
|
5231
|
+
// Esri MapServer export endpoint rendered as one image per view.
|
|
5232
|
+
// Here the same theme becomes a MapLibre raster tile source (one
|
|
5233
|
+
// 256px image per XYZ tile via the {bbox-epsg-3857} placeholder —
|
|
5234
|
+
// real cached tiles, not a re-fetched viewport image per pan), see
|
|
5235
|
+
// syncWmsLayers: no records to load, ever
|
|
5236
|
+
const wms = spec.flags.has('WMS') || spec.flags.has('IMAGE');
|
|
5237
|
+
if (deferred || named || wms) {
|
|
4597
5238
|
raw = { type: 'FeatureCollection', features: [] };
|
|
4598
|
-
cacheKey = named ? 'name:' + spec.data.name : 'deferred:' + spec.name;
|
|
5239
|
+
cacheKey = named ? 'name:' + spec.data.name : wms ? 'wms:' + spec.name : 'deferred:' + spec.name;
|
|
4599
5240
|
} else if (spec.data && spec.data.obj) {
|
|
4600
5241
|
raw = await fetchLayerData(spec.data, spec.binding, this._engineOptions);
|
|
4601
5242
|
cacheKey = 'obj:' + spec.name;
|
|
@@ -4618,6 +5259,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4618
5259
|
rt._named = named;
|
|
4619
5260
|
rt._zoomToPending = wantsZoomToExtent(spec.flags);
|
|
4620
5261
|
runtimes.push(rt);
|
|
5262
|
+
if (fc._sphere) sphereBackdrops.push(rt.style);
|
|
4621
5263
|
notifyNewTheme(rt);
|
|
4622
5264
|
_globalThemeRegistry.set(rt.name, rt);
|
|
4623
5265
|
// See findRuntime's own comment on the three real theme-id
|
|
@@ -4676,6 +5318,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4676
5318
|
renderWorldCopies: this._engineOptions.worldcopies !== false
|
|
4677
5319
|
});
|
|
4678
5320
|
engineOptionsByMap.set(map, this._engineOptions);
|
|
5321
|
+
sphereBackdrops.forEach(style => applySphereBackdrop(map, style));
|
|
4679
5322
|
// the live-SVG pane USER charts render into (see chartHostFor) —
|
|
4680
5323
|
// registered here, keyed by this map's options object, so every
|
|
4681
5324
|
// runtime (including later loadProject/map.layer ones) finds it
|
|
@@ -4810,7 +5453,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4810
5453
|
// a page legend (map.legend(html)) is positioned by the map's
|
|
4811
5454
|
// own align option — the same rule that places the native stack
|
|
4812
5455
|
legendEl.style.cssText = 'position:absolute;' + legendAlignCssFor(this.mapOptions) + 'z-index:5;pointer-events:none';
|
|
4813
|
-
|
|
5456
|
+
// flat's legend takes the pane's html OR a URL to it (htmlgui):
|
|
5457
|
+
// a value without a tag is fetched, its html shown
|
|
5458
|
+
if (/<[a-z!]/i.test(this._legendHtml)) legendEl.innerHTML = this._legendHtml;
|
|
5459
|
+
else fetch(this._legendHtml).then(r => r.ok ? r.text() : '').then(t => { legendEl.innerHTML = t; }).catch(() => {});
|
|
4814
5460
|
// inside the map container, never the page's body (see the pinned
|
|
4815
5461
|
// tooltip mount) — styling body breaks page-positioned elements
|
|
4816
5462
|
anchorInside(el);
|
|
@@ -5166,6 +5812,16 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5166
5812
|
const rt = findRuntime(themeId);
|
|
5167
5813
|
if (rt) { rt._hidden = !visible; refresh(); }
|
|
5168
5814
|
},
|
|
5815
|
+
// flat's Api.switchLayer (htmlgui.js 3383, htmlgui_flat.js 1531):
|
|
5816
|
+
// every theme of the LAYER named — setThemeVisible takes one theme
|
|
5817
|
+
// id, this takes the layer name and switches all of its themes
|
|
5818
|
+
switchLayer: (layerName, visible) => {
|
|
5819
|
+
const targets = runtimes.filter(r => r.name === layerName);
|
|
5820
|
+
if (!targets.length) console.warn(`[ixmaps-gl] switchLayer: no layer named "${layerName}"`);
|
|
5821
|
+
targets.forEach(rt => { rt._hidden = !visible; });
|
|
5822
|
+
if (targets.length) refresh();
|
|
5823
|
+
return engineApi;
|
|
5824
|
+
},
|
|
5169
5825
|
getCategoryColor: (themeId, value) => {
|
|
5170
5826
|
const rt = findRuntime(themeId);
|
|
5171
5827
|
if (!rt || !rt.categoryIndexByLabel) return null;
|
|
@@ -5175,7 +5831,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5175
5831
|
// called after every redraw (pan/zoom, or any of the setters above)
|
|
5176
5832
|
// so a page can keep a facets sidebar in sync, mirroring the real
|
|
5177
5833
|
// engine's htmlgui_onDrawTheme hook
|
|
5178
|
-
|
|
5834
|
+
// returns an unsubscribe (the legend drops its listener with its panel)
|
|
5835
|
+
onRedraw: (cb) => {
|
|
5836
|
+
redrawListeners.push(cb);
|
|
5837
|
+
return () => { const i = redrawListeners.indexOf(cb); if (i >= 0) redrawListeners.splice(i, 1); };
|
|
5838
|
+
},
|
|
5179
5839
|
refresh: () => refresh(),
|
|
5180
5840
|
// Adds ONE new layer/theme to an ALREADY-BUILT map — build()'s own
|
|
5181
5841
|
// loop above only ever runs once, at construction, so a caller
|
|
@@ -5300,9 +5960,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5300
5960
|
for (const rt of targets) {
|
|
5301
5961
|
if (rt._named) { await loadNamedTheme(rt, true); continue; }
|
|
5302
5962
|
const spec = normalizeTheme(rt._definition);
|
|
5303
|
-
//
|
|
5304
|
-
//
|
|
5305
|
-
const raw = spec.
|
|
5963
|
+
// a WMS theme has no records to reload (see build()'s wms
|
|
5964
|
+
// branch) — its raster layer is re-synced by the refresh below
|
|
5965
|
+
const raw = (spec.flags.has('WMS') || spec.flags.has('IMAGE'))
|
|
5966
|
+
? { type: 'FeatureCollection', features: [] }
|
|
5967
|
+
: spec.data && spec.data.obj
|
|
5306
5968
|
? await fetchLayerData(spec.data, rt._specBinding, builder._engineOptions)
|
|
5307
5969
|
: await cachedLayerData(builder._dataCache, spec.data, rt._specBinding, builder._engineOptions, { reload: true }).promise;
|
|
5308
5970
|
rt.replaceFeatures(filterAndJoin(raw, spec, runtimes));
|
|
@@ -5323,7 +5985,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5323
5985
|
// layers — leaving its groups mounted keeps the charts on the
|
|
5324
5986
|
// map after map.remove()/replace() (censimenti's sidebar)
|
|
5325
5987
|
if (rt._clearUserChartPane) rt._clearUserChartPane();
|
|
5988
|
+
if (rt._chartPaneDefs) { rt._chartPaneDefs.remove(); rt._chartPaneDefs = null; }
|
|
5326
5989
|
if (rt._chartHost && rt._chartHost.runtimes) rt._chartHost.runtimes.delete(rt);
|
|
5990
|
+
// and its hidden <name>_init group (userChartHost, on the body)
|
|
5991
|
+
if (rt._userChartGroup) { rt._userChartGroup.remove(); rt._userChartGroup = null; }
|
|
5327
5992
|
});
|
|
5328
5993
|
const removed = removedRuntimes.length > 0;
|
|
5329
5994
|
// stale-registry guard: _globalThemeRegistry is a bare name->
|
|
@@ -5531,8 +6196,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5531
6196
|
let raw;
|
|
5532
6197
|
const deferred = deferredFeatureLoad(spec, map.getZoom());
|
|
5533
6198
|
const named = namedDataTheme(spec);
|
|
6199
|
+
// WMS themes carry no records (see build()'s own wms branch)
|
|
6200
|
+
const wms = spec.flags.has('WMS') || spec.flags.has('IMAGE');
|
|
5534
6201
|
let sourceKey = null;
|
|
5535
|
-
if (deferred || named) {
|
|
6202
|
+
if (deferred || named || wms) {
|
|
5536
6203
|
raw = { type: 'FeatureCollection', features: [] };
|
|
5537
6204
|
} else if (spec.data && spec.data.obj) {
|
|
5538
6205
|
// in memory already — nothing to cache (see build())
|
|
@@ -5552,6 +6219,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5552
6219
|
rt._dataSourceKey = named ? 'name:' + spec.data.name
|
|
5553
6220
|
: 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) });
|
|
5554
6221
|
runtimes.push(rt);
|
|
6222
|
+
if (fc._sphere) applySphereBackdrop(map, rt.style);
|
|
5555
6223
|
_globalThemeRegistry.set(rt.name, rt);
|
|
5556
6224
|
if (rt.meta && rt.meta.name && rt.meta.name !== rt.name) _globalThemeRegistry.set(rt.meta.name, rt);
|
|
5557
6225
|
if (themeIdOf(rt) !== rt.name) _globalThemeRegistry.set(themeIdOf(rt), rt);
|
|
@@ -5622,8 +6290,6 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5622
6290
|
const svgProjection = flatSvgProjectionOf(svg) || (/orthographic\.svg$/i.test(svg) ? FLAT_ORTHOGRAPHIC_PROJECTION : null);
|
|
5623
6291
|
if (svgProjection) flatSvgProjectionByOptions.set(builder._engineOptions, svgProjection);
|
|
5624
6292
|
else flatSvgProjectionByOptions.delete(builder._engineOptions);
|
|
5625
|
-
if (svgProjection && svgProjection.invert) flatPlaneProjectionByOptions.set(builder._engineOptions, svgProjection);
|
|
5626
|
-
else flatPlaneProjectionByOptions.delete(builder._engineOptions);
|
|
5627
6293
|
if (/orthographic\.svg$/i.test(svg)) flatOrthographicByOptions.set(builder._engineOptions, true);
|
|
5628
6294
|
else { flatOrthographicByOptions.delete(builder._engineOptions); flatViewCenterByOptions.delete(builder._engineOptions); }
|
|
5629
6295
|
}
|
|
@@ -5794,7 +6460,87 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5794
6460
|
if (order !== refreshLayers._lastOrder) { refreshLayers._lastOrder = order; console.info('[ixmaps-gl debug] draw order (bottom → top): ' + order); }
|
|
5795
6461
|
}
|
|
5796
6462
|
overlay.setProps({ layers });
|
|
6463
|
+
// start/stop the flow repaint loop: any layer whose paths carry
|
|
6464
|
+
// dash offsets animates, and only then (a static map renders
|
|
6465
|
+
// nothing new; the loop is one rAF, one triggerRepaint each)
|
|
6466
|
+
const flowOn = layers.some(l => l.constructor.layerName === 'FlowPathLayer'
|
|
6467
|
+
&& l.props.data && l.props.data.some(d => d.dash && d.dash[0] + d.dash[1] > 0));
|
|
6468
|
+
if (glDebug()) console.info('[ixmaps-gl debug] flow loop on: ' + flowOn + ' (' + layers.filter(l => l.constructor.layerName === 'FlowPathLayer').length + ' flow layers, '
|
|
6469
|
+
+ (layers.find(l => l.constructor.layerName === 'FlowPathLayer') || {props: {data: []}}).props.data.length + ' paths)');
|
|
6470
|
+
if (flowOn) {
|
|
6471
|
+
if (_flowRepaintRAF == null) {
|
|
6472
|
+
const loop = () => { if (map.triggerRepaint) map.triggerRepaint(); _flowRepaintRAF = global.requestAnimationFrame(loop); };
|
|
6473
|
+
_flowRepaintRAF = global.requestAnimationFrame(loop);
|
|
6474
|
+
}
|
|
6475
|
+
} else if (_flowRepaintRAF != null) {
|
|
6476
|
+
global.cancelAnimationFrame(_flowRepaintRAF);
|
|
6477
|
+
_flowRepaintRAF = null;
|
|
6478
|
+
}
|
|
6479
|
+
// flat's WMS themes render as native raster layers, not deck.gl —
|
|
6480
|
+
// reconcile them with the current runtimes on every rebuild
|
|
6481
|
+
syncWmsLayers();
|
|
6482
|
+
}
|
|
6483
|
+
|
|
6484
|
+
// flat's WMS theme type (szFlag /\bWMS|IMAGE\b/, maptheme.js 8207-8247):
|
|
6485
|
+
// .data({server}) names an Esri MapServer export endpoint, flat draws
|
|
6486
|
+
// ONE f=image request per view (bbox in 4326, the view's pixel size),
|
|
6487
|
+
// opacity from fillOpacity || nOpacity || 0.8. Here the same theme is
|
|
6488
|
+
// a MapLibre raster tile source instead — the SAME export call per
|
|
6489
|
+
// 256px XYZ tile ({bbox-epsg-3857} → bboxSR=3857), which MapLibre
|
|
6490
|
+
// caches, loads progressively and never re-fetches per pan step;
|
|
6491
|
+
// deck.gl's interleaved layers stay on top of it. server is read
|
|
6492
|
+
// from .style() as flat does (maptheme.js 1408: styleObj.server),
|
|
6493
|
+
// or from .data() as this port's own pages write it.
|
|
6494
|
+
// ids of the raster layers this function ever added — lets it skip
|
|
6495
|
+
// map.getStyle() (a deep copy of the whole style) on every rebuild
|
|
6496
|
+
// of a page that has no WMS theme
|
|
6497
|
+
const wmsLayerIds = new Set();
|
|
6498
|
+
function syncWmsLayers() {
|
|
6499
|
+
if (!map || !map.getLayer) return;
|
|
6500
|
+
const idOf = name => 'ixmaps-wms:' + String(name).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
6501
|
+
const desired = new Map();
|
|
6502
|
+
runtimes.forEach(rt => {
|
|
6503
|
+
if (!(rt.flags.has('WMS') || rt.flags.has('IMAGE'))) return;
|
|
6504
|
+
const server = (rt.style && rt.style.server) || (rt._definition && rt._definition.data && rt._definition.data.server);
|
|
6505
|
+
if (server) desired.set(rt.name, { rt, server });
|
|
6506
|
+
});
|
|
6507
|
+
if (!desired.size && !wmsLayerIds.size) return;
|
|
6508
|
+
// a removed WMS theme takes its raster layer with it — as flat's
|
|
6509
|
+
// clearWMS does (ids are sanitized names, compare id-to-id)
|
|
6510
|
+
const ownedIds = new Set([...desired.keys()].map(idOf));
|
|
6511
|
+
[...wmsLayerIds].forEach(id => {
|
|
6512
|
+
if (ownedIds.has(id)) return;
|
|
6513
|
+
wmsLayerIds.delete(id);
|
|
6514
|
+
try { if (map.getLayer(id)) map.removeLayer(id); } catch (e) { /* already gone */ }
|
|
6515
|
+
try { if (map.getSource(id)) map.removeSource(id); } catch (e) { /* already gone */ }
|
|
6516
|
+
});
|
|
6517
|
+
desired.forEach(({ rt, server }, name) => {
|
|
6518
|
+
const id = idOf(name);
|
|
6519
|
+
wmsLayerIds.add(id);
|
|
6520
|
+
const sep = server.indexOf('?') >= 0 ? '&' : '?';
|
|
6521
|
+
const tiles = [server + sep + 'f=image&transparent=true&bbox={bbox-epsg-3857}&bboxSR=3857&size=256,256'];
|
|
6522
|
+
if (!map.getSource(id)) {
|
|
6523
|
+
map.addSource(id, { type: 'raster', tiles, tileSize: 256 });
|
|
6524
|
+
}
|
|
6525
|
+
if (!map.getLayer(id)) {
|
|
6526
|
+
map.addLayer({ id, type: 'raster', source: id, paint: { 'raster-opacity': 1 } });
|
|
6527
|
+
}
|
|
6528
|
+
// flat's own opacity reading (maptheme.js 8240): fillOpacity ||
|
|
6529
|
+
// nOpacity || 0.8 — style "opacity" already folded into
|
|
6530
|
+
// fillopacity by normalizeTheme
|
|
6531
|
+
const opacity = styleNum(rt.style.fillopacity) || 0.8;
|
|
6532
|
+
if ((map.getPaintProperty(id, 'raster-opacity') || 1) !== opacity) {
|
|
6533
|
+
map.setPaintProperty(id, 'raster-opacity', opacity);
|
|
6534
|
+
}
|
|
6535
|
+
const vis = rt._hidden ? 'none' : 'visible';
|
|
6536
|
+
if ((map.getLayoutProperty(id, 'visibility') || 'visible') !== vis) {
|
|
6537
|
+
map.setLayoutProperty(id, 'visibility', vis);
|
|
6538
|
+
}
|
|
6539
|
+
});
|
|
5797
6540
|
}
|
|
6541
|
+
// a style swap (setMapType) drops every native layer — rebuild the
|
|
6542
|
+
// WMS ones once the new style stands
|
|
6543
|
+
map.on('style.load', () => syncWmsLayers());
|
|
5798
6544
|
|
|
5799
6545
|
// isZooming flips true/false on every 'zoomstart'/'zoomend' pair —
|
|
5800
6546
|
// and a scroll-wheel or trackpad zoom fires MANY such pairs per
|
|
@@ -5890,7 +6636,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5890
6636
|
} finally { inDrawThemeHook = false; }
|
|
5891
6637
|
}
|
|
5892
6638
|
function notifyRedraw() {
|
|
5893
|
-
redrawListeners.forEach(cb => { try { cb(); } catch (err) { console.error('[ixmaps-gl] onRedraw callback failed:', err); } });
|
|
6639
|
+
redrawListeners.slice().forEach(cb => { try { cb(); } catch (err) { console.error('[ixmaps-gl] onRedraw callback failed:', err); } });
|
|
5894
6640
|
callDrawThemeHooks();
|
|
5895
6641
|
}
|
|
5896
6642
|
|
|
@@ -6072,6 +6818,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6072
6818
|
};
|
|
6073
6819
|
}
|
|
6074
6820
|
_resolveMapReady(engineApi);
|
|
6821
|
+
if (this.mapOptions && this.mapOptions.name) namedMapReady(this.mapOptions.name).resolve(engineApi);
|
|
6075
6822
|
resolveBuilt();
|
|
6076
6823
|
namedLoads.forEach(rt => { listenNamedData(rt); if (!rt._deferredLoad) loadNamedTheme(rt, false); });
|
|
6077
6824
|
return engineApi;
|
|
@@ -6090,7 +6837,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6090
6837
|
// theirs and are not overridden.
|
|
6091
6838
|
for (const m of ['replace', 'add', 'newTheme', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
6092
6839
|
'refreshTheme', 'redrawTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'setProjection', 'resize',
|
|
6093
|
-
'loadProject', 'project', 'setThemeVisible', 'setThemeTimeFrame', 'show', 'hide', 'getZoom', 'getThemeObj',
|
|
6840
|
+
'loadProject', 'project', 'setThemeVisible', 'switchLayer', 'setThemeTimeFrame', 'show', 'hide', 'getZoom', 'getThemeObj',
|
|
6094
6841
|
'getThemes', 'theme', 'setView']) {
|
|
6095
6842
|
if (!MapBuilder.prototype[m]) {
|
|
6096
6843
|
MapBuilder.prototype[m] = function (...args) {
|
|
@@ -6103,6 +6850,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6103
6850
|
// class used throughout this file — a real bug caught during testing
|
|
6104
6851
|
// (every `new Map()` below silently resolved to *this* function instead)
|
|
6105
6852
|
function createMap(containerId, options, mapFn) {
|
|
6853
|
+
if (options && options.name) namedMapReady(options.name);
|
|
6106
6854
|
const builder = new MapBuilder(containerId, options);
|
|
6107
6855
|
if (mapFn) mapFn(builder);
|
|
6108
6856
|
// If build() rejects (bad data URL, misconfigured CHOROPLETH join,
|
|
@@ -6113,7 +6861,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6113
6861
|
// .catch()/await still sees the real failure — this only guarantees
|
|
6114
6862
|
// the splash doesn't outlive it.
|
|
6115
6863
|
const promise = builder.build().catch(err => {
|
|
6116
|
-
|
|
6864
|
+
// builder.containerId, not the closure's: build() reassigns it when
|
|
6865
|
+
// it had to auto-create a missing container (flat's fallback above)
|
|
6866
|
+
const el = document.getElementById(builder.containerId);
|
|
6117
6867
|
const splash = el && el.querySelector('.ixmaps-splash');
|
|
6118
6868
|
if (splash) hideSplash(splash);
|
|
6119
6869
|
throw err;
|
|
@@ -6168,7 +6918,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6168
6918
|
// flat's mapApi.setView(center, zoom) (htmlgui_flat.js 1327) — the
|
|
6169
6919
|
// same live-or-build-time split as .view() above
|
|
6170
6920
|
promise.setView = (...args) => promise.view(...args);
|
|
6171
|
-
|
|
6921
|
+
// post-build: the live map's options() (merged into the options
|
|
6922
|
+
// object the running themes hold), as .view() above — the builder's
|
|
6923
|
+
// own is read once, by build()
|
|
6924
|
+
promise.options = (...args) => { if (resolvedApi) resolvedApi.options(...args); else builder.options(...args); return promise; };
|
|
6172
6925
|
promise.attribution = (...args) => { builder.attribution(...args); return promise; };
|
|
6173
6926
|
promise.legend = (...args) => { builder.legend(...args); return promise; };
|
|
6174
6927
|
promise.local = (...args) => { builder.local(...args); return promise; };
|
|
@@ -6392,11 +7145,6 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6392
7145
|
// the flat SVG projection a map's options object belongs to (set by the
|
|
6393
7146
|
// map's init and loadProject; the runtimes only see the options object)
|
|
6394
7147
|
const flatSvgProjectionByOptions = new WeakMap();
|
|
6395
|
-
// the projection plane a map's VECTOR / BEZIER flows are drawn in: flat's
|
|
6396
|
-
// map file, or the Map() option mapProjection ("equalearth") — flat computes
|
|
6397
|
-
// the curves in its map's own coordinates, so the same flow bows around the
|
|
6398
|
-
// pole there. (Unlike flatSvgProjectionByOptions it leaves the view framing alone.)
|
|
6399
|
-
const flatPlaneProjectionByOptions = new WeakMap();
|
|
6400
7148
|
// a map that is flat's orthographic one (drawn as MapLibre's globe): its object scale is
|
|
6401
7149
|
// flat's (scale × the globe's pixels per degree at its center, measured on flat 2026-10-10
|
|
6402
7150
|
// on the mechanical-goods globe page, the same at view zooms 2.5 and 3.5: 197.44e6; the
|
|
@@ -6439,12 +7187,6 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6439
7187
|
const radius = 512 * Math.pow(2, zoom) / (2 * Math.PI * c);
|
|
6440
7188
|
return Math.log2(globeDiscRadius(radius, height > 0 ? height : 768) * 2 * Math.PI / 512);
|
|
6441
7189
|
}
|
|
6442
|
-
function flatPlaneProjectionOf(mapOptions) {
|
|
6443
|
-
const byUrl = flatSvgProjectionOf(mapOptions.map);
|
|
6444
|
-
if (byUrl) return byUrl;
|
|
6445
|
-
const named = FLAT_SVG_PROJECTIONS[String(mapOptions.mapProjection || '').trim().toLowerCase()];
|
|
6446
|
-
return named || null;
|
|
6447
|
-
}
|
|
6448
7190
|
// flat SVG map scale ÷ the Mercator one at the same on-screen size (1
|
|
6449
7191
|
// without a flat SVG projection)
|
|
6450
7192
|
function flatMapScaleRatio(mapOptions) {
|
|
@@ -7232,8 +7974,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7232
7974
|
_measureCtx.font = fontPx + 'px arial';
|
|
7233
7975
|
return _measureCtx.measureText(text).width;
|
|
7234
7976
|
}
|
|
7235
|
-
function wrapTitleLines(text, fontPx, widthPx) {
|
|
7236
|
-
const width = w =>
|
|
7977
|
+
function wrapTitleLines(text, fontPx, widthPx, measure = titleTextWidth) {
|
|
7978
|
+
const width = w => measure(w, fontPx);
|
|
7237
7979
|
const lines = [];
|
|
7238
7980
|
for (const word of String(text).split(/\s+/).filter(Boolean)) {
|
|
7239
7981
|
const last = lines.length ? lines[lines.length - 1] + ' ' + word : null;
|
|
@@ -7519,6 +8261,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7519
8261
|
// independently-rendered systems (deck.gl's icon quad, MapLibre's own
|
|
7520
8262
|
// globe-sphere mesh) competing for the same depth-buffer real estate.
|
|
7521
8263
|
const ICON_LAYER_GLOBE_PARAMETERS = { cullMode: 'none', depthCompare: 'always' };
|
|
8264
|
+
// a transparent 1x1 PNG — the placeholder icon of an external symbol
|
|
8265
|
+
// file still being rasterized (see _buildSequenceChartLayers' urlIcon)
|
|
8266
|
+
const TRANSPARENT_1PX_PNG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
|
|
7522
8267
|
|
|
7523
8268
|
// GLOBE_HORIZON_DEG / isOnVisibleHemisphere: `depthCompare: 'always'`
|
|
7524
8269
|
// above (needed to stop the near-surface z-fighting/half-moon bug —
|
|
@@ -7790,6 +8535,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7790
8535
|
switch (shape) {
|
|
7791
8536
|
case 'squarefull': return Math.abs(dx) <= r && Math.abs(dy) <= r;
|
|
7792
8537
|
case 'square': return Math.abs(dx) <= r * Math.SQRT1_2 && Math.abs(dy) <= r * Math.SQRT1_2;
|
|
8538
|
+
case 'roundrectfull': return Math.abs(dx) <= r && Math.abs(dy) <= r;
|
|
7793
8539
|
case 'roundrect': return Math.abs(dx) <= r * 0.8 && Math.abs(dy) <= r * 0.8;
|
|
7794
8540
|
case 'diamond': case 'carot': return Math.abs(dx) + Math.abs(dy) <= r;
|
|
7795
8541
|
case 'empty': return false;
|
|
@@ -8179,39 +8925,55 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8179
8925
|
return { bars, texts, lines, extent: ext[0] <= ext[2] ? ext : [0, 0, 0, 0], width: W, sizer };
|
|
8180
8926
|
}
|
|
8181
8927
|
|
|
8182
|
-
//
|
|
8183
|
-
//
|
|
8184
|
-
//
|
|
8185
|
-
//
|
|
8186
|
-
//
|
|
8187
|
-
//
|
|
8188
|
-
//
|
|
8189
|
-
//
|
|
8190
|
-
|
|
8928
|
+
// THE box-and-title layout of every chart type (flat's BOX block,
|
|
8929
|
+
// maptheme.js 18757-18925): single symbol, SEQUENCE, PIE and BAR all call
|
|
8930
|
+
// this. `extent`: the chart's drawn [x0, y0, x1, y1] around its anchor (px;
|
|
8931
|
+
// chart units with unit 1). o: { unit (px per flat unit, default 1), margin
|
|
8932
|
+
// (boxmargin, flat's 2), circular (CIRCULARBOX), radius / center (the
|
|
8933
|
+
// circle's base radius and center; default the extent's half longer side
|
|
8934
|
+
// and its center), titleText (with the titleunits suffix, '' / null: no
|
|
8935
|
+
// title), titleFont (px, or w => px for DTEXT), bottomTitle, alignRight,
|
|
8936
|
+
// wrap, measure }. Returns { rect (rectangular box), boxR / cx / cy (the
|
|
8937
|
+
// circular box: radius, center), w (the extent's width), title { text,
|
|
8938
|
+
// lines, x, anchor, top, baseline, f, extent } }.
|
|
8939
|
+
// The margin is min(2 m u, m w / 30); the title's first baseline 0.7 font
|
|
8940
|
+
// above the box (BOTTOMTITLE: one font below), wrapped to the chart's width
|
|
8941
|
+
// (the circle's diameter); a rectangular box grows to enclose the title,
|
|
8942
|
+
// its top trimmed by 0.1 font. arial: ascent 0.905, descent 0.212.
|
|
8943
|
+
function chartBoxLayout(extent, o) {
|
|
8191
8944
|
const e = extent.slice();
|
|
8192
|
-
const w = e[2] - e[0];
|
|
8193
|
-
const
|
|
8945
|
+
const w = e[2] - e[0], h = e[3] - e[1];
|
|
8946
|
+
const unit = o.unit == null ? 1 : o.unit;
|
|
8947
|
+
const circular = !!o.circular;
|
|
8948
|
+
const margin = Math.min(2 * o.margin * unit, o.margin * w / 30);
|
|
8949
|
+
const center = o.center || [(e[0] + e[2]) / 2, (e[1] + e[3]) / 2];
|
|
8950
|
+
const boxR = circular ? (o.radius != null ? o.radius : Math.max(w, h) / 2) * 1.1 + margin : 0;
|
|
8194
8951
|
let title = null;
|
|
8195
|
-
if (o.
|
|
8196
|
-
const f = o.titleFont;
|
|
8197
|
-
const measure = o.measure ||
|
|
8198
|
-
const lines =
|
|
8199
|
-
for (const word of String(o.title).split(/\s+/).filter(Boolean)) {
|
|
8200
|
-
const last = lines.length ? lines[lines.length - 1] + ' ' + word : null;
|
|
8201
|
-
if (last !== null && measure(last, f) <= w) lines[lines.length - 1] = last;
|
|
8202
|
-
else lines.push(word);
|
|
8203
|
-
}
|
|
8952
|
+
if (o.titleText) {
|
|
8953
|
+
const f = typeof o.titleFont === 'function' ? o.titleFont(w) : o.titleFont;
|
|
8954
|
+
const measure = o.measure || titleTextWidth;
|
|
8955
|
+
const lines = wrapTitleLines(o.titleText, f, circular ? boxR * 2 : w, measure).split('\n');
|
|
8204
8956
|
const width = Math.max(...lines.map(l => measure(l, f)));
|
|
8205
|
-
|
|
8206
|
-
const
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
8957
|
+
// first baseline; lines go down one font each
|
|
8958
|
+
const base = circular
|
|
8959
|
+
? (o.bottomTitle ? center[1] + boxR + f : center[1] - boxR - 0.7 * f - (lines.length - 1) * f)
|
|
8960
|
+
: (o.bottomTitle ? e[3] + f : e[1] - 0.7 * f - (lines.length - 1) * f);
|
|
8961
|
+
const x = circular ? center[0] + unit / 2 : (o.alignRight ? e[2] : e[0]);
|
|
8962
|
+
const anchor = circular ? 'middle' : (o.alignRight ? 'end' : 'start');
|
|
8963
|
+
const x0 = anchor === 'middle' ? x - width / 2 : anchor === 'end' ? x - width : x;
|
|
8964
|
+
title = { text: lines.join('\n'), lines, x, anchor, top: base - 0.8 * f, baseline: base, f,
|
|
8965
|
+
extent: [x0, base - 0.905 * f, x0 + width, base + (lines.length - 1) * f + 0.212 * f] };
|
|
8213
8966
|
}
|
|
8214
|
-
|
|
8967
|
+
let rect = null;
|
|
8968
|
+
if (!circular) {
|
|
8969
|
+
const b = e.slice();
|
|
8970
|
+
if (title) {
|
|
8971
|
+
b[0] = Math.min(b[0], title.extent[0]); b[1] = Math.min(b[1], title.extent[1]) + 0.1 * title.f;
|
|
8972
|
+
b[2] = Math.max(b[2], title.extent[2]); b[3] = Math.max(b[3], title.extent[3]);
|
|
8973
|
+
}
|
|
8974
|
+
rect = [b[0] - margin, b[1] - margin, b[2] + margin, b[3] + margin];
|
|
8975
|
+
}
|
|
8976
|
+
return { rect, boxR, cx: center[0], cy: center[1], w, margin, title };
|
|
8215
8977
|
}
|
|
8216
8978
|
|
|
8217
8979
|
// CHART|PIE / DONUT: one pie's slices as real ixmaps-flat lays them out —
|
|
@@ -8243,10 +9005,38 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8243
9005
|
// centered on 12 o'clock), HALF over 180° from 270° (HALFPLUS10 200°
|
|
8244
9006
|
// from 260°, HALFMINUS10 160° from 280°, HALFPLUS20 220° from 250°,
|
|
8245
9007
|
// HALFMINUS20 140° from 290°)
|
|
9008
|
+
// - STARBURST (piechart.js 246, 324-355, 415-420): every part >= 0, the
|
|
9009
|
+
// angles EQUAL (the value is the ray's radius, not its sweep) and the
|
|
9010
|
+
// percents NOT normalized — raw values. The FLOWER/RAYS variants
|
|
9011
|
+
// (XLFLOWER/XLRAYS, LFLOWER/LRAYS, XSFLOWER/XSRAYS, SFLOWER/SRAYS,
|
|
9012
|
+
// FLOWER/RAYS) cap the ray angle (maxAngle / 4 / 12 / 10 / none) and
|
|
9013
|
+
// inset a gap (10 / 20 / 40 / 33 / 35 % of it) on both sides of each
|
|
9014
|
+
// ray. The ray RADIUS is
|
|
9015
|
+
// computed in _buildPieLayers (it needs the theme's maxValue)
|
|
8246
9016
|
function pieSliceLayout(values, t, opts = {}) {
|
|
8247
9017
|
t = String(t || '');
|
|
8248
9018
|
const has = re => re.test(t);
|
|
8249
|
-
|
|
9019
|
+
// flat's STARBURST (piechart.js realize 246, 324-355, 415-420): the
|
|
9020
|
+
// parts get EQUAL angles — the value is the ray's radius, not its
|
|
9021
|
+
// sweep — and every part is clamped to >= 0. The FLOWER/RAYS
|
|
9022
|
+
// variants (flat maps XLFLOWER to XLRAYS & kin, maptheme.js
|
|
9023
|
+
// 20169-20186) cap the ray angle and leave a gap between the rays.
|
|
9024
|
+
// flat's `nRadInner = max(5, nRadInner)` for the ray variants is 5
|
|
9025
|
+
// units of the chart's INTERNAL coordinate system — measured on the
|
|
9026
|
+
// ISTAT story: 5 of the disc's 197 internal units, 0.28 screen px,
|
|
9027
|
+
// sub-pixel and invisible (the petals go to the center; an inner
|
|
9028
|
+
// radius shows only with DONUT) — so starInner here is 0 for the
|
|
9029
|
+
// ray variants too, and only DONUT+STARBURST starts its rays at
|
|
9030
|
+
// the donut's inner radius (see _buildPieLayers)
|
|
9031
|
+
const star = has(/STARBURST/);
|
|
9032
|
+
const starRay = !star ? null
|
|
9033
|
+
: /XLFLOWER|XLRAYS/.test(t) ? { cap: 4, gap: 0.10 }
|
|
9034
|
+
: /LFLOWER|LRAYS/.test(t) ? { cap: 4, gap: 0.20 }
|
|
9035
|
+
: /XSFLOWER|XSRAYS/.test(t) ? { cap: 12, gap: 0.40 }
|
|
9036
|
+
: /SFLOWER|SRAYS/.test(t) ? { cap: 10, gap: 0.33 }
|
|
9037
|
+
: /FLOWER|RAYS/.test(t) ? { cap: 1, gap: 0.35 }
|
|
9038
|
+
: { cap: 1, gap: 0 };
|
|
9039
|
+
const parts = values.map(v => (isNaN(v) ? 0 : star ? Math.max(0, v) : v));
|
|
8250
9040
|
const sum = parts.reduce((a, v) => a + v, 0);
|
|
8251
9041
|
const autocomplete = has(/AUTOCOMPLETE/);
|
|
8252
9042
|
if (sum === 0 && !autocomplete) return null;
|
|
@@ -8299,13 +9089,18 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8299
9089
|
// flat draws its "Pie error: no values !" circle here
|
|
8300
9090
|
if (!any && !autocomplete) return null;
|
|
8301
9091
|
const pctSum = slices.reduce((a, sl) => a + sl.percent, 0);
|
|
8302
|
-
|
|
9092
|
+
// flat's STARBURST keeps the parts' raw values (piechart.js 246-258:
|
|
9093
|
+
// no 100% normalization, no AUTOCOMPLETE) — the value is the radius
|
|
9094
|
+
if (!star && pctSum !== 100) {
|
|
8303
9095
|
if (autocomplete && pctSum < 100) slices.push({ i: -1, value: null, percent: Math.floor((100 - pctSum) * 100000) / 100000, complement: true });
|
|
8304
9096
|
else slices.forEach(sl => { sl.percent = sl.percent / pctSum * 100; });
|
|
8305
9097
|
}
|
|
8306
9098
|
let end = 0;
|
|
8307
9099
|
if (has(/SYMMETRIC/) && slices.length) {
|
|
8308
|
-
if (
|
|
9100
|
+
if (star) {
|
|
9101
|
+
// flat piechart.js 296-299: the first ray centered on 12 o'clock
|
|
9102
|
+
end = 360 - Math.floor(360 / parts.length / 2);
|
|
9103
|
+
} else if (has(/BIGTOTOP/)) {
|
|
8309
9104
|
end = 360;
|
|
8310
9105
|
let x = 0;
|
|
8311
9106
|
for (; x < slices.length && slices[x] !== biggest; x++) end -= slices[x].percent < 100 ? Math.floor(360 / 100 * slices[x].percent) : 0;
|
|
@@ -8326,6 +9121,20 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8326
9121
|
if (has(/HALFMINUS20/)) maxAngle = 140;
|
|
8327
9122
|
const out = [];
|
|
8328
9123
|
slices.forEach((sl, k) => {
|
|
9124
|
+
if (star) {
|
|
9125
|
+
// flat piechart.js 324-355, 395-403: the angles advance by the
|
|
9126
|
+
// FULL equal angle (the ray variants capped), the gap inset on
|
|
9127
|
+
// both sides of a ray; a ray's own sweep is that reduced angle
|
|
9128
|
+
const angle = Math.min(maxAngle / starRay.cap, maxAngle / parts.length);
|
|
9129
|
+
const base = end % 360;
|
|
9130
|
+
end = base + angle;
|
|
9131
|
+
if (end > 360) end = end % 360;
|
|
9132
|
+
const sweep = angle * (1 - 2 * starRay.gap);
|
|
9133
|
+
const start = base + angle * starRay.gap;
|
|
9134
|
+
if (sweep <= 0 || start < 0) return;
|
|
9135
|
+
out.push(Object.assign(sl, { start, sweep, k }));
|
|
9136
|
+
return;
|
|
9137
|
+
}
|
|
8329
9138
|
const sweep = sl.percent / 100 * maxAngle;
|
|
8330
9139
|
const start = end % 360;
|
|
8331
9140
|
end = start + sweep;
|
|
@@ -8452,6 +9261,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8452
9261
|
const parts = [], zeroNot = [];
|
|
8453
9262
|
for (let k = 0; k < o.fields.length; k++) {
|
|
8454
9263
|
let v = parseFloat(raw[o.fields[k]]);
|
|
9264
|
+
// flat's weights multiply every record's per-field value as it is
|
|
9265
|
+
// accumulated (maptheme.js 10506-10508) — the SIZE (nValueSize)
|
|
9266
|
+
// stays raw, exactly as there
|
|
9267
|
+
if (o.weights) v = v * o.weights(k);
|
|
8455
9268
|
zeroNot[k] = false;
|
|
8456
9269
|
if (isNaN(v)) {
|
|
8457
9270
|
if (!r.undefinedOk) return null;
|
|
@@ -8661,6 +9474,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8661
9474
|
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
|
8662
9475
|
return (h >>> 0) / 4294967296;
|
|
8663
9476
|
}
|
|
9477
|
+
// a flow's dash phase in [0, 1): flat's random one, the same for the
|
|
9478
|
+
// same arc on every rebuild
|
|
9479
|
+
function flowDashPhase(p1, p2) {
|
|
9480
|
+
return hashUnit('dash:' + p1[0] + ',' + p1[1] + '&' + p2[0] + ',' + p2[1]);
|
|
9481
|
+
}
|
|
8664
9482
|
|
|
8665
9483
|
// points of a cubic Bézier, n segments (n + 1 points)
|
|
8666
9484
|
function cubicBezierPoints(p0, c1, c2, p3, n) {
|
|
@@ -8804,7 +9622,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8804
9622
|
const word = w => { const re = new RegExp('\\b' + w + '\\b'); return [...flags].some(f => re.test(f)); };
|
|
8805
9623
|
const info = {
|
|
8806
9624
|
sort: word('SORT'), up: word('UP'), down: word('DOWN'),
|
|
8807
|
-
star: has('STAR') && !
|
|
9625
|
+
star: has('STAR') && !flatFlag(flags, 'CENTER'), horz: has('HORZ') && !flatFlag(flags, 'CENTER'),
|
|
8808
9626
|
expand: has('EXPAND'), expandmax: has('EXPANDMAX'), linear: has('LINEAR'), sizep1: has('SIZEP1'),
|
|
8809
9627
|
compress: has('COMPRESS'), compressmax: has('COMPRESSMAX')
|
|
8810
9628
|
};
|
|
@@ -8823,14 +9641,21 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8823
9641
|
const up = fl.up;
|
|
8824
9642
|
const maxR = opts.maxRadius || 0;
|
|
8825
9643
|
const nStarParts = opts.nParts || counts.length;
|
|
8826
|
-
|
|
9644
|
+
// flat's star radius (nStarRadius, 0 per chart) is set ONLY by the FIRST
|
|
9645
|
+
// part of the sorted order (maptheme.js 22301-22316: `if (i === 0)`), and a
|
|
9646
|
+
// part of value 0 is skipped before it (22222): a chart whose first part
|
|
9647
|
+
// (with SORT|UP the smallest) is 0 keeps star radius 0, so with UP every
|
|
9648
|
+
// part sits on the center — only a chart with ALL parts non-zero spreads
|
|
9649
|
+
// into its star. (A cell of a CATEGORICAL AGGREGATE theme has a zero part
|
|
9650
|
+
// for every category it lacks.)
|
|
9651
|
+
let angle = 0, starR = 0, x = 0;
|
|
8827
9652
|
order.forEach((p, k) => {
|
|
8828
9653
|
if (!p.v) return;
|
|
8829
9654
|
const r = radiusOf(p.v);
|
|
8830
9655
|
if (!(r > 0)) return;
|
|
8831
9656
|
let px = 0, py = 0;
|
|
8832
9657
|
if (star) {
|
|
8833
|
-
if (
|
|
9658
|
+
if (k === 0) {
|
|
8834
9659
|
starR = r;
|
|
8835
9660
|
if (fl.expand && fl.sort && up) {
|
|
8836
9661
|
const big = order[order.length - 1].v, mv = opts.maxValue || big || 1;
|
|
@@ -8838,7 +9663,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8838
9663
|
}
|
|
8839
9664
|
if (fl.compress) starR *= fl.compressmax ? 0.25 : 0.5;
|
|
8840
9665
|
}
|
|
8841
|
-
|
|
9666
|
+
// flat: `(i > 0 || UP) && nRadius` — i the part's index in the sorted order
|
|
9667
|
+
if (k > 0 || up) {
|
|
8842
9668
|
const c = starR + r;
|
|
8843
9669
|
const b = Math.sqrt(Math.max(0, c * c - r * r));
|
|
8844
9670
|
let f = 90 - Math.asin(Math.min(1, b / c)) / Math.PI * 180;
|
|
@@ -9052,6 +9878,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9052
9878
|
// which every other caller here still relies on unchanged.
|
|
9053
9879
|
function resolvePartsClass(value, partsA) {
|
|
9054
9880
|
if (!partsA || isNaN(value)) return null;
|
|
9881
|
+
// explicit ranges: a value below the first break is class 0, as flat's
|
|
9882
|
+
// class loop (value < parts[0].max) puts it
|
|
9883
|
+
if (partsA.length && partsA[0].clampLow && value < partsA[0].min) return 0;
|
|
9055
9884
|
for (let i = 0; i < partsA.length; i++) {
|
|
9056
9885
|
const isLast = i === partsA.length - 1;
|
|
9057
9886
|
const inLower = value >= partsA[i].min;
|
|
@@ -9384,7 +10213,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9384
10213
|
// 7740-7801): a value that is no number counts as 0 — unless
|
|
9385
10214
|
// UNDEFINEDISNOTVALUE —, and 0 is a value — unless ZEROISNOTVALUE
|
|
9386
10215
|
function flatNumber(v, flags) {
|
|
9387
|
-
|
|
10216
|
+
// flat's own value scan (maptheme.js __scanValue 9265-9272): a
|
|
10217
|
+
// value with a comma is EUROPEAN format — dots stripped as
|
|
10218
|
+
// thousands separators, the comma is the decimal ("78,8" -> 78.8);
|
|
10219
|
+
// a value without one is parsed plain, blanks stripped. Applied
|
|
10220
|
+
// to every theme value in flat, so also here
|
|
10221
|
+
const s = String(v);
|
|
10222
|
+
let n = s.includes(',') ? parseFloat(s.replace(/\./g, '').replace(/,/g, '.')) : parseFloat(s.replace(/ /g, ''));
|
|
9388
10223
|
if (isNaN(n)) n = flags && flags.has('UNDEFINEDISNOTVALUE') ? NaN : 0;
|
|
9389
10224
|
if (n === 0 && flags && flags.has('ZEROISNOTVALUE')) return NaN;
|
|
9390
10225
|
return n;
|
|
@@ -9395,11 +10230,35 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9395
10230
|
const bins = [arr.reduce((a, v) => (v < a ? v : a), Infinity), m];
|
|
9396
10231
|
while (arr.length > 1) {
|
|
9397
10232
|
arr = arr.filter(d => d > m);
|
|
10233
|
+
// every value left equals the last mean (a tie at the top): there is
|
|
10234
|
+
// no head above it — mean([]) would add a NaN break (flat's own bug)
|
|
10235
|
+
if (!arr.length) break;
|
|
9398
10236
|
m = mean(arr);
|
|
9399
10237
|
bins.push(m);
|
|
9400
10238
|
}
|
|
9401
10239
|
return bins;
|
|
9402
10240
|
}
|
|
10241
|
+
// Explicit class breaks: .style({ranges}) with the RANGES flag (flat,
|
|
10242
|
+
// maptheme.js 2324 / 12905-12960). n+1 break values (a comma or "|"
|
|
10243
|
+
// separated string, or an array) are n classes [r_i, r_i+1] — a class
|
|
10244
|
+
// max is nudged by 1e-9 as flat does, so a break value belongs to the class
|
|
10245
|
+
// below it. Not for CATEGORICAL (there the values are exact categories).
|
|
10246
|
+
// null when the theme has no usable explicit ranges.
|
|
10247
|
+
function explicitRangeBreaks(style, flags) {
|
|
10248
|
+
if (!flags || !flags.has('RANGES') || flags.has('CATEGORICAL')) return null;
|
|
10249
|
+
let r = style && style.ranges;
|
|
10250
|
+
if (typeof r === 'string') r = r.split(r.includes('|') ? '|' : ',');
|
|
10251
|
+
if (!Array.isArray(r)) return null;
|
|
10252
|
+
r = r.map(Number);
|
|
10253
|
+
return r.length > 1 && r.every(isFinite) ? r : null;
|
|
10254
|
+
}
|
|
10255
|
+
function explicitRangeParts(breaks) {
|
|
10256
|
+
const parts = [];
|
|
10257
|
+
for (let i = 0; i < breaks.length - 1; i++) parts.push({ min: breaks[i], max: breaks[i + 1] + 0.000000001 });
|
|
10258
|
+
parts[0].clampLow = true;
|
|
10259
|
+
return parts;
|
|
10260
|
+
}
|
|
10261
|
+
|
|
9403
10262
|
function flatRangeParts(values, nMin, nMax, nParts, flags, centerValue) {
|
|
9404
10263
|
const rawMin = nMin, rawMax = nMax;
|
|
9405
10264
|
if (centerValue != null && !isNaN(centerValue)) {
|
|
@@ -9424,6 +10283,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9424
10283
|
// values above the last mean, while more than one is left
|
|
9425
10284
|
if (flags.has('HEADTAIL')) {
|
|
9426
10285
|
const b = headTailBreaks(values);
|
|
10286
|
+
// fewer breaks than classes (small or low-variance data): the surplus
|
|
10287
|
+
// classes padded at the top with the last break, as naturalBreaks
|
|
10288
|
+
// does — flat reads past the end (undefined bounds, records in no class)
|
|
10289
|
+
while (b.length < nParts + 1) b.push(b[b.length - 1]);
|
|
9427
10290
|
parts = new Array(nParts).fill(null).map((_, i) => ({ min: b[i], max: b[i + 1] - 0.000001 }));
|
|
9428
10291
|
} else if (flags.has('QUANTILE')) parts = quantileBreaks(values, nParts);
|
|
9429
10292
|
else if (flags.has('NATURAL')) parts = naturalBreaks(values, nParts);
|
|
@@ -9523,6 +10386,44 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9523
10386
|
// On copies of the features' properties — the page's data is not changed.
|
|
9524
10387
|
// AGGREGATE themes: not here — flat computes field100 on the aggregated
|
|
9525
10388
|
// sums (aggregateField100).
|
|
10389
|
+
// .style({weights}) — flat's weighted values: every record's per-field
|
|
10390
|
+
// value × its field's weight, BEFORE the field100 arithmetic (the
|
|
10391
|
+
// per-record multiply in addItemValues, maptheme.js 9007-9009; the
|
|
10392
|
+
// aggregate loader reads the weight per value field at 10506-10508).
|
|
10393
|
+
// One number, or a pipe list with one weight per value field; a field
|
|
10394
|
+
// beyond the list takes the first weight (flat: `szWeightsA[i] ||
|
|
10395
|
+
// szWeightsA[0]`), the aggregate loader instead repeats the LAST one
|
|
10396
|
+
// (10262-10266) — `repeatLast`; no weights at all leaves the values raw.
|
|
10397
|
+
// The Milano page: weights 0.1 over km-long street lengths + FRACTION →
|
|
10398
|
+
// cars per 100 m
|
|
10399
|
+
function fieldWeightOf(style, nFields, repeatLast) {
|
|
10400
|
+
const w = style && style.weights;
|
|
10401
|
+
if (w == null || w === '') return null;
|
|
10402
|
+
// flat's toArray (maptheme.js 999-1014): split on '|' when the
|
|
10403
|
+
// string has one, else on ',' — "1,10,10" is a THREE-weight list
|
|
10404
|
+
// (the Happy Planet page), not one weight of 1
|
|
10405
|
+
const s = Array.isArray(w) ? w.join('|') : String(w);
|
|
10406
|
+
const list = (s.includes('|') ? s.split('|') : s.split(',')).map(v => parseFloat(v));
|
|
10407
|
+
if (!list.some(v => !isNaN(v))) return null;
|
|
10408
|
+
if (repeatLast) { const last = list[list.length - 1]; while (list.length < nFields) list.push(last); }
|
|
10409
|
+
return i => list[i] || list[0] || 1;
|
|
10410
|
+
}
|
|
10411
|
+
// flat's weighted record for the AGGREGATE path (maptheme.js 10506-10508):
|
|
10412
|
+
// the value fields' own values weighted — the class value and the field100
|
|
10413
|
+
// value stay raw, exactly as flat reads them
|
|
10414
|
+
function weightedRecord(f, weightOf) {
|
|
10415
|
+
// a pie record's parts were weighted in flatPieRecord, and its size
|
|
10416
|
+
// (nValueSize) stays raw — nothing to do here
|
|
10417
|
+
if (f.properties && f.properties.pie) return f;
|
|
10418
|
+
const p = Object.assign({}, f.properties);
|
|
10419
|
+
if (p.value !== undefined && !isNaN(p.value)) p.value = p.value * weightOf(0);
|
|
10420
|
+
if (Array.isArray(p.series)) p.series = p.series.map((v, i) => (isNaN(v) ? v : v * weightOf(i)));
|
|
10421
|
+
// the multi-field pie's parts — the SAME per-field values as series,
|
|
10422
|
+
// but the grid cell's pie accumulates THESE (flatPieAccumulate's
|
|
10423
|
+
// rec.parts), so they carry the weights too
|
|
10424
|
+
if (Array.isArray(p.parts)) p.parts = p.parts.map((v, i) => (isNaN(v) ? v : v * weightOf(i)));
|
|
10425
|
+
return Object.assign({}, f, { properties: p });
|
|
10426
|
+
}
|
|
9526
10427
|
function applyField100(features, binding, flags, style) {
|
|
9527
10428
|
if (!binding || !binding.value || !Array.isArray(features)) return features;
|
|
9528
10429
|
// AGGREGATE: flat applies field100 to the aggregated sums
|
|
@@ -9532,7 +10433,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9532
10433
|
const fields = String(binding.value).split('|');
|
|
9533
10434
|
const auto100 = flags.has('AUTO100') && !flags.has('AGGREGATE');
|
|
9534
10435
|
const seriesDifference = flags.has('DIFFERENCE') && fields.length > 1;
|
|
9535
|
-
|
|
10436
|
+
// flat's weights run even without a field100 (addItemValues multiplies
|
|
10437
|
+
// every record's values, maptheme.js 9007-9009)
|
|
10438
|
+
const weightOf = fieldWeightOf(style, fields.length, false);
|
|
10439
|
+
if (!f100 && !auto100 && !seriesDifference && !weightOf) return features;
|
|
9536
10440
|
const f100A = f100 ? String(f100).split('|') : null;
|
|
9537
10441
|
const fractionScale = styleNum(style && style.fractionscale) || 1;
|
|
9538
10442
|
const names = fields.map(field => (field[0] === '!' ? field.slice(1) : field));
|
|
@@ -9540,12 +10444,16 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9540
10444
|
return features.map(f => {
|
|
9541
10445
|
if (!f || !f.properties) return f;
|
|
9542
10446
|
const props = Object.assign({}, f.properties);
|
|
9543
|
-
let values = names.map(name =>
|
|
10447
|
+
let values = names.map(name => flatScanValue(f.properties[name]));
|
|
10448
|
+
// the weights run BEFORE the field100 arithmetic, as in flat's
|
|
10449
|
+
// addItemValues — the FRACTION of a weighted value is the weighted
|
|
10450
|
+
// per-unit value
|
|
10451
|
+
if (weightOf) values = values.map((v, i) => (isNaN(v) ? v : v * weightOf(i)));
|
|
9544
10452
|
if (f100A || auto100) {
|
|
9545
10453
|
const sum = auto100 ? values.reduce((a, v) => a + (isNaN(v) ? 0 : v), 0) : 0;
|
|
9546
10454
|
values = values.map((v, i) => {
|
|
9547
10455
|
if (isNaN(v)) return v;
|
|
9548
|
-
const v100 = auto100 ? sum :
|
|
10456
|
+
const v100 = auto100 ? sum : flatScanValue(f.properties[f100A[Math.min(i, f100A.length - 1)]]);
|
|
9549
10457
|
if (v === 0 && !flags.has('ZEROISVALUE')) return 0;
|
|
9550
10458
|
if (fields[i][0] === '!') return (isNaN(v100) ? 0 : v100) - v;
|
|
9551
10459
|
if (flags.has('CALCVAL') || flags.has('CALC100')) return v100 ? Math.round(v * v100 / 100) : v;
|
|
@@ -9591,7 +10499,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9591
10499
|
// DIFFERENCE over several value fields: the difference is the value
|
|
9592
10500
|
const fields = String(binding.value).split('|');
|
|
9593
10501
|
const valueOf = flags && flags.has('DIFFERENCE') && fields.length > 1
|
|
9594
|
-
|
|
10502
|
+
// without AGGREGATE applyField100 already differenced every record
|
|
10503
|
+
// (flat's nValuesA, popped): the first field IS the value
|
|
10504
|
+
? (!flags.has('AGGREGATE') ? p => flatNumber(p[fields[0]], flags)
|
|
10505
|
+
: p => cellAggregatedValues({ sums: fields.map(k => parseFloat(p[k]) || 0), counts: fields.map(() => 1) }, flags, style)[0])
|
|
9595
10506
|
// "$item$" is flat's record count: 1 per record
|
|
9596
10507
|
: binding.value === '$item$' ? () => 1
|
|
9597
10508
|
: p => (p && Object.keys(p).length ? flatNumber(p[binding.value], flags) : NaN);
|
|
@@ -9618,11 +10529,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9618
10529
|
out._valueMax = nMax;
|
|
9619
10530
|
const sorted = values.slice().sort((a, b) => a - b);
|
|
9620
10531
|
out._valueMedian = sorted[Math.floor((sorted.length - 1) / 2)];
|
|
9621
|
-
const
|
|
10532
|
+
const breaks = explicitRangeBreaks(style, flags);
|
|
10533
|
+
const nParts = breaks ? breaks.length - 1
|
|
10534
|
+
: Math.trunc(styleNum(style.classes)) || colorSchemeClassCount(style.colorscheme) || DEFAULT_RANGE_CLASSES;
|
|
9622
10535
|
const placeholders = new Array(nParts).fill('');
|
|
9623
|
-
const colorsRgb = resolveClassColors(style.colorscheme, placeholders, style.classes);
|
|
10536
|
+
const colorsRgb = resolveClassColors(style.colorscheme, placeholders, breaks ? nParts : style.classes);
|
|
9624
10537
|
|
|
9625
|
-
out.partsA = flatRangeParts(values, nMin, nMax, nParts, flags, styleNum(style.rangecentervalue));
|
|
10538
|
+
out.partsA = breaks ? explicitRangeParts(breaks) : flatRangeParts(values, nMin, nMax, nParts, flags, styleNum(style.rangecentervalue));
|
|
9626
10539
|
out._nRangeParts = nParts; // the configured count; partsA may have fewer (all values equal)
|
|
9627
10540
|
|
|
9628
10541
|
out.categoryLabels = out.partsA.map(p => `${formatValue(p.min)} - ${formatValue(p.max)}`);
|
|
@@ -9760,6 +10673,15 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9760
10673
|
// (maptheme.js 11086-11101): each value becomes the next minus it
|
|
9761
10674
|
// (RELATIVE: in % of it when it is above field100min, else 0), the last
|
|
9762
10675
|
// one is dropped
|
|
10676
|
+
// How a record joins its cell's running value (flat's MAX / MIN / SUM
|
|
10677
|
+
// aggregation, maptheme.js 10880-10901): MAX / MIN keep the extreme
|
|
10678
|
+
// (a missing part counts 0, as in flat), SUM adds. `first`: the cell's
|
|
10679
|
+
// first record, which just starts the value.
|
|
10680
|
+
function aggCombiner(flags) {
|
|
10681
|
+
const max = flags.has('MAX'), min = flags.has('MIN');
|
|
10682
|
+
return (acc, v, first) => (first ? v : max ? Math.max(acc || 0, v) : min ? Math.min(acc || 0, v) : acc + v);
|
|
10683
|
+
}
|
|
10684
|
+
|
|
9763
10685
|
function cellAggregatedValues(cell, flags, style) {
|
|
9764
10686
|
const useMean = flags.has('MEAN');
|
|
9765
10687
|
const values = cell.sums.map((s, i) => (useMean && cell.counts[i] > 0 ? s / cell.counts[i] : s));
|
|
@@ -9825,6 +10747,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9825
10747
|
// relocates a field aggregation, maptheme.js 11141) — instead of a grid
|
|
9826
10748
|
function aggregateOnGrid(features, zoom, cellPx, flags, post, field) {
|
|
9827
10749
|
const relocate = flags.has('RELOCATE') || !!field;
|
|
10750
|
+
const combine = aggCombiner(flags);
|
|
10751
|
+
// MAX / MIN aggregation: the cell IS one defined record — the one
|
|
10752
|
+
// holding the biggest (smallest) size — so its title follows THAT
|
|
10753
|
+
// record, not the first one's (flat re-sets the merging record's
|
|
10754
|
+
// szTitle under __fMax, maptheme.js 10870-10874; here it is the
|
|
10755
|
+
// record that actually holds it)
|
|
10756
|
+
const maxAgg = flags.has('MAX'), minAgg = flags.has('MIN');
|
|
9828
10757
|
const cells = new Map();
|
|
9829
10758
|
for (const f of features) {
|
|
9830
10759
|
const [lng, lat] = f.geometry.coordinates;
|
|
@@ -9846,34 +10775,54 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9846
10775
|
(cell.raws = cell.raws || []).push(f.properties.raw);
|
|
9847
10776
|
}
|
|
9848
10777
|
cell.sumX += p.x; cell.sumY += p.y; cell.n++;
|
|
9849
|
-
cell.
|
|
9850
|
-
|
|
9851
|
-
if (
|
|
10778
|
+
const first = cell.n === 1;
|
|
10779
|
+
cell.value = combine(cell.value, f.properties.value, first);
|
|
10780
|
+
if (f.properties.classValue !== undefined) cell.classValue = combine(cell.classValue, f.properties.classValue, first);
|
|
10781
|
+
if (post) cell.value100 = combine(cell.value100, f.properties.value100 || 0, first);
|
|
9852
10782
|
// an AGGREGATE / CATEGORICAL pie's record (flatPieRecord): its parts
|
|
9853
10783
|
// and size added to the cell's item as flat does (flatPieAccumulate)
|
|
9854
10784
|
if (f.properties.pie) cell.pie = flatPieAccumulate(cell.pie || null, f.properties.pie, flags);
|
|
9855
10785
|
const series = f.properties.series;
|
|
9856
10786
|
if (series) {
|
|
9857
10787
|
if (!cell.series) cell.series = new Array(series.length).fill(0);
|
|
9858
|
-
for (let i = 0; i < series.length; i++) cell.series[i]
|
|
10788
|
+
for (let i = 0; i < series.length; i++) cell.series[i] = combine(cell.series[i], series[i], first);
|
|
10789
|
+
}
|
|
10790
|
+
// a multi-field SEQUENCE record's size-field value (flat's
|
|
10791
|
+
// nValueSize): MAX/MIN/summed into the item's nSize, as flat does
|
|
10792
|
+
if (f.properties.size !== undefined) cell.size = combine(typeof cell.size === 'number' ? cell.size : 0, f.properties.size, first);
|
|
10793
|
+
// MAX / MIN: which record the cell's title belongs to (see the
|
|
10794
|
+
// comment at the top — the size field carries the magnitude, else
|
|
10795
|
+
// the combined value)
|
|
10796
|
+
if (maxAgg || minAgg) {
|
|
10797
|
+
const m = typeof f.properties.size === 'number' ? Math.abs(f.properties.size) : Math.abs(f.properties.value || 0);
|
|
10798
|
+
if (cell.titleMax === undefined || (maxAgg ? m > cell.titleMax : m < cell.titleMax)) {
|
|
10799
|
+
cell.titleMax = m;
|
|
10800
|
+
cell.titleFrom = f;
|
|
10801
|
+
cell.titleX = p.x; cell.titleY = p.y;
|
|
10802
|
+
}
|
|
9859
10803
|
}
|
|
9860
10804
|
}
|
|
9861
10805
|
const out = [];
|
|
9862
10806
|
for (const cell of cells.values()) {
|
|
9863
|
-
|
|
9864
|
-
|
|
10807
|
+
// RELOCATE: at the records' mean position — with MAX / MIN at the
|
|
10808
|
+
// record holding the max (min), the one the cell IS (and its title's);
|
|
10809
|
+
// only the position follows it, never a value
|
|
10810
|
+
const atTitle = relocate && cell.titleFrom;
|
|
10811
|
+
const x = atTitle ? cell.titleX : relocate ? cell.sumX / cell.n : cell.snapped.x;
|
|
10812
|
+
const y = atTitle ? cell.titleY : relocate ? cell.sumY / cell.n : cell.snapped.y;
|
|
9865
10813
|
const ll = worldPixelToLngLat(x, y, zoom);
|
|
9866
10814
|
const geometry = { type: 'Point', coordinates: [ll.lng, ll.lat] };
|
|
9867
10815
|
const hasClass = cell.first.properties.classValue !== undefined;
|
|
9868
10816
|
let props = cell.n > 1
|
|
9869
|
-
// titleRaw: the cell's
|
|
9870
|
-
//
|
|
10817
|
+
// titleRaw: the record the cell's title belongs to — with MAX/MIN
|
|
10818
|
+
// the one holding the max (min), else the first (flat's itemTitle)
|
|
9871
10819
|
? { cluster: true, point_count: cell.n, value: cell.value, ...(hasClass ? { classValue: cell.classValue } : {}),
|
|
9872
|
-
titleRaw: cell.first.properties.raw || cell.first.properties,
|
|
10820
|
+
titleRaw: (cell.titleFrom || cell.first).properties.raw || (cell.titleFrom || cell.first).properties,
|
|
9873
10821
|
...(cell.raws && cell.raws.length ? { raws: cell.raws } : {}) }
|
|
9874
10822
|
: cell.first.properties;
|
|
9875
10823
|
if (cell.series && cell.n > 1) props = Object.assign({}, props, { series: cell.series });
|
|
9876
10824
|
if (cell.pie) props = Object.assign({}, props, { pie: cell.pie });
|
|
10825
|
+
if (typeof cell.size === 'number' && cell.n > 1) props = Object.assign({}, props, { size: cell.size });
|
|
9877
10826
|
// a field aggregation's cell key (the field value): the categories of
|
|
9878
10827
|
// one cell are merged by it (groupCoLocated), not by their positions
|
|
9879
10828
|
if (field) props = Object.assign({}, props, { aggKey: cell.key });
|
|
@@ -9937,7 +10886,18 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9937
10886
|
let cell = cells.get(key);
|
|
9938
10887
|
if (!cell) { cell = { sumX: 0, sumY: 0, n: 0, cx: snapped.x, cy: snapped.y, counts: new Array(n).fill(0), recordCounts: new Array(n).fill(0), classTotal: undefined }; cells.set(key, cell); }
|
|
9939
10888
|
cell.sumX += p.x; cell.sumY += p.y; cell.n++;
|
|
9940
|
-
|
|
10889
|
+
// the cell's title: with MAX/MIN aggregation the member holding the
|
|
10890
|
+
// biggest (smallest) size (the cell IS that one item), else the
|
|
10891
|
+
// first — flat's itemTitle
|
|
10892
|
+
const maxAgg = flags.has('MAX'), minAgg = flags.has('MIN');
|
|
10893
|
+
const titleOf = f => f.properties.titleRaw || f.properties.raw || null;
|
|
10894
|
+
if (maxAgg || minAgg) {
|
|
10895
|
+
const m = typeof f.properties.size === 'number' ? Math.abs(f.properties.size) : Math.abs(f.properties.total || f.properties.value || 0);
|
|
10896
|
+
if (cell.titleMax === undefined || (maxAgg ? m > cell.titleMax : m < cell.titleMax)) {
|
|
10897
|
+
cell.titleMax = m;
|
|
10898
|
+
cell.titleRaw = titleOf(f);
|
|
10899
|
+
}
|
|
10900
|
+
} else if (!cell.titleRaw) cell.titleRaw = titleOf(f);
|
|
9941
10901
|
// the members' own records, for the tooltip's {{theme.item.data}} —
|
|
9942
10902
|
// flat's __add_data tables them (numbered, up to 50) for aggregated
|
|
9943
10903
|
// items too; references only, so no copy is made. A member can itself
|
|
@@ -9952,6 +10912,18 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9952
10912
|
}
|
|
9953
10913
|
cell.counts[f.properties.cat] += f.properties.value;
|
|
9954
10914
|
if (f.properties.classValue !== undefined) cell.classTotal = (cell.classTotal || 0) + f.properties.classValue;
|
|
10915
|
+
// a multi-field SEQUENCE cell's per-field parts and its size-field
|
|
10916
|
+
// value (see _buildAggregationIndex) — combined with the theme's
|
|
10917
|
+
// own MAX/MIN/sum combiner, so a re-grouped cell keeps them
|
|
10918
|
+
const combineSeries = cell.combineSeries || (cell.combineSeries = aggCombiner(flags));
|
|
10919
|
+
const memberSeries = f.properties.series;
|
|
10920
|
+
if (memberSeries) {
|
|
10921
|
+
if (!cell.series) cell.series = new Array(memberSeries.length).fill(0);
|
|
10922
|
+
for (let i = 0; i < memberSeries.length; i++) cell.series[i] = combineSeries(cell.series[i], memberSeries[i], cell.n === 1);
|
|
10923
|
+
}
|
|
10924
|
+
if (typeof f.properties.size === 'number') {
|
|
10925
|
+
cell.size = combineSeries(typeof cell.size === 'number' ? cell.size : 0, f.properties.size, cell.n === 1);
|
|
10926
|
+
}
|
|
9955
10927
|
// point_count is the aggregated-record count of a multi-record
|
|
9956
10928
|
// cell (aggregateOnGrid; absent on a single-record cell)
|
|
9957
10929
|
// — tracked separately from counts (the summed bound VALUE) purely
|
|
@@ -9965,7 +10937,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9965
10937
|
geometry: { type: 'Point', coordinates: [ll.lng, ll.lat] },
|
|
9966
10938
|
properties: { counts: cell.counts, total: cell.counts.reduce((a, c) => a + c, 0), recordCounts: cell.recordCounts, ...(cell.classTotal !== undefined ? { classTotal: cell.classTotal } : {}),
|
|
9967
10939
|
...(cell.raws && cell.raws.length ? { raws: cell.raws } : {}),
|
|
9968
|
-
...(cell.titleRaw ? { titleRaw: cell.titleRaw } : {})
|
|
10940
|
+
...(cell.titleRaw ? { titleRaw: cell.titleRaw } : {}),
|
|
10941
|
+
...(cell.series ? { series: cell.series } : {}),
|
|
10942
|
+
...(typeof cell.size === 'number' ? { size: cell.size } : {}) }
|
|
9969
10943
|
};
|
|
9970
10944
|
});
|
|
9971
10945
|
}
|
|
@@ -10136,17 +11110,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10136
11110
|
// could ever need it (a plain "px" gridwidth, or an aggregation
|
|
10137
11111
|
// array with only "px" entries, never reads this field).
|
|
10138
11112
|
const gridwidthIsMeters = typeof this.style.gridwidth === 'string' ? !/px\s*$/i.test(this.style.gridwidth) : this.style.gridwidth != null;
|
|
10139
|
-
|
|
10140
|
-
|
|
10141
|
-
|
|
10142
|
-
const lat = f.geometry && f.geometry.coordinates && f.geometry.coordinates[1];
|
|
10143
|
-
if (typeof lat === 'number' && !isNaN(lat)) {
|
|
10144
|
-
if (lat < minLat) minLat = lat;
|
|
10145
|
-
if (lat > maxLat) maxLat = lat;
|
|
10146
|
-
}
|
|
10147
|
-
});
|
|
10148
|
-
this._gridRefLat = (minLat <= maxLat) ? (minLat + maxLat) / 2 : 0;
|
|
10149
|
-
}
|
|
11113
|
+
// aggregationscale: flat's alias of aggregation, read the same way in
|
|
11114
|
+
// _ensureClusterIndices
|
|
11115
|
+
if (gridwidthIsMeters || aggregationHasMetersEntry(this.style.aggregation || this.style.aggregationscale)) this._scanGridRefLat();
|
|
10150
11116
|
|
|
10151
11117
|
// a bar chart's parts are a pie's (its value fields or categories)
|
|
10152
11118
|
if ((this._isPieChart() || this._isBarChart()) && this.binding.value) {
|
|
@@ -10233,6 +11199,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10233
11199
|
let max = 0;
|
|
10234
11200
|
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; }
|
|
10235
11201
|
this._multiFieldPartMax = max || 1;
|
|
11202
|
+
// AUTO100's scale, measured on these features by _buildSequenceChartLayers
|
|
11203
|
+
this._seqAuto100Max = null;
|
|
10236
11204
|
} else if (this.binding.value) {
|
|
10237
11205
|
// real engine's OTHER coloring mode (maptheme.js distributeValues,
|
|
10238
11206
|
// partsA): a NUMERIC bound value, not CATEGORICAL, is classed into
|
|
@@ -10369,6 +11337,33 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10369
11337
|
// pure: resolveComposedColor → see "Classification — pure functions"
|
|
10370
11338
|
_resolveComposedColor(props) { return resolveComposedColor(this, props); }
|
|
10371
11339
|
|
|
11340
|
+
// flat's DOPACITY value inside the COMPOSECOLOR branch (maptheme.js
|
|
11341
|
+
// 13489-13505): nMyValue = the item's raw weighted value sum /
|
|
11342
|
+
// ((field100/100) || 1), the max the color blend's own (nMax). The
|
|
11343
|
+
// raw weighted sum is reconstructed from the part values the field100
|
|
11344
|
+
// arithmetic left in the properties — those carry the arithmetic's own
|
|
11345
|
+
// multiplier (FRACTION ×fractionscale, PERMILLE ×1000, the default
|
|
11346
|
+
// percent ×100, see applyField100), which must be divided back out:
|
|
11347
|
+
// flat's nValueSum is the RAW weighted sum, whatever the flags.
|
|
11348
|
+
// (RELATIVE/INVERT subtract after the multiply — not reconstructable
|
|
11349
|
+
// from the parts; flat's own composed branch never pairs them.)
|
|
11350
|
+
_composedDopacityAlpha(props) {
|
|
11351
|
+
const fields = this._multiFields || [];
|
|
11352
|
+
const f100A = this.binding && this.binding.field100 ? String(this.binding.field100).split('|') : null;
|
|
11353
|
+
const multiplier = this.flags.has('FRACTION') ? (styleNum(this.style.fractionscale) || 1)
|
|
11354
|
+
: this.flags.has('PERMILLE') ? 1000 : 100;
|
|
11355
|
+
let sum = 0;
|
|
11356
|
+
fields.forEach((field, i) => {
|
|
11357
|
+
const part = parseFloat(props[field]);
|
|
11358
|
+
if (isNaN(part)) return;
|
|
11359
|
+
const v100 = f100A ? parseFloat(props[f100A[Math.min(i, f100A.length - 1)]]) : NaN;
|
|
11360
|
+
sum += isNaN(v100) ? part : (part * v100) / multiplier;
|
|
11361
|
+
});
|
|
11362
|
+
const v100_0 = f100A ? parseFloat(props[f100A[0]]) : NaN;
|
|
11363
|
+
const nMyValue = sum / ((v100_0 / 100) || 1);
|
|
11364
|
+
return composedDopacityAlpha(this.style, this.flags, nMyValue, this._composeColorMax || 1);
|
|
11365
|
+
}
|
|
11366
|
+
|
|
10372
11367
|
// pure: buildPartsA → see "Classification — pure functions"
|
|
10373
11368
|
_buildPartsA() { const r = computeRangeClasses(this.features, this.binding, this.style, this.flags, v => this._formatTooltipValue(v)); if (r) Object.assign(this, r); }
|
|
10374
11369
|
|
|
@@ -10395,6 +11390,14 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10395
11390
|
_resolveAggregateValue(props) { return resolveAggregateValue(this.binding, this.flags, props); }
|
|
10396
11391
|
|
|
10397
11392
|
_buildAggregationIndex(sourceFeatures) {
|
|
11393
|
+
// only records with a point position are drawn: a GeoJSON feature may
|
|
11394
|
+
// have geometry null (RFC 7946 — open-data exports of unlocated
|
|
11395
|
+
// records), or a shape chartAnchoredFeatures could not anchor; they
|
|
11396
|
+
// stay in the data (counts, facets), as on flat, but have no chart
|
|
11397
|
+
sourceFeatures = sourceFeatures.filter(f => {
|
|
11398
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
11399
|
+
return !!c && typeof c[0] === 'number' && isFinite(c[0]) && isFinite(c[1]);
|
|
11400
|
+
});
|
|
10398
11401
|
// an AGGREGATE / CATEGORICAL pie: one bucket, each record carrying its
|
|
10399
11402
|
// contribution to its item (flatPieRecord), added up per position or
|
|
10400
11403
|
// grid cell by aggregateOnGrid (flatPieAccumulate)
|
|
@@ -10404,9 +11407,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10404
11407
|
const rules = flatValueRules(t, fields ? fields.length : 1);
|
|
10405
11408
|
const sizeField = this.binding.size && this.binding.size !== '$item$' ? this.binding.size : null;
|
|
10406
11409
|
const feats = [];
|
|
11410
|
+
// flat's per-field weights (maptheme.js 10506-10508), applied to
|
|
11411
|
+
// every record's parts inside flatPieRecord
|
|
11412
|
+
const pieWeightOf = fieldWeightOf(this.style, fields ? fields.length : 1, true);
|
|
10407
11413
|
for (const f of sourceFeatures) {
|
|
10408
|
-
const c = f.geometry && f.geometry.coordinates;
|
|
10409
|
-
if (!c || typeof c[0] !== 'number' || !isFinite(c[0]) || !isFinite(c[1])) continue;
|
|
10410
11414
|
const p = f.properties || {};
|
|
10411
11415
|
let cat;
|
|
10412
11416
|
if (this._pieCategorical) {
|
|
@@ -10414,7 +11418,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10414
11418
|
const i = k == null ? undefined : this._pieCategoryIndex.get(k);
|
|
10415
11419
|
cat = i == null ? -1 : i;
|
|
10416
11420
|
}
|
|
10417
|
-
const pie = flatPieRecord(p, { rules, sizeField, cat, fields });
|
|
11421
|
+
const pie = flatPieRecord(p, { rules, sizeField, cat, fields, weights: pieWeightOf });
|
|
10418
11422
|
if (!pie) continue;
|
|
10419
11423
|
feats.push({ type: 'Feature', geometry: f.geometry, properties: { value: pie.size, pie, raw: p } });
|
|
10420
11424
|
}
|
|
@@ -10437,11 +11441,18 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10437
11441
|
// 7-class color once it knows each cell's actual aggregated total.
|
|
10438
11442
|
if (this._multiFields && this._isMultiFieldSequence()) {
|
|
10439
11443
|
// one bucket; each record carries its field values as the chart's
|
|
10440
|
-
// parts
|
|
10441
|
-
//
|
|
11444
|
+
// parts — and, the same array, as `series` so that an AGGREGATE
|
|
11445
|
+
// theme's grid cells combine them PER FIELD (aggregateOnGrid), as
|
|
11446
|
+
// flat accumulates its per-field nValuesA (maptheme.js 10876-10889)
|
|
11447
|
+
// — plus its size-field value (flat's nValueSize, the item nSize
|
|
11448
|
+
// MAX/MIN/sum). Its value — the draw order — is the first field's,
|
|
11449
|
+
// as flat's chart node value (NYC status: value="63" = e-bikes)
|
|
11450
|
+
const sizeField = this.binding.size && this.binding.size !== '$item$' ? this.binding.size : null;
|
|
10442
11451
|
this._featuresByCategory = [sourceFeatures.map(f => {
|
|
10443
11452
|
const parts = this._multiFields.map(k => parseFloat(f.properties[k]) || 0);
|
|
10444
|
-
|
|
11453
|
+
const size = sizeField ? Math.abs(parseFloat(f.properties[sizeField]) || 0) : undefined;
|
|
11454
|
+
return { type: 'Feature', geometry: f.geometry,
|
|
11455
|
+
properties: { value: parts[0], parts, series: parts, ...(size !== undefined ? { size } : {}), raw: f.properties } };
|
|
10445
11456
|
})];
|
|
10446
11457
|
this._clusterIndices = null;
|
|
10447
11458
|
this._clusterRadiusPx = null;
|
|
@@ -10466,7 +11477,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10466
11477
|
const seriesFields = String(this.binding.value || '').split('|');
|
|
10467
11478
|
this._seriesDifference = this.flags.has('DIFFERENCE') && seriesFields.length > 1 && !this._aggregateField100;
|
|
10468
11479
|
const seriesOf = props => seriesFields.map(k => parseFloat(props[k]) || 0);
|
|
10469
|
-
|
|
11480
|
+
// without AGGREGATE the records are already differenced (applyField100)
|
|
11481
|
+
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];
|
|
10470
11482
|
this._featuresByCategory = [sourceFeatures.map(f => ({
|
|
10471
11483
|
type: 'Feature',
|
|
10472
11484
|
geometry: f.geometry,
|
|
@@ -10824,6 +11836,20 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10824
11836
|
});
|
|
10825
11837
|
}
|
|
10826
11838
|
|
|
11839
|
+
// the meters grid's reference latitude: the middle of the data's own
|
|
11840
|
+
// latitude extent (see _prepare)
|
|
11841
|
+
_scanGridRefLat() {
|
|
11842
|
+
let minLat = Infinity, maxLat = -Infinity;
|
|
11843
|
+
this.features.forEach(f => {
|
|
11844
|
+
const lat = f.geometry && f.geometry.coordinates && f.geometry.coordinates[1];
|
|
11845
|
+
if (typeof lat === 'number' && !isNaN(lat)) {
|
|
11846
|
+
if (lat < minLat) minLat = lat;
|
|
11847
|
+
if (lat > maxLat) maxLat = lat;
|
|
11848
|
+
}
|
|
11849
|
+
});
|
|
11850
|
+
this._gridRefLat = (minLat <= maxLat) ? (minLat + maxLat) / 2 : 0;
|
|
11851
|
+
}
|
|
11852
|
+
|
|
10827
11853
|
// (re)builds the per-category GridAggregateIndex for the grid width
|
|
10828
11854
|
// resolved at the given zoom, only when that width changed. Grid width,
|
|
10829
11855
|
// as flat reads it (maptheme.js:1892-1908, 7990-8008): gridwidthpx and
|
|
@@ -10834,6 +11860,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10834
11860
|
// style.aggregation bracket; none → no grid (exact positions).
|
|
10835
11861
|
_ensureClusterIndices(zoom) {
|
|
10836
11862
|
let radiusPx;
|
|
11863
|
+
// a meters grid set later by setStyle (gridwidth / aggregation) never
|
|
11864
|
+
// went through _prepare's scan — not the equator, the data's latitude
|
|
11865
|
+
if (this._gridRefLat === undefined) this._scanGridRefLat();
|
|
10837
11866
|
// GL-PORT COMPAT: a real-ixmaps-flat page's own gridwidth is
|
|
10838
11867
|
// sometimes a fixed SCREEN-PIXEL size (e.g. style.gridwidth:"3px",
|
|
10839
11868
|
// confirmed against a real ported page) rather than this engine's
|
|
@@ -10880,6 +11909,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10880
11909
|
this._clusterUsesFixedZoom = false;
|
|
10881
11910
|
}
|
|
10882
11911
|
if (this._clusterIndices && this._clusterRadiusPx === radiusPx && this._clusterField === aggField) return;
|
|
11912
|
+
// new cells (e.g. a runtime gridwidth): the zoom-keyed stats of the
|
|
11913
|
+
// old ones (breaks, NORMALIZE, NOOUTLIER, sizeMax) no longer apply
|
|
11914
|
+
this._aggregateStatsCache = null;
|
|
10883
11915
|
this._clusterRadiusPx = radiusPx;
|
|
10884
11916
|
this._clusterField = aggField;
|
|
10885
11917
|
// after aggregation, on the cell sums: field100 of a value-based
|
|
@@ -10888,7 +11920,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10888
11920
|
const post = this._seriesDifference
|
|
10889
11921
|
? (v, v100, series) => (series ? cellAggregatedValues({ sums: series, counts: series.map(() => 1) }, this.flags, this.style)[0] : v)
|
|
10890
11922
|
: this._aggregateField100 ? (v, v100) => aggregateField100(v, v100, this.flags, this.style) : undefined;
|
|
10891
|
-
|
|
11923
|
+
// flat's weights multiply every record's value fields as they are
|
|
11924
|
+
// accumulated (maptheme.js 10506-10508), before the sums' field100
|
|
11925
|
+
// arithmetic above — a weighted SUM, not a sum weighted afterward
|
|
11926
|
+
const weightOf = fieldWeightOf(this.style, (this._multiFields || [this.binding && this.binding.value]).length, true);
|
|
11927
|
+
this._clusterIndices = this._featuresByCategory.map(feats =>
|
|
11928
|
+
new GridAggregateIndex(radiusPx, this.flags, post, aggField).load(weightOf ? feats.map(f => weightedRecord(f, weightOf)) : feats));
|
|
10892
11929
|
}
|
|
10893
11930
|
|
|
10894
11931
|
// Reverted from the source-accurate two-flat-circle GLOW (radius*6
|
|
@@ -11139,6 +12176,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11139
12176
|
const raw = props.raw || props.titleRaw;
|
|
11140
12177
|
const title = this.binding.title && raw && raw[this.binding.title] != null ? String(raw[this.binding.title]) : '';
|
|
11141
12178
|
const aggregated = !!(props.counts || props.cluster);
|
|
12179
|
+
// MAX / MIN aggregation: the item IS one defined record — the one
|
|
12180
|
+
// holding the max (min) — so it carries that record's real title;
|
|
12181
|
+
// "(n)" is the label of a summed/mean aggregate
|
|
12182
|
+
if (this.flags.has('MAX') || this.flags.has('MIN')) return title;
|
|
11142
12183
|
if (aggregated && this._clusterRadiusPx && !this._clusterField) {
|
|
11143
12184
|
const n = groupRecordCount(props);
|
|
11144
12185
|
if (!title || n > 3) return '(' + n + ')';
|
|
@@ -11471,7 +12512,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11471
12512
|
// companion layer) takes the uniform mesh shortcut.
|
|
11472
12513
|
if (this.flags.has('GRIDSIZE') && this.flags.has('PLOT')) return this._buildPlotLayers(zoom, bbox);
|
|
11473
12514
|
// PLOT without GRIDSIZE: one line chart per item over its value fields
|
|
11474
|
-
if (this.flags.has('PLOT') && this._isItemPlot()) return this._buildItemPlotLayers(zoom);
|
|
12515
|
+
if (this.flags.has('PLOT') && this._isItemPlot()) return this._buildItemPlotLayers(zoom, liveZoom);
|
|
11475
12516
|
// a plain grid (one color, no values or glow — the Germany page's
|
|
11476
12517
|
// companion "grid" layer) as one square per cell; a classed GRIDSIZE
|
|
11477
12518
|
// chart (the fuel-price page) is a symbol chart sized to the grid,
|
|
@@ -11498,6 +12539,15 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11498
12539
|
// unmatched polygon, so the donor's fill serves no purpose once
|
|
11499
12540
|
// superseded.
|
|
11500
12541
|
if (this._isChoroplethGeometryDonor) return [];
|
|
12542
|
+
// flat's WMS theme (one server image over the view) renders here as
|
|
12543
|
+
// a native MapLibre raster source+layer managed by syncWmsLayers —
|
|
12544
|
+
// not as deck.gl layers — so its own build returns nothing
|
|
12545
|
+
if (this.flags.has('WMS') || this.flags.has('IMAGE')) return [];
|
|
12546
|
+
// flat's POSITION theme (maptheme.js 8328-8353): loads its data and
|
|
12547
|
+
// draws nothing — it only exists as this layer's id→position provider
|
|
12548
|
+
// for the join (joinChartPositions reads its features, its binding.id
|
|
12549
|
+
// against the consumers' lookup key), so no renderer runs for it
|
|
12550
|
+
if (this.flags.has('POSITION')) return [];
|
|
11501
12551
|
if (this.flags.has('FEATURE') || this.flags.has('FEATURES')) {
|
|
11502
12552
|
return featuresHiddenByScale(this.style, liveZoom) ? [] : this._buildFeaturesLayers(liveZoom, bbox);
|
|
11503
12553
|
}
|
|
@@ -11539,6 +12589,20 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11539
12589
|
// each shape by its class), not one flat fill
|
|
11540
12590
|
const byCategory = filled && this.flags.has('CATEGORICAL') && this.categoryIndexByLabel && this.binding.value
|
|
11541
12591
|
&& this.categoryColorsRgb && this.categoryColorsRgb.length;
|
|
12592
|
+
// a NUMERIC bound value classes every shape too (flat's
|
|
12593
|
+
// distributeValues → partsA; paintShape, maptheme.js 13991-14018,
|
|
12594
|
+
// paints each shape in `colorScheme[nIndex]` — a line as
|
|
12595
|
+
// fill:none;stroke:class-color, a polygon filled with it): the
|
|
12596
|
+
// TELRAAM speed segments, green→red by v85
|
|
12597
|
+
const byClass = !byCategory && this._rangeClassed && this.partsA
|
|
12598
|
+
&& this.categoryColorsRgb && this.categoryColorsRgb.length
|
|
12599
|
+
// "$item$" (or a field list) is no numeric field: a plain FEATURE
|
|
12600
|
+
// layer keeps its one fill color
|
|
12601
|
+
&& this.binding.value && this.binding.value !== '$item$' && !String(this.binding.value).includes('|');
|
|
12602
|
+
const classColorOf = f => {
|
|
12603
|
+
const cls = this._resolvePartsClass(parseFloat(f.properties && f.properties[this.binding.value]));
|
|
12604
|
+
return this.classRgb(cls);
|
|
12605
|
+
};
|
|
11542
12606
|
const catAlpha = Math.round(255 * (styleNum(this.style.fillopacity) || 1));
|
|
11543
12607
|
const nodataRgb = parseCssColor(this.style.nodatacolor) || [255, 255, 255];
|
|
11544
12608
|
const shadow = filled && flatShadowOn(this.style, this.features.length, zoom)
|
|
@@ -11558,14 +12622,19 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11558
12622
|
// flat puts fillopacity into the shape's fill-opacity only
|
|
11559
12623
|
// (maptheme.js 13407, 14071-14078): the outline stays opaque
|
|
11560
12624
|
// (a fill color's own alpha — rgba(…, 0.5) — times fillopacity)
|
|
11561
|
-
...(byCategory ? { updateTriggers: { getFillColor: JSON.stringify(this.categoryColorsRgb) } } : {}),
|
|
12625
|
+
...(byCategory || byClass ? { updateTriggers: { getFillColor: JSON.stringify(this.categoryColorsRgb), getLineColor: JSON.stringify(this.categoryColorsRgb) } } : {}),
|
|
11562
12626
|
getFillColor: byCategory
|
|
11563
12627
|
? f => {
|
|
11564
12628
|
const cat = this._resolveClassIndex(f.properties[this.binding.value]);
|
|
11565
12629
|
return [...(cat != null ? (this.categoryColorsRgb || [])[cat] || nodataRgb : nodataRgb).slice(0, 3), catAlpha];
|
|
11566
12630
|
}
|
|
12631
|
+
: byClass ? f => [...classColorOf(f).slice(0, 3), catAlpha]
|
|
11567
12632
|
: filled ? [...hexOrNamedToRgb(raw).slice(0, 3), Math.round(255 * cssColorAlpha(raw) * (styleNum(this.style.fillopacity) || 1))] : [0, 0, 0, 0],
|
|
11568
|
-
getLineColor:
|
|
12633
|
+
getLineColor: byClass
|
|
12634
|
+
// flat's paintShape strokes a line shape with its class color
|
|
12635
|
+
// (fill:none;stroke:class-color), at stroke-opacity fillopacity
|
|
12636
|
+
? f => [...classColorOf(f).slice(0, 3), catAlpha]
|
|
12637
|
+
: this.style.linecolor
|
|
11569
12638
|
? withAlpha(hexOrNamedToRgb(styleLineColor(this.style.linecolor)), cssColorAlpha(styleLineColor(this.style.linecolor)))
|
|
11570
12639
|
: [130, 130, 130],
|
|
11571
12640
|
lineWidthMinPixels: styleNum(this.style.linewidth) || 1,
|
|
@@ -11655,12 +12724,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11655
12724
|
const isComposeColor = this.flags.has('COMPOSECOLOR');
|
|
11656
12725
|
const baseOpacity = resolveFillOpacity(this.style, this.mapOptions, zoom);
|
|
11657
12726
|
// DOPACITY needs a PER-FEATURE opacity, which deck.gl only supports
|
|
11658
|
-
// via
|
|
11659
|
-
// prop is one number for every feature) — see
|
|
11660
|
-
//
|
|
11661
|
-
//
|
|
11662
|
-
//
|
|
11663
|
-
|
|
12727
|
+
// via the color accessors' own alpha channel (its layer-level
|
|
12728
|
+
// `opacity` prop is one number for every feature) — see
|
|
12729
|
+
// _resolveDopacityAlpha. COMPOSECOLOR runs DOPACITY too, with its own
|
|
12730
|
+
// formula on the item's value sum (flat maptheme.js 13485-13506,
|
|
12731
|
+
// composedDopacityAlpha) — it is only the DOMINANT/range
|
|
12732
|
+
// _resolveDopacityAlpha that has no single driving value there
|
|
12733
|
+
const dopacityActive =
|
|
11664
12734
|
(this.flags.has('DOPACITY') || this.flags.has('DOPACITYMIN') || this.flags.has('DOPACITYMAX') || this.flags.has('DOPACITYMINMAX'));
|
|
11665
12735
|
// AGGREGATE CATEGORICAL: every polygon's parts and dominant class
|
|
11666
12736
|
// (aggregatedCategoricalClass), the biggest part per class over all
|
|
@@ -11703,7 +12773,15 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11703
12773
|
return {
|
|
11704
12774
|
type: 'Feature',
|
|
11705
12775
|
geometry: f.geometry,
|
|
11706
|
-
properties: {
|
|
12776
|
+
properties: {
|
|
12777
|
+
raw: f.properties, composedColor: this._resolveComposedColor(f.properties),
|
|
12778
|
+
// flat's DOPACITY inside the COMPOSECOLOR branch (maptheme.js
|
|
12779
|
+
// 13485-13506): opacity from the item's raw weighted value
|
|
12780
|
+
// sum over (field100/100), against the same nMax the color
|
|
12781
|
+
// blend normalizes by. The raw weighted sum is reconstructed
|
|
12782
|
+
// from the fractioned part values (raw = part × its field100)
|
|
12783
|
+
dopacityAlpha: dopacityActive ? this._composedDopacityAlpha(f.properties) : null
|
|
12784
|
+
}
|
|
11707
12785
|
};
|
|
11708
12786
|
}
|
|
11709
12787
|
if (isDominant) {
|
|
@@ -11759,6 +12837,29 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11759
12837
|
// deck.gl re-runs the color accessors only for new data or a changed trigger
|
|
11760
12838
|
const colorKey = JSON.stringify([[...this._markedClasses], !!this._onMarksChanged, evidenceMode,
|
|
11761
12839
|
this.categoryColorsRgb, fallbackRgb, classAlpha, lineRgb]);
|
|
12840
|
+
// flat paints a LINE shape's value color as its STROKE (paintShape on
|
|
12841
|
+
// a line shape, szShapeType line — its DOPACITY writes stroke-opacity
|
|
12842
|
+
// there too): a choropleth over line geometry, the Milano street lines,
|
|
12843
|
+
// shows the value color ON the line; a polygon keeps it as its fill
|
|
12844
|
+
const isLineGeometry = d => { const t = d.geometry && d.geometry.type; return t === 'LineString' || t === 'MultiLineString'; };
|
|
12845
|
+
const valueColorOf = d => {
|
|
12846
|
+
if (!hasData(d)) return [0, 0, 0, 0];
|
|
12847
|
+
const rgb = d.properties.composedColor
|
|
12848
|
+
|| (d.properties.cat != null ? (this.categoryColorsRgb || [])[d.properties.cat] : null) || fallbackRgb;
|
|
12849
|
+
const evidence = unmarkedEvidence(d);
|
|
12850
|
+
if (evidence === 'hide') return [0, 0, 0, 0];
|
|
12851
|
+
if (evidence === 'gray') return [...grayOf(rgb), 90];
|
|
12852
|
+
const a = alphaOfClass(d.properties.cat);
|
|
12853
|
+
return d.properties.dopacityAlpha != null ? [...rgb.slice(0, 3), Math.round(d.properties.dopacityAlpha * a * 255)] : withAlpha(rgb, a);
|
|
12854
|
+
};
|
|
12855
|
+
// flat re-styles the SAME shape nodes the FEATURE base drew, so a line
|
|
12856
|
+
// choropleth keeps the base's own line width — the donor's linewidth
|
|
12857
|
+
// is the fallback when the choropleth has none of its own
|
|
12858
|
+
const donorRt = [..._globalThemeRegistry.values()].find(r => r.name === this.name && r._isChoroplethGeometryDonor);
|
|
12859
|
+
const anyLine = collection.features.some(isLineGeometry);
|
|
12860
|
+
const isolating = !!(this._markedClasses.size && !this._onMarksChanged && evidenceMode === 'isolate');
|
|
12861
|
+
const lineW = styleNum(this.style.linewidth)
|
|
12862
|
+
|| (anyLine && donorRt && styleNum(donorRt.style.linewidth)) || 1;
|
|
11762
12863
|
return [new GeoJsonLayer({
|
|
11763
12864
|
id: this._dataLayerId(`ix-choropleth-${this.name}`),
|
|
11764
12865
|
data: collection,
|
|
@@ -11766,21 +12867,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11766
12867
|
pickable: true,
|
|
11767
12868
|
stroked: true,
|
|
11768
12869
|
filled: true,
|
|
11769
|
-
getFillColor: d =>
|
|
11770
|
-
|
|
11771
|
-
|
|
11772
|
-
|
|
11773
|
-
|
|
11774
|
-
|
|
11775
|
-
if (evidence === 'gray') return [...grayOf(rgb), 90];
|
|
11776
|
-
const a = alphaOfClass(d.properties.cat);
|
|
11777
|
-
return d.properties.dopacityAlpha != null ? [...rgb.slice(0, 3), Math.round(d.properties.dopacityAlpha * a * 255)] : withAlpha(rgb, a);
|
|
11778
|
-
},
|
|
11779
|
-
// a per-polygon accessor only while a class is isolated
|
|
11780
|
-
getLineColor: this._markedClasses.size && !this._onMarksChanged && evidenceMode === 'isolate'
|
|
11781
|
-
? d => (unmarkedEvidence(d) === 'hide' ? [0, 0, 0, 0] : lineRgb)
|
|
11782
|
-
: lineRgb,
|
|
11783
|
-
lineWidthMinPixels: styleNum(this.style.linewidth) || 1,
|
|
12870
|
+
getFillColor: d => valueColorOf(d),
|
|
12871
|
+
// a constant unless a shape needs its own color (line geometry, or a
|
|
12872
|
+
// class isolated) — deck.gl then skips a per-item accessor call
|
|
12873
|
+
getLineColor: !anyLine && !isolating ? lineRgb
|
|
12874
|
+
: d => choroplethOutlineColor(isLineGeometry(d), valueColorOf, d, isolating, unmarkedEvidence, lineRgb),
|
|
12875
|
+
lineWidthMinPixels: lineW,
|
|
11784
12876
|
// per-feature alpha (baked above) already carries the resolved
|
|
11785
12877
|
// base opacity — a layer-level opacity on TOP of that would
|
|
11786
12878
|
// multiply it a second time.
|
|
@@ -11817,6 +12909,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11817
12909
|
const categories = this._plotCategories();
|
|
11818
12910
|
|
|
11819
12911
|
const cells = new Map();
|
|
12912
|
+
const combine = aggCombiner(this.flags);
|
|
11820
12913
|
source.forEach(f => {
|
|
11821
12914
|
const ci = this._gridCategoryIndex(f.properties[this.binding.value]);
|
|
11822
12915
|
if (ci == null) return;
|
|
@@ -11834,7 +12927,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11834
12927
|
cells.set(key, cell);
|
|
11835
12928
|
}
|
|
11836
12929
|
const v = this._resolveAggregateValue(f.properties);
|
|
11837
|
-
cell.sums[ci]
|
|
12930
|
+
cell.sums[ci] = combine(cell.sums[ci], v, cell.counts[ci] === 0);
|
|
11838
12931
|
cell.counts[ci]++;
|
|
11839
12932
|
});
|
|
11840
12933
|
|
|
@@ -11872,8 +12965,14 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11872
12965
|
// a per-item SEQUENCE chart: CHART|SYMBOL|SEQUENCE, several value
|
|
11873
12966
|
// fields per item, one part per field (no CATEGORICAL, no AGGREGATE)
|
|
11874
12967
|
_isMultiFieldSequence() {
|
|
12968
|
+
// AGGREGATE is allowed: flat's loadAndAggregateValuesOfTheme
|
|
12969
|
+
// aggregates a multi-field SEQUENCE theme per FIELD (its per-field
|
|
12970
|
+
// nValuesA, MAX/MIN per field, else summed — maptheme.js 10876-10889)
|
|
12971
|
+
// and AUTO100 then normalizes every item's parts to % of their own
|
|
12972
|
+
// sum (12166-12176) — the parts ride the grid cells as `series` and
|
|
12973
|
+
// the per-item % is applied in _buildSequenceChartLayers
|
|
11875
12974
|
return this.flags.has('CHART') && this.flags.has('SYMBOL') && flatFlag(this.flags, 'SEQUENCE')
|
|
11876
|
-
&& !this.flags.has('CATEGORICAL') && !this.flags.has('
|
|
12975
|
+
&& !this.flags.has('CATEGORICAL') && !this.flags.has('PLOT')
|
|
11877
12976
|
&& String(this.binding.value || '').includes('|');
|
|
11878
12977
|
}
|
|
11879
12978
|
_itemPlotFields() {
|
|
@@ -11999,16 +13098,20 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11999
13098
|
return explicit ? fields.map((f, i) => String(explicit[i] != null ? explicit[i] : f)) : fields;
|
|
12000
13099
|
}
|
|
12001
13100
|
|
|
12002
|
-
_buildItemPlotLayers(zoom) {
|
|
13101
|
+
_buildItemPlotLayers(zoom, liveZoom = zoom) {
|
|
12003
13102
|
const fields = this._itemPlotFields();
|
|
12004
13103
|
const items = [];
|
|
12005
13104
|
let dMin = Infinity, dMax = -Infinity;
|
|
13105
|
+
// facet / runtime WHERE / time frame narrow the drawn charts, as in
|
|
13106
|
+
// every other renderer; the value range stays the full data's (as
|
|
13107
|
+
// the bubbles' _maxSizeValue), so a filter does not rescale them
|
|
13108
|
+
const active = this._activeFeatures ? new Set(this._activeFeatures) : null;
|
|
12006
13109
|
for (const f of this.features) {
|
|
12007
13110
|
const pos = itemAnchor(f.geometry);
|
|
12008
13111
|
if (!pos) continue;
|
|
12009
13112
|
const values = fields.map(k => parseFloat(f.properties[k]));
|
|
12010
13113
|
for (const v of values) if (!isNaN(v)) { dMin = Math.min(dMin, v); dMax = Math.max(dMax, v); }
|
|
12011
|
-
items.push({ geometry: { type: 'Point', coordinates: pos }, properties: { values, raw: f.properties } });
|
|
13114
|
+
if (!active || active.has(f)) items.push({ geometry: { type: 'Point', coordinates: pos }, properties: { values, raw: f.properties } });
|
|
12012
13115
|
}
|
|
12013
13116
|
if (!items.length) return [];
|
|
12014
13117
|
const sMin = styleNum(this.style.minvalue), sMaxRaw = this.style.maxvalue;
|
|
@@ -12027,7 +13130,18 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12027
13130
|
const S = 2 * NORMAL_RADIUS_PX * (styleNum(this.style.scale) || 1) * objectZoomFactor(zoom, this.mapOptions);
|
|
12028
13131
|
const color = parseCssColor(Array.isArray(this.style.colorscheme) ? this.style.colorscheme[0] : this.style.colorscheme) || [0, 102, 204];
|
|
12029
13132
|
const fillOpacity = styleNum(this.style.fillopacity);
|
|
12030
|
-
|
|
13133
|
+
// BOX / TITLE: flat's chart-group box around every chart; the chart's
|
|
13134
|
+
// drawn extent is its plot geometry's bounding box (chart units × S px,
|
|
13135
|
+
// y up in the geometry, down on screen)
|
|
13136
|
+
const boxRows = this._boxRows(items, { unit: S / (2 * NORMAL_RADIUS_PX), liveZoom, extentOf: d => {
|
|
13137
|
+
const b = itemPlotGeometry(d.properties.values, rangeFor(d.properties.values), this.style, this.flags, sizeMax).bbox;
|
|
13138
|
+
return [b.x0 * S, -b.y1 * S, b.x1 * S, -b.y0 * S];
|
|
13139
|
+
} });
|
|
13140
|
+
const boxLayers = boxRows ? this._boxTitleLayers(boxRows, {
|
|
13141
|
+
unit: S / (2 * NORMAL_RADIUS_PX), liveZoom, positionOf: b => b.d.geometry.coordinates, offsetOf: () => [0, 0],
|
|
13142
|
+
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}` }
|
|
13143
|
+
}) : { box: [], titles: [] };
|
|
13144
|
+
return [...boxLayers.box, new IconLayer({
|
|
12031
13145
|
id: `ix-itemplot-${this.name}-g${this._iconGeneration}`,
|
|
12032
13146
|
data: items, pickable: true,
|
|
12033
13147
|
getPosition: d => d.geometry.coordinates,
|
|
@@ -12037,7 +13151,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12037
13151
|
getSize: d => this._buildItemPlotIcon(d.properties.values, rangeFor(d.properties.values), sizeMax, color, fillOpacity).heightUnits * S,
|
|
12038
13152
|
sizeUnits: 'pixels',
|
|
12039
13153
|
updateTriggers: { getSize: [S] }
|
|
12040
|
-
})];
|
|
13154
|
+
}), ...boxLayers.titles];
|
|
12041
13155
|
}
|
|
12042
13156
|
|
|
12043
13157
|
// canvas for one per-item PLOT chart (itemPlotGeometry), RASTER px per
|
|
@@ -12175,14 +13289,22 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12175
13289
|
datasetMax = datasetMax || 1;
|
|
12176
13290
|
if (isNaN(normalSizeValue)) normalSizeValue = datasetMax;
|
|
12177
13291
|
|
|
12178
|
-
|
|
13292
|
+
// BOX / TITLE: flat's chart-group box around every chart; the chart is
|
|
13293
|
+
// the iconSize square on the cell's center (flat's chart size is 30 units)
|
|
13294
|
+
const boxRows = this._boxRows(data, { unit: iconSize / 30, liveZoom: zoom,
|
|
13295
|
+
extentOf: () => [-iconSize / 2, -iconSize / 2, iconSize / 2, iconSize / 2] });
|
|
13296
|
+
const boxLayers = boxRows ? this._boxTitleLayers(boxRows, {
|
|
13297
|
+
unit: iconSize / 30, liveZoom: zoom, positionOf: b => b.d.geometry.coordinates, offsetOf: () => [0, 0],
|
|
13298
|
+
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}` }
|
|
13299
|
+
}) : { box: [], titles: [] };
|
|
13300
|
+
return [...boxLayers.box, new IconLayer({
|
|
12179
13301
|
id: `ix-plot-${this.name}-g${this._iconGeneration}`,
|
|
12180
13302
|
data, pickable: true,
|
|
12181
13303
|
getPosition: d => d.geometry.coordinates,
|
|
12182
13304
|
getIcon: d => this._buildPlotIcon(d.properties.values, categories, labels, normalSizeValue, datasetMax, datasetMin),
|
|
12183
13305
|
getSize: iconSize,
|
|
12184
13306
|
sizeUnits: 'pixels'
|
|
12185
|
-
})];
|
|
13307
|
+
}), ...boxLayers.titles];
|
|
12186
13308
|
}
|
|
12187
13309
|
|
|
12188
13310
|
// Canvas rendering for one cell's mini-chart — BOX background, AREA
|
|
@@ -12300,9 +13422,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12300
13422
|
canvas.width = W; canvas.height = H;
|
|
12301
13423
|
const ctx = canvas.getContext('2d');
|
|
12302
13424
|
|
|
12303
|
-
// BOX
|
|
13425
|
+
// BOX (a plot without the BOX flag keeps its own canvas box; with it the
|
|
13426
|
+
// shared box layer draws it — see _buildPlotLayers)
|
|
12304
13427
|
const boxOpacity = styleNum(this.style.boxopacity);
|
|
12305
|
-
if (!isNaN(boxOpacity) && boxOpacity > 0) {
|
|
13428
|
+
if (!flatFlag(this.flags, 'BOX') && !isNaN(boxOpacity) && boxOpacity > 0) {
|
|
12306
13429
|
ctx.fillStyle = `rgba(255,255,255,${boxOpacity})`;
|
|
12307
13430
|
ctx.fillRect(0, 0, W, H);
|
|
12308
13431
|
}
|
|
@@ -12548,6 +13671,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12548
13671
|
// async image decode, the whole drawing is synchronous
|
|
12549
13672
|
const items = [];
|
|
12550
13673
|
const defsSeen = new Set();
|
|
13674
|
+
// this drawing's defs, swapped in for the previous drawing's below —
|
|
13675
|
+
// appended to the shared pane <defs> on every draw they grew without
|
|
13676
|
+
// bound (and getElementById then found a stale earlier copy)
|
|
13677
|
+
const paneDefs = document.createElementNS(SVG_NS, 'g');
|
|
12551
13678
|
let html = '';
|
|
12552
13679
|
for (const d of combined) {
|
|
12553
13680
|
const value = valueOf(d);
|
|
@@ -12564,11 +13691,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12564
13691
|
for (const id of chart.ids) {
|
|
12565
13692
|
if (defsSeen.has(id)) continue;
|
|
12566
13693
|
defsSeen.add(id);
|
|
12567
|
-
|
|
12568
|
-
|
|
13694
|
+
// drawn now (fresh), else a cached chart's: the previous drawing's copy
|
|
13695
|
+
const def = this._userChartGroup.querySelector('#' + CSS.escape(id)) || this._userChartGroup.ownerDocument.getElementById(id);
|
|
13696
|
+
if (def) paneDefs.appendChild(def.cloneNode(true));
|
|
12569
13697
|
}
|
|
12570
13698
|
items.push({ d, coords: d.geometry.coordinates, ox: chart.ox, oy: chart.oy, s: pxPerUnit,
|
|
12571
|
-
r: Math.max(chart.vw, chart.vh) * pxPerUnit / 2
|
|
13699
|
+
r: Math.max(chart.vw, chart.vh) * pxPerUnit / 2,
|
|
13700
|
+
// the chart's measured box (flat's getBox of the chart group) in px
|
|
13701
|
+
// around the map position: the pane draws a chart point at
|
|
13702
|
+
// p + s · (point − origin)
|
|
13703
|
+
ext: [(chart.bb.x - chart.ox) * pxPerUnit, (chart.bb.y - chart.oy) * pxPerUnit,
|
|
13704
|
+
(chart.bb.x + chart.bb.width - chart.ox) * pxPerUnit, (chart.bb.y + chart.bb.height - chart.oy) * pxPerUnit] });
|
|
12572
13705
|
}
|
|
12573
13706
|
if (glDebug()) console.info(`[ixmaps-gl debug] ${themeIdOf(this)}: zoom ${mapLibreToFlatZoom(liveZoom).toFixed(2)}, ${combined.length} charts, ${items.length} drawn${this._viewMoving ? ' (moving)' : ''}`);
|
|
12574
13707
|
if (!items.length) {
|
|
@@ -12577,6 +13710,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12577
13710
|
this._chartItems = null;
|
|
12578
13711
|
return (this._userChartLastLayers = []);
|
|
12579
13712
|
}
|
|
13713
|
+
chartHost.defs.appendChild(paneDefs);
|
|
13714
|
+
if (this._chartPaneDefs) this._chartPaneDefs.remove();
|
|
13715
|
+
this._chartPaneDefs = paneDefs;
|
|
12580
13716
|
if (this._chartPaneGroup) this._chartPaneGroup.remove();
|
|
12581
13717
|
this._chartPaneGroup = document.createElementNS(SVG_NS, 'g');
|
|
12582
13718
|
chartHost.pane.appendChild(this._chartPaneGroup);
|
|
@@ -12585,11 +13721,24 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12585
13721
|
for (let i = 0; i < items.length; i++) items[i].el = els[i];
|
|
12586
13722
|
this._chartItems = items;
|
|
12587
13723
|
chartHost.runtimes.add(this);
|
|
13724
|
+
this._chartHost = chartHost; // removeRuntimes takes it out again
|
|
12588
13725
|
chartHost.update();
|
|
12589
13726
|
// picking: a transparent circle per chart — the VISIBLE chart is the
|
|
12590
13727
|
// live SVG in the pane; this deck layer only serves hover, click and
|
|
12591
13728
|
// the tooltips (pickedFeature unwraps the {d, icon} entry shape)
|
|
12592
|
-
|
|
13729
|
+
// BOX / TITLE: flat's chart-group box around the chart as the page's
|
|
13730
|
+
// function drew it (its measured box). The chart itself is a live SVG in
|
|
13731
|
+
// the pane above the deck canvas, so the box lies under it and the title
|
|
13732
|
+
// (outside the box) beside it
|
|
13733
|
+
const extOf = new Map(items.map(it => [it.d, it.ext]));
|
|
13734
|
+
const boxRows = this._boxRows(items.map(it => it.d), {
|
|
13735
|
+
unit: objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1), liveZoom, extentOf: d => extOf.get(d) });
|
|
13736
|
+
const boxLayers = boxRows ? this._boxTitleLayers(boxRows, {
|
|
13737
|
+
unit: objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1), liveZoom,
|
|
13738
|
+
positionOf: b => b.d.geometry.coordinates, offsetOf: () => [0, 0],
|
|
13739
|
+
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}` }
|
|
13740
|
+
}) : { box: [], titles: [] };
|
|
13741
|
+
this._userChartLastLayers = [...boxLayers.box, new ScatterplotLayer({
|
|
12593
13742
|
id: `ix-bubbles-${this.name}`,
|
|
12594
13743
|
data: items.map(it => ({ d: it.d, icon: true, r: it.r })),
|
|
12595
13744
|
pickable: true,
|
|
@@ -12598,7 +13747,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12598
13747
|
radiusUnits: 'pixels', radiusMinPixels: 1,
|
|
12599
13748
|
stroked: false, filled: true, getFillColor: [0, 0, 0, 0],
|
|
12600
13749
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
12601
|
-
})];
|
|
13750
|
+
}), ...boxLayers.titles];
|
|
12602
13751
|
return this._userChartLastLayers;
|
|
12603
13752
|
}
|
|
12604
13753
|
// the chart's SVG, cached by its draw parameters — a failing draw
|
|
@@ -12653,7 +13802,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12653
13802
|
const vx = bb.x - pad, vy = bb.y - pad, vw = bb.width + 2 * pad, vh = bb.height + 2 * pad;
|
|
12654
13803
|
// the chart's origin (0,0 — or the point flat moves it to, ret) sits at the map position
|
|
12655
13804
|
const ox = (ret && ret.x) || 0, oy = (ret && ret.y) || 0;
|
|
12656
|
-
chart = { vx, vy, vw, vh, ox, oy, markup, ids: [...ids],
|
|
13805
|
+
chart = { vx, vy, vw, vh, ox, oy, markup, ids: [...ids], bb: { x: bb.x, y: bb.y, width: bb.width, height: bb.height },
|
|
12657
13806
|
svg: `<svg xmlns="${SVG_NS}" width="__W__" height="__H__" viewBox="${vx} ${vy} ${vw} ${vh}"><defs>${defs}</defs>${markup}</svg>` };
|
|
12658
13807
|
}
|
|
12659
13808
|
} catch (e) {
|
|
@@ -12666,6 +13815,187 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12666
13815
|
return chart;
|
|
12667
13816
|
}
|
|
12668
13817
|
|
|
13818
|
+
// flat's BOX block belongs to the box: the box owns its TITLE — no BOX, no
|
|
13819
|
+
// title — so everything about both is decided here and in _boxRows, for
|
|
13820
|
+
// every chart type (single symbol, SEQUENCE, PIE, BAR): the gates (BOX /
|
|
13821
|
+
// TITLE flags, boxupper / boxlower / titleupper scale), the margin, the
|
|
13822
|
+
// title font (DTEXT follows the chart), BOTTOMTITLE, align, CIRCULARBOX.
|
|
13823
|
+
// null when there is no box at this zoom.
|
|
13824
|
+
_boxSpec(liveZoom) {
|
|
13825
|
+
const st = this.style;
|
|
13826
|
+
if (!(flatFlag(this.flags, 'BOX') && !boxHiddenByScale(st, liveZoom))) return null;
|
|
13827
|
+
const titleUpper = scaleDenom(st.titleupper);
|
|
13828
|
+
const titleScale = styleNum(st.textscale) || styleNum(st.valuescale) || 1;
|
|
13829
|
+
return {
|
|
13830
|
+
circular: flatFlag(this.flags, 'CIRCULAR'),
|
|
13831
|
+
margin: styleNum(st.boxmargin) || 2,
|
|
13832
|
+
bottomTitle: flatFlag(this.flags, 'BOTTOMTITLE'),
|
|
13833
|
+
alignRight: /right/.test(String(st.align || '')),
|
|
13834
|
+
titleShown: flatFlag(this.flags, 'TITLE') && !!this.binding.title
|
|
13835
|
+
&& !(titleUpper && scaleDenominatorAt(liveZoom) > titleUpper),
|
|
13836
|
+
// DTEXT: the title font follows the chart, max(0.9u, min(W/5, 14u /
|
|
13837
|
+
// scale)) (18815) — measured on the fire areas page's SIF chart
|
|
13838
|
+
// (W 362 units → font 72.35)
|
|
13839
|
+
titleFontOf: (unit, w) => (flatFlag(this.flags, 'DTEXT')
|
|
13840
|
+
? Math.max(0.9 * unit, Math.min(w / 5, 14 * unit / (styleNum(st.scale) || 1)))
|
|
13841
|
+
: 5 * unit) * titleScale
|
|
13842
|
+
};
|
|
13843
|
+
}
|
|
13844
|
+
|
|
13845
|
+
// the layout row of every item's box and title (chartBoxLayout + the spec
|
|
13846
|
+
// above); null without a box (items: the array, or a function building it
|
|
13847
|
+
// only when there is a box). o: { unit (px per flat unit), liveZoom,
|
|
13848
|
+
// extentOf (item -> its drawn [x0, y0, x1, y1], px around the anchor),
|
|
13849
|
+
// radiusOf / centerOf (the circular box's base, default the extent's) }.
|
|
13850
|
+
// A row: { d: item, geometry, properties, w (the chart's width — flat's
|
|
13851
|
+
// size measure W, the stroke and corner basis), boxR / cx / cy (circle),
|
|
13852
|
+
// rect (rectangle), title }
|
|
13853
|
+
_boxRows(items, o) {
|
|
13854
|
+
const spec = this._boxSpec(o.liveZoom);
|
|
13855
|
+
if (!spec) return null;
|
|
13856
|
+
// flat appends style.titleunits (its parse adds the leading space,
|
|
13857
|
+
// maptheme.js 1470; szTitle += at 18827)
|
|
13858
|
+
const units = this.style.titleunits ? ' ' + this.style.titleunits : '';
|
|
13859
|
+
return (typeof items === 'function' ? items() : items).map(d => {
|
|
13860
|
+
const itemTitle = spec.titleShown ? this._itemTitle(d.properties) : null;
|
|
13861
|
+
const L = chartBoxLayout(o.extentOf(d), { unit: o.unit, margin: spec.margin, circular: spec.circular,
|
|
13862
|
+
radius: o.radiusOf ? o.radiusOf(d) : undefined, center: o.centerOf ? o.centerOf(d) : undefined,
|
|
13863
|
+
titleText: itemTitle ? itemTitle + units : null, titleFont: w => spec.titleFontOf(o.unit, w),
|
|
13864
|
+
bottomTitle: spec.bottomTitle, alignRight: spec.alignRight });
|
|
13865
|
+
// geometry/properties of the chart's feature on the row: the circular
|
|
13866
|
+
// box is pickable, and the tooltip (buildTooltipHtml, the click-pin)
|
|
13867
|
+
// works on the feature's own properties
|
|
13868
|
+
return { d, geometry: d.geometry, properties: d.properties, w: L.w, boxR: L.boxR, cx: L.cx, cy: L.cy,
|
|
13869
|
+
rect: L.rect, title: L.title };
|
|
13870
|
+
});
|
|
13871
|
+
}
|
|
13872
|
+
|
|
13873
|
+
// THE box-and-title layers of every chart type: rows are _boxRows results.
|
|
13874
|
+
// o: { unit, liveZoom, positionOf (row -> [lng, lat]), offsetOf (row ->
|
|
13875
|
+
// the chart's [dx, dy] px move: align / offsetx / offsety), box (false:
|
|
13876
|
+
// the title layer only — a BAR draws its box as a shader shape), ids:
|
|
13877
|
+
// { circle, icon, polygons, titles } } — the layer ids stay each chart
|
|
13878
|
+
// type's own. Returns { box: [layers], titles: [layers] }: the caller
|
|
13879
|
+
// pushes them where its draw order wants them.
|
|
13880
|
+
_boxTitleLayers(rows, o) {
|
|
13881
|
+
const st = this.style;
|
|
13882
|
+
const { unit, liveZoom, positionOf, offsetOf, ids } = o;
|
|
13883
|
+
const circular = flatFlag(this.flags, 'CIRCULAR');
|
|
13884
|
+
const box = [], titles = [];
|
|
13885
|
+
// flat: fill-opacity (boxopacity || 1) on the raw style value — the usual
|
|
13886
|
+
// string "0" is transparent; typed here, a given 0 is too
|
|
13887
|
+
const boxOpacity = isNaN(styleNum(st.boxopacity)) ? 1 : styleNum(st.boxopacity);
|
|
13888
|
+
if (o.box === false) {
|
|
13889
|
+
// the title layer only
|
|
13890
|
+
} else if (circular) {
|
|
13891
|
+
const boxNone = isNoneColor(st.boxcolor), borderNone = isNoneColor(st.bordercolor);
|
|
13892
|
+
const boxRgb = hexOrNamedToRgb(boxNone ? '#eeeeee' : st.boxcolor || '#eeeeee');
|
|
13893
|
+
const boxAlpha = boxNone ? 0 : Math.round(255 * boxOpacity);
|
|
13894
|
+
const borderRgb = borderNone ? null : hexOrNamedToRgb(st.bordercolor || '#dddddd');
|
|
13895
|
+
// Always a ScatterplotLayer: its shader circle is smooth at any
|
|
13896
|
+
// size, where an icon — a 48px canvas texture deck.gl scales up —
|
|
13897
|
+
// pixelated with large boxes. ScatterplotLayer has no pixel offset,
|
|
13898
|
+
// so the chart's align/offsetx/offsety move and flat's circular box
|
|
13899
|
+
// position (the chart's bounding-box center, 18793) are baked into
|
|
13900
|
+
// map coordinates (pixelOffsetLngLat). Pickable: flat's box is part
|
|
13901
|
+
// of the chart group, so hovering it fires the chart's tooltip — the
|
|
13902
|
+
// symbols sit above it, so they keep their own
|
|
13903
|
+
box.push(new ScatterplotLayer({
|
|
13904
|
+
id: ids.circle,
|
|
13905
|
+
data: rows, pickable: true,
|
|
13906
|
+
getPosition: b => { const f = offsetOf(b); return pixelOffsetLngLat(positionOf(b), [f[0] + b.cx, f[1] + b.cy], liveZoom); },
|
|
13907
|
+
getRadius: b => b.boxR,
|
|
13908
|
+
radiusUnits: 'pixels',
|
|
13909
|
+
stroked: !borderNone,
|
|
13910
|
+
getFillColor: [...boxRgb.slice(0, 3), boxAlpha],
|
|
13911
|
+
getLineColor: [...(borderRgb || boxRgb).slice(0, 3), 255],
|
|
13912
|
+
getLineWidth: unit / 20,
|
|
13913
|
+
lineWidthUnits: 'pixels',
|
|
13914
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
13915
|
+
}));
|
|
13916
|
+
} else {
|
|
13917
|
+
// flat's rectangular box (the chart and its title plus the margin)
|
|
13918
|
+
const borderStyle = String(st.borderstyle || '');
|
|
13919
|
+
const boxLook = {
|
|
13920
|
+
fill: st.boxcolor || 'white',
|
|
13921
|
+
fillOpacity: boxOpacity,
|
|
13922
|
+
stroke: borderStyle === 'none' || isNoneColor(styleLineColor(st.bordercolor)) ? null : styleLineColor(st.bordercolor) || '#bbbbbb',
|
|
13923
|
+
strokeOpacity: /^(solid|dotted|dashed)$/.test(borderStyle) ? 1 : 0.3,
|
|
13924
|
+
};
|
|
13925
|
+
const borderWidth = styleNum(st.borderwidth) || 1;
|
|
13926
|
+
const borderRadius = styleNum(st.borderradius);
|
|
13927
|
+
const strokeOf = b => borderWidth * 0.2 * b.w / 30;
|
|
13928
|
+
// a box larger than BOX_ICON_MAX_PX (a MULTIQUAD grid of thousands
|
|
13929
|
+
// of items) would need a huge canvas: it is drawn as a polygon in
|
|
13930
|
+
// map coordinates instead (square corners)
|
|
13931
|
+
const bigBox = b => Math.max(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1]) > BOX_ICON_MAX_PX;
|
|
13932
|
+
const iconBoxes = rows.filter(b => !bigBox(b)), polygonBoxes = rows.filter(bigBox);
|
|
13933
|
+
iconBoxes.forEach(b => {
|
|
13934
|
+
b.icon = this._buildBoxIcon(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1],
|
|
13935
|
+
isNaN(borderRadius) ? 0 : borderRadius * Math.min(unit, b.w / 30),
|
|
13936
|
+
strokeOf(b), boxLook);
|
|
13937
|
+
});
|
|
13938
|
+
if (polygonBoxes.length) {
|
|
13939
|
+
const fillRgb = hexOrNamedToRgb(boxLook.fill);
|
|
13940
|
+
const strokeRgb = boxLook.stroke ? hexOrNamedToRgb(boxLook.stroke) : null;
|
|
13941
|
+
box.push(new GeoJsonLayer({
|
|
13942
|
+
id: ids.polygons,
|
|
13943
|
+
data: polygonBoxes.map(b => {
|
|
13944
|
+
const f = offsetOf(b), c = positionOf(b), r = b.rect;
|
|
13945
|
+
const at = (x, y) => pixelOffsetLngLat(c, [x + f[0], y + f[1]], liveZoom);
|
|
13946
|
+
return { type: 'Feature', properties: { stroke: strokeOf(b) },
|
|
13947
|
+
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])]] } };
|
|
13948
|
+
}),
|
|
13949
|
+
pickable: false,
|
|
13950
|
+
filled: boxLook.fillOpacity > 0,
|
|
13951
|
+
stroked: !!strokeRgb,
|
|
13952
|
+
getFillColor: [...fillRgb.slice(0, 3), Math.round(255 * boxLook.fillOpacity)],
|
|
13953
|
+
getLineColor: strokeRgb ? [...strokeRgb.slice(0, 3), Math.round(255 * boxLook.strokeOpacity)] : [0, 0, 0, 0],
|
|
13954
|
+
getLineWidth: f => f.properties.stroke,
|
|
13955
|
+
lineWidthUnits: 'pixels',
|
|
13956
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
13957
|
+
}));
|
|
13958
|
+
}
|
|
13959
|
+
if (iconBoxes.length) box.push(new IconLayer({
|
|
13960
|
+
// the generation suffix (like every icon-atlas layer id, see
|
|
13961
|
+
// ICON_ATLAS_RESET_AFTER): every zoom creates a new set of box
|
|
13962
|
+
// rasters, and deck.gl's atlas never evicts
|
|
13963
|
+
id: `${ids.icon}-g${this._iconGeneration}`,
|
|
13964
|
+
data: iconBoxes, pickable: false,
|
|
13965
|
+
getPosition: positionOf,
|
|
13966
|
+
getIcon: b => b.icon,
|
|
13967
|
+
// the icon is drawn at 2× with a stroke padding around the box
|
|
13968
|
+
getSize: b => b.icon.height / 2,
|
|
13969
|
+
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]]; },
|
|
13970
|
+
sizeUnits: 'pixels',
|
|
13971
|
+
billboard: true,
|
|
13972
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
13973
|
+
}));
|
|
13974
|
+
}
|
|
13975
|
+
const titleData = rows.filter(b => b.title);
|
|
13976
|
+
if (titleData.length) titles.push(new TextLayer({
|
|
13977
|
+
id: ids.titles,
|
|
13978
|
+
data: titleData, pickable: false,
|
|
13979
|
+
getPosition: positionOf,
|
|
13980
|
+
getText: b => b.title.text,
|
|
13981
|
+
getSize: b => b.title.f,
|
|
13982
|
+
sizeUnits: 'pixels',
|
|
13983
|
+
fontFamily: 'arial',
|
|
13984
|
+
characterSet: 'auto',
|
|
13985
|
+
lineHeight: 1,
|
|
13986
|
+
getColor: [...hexOrNamedToRgb(st.textcolor || '#888888').slice(0, 3), 255],
|
|
13987
|
+
fontSettings: { sdf: true },
|
|
13988
|
+
outlineWidth: 1 / 7,
|
|
13989
|
+
outlineColor: [255, 255, 255, 128],
|
|
13990
|
+
getTextAnchor: b => b.title.anchor,
|
|
13991
|
+
// top of the text ≈ first baseline − 0.8 font
|
|
13992
|
+
getAlignmentBaseline: 'top',
|
|
13993
|
+
getPixelOffset: b => { const f = offsetOf(b); return [b.title.x + f[0], b.title.top + f[1]]; },
|
|
13994
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
13995
|
+
}));
|
|
13996
|
+
return { box, titles };
|
|
13997
|
+
}
|
|
13998
|
+
|
|
12669
13999
|
// BOX (see the BOX block in _buildChartLayers): a rectangle icon
|
|
12670
14000
|
// w×h px, rounded corners rx, stroke sw px; drawn at twice the size
|
|
12671
14001
|
_buildBoxIcon(w, h, rx, sw, look) {
|
|
@@ -12684,8 +14014,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12684
14014
|
ctx.arcTo(x, y + hh, x, y, rr); ctx.arcTo(x, y, x + ww, y, rr); ctx.closePath();
|
|
12685
14015
|
};
|
|
12686
14016
|
path();
|
|
12687
|
-
ctx.globalAlpha = look.fillOpacity; ctx.fillStyle = look.fill; ctx.fill();
|
|
12688
|
-
if (look.stroke && S > 0) { ctx.globalAlpha = look.strokeOpacity; ctx.strokeStyle = look.stroke; ctx.lineWidth = S * k; ctx.stroke(); }
|
|
14017
|
+
if (!isNoneColor(look.fill)) { ctx.globalAlpha = look.fillOpacity; ctx.fillStyle = look.fill; ctx.fill(); }
|
|
14018
|
+
if (look.stroke && !isNoneColor(look.stroke) && S > 0) { ctx.globalAlpha = look.strokeOpacity; ctx.strokeStyle = look.stroke; ctx.lineWidth = S * k; ctx.stroke(); }
|
|
12689
14019
|
// getSize is the icon's height: the padding is part of it
|
|
12690
14020
|
const icon = { url: canvas.toDataURL(), width: canvas.width, height: canvas.height, anchorX: canvas.width / 2, anchorY: canvas.height / 2, id: key };
|
|
12691
14021
|
return this._cacheIcon(key, icon);
|
|
@@ -12836,7 +14166,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12836
14166
|
const groups = doRelocate ? this._groupCoLocated(clusterFeatures, effectiveZoom) : clusterFeatures.map(f => {
|
|
12837
14167
|
return {
|
|
12838
14168
|
geometry: f.geometry,
|
|
12839
|
-
|
|
14169
|
+
// counts is the summed bound VALUE and recordCounts the record
|
|
14170
|
+
// count, as groupCoLocated builds them (RELOCATE) — RELOCATE
|
|
14171
|
+
// moves the item, it never changes a value
|
|
14172
|
+
properties: { counts: this._oneHot(f.properties.cat, f.properties.value),
|
|
14173
|
+
recordCounts: this._oneHot(f.properties.cat, f.properties.point_count), total: f.properties.value,
|
|
12840
14174
|
...(f.properties.classValue !== undefined ? { classTotal: f.properties.classValue } : {}),
|
|
12841
14175
|
...(f.properties.titleRaw ? { titleRaw: f.properties.titleRaw } : {}) }
|
|
12842
14176
|
};
|
|
@@ -12936,7 +14270,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12936
14270
|
const rangeSource = this._classSeparate && this.binding.size && !normalize ? rawSizeTotals : totals;
|
|
12937
14271
|
let nMin = Infinity, nMax = -Infinity;
|
|
12938
14272
|
for (const v of rangeSource) { if (v < nMin) nMin = v; if (v > nMax) nMax = v; }
|
|
12939
|
-
|
|
14273
|
+
const explicit = explicitRangeBreaks(this.style, this.flags);
|
|
14274
|
+
breaks = explicit ? explicitRangeParts(explicit)
|
|
14275
|
+
: flatRangeParts(totals, nMin, nMax, nParts, this.flags, styleNum(this.style.rangecentervalue));
|
|
12940
14276
|
}
|
|
12941
14277
|
|
|
12942
14278
|
// The theme's own classes follow the aggregated items, as flat's do
|
|
@@ -13253,15 +14589,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13253
14589
|
// DTEXT: the title font follows the chart, max(0.9u, min(W/5,
|
|
13254
14590
|
// 14u / scale)) (18815) — measured on the fire areas page's SIF chart
|
|
13255
14591
|
// (W 362 units → font 72.35).
|
|
13256
|
-
const boxShown = flatFlag(this.flags, 'BOX') && !boxHiddenByScale(this.style, liveZoom);
|
|
13257
|
-
const circular = flatFlag(this.flags, 'CIRCULAR');
|
|
13258
14592
|
const unitPx = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1);
|
|
13259
|
-
const boxMarginU = styleNum(this.style.boxmargin) || 2;
|
|
13260
|
-
const titleShown = boxShown && flatFlag(this.flags, 'TITLE') && !!this.binding.title;
|
|
13261
|
-
const titleScale = styleNum(this.style.textscale) || styleNum(this.style.valuescale) || 1;
|
|
13262
|
-
const titleFontOf = W => (flatFlag(this.flags, 'DTEXT')
|
|
13263
|
-
? Math.max(0.9 * unitPx, Math.min(W / 5, 14 * unitPx / (styleNum(this.style.scale) || 1)))
|
|
13264
|
-
: 5 * unitPx) * titleScale;
|
|
13265
14593
|
// the chart's value text and label (see VALUES and LABEL below), shared
|
|
13266
14594
|
// with the box: flat's box encloses whatever the chart group draws
|
|
13267
14595
|
const valuesShown = this.flags.has('VALUES') && !valuesHiddenByScale(this.style, zoom);
|
|
@@ -13269,144 +14597,35 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13269
14597
|
const labelShown = !!this.style.labelfield && !flatFlag(this.flags, 'NORMSIZE') && !flatFlag(this.flags, 'ZOOM')
|
|
13270
14598
|
&& !labelHiddenByScale(this.style, zoom);
|
|
13271
14599
|
const labelOf = labelShown ? this._chartLabelFn(unitPx) : null;
|
|
13272
|
-
//
|
|
13273
|
-
//
|
|
13274
|
-
const
|
|
13275
|
-
|
|
13276
|
-
|
|
13277
|
-
|
|
13278
|
-
|
|
13279
|
-
|
|
13280
|
-
|
|
13281
|
-
|
|
13282
|
-
|
|
13283
|
-
|
|
13284
|
-
|
|
13285
|
-
const w = chart[2] - chart[0];
|
|
13286
|
-
const margin = Math.min(2 * boxMarginU * unitPx, boxMarginU * w / 30);
|
|
13287
|
-
const boxR = circular ? r * 1.1 + margin : r;
|
|
13288
|
-
const titleFontPx = titleFontOf(w);
|
|
13289
|
-
let title = null;
|
|
13290
|
-
if (titleShown) {
|
|
13291
|
-
const text = this._itemTitle(d.properties);
|
|
13292
|
-
if (text) {
|
|
13293
|
-
const f = titleFontPx;
|
|
13294
|
-
const lines = wrapTitleLines(text, f, circular ? boxR * 2 : w).split('\n');
|
|
13295
|
-
const width = Math.max(...lines.map(l => titleTextWidth(l, f)));
|
|
13296
|
-
// first baseline; lines go down one font each
|
|
13297
|
-
const base = circular
|
|
13298
|
-
? (bottomTitle ? boxR + f : -boxR - 0.7 * f - (lines.length - 1) * f)
|
|
13299
|
-
: (bottomTitle ? chart[3] + f : chart[1] - 0.7 * f - (lines.length - 1) * f);
|
|
13300
|
-
const x = circular ? unitPx / 2 : (alignRight ? chart[2] : chart[0]);
|
|
13301
|
-
const anchor = circular ? 'middle' : (alignRight ? 'end' : 'start');
|
|
13302
|
-
const x0 = anchor === 'middle' ? x - width / 2 : anchor === 'end' ? x - width : x;
|
|
13303
|
-
// arial: ascent 0.905, descent 0.212 of the font size
|
|
13304
|
-
title = { text: lines.join('\n'), x, anchor, top: base - 0.8 * f, f,
|
|
13305
|
-
extent: [x0, base - 0.905 * f, x0 + width, base + (lines.length - 1) * f + 0.212 * f] };
|
|
13306
|
-
}
|
|
13307
|
-
}
|
|
13308
|
-
let rect = null;
|
|
13309
|
-
if (!circular) {
|
|
13310
|
-
const e = chart.slice();
|
|
13311
|
-
// with a title flat trims 0.1 font off the top of chart + title
|
|
13312
|
-
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]); }
|
|
13313
|
-
rect = [e[0] - margin, e[1] - margin, e[2] + margin, e[3] + margin];
|
|
14600
|
+
// with MULTIQUAD only the first item of a position gets its box/title;
|
|
14601
|
+
// the circle sits on the anchor (0, 0), its base radius the symbol's
|
|
14602
|
+
const boxLayout = this._boxRows(() => combined.filter(d => !multi || multi.index.get(d) === 0), {
|
|
14603
|
+
unit: unitPx, liveZoom,
|
|
14604
|
+
radiusOf: d => vRadius(sizeValueOf(d)), centerOf: () => [0, 0],
|
|
14605
|
+
extentOf: d => {
|
|
14606
|
+
const r = vRadius(sizeValueOf(d));
|
|
14607
|
+
const ext = multi ? multi.extent.get(d) : [0, 0, 0, 0];
|
|
14608
|
+
const chart = [ext[0] - r, ext[1] - r, ext[2] + r, ext[3] + r];
|
|
14609
|
+
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]); };
|
|
14610
|
+
if (valueTextOf && !d.properties.counts) { const v = valueTextOf(d, r); if (v) grow(v.extent); }
|
|
14611
|
+
if (labelOf) { const l = labelOf(d, r); if (l) grow(l.extent); }
|
|
14612
|
+
return chart;
|
|
13314
14613
|
}
|
|
13315
|
-
|
|
14614
|
+
});
|
|
14615
|
+
// (the title layers are pushed after the symbols, below)
|
|
14616
|
+
const boxLayers = boxLayout ? this._boxTitleLayers(boxLayout, {
|
|
14617
|
+
unit: unitPx, liveZoom,
|
|
14618
|
+
positionOf: b => b.d.geometry.coordinates, offsetOf: b => alignOf(b.d),
|
|
14619
|
+
ids: { circle: `ix-box-${this.name}`, icon: `ix-box-${this.name}`, polygons: `ix-box-polygons-${this.name}`, titles: `ix-titles-${this.name}` }
|
|
13316
14620
|
}) : null;
|
|
13317
|
-
if (
|
|
13318
|
-
const boxRgb = hexOrNamedToRgb(this.style.boxcolor || '#eeeeee');
|
|
13319
|
-
const boxAlpha = Math.round(255 * boxOpacity);
|
|
13320
|
-
const borderRgb = hexOrNamedToRgb(this.style.bordercolor || '#dddddd');
|
|
13321
|
-
// ScatterplotLayer has no pixel offset: a moved chart (align,
|
|
13322
|
-
// offsetx/offsety) gets its circle as an icon
|
|
13323
|
-
if (alignOf.active) layers.push(new IconLayer({
|
|
13324
|
-
id: `ix-box-${this.name}-g${this._iconGeneration}`,
|
|
13325
|
-
data: boxLayout, pickable: false,
|
|
13326
|
-
getPosition: b => b.d.geometry.coordinates,
|
|
13327
|
-
getIcon: b => this._buildSingleIcon(boxRgb, boxAlpha / 255, 'circle', borderRgb,
|
|
13328
|
-
Math.round(unitPx / 20 * BUBBLE_ICON_SIZE / Math.max(1, 2 * b.boxR) * 4) / 4),
|
|
13329
|
-
getSize: b => b.boxR * 2,
|
|
13330
|
-
getPixelOffset: b => alignOf(b.d),
|
|
13331
|
-
sizeUnits: 'pixels',
|
|
13332
|
-
billboard: true,
|
|
13333
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
13334
|
-
}));
|
|
13335
|
-
else layers.push(new ScatterplotLayer({
|
|
13336
|
-
id: `ix-box-${this.name}`,
|
|
13337
|
-
data: boxLayout, pickable: false,
|
|
13338
|
-
getPosition: b => b.d.geometry.coordinates,
|
|
13339
|
-
getRadius: b => b.boxR,
|
|
13340
|
-
radiusUnits: 'pixels',
|
|
13341
|
-
stroked: true,
|
|
13342
|
-
getFillColor: [...boxRgb.slice(0, 3), boxAlpha],
|
|
13343
|
-
getLineColor: [...borderRgb.slice(0, 3), 255],
|
|
13344
|
-
getLineWidth: unitPx / 20,
|
|
13345
|
-
lineWidthUnits: 'pixels',
|
|
13346
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
13347
|
-
}));
|
|
13348
|
-
} else if (boxShown) {
|
|
13349
|
-
const borderStyle = String(this.style.borderstyle || '');
|
|
13350
|
-
const boxLook = {
|
|
13351
|
-
fill: this.style.boxcolor || 'white',
|
|
13352
|
-
fillOpacity: boxOpacity,
|
|
13353
|
-
stroke: borderStyle === 'none' ? null : styleLineColor(this.style.bordercolor) || '#bbbbbb',
|
|
13354
|
-
strokeOpacity: /^(solid|dotted|dashed)$/.test(borderStyle) ? 1 : 0.3,
|
|
13355
|
-
};
|
|
13356
|
-
const borderWidth = styleNum(this.style.borderwidth) || 1;
|
|
13357
|
-
const borderRadius = styleNum(this.style.borderradius);
|
|
13358
|
-
const strokeOf = b => borderWidth * 0.2 * b.w / 30;
|
|
13359
|
-
// a box larger than BOX_ICON_MAX_PX (a MULTIQUAD grid of thousands
|
|
13360
|
-
// of items) would need a huge canvas: it is drawn as a polygon in
|
|
13361
|
-
// map coordinates instead (square corners)
|
|
13362
|
-
const bigBox = b => Math.max(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1]) > BOX_ICON_MAX_PX;
|
|
13363
|
-
const iconBoxes = boxLayout.filter(b => !bigBox(b)), polygonBoxes = boxLayout.filter(bigBox);
|
|
13364
|
-
iconBoxes.forEach(b => {
|
|
13365
|
-
b.icon = this._buildBoxIcon(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1],
|
|
13366
|
-
isNaN(borderRadius) ? 0 : borderRadius * Math.min(unitPx, b.w / 30),
|
|
13367
|
-
strokeOf(b), boxLook);
|
|
13368
|
-
});
|
|
13369
|
-
if (polygonBoxes.length) {
|
|
13370
|
-
const fillRgb = hexOrNamedToRgb(boxLook.fill);
|
|
13371
|
-
const strokeRgb = boxLook.stroke ? hexOrNamedToRgb(boxLook.stroke) : null;
|
|
13372
|
-
layers.push(new GeoJsonLayer({
|
|
13373
|
-
id: `ix-box-polygons-${this.name}`,
|
|
13374
|
-
data: polygonBoxes.map(b => {
|
|
13375
|
-
const o = alignOf(b.d), c = b.d.geometry.coordinates, r = b.rect;
|
|
13376
|
-
const at = (x, y) => pixelOffsetLngLat(c, [x + o[0], y + o[1]], liveZoom);
|
|
13377
|
-
return { type: 'Feature', properties: { stroke: strokeOf(b) },
|
|
13378
|
-
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])]] } };
|
|
13379
|
-
}),
|
|
13380
|
-
pickable: false,
|
|
13381
|
-
filled: boxLook.fillOpacity > 0,
|
|
13382
|
-
stroked: !!strokeRgb,
|
|
13383
|
-
getFillColor: [...fillRgb.slice(0, 3), Math.round(255 * boxLook.fillOpacity)],
|
|
13384
|
-
getLineColor: strokeRgb ? [...strokeRgb.slice(0, 3), Math.round(255 * boxLook.strokeOpacity)] : [0, 0, 0, 0],
|
|
13385
|
-
getLineWidth: f => f.properties.stroke,
|
|
13386
|
-
lineWidthUnits: 'pixels',
|
|
13387
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
13388
|
-
}));
|
|
13389
|
-
}
|
|
13390
|
-
if (iconBoxes.length) layers.push(new IconLayer({
|
|
13391
|
-
id: `ix-box-${this.name}-g${this._iconGeneration}`,
|
|
13392
|
-
data: iconBoxes, pickable: false,
|
|
13393
|
-
getPosition: b => b.d.geometry.coordinates,
|
|
13394
|
-
getIcon: b => b.icon,
|
|
13395
|
-
// the icon is drawn at 2× with a stroke padding around the box
|
|
13396
|
-
getSize: b => b.icon.height / 2,
|
|
13397
|
-
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]]; },
|
|
13398
|
-
sizeUnits: 'pixels',
|
|
13399
|
-
billboard: true,
|
|
13400
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
13401
|
-
}));
|
|
13402
|
-
}
|
|
14621
|
+
if (boxLayers) layers.push(...boxLayers.box);
|
|
13403
14622
|
|
|
13404
14623
|
// flat's per-part SYMBOL branch (everything but CATEGORICAL without
|
|
13405
|
-
// AGGREGATE/GROUP) draws a square of side 2r, the other
|
|
13406
|
-
// (drawSymbolPath's own square)
|
|
14624
|
+
// AGGREGATE/GROUP) draws a square or roundrect of side 2r, the other
|
|
14625
|
+
// one 1.6r (drawSymbolPath's own square / roundrect)
|
|
13407
14626
|
const perPartBranch = flatChartBranch(this.flags) === 'symbol'
|
|
13408
14627
|
&& !(this.flags.has('CATEGORICAL') && !this.flags.has('AGGREGATE') && !flatFlag(this.flags, 'GROUP'));
|
|
13409
|
-
const shapeOf = props => { const sh = this._resolveSymbolShape(props); return sh === 'square'
|
|
14628
|
+
const shapeOf = props => { const sh = this._resolveSymbolShape(props); return perPartBranch && (sh === 'square' || sh === 'roundrect') ? sh + 'full' : sh; };
|
|
13410
14629
|
// a range-classed cell is ONE class: one symbol in its class color,
|
|
13411
14630
|
// fillopacity and linecolor, as a single item (flat draws one
|
|
13412
14631
|
// symbol) — the packed multi-category icon is for CATEGORICAL cells
|
|
@@ -13507,29 +14726,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13507
14726
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
13508
14727
|
}));
|
|
13509
14728
|
|
|
13510
|
-
|
|
13511
|
-
if (titleData.length) {
|
|
13512
|
-
layers.push(new TextLayer({
|
|
13513
|
-
id: `ix-titles-${this.name}`,
|
|
13514
|
-
data: titleData, pickable: false,
|
|
13515
|
-
getPosition: b => b.d.geometry.coordinates,
|
|
13516
|
-
getText: b => b.title.text,
|
|
13517
|
-
getSize: b => b.title.f,
|
|
13518
|
-
sizeUnits: 'pixels',
|
|
13519
|
-
fontFamily: 'arial',
|
|
13520
|
-
characterSet: 'auto',
|
|
13521
|
-
lineHeight: 1,
|
|
13522
|
-
getColor: [...hexOrNamedToRgb(this.style.textcolor || '#888888').slice(0, 3), 255],
|
|
13523
|
-
fontSettings: { sdf: true },
|
|
13524
|
-
outlineWidth: 1 / 7,
|
|
13525
|
-
outlineColor: [255, 255, 255, 128],
|
|
13526
|
-
getTextAnchor: b => b.title.anchor,
|
|
13527
|
-
// top of the text ≈ first baseline − 0.8 font
|
|
13528
|
-
getAlignmentBaseline: 'top',
|
|
13529
|
-
getPixelOffset: b => { const o = alignOf(b.d); return [b.title.x + o[0], b.title.top + o[1]]; },
|
|
13530
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
13531
|
-
}));
|
|
13532
|
-
}
|
|
14729
|
+
if (boxLayers) layers.push(...boxLayers.titles);
|
|
13533
14730
|
|
|
13534
14731
|
// VALUES: bold value label centered on each bubble (see
|
|
13535
14732
|
// flatValueText/valuesFontSizePx above). Labels are their own
|
|
@@ -13730,13 +14927,55 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13730
14927
|
_buildSequenceChartLayers(charts, zoom, liveZoom) {
|
|
13731
14928
|
const st = this.style;
|
|
13732
14929
|
const multi = !!this._multiFields && this._isMultiFieldSequence();
|
|
13733
|
-
|
|
14930
|
+
// AGGREGATE + AUTO100 (flat normalizes the aggregated item's
|
|
14931
|
+
// nValuesA to % of their own sum, maptheme.js 12166-12176): every
|
|
14932
|
+
// item's parts in % of that item. Without AGGLEARATE, applyField100
|
|
14933
|
+
// already normalized the records' field values (auto100 there) —
|
|
14934
|
+
// this must not run twice
|
|
14935
|
+
const auto100 = multi && this.flags.has('AUTO100') && this.flags.has('AGGREGATE');
|
|
14936
|
+
// The value scale must be measured on the SAME scale the parts are
|
|
14937
|
+
// drawn in. With AGGREGATE, AUTO100 normalizes every item's parts to
|
|
14938
|
+
// % of their own sum at layout time (the records themselves stay
|
|
14939
|
+
// raw — applyField100 skips AGGREGATE themes), so the raw-features
|
|
14940
|
+
// _multiFieldPartMax (~1.0 here) is 100x off the drawn values and
|
|
14941
|
+
// would blow every part's radius up by sqrt(100). flat's own
|
|
14942
|
+
// nMaxValue fallback (nNormalSizeValue || nAllMaxValue,
|
|
14943
|
+
// maptheme.js 21796) measures nAllMaxValue on the NORMALIZED item
|
|
14944
|
+
// values (12166-12176), so the scale matches the parts. Without
|
|
14945
|
+
// AUTO100 the records were already normalized by applyField100 and
|
|
14946
|
+
// _multiFieldPartMax IS that scale (NYC status page)
|
|
14947
|
+
if (multi && auto100 && !this._seqAuto100Max) {
|
|
14948
|
+
let m = 0;
|
|
14949
|
+
for (const f of this.features) {
|
|
14950
|
+
const parts = this._multiFields.map(k => parseFloat(f.properties && f.properties[k]) || 0);
|
|
14951
|
+
const sum = parts.reduce((a, v) => a + v, 0);
|
|
14952
|
+
if (sum > 0) for (const v of parts) { const nv = v / (sum / 100); if (nv > m) m = nv; }
|
|
14953
|
+
}
|
|
14954
|
+
this._seqAuto100Max = m || 1;
|
|
14955
|
+
}
|
|
14956
|
+
const seqMax = multi
|
|
14957
|
+
? (auto100 ? this._seqAuto100Max : (styleNum(st.normalsizevalue) || this._multiFieldPartMax))
|
|
14958
|
+
: this._sequenceMaxValue(zoom);
|
|
13734
14959
|
const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(st.scale) || 1);
|
|
13735
14960
|
const normalR = NORMAL_RADIUS_PX * unit;
|
|
13736
14961
|
const sizePow = resolveSizePow(st, this.flags);
|
|
13737
|
-
|
|
13738
|
-
|
|
13739
|
-
|
|
14962
|
+
// flat's size field on a SEQUENCE chart part (maptheme.js 21851-21874):
|
|
14963
|
+
// the radius carries BOTH the part's value AND the item's size —
|
|
14964
|
+
// nMaxRadius · (nValue/nMaxValue)^(1/p) · (nSize/nMaxSize)^(1/p),
|
|
14965
|
+
// nMaxSize the biggest item size (measured on the records; MAX, the
|
|
14966
|
+
// one combiner flat's star-part charts pair with a size field, keeps
|
|
14967
|
+
// the cells' max equal to the records' max)
|
|
14968
|
+
const sizeField = multi && this.binding.size && this.binding.size !== '$item$' ? this.binding.size : null;
|
|
14969
|
+
let sizeMax = 0;
|
|
14970
|
+
if (sizeField) {
|
|
14971
|
+
for (const f of this.features) { const v = Math.abs(parseFloat(f.properties && f.properties[sizeField]) || 0); if (v > sizeMax) sizeMax = v; }
|
|
14972
|
+
}
|
|
14973
|
+
const itemSizeFactor = d => {
|
|
14974
|
+
if (!sizeField) return 1;
|
|
14975
|
+
const sz = typeof d.properties.size === 'number' ? d.properties.size
|
|
14976
|
+
: Math.abs(parseFloat((d.properties.raw || {})[sizeField]) || 0);
|
|
14977
|
+
return sizeMax > 0 ? Math.pow(sz / sizeMax, 1 / sizePow) : 0;
|
|
14978
|
+
};
|
|
13740
14979
|
const rings = flatFlag(this.flags, 'RINGS');
|
|
13741
14980
|
const centerValues = flatFlag(this.flags, 'CENTERVALUES');
|
|
13742
14981
|
const noFill = this._partNoFill || new Set();
|
|
@@ -13749,17 +14988,116 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13749
14988
|
const borderRgb = st.linecolor && styleLineColor(st.linecolor) !== 'none' ? hexOrNamedToRgb(styleLineColor(st.linecolor)) : null;
|
|
13750
14989
|
const lineWidth = styleNum(st.linewidth) || 1;
|
|
13751
14990
|
const symbols = Array.isArray(st.symbols) ? st.symbols : null;
|
|
14991
|
+
// flat draws an external .svg/.png/.jpeg symbol file as an <image>
|
|
14992
|
+
// node (maptheme.js 22190-22199), the chart's own shape otherwise.
|
|
14993
|
+
// Here the same URL is the part's IconLayer icon — deck.gl loads
|
|
14994
|
+
// it itself (its ImageBitmap loader falls back to <img>, which
|
|
14995
|
+
// rasterizes SVG) — NOT tinted: these files carry their own colors,
|
|
14996
|
+
// and flat doesn't recolor an <image> either
|
|
14997
|
+
const urlSymbolOf = i => {
|
|
14998
|
+
const s = symbols && (symbols[i] || symbols[0]);
|
|
14999
|
+
return typeof s === 'string' && /\.(svg|png|jpe?g)([?#]|$)/i.test(s.trim()) ? s.trim() : null;
|
|
15000
|
+
};
|
|
15001
|
+
if (!this._urlIconCache) this._urlIconCache = new Map();
|
|
15002
|
+
const urlIcon = u => {
|
|
15003
|
+
if (!this._urlIconCache.has(u)) {
|
|
15004
|
+
// the placeholder is a transparent 1x1 PNG, NOT the raw symbol
|
|
15005
|
+
// URL: deck.gl would immediately try to load that itself, and an
|
|
15006
|
+
// SVG without natural dimensions (a viewBox only, the bus
|
|
15007
|
+
// pictogram of the City-moving page) makes Chrome's
|
|
15008
|
+
// createImageBitmap throw — a console error and a part that
|
|
15009
|
+
// never renders. The real raster replaces it below
|
|
15010
|
+
const entry = { url: TRANSPARENT_1PX_PNG, width: 256, height: 256, anchorX: 128, anchorY: 128, mask: false, id: 'seq-sym-pending:' + u };
|
|
15011
|
+
this._urlIconCache.set(u, entry);
|
|
15012
|
+
// fetch the symbol ONCE and rasterize it into the descriptor's
|
|
15013
|
+
// own 256px cell here: deck then always sees exactly the declared
|
|
15014
|
+
// dimensions, and dimension-less SVGs work like any other. The
|
|
15015
|
+
// redraw lets the next getIcon see the patched descriptor
|
|
15016
|
+
fetch(u).then(r => r.ok ? r.text() : null).then(text => new Promise((resolve, reject) => {
|
|
15017
|
+
if (typeof text !== 'string' || !/<svg[\s>]/i.test(text)) { reject(new Error('not svg')); return; }
|
|
15018
|
+
if (!/\swidth\s*=/i.test(text) || !/\sheight\s*=/i.test(text)) {
|
|
15019
|
+
const vb = text.match(/viewBox\s*=\s*["']([^"']*)["']/i);
|
|
15020
|
+
const p = vb && vb[1].trim().split(/[\s,]+/).map(Number);
|
|
15021
|
+
if (p && p.length === 4 && isFinite(p[2]) && p[2] > 0 && isFinite(p[3]) && p[3] > 0) {
|
|
15022
|
+
text = text.replace(/<svg/i, '<svg width="' + p[2] + '" height="' + p[3] + '"');
|
|
15023
|
+
}
|
|
15024
|
+
}
|
|
15025
|
+
const img = new Image();
|
|
15026
|
+
img.onload = () => resolve(img);
|
|
15027
|
+
img.onerror = () => reject(new Error('svg decode failed'));
|
|
15028
|
+
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(text);
|
|
15029
|
+
})).then(img => {
|
|
15030
|
+
const size = entry.width;
|
|
15031
|
+
const canvas = document.createElement('canvas');
|
|
15032
|
+
canvas.width = size; canvas.height = size;
|
|
15033
|
+
const ctx = canvas.getContext('2d');
|
|
15034
|
+
const s = Math.min(size / (img.naturalWidth || size), size / (img.naturalHeight || size));
|
|
15035
|
+
const dw = (img.naturalWidth || size) * s, dh = (img.naturalHeight || size) * s;
|
|
15036
|
+
ctx.drawImage(img, (size - dw) / 2, (size - dh) / 2, dw, dh);
|
|
15037
|
+
entry.url = canvas.toDataURL('image/png');
|
|
15038
|
+
entry.id = 'seq-sym-raster:' + u;
|
|
15039
|
+
if (this._triggerRedraw) this._triggerRedraw();
|
|
15040
|
+
}).catch(() => {
|
|
15041
|
+
// not an SVG (a PNG/JPEG symbol): deck's own URL loading takes
|
|
15042
|
+
// it from here — and a pre-sized SVG that failed to rasterize
|
|
15043
|
+
// gets that same second chance
|
|
15044
|
+
entry.url = u;
|
|
15045
|
+
entry.id = 'seq-sym-natural:' + u;
|
|
15046
|
+
if (this._triggerRedraw) this._triggerRedraw();
|
|
15047
|
+
});
|
|
15048
|
+
}
|
|
15049
|
+
return this._urlIconCache.get(u);
|
|
15050
|
+
};
|
|
13752
15051
|
const shapeOf = i => { const sh = symbols && (symbols[i] || symbols[0]); return normalizeSymbolShape(sh); };
|
|
13753
15052
|
const colorOf = i => this.classRgb(i);
|
|
13754
|
-
const
|
|
15053
|
+
const allParts = [], allGlows = [];
|
|
13755
15054
|
for (const d of charts) {
|
|
13756
|
-
|
|
15055
|
+
// parts: the record's own field values (a per-item chart, or the
|
|
15056
|
+
// single-record cell of an aggregated one); series: a multi-record
|
|
15057
|
+
// AGGREGATE cell's per-field combination (aggregateOnGrid); counts:
|
|
15058
|
+
// the CATEGORICAL variant
|
|
15059
|
+
let counts = d.properties.parts || d.properties.series || d.properties.counts;
|
|
15060
|
+
if (!Array.isArray(counts)) continue;
|
|
15061
|
+
if (auto100) {
|
|
15062
|
+
const sum = counts.reduce((a, v) => a + (v || 0), 0);
|
|
15063
|
+
counts = counts.map(v => sum > 0 ? v / (sum / 100) : 0);
|
|
15064
|
+
}
|
|
15065
|
+
// flat's aggregated CATEGORICAL item holds a value per category only up
|
|
15066
|
+
// to the highest category it has (measured on flat: the chart's star and
|
|
15067
|
+
// glow follow the categories present as 0..k, 985 of 988 charts of the
|
|
15068
|
+
// concessioni page): trailing zeros are no parts, so with SORT|UP the
|
|
15069
|
+
// smallest part — order index 0, the one that sets the star radius and
|
|
15070
|
+
// carries the glow — is a zero only when a category BELOW the highest
|
|
15071
|
+
// one is missing
|
|
15072
|
+
if (!d.properties.parts && !d.properties.series) {
|
|
15073
|
+
let n = counts.length;
|
|
15074
|
+
while (n > 0 && !counts[n - 1]) n--;
|
|
15075
|
+
if (n < counts.length) counts = counts.slice(0, n);
|
|
15076
|
+
}
|
|
15077
|
+
const sizeFactor = itemSizeFactor(d);
|
|
15078
|
+
const partRadius = multi
|
|
15079
|
+
? v => Math.max(VALUE_RADIUS_MIN, normalR * Math.pow(Math.abs(v) / seqMax, 1 / sizePow) * sizeFactor)
|
|
15080
|
+
: v => valueRadius(v, liveZoom, st, this.mapOptions, this.flags, seqMax);
|
|
13757
15081
|
let layout = rings ? ringsLayout(counts, partRadius)
|
|
13758
15082
|
: sequenceLayout(counts, this.flags, partRadius, { maxRadius: normalR, maxValue: seqMax, nParts: nCats, rangeScale: styleNum(st.rangescale) || 0 });
|
|
13759
15083
|
if (!layout.length) continue;
|
|
13760
15084
|
// GLOW belongs to the first part (sorted) or to every part, and
|
|
13761
15085
|
// hides with it when its class is not marked
|
|
13762
|
-
|
|
15086
|
+
// every chart glows (deliberately NOT flat's rule, which gives the glow to
|
|
15087
|
+
// the part at sorted order index 0 — a zero, skipped, whenever a category
|
|
15088
|
+
// below the highest is missing — so only about a quarter of flat's charts
|
|
15089
|
+
// glow): a sorted chart's glow belongs to its biggest part, an unsorted
|
|
15090
|
+
// one glows with every part as in flat
|
|
15091
|
+
const biggest = layout.reduce((m, p) => (p.v > m.v ? p : m), layout[0]);
|
|
15092
|
+
const glowParts = new Set(sorted ? [biggest.i] : layout.map(p => p.i));
|
|
15093
|
+
// the chart's drawn extent (flat: map.Dom.getBox of its shape
|
|
15094
|
+
// group) — the parts' star cluster — for BOX / CIRCULARBOX sizing
|
|
15095
|
+
let ex0 = Infinity, ey0 = Infinity, ex1 = -Infinity, ey1 = -Infinity;
|
|
15096
|
+
layout.forEach(p => {
|
|
15097
|
+
ex0 = Math.min(ex0, p.x - p.r); ey0 = Math.min(ey0, p.y - p.r);
|
|
15098
|
+
ex1 = Math.max(ex1, p.x + p.r); ey1 = Math.max(ey1, p.y + p.r);
|
|
15099
|
+
});
|
|
15100
|
+
d._seqExtent = [ex0, ey0, ex1, ey1];
|
|
13763
15101
|
if (marking && !gray) {
|
|
13764
15102
|
layout = layout.filter(p => this._markedClasses.has(p.i)).map(p => ({ ...p, x: 0, y: 0 }));
|
|
13765
15103
|
if (!layout.length) continue;
|
|
@@ -13768,101 +15106,286 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13768
15106
|
d._seqLast = layout[layout.length - 1].r;
|
|
13769
15107
|
const items = layout.map(p => {
|
|
13770
15108
|
const item = { geometry: d.geometry, properties: d.properties, chart: d, part: p };
|
|
13771
|
-
if (glowParts.has(p.i))
|
|
15109
|
+
if (glowParts.has(p.i)) allGlows.push(item);
|
|
13772
15110
|
return item;
|
|
13773
15111
|
});
|
|
13774
15112
|
// STAR: the first part (its center) is put on top at the end (23145)
|
|
13775
15113
|
if (star && items.length > 1) items.push(items.shift());
|
|
13776
|
-
|
|
15114
|
+
allParts.push(...items);
|
|
13777
15115
|
}
|
|
13778
15116
|
const alignOf = this._chartAlignFn(liveZoom, d => (d.chart || d)._seqLast || 0);
|
|
13779
15117
|
const offsetOf = item => { const o = alignOf(item.chart); return [o[0] + item.part.x, o[1] + item.part.y]; };
|
|
13780
15118
|
const alphaOf = item => (gray && !this._markedClasses.has(item.part.i) ? DIMMED_ICON_ALPHA : 255);
|
|
13781
15119
|
const layers = [];
|
|
13782
|
-
|
|
13783
|
-
|
|
13784
|
-
|
|
13785
|
-
|
|
13786
|
-
|
|
13787
|
-
|
|
13788
|
-
|
|
13789
|
-
|
|
13790
|
-
|
|
13791
|
-
|
|
13792
|
-
|
|
13793
|
-
|
|
13794
|
-
|
|
15120
|
+
// BOX / CIRCULARBOX with TITLE / BOTTOMTITLE — flat's own box block
|
|
15121
|
+
// (maptheme.js 18757-18925) on a SEQUENCE chart's extent: the star
|
|
15122
|
+
// cluster of parts (d._seqExtent), not one bubble's radius.
|
|
15123
|
+
// - CIRCULARBOX: a circle of max(w,h)/2 · 1.1 + margin, margin
|
|
15124
|
+
// min(2m, m·w/30u), m = boxmargin (2) · u; boxcolor (#eeeeee) at
|
|
15125
|
+
// boxopacity, stroke bordercolor (#dddddd) one SVG unit (u/20)
|
|
15126
|
+
// - the title (TITLE / BOTTOMTITLE) in textcolor (#888888) over
|
|
15127
|
+
// the half-opaque white halo, centered under the circle
|
|
15128
|
+
// (BOTTOMTITLE: one font below its bottom edge), DTEXT font
|
|
15129
|
+
// max(0.9u, min(w/5, 14u/scale)) · textscale, wrapped to the
|
|
15130
|
+
// circle's width — gated by titleupper (shown while the map
|
|
15131
|
+
// scale is that bound or coarser, 18810) and boxupper/boxlower
|
|
15132
|
+
// (flat centers the circular box and its title on the chart's
|
|
15133
|
+
// bounding-box CENTER — maptheme.js 18793-18798 — not on its anchor: a
|
|
15134
|
+
// star cluster's satellites spread asymmetrically around it; the
|
|
15135
|
+
// circle's title baselines are measured on the resized box, 18798-18830)
|
|
15136
|
+
const circular = flatFlag(this.flags, 'CIRCULAR');
|
|
15137
|
+
const boxLayoutAll = this._boxRows(() => charts.filter(d => d._seqExtent), { unit, liveZoom, extentOf: d => d._seqExtent });
|
|
15138
|
+
// per-chart overlap bands (see the band computation in the
|
|
15139
|
+
// box block): a Map chart → band index, filled only when
|
|
15140
|
+
// boxes are drawn; empty (single band) otherwise
|
|
15141
|
+
const bandOfChart = new Map();
|
|
15142
|
+
let nBands = 1;
|
|
15143
|
+
if (boxLayoutAll) {
|
|
15144
|
+
// flat draws every chart as ONE group — its box, its symbols,
|
|
15145
|
+
// its title, its values together — and the groups are sorted by
|
|
15146
|
+
// size, so a bigger chart covers a smaller one ENTIRELY
|
|
15147
|
+
// (chartMap's ascending sort, the biggest drawn last). deck.gl
|
|
15148
|
+
// stacks whole layers, so with every box in one layer below
|
|
15149
|
+
// every symbol, a bigger chart's box covered a smaller chart's
|
|
15150
|
+
// box while that chart's symbols floated on top of it (the
|
|
15151
|
+
// City-moving page's overlapping circles). Charts are grouped
|
|
15152
|
+
// into overlap "bands" instead — an interval coloring assigned
|
|
15153
|
+
// biggest-chart-first: within a band no two charts overlap
|
|
15154
|
+
// (their box + title extents, in map pixel space, disjoint),
|
|
15155
|
+
// and a chart is always in a band above every smaller chart it
|
|
15156
|
+
// overlaps. Each band then draws its charts' complete group
|
|
15157
|
+
// (box, title, glow, symbols, values), the smallest band first
|
|
15158
|
+
// — deck.gl draws the first layer at the bottom — so between
|
|
15159
|
+
// two overlapping charts the bigger one is wholly on top, as
|
|
15160
|
+
// flat draws it. Overlap-free themes keep one band and today's
|
|
15161
|
+
// exact single layers
|
|
15162
|
+
const world = 512 * Math.pow(2, liveZoom);
|
|
15163
|
+
const anchorPx = d => {
|
|
15164
|
+
const o = alignOf(d), c = d.geometry.coordinates;
|
|
15165
|
+
const lat = c[1] * Math.PI / 180;
|
|
15166
|
+
return [(c[0] + 180) / 360 * world + o[0],
|
|
15167
|
+
(0.5 - Math.log(Math.tan(Math.PI / 4 + lat / 2)) / (2 * Math.PI)) * world + o[1]];
|
|
15168
|
+
};
|
|
15169
|
+
const sizeOfBand = b => circular ? b.boxR
|
|
15170
|
+
: b.rect ? Math.max(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1]) / 2 : 0;
|
|
15171
|
+
const bandRects = [];
|
|
15172
|
+
for (const b of boxLayoutAll.slice().sort((a, c) => sizeOfBand(c) - sizeOfBand(a))) {
|
|
15173
|
+
// the chart's whole drawn extent: parts cluster, box and
|
|
15174
|
+
// title, as px around the (align-moved) anchor
|
|
15175
|
+
const e = b.d._seqExtent || [0, 0, 0, 0];
|
|
15176
|
+
let x0 = e[0], y0 = e[1], x1 = e[2], y1 = e[3];
|
|
15177
|
+
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); }
|
|
15178
|
+
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]); }
|
|
15179
|
+
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]); }
|
|
15180
|
+
const p = anchorPx(b.d);
|
|
15181
|
+
const r = [p[0] + x0, p[1] + y0, p[0] + x1, p[1] + y1];
|
|
15182
|
+
let k = 0;
|
|
15183
|
+
while (bandRects[k] && bandRects[k].some(q => r[0] < q[2] && q[0] < r[2] && r[1] < q[3] && q[1] < r[3])) k++;
|
|
15184
|
+
(bandRects[k] = bandRects[k] || []).push(r);
|
|
15185
|
+
bandOfChart.set(b.d, k);
|
|
15186
|
+
}
|
|
15187
|
+
nBands = bandRects.length;
|
|
15188
|
+
}
|
|
15189
|
+
// one band's whole chart group — box, title, glow, symbols and
|
|
15190
|
+
// values of ITS charts only (the closure's boxLayout, glows and
|
|
15191
|
+
// parts are the band's slices) — pushed as one deck.gl layer
|
|
15192
|
+
// sequence, in today's order. bs: the band's id suffix
|
|
15193
|
+
const drawBand = (boxLayout, glows, parts, bs) => {
|
|
15194
|
+
if (boxLayout) {
|
|
15195
|
+
const L = this._boxTitleLayers(boxLayout, {
|
|
15196
|
+
unit, liveZoom,
|
|
15197
|
+
positionOf: b => b.d.geometry.coordinates, offsetOf: b => alignOf(b.d),
|
|
15198
|
+
ids: { circle: `ix-seqbox-${this.name}${bs}`, icon: `ix-seqbox-${this.name}${bs}`,
|
|
15199
|
+
polygons: `ix-seqbox-polygons-${this.name}${bs}`, titles: `ix-seqtitles-${this.name}${bs}` }
|
|
15200
|
+
});
|
|
15201
|
+
layers.push(...L.box, ...L.titles);
|
|
15202
|
+
}
|
|
15203
|
+
if (glows.length && this.flags.has('GLOW') && !glowHiddenByScale(st, liveZoom)) {
|
|
15204
|
+
layers.push(new IconLayer({
|
|
15205
|
+
id: `ix-glow-${this.name}${bs}-g${this._iconGeneration}`,
|
|
15206
|
+
data: glows, pickable: false,
|
|
15207
|
+
getPosition: d => d.geometry.coordinates,
|
|
15208
|
+
getIcon: d => this._getGlowIcon(colorOf(d.part.i)),
|
|
15209
|
+
getSize: d => d.part.r * 11,
|
|
15210
|
+
getPixelOffset: offsetOf,
|
|
15211
|
+
getColor: d => [255, 255, 255, alphaOf(d)],
|
|
15212
|
+
sizeUnits: 'pixels', billboard: true,
|
|
15213
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
15214
|
+
}));
|
|
15215
|
+
} else if (glows.length && this.flags.has('AURA') && !glowHiddenByScale(st, liveZoom)) {
|
|
15216
|
+
layers.push(new IconLayer({
|
|
15217
|
+
id: `ix-aura-${this.name}${bs}-g${this._iconGeneration}`,
|
|
15218
|
+
data: glows, pickable: false,
|
|
15219
|
+
getPosition: d => d.geometry.coordinates,
|
|
15220
|
+
getIcon: d => this._buildSingleIcon(borderRgb || colorOf(d.part.i), 0.3, 'circle', null, 0),
|
|
15221
|
+
getSize: d => d.part.r * 2 * 1.3,
|
|
15222
|
+
getPixelOffset: offsetOf,
|
|
15223
|
+
getColor: d => [255, 255, 255, alphaOf(d)],
|
|
15224
|
+
sizeUnits: 'pixels', billboard: true,
|
|
15225
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
15226
|
+
}));
|
|
15227
|
+
}
|
|
13795
15228
|
layers.push(new IconLayer({
|
|
13796
|
-
id: `ix-
|
|
13797
|
-
data:
|
|
15229
|
+
id: `ix-bubbles-${this.name}${bs}-g${this._iconGeneration}`,
|
|
15230
|
+
data: parts, pickable: true,
|
|
13798
15231
|
getPosition: d => d.geometry.coordinates,
|
|
13799
|
-
|
|
13800
|
-
|
|
15232
|
+
// the stroke, in the icon raster's pixels (the icon is drawn at 2r)
|
|
15233
|
+
getIcon: d => {
|
|
15234
|
+
// an external symbol file: the URL icon as flat's <image> (see
|
|
15235
|
+
// urlSymbolOf above), scaled to the part's diameter like flat's
|
|
15236
|
+
// fu.scale(nRadius / nMaxRadius) on a nChartSize image
|
|
15237
|
+
const u = urlSymbolOf(d.part.i);
|
|
15238
|
+
if (u) return urlIcon(u);
|
|
15239
|
+
return this._buildSingleIcon(colorOf(d.part.i), noFill.has(d.part.i) ? 0 : fillOpacity, shapeOf(d.part.i), borderRgb,
|
|
15240
|
+
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);
|
|
15241
|
+
},
|
|
15242
|
+
getSize: d => d.part.r * 2,
|
|
13801
15243
|
getPixelOffset: offsetOf,
|
|
13802
15244
|
getColor: d => [255, 255, 255, alphaOf(d)],
|
|
13803
15245
|
sizeUnits: 'pixels', billboard: true,
|
|
13804
15246
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
13805
15247
|
}));
|
|
13806
|
-
|
|
13807
|
-
|
|
13808
|
-
|
|
13809
|
-
|
|
13810
|
-
|
|
13811
|
-
//
|
|
13812
|
-
|
|
13813
|
-
|
|
13814
|
-
|
|
13815
|
-
|
|
13816
|
-
|
|
13817
|
-
|
|
13818
|
-
|
|
13819
|
-
|
|
13820
|
-
|
|
13821
|
-
|
|
13822
|
-
|
|
13823
|
-
|
|
13824
|
-
|
|
13825
|
-
|
|
13826
|
-
|
|
13827
|
-
|
|
13828
|
-
|
|
13829
|
-
|
|
13830
|
-
|
|
13831
|
-
|
|
13832
|
-
|
|
13833
|
-
|
|
13834
|
-
|
|
13835
|
-
|
|
13836
|
-
|
|
13837
|
-
|
|
13838
|
-
|
|
13839
|
-
|
|
13840
|
-
|
|
13841
|
-
|
|
13842
|
-
|
|
13843
|
-
|
|
13844
|
-
|
|
13845
|
-
|
|
13846
|
-
|
|
13847
|
-
|
|
13848
|
-
|
|
13849
|
-
|
|
15248
|
+
// VALUES on each part (maptheme.js 22188-22260): the part's value — with
|
|
15249
|
+
// COUNT its record count, with valuefield on the categorical field the
|
|
15250
|
+
// class name — formatted with valuedecimals (else none) plus the unit.
|
|
15251
|
+
// - a SHAPE part (circle & kin): font min(1, 3.2 / L) · r · valuescale,
|
|
15252
|
+
// L the length of the theme's biggest value text (szMaxText), normal
|
|
15253
|
+
// weight, in valuecolor / textcolor, else ChartColors.textColor of
|
|
15254
|
+
// the part color
|
|
15255
|
+
// - an IMAGE-symbol part (an external .svg, the TELRAAM pictograms):
|
|
15256
|
+
// flat's own other branch (22253-22256) — font max(r · 0.8, 1) ·
|
|
15257
|
+
// valuescale, the text over createTextLabel's background box (white,
|
|
15258
|
+
// its rgba(…,0.3) fill × the 0.5 fill-opacity) and in
|
|
15259
|
+
// ChartColors.lowColor — the 0.7 derivate of the part color —
|
|
15260
|
+
// regardless of valuecolor/textcolor (flat passes cColor.lowColor
|
|
15261
|
+
// straight into createTextLabel)
|
|
15262
|
+
// CENTERVALUES: only on the first part (class 0), its own value
|
|
15263
|
+
if ((this.flags.has('VALUES') || centerValues) && !valuesHiddenByScale(st, zoom)) {
|
|
15264
|
+
const valueScale = styleNum(st.valuescale) || 1;
|
|
15265
|
+
const { noBreaks } = this._valueTextOpts();
|
|
15266
|
+
const units = st.units ? String(st.units) : '';
|
|
15267
|
+
const unit = units ? (units[0] === '.' ? '' : ' ') + units : '';
|
|
15268
|
+
const unitText = unit.length <= 5 ? unit : '';
|
|
15269
|
+
const dec = explicitValueDecimals(st) || 0;
|
|
15270
|
+
const fmt = v => flatGroupedValue(v, dec, noBreaks) + unitText;
|
|
15271
|
+
const maxLen = fmt(seqMax).length;
|
|
15272
|
+
const textOverride = st.valuecolor || st.textcolor;
|
|
15273
|
+
const counting = flatFlag(this.flags, 'COUNT');
|
|
15274
|
+
const labels = [];
|
|
15275
|
+
parts.forEach(d => {
|
|
15276
|
+
if (centerValues && d.part.i !== 0) return;
|
|
15277
|
+
const rec = this._categoryValueRecord(d.part.i);
|
|
15278
|
+
const name = rec ? String(rec[st.valuefield]) : null;
|
|
15279
|
+
const t = counting && d.properties.recordCounts ? d.properties.recordCounts[d.part.i] : d.part.v;
|
|
15280
|
+
const text = name != null ? name : fmt(t);
|
|
15281
|
+
const image = !!urlSymbolOf(d.part.i);
|
|
15282
|
+
const L = Math.max((name != null ? name.length : 0) + 1, maxLen);
|
|
15283
|
+
const fontSize = image
|
|
15284
|
+
? Math.max(d.part.r * 0.8, 1) * valueScale
|
|
15285
|
+
: Math.min(1, 3.2 / L) * d.part.r * valueScale;
|
|
15286
|
+
if (fontSize > VALUES_MIN_FONT_PX) {
|
|
15287
|
+
labels.push({ geometry: d.geometry, text, fontSize, pixelOffset: offsetOf(d), alpha: alphaOf(d),
|
|
15288
|
+
image,
|
|
15289
|
+
color: image ? flatDerivateRgb(colorOf(d.part.i), 0.7)
|
|
15290
|
+
: textOverride ? hexOrNamedToRgb(textOverride) : flatChartTextRgb(colorOf(d.part.i)) });
|
|
15291
|
+
}
|
|
15292
|
+
});
|
|
15293
|
+
if (labels.length) {
|
|
15294
|
+
// VALUEBACKGROUND on URL-symbol parts: a fading white border
|
|
15295
|
+
// around the value's own CHARACTER SHAPES — the glyphs
|
|
15296
|
+
// re-drawn in white on rings of pixel offsets (a stroke of
|
|
15297
|
+
// copies, no box): the width is a fraction of the font size,
|
|
15298
|
+
// so the halo scales with every label; the shape-part values
|
|
15299
|
+
// keep flat's plain text (no outline)
|
|
15300
|
+
if (flatFlag(this.flags, 'VALUEBACKGROUND')) {
|
|
15301
|
+
const haloLabels = labels.filter(l => l.image);
|
|
15302
|
+
const plainLabels = labels.filter(l => !l.image);
|
|
15303
|
+
if (haloLabels.length) {
|
|
15304
|
+
// a real outline around the characters: the label's own
|
|
15305
|
+
// glyphs re-drawn in white on rings of pixel offsets — a
|
|
15306
|
+
// stroke of copies that hugs the character shapes (deck's
|
|
15307
|
+
// SDF outline is only a faint, buffer-clipped rim). The
|
|
15308
|
+
// base radius is a small fraction of the font size (the
|
|
15309
|
+
// 1px floor keeps it tiny until labels get big), and the
|
|
15310
|
+
// ring alphas fall off, so the border fades to nothing
|
|
15311
|
+
const DIRS = 16, RINGS = [[1, 210], [2, 120], [3.2, 65]];
|
|
15312
|
+
const haloCopies = [];
|
|
15313
|
+
for (const l of haloLabels) {
|
|
15314
|
+
const rb = Math.max(0.75, l.fontSize * 0.015);
|
|
15315
|
+
for (let ring = 0; ring < RINGS.length; ring++) {
|
|
15316
|
+
for (let dir = 0; dir < DIRS; dir++) {
|
|
15317
|
+
const a = (dir / DIRS) * 2 * Math.PI;
|
|
15318
|
+
haloCopies.push({ ...l,
|
|
15319
|
+
dx: Math.cos(a), dy: Math.sin(a),
|
|
15320
|
+
radius: rb * RINGS[ring][0],
|
|
15321
|
+
haloAlpha: Math.round(RINGS[ring][1] * l.alpha / 255) });
|
|
15322
|
+
}
|
|
15323
|
+
}
|
|
15324
|
+
}
|
|
15325
|
+
layers.push(new TextLayer({
|
|
15326
|
+
id: `ix-cluster-values-halo-${this.name}${bs}`,
|
|
15327
|
+
data: haloCopies, pickable: false,
|
|
15328
|
+
getPosition: d => d.geometry.coordinates,
|
|
15329
|
+
getText: d => d.text,
|
|
15330
|
+
getSize: d => d.fontSize,
|
|
15331
|
+
getColor: d => [255, 255, 255, d.haloAlpha],
|
|
15332
|
+
getPixelOffset: d => [d.pixelOffset[0] + d.dx * d.radius, d.pixelOffset[1] + d.dy * d.radius],
|
|
15333
|
+
sizeUnits: 'pixels', fontFamily: 'arial', fontWeight: 'normal',
|
|
15334
|
+
characterSet: 'auto',
|
|
15335
|
+
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
15336
|
+
}));
|
|
15337
|
+
layers.push(new TextLayer({
|
|
15338
|
+
id: `ix-cluster-values-${this.name}${bs}`,
|
|
15339
|
+
data: haloLabels, pickable: false,
|
|
15340
|
+
getPosition: d => d.geometry.coordinates,
|
|
15341
|
+
getText: d => d.text,
|
|
15342
|
+
getSize: d => d.fontSize,
|
|
15343
|
+
getColor: d => [...d.color.slice(0, 3), d.alpha],
|
|
15344
|
+
getPixelOffset: d => d.pixelOffset,
|
|
15345
|
+
sizeUnits: 'pixels', fontFamily: 'arial', fontWeight: 'normal',
|
|
15346
|
+
characterSet: 'auto',
|
|
15347
|
+
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
15348
|
+
}));
|
|
15349
|
+
}
|
|
15350
|
+
if (plainLabels.length) layers.push(new TextLayer({
|
|
15351
|
+
id: `ix-cluster-values-plain-${this.name}${bs}`,
|
|
15352
|
+
data: plainLabels, pickable: false,
|
|
15353
|
+
getPosition: d => d.geometry.coordinates,
|
|
15354
|
+
getText: d => d.text,
|
|
15355
|
+
getSize: d => d.fontSize,
|
|
15356
|
+
getColor: d => [...d.color.slice(0, 3), d.alpha],
|
|
15357
|
+
getPixelOffset: d => d.pixelOffset,
|
|
15358
|
+
sizeUnits: 'pixels', fontFamily: 'arial', fontWeight: 'normal',
|
|
15359
|
+
characterSet: 'auto',
|
|
15360
|
+
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
15361
|
+
}));
|
|
15362
|
+
} else {
|
|
15363
|
+
layers.push(new TextLayer({
|
|
15364
|
+
id: `ix-cluster-values-${this.name}${bs}`,
|
|
15365
|
+
data: labels, pickable: false,
|
|
15366
|
+
getPosition: d => d.geometry.coordinates,
|
|
15367
|
+
getText: d => d.text,
|
|
15368
|
+
getSize: d => d.fontSize,
|
|
15369
|
+
getColor: d => [...d.color.slice(0, 3), d.alpha],
|
|
15370
|
+
getPixelOffset: d => d.pixelOffset,
|
|
15371
|
+
sizeUnits: 'pixels', fontFamily: 'arial', fontWeight: 'normal',
|
|
15372
|
+
characterSet: 'auto',
|
|
15373
|
+
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
15374
|
+
}));
|
|
15375
|
+
}
|
|
13850
15376
|
}
|
|
13851
|
-
});
|
|
13852
|
-
if (labels.length) {
|
|
13853
|
-
layers.push(new TextLayer({
|
|
13854
|
-
id: `ix-cluster-values-${this.name}`,
|
|
13855
|
-
data: labels, pickable: false,
|
|
13856
|
-
getPosition: d => d.geometry.coordinates,
|
|
13857
|
-
getText: d => d.text,
|
|
13858
|
-
getSize: d => d.fontSize,
|
|
13859
|
-
getColor: d => [...d.color.slice(0, 3), d.alpha],
|
|
13860
|
-
getPixelOffset: d => d.pixelOffset,
|
|
13861
|
-
sizeUnits: 'pixels', fontFamily: 'arial', fontWeight: 'normal',
|
|
13862
|
-
characterSet: 'auto',
|
|
13863
|
-
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
13864
|
-
}));
|
|
13865
15377
|
}
|
|
15378
|
+
};
|
|
15379
|
+
const chartBand = c => bandOfChart.get(c) || 0;
|
|
15380
|
+
if (nBands > 1) {
|
|
15381
|
+
// deck.gl draws the FIRST layer at the bottom: the smallest
|
|
15382
|
+
// charts' band first, the biggest (band 0) last, on top
|
|
15383
|
+
for (let k = nBands - 1; k >= 0; k--) drawBand(
|
|
15384
|
+
boxLayoutAll ? boxLayoutAll.filter(b => chartBand(b.d) === k) : null,
|
|
15385
|
+
allGlows.filter(g => chartBand(g.chart) === k),
|
|
15386
|
+
allParts.filter(p => chartBand(p.chart) === k), '-b' + k);
|
|
15387
|
+
} else {
|
|
15388
|
+
drawBand(boxLayoutAll, allGlows, allParts, '');
|
|
13866
15389
|
}
|
|
13867
15390
|
return layers;
|
|
13868
15391
|
}
|
|
@@ -13922,8 +15445,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13922
15445
|
// CENTERVALUE the item's record count
|
|
13923
15446
|
// Not drawn here (a console note at type parsing, PIE_INERT_FLAGS,
|
|
13924
15447
|
// PIE_AGGREGATE_INERT_FLAGS):
|
|
13925
|
-
// 3D, VOLUME, HEIGHT,
|
|
13926
|
-
//
|
|
15448
|
+
// 3D, VOLUME, HEIGHT, BOW, ZOOM, DIRECTION / DIRECTED — the pie is
|
|
15449
|
+
// drawn flat and plain instead; POLAR without STARBURST has no
|
|
15450
|
+
// effect in flat either. STARBURST (with the FLOWER/RAYS angle/gap
|
|
15451
|
+
// variants) and its POLAR circles ARE drawn (pieSliceLayout). Nor
|
|
13927
15452
|
// style.clipparts, showparts, gridx (flat's several pies per item).
|
|
13928
15453
|
// Known divergence: flat draws a chart's value labels in the chart's
|
|
13929
15454
|
// own group, so a bigger pie drawn later covers a smaller neighbour's
|
|
@@ -13987,6 +15512,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13987
15512
|
const lineRgb = line === 'none' ? null : hexOrNamedToRgb(line);
|
|
13988
15513
|
const lineWidthOf = r => (lineRgb ? (styleNum(st.linewidth) || 0.1) * r / 15 : 0);
|
|
13989
15514
|
const lineRgba = lineRgb ? [...lineRgb.slice(0, 3), Math.round(255 * lineAlpha)] : [0, 0, 0, 0];
|
|
15515
|
+
// flat's STARBURST (maptheme.js 20262-20275, piechart.js 281-287,
|
|
15516
|
+
// 415-420): the parts get equal angles and a ray's radius carries
|
|
15517
|
+
// its value — inner + span · sqrt(v / maxValue) · scale, scale =
|
|
15518
|
+
// maxValue / (normalsizevalue || maxValue) · 2.14 (with SIZE flat
|
|
15519
|
+
// sets only maxValue, so a part at the reference value reaches
|
|
15520
|
+
// 2.14 R). maxValue: style.maxvalue (the reference — the ISTAT
|
|
15521
|
+
// story's national mean), else the biggest part value over the
|
|
15522
|
+
// items (flat's max of the per-part maxima nMaxA). The star's
|
|
15523
|
+
// inner px (the ray variants' 5px) comes off the layout
|
|
15524
|
+
const star = has(/STARBURST/);
|
|
15525
|
+
let starMaxPart = 0;
|
|
13990
15526
|
|
|
13991
15527
|
const charts = [];
|
|
13992
15528
|
if (agg) {
|
|
@@ -14001,6 +15537,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14001
15537
|
if (!layout) continue;
|
|
14002
15538
|
if (cat) layout.slices.forEach(sl => { sl.single = values.length === 1; });
|
|
14003
15539
|
const sum = values.reduce((a, v) => a + (isNaN(v) ? 0 : v), 0);
|
|
15540
|
+
if (star) for (const v of values) if (v > starMaxPart) starMaxPart = v;
|
|
14004
15541
|
const r = nSizeOf(sum, size) * unit;
|
|
14005
15542
|
if (!(r > 0) || !isFinite(r)) continue;
|
|
14006
15543
|
// flat's draw-order key (maptheme.js 16812-16860): with a size
|
|
@@ -14025,6 +15562,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14025
15562
|
const layout = pieSliceLayout(values, t, { centerPart: st.centerpart });
|
|
14026
15563
|
if (!layout) continue;
|
|
14027
15564
|
const sum = values.reduce((a, v) => a + v, 0);
|
|
15565
|
+
if (star) for (const v of values) if (v > starMaxPart) starMaxPart = v;
|
|
14028
15566
|
const size = this.binding.size ? parseFloat(raw[this.binding.size]) : undefined;
|
|
14029
15567
|
const r = nSizeOf(sum, size) * unit;
|
|
14030
15568
|
if (!(r > 0) || !isFinite(r)) continue;
|
|
@@ -14057,14 +15595,80 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14057
15595
|
});
|
|
14058
15596
|
|
|
14059
15597
|
const TAU = 2 * Math.PI;
|
|
14060
|
-
const slices = [], centers = [], glows = [];
|
|
15598
|
+
const slices = [], centers = [], glows = [], polar = [], polarDashes = [];
|
|
14061
15599
|
const glowOn = has(/GLOW/) && !has(/ZOOM/) && !glowHiddenByScale(st, liveZoom);
|
|
15600
|
+
// the STARBURST ray scale (see the star comment above): style
|
|
15601
|
+
// maxvalue, else the biggest part value over the items — flat's
|
|
15602
|
+
// `this.nMaxValue || max(nMaxA)`. Without SIZE flat also sets the
|
|
15603
|
+
// donut's normalSizeValue (normalsizevalue, else maxValue), which
|
|
15604
|
+
// dampens the scale
|
|
15605
|
+
const starMax = styleNum(st.maxvalue);
|
|
15606
|
+
const starRef = !isNaN(starMax) && starMax > 0 ? starMax : (starMaxPart > 0 ? starMaxPart : 1);
|
|
15607
|
+
const starNsv = has(/\bSIZE\b/) ? starRef : (nsv || starRef);
|
|
15608
|
+
const starScale = starRef / starNsv * 2.14;
|
|
15609
|
+
// a chart's rays start at the CENTER — flat's `max(5, nRadInner)`
|
|
15610
|
+
// for the ray variants is 5 units of the chart's internal
|
|
15611
|
+
// coordinate system, sub-pixel and invisible on screen (see
|
|
15612
|
+
// pieSliceLayout) — and only with DONUT at the donut's inner
|
|
15613
|
+
// radius (flat piechart.js 326-327: nRadOuter += nRadInner, the
|
|
15614
|
+
// span stays R)
|
|
15615
|
+
const starInnerOf = ch => (ch.layout.inner || 0) * ch.r;
|
|
15616
|
+
const starRadius = (v, R, inner) => Math.max(1, inner + (R - inner) * Math.sqrt(Math.max(0, v) / starRef) * starScale);
|
|
14062
15617
|
for (const ch of charts) {
|
|
15618
|
+
const starInner = star ? starInnerOf(ch) : 0;
|
|
15619
|
+
if (star && has(/POLAR/)) {
|
|
15620
|
+
// flat piechart.js 435-452: the STARBURST's reference circles —
|
|
15621
|
+
// a white disc at the reference value (fill-opacity 0.25,
|
|
15622
|
+
// #444444 border, solid) and range rings at 75 / 50 / 25 % of
|
|
15623
|
+
// it, the 75% and 25% rings DASHED (stroke-dasharray 50 50 in
|
|
15624
|
+
// the chart group's units, on for half of each cycle). All of
|
|
15625
|
+
// them under every ray. The stroke widths are flat's 1px /
|
|
15626
|
+
// 3px — in the CHART GROUP's coordinate system, whose scale
|
|
15627
|
+
// is ~0.017 of the screen, so on screen they are sub-pixel
|
|
15628
|
+
// hairlines (measured on the ISTAT story: the disc's 197
|
|
15629
|
+
// group-units radius is 11.2px on screen); 1px ≈ 1.1% and
|
|
15630
|
+
// 3px ≈ 3.2% of the base radius R, taken here as fractions
|
|
15631
|
+
// of ch.r so they stay hairlines at every zoom, as in flat
|
|
15632
|
+
const lw1 = ch.r * 0.0108, lw3 = ch.r * 0.0325;
|
|
15633
|
+
const w = ch.r - starInner;
|
|
15634
|
+
polar.push({ position: ch.c, r: starInner + w * starScale, off: ch.off,
|
|
15635
|
+
fill: [255, 255, 255, 64], line: [68, 68, 68, 255], lw: lw1 });
|
|
15636
|
+
polar.push({ position: ch.c, r: starInner + w * Math.sqrt(0.5) * starScale, off: ch.off,
|
|
15637
|
+
fill: [0, 0, 0, 0], line: [68, 68, 68, 255], lw: lw1 });
|
|
15638
|
+
// the dashed rings: one chord per dash — flat's own arcs are
|
|
15639
|
+
// octant polylines, so straight chords are as faithful. The
|
|
15640
|
+
// dash count is flat's geometry: a 50-unit dash on the ring's
|
|
15641
|
+
// group radius — the ring in group units is ~17.7 × its
|
|
15642
|
+
// screen radius (the chart group scale measured on the
|
|
15643
|
+
// story), so a cycle is 100 group units = 100/(17.7 · 2π ·
|
|
15644
|
+
// ringR_screen) of the circle: ~11 dashes around the 75%,
|
|
15645
|
+
// ~6 around the 25% ring, at every zoom
|
|
15646
|
+
for (const q of [0.75, 0.25]) {
|
|
15647
|
+
const rr = starInner + w * Math.sqrt(q) * starScale;
|
|
15648
|
+
const cycles = Math.max(q > 0.5 ? 4 : 3, Math.round((q > 0.5 ? 2.06 : 1.19) * ch.r));
|
|
15649
|
+
const seg = 2 * Math.PI / cycles;
|
|
15650
|
+
for (let i = 0; i < cycles; i++) {
|
|
15651
|
+
const a0 = i * seg, a1 = a0 + seg / 2;
|
|
15652
|
+
// 0 at 12 o'clock, clockwise (pieSliceLayout's convention);
|
|
15653
|
+
// pixelOffsetLngLat's y grows down
|
|
15654
|
+
polarDashes.push({
|
|
15655
|
+
path: [
|
|
15656
|
+
pixelOffsetLngLat(ch.c, [Math.sin(a0) * rr, -Math.cos(a0) * rr], liveZoom),
|
|
15657
|
+
pixelOffsetLngLat(ch.c, [Math.sin(a1) * rr, -Math.cos(a1) * rr], liveZoom)
|
|
15658
|
+
], lw: lw3
|
|
15659
|
+
});
|
|
15660
|
+
}
|
|
15661
|
+
}
|
|
15662
|
+
}
|
|
14063
15663
|
for (const sl of ch.layout.slices) {
|
|
14064
15664
|
const rgb = sliceRgb(sl);
|
|
15665
|
+
// STARBURST: this ray's own radius (the value), its inner the
|
|
15666
|
+
// px starInner as a fraction of that radius — the shader takes
|
|
15667
|
+
// a fraction of the instance radius, which varies per ray here
|
|
15668
|
+
const rayR = star ? starRadius(sl.value, ch.r, starInner) : ch.r;
|
|
14065
15669
|
slices.push({
|
|
14066
|
-
position: ch.c, r:
|
|
14067
|
-
slice: [sl.start * Math.PI / 180, sl.sweep >= 360 ? TAU : sl.sweep * Math.PI / 180, ch.layout.inner],
|
|
15670
|
+
position: ch.c, r: rayR, off: ch.off, properties: ch.properties, chart: ch, part: sl,
|
|
15671
|
+
slice: [sl.start * Math.PI / 180, sl.sweep >= 360 ? TAU : sl.sweep * Math.PI / 180, star ? starInner / rayR : ch.layout.inner],
|
|
14068
15672
|
fill: [...rgb.slice(0, 3), !sl.complement && noFill.has(sl.i) ? 0 : Math.round(255 * fillAlpha)]
|
|
14069
15673
|
});
|
|
14070
15674
|
}
|
|
@@ -14088,13 +15692,80 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14088
15692
|
radiusUnits: 'pixels', billboard: true, stroked: false, filled: true, antialiasing: true,
|
|
14089
15693
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14090
15694
|
};
|
|
14091
|
-
|
|
15695
|
+
// flat's BOX / TITLE for a pie (maptheme.js 18446-18925, the same
|
|
15696
|
+
// block the sequence builder draws): a rectangle around the
|
|
15697
|
+
// chart's drawn extent — on a STARBURST the longest ray, the
|
|
15698
|
+
// disc otherwise — plus, with TITLE, the item's title (an
|
|
15699
|
+
// aggregated grid cell of more than 3 records without its own:
|
|
15700
|
+
// "(n)", see _itemTitle) at the chart's left edge, wrapped to
|
|
15701
|
+
// the chart's width, in textcolor (#888888) over a half-opaque
|
|
15702
|
+
// white halo, its first baseline 0.7 font above the box — the
|
|
15703
|
+
// box grows to enclose it, its top trimmed by 0.1 font. The
|
|
15704
|
+
// box: white at boxopacity, #bbbbbb border at 0.3 opacity
|
|
15705
|
+
// (borderstyle solid/dotted/dashed: 1, none: no border),
|
|
15706
|
+
// borderwidth · 0.2 · width/chartSize px wide. Both are removed
|
|
15707
|
+
// outside boxupper / titleupper (maptheme.js 18758-18760, the
|
|
15708
|
+
// TITLE gate 18809-18811)
|
|
15709
|
+
// the chart's drawn extent, px around the anchor (flat's getBox of the
|
|
15710
|
+
// shape group — the longest ray on a STARBURST)
|
|
15711
|
+
const boxRows = this._boxRows(charts, { unit, liveZoom, extentOf: ch => {
|
|
15712
|
+
let e = ch.r;
|
|
15713
|
+
if (star) { const in0 = starInnerOf(ch); for (const sl of ch.layout.slices) e = Math.max(e, starRadius(sl.value, ch.r, in0)); }
|
|
15714
|
+
return [-e, -e, e, e];
|
|
15715
|
+
} });
|
|
15716
|
+
const layers = [];
|
|
15717
|
+
if (boxRows && boxRows.length) {
|
|
15718
|
+
const L = this._boxTitleLayers(boxRows, {
|
|
15719
|
+
unit, liveZoom,
|
|
15720
|
+
positionOf: b => b.d.c, offsetOf: b => b.d.off,
|
|
15721
|
+
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}` }
|
|
15722
|
+
});
|
|
15723
|
+
layers.push(...L.box, ...L.titles);
|
|
15724
|
+
}
|
|
15725
|
+
if (polar.length) {
|
|
15726
|
+
layers.push(new ScatterplotLayer({
|
|
15727
|
+
id: `ix-pie-polar-${this.name}`,
|
|
15728
|
+
data: polar, pickable: false,
|
|
15729
|
+
getPosition: d => d.position,
|
|
15730
|
+
getRadius: d => d.r,
|
|
15731
|
+
getPixelOffset: d => d.off,
|
|
15732
|
+
radiusUnits: 'pixels', billboard: true,
|
|
15733
|
+
filled: true, stroked: true,
|
|
15734
|
+
getFillColor: d => d.fill,
|
|
15735
|
+
getLineColor: d => d.line,
|
|
15736
|
+
getLineWidth: d => d.lw,
|
|
15737
|
+
lineWidthUnits: 'pixels',
|
|
15738
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
15739
|
+
}));
|
|
15740
|
+
// flat's dashed range rings (see the polar block): one chord per
|
|
15741
|
+
// dash, drawn in map coordinates at the current zoom — the
|
|
15742
|
+
// chart anchors move on zoom, and the layers rebuild with them
|
|
15743
|
+
if (polarDashes.length) {
|
|
15744
|
+
layers.push(new PathLayer({
|
|
15745
|
+
id: `ix-pie-polar-dashes-${this.name}`,
|
|
15746
|
+
data: polarDashes, pickable: false,
|
|
15747
|
+
getPath: d => d.path,
|
|
15748
|
+
getColor: [68, 68, 68, 255],
|
|
15749
|
+
getWidth: d => d.lw,
|
|
15750
|
+
widthUnits: 'pixels',
|
|
15751
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
15752
|
+
}));
|
|
15753
|
+
}
|
|
15754
|
+
}
|
|
15755
|
+
layers.push(new PieSliceLayer(Object.assign({}, discProps, {
|
|
14092
15756
|
id: `ix-pie-${this.name}`,
|
|
14093
15757
|
data: slices, pickable: true,
|
|
14094
15758
|
getLineColor: lineRgba,
|
|
14095
15759
|
getSlice: d => d.slice,
|
|
14096
|
-
|
|
14097
|
-
|
|
15760
|
+
// flat's pie outline (piechart.js: normalX(linewidth) · R / 300)
|
|
15761
|
+
// is 2% of the radius — sub-pixel for every small chart, and the
|
|
15762
|
+
// browser rasterizes it as a HAIRLINE: one device pixel at full
|
|
15763
|
+
// opacity (Skia's stroke floor). The 1 device-px floor here
|
|
15764
|
+
// matches that: the width is in the shader's device-pixel
|
|
15765
|
+
// space (pieAA = fwidth), so the CSS width is scaled by the
|
|
15766
|
+
// device pixel ratio first
|
|
15767
|
+
getSliceLineWidth: d => (lineWidthOf(d.r) > 0 ? Math.max(lineWidthOf(d.r) * (global.devicePixelRatio || 1), 1) : 0)
|
|
15768
|
+
})));
|
|
14098
15769
|
if (centers.length) {
|
|
14099
15770
|
layers.push(new PieSliceLayer(Object.assign({}, discProps, { id: `ix-pie-center-${this.name}`, data: centers, pickable: true })));
|
|
14100
15771
|
}
|
|
@@ -14308,7 +15979,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14308
15979
|
// STACKED beside the column on leader lines; flat's values color
|
|
14309
15980
|
// #798697 (textcolor), negative red (valuecolor); DTEXT and a sized
|
|
14310
15981
|
// POINTER fade small values
|
|
14311
|
-
// - BOX / TITLE (
|
|
15982
|
+
// - BOX / TITLE (chartBoxLayout): a box around chart and title, the
|
|
14312
15983
|
// item's title above it — boxcolor (white) at boxopacity, bordercolor
|
|
14313
15984
|
// (#bbbbbb) at 0.3, borderwidth · 0.2 · w / 30; title 5 u ·
|
|
14314
15985
|
// textscale in textcolor (#888888) over a white halo
|
|
@@ -14404,13 +16075,15 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14404
16075
|
}
|
|
14405
16076
|
applyFlatMaxCharts(charts, st);
|
|
14406
16077
|
|
|
14407
|
-
|
|
14408
|
-
|
|
14409
|
-
|
|
16078
|
+
// a BAR draws its box as a shader shape (rectangle only, no CIRCULARBOX):
|
|
16079
|
+
// the gates, margin, title font and placement are the box's, shared with
|
|
16080
|
+
// every chart type (_boxSpec / chartBoxLayout, here in chart units)
|
|
16081
|
+
const boxSpec = this._boxSpec(liveZoom);
|
|
16082
|
+
const boxUnits = st.titleunits ? ' ' + st.titleunits : '';
|
|
14410
16083
|
const boxOpacity = isNaN(styleNum(st.boxopacity)) ? 1 : styleNum(st.boxopacity);
|
|
14411
16084
|
const borderStyle = String(st.borderstyle || '');
|
|
14412
|
-
const boxFill = [...hexOrNamedToRgb(st.boxcolor || 'white').slice(0, 3), Math.round(255 * boxOpacity)];
|
|
14413
|
-
const boxLine = borderStyle === 'none' ? [0, 0, 0, 0]
|
|
16085
|
+
const boxFill = [...hexOrNamedToRgb(isNoneColor(st.boxcolor) ? 'white' : st.boxcolor || 'white').slice(0, 3), isNoneColor(st.boxcolor) ? 0 : Math.round(255 * boxOpacity)];
|
|
16086
|
+
const boxLine = borderStyle === 'none' || isNoneColor(styleLineColor(st.bordercolor)) ? [0, 0, 0, 0]
|
|
14414
16087
|
: [...hexOrNamedToRgb(styleLineColor(st.bordercolor) || '#bbbbbb').slice(0, 3), Math.round(255 * (/^(solid|dotted|dashed)$/.test(borderStyle) ? 1 : 0.3))];
|
|
14415
16088
|
const borderWidth = num(st.borderwidth) || 1;
|
|
14416
16089
|
const lc = st.linecolor != null && st.linecolor !== '' ? styleLineColor(st.linecolor) : null;
|
|
@@ -14435,10 +16108,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14435
16108
|
for (const ch of charts) {
|
|
14436
16109
|
const L = ch.layout;
|
|
14437
16110
|
let box = null;
|
|
14438
|
-
if (
|
|
14439
|
-
const title = titleShown ? this._itemTitle(ch.properties) : '';
|
|
14440
|
-
box =
|
|
14441
|
-
|
|
16111
|
+
if (boxSpec) {
|
|
16112
|
+
const title = boxSpec.titleShown ? this._itemTitle(ch.properties) : '';
|
|
16113
|
+
box = chartBoxLayout(L.extent, { unit: 1, margin: boxSpec.margin, titleText: title ? title + boxUnits : '',
|
|
16114
|
+
titleFont: w => boxSpec.titleFontOf(1, w), bottomTitle: boxSpec.bottomTitle, alignRight: boxSpec.alignRight, measure });
|
|
14442
16115
|
}
|
|
14443
16116
|
const ext = box ? box.rect : L.extent;
|
|
14444
16117
|
// align / offsetx / offsety (flatChartAlignOffset), the chart's
|
|
@@ -14447,9 +16120,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14447
16120
|
if (box) {
|
|
14448
16121
|
const r = box.rect;
|
|
14449
16122
|
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 },
|
|
14450
|
-
boxFill, boxLine, borderWidth * 0.2 * box.
|
|
14451
|
-
|
|
14452
|
-
|
|
16123
|
+
boxFill, boxLine, borderWidth * 0.2 * box.w / 30, null, true);
|
|
16124
|
+
// the title in px, rendered by the shared title layer
|
|
16125
|
+
if (box.title) titles.push({ d: ch, title: { text: box.title.text, x: box.title.x * unit, anchor: box.title.anchor,
|
|
16126
|
+
top: box.title.top * unit, f: box.title.f * unit } });
|
|
14453
16127
|
}
|
|
14454
16128
|
for (const b of L.bars) {
|
|
14455
16129
|
const rgb = this.classRgb(b.cls);
|
|
@@ -14517,18 +16191,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14517
16191
|
backgroundPadding: [0.3, 0, 0.4, 0.1].map(k => k * boxed.reduce((a, d) => a + d.font, 0) / boxed.length)
|
|
14518
16192
|
})));
|
|
14519
16193
|
}
|
|
14520
|
-
if (titles.length) {
|
|
14521
|
-
|
|
14522
|
-
|
|
14523
|
-
|
|
14524
|
-
sizeUnits: 'pixels', fontFamily: 'arial', characterSet: 'auto', lineHeight: 1,
|
|
14525
|
-
getColor: [...hexOrNamedToRgb(st.textcolor || '#888888').slice(0, 3), 255],
|
|
14526
|
-
fontSettings: { sdf: true }, outlineWidth: 1 / 7, outlineColor: [255, 255, 255, 128],
|
|
14527
|
-
getTextAnchor: d => d.anchor, getAlignmentBaseline: 'top',
|
|
14528
|
-
getPixelOffset: d => d.off,
|
|
14529
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14530
|
-
}));
|
|
14531
|
-
}
|
|
16194
|
+
if (titles.length) layers.push(...this._boxTitleLayers(titles, {
|
|
16195
|
+
box: false, unit, liveZoom, positionOf: b => b.d.c, offsetOf: b => b.d.off,
|
|
16196
|
+
ids: { titles: `ix-bar-titles-${this.name}` }
|
|
16197
|
+
}).titles);
|
|
14532
16198
|
return layers;
|
|
14533
16199
|
}
|
|
14534
16200
|
|
|
@@ -14715,22 +16381,28 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14715
16381
|
return [(lng + 180) / 360 * world, (0.5 - Math.log((1 + s) / (1 - s)) / (4 * Math.PI)) * world];
|
|
14716
16382
|
};
|
|
14717
16383
|
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];
|
|
14718
|
-
// the curves are computed in the plane
|
|
14719
|
-
//
|
|
14720
|
-
//
|
|
14721
|
-
//
|
|
14722
|
-
//
|
|
16384
|
+
// the curves are computed in the plane the map is DISPLAYED in: Web
|
|
16385
|
+
// Mercator under MapLibre's mercator projection; on the globe (flat's
|
|
16386
|
+
// orthographic map) the orthographic plane of the view center, for
|
|
16387
|
+
// the flows whose two ends face the viewer (the others keep
|
|
16388
|
+
// Mercator's). flat's own Equal Earth / Winkel plane (e1cb264) is NOT
|
|
16389
|
+
// used: this engine displays those maps in Web Mercator too (a flat
|
|
16390
|
+
// SVG map in another projection is reported "not available — mercator
|
|
16391
|
+
// used"), and a curve bowed in the Equal Earth plane swings far
|
|
16392
|
+
// toward the pole once Mercator renders it — measured on
|
|
16393
|
+
// mechanical_goods_flows.html: Japan→USA at 82°N, its bow almost half
|
|
16394
|
+
// the chord, where the Mercator curve stays at 63°N — the "deviated
|
|
16395
|
+
// near the north pole" vectors. Flat bows its curves in the plane of
|
|
16396
|
+
// ITS map; here the plane of the display.
|
|
14723
16397
|
const planeFns = (proj, k) => ({
|
|
14724
16398
|
toPx: ([lng, lat]) => { const q = proj.project(lat, lng); return [q[0] * k, -q[1] * k]; },
|
|
14725
16399
|
toLngLat: ([x, y]) => { const q = proj.invert(x / k, -y / k); return [q[1], q[0]]; }
|
|
14726
16400
|
});
|
|
14727
16401
|
const mercFns = { toPx: mercToPx, toLngLat: mercToLngLat };
|
|
14728
|
-
const fixedPlane = flatPlaneProjectionByOptions.get(this.mapOptions);
|
|
14729
|
-
const fixedFns = fixedPlane && fixedPlane.invert ? planeFns(fixedPlane, (world / 360) / fixedPlane.project(0, 1)[0]) : null;
|
|
14730
16402
|
const planeCenter = flatViewCenterByOptions.get(this.mapOptions) || globeCenter;
|
|
14731
|
-
const ortho =
|
|
16403
|
+
const ortho = globeCenter ? orthographicPlane(planeCenter.lat, planeCenter.lng) : null;
|
|
14732
16404
|
const orthoFns = ortho ? planeFns(ortho, world / (2 * Math.PI)) : null;
|
|
14733
|
-
const fnsOf = it => (
|
|
16405
|
+
const fnsOf = it => (orthoFns && ortho.inFront(it.p1) && ortho.inFront(it.p2) ? orthoFns : mercFns);
|
|
14734
16406
|
const inBox = c => c && c[0] >= bbox[0] && c[0] <= bbox[2] && c[1] >= bbox[1] && c[1] <= bbox[3];
|
|
14735
16407
|
|
|
14736
16408
|
const sizePow = resolveSizePow(st, this.flags);
|
|
@@ -14829,7 +16501,6 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14829
16501
|
// — every size equal — SVG ignores: the opacity stays)
|
|
14830
16502
|
if (dop || dopMin) { const d = dopacity(it); if (isFinite(d)) { lineAlpha = d; headAlpha = d; } }
|
|
14831
16503
|
if (gradient) stops = fadeGradientStops(pts, lay.vx, lay.vy, fade);
|
|
14832
|
-
if (dash) dashArr = [50 * oz, 5 * oz + sw];
|
|
14833
16504
|
pts = pts.map(p => [p[0] + a[0], p[1] + a[1]]);
|
|
14834
16505
|
end = [end[0] + a[0], end[1] + a[1]];
|
|
14835
16506
|
} else {
|
|
@@ -14843,7 +16514,25 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14843
16514
|
headS = 4; refX = 4; refY = 2;
|
|
14844
16515
|
lineAlpha = fo || 0.3;
|
|
14845
16516
|
headAlpha = Math.min(1, (fo || 0.3) * 2);
|
|
14846
|
-
|
|
16517
|
+
}
|
|
16518
|
+
if (dash) {
|
|
16519
|
+
// flat's dash pattern: one long dash (50 px at the object scale)
|
|
16520
|
+
// and a small gap, its phase walking toward the target — a flow
|
|
16521
|
+
// of any length gets its own count of dashes that way: long ones
|
|
16522
|
+
// many, short ones none at all (a solid line). style.dashcount
|
|
16523
|
+
// instead divides the flow into that many dash+gap periods:
|
|
16524
|
+
// every flow, long or short, shows the same pattern and moves at
|
|
16525
|
+
// the same relative speed (one full dash cycle every 2s), the
|
|
16526
|
+
// arrows as on the region-migration page
|
|
16527
|
+
const nDashes = Math.round(styleNum(st.dashcount) || 0);
|
|
16528
|
+
if (nDashes >= 1) {
|
|
16529
|
+
let flowPx = 0;
|
|
16530
|
+
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]);
|
|
16531
|
+
const period = flowPx / nDashes;
|
|
16532
|
+
dashArr = [period * 0.85, period * 0.15];
|
|
16533
|
+
} else {
|
|
16534
|
+
dashArr = bezier ? [50 * oz, 5 * oz + sw] : [50 * oz, 15 * oz];
|
|
16535
|
+
}
|
|
14847
16536
|
}
|
|
14848
16537
|
// a line under a pixel: one pixel at its width as opacity
|
|
14849
16538
|
const drawW = Math.max(1, sw);
|
|
@@ -14869,7 +16558,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14869
16558
|
let run = [], runColors = [];
|
|
14870
16559
|
const flush = () => {
|
|
14871
16560
|
if (run.length > 1) {
|
|
14872
|
-
|
|
16561
|
+
// the flow triple: [dash, gap] in px (the shader's path
|
|
16562
|
+
// distance unit) + flat's random per-arc dashoffset phase
|
|
16563
|
+
// (maptheme.js 18249), fixed per arc: the layers are rebuilt on
|
|
16564
|
+
// every pan/zoom, a new random phase each time made dashes jump
|
|
16565
|
+
const dash = dashArr ? [dashArr[0], dashArr[1], flowDashPhase(p1, p2)] : [0, 0, 0];
|
|
14873
16566
|
if (globeCenter && !dashArr) {
|
|
14874
16567
|
// on the globe deck.gl draws only the first three quarters of a path that has a
|
|
14875
16568
|
// color per vertex (also an equal one): one color per path, a gradient as one
|
|
@@ -14904,12 +16597,22 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14904
16597
|
fill: [...rgb.slice(0, 3), Math.round(255 * Math.min(1, headAlpha) * groupAlpha)] });
|
|
14905
16598
|
}
|
|
14906
16599
|
}
|
|
14907
|
-
|
|
16600
|
+
// always FlowPathLayer: without dash offsets its flow hook no-ops
|
|
16601
|
+
// (solid lines), with them the pattern walks toward the target
|
|
16602
|
+
// (see makeFlowPathLayer). The dash triple is px; deck's path
|
|
16603
|
+
// shader measures along-path positions in HALF-STROKE-WIDTH
|
|
16604
|
+
// units (see makeFlowPathLayer), so the accessors convert:
|
|
16605
|
+
// getDashArray carries the pattern period (its gap stays 0 — the
|
|
16606
|
+
// extension renders solid, the moving gap is the flow hook),
|
|
16607
|
+
// getFlowPhase the solid length and flat's random per-arc phase
|
|
16608
|
+
const layers = [new FlowPathLayer({
|
|
14908
16609
|
id: `ix-vector-${this.name}`, data: paths, pickable: true,
|
|
14909
16610
|
getPath: d => d.path, getColor: d => d.colors, getWidth: d => d.width,
|
|
16611
|
+
getDashArray: d => [(d.dash[0] + d.dash[1]) * 2 / Math.max(d.width, 1), 0],
|
|
16612
|
+
getFlowPhase: d => [d.dash[0] * 2 / Math.max(d.width, 1), d.dash[2]],
|
|
14910
16613
|
widthUnits: 'pixels', capRounded: dash && bezier, jointRounded: false, billboard: false,
|
|
14911
|
-
|
|
14912
|
-
|
|
16614
|
+
extensions: [new PathStyleExtension({ dash: true, dashMode: 'path' })],
|
|
16615
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14913
16616
|
})];
|
|
14914
16617
|
if (heads.length) {
|
|
14915
16618
|
// the head count in the id: deck kept a same-id polygon layer's
|
|
@@ -15009,7 +16712,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15009
16712
|
const v = raw && raw[this.style.labelfield];
|
|
15010
16713
|
let text = v == null || v === '' ? '' : String(v);
|
|
15011
16714
|
if (!text) return null;
|
|
15012
|
-
|
|
16715
|
+
// flat reads it with __scanValue: "1.234,5", "12 500"
|
|
16716
|
+
const num = flatScanValue(text);
|
|
15013
16717
|
text = (!isNaN(num) ? flatGroupedValue(num, dec != null ? dec : 2, noBreaks) : text) + units;
|
|
15014
16718
|
const width = Math.max(titleTextWidth(text, f), (text.length + 2) * f * 0.5);
|
|
15015
16719
|
const x = width / 3, y = f / 3 + r + 5 * unitPx;
|
|
@@ -15181,6 +16885,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15181
16885
|
// the box is sized by the value's own text, not the valuefield's
|
|
15182
16886
|
const boxStyle = Object.assign({}, this.style, { valuefield: undefined });
|
|
15183
16887
|
const boxes = [], texts = [];
|
|
16888
|
+
// the label's drawn extent for the chart-group BOX (below): x from -r
|
|
16889
|
+
// to 1.05r, y from -0.83f to 0.77f around the chart origin
|
|
16890
|
+
const labelExtent = new Map();
|
|
15184
16891
|
for (const d of combined) {
|
|
15185
16892
|
const r = radiusOf(d);
|
|
15186
16893
|
if (!(r > 0)) continue;
|
|
@@ -15195,6 +16902,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15195
16902
|
const o = alignOf(d);
|
|
15196
16903
|
// the box's center relative to the chart's: x from −r to 1.05r, y from −0.83f to 0.77f
|
|
15197
16904
|
boxes.push({ ...d, icon, pixelOffset: [o[0] + 0.025 * r, o[1] - 0.03 * f] });
|
|
16905
|
+
labelExtent.set(d, [-r, -0.83 * f, 1.05 * r, 0.77 * f]);
|
|
15198
16906
|
if (showValues) {
|
|
15199
16907
|
const text = textOf(props);
|
|
15200
16908
|
const fontSize = Math.min(r * 0.8, r * 3.3 / Math.max(1, text.length)) * valueScale;
|
|
@@ -15202,7 +16910,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15202
16910
|
if (fontSize > 0) texts.push({ ...d, text, fontSize, color, pixelOffset: o });
|
|
15203
16911
|
}
|
|
15204
16912
|
}
|
|
15205
|
-
|
|
16913
|
+
// BOX / TITLE: flat's chart-group box around the label chart
|
|
16914
|
+
const boxRows = this._boxRows(() => combined.filter(d => labelExtent.has(d)), { unit, liveZoom, extentOf: d => labelExtent.get(d) });
|
|
16915
|
+
const boxLayers = boxRows ? this._boxTitleLayers(boxRows, {
|
|
16916
|
+
unit, liveZoom, positionOf: b => b.d.geometry.coordinates, offsetOf: b => alignOf(b.d),
|
|
16917
|
+
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}` }
|
|
16918
|
+
}) : null;
|
|
16919
|
+
const layers = boxLayers ? [...boxLayers.box] : [];
|
|
15206
16920
|
if (boxes.length) {
|
|
15207
16921
|
layers.push(new IconLayer({
|
|
15208
16922
|
id: `ix-bubbles-${this.name}-g${this._iconGeneration}`,
|
|
@@ -15236,6 +16950,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15236
16950
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
15237
16951
|
}));
|
|
15238
16952
|
}
|
|
16953
|
+
if (boxLayers) layers.push(...boxLayers.titles);
|
|
15239
16954
|
return layers;
|
|
15240
16955
|
}
|
|
15241
16956
|
|
|
@@ -15295,8 +17010,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15295
17010
|
ctx.lineTo(cx + r, cy - r * 0.6);
|
|
15296
17011
|
ctx.lineTo(cx, cy + r);
|
|
15297
17012
|
break;
|
|
17013
|
+
// flat's per-part SYMBOL roundrect (maptheme.js 22039): side 2r, like
|
|
17014
|
+
// 'squarefull'; the other branch (21639) is 1.6r
|
|
17015
|
+
case 'roundrectfull':
|
|
15298
17016
|
case 'roundrect': {
|
|
15299
|
-
const s = r * 0.8;
|
|
17017
|
+
const s = shape === 'roundrectfull' ? r : r * 0.8;
|
|
15300
17018
|
if (ctx.roundRect) ctx.roundRect(cx - s, cy - s, s * 2, s * 2, r * 0.3);
|
|
15301
17019
|
else ctx.rect(cx - s, cy - s, s * 2, s * 2);
|
|
15302
17020
|
break;
|
|
@@ -15365,6 +17083,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15365
17083
|
function withAlpha(rgb, a) {
|
|
15366
17084
|
return a >= 1 ? rgb.slice(0, 3) : [...rgb.slice(0, 3), Math.round(255 * Math.max(0, a))];
|
|
15367
17085
|
}
|
|
17086
|
+
// a style color of "none": no paint (flat writes it into the SVG style:
|
|
17087
|
+
// fill:none / stroke:none) — neither a color nor a canvas fillStyle
|
|
17088
|
+
function isNoneColor(v) {
|
|
17089
|
+
return typeof v === 'string' && v.trim().toLowerCase() === 'none';
|
|
17090
|
+
}
|
|
15368
17091
|
function hexOrNamedToRgb(v) {
|
|
15369
17092
|
const NAMED = { gray: [128, 128, 128], grey: [128, 128, 128], black: [0, 0, 0], white: [255, 255, 255] };
|
|
15370
17093
|
return parseCssColor(v) || NAMED[v] || [130, 130, 130];
|
|
@@ -15481,21 +17204,34 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15481
17204
|
// last built map; called while the map is still building, they run once
|
|
15482
17205
|
// it is ready.
|
|
15483
17206
|
const MAP_HANDLE_METHODS = ['replace', 'add', 'newTheme', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
15484
|
-
'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'setProjection', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible', 'setThemeTimeFrame', 'attribution'];
|
|
17207
|
+
'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'setProjection', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible', 'switchLayer', 'setThemeTimeFrame', 'attribution'];
|
|
15485
17208
|
const _mapHandles = Object.create(null);
|
|
15486
17209
|
function makeMapHandle(szMap) {
|
|
15487
17210
|
const h = {};
|
|
15488
|
-
|
|
17211
|
+
// a name a Map() declared waits for that map; any other name (or none)
|
|
17212
|
+
// is the last built map, as before
|
|
17213
|
+
const declared = () => szMap && _namedMapReady[szMap];
|
|
17214
|
+
const api = () => (szMap && _mapApis[szMap]) || (declared() ? null : _lastMapApi);
|
|
15489
17215
|
for (const m of MAP_HANDLE_METHODS) {
|
|
15490
17216
|
h[m] = (...args) => {
|
|
15491
17217
|
const a = api();
|
|
15492
17218
|
if (a) { const r = a[m](...args); return r === a ? h : r; }
|
|
15493
|
-
_mapReady.then(ready => ready[m](...args))
|
|
17219
|
+
(declared() ? declared().promise : _mapReady).then(ready => ready[m](...args))
|
|
17220
|
+
.catch(err => console.error(`[ixmaps-gl] map(${szMap ? '"' + szMap + '"' : ''}).${m}:`, err));
|
|
15494
17221
|
return h;
|
|
15495
17222
|
};
|
|
15496
17223
|
}
|
|
15497
17224
|
h.getZoom = () => { const a = api(); return a ? a.getZoom() : undefined; };
|
|
15498
17225
|
h.getThemeObj = id => getThemeObj(id);
|
|
17226
|
+
// flat's mapApi.message(szText) (htmlgui_flat.js 1381): a short status
|
|
17227
|
+
// line over the map — the same accepted no-op as the global
|
|
17228
|
+
// ixmaps.message ("flat shows a short status message on the map;
|
|
17229
|
+
// not ported"), here for a page's own map().message('...') calls
|
|
17230
|
+
h.message = () => h;
|
|
17231
|
+
// flat's mapApi.getProjectString() (htmlgui_flat.js 1375): the
|
|
17232
|
+
// project JSON of the map — the global's, of the last built map (a
|
|
17233
|
+
// page's code-view overlay calls it through the handle, WBGT port)
|
|
17234
|
+
h.getProjectString = () => getProjectString();
|
|
15499
17235
|
// flat's mapApi.theme(szTheme) (htmlgui_flat.js 1571): the theme handle
|
|
15500
17236
|
h.theme = szTheme => new themeApi(szMap, szTheme);
|
|
15501
17237
|
// flat's mapApi aliases (htmlgui_flat.js 1286+): show/hide a theme,
|
|
@@ -15946,9 +17682,14 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15946
17682
|
// their own comments) are still real, directly-callable globals a
|
|
15947
17683
|
// page's OWN custom UI can use too — only THIS generated markup
|
|
15948
17684
|
// happens to reach them via delegation instead of inline HTML.
|
|
17685
|
+
// the handlers below outlive this call: they read the theme id of
|
|
17686
|
+
// the LATEST call into this container (a theme switch re-renders
|
|
17687
|
+
// the same div for another theme)
|
|
17688
|
+
container._ixFacetsId = szId;
|
|
15949
17689
|
if (!container.dataset.ixFacetsWired) {
|
|
15950
17690
|
container.dataset.ixFacetsWired = '1';
|
|
15951
17691
|
container.addEventListener('click', e => {
|
|
17692
|
+
const szId = container._ixFacetsId;
|
|
15952
17693
|
const moreBtn = e.target.closest('[data-role="more"]');
|
|
15953
17694
|
if (moreBtn) { moreBtn.hidden = true; moreBtn.nextElementSibling.hidden = false; return; }
|
|
15954
17695
|
|
|
@@ -15974,7 +17715,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15974
17715
|
container.addEventListener('keyup', e => {
|
|
15975
17716
|
if (e.key !== 'Enter') return;
|
|
15976
17717
|
const input = e.target.closest('[data-role="search"]');
|
|
15977
|
-
if (input) global.__setFilter(
|
|
17718
|
+
if (input) global.__setFilter(container._ixFacetsId, input.closest('[data-field]').dataset.field, input.value);
|
|
15978
17719
|
});
|
|
15979
17720
|
}
|
|
15980
17721
|
}
|
|
@@ -16157,6 +17898,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16157
17898
|
newTheme: (title, theme, flag) => _mapHandle.newTheme(title, theme, flag),
|
|
16158
17899
|
// flat's time slider: show the records of a theme (null = all themes) whose timefield lies in [min, max)
|
|
16159
17900
|
setThemeTimeFrame: (szId, nMin, nMax) => _mapHandle.setThemeTimeFrame(szId, nMin, nMax),
|
|
17901
|
+
// flat's ixmaps.switchLayer(szLayerName, fState) (htmlgui.js 3383): every
|
|
17902
|
+
// theme of the named layer on the last built map shown or hidden
|
|
17903
|
+
switchLayer: (szLayerName, fState) => _mapHandle.switchLayer(szLayerName, fState),
|
|
16160
17904
|
setBasemapOpacity: compatSetBasemapOpacity,
|
|
16161
17905
|
// this engine's version (flat's own ixmaps.version numbers flat)
|
|
16162
17906
|
glVersion: IXMAPS_GL_VERSION,
|
|
@@ -16181,19 +17925,19 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16181
17925
|
// test-only: lets test/unit/*.test.mjs call pure internals directly (the
|
|
16182
17926
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
16183
17927
|
global.__ixmapsGlInternals = {
|
|
16184
|
-
IXMAPS_GL_VERSION, applyFlatMaxCharts, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout, ringsLayout, pieSliceLayout, pieValueLabelLayout, isBarChart, flatBarLayout,
|
|
16185
|
-
normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
17928
|
+
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,
|
|
17929
|
+
chartBoxLayout, choroplethOutlineColor, normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
16186
17930
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
16187
17931
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|
|
16188
17932
|
computeAlphaStats, computeMultiFieldClasses, computeDominantStats, resolveDominantClass, computeComposeColorStats,
|
|
16189
17933
|
resolveComposedColor, computeRangeClasses, colorSchemeClassCount, resolveDopacityAlpha,
|
|
16190
17934
|
flatColorSweep, applyClassesToColorScheme, resolveClassColors, flatOutlierStats, parseCssColor, flatLegendLook,
|
|
16191
17935
|
flatToMapLibreZoom, mapLibreToFlatZoom, flatViewToMapLibreZoom, mapLibreToFlatViewZoom, flatSvgProjectionOf, flatSvgProjectionByOptions, normalSizeScaleOf, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
|
|
16192
|
-
fetchLayerData, parseCsvText, dataTableRows, geometryRowsToFeatureCollection, filterFlatValues, applyField100, field100Binding, rangeClassLegendTotals, resolveAggregateValue, classValueSeparate, cellAggregatedValues, oneHot, groupCoLocated, aggregateOnGrid, GridAggregateIndex, flatRangeParts, aggregateField100, valueRadius, itemPlotGeometry, itemAnchor, objectZoomFactor, resolveZoomReference, defaultNormalSizeValue,
|
|
16193
|
-
applyWhereFilter, joinChartPositions, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatBubbleLook, bubbleIconLook, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
17936
|
+
fetchLayerData, QUERY_HANDOFF, parseCsvText, dataTableRows, geometryRowsToFeatureCollection, filterFlatValues, applyField100, field100Binding, rangeClassLegendTotals, resolveAggregateValue, classValueSeparate, cellAggregatedValues, oneHot, groupCoLocated, aggregateOnGrid, GridAggregateIndex, flatRangeParts, explicitRangeBreaks, explicitRangeParts, resolvePartsClass, isNoneColor, aggregateField100, valueRadius, itemPlotGeometry, itemAnchor, objectZoomFactor, resolveZoomReference, defaultNormalSizeValue,
|
|
17937
|
+
applyWhereFilter, joinChartPositions, joinChoroplethFeatures, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatBubbleLook, bubbleIconLook, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
16194
17938
|
drawSymbolPath, normalizeSymbolShape,
|
|
16195
17939
|
flatFormatValue, themeStyleArgs, flatChangedStyleValue, explicitValueDecimals, legendDecimals,
|
|
16196
|
-
dataCacheKey, dataCacheDisabled, cachedLayerData, featuresBounds, wantsZoomToExtent, legendIsOn,
|
|
17940
|
+
dataCacheKey, dataCacheDisabled, cachedLayerData, featuresBounds, wantsZoomToExtent, legendIsOn, applySphereBackdrop,
|
|
16197
17941
|
};
|
|
16198
17942
|
global.__setFilter = __setFilter;
|
|
16199
17943
|
global.__removeFacets = __removeFacets;
|