ixmaps-gl 0.2.20 → 0.2.21
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 +45 -8
- package/ixmaps-gl.js +2134 -95
- package/package.json +1 -1
package/ixmaps-gl.js
CHANGED
|
@@ -53,12 +53,22 @@
|
|
|
53
53
|
// PIE|DONUT) a pie per record, a slice per value field, flat's sizing,
|
|
54
54
|
// CENTER part and VALUES leader labels (_buildPieLayers; 3D/VOLUME/
|
|
55
55
|
// STARBURST/RAYS/BOW variants are noted, drawn flat; AGGREGATE and
|
|
56
|
-
// CATEGORICAL pies
|
|
56
|
+
// CATEGORICAL pies a pie per item, a slice per category or field, flat's
|
|
57
|
+
// aggregation), CHART|VECTOR and CHART|BEZIER a flow line
|
|
58
|
+
// per item from its position to its second one (binding position2 /
|
|
59
|
+
// style lookupfield2), per record or AGGREGATE'd per origin–destination
|
|
60
|
+
// pair, with flat's widths, colors, arrow heads and GRADIENT / FADEIN
|
|
61
|
+
// (_buildVectorLayers; VALUES, DYNAMICWIDTH, LONG and the DASH animation
|
|
62
|
+
// are noted, see VECTOR_INERT_FLAGS), CHART|BAR (and BARS) a bar chart
|
|
63
|
+
// per record or aggregated item, a bar per value field or category, flat's
|
|
64
|
+
// sizing, HORZ / STACKED / SORT, POINTER and ARROW pointers, VALUES texts,
|
|
65
|
+
// BOX and TITLE (_buildBarLayers; 3D, VOLUME, DOTTED, TRENDLINE, the axis
|
|
66
|
+
// labels … are noted, drawn plain, see BAR_INERT_FLAGS). These
|
|
57
67
|
// base types are dispatched by buildDeckLayers in the real engine's own
|
|
58
68
|
// precedence order — DOT is checked first because the real engine's DOT
|
|
59
69
|
// bypasses the whole drawChart/modifier pipeline rather than being
|
|
60
|
-
// "BUBBLE with the size locked." Other real base types (QUAD,
|
|
61
|
-
//
|
|
70
|
+
// "BUBBLE with the size locked." Other real base types (QUAD, WAFFLE,
|
|
71
|
+
// and SYMBOL's own shape variants) are NOT
|
|
62
72
|
// implemented yet — adding one means a new _buildXLayers() method plus a
|
|
63
73
|
// dispatch line, not a rewrite, but each should be added deliberately
|
|
64
74
|
// (checking, per real source, which modifiers it actually shares with
|
|
@@ -86,7 +96,7 @@
|
|
|
86
96
|
// ---------------------------------------------------------------
|
|
87
97
|
// this engine's release (= package.json "version", checked by
|
|
88
98
|
// test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
|
|
89
|
-
const IXMAPS_GL_VERSION = '0.2.
|
|
99
|
+
const IXMAPS_GL_VERSION = '0.2.21';
|
|
90
100
|
|
|
91
101
|
const LIB_URLS = {
|
|
92
102
|
// Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
|
|
@@ -203,10 +213,10 @@
|
|
|
203
213
|
// byte-grep of the shipped file (deck.gl's own @deck.gl/maplibre
|
|
204
214
|
// overview page claims ES-modules-only, which the actual UMD bundle
|
|
205
215
|
// contradicts).
|
|
206
|
-
let IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer;
|
|
207
|
-
// PieSliceLayer (see makePieSliceLayer)
|
|
208
|
-
// once deck.gl is there
|
|
209
|
-
let PieSliceLayer;
|
|
216
|
+
let IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer, PathLayer, PathStyleExtension;
|
|
217
|
+
// PieSliceLayer (see makePieSliceLayer) and BarShapeLayer
|
|
218
|
+
// (makeBarShapeLayer) are derived from ScatterplotLayer once deck.gl is there
|
|
219
|
+
let PieSliceLayer, BarShapeLayer;
|
|
210
220
|
|
|
211
221
|
// CHART|PIE / DONUT slices (_buildPieLayers): one instance per slice — a
|
|
212
222
|
// ScatterplotLayer circle (billboarded, pixel radius, pixel offset, the
|
|
@@ -310,6 +320,107 @@ in vec4 vPieLineColor;
|
|
|
310
320
|
return Layer;
|
|
311
321
|
}
|
|
312
322
|
|
|
323
|
+
// CHART|BAR / BARS bars (_buildBarLayers): one instance per bar — a
|
|
324
|
+
// ScatterplotLayer circle (billboarded, pixel radius, pixel offset, the
|
|
325
|
+
// same projection path as every other chart, globe included) whose
|
|
326
|
+
// fragment shader keeps only the bar's shape inside the circle and draws
|
|
327
|
+
// flat's outline along its edge, centered on it (an SVG stroke).
|
|
328
|
+
// getBarShape: [half width, shaft half width, shaft length, band] px
|
|
329
|
+
// getBarShape2: [head length, direction (0 up, 1 right, 2 down,
|
|
330
|
+
// 3 left; + 4: never picked), outline width px, the base's distance from
|
|
331
|
+
// the circle's center along the direction] — the circle
|
|
332
|
+
// centered on the shape, its radius covering it
|
|
333
|
+
// A plain bar is a rectangle (shaft = half width, no band, no head); a
|
|
334
|
+
// POINTER / ARROW its shaft, a band as wide as the bar and a triangular
|
|
335
|
+
// head (flat's pointer path). Why not a canvas icon per bar: every bar
|
|
336
|
+
// has its own length (~1 000 distinct icons per view on a comuni page,
|
|
337
|
+
// beyond deck.gl's never-evicting icon atlas); nor polygons in map
|
|
338
|
+
// coordinates, re-tessellated at every zoom step and wrong on the globe.
|
|
339
|
+
function makeBarShapeLayer(Base) {
|
|
340
|
+
class Layer extends Base {
|
|
341
|
+
getShaders() {
|
|
342
|
+
const shaders = super.getShaders();
|
|
343
|
+
const inject = Object.assign({}, shaders.inject || {});
|
|
344
|
+
inject['vs:#decl'] = (inject['vs:#decl'] || '') + `
|
|
345
|
+
in vec4 instanceBarShapes;
|
|
346
|
+
in vec4 instanceBarShapes2;
|
|
347
|
+
out vec4 vBarShape;
|
|
348
|
+
out vec4 vBarShape2;
|
|
349
|
+
out float vBarRadius;
|
|
350
|
+
out vec4 vBarLineColor;
|
|
351
|
+
`;
|
|
352
|
+
inject['vs:#main-end'] = (inject['vs:#main-end'] || '') + `
|
|
353
|
+
vBarShape = instanceBarShapes;
|
|
354
|
+
vBarShape2 = instanceBarShapes2;
|
|
355
|
+
vBarRadius = outerRadiusPixels;
|
|
356
|
+
vBarLineColor = vec4(instanceLineColors.rgb, instanceLineColors.a * layer.opacity);
|
|
357
|
+
`;
|
|
358
|
+
inject['fs:#decl'] = (inject['fs:#decl'] || '') + `
|
|
359
|
+
in vec4 vBarShape;
|
|
360
|
+
in vec4 vBarShape2;
|
|
361
|
+
in float vBarRadius;
|
|
362
|
+
in vec4 vBarLineColor;
|
|
363
|
+
float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
364
|
+
vec2 ab = b - a;
|
|
365
|
+
float l2 = dot(ab, ab);
|
|
366
|
+
float t = l2 > 0.0 ? clamp(dot(p - a, ab) / l2, 0.0, 1.0) : 0.0;
|
|
367
|
+
return length(p - a - t * ab);
|
|
368
|
+
}
|
|
369
|
+
`;
|
|
370
|
+
// before the picking module's own filter (it replaces the color
|
|
371
|
+
// when picking): the discards shape the pick area too
|
|
372
|
+
inject['fs:DECKGL_FILTER_COLOR'] = (inject['fs:DECKGL_FILTER_COLOR'] || '') + `
|
|
373
|
+
{
|
|
374
|
+
vec2 barS = geometry.uv * vBarRadius;
|
|
375
|
+
barS.y = -barS.y;
|
|
376
|
+
// a direction beyond 3: drawn, never picked (a chart's BOX)
|
|
377
|
+
float barDir = vBarShape2.y;
|
|
378
|
+
bool barNoPick = barDir > 3.5;
|
|
379
|
+
if (barNoPick) barDir -= 4.0;
|
|
380
|
+
if (barNoPick && picking.isActive > 0.5) discard;
|
|
381
|
+
vec2 barD = barDir < 0.5 ? vec2(0.0, -1.0) : barDir < 1.5 ? vec2(1.0, 0.0) : barDir < 2.5 ? vec2(0.0, 1.0) : vec2(-1.0, 0.0);
|
|
382
|
+
vec2 barP = vec2(abs(dot(barS, vec2(-barD.y, barD.x))), dot(barS, barD) + vBarShape2.w);
|
|
383
|
+
float barW2 = vBarShape.x, barSW = vBarShape.y, barL = vBarShape.z, barB = vBarShape.w, barH = vBarShape2.x;
|
|
384
|
+
bool barIn = (barP.x <= barSW && barP.y >= 0.0 && barP.y <= barL)
|
|
385
|
+
|| (barP.x <= barW2 && barP.y >= barL && barP.y <= barL + barB)
|
|
386
|
+
|| (barH > 0.0 && barP.y >= barL + barB && barP.y <= barL + barB + barH && barP.x <= barW2 * (1.0 - (barP.y - barL - barB) / barH));
|
|
387
|
+
float barD0 = barSegDist(barP, vec2(0.0, 0.0), vec2(barSW, 0.0));
|
|
388
|
+
barD0 = min(barD0, barSegDist(barP, vec2(barSW, 0.0), vec2(barSW, barL)));
|
|
389
|
+
barD0 = min(barD0, barSegDist(barP, vec2(barSW, barL), vec2(barW2, barL)));
|
|
390
|
+
barD0 = min(barD0, barSegDist(barP, vec2(barW2, barL), vec2(barW2, barL + barB)));
|
|
391
|
+
barD0 = min(barD0, barSegDist(barP, vec2(barW2, barL + barB), vec2(0.0, barL + barB + barH)));
|
|
392
|
+
// in device pixels (fwidth: CSS px per device pixel): edges anti-aliased
|
|
393
|
+
// over one device pixel, a hairline outline fades in by its width
|
|
394
|
+
float barAA = max(fwidth(barS.x), 1e-4);
|
|
395
|
+
float barFill = barIn ? clamp(barD0 / barAA + 0.5, 0.0, 1.0) : clamp(0.5 - barD0 / barAA, 0.0, 1.0);
|
|
396
|
+
// a bar thinner than a device pixel covers only its share of it (a 0
|
|
397
|
+
// value's bar is no line)
|
|
398
|
+
barFill = min(barFill, clamp(min(2.0 * barW2, barL + barB + barH) / barAA, 0.0, 1.0));
|
|
399
|
+
float barLW = vBarShape2.z;
|
|
400
|
+
float barLine = barLW > 0.0 ? clamp(min(barLW * 0.5 / barAA + 0.5 - barD0 / barAA, barLW / barAA), 0.0, 1.0) : 0.0;
|
|
401
|
+
color = vec4(mix(color.rgb, vBarLineColor.rgb, barLine), mix(color.a * barFill, vBarLineColor.a, barLine));
|
|
402
|
+
if (color.a <= 0.0) discard;
|
|
403
|
+
}
|
|
404
|
+
`;
|
|
405
|
+
shaders.inject = inject;
|
|
406
|
+
return shaders;
|
|
407
|
+
}
|
|
408
|
+
initializeState() {
|
|
409
|
+
super.initializeState();
|
|
410
|
+
this.getAttributeManager().addInstanced({
|
|
411
|
+
instanceBarShapes: { size: 4, accessor: 'getBarShape', defaultValue: [0, 0, 0, 0] },
|
|
412
|
+
instanceBarShapes2: { size: 4, accessor: 'getBarShape2', defaultValue: [0, 0, 0, 0] }
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
Layer.layerName = 'BarShapeLayer';
|
|
417
|
+
Layer.defaultProps = {
|
|
418
|
+
getBarShape: { type: 'accessor', value: [0, 0, 0, 0] },
|
|
419
|
+
getBarShape2: { type: 'accessor', value: [0, 0, 0, 0] }
|
|
420
|
+
};
|
|
421
|
+
return Layer;
|
|
422
|
+
}
|
|
423
|
+
|
|
313
424
|
// Cached so multiple ixmaps.Map() calls on one page (or a page that
|
|
314
425
|
// still has its own static <script> tags for these libraries) only
|
|
315
426
|
// ever fetch once: a library a page already loaded is reused when it is
|
|
@@ -385,8 +496,9 @@ in vec4 vPieLineColor;
|
|
|
385
496
|
global.Mustache ? Promise.resolve() : loadScript(LIB_URLS.mustache),
|
|
386
497
|
[...document.styleSheets].some(s => s.href === LIB_URLS.maplibreCss) ? Promise.resolve() : loadStylesheet(LIB_URLS.maplibreCss)
|
|
387
498
|
]).then(() => {
|
|
388
|
-
({ IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer } = global.deck);
|
|
499
|
+
({ IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer, PathLayer, PathStyleExtension } = global.deck);
|
|
389
500
|
PieSliceLayer = makePieSliceLayer(ScatterplotLayer);
|
|
501
|
+
BarShapeLayer = makeBarShapeLayer(ScatterplotLayer);
|
|
390
502
|
// which engine and libraries this page really runs (a page may
|
|
391
503
|
// bring its own MapLibre/deck.gl — the loaded ones are named)
|
|
392
504
|
const ml = global.maplibregl && typeof global.maplibregl.getVersion === 'function' ? global.maplibregl.getVersion() : '?';
|
|
@@ -1547,13 +1659,17 @@ in vec4 vPieLineColor;
|
|
|
1547
1659
|
const lookupField = spec.binding.lookup;
|
|
1548
1660
|
let layers = String(spec.name).split('|');
|
|
1549
1661
|
if (layers.length > 1 && spec.flags && spec.flags.has('DIFFERENCE')) layers = layers.slice(-1);
|
|
1662
|
+
const lookup2 = spec.binding.lookup2;
|
|
1550
1663
|
const features = [];
|
|
1551
|
-
|
|
1664
|
+
const indexes = layers.map(layer => {
|
|
1552
1665
|
const index = new Map();
|
|
1553
1666
|
runtimes.filter(r => r.name === layer && (r.flags.has('FEATURE') || r.flags.has('FEATURES'))).forEach(r => {
|
|
1554
1667
|
// in theme order, as flat's layer holds the themes' shapes
|
|
1555
1668
|
for (const [k, v] of featurePositionIndex(r)) { if (flatShapeWins(index.get(k), v.area)) index.set(k, v); }
|
|
1556
1669
|
});
|
|
1670
|
+
return index;
|
|
1671
|
+
});
|
|
1672
|
+
for (const index of indexes) {
|
|
1557
1673
|
if (!index.size) continue;
|
|
1558
1674
|
for (const row of table.rows) {
|
|
1559
1675
|
const v = row[lookupField];
|
|
@@ -1563,7 +1679,18 @@ in vec4 vPieLineColor;
|
|
|
1563
1679
|
// joined shape, as flat) needs the shape's km² area carried along
|
|
1564
1680
|
// (featureAreaKm2 prefers this marker), exactly as the choropleth
|
|
1565
1681
|
// join stamps it
|
|
1566
|
-
if (hit)
|
|
1682
|
+
if (!hit) continue;
|
|
1683
|
+
const f = { type: 'Feature', geometry: { type: 'Point', coordinates: hit.pos }, properties: row, _flatAreaKm2: hit.areaKm2 };
|
|
1684
|
+
// VECTOR / BEZIER: the second position by its own key (flat looks
|
|
1685
|
+
// it up in every layer of the theme, maptheme.js 10431-10436); a
|
|
1686
|
+
// record without one stays an item that draws no line (null)
|
|
1687
|
+
if (lookup2 && !String(lookup2).includes('|')) {
|
|
1688
|
+
const v2 = row[lookup2];
|
|
1689
|
+
let hit2 = null;
|
|
1690
|
+
if (v2 != null && v2 !== '') for (const idx of indexes) { hit2 = idx.get(flatLookupKey(v2, spec.style)); if (hit2) break; }
|
|
1691
|
+
f._pos2 = hit2 ? hit2.pos : null;
|
|
1692
|
+
}
|
|
1693
|
+
features.push(f);
|
|
1567
1694
|
}
|
|
1568
1695
|
}
|
|
1569
1696
|
return { type: 'FeatureCollection', features };
|
|
@@ -2064,6 +2191,26 @@ in vec4 vPieLineColor;
|
|
|
2064
2191
|
'POW2', 'POW3', 'NOSORT'];
|
|
2065
2192
|
// flat's PIE / DONUT variants with no rendering here (see _buildPieLayers)
|
|
2066
2193
|
const PIE_INERT_FLAGS = [/3D/, /VOLUME/, /HEIGHT/, /STARBURST/, /FLOWER/, /RAYS/, /BOW/, /ZOOM/, /DIRECTION/, /DIRECTED/, /POLAR/];
|
|
2194
|
+
// ... and of AGGREGATE / CATEGORICAL pies: GRIDSIZE's pie the size of its
|
|
2195
|
+
// grid cell (drawn by the SIZE rule instead), AUTOSIZE, DOPACITY, the
|
|
2196
|
+
// chart BOX and TITLE, GAUGE, MULTIPLE / DIFFUSE, the post-aggregation
|
|
2197
|
+
// value arithmetic (DIFFERENCE, FRACTION, PERCENT …, field100)
|
|
2198
|
+
const PIE_AGGREGATE_INERT_FLAGS = [/^GRIDSIZE$/, /AUTOSIZE/, /DOPACITY/, /BOX/, /TITLE/, /^GAUGE$/, /^MULTIPLE$/, /^DIFFUSE$/,
|
|
2199
|
+
/^DIFFERENCE$/, /^FRACTION$/, /^PERCENT$/, /^PERMILLE$/, /^PRODUCT$/, /^CALCVAL$/, /^RELATIVE$/];
|
|
2200
|
+
// flat's BAR / BARS variants with no rendering here (see flatBarLayout):
|
|
2201
|
+
// the bar is drawn plain (SMALLARROW as ARROW) — tested as substrings,
|
|
2202
|
+
// as flat does
|
|
2203
|
+
const BAR_INERT_FLAGS = [/3D/, /VOLUME/, /COLUMN/, /DOTTED/, /SMALLARROW/, /TRENDLINE/, /AXIS/, /^CLIP$/, /MORPH/,
|
|
2204
|
+
/ZOOM/, /NORMSIZE/, /MENUSIZE/, /INVERT/, /DIAGLEFT/, /DIAGRIGHT/, /^WIDTH$/, /^MULTIPLE$/, /^DIFFUSE$/,
|
|
2205
|
+
/^DIFFERENCE$/, /^FRACTION$/, /^PERCENT$/, /^PERMILLE$/, /^PRODUCT$/, /^CALCVAL$/, /^RELATIVE$/];
|
|
2206
|
+
// flat's VECTOR / BEZIER modifiers with no rendering here (see
|
|
2207
|
+
// _buildVectorLayers), the word → what happens instead
|
|
2208
|
+
const VECTOR_INERT_FLAGS = {
|
|
2209
|
+
VALUES: 'the value text along the curve is not drawn yet',
|
|
2210
|
+
DYNAMICWIDTH: 'not drawn yet (flat widens by the log of the length in its own map units) — the plain width instead',
|
|
2211
|
+
LONG: 'not drawn yet (its bow depends on flat\'s own map units) — the default bow instead',
|
|
2212
|
+
DASH: 'drawn as static dashes — flat\'s dash animation is not ported'
|
|
2213
|
+
};
|
|
2067
2214
|
const _warnedFlags = new Set();
|
|
2068
2215
|
|
|
2069
2216
|
// GL-PORT COMPAT: real ixmaps-flat's global ixmaps.getThemeObj(szId) /
|
|
@@ -2401,6 +2548,9 @@ in vec4 vPieLineColor;
|
|
|
2401
2548
|
'style.valuefield': ['style', 'valuefield'],
|
|
2402
2549
|
'style.timefield': ['binding', 'time'], // setThemeTimeFrame, see LayerRuntime.setTimeFrame
|
|
2403
2550
|
'style.titlefield': ['binding', 'title'], // {{theme.item.title}}, see _buildTooltipContext
|
|
2551
|
+
// VECTOR / BEZIER's second position: a join key ("a") or lat/lon
|
|
2552
|
+
// fields ("a|b"), see joinChartPositions and _vectorRecords
|
|
2553
|
+
'style.lookupfield2': ['binding', 'lookup2'],
|
|
2404
2554
|
// the lookup key's normalization (flatLookupKey): flat's newTheme moves
|
|
2405
2555
|
// binding.tonumber / digits / … into these style keys (htmlgui.js 1712)
|
|
2406
2556
|
'style.lookupdigits': ['style', 'lookupdigits'],
|
|
@@ -2633,7 +2783,10 @@ in vec4 vPieLineColor;
|
|
|
2633
2783
|
// real ixmaps-flat (htmlgui.js newTheme) merges .meta() INTO style first
|
|
2634
2784
|
// — meta wins — and only then reads type, bindings and style properties
|
|
2635
2785
|
const style = Object.assign({}, def.style, def.meta);
|
|
2636
|
-
|
|
2786
|
+
let typeStr = style.type != null ? String(style.type) : '';
|
|
2787
|
+
// flat (htmlgui-side theme parse, maptheme.js 1192): the old type
|
|
2788
|
+
// word EXACT is CATEGORICAL
|
|
2789
|
+
if (/\bEXACT\b/.test(typeStr) && !/\bCATEGORICAL\b/.test(typeStr)) typeStr += '|CATEGORICAL';
|
|
2637
2790
|
const filter = style.filter;
|
|
2638
2791
|
const title = style.title;
|
|
2639
2792
|
delete style.type;
|
|
@@ -2664,6 +2817,37 @@ in vec4 vPieLineColor;
|
|
|
2664
2817
|
console.info(`[ixmaps-gl] type flag "${flag}" on a PIE chart recognized, not drawn yet — the pie is drawn flat and plain`);
|
|
2665
2818
|
}
|
|
2666
2819
|
});
|
|
2820
|
+
// AGGREGATE / CATEGORICAL pies (_isAggregatedPie): what is not ported
|
|
2821
|
+
if (flags.has('AGGREGATE') || flags.has('CATEGORICAL')) {
|
|
2822
|
+
flags.forEach(flag => {
|
|
2823
|
+
const inert = PIE_AGGREGATE_INERT_FLAGS.find(re => re.test(flag));
|
|
2824
|
+
if (inert && !_warnedFlags.has('PIEAGG:' + flag)) {
|
|
2825
|
+
_warnedFlags.add('PIEAGG:' + flag);
|
|
2826
|
+
console.info(`[ixmaps-gl] type flag "${flag}" on an aggregated PIE chart recognized, not ported yet — ignored`);
|
|
2827
|
+
}
|
|
2828
|
+
});
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
// BAR variants _buildBarLayers doesn't draw (the bars are drawn plain)
|
|
2833
|
+
if (isBarChart(flags)) {
|
|
2834
|
+
flags.forEach(flag => {
|
|
2835
|
+
if (BAR_INERT_FLAGS.some(re => re.test(flag)) && !_warnedFlags.has('BAR:' + flag)) {
|
|
2836
|
+
_warnedFlags.add('BAR:' + flag);
|
|
2837
|
+
console.info(`[ixmaps-gl] type flag "${flag}" on a BAR chart recognized, not ported yet — the bars are drawn plain`);
|
|
2838
|
+
}
|
|
2839
|
+
});
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
// VECTOR / BEZIER modifiers _buildVectorLayers doesn't draw
|
|
2843
|
+
if (isVectorChart(flags)) {
|
|
2844
|
+
flags.forEach(flag => {
|
|
2845
|
+
const why = VECTOR_INERT_FLAGS[flag];
|
|
2846
|
+
if (why && !_warnedFlags.has('VECTOR:' + flag)) {
|
|
2847
|
+
_warnedFlags.add('VECTOR:' + flag);
|
|
2848
|
+
console.info(`[ixmaps-gl] type flag "${flag}" on a VECTOR/BEZIER chart recognized, ${why}`);
|
|
2849
|
+
}
|
|
2850
|
+
});
|
|
2667
2851
|
}
|
|
2668
2852
|
|
|
2669
2853
|
const rawBinding = Object.assign({}, def.binding);
|
|
@@ -2726,6 +2910,9 @@ in vec4 vPieLineColor;
|
|
|
2726
2910
|
opacity: 1; transition: opacity 0.4s ease;
|
|
2727
2911
|
}
|
|
2728
2912
|
.ixmaps-splash.ixmaps-splash-hidden { opacity: 0; pointer-events: none; }
|
|
2913
|
+
.ixmaps-splash.ixmaps-splash-dark { background: #1a1a1a; }
|
|
2914
|
+
.ixmaps-splash-dark .ixmaps-splash-word { color: #e8e8e8; }
|
|
2915
|
+
.ixmaps-splash-dark .ixmaps-splash-text { color: #9a9a9a; }
|
|
2729
2916
|
.ixmaps-splash-mark { position: relative; width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; }
|
|
2730
2917
|
.ixmaps-splash-ping {
|
|
2731
2918
|
position: absolute; width: 16px; height: 16px; border-radius: 50%;
|
|
@@ -2761,15 +2948,21 @@ in vec4 vPieLineColor;
|
|
|
2761
2948
|
// flat's Dictionary.getLocalText (mapscript.js 5631-5648) over the page's
|
|
2762
2949
|
// .local(text, translation) pairs: the whole text, else word by word
|
|
2763
2950
|
// (a word's entry is written in quotes, 'word')
|
|
2951
|
+
// plus the page-wide entries of ixmaps.setLocalString (flat's
|
|
2952
|
+
// Api.setLocalString → Dictionary.replace, mapapi.js 123), read at call
|
|
2953
|
+
// time so strings set after the map is built count; the later
|
|
2954
|
+
// setLocalString wins over a .local() pair of the same text
|
|
2955
|
+
const _localStrings = Object.create(null);
|
|
2764
2956
|
function makeLocalText(locals) {
|
|
2765
2957
|
const dict = {};
|
|
2766
2958
|
(locals || []).forEach(a => {
|
|
2767
2959
|
if (a && typeof a[0] === 'object' && a[0]) Object.assign(dict, a[0]);
|
|
2768
2960
|
else if (a && a.length >= 2) dict[a[0]] = a[1];
|
|
2769
2961
|
});
|
|
2962
|
+
const entry = k => (_localStrings[k] !== undefined ? _localStrings[k] : dict[k]);
|
|
2770
2963
|
return text => {
|
|
2771
|
-
if (
|
|
2772
|
-
return String(text).split(' ').map(w => (
|
|
2964
|
+
if (entry(text) !== undefined) return String(entry(text));
|
|
2965
|
+
return String(text).split(' ').map(w => (entry("'" + w + "'") !== undefined ? String(entry("'" + w + "'")) : w)).join(' ');
|
|
2773
2966
|
};
|
|
2774
2967
|
}
|
|
2775
2968
|
// make the map container the positioning context of what the engine
|
|
@@ -2782,7 +2975,7 @@ in vec4 vPieLineColor;
|
|
|
2782
2975
|
function anchorInside(el) {
|
|
2783
2976
|
if (el && getComputedStyle(el).position === 'static') el.style.position = 'relative';
|
|
2784
2977
|
}
|
|
2785
|
-
function showSplash(el, text) {
|
|
2978
|
+
function showSplash(el, text, dark) {
|
|
2786
2979
|
ensureSplashStyle();
|
|
2787
2980
|
// Checks the actual RENDERED position, not el.style (the inline
|
|
2788
2981
|
// attribute only) — every example page's own #map_div already sets
|
|
@@ -2797,7 +2990,7 @@ in vec4 vPieLineColor;
|
|
|
2797
2990
|
// this at all.
|
|
2798
2991
|
anchorInside(el);
|
|
2799
2992
|
const splash = document.createElement('div');
|
|
2800
|
-
splash.className = 'ixmaps-splash';
|
|
2993
|
+
splash.className = 'ixmaps-splash' + (dark ? ' ixmaps-splash-dark' : '');
|
|
2801
2994
|
splash.innerHTML = '<div class="ixmaps-splash-mark"><span class="ixmaps-splash-ping"></span><span class="ixmaps-splash-dot"></span></div>' +
|
|
2802
2995
|
'<div class="ixmaps-splash-word">ixmaps<sup>gl</sup></div>' +
|
|
2803
2996
|
'<div class="ixmaps-splash-text"></div>';
|
|
@@ -3663,7 +3856,9 @@ in vec4 vPieLineColor;
|
|
|
3663
3856
|
// aggregation" reading (style.valuefield, falling back to
|
|
3664
3857
|
// the bound size field); anything else (plain CATEGORICAL,
|
|
3665
3858
|
// no SUM) falls back to a per-category record COUNT.
|
|
3666
|
-
|
|
3859
|
+
// a VECTOR / BEZIER flow legend sums its size field (flat's rows
|
|
3860
|
+
// show the flows' summed sizes, measured on the twin page)
|
|
3861
|
+
const useSum = flatFlag(rt.flags, 'SUM') && (rt.style.valuefield || ((rt._isVectorChart() || rt._isAggregatedPie()) && rt.binding.size));
|
|
3667
3862
|
const valueField = rt.style.valuefield || rt.binding.size;
|
|
3668
3863
|
// legendunits wins over the theme's general-purpose units
|
|
3669
3864
|
// (used elsewhere for tooltips, e.g. _renderItemChartHtml) —
|
|
@@ -3672,13 +3867,18 @@ in vec4 vPieLineColor;
|
|
|
3672
3867
|
// space injected), matching how style.units/legendunits are
|
|
3673
3868
|
// themselves authored with their own leading space (e.g.
|
|
3674
3869
|
// " MW") in real pages.
|
|
3675
|
-
|
|
3870
|
+
// (a VECTOR / BEZIER or aggregated PIE legend: flat's szUnit, the
|
|
3871
|
+
// units after a space unless they start with ".", measured on the
|
|
3872
|
+
// twin pages)
|
|
3873
|
+
const flatUnitLegend = rt._isVectorChart() || rt._isAggregatedPie();
|
|
3874
|
+
const rawLegendUnit = rt.style.legendunits || rt.style.units || '';
|
|
3875
|
+
const legendUnit = flatUnitLegend && rawLegendUnit && rawLegendUnit[0] !== '.' && rawLegendUnit[0] !== ' ' ? ' ' + rawLegendUnit : rawLegendUnit;
|
|
3676
3876
|
// a per-item chart over several value fields: the row values as
|
|
3677
3877
|
// flat's legend prints them (legend.js 610, 810: __formatValue
|
|
3678
3878
|
// with valuedecimals, else 2, "BLANK" — whole numbers without
|
|
3679
3879
|
// decimals), a colorscheme "none" part as an outline in linecolor
|
|
3680
3880
|
const multiFieldLegend = rt._isMultiFieldChart();
|
|
3681
|
-
const legendTotalText = v => (multiFieldLegend
|
|
3881
|
+
const legendTotalText = v => ((multiFieldLegend || flatUnitLegend)
|
|
3682
3882
|
? flatFormatValue(v, legendDecimals(rt.style), 'BLANK')
|
|
3683
3883
|
: rt._formatTooltipValue(v));
|
|
3684
3884
|
const outlineRgb = rt.style.linecolor && styleLineColor(rt.style.linecolor) !== 'none' ? hexOrNamedToRgb(styleLineColor(rt.style.linecolor)) : [128, 128, 128];
|
|
@@ -4238,6 +4438,11 @@ in vec4 vPieLineColor;
|
|
|
4238
4438
|
await null;
|
|
4239
4439
|
const el = document.getElementById(this.containerId);
|
|
4240
4440
|
if (!el) throw new Error(`[ixmaps-gl] container #${this.containerId} not found`);
|
|
4441
|
+
// flat's init replaces the container's content with its own map page
|
|
4442
|
+
// (mappage.html via innerHTML), so a page's placeholder inside the
|
|
4443
|
+
// map div ("connecting to Internet ...", the fuel-price page) goes
|
|
4444
|
+
// away; here it would stay on top of the map — remove it the same way
|
|
4445
|
+
while (el.firstChild) el.removeChild(el.firstChild);
|
|
4241
4446
|
// GL-PORT COMPAT: ixmaps.Map(id, {width, height}) — flat sizes its
|
|
4242
4447
|
// inner map div with these and gives PIXEL values to the page's own
|
|
4243
4448
|
// container too (htmlgui_flat.js:866-876, parent().css(...)); here
|
|
@@ -4248,6 +4453,11 @@ in vec4 vPieLineColor;
|
|
|
4248
4453
|
const v = this.mapOptions && this.mapOptions[dim];
|
|
4249
4454
|
if (typeof v === 'string' && /^\s*\d+(\.\d+)?\s*px\s*$/i.test(v)) el.style[dim] = v.trim();
|
|
4250
4455
|
else if (typeof v === 'number' && v > 0) el.style[dim] = v + 'px';
|
|
4456
|
+
// a percentage stays the page's CSS — unless that CSS left the
|
|
4457
|
+
// container collapsed on this side (e.g. an empty flex item, the
|
|
4458
|
+
// UNHCR page): flat's own pixel-sized map div fills it there
|
|
4459
|
+
else if (typeof v === 'string' && /^\s*\d+(\.\d+)?\s*%\s*$/.test(v)
|
|
4460
|
+
&& !(dim === 'width' ? el.clientWidth : el.clientHeight)) el.style[dim] = v.trim();
|
|
4251
4461
|
}
|
|
4252
4462
|
// without width/height flat's map is "fullscreen" (htmlgui_flat.js
|
|
4253
4463
|
// 847-876): it fills the window, whatever the page's CSS gives the
|
|
@@ -4263,15 +4473,29 @@ in vec4 vPieLineColor;
|
|
|
4263
4473
|
// overrides the default message, .options({splash:false}) skips it
|
|
4264
4474
|
// entirely (e.g. for a map embedded somewhere a full-cover overlay
|
|
4265
4475
|
// would be wrong, or one that's expected to load near-instantly).
|
|
4476
|
+
// dark on a dark map (no basemap style yet: flatLegendLook's name/
|
|
4477
|
+
// color rule, or a basemap faded to <= 0.5 over a dark page, as
|
|
4478
|
+
// effectiveMapDark) — a light sheet flashed between a dark page and
|
|
4479
|
+
// a dark map (the UNHCR page)
|
|
4480
|
+
const splashOpacity = parseFloat(this._engineOptions.basemapopacity);
|
|
4481
|
+
const splashLegendBg = this.mapOptions.legendBackground || this.mapOptions.legendbackground;
|
|
4482
|
+
const splashDark = flatLegendLook(this.mapOptions.mapType, isNaN(splashOpacity) ? 1 : splashOpacity, splashLegendBg).dark
|
|
4483
|
+
|| (splashOpacity <= 0.5 && flatIsDarkColor('rgb(' + pageBackgroundColorOf(el).map(Math.round).join(',') + ')'));
|
|
4266
4484
|
const splash = this._engineOptions.splash === false ? null
|
|
4267
|
-
: showSplash(el, this._engineOptions.splashText || 'loading…');
|
|
4485
|
+
: showSplash(el, this._engineOptions.splashText || 'loading…', splashDark);
|
|
4268
4486
|
|
|
4269
4487
|
// ixmaps.getBoundingBox()/setTitle() for brokers called below, before
|
|
4270
4488
|
// the MapLibre map exists (same default view as the map gets)
|
|
4271
4489
|
_titleHost = el;
|
|
4272
|
-
|
|
4490
|
+
// a flat SVG map in another projection (equalearth.svg): its view
|
|
4491
|
+
// framing and map scale (see FLAT_SVG_PROJECTIONS)
|
|
4492
|
+
const svgProjection = flatSvgProjectionOf(this.mapOptions.map);
|
|
4493
|
+
if (svgProjection) flatSvgProjectionByOptions.set(this._engineOptions, svgProjection);
|
|
4494
|
+
const [vLat, vLng] = this._viewCenter || [45.5, 9.2];
|
|
4495
|
+
const initialZoom = this._viewZoom != null && this._viewZoom !== ''
|
|
4496
|
+
? flatViewToMapLibreZoom(Number(this._viewZoom), Number(vLat), Number(vLng), el.clientWidth, el.clientHeight, svgProjection)
|
|
4497
|
+
: 8;
|
|
4273
4498
|
{
|
|
4274
|
-
const [vLat, vLng] = this._viewCenter || [45.5, 9.2];
|
|
4275
4499
|
_boundsSource = () => viewBounds(Number(vLat), Number(vLng), initialZoom, el.clientWidth, el.clientHeight);
|
|
4276
4500
|
}
|
|
4277
4501
|
|
|
@@ -4390,7 +4614,7 @@ in vec4 vPieLineColor;
|
|
|
4390
4614
|
style: mapTypeColor ? buildBlankBackgroundStyle(mapTypeColor)
|
|
4391
4615
|
: resolveBasemapStyleUrl(this.mapOptions.mapType),
|
|
4392
4616
|
center: [lon, lat],
|
|
4393
|
-
zoom:
|
|
4617
|
+
zoom: initialZoom,
|
|
4394
4618
|
// MapLibre's own AttributionControl (bottom right) keeps the
|
|
4395
4619
|
// basemap's required CARTO/OpenStreetMap credit; the page's own
|
|
4396
4620
|
// .attribution() goes bottom left, as flat shows it
|
|
@@ -4414,6 +4638,7 @@ in vec4 vPieLineColor;
|
|
|
4414
4638
|
// world once, at the cost of that zoom limit.
|
|
4415
4639
|
renderWorldCopies: this._engineOptions.worldcopies !== false
|
|
4416
4640
|
});
|
|
4641
|
+
engineOptionsByMap.set(map, this._engineOptions);
|
|
4417
4642
|
// the live-SVG pane USER charts render into (see chartHostFor) —
|
|
4418
4643
|
// registered here, keyed by this map's options object, so every
|
|
4419
4644
|
// runtime (including later loadProject/map.layer ones) finds it
|
|
@@ -4461,7 +4686,7 @@ in vec4 vPieLineColor;
|
|
|
4461
4686
|
if (typeof hook !== 'function' || !prev) return;
|
|
4462
4687
|
const p = map.project(c), q = map.project(prev.c);
|
|
4463
4688
|
if (z === prev.z && Math.abs(p.x - q.x) <= 10 && Math.abs(p.y - q.y) <= 10) return;
|
|
4464
|
-
try { hook.call(global.ixmaps,
|
|
4689
|
+
try { hook.call(global.ixmaps, flatViewZoomOf(map, z)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onZoomAndPan:', e); }
|
|
4465
4690
|
});
|
|
4466
4691
|
notifyZoomAndPanMidGesture = () => {
|
|
4467
4692
|
const hook = pageIxmaps() && pageIxmaps().htmlgui_onZoomAndPan;
|
|
@@ -4469,13 +4694,13 @@ in vec4 vPieLineColor;
|
|
|
4469
4694
|
const z = map.getZoom();
|
|
4470
4695
|
if (z === last.z) return; // a pan alone waits for 'moveend' (and its 10 px rule)
|
|
4471
4696
|
last = { z, c: map.getCenter() };
|
|
4472
|
-
try { hook.call(global.ixmaps,
|
|
4697
|
+
try { hook.call(global.ixmaps, flatViewZoomOf(map, z)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onZoomAndPan:', e); }
|
|
4473
4698
|
};
|
|
4474
4699
|
// flat calls it on the first draw too (its old zoom is unset then)
|
|
4475
4700
|
map.once('load', () => {
|
|
4476
4701
|
last = { z: map.getZoom(), c: map.getCenter() };
|
|
4477
4702
|
const hook = pageIxmaps() && pageIxmaps().htmlgui_onZoomAndPan;
|
|
4478
|
-
if (typeof hook === 'function') { try { hook.call(global.ixmaps,
|
|
4703
|
+
if (typeof hook === 'function') { try { hook.call(global.ixmaps, flatViewZoomOf(map, last.z)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onZoomAndPan:', e); } }
|
|
4479
4704
|
});
|
|
4480
4705
|
}
|
|
4481
4706
|
|
|
@@ -4505,10 +4730,12 @@ in vec4 vPieLineColor;
|
|
|
4505
4730
|
// carry the suffix; stripping a pattern that isn't there is a
|
|
4506
4731
|
// no-op. A theme's single circles (ix-bubbles-<name>-circles-gN,
|
|
4507
4732
|
// a ScatterplotLayer) route like its bubbles.
|
|
4508
|
-
const base = layerId.replace(/-g\d+$/, '').replace(/-circles$/, '');
|
|
4733
|
+
const base = layerId.replace(/-g\d+$/, '').replace(/-circles$/, '').replace(/^(ix-vector-heads-.*)-n\d+$/, '$1');
|
|
4509
4734
|
return runtimes.find(r => {
|
|
4510
4735
|
if (base === `ix-bubbles-${r.name}`) return isSymbolChart(r.flags);
|
|
4511
4736
|
if (base === `ix-pie-${r.name}`) return r._isPieChart();
|
|
4737
|
+
if (base === `ix-bar-${r.name}`) return r._isBarChart();
|
|
4738
|
+
if (base === `ix-vector-${r.name}` || base === `ix-vector-heads-${r.name}`) return r._isVectorChart();
|
|
4512
4739
|
if (base === `ix-dot-${r.name}`) return r.flags.has('DOT');
|
|
4513
4740
|
if (base === `ix-choropleth-${r.name}`) return r.flags.has('CHOROPLETH');
|
|
4514
4741
|
if (base === `ix-features-${r.name}`) return r.flags.has('FEATURE') || r.flags.has('FEATURES');
|
|
@@ -4665,7 +4892,7 @@ in vec4 vPieLineColor;
|
|
|
4665
4892
|
resize: () => { map.resize(); return engineApi; },
|
|
4666
4893
|
// broker calls in flight (themes whose data comes by name)
|
|
4667
4894
|
pendingLoads: () => pendingLoads,
|
|
4668
|
-
getZoom: () =>
|
|
4895
|
+
getZoom: () => flatViewZoomOf(map),
|
|
4669
4896
|
require: (url) => { loadRequiredScripts([url]); return engineApi; },
|
|
4670
4897
|
// flat's theme-level calls by theme id (style.name / meta.name, else
|
|
4671
4898
|
// the layer name): add(theme, flag) — "replace" swaps a theme of the
|
|
@@ -4938,7 +5165,7 @@ in vec4 vPieLineColor;
|
|
|
4938
5165
|
const z = isOpts ? latlonOrOpts.zoom : zoom;
|
|
4939
5166
|
if (c) {
|
|
4940
5167
|
const lngLat = Array.isArray(c) ? [c[1], c[0]] : [c.lng, c.lat];
|
|
4941
|
-
jumpLive(Object.assign({ center: lngLat }, z != null ? { zoom:
|
|
5168
|
+
jumpLive(Object.assign({ center: lngLat }, z != null ? { zoom: mapLibreZoomOfFlatView(map, Number(z), lngLat[1], lngLat[0]) } : {}));
|
|
4942
5169
|
}
|
|
4943
5170
|
return engineApi;
|
|
4944
5171
|
},
|
|
@@ -5345,6 +5572,12 @@ in vec4 vPieLineColor;
|
|
|
5345
5572
|
|
|
5346
5573
|
function applyProjectMap(m, f, report) {
|
|
5347
5574
|
const svg = String(m.map || '');
|
|
5575
|
+
// its view and map scale follow the project's map file, as on flat
|
|
5576
|
+
if (svg) {
|
|
5577
|
+
const svgProjection = flatSvgProjectionOf(svg);
|
|
5578
|
+
if (svgProjection) flatSvgProjectionByOptions.set(builder._engineOptions, svgProjection);
|
|
5579
|
+
else flatSvgProjectionByOptions.delete(builder._engineOptions);
|
|
5580
|
+
}
|
|
5348
5581
|
if (svg && !/generic\/(mercator|orthographic)\.svg$/i.test(svg)) report.notes.push(`flat SVG map "${svg.split('/').pop()}" is not available in ixmaps-gl — mercator used`);
|
|
5349
5582
|
if (typeof map.setProjection === 'function') {
|
|
5350
5583
|
try {
|
|
@@ -5365,8 +5598,8 @@ in vec4 vPieLineColor;
|
|
|
5365
5598
|
const c = m.center || {};
|
|
5366
5599
|
const lat = Number(c.lat), lng = Number(c.lng);
|
|
5367
5600
|
const z = m.zoom != null && m.zoom !== '' ? Number(m.zoom) : NaN;
|
|
5368
|
-
if (Number.isFinite(lat) && Number.isFinite(lng)) jumpLive(Object.assign({ center: [lng, lat] }, Number.isFinite(z) ? { zoom:
|
|
5369
|
-
else if (Number.isFinite(z)) jumpLive({ zoom:
|
|
5601
|
+
if (Number.isFinite(lat) && Number.isFinite(lng)) jumpLive(Object.assign({ center: [lng, lat] }, Number.isFinite(z) ? { zoom: mapLibreZoomOfFlatView(map, z, lat, lng) } : {}));
|
|
5602
|
+
else if (Number.isFinite(z)) jumpLive({ zoom: mapLibreZoomOfFlatView(map, z, map.getCenter().lat, map.getCenter().lng) });
|
|
5370
5603
|
}
|
|
5371
5604
|
// a project can't authorize its own scripts — trustedscripts is the page's
|
|
5372
5605
|
if (m.options && typeof m.options === 'object') {
|
|
@@ -6040,6 +6273,110 @@ in vec4 vPieLineColor;
|
|
|
6040
6273
|
const FLAT_ZOOM_OFFSET = 1;
|
|
6041
6274
|
const flatToMapLibreZoom = z => z - FLAT_ZOOM_OFFSET;
|
|
6042
6275
|
const mapLibreToFlatZoom = z => z + FLAT_ZOOM_OFFSET;
|
|
6276
|
+
|
|
6277
|
+
// flat's world SVG maps in a projection other than Mercator
|
|
6278
|
+
// (map:".../generic/equalearth.svg"), which this engine draws in Web
|
|
6279
|
+
// Mercator. Flat syncs such a map to its Leaflet view by fitting the
|
|
6280
|
+
// Leaflet bounds' SW/NE corners, projected, into the map
|
|
6281
|
+
// (htmlgui_sync.js setBoundsLatLon → mapscript2.js doSetMapToGeoBounds /
|
|
6282
|
+
// doZoomMapToArea): both axes fit (min) while the bounds span ≥ 180° of
|
|
6283
|
+
// longitude, else the x axis fits the width on its own (mapapi.js
|
|
6284
|
+
// doSetMapToGeoBounds drops fPreserveMapRatio) — so the same view zoom
|
|
6285
|
+
// frames these maps differently from Mercator (Equal Earth at 2 on
|
|
6286
|
+
// 1024×748: 1.55× the Mercator scale; at 4: 1.09×).
|
|
6287
|
+
// `project` is flat's own forward projection (mapscript.js
|
|
6288
|
+
// _LLtoEqualEarth, _LLtoWinkelTripel — Winkel's cos(50.467) in radians
|
|
6289
|
+
// included), in units of the earth radius.
|
|
6290
|
+
// `scaleConstant`: flat's map scale (nTrueMapScale · nZoomScale, the
|
|
6291
|
+
// scale its dynamic object scaling and gates use) × the screen pixels
|
|
6292
|
+
// per degree of longitude on the equator — constant per map file,
|
|
6293
|
+
// measured on flat (2026-10-09, view zooms 2-5): Equal Earth 170.1e6,
|
|
6294
|
+
// Winkel 195.45e6; for flat's Mercator map 442 913 385 · 256 / 360 =
|
|
6295
|
+
// 314.96e6 (FLAT_OBJECT_SCALE_CONSTANT). These SVG maps carry the
|
|
6296
|
+
// Mercator map's mapscale (177 165 354), so at the same on-screen size
|
|
6297
|
+
// flat calls their scale 0.54× (0.62×) the Mercator one, and its
|
|
6298
|
+
// symbols are drawn larger by that ratio^(1/dynamicScalePow). Framing
|
|
6299
|
+
// matches flat's pixels per degree of longitude at the view's center;
|
|
6300
|
+
// the scale gates (scaleDenominatorAt) still use the Mercator scale.
|
|
6301
|
+
const FLAT_MERCATOR_SCALE_CONSTANT = FLAT_OBJECT_SCALE_CONSTANT * 256 / 360;
|
|
6302
|
+
const FLAT_SVG_PROJECTIONS = {
|
|
6303
|
+
equalearth: {
|
|
6304
|
+
svg: 'maps/svg/maps/generic/equalearth.svg',
|
|
6305
|
+
scaleConstant: 170.1e6,
|
|
6306
|
+
project: (lat, lon) => {
|
|
6307
|
+
const M = Math.sqrt(3) / 2, A1 = 1.340264, A2 = -0.081106, A3 = 0.000893, A4 = 0.003796;
|
|
6308
|
+
const l = Math.asin(M * Math.sin(lat * Math.PI / 180)), l2 = l * l, l6 = l2 * l2 * l2;
|
|
6309
|
+
return [lon * Math.PI / 180 * Math.cos(l) / (M * (A1 + 3 * A2 * l2 + l6 * (7 * A3 + 9 * A4 * l2))), l * (A1 + A2 * l2 + l6 * (A3 + A4 * l2))];
|
|
6310
|
+
}
|
|
6311
|
+
},
|
|
6312
|
+
winkel: {
|
|
6313
|
+
svg: 'maps/svg/maps/generic/winkel.svg',
|
|
6314
|
+
scaleConstant: 195.45e6,
|
|
6315
|
+
project: (lat, lon) => {
|
|
6316
|
+
const p = lat * Math.PI / 180, q = Math.max(-180, Math.min(180, lon)) * Math.PI / 360;
|
|
6317
|
+
const D = Math.acos(Math.cos(p) * Math.cos(q));
|
|
6318
|
+
let x1 = 0, y1 = 0;
|
|
6319
|
+
if (D !== 0) { const C = Math.sin(p) / Math.sin(D); x1 = (q < 0 ? -2 : 2) * D * Math.sqrt(1 - C * C); y1 = D * C; }
|
|
6320
|
+
return [0.5 * (x1 + 2 * q * Math.cos(50.467)), 0.5 * (y1 + p)];
|
|
6321
|
+
}
|
|
6322
|
+
}
|
|
6323
|
+
};
|
|
6324
|
+
// the flat SVG map projection of a map:/project map URL (generic file
|
|
6325
|
+
// name), null for Mercator and every map drawn as is (orthographic → globe)
|
|
6326
|
+
function flatSvgProjectionOf(svgUrl) {
|
|
6327
|
+
const m = /(?:^|\/)(equalearth|winkel)\.svgz?(?:[?#].*)?$/i.exec(String(svgUrl || ''));
|
|
6328
|
+
return m ? FLAT_SVG_PROJECTIONS[m[1].toLowerCase()] : null;
|
|
6329
|
+
}
|
|
6330
|
+
// the flat SVG projection a map's options object belongs to (set by the
|
|
6331
|
+
// map's init and loadProject; the runtimes only see the options object)
|
|
6332
|
+
const flatSvgProjectionByOptions = new WeakMap();
|
|
6333
|
+
// flat SVG map scale ÷ the Mercator one at the same on-screen size (1
|
|
6334
|
+
// without a flat SVG projection)
|
|
6335
|
+
function flatMapScaleRatio(mapOptions) {
|
|
6336
|
+
const proj = mapOptions && typeof mapOptions === 'object' ? flatSvgProjectionByOptions.get(mapOptions) : null;
|
|
6337
|
+
return proj ? proj.scaleConstant / FLAT_MERCATOR_SCALE_CONSTANT : 1;
|
|
6338
|
+
}
|
|
6339
|
+
// a flat view zoom → the MapLibre zoom showing what flat shows at it:
|
|
6340
|
+
// under a flat SVG projection the same screen pixels per degree of
|
|
6341
|
+
// longitude at the center latitude as flat's fitted map (see
|
|
6342
|
+
// FLAT_SVG_PROJECTIONS), else flatToMapLibreZoom
|
|
6343
|
+
function flatViewToMapLibreZoom(flatZoom, lat, lng, width, height, proj) {
|
|
6344
|
+
if (!proj || !(width > 0) || !(height > 0) || !Number.isFinite(lat) || !Number.isFinite(lng)) return flatToMapLibreZoom(flatZoom);
|
|
6345
|
+
// flat's Leaflet bounds (256 px tiles) at this zoom
|
|
6346
|
+
const [sw, ne] = viewBounds(lat, lng, flatToMapLibreZoom(flatZoom), width, height);
|
|
6347
|
+
const a = proj.project(sw.lat, sw.lng), b = proj.project(ne.lat, ne.lng);
|
|
6348
|
+
const kx = width / Math.abs(b[0] - a[0]), ky = height / Math.abs(b[1] - a[1]);
|
|
6349
|
+
const k = ne.lng - sw.lng < 180 ? kx : Math.min(kx, ky);
|
|
6350
|
+
const pxPerDegree = k * (proj.project(lat, lng + 0.5)[0] - proj.project(lat, lng - 0.5)[0]);
|
|
6351
|
+
return Math.log2(pxPerDegree * 360 / 512);
|
|
6352
|
+
}
|
|
6353
|
+
// the inverse: the flat view zoom of a MapLibre zoom (bisection — the
|
|
6354
|
+
// fit above is monotonic in the zoom)
|
|
6355
|
+
function mapLibreToFlatViewZoom(mapLibreZoom, lat, lng, width, height, proj) {
|
|
6356
|
+
if (!proj || !(width > 0) || !(height > 0) || !Number.isFinite(lat) || !Number.isFinite(lng)) return mapLibreToFlatZoom(mapLibreZoom);
|
|
6357
|
+
let lo = mapLibreToFlatZoom(mapLibreZoom) - 4, hi = mapLibreToFlatZoom(mapLibreZoom) + 4;
|
|
6358
|
+
for (let i = 0; i < 40; i++) {
|
|
6359
|
+
const mid = (lo + hi) / 2;
|
|
6360
|
+
if (flatViewToMapLibreZoom(mid, lat, lng, width, height, proj) < mapLibreZoom) lo = mid; else hi = mid;
|
|
6361
|
+
}
|
|
6362
|
+
return (lo + hi) / 2;
|
|
6363
|
+
}
|
|
6364
|
+
// a live MapLibre map's options object (its runtimes' mapOptions), for
|
|
6365
|
+
// the conversions below
|
|
6366
|
+
const engineOptionsByMap = new WeakMap();
|
|
6367
|
+
function mapSvgProjection(map) {
|
|
6368
|
+
const options = engineOptionsByMap.get(map);
|
|
6369
|
+
return (options && flatSvgProjectionByOptions.get(options)) || null;
|
|
6370
|
+
}
|
|
6371
|
+
// the flat view zoom a live map shows / the MapLibre zoom of a flat view
|
|
6372
|
+
function flatViewZoomOf(map, mapLibreZoom) {
|
|
6373
|
+
const c = map.getCenter(), el = map.getContainer();
|
|
6374
|
+
return mapLibreToFlatViewZoom(mapLibreZoom == null ? map.getZoom() : mapLibreZoom, c.lat, c.lng, el.clientWidth, el.clientHeight, mapSvgProjection(map));
|
|
6375
|
+
}
|
|
6376
|
+
function mapLibreZoomOfFlatView(map, flatZoom, lat, lng) {
|
|
6377
|
+
const el = map.getContainer();
|
|
6378
|
+
return flatViewToMapLibreZoom(flatZoom, lat, lng, el.clientWidth, el.clientHeight, mapSvgProjection(map));
|
|
6379
|
+
}
|
|
6043
6380
|
// map scale denominator at a MapLibre zoom (null → the default reference):
|
|
6044
6381
|
// flat's own map scale, the one its scale bar shows and every scale gate
|
|
6045
6382
|
// (featureupper, chartupper, boxupper, valueupper, aggregation brackets)
|
|
@@ -6064,9 +6401,20 @@ in vec4 vPieLineColor;
|
|
|
6064
6401
|
// (e.g. "fixed") means they don't — same pixel size at every zoom.
|
|
6065
6402
|
// the zoom at which flat's object scale equals normalSizeScale (symbols
|
|
6066
6403
|
// at their normal size) — see FLAT_OBJECT_SCALE_CONSTANT
|
|
6404
|
+
// (under a flat SVG projection, flat's scale at that zoom is
|
|
6405
|
+
// flatMapScaleRatio × the Mercator one)
|
|
6067
6406
|
function resolveZoomReference(mapOptions) {
|
|
6068
|
-
const scaleDenominator = parseFloat(mapOptions
|
|
6069
|
-
return flatToMapLibreZoom(Math.log2(FLAT_OBJECT_SCALE_CONSTANT / scaleDenominator));
|
|
6407
|
+
const scaleDenominator = parseFloat(normalSizeScaleOf(mapOptions)) || FLAT_DEFAULT_NORMAL_SIZE_SCALE;
|
|
6408
|
+
return flatToMapLibreZoom(Math.log2(FLAT_OBJECT_SCALE_CONSTANT * flatMapScaleRatio(mapOptions) / scaleDenominator));
|
|
6409
|
+
}
|
|
6410
|
+
// .options({normalSizeScale}) under any spelling: flat matches the key
|
|
6411
|
+
// case-insensitively (htmlgui.js, /normalSizeScale/i → setScaleParam;
|
|
6412
|
+
// the last matching key wins), e.g. "normalsizescale"
|
|
6413
|
+
function normalSizeScaleOf(mapOptions) {
|
|
6414
|
+
if (!mapOptions || typeof mapOptions !== 'object') return undefined;
|
|
6415
|
+
let value;
|
|
6416
|
+
for (const key of Object.keys(mapOptions)) if (/normalSizeScale/i.test(key)) value = mapOptions[key];
|
|
6417
|
+
return value;
|
|
6070
6418
|
}
|
|
6071
6419
|
|
|
6072
6420
|
// .options({dynamicScalePow}) — the REAL parameter name (confirmed in
|
|
@@ -6139,7 +6487,7 @@ in vec4 vPieLineColor;
|
|
|
6139
6487
|
// scale (resolveZoomReference); flat's real rule (maptheme.js 13408,
|
|
6140
6488
|
// 0.3 + 0.7 / ln(nZoom / dx)) is not ported yet
|
|
6141
6489
|
function autoOpacityZoomReference(mapOptions) {
|
|
6142
|
-
const scaleDenominator = parseFloat(mapOptions
|
|
6490
|
+
const scaleDenominator = parseFloat(normalSizeScaleOf(mapOptions));
|
|
6143
6491
|
if (!scaleDenominator) return flatToMapLibreZoom(DEFAULT_ZOOM_REFERENCE);
|
|
6144
6492
|
return flatToMapLibreZoom(Math.log2(WEBMERCATOR_SCALE_CONSTANT / scaleDenominator));
|
|
6145
6493
|
}
|
|
@@ -6474,6 +6822,12 @@ in vec4 vPieLineColor;
|
|
|
6474
6822
|
|
|
6475
6823
|
// the themes drawn by the symbol chart pipeline (_buildChartLayers): flat's
|
|
6476
6824
|
// BUBBLE/SQUARE/LABEL branch (maptheme.js 20647-20652), symbols, user charts
|
|
6825
|
+
// CHART|VECTOR / CHART|BEZIER — flat's flow lines (maptheme.js chartMap
|
|
6826
|
+
// 18148-18435, ahead of drawChart: they take precedence over PIE,
|
|
6827
|
+
// BUBBLE, SYMBOL ...; DOT's branch comes first)
|
|
6828
|
+
function isVectorChart(flags) {
|
|
6829
|
+
return flags.has('CHART') && (flags.has('VECTOR') || flags.has('BEZIER')) && !flags.has('DOT');
|
|
6830
|
+
}
|
|
6477
6831
|
function isSymbolChart(flags) {
|
|
6478
6832
|
return flags.has('CHART') && (flags.has('SYMBOL') || flags.has('USER') || flags.has('LABEL'));
|
|
6479
6833
|
}
|
|
@@ -6509,6 +6863,17 @@ in vec4 vPieLineColor;
|
|
|
6509
6863
|
// the item's radius, SYMBOL: the theme's normal radius)
|
|
6510
6864
|
// So "left" puts the chart's left edge o to the right of the point (and
|
|
6511
6865
|
// drops offsetx), "2left" one more h. Returns deck.gl's pixel offset.
|
|
6866
|
+
// flat's maxcharts (maptheme.js 16879-16885): chartMap sorts the charts
|
|
6867
|
+
// ascending (the biggest drawn last, on top) and skips the first
|
|
6868
|
+
// nToDraw − nMaxCharts, so only the biggest maxcharts are drawn — for
|
|
6869
|
+
// every chart type (symbols, pies, bars, VECTOR/BEZIER). In place, on the
|
|
6870
|
+
// already sorted list of charts to draw
|
|
6871
|
+
function applyFlatMaxCharts(list, style) {
|
|
6872
|
+
const n = Math.round(styleNum(style && style.maxcharts)) || 0;
|
|
6873
|
+
if (n > 0 && list.length > n) list.splice(0, list.length - n);
|
|
6874
|
+
return list;
|
|
6875
|
+
}
|
|
6876
|
+
|
|
6512
6877
|
function flatChartAlignOffset(style, g) {
|
|
6513
6878
|
const num = v => (isNaN(styleNum(v)) ? 0 : styleNum(v));
|
|
6514
6879
|
const sym = g.symbolScale || 1;
|
|
@@ -6551,6 +6916,53 @@ in vec4 vPieLineColor;
|
|
|
6551
6916
|
}
|
|
6552
6917
|
return [r, g, b].map(c => Math.min(255, Math.floor(c * f)));
|
|
6553
6918
|
}
|
|
6919
|
+
// A BUBBLE chart's look in flat (maptheme.js, the BUBBLE branch), for a
|
|
6920
|
+
// circle of screen radius r in the class color rgb, `unit` = flat's
|
|
6921
|
+
// normalX(1) on screen (the object zoom factor × style.scale):
|
|
6922
|
+
// - stroke (20740-20780, 20895): style.linecolor, else ChartColors.lowColor
|
|
6923
|
+
// (the color × 0.7, __maptheme_getChartColors) — none with NOLINES or
|
|
6924
|
+
// linecolor "none"
|
|
6925
|
+
// - width (20898-20903): linewidth (0.1) · unit · √(r / normal radius),
|
|
6926
|
+
// the normal radius 15 · unit; OUTLINE: unit · min(1, r / normal radius)
|
|
6927
|
+
// - fill-opacity fillopacity (1); stroke-opacity (21050-21056) 1 with
|
|
6928
|
+
// OUTLINE or a linecolor, else 1 below fillopacity 0.5, 0.3 above
|
|
6929
|
+
// (_buildLabelChartLayers spells the same rule out for CHART|LABEL's box.)
|
|
6930
|
+
function flatBubbleLook(rgb, r, unit, style, flags) {
|
|
6931
|
+
const fillOpacity = styleNum(style.fillopacity) || 1;
|
|
6932
|
+
const outline = flatFlag(flags, 'OUTLINE');
|
|
6933
|
+
const lineColor = style.linecolor ? styleLineColor(style.linecolor) : null;
|
|
6934
|
+
const stroke = lineColor ? (lineColor === 'none' ? null : hexOrNamedToRgb(lineColor))
|
|
6935
|
+
: flatFlag(flags, 'NOLINES') ? null : flatDerivateRgb(rgb, 0.7);
|
|
6936
|
+
const ratio = r / (NORMAL_RADIUS_PX * unit);
|
|
6937
|
+
const width = outline ? unit * Math.min(1, ratio) : unit * (styleNum(style.linewidth) || 0.1) * Math.sqrt(ratio);
|
|
6938
|
+
const strokeOpacity = outline || lineColor ? 1 : (fillOpacity < 0.5 ? 1 : 0.3);
|
|
6939
|
+
return { fillOpacity, stroke, strokeOpacity, width };
|
|
6940
|
+
}
|
|
6941
|
+
|
|
6942
|
+
// flatBubbleLook for a cached icon raster `size` canvas px wide, drawn 2r
|
|
6943
|
+
// screen px wide: the stroke width in canvas px, in quarter-octave steps
|
|
6944
|
+
// (≤ 19 % apart, at most the icon's radius) — every distinct width is
|
|
6945
|
+
// another icon in deck.gl's shared atlas (see ICON_ATLAS_RESET_AFTER), and
|
|
6946
|
+
// a continuous width would make one per bubble. It hardly depends on the
|
|
6947
|
+
// zoom: r and the width both scale with the object zoom factor. `stroke`
|
|
6948
|
+
// only when it is style.linecolor (a packed icon derives each part's own,
|
|
6949
|
+
// see _buildBubbleIcon), noStroke for NOLINES / linecolor "none";
|
|
6950
|
+
// outlineRatio (OUTLINE): r / normal radius, also stepped, as 1 from 1 up
|
|
6951
|
+
// (where flat's OUTLINE width stops growing).
|
|
6952
|
+
function bubbleIconLook(r, unit, style, flags, size) {
|
|
6953
|
+
const look = flatBubbleLook([0, 0, 0], r, unit, style, flags);
|
|
6954
|
+
const step = v => (v > 0 ? Math.round(Math.pow(2, Math.round(Math.log2(v) * 4) / 4) * 1000) / 1000 : 0);
|
|
6955
|
+
const out = {
|
|
6956
|
+
fillOpacity: look.fillOpacity,
|
|
6957
|
+
strokeOpacity: look.strokeOpacity,
|
|
6958
|
+
stroke: style.linecolor ? look.stroke : null,
|
|
6959
|
+
noStroke: !look.stroke,
|
|
6960
|
+
width: r > 0 ? Math.min(size / 2, step(look.width * size / (2 * r))) : 0,
|
|
6961
|
+
};
|
|
6962
|
+
if (flatFlag(flags, 'OUTLINE')) out.outlineRatio = Math.min(1, step(r / (NORMAL_RADIUS_PX * unit)));
|
|
6963
|
+
return out;
|
|
6964
|
+
}
|
|
6965
|
+
|
|
6554
6966
|
// ChartColors.textColor (colorscheme.js 104-116): darker on light colors
|
|
6555
6967
|
function flatChartTextRgb(rgb) {
|
|
6556
6968
|
return flatDerivateRgb(rgb, rgb[0] + rgb[1] + rgb[2] > 450 ? 0.6 : 3);
|
|
@@ -7269,6 +7681,412 @@ in vec4 vPieLineColor;
|
|
|
7269
7681
|
return [lngLat[0] + offset[0] * 360 / world, lat];
|
|
7270
7682
|
}
|
|
7271
7683
|
|
|
7684
|
+
// CHART|BAR / BARS: which flat draws as a bar chart — drawChart's BAR
|
|
7685
|
+
// branch (maptheme.js 23283) comes after USER, PIE, WAFFLE, BUBBLE,
|
|
7686
|
+
// SYMBOL and BUFFER; flat tests /BAR/ as a substring (BARS too)
|
|
7687
|
+
function isBarChart(flags) {
|
|
7688
|
+
if (!flags.has('CHART') || isVectorChart(flags) || flags.has('DOT')) return false;
|
|
7689
|
+
const t = flags.typeString != null ? flags.typeString : [...flags].join('|');
|
|
7690
|
+
return /BAR/.test(t) && !/\bUSER\b|PIE|WAFFLE|BUBBLE|SYMBOL|BUFFER/.test(t);
|
|
7691
|
+
}
|
|
7692
|
+
|
|
7693
|
+
// One item's bar chart as real ixmaps-flat draws it — maptheme.js
|
|
7694
|
+
// drawChart's BAR branch (23283-24425) — in flat's normal units (u:
|
|
7695
|
+
// normalX(1), the chart size is 30 u; px = u · the object zoom factor ×
|
|
7696
|
+
// style.scale), y down, relative to the item's position. `values`: the
|
|
7697
|
+
// item's parts (nValuesA, after aggregation); `t`: the type string,
|
|
7698
|
+
// tested as flat does, by substring. `o`:
|
|
7699
|
+
// themeMin / themeMax the theme's nMin / nMax (the items' sizes with a
|
|
7700
|
+
// size field or CATEGORICAL, else their parts)
|
|
7701
|
+
// maxSize, itemSize the theme's nMaxSize, the item's nSize (size field)
|
|
7702
|
+
// sizeField a size field is bound
|
|
7703
|
+
// normalSizeValue, sizePow, rangeScale, valueScale, units,
|
|
7704
|
+
// valueDecimals (explicit, else undefined), noBreaks, gridX, nClasses
|
|
7705
|
+
// (partsA.length), classMax (partsA[k].max), nColors, fillOpacity,
|
|
7706
|
+
// lineWidth, lineColor (linecolor, else undefined), fadeNegative,
|
|
7707
|
+
// fadeValuePow, hideValues (valueupper/lower), means / medians / devs
|
|
7708
|
+
// (per part, OFFSETMEAN / OFFSETMEDIAN / DEVIATION)
|
|
7709
|
+
// measure(text, font) the text's width at that font size (u)
|
|
7710
|
+
// Returns null when flat draws no chart (no size, or a single 0 part),
|
|
7711
|
+
// else { bars, texts, lines, extent: [x0, y0, x1, y1] } —
|
|
7712
|
+
// bars: { part (the value's index), cls (its color index), base
|
|
7713
|
+
// [x, y] (the middle of the shaft's foot), dir (0 up, 1 right,
|
|
7714
|
+
// 2 down, 3 left), w2 (half width), sw (shaft half width), len
|
|
7715
|
+
// (shaft), band, head, value, textValue, fill: { opacity },
|
|
7716
|
+
// line: { rgb: 'color' (the bar's) | null (lineColor or black),
|
|
7717
|
+
// width, opacity }, opacity (FADEIN / FADENEGATIVE) }
|
|
7718
|
+
// texts: { text, x, y (the baseline's start), angle (0 reading right,
|
|
7719
|
+
// 90 up, -90 down), font, opacity, color ('value' — flat's
|
|
7720
|
+
// #798697 values color, 'red', 'contrast' — ChartColors
|
|
7721
|
+
// .textColor of the bar's color —, or a CSS color), bg (null,
|
|
7722
|
+
// 'white' — #fefeff —, or 'bar' — the bar's color at 0.5),
|
|
7723
|
+
// cls, part }
|
|
7724
|
+
// lines: STACKED leader lines { x0, y0, x1, y1 }
|
|
7725
|
+
// Divergences (not drawn here): 3D, VOLUME, COLUMN's cylinders, DOTTED,
|
|
7726
|
+
// SMALLARROW (drawn as ARROW), TRENDLINE, the XAXIS/AXIS labels, the
|
|
7727
|
+
// mean/median marks, CLIP / MORPH frames, showparts / clipparts, ZOOM /
|
|
7728
|
+
// NORMSIZE / MENUSIZE, INVERT, DIAGLEFT / DIAGRIGHT — bars are drawn
|
|
7729
|
+
// plain instead (see BAR_INERT_FLAGS).
|
|
7730
|
+
function flatBarLayout(values, t, o) {
|
|
7731
|
+
t = String(t || '');
|
|
7732
|
+
const has = re => re.test(t);
|
|
7733
|
+
const n = values.length;
|
|
7734
|
+
if (!n) return null;
|
|
7735
|
+
const parts = values.map(v => (isFinite(v) ? v : 0));
|
|
7736
|
+
const sum = parts.reduce((a, v) => a + v, 0);
|
|
7737
|
+
const stacked = has(/STACKED/), pointer = has(/POINTER/), horz = has(/HOR/);
|
|
7738
|
+
const sizeFlag = has(/SIZE/) && !has(/NORMSIZE/);
|
|
7739
|
+
const sizePow = o.sizePow || 2;
|
|
7740
|
+
const CHART = 30;
|
|
7741
|
+
let nMax = Math.max(o.themeMax, Math.abs(o.themeMin));
|
|
7742
|
+
let nRange = o.themeMax - o.themeMin;
|
|
7743
|
+
let maxSize = o.maxSize;
|
|
7744
|
+
if (o.normalSizeValue) nRange = maxSize = o.normalSizeValue;
|
|
7745
|
+
// flat's getChartSize (no chart when 0) and its "a single 0" rule
|
|
7746
|
+
if (sizeFlag && !has(/NOSIZE/)) {
|
|
7747
|
+
const s = o.sizeField ? o.itemSize : Math.abs(sum);
|
|
7748
|
+
const ref = o.sizeField ? maxSize : nRange;
|
|
7749
|
+
const size = has(/SIZELOG/) ? Math.max(1, 15 / Math.log(ref) * Math.log(s)) : 15 / Math.pow(ref, 1 / sizePow) * Math.pow(s, 1 / sizePow);
|
|
7750
|
+
if (isNaN(size) || size === 0) return null;
|
|
7751
|
+
}
|
|
7752
|
+
if (n === 1 && parts[0] === 0 && !has(/ZEROISVALUE|AUTOCOMPLETE|CATEGORICAL/)) return null;
|
|
7753
|
+
|
|
7754
|
+
// the sizer: the chart's size by the size field or by the values' sum
|
|
7755
|
+
let sizer = 1;
|
|
7756
|
+
if (sizeFlag && o.sizeField && o.itemSize != null) {
|
|
7757
|
+
sizer = has(/SIZELOG/) ? 1 / Math.max(1, Math.log(maxSize) * Math.log(o.itemSize))
|
|
7758
|
+
: Math.pow(o.itemSize, 1 / sizePow) / Math.pow(maxSize, 1 / sizePow);
|
|
7759
|
+
} else if (sizeFlag) {
|
|
7760
|
+
const s = pointer ? sum / n : sum;
|
|
7761
|
+
sizer = has(/SIZELOG/) ? 1 / Math.max(1, Math.log(nRange) * Math.log(Math.abs(s)))
|
|
7762
|
+
: Math.pow(Math.abs(s), 1 / sizePow) / Math.pow(nRange, 1 / sizePow);
|
|
7763
|
+
}
|
|
7764
|
+
if (sizer === 0 && sum !== 0) sizer = 1;
|
|
7765
|
+
const chartSize = CHART * sizer;
|
|
7766
|
+
|
|
7767
|
+
// bar width and value text size
|
|
7768
|
+
let W = chartSize / n * 0.66;
|
|
7769
|
+
if (stacked) {
|
|
7770
|
+
W = chartSize / 3;
|
|
7771
|
+
if (o.gridX && !has(/MULTI/)) W /= 2;
|
|
7772
|
+
}
|
|
7773
|
+
let f = W < 5 ? W * 4 / 5 : W;
|
|
7774
|
+
let textOpacity = 1;
|
|
7775
|
+
if (has(/COLUMN/)) W *= has(/THICK/) ? 2 : has(/THIN/) ? 1 : 1.5;
|
|
7776
|
+
if (has(/THIN/)) { W *= 0.6; f *= 0.75; }
|
|
7777
|
+
if (has(/THICK/)) { W *= 1.5; f *= 1.5; }
|
|
7778
|
+
if (stacked) f = 5;
|
|
7779
|
+
if (stacked && !has(/ZOOM/)) f *= sizer;
|
|
7780
|
+
const valueScale = o.valueScale || 1;
|
|
7781
|
+
f *= valueScale;
|
|
7782
|
+
|
|
7783
|
+
// the bar length per value unit (nStep)
|
|
7784
|
+
if (o.normalSizeValue && (!o.field100 || pointer)) nMax = o.normalSizeValue;
|
|
7785
|
+
let step0 = CHART / nMax;
|
|
7786
|
+
if (has(/OFFSETMEAN|OFFSETMEDIAN|DEVIATION/)) step0 = chartSize / 300;
|
|
7787
|
+
if (sizeFlag && has(/SEQUENCE/) && n > 1 && !o.sizeField && !o.field100) step0 = step0 * n / sizer;
|
|
7788
|
+
else if (sizeFlag && n > 1 && !has(/EXPAND/)) {
|
|
7789
|
+
if (o.field100 || pointer) step0 *= sizer;
|
|
7790
|
+
else step0 /= has(/SIZEP4/) ? sizer * sizer : sizer;
|
|
7791
|
+
}
|
|
7792
|
+
if (o.rangeScale) step0 *= o.rangeScale;
|
|
7793
|
+
if (has(/COMPRESSMAX/)) step0 /= 4;
|
|
7794
|
+
else if (has(/COMPRESSMORE/)) step0 /= 3;
|
|
7795
|
+
else if (has(/COMPRESS/)) step0 /= 2;
|
|
7796
|
+
else if (has(/EXPANDMAX/)) step0 *= 4;
|
|
7797
|
+
else if (has(/EXPANDMORE/)) step0 *= 3;
|
|
7798
|
+
else if (has(/EXPAND/)) step0 *= 2;
|
|
7799
|
+
if (pointer && !has(/NORMSIZE/)) step0 *= 1.2;
|
|
7800
|
+
|
|
7801
|
+
// the bar order: SORT descending (STACKED ascending), UP reversed
|
|
7802
|
+
let sorted = null;
|
|
7803
|
+
if (has(/\bSORT\b/)) {
|
|
7804
|
+
sorted = parts.map((v, i) => ({ i, v }));
|
|
7805
|
+
sorted.sort(stacked ? (a, b) => a.v - b.v : (a, b) => b.v - a.v);
|
|
7806
|
+
sorted = sorted.map(s => s.i);
|
|
7807
|
+
}
|
|
7808
|
+
const startI = has(/\bUP\b/) ? n - 1 : 0;
|
|
7809
|
+
const nC = has(/CENTER/) ? 2 : 1;
|
|
7810
|
+
const fmt = (v, d) => flatGroupedValue(v, d, o.noBreaks);
|
|
7811
|
+
const measure = o.measure || ((text, font) => text.length * font * 0.55);
|
|
7812
|
+
|
|
7813
|
+
const bars = [], texts = [], lines = [];
|
|
7814
|
+
let posX = 0, posY = 0, minNext = 0, barsDrawn = 0, step = step0;
|
|
7815
|
+
for (let i = 0; i < n; i++) {
|
|
7816
|
+
step = step0;
|
|
7817
|
+
let idx = Math.abs(startI - i);
|
|
7818
|
+
if (sorted) idx = sorted[idx];
|
|
7819
|
+
let v = parts[idx];
|
|
7820
|
+
let tv = v;
|
|
7821
|
+
if (has(/OFFSETMEAN|OFFSETMEDIAN/)) {
|
|
7822
|
+
const ref = has(/OFFSETMEDIAN/) ? (o.medians || [])[idx] : (o.means || [])[idx];
|
|
7823
|
+
v = 100 / ref * v - 100;
|
|
7824
|
+
v = isFinite(v) ? v : 0;
|
|
7825
|
+
tv = v;
|
|
7826
|
+
nMax = 100;
|
|
7827
|
+
} else if (has(/DEVIATION/)) {
|
|
7828
|
+
v = (v - (o.means || [])[idx]) / (o.devs || [])[idx];
|
|
7829
|
+
v = isFinite(v) ? v : 0;
|
|
7830
|
+
tv = v;
|
|
7831
|
+
v *= 100;
|
|
7832
|
+
}
|
|
7833
|
+
// the color: the part's, or (SEQUENCE, a single part) the class of
|
|
7834
|
+
// the value
|
|
7835
|
+
let cls = o.nColors ? (idx < o.nColors ? idx : idx % (o.gridX || 1000000)) : idx;
|
|
7836
|
+
if ((has(/SEQUENCE|CLIP/) || n === 1) && o.nColors >= 2 && o.classMax && o.classMax.length) {
|
|
7837
|
+
cls = o.classMax.length - 1;
|
|
7838
|
+
for (let k = 0; k < o.classMax.length; k++) if (v < o.classMax[k]) { cls = k; break; }
|
|
7839
|
+
}
|
|
7840
|
+
if (has(/LEFT/) && has(/HORZ/)) v = -v;
|
|
7841
|
+
if (v < 0 && !stacked) { posY = -v * step; v = -v; }
|
|
7842
|
+
if (pointer && sizeFlag) {
|
|
7843
|
+
// flat's sized pointer: its width by the value (the WIDTH variant
|
|
7844
|
+
// by value100 is not ported)
|
|
7845
|
+
W = chartSize * Math.pow(Math.abs(v / nMax), 1 / 3);
|
|
7846
|
+
f = W * 0.6;
|
|
7847
|
+
if (!has(/ZOOM|XAXIS/)) {
|
|
7848
|
+
textOpacity = Math.pow(Math.abs(v), 1 / 2) / Math.pow(o.themeMax, 1 / 2);
|
|
7849
|
+
if (o.fadeValuePow) textOpacity = Math.pow(Math.abs(v), o.fadeValuePow) / Math.pow(nMax, o.fadeValuePow);
|
|
7850
|
+
}
|
|
7851
|
+
}
|
|
7852
|
+
if (has(/DTEXT/) && !has(/ZOOM/)) {
|
|
7853
|
+
f = (chartSize * 2 / 3) * Math.pow(Math.abs(v / nMax), 1 / 2) * 0.75;
|
|
7854
|
+
textOpacity = Math.pow(Math.abs(v), 1 / 3) / Math.pow(nMax, 1 / 3);
|
|
7855
|
+
}
|
|
7856
|
+
v = isFinite(v) ? v : 0;
|
|
7857
|
+
if (v === 0 && has(/NOZERO/)) { posY = 0; continue; }
|
|
7858
|
+
if (tv < 0 && has(/NONEGATIVE/)) { posY = 0; continue; }
|
|
7859
|
+
if (tv > 0 && has(/ONLYNEGATIVE/)) { posY = 0; continue; }
|
|
7860
|
+
|
|
7861
|
+
const oy = posY;
|
|
7862
|
+
const bar = { part: idx, cls, value: v, textValue: tv, dir: 0, w2: W / 2, sw: W / 2, band: 0, head: 0, len: 0,
|
|
7863
|
+
fill: { opacity: o.fillOpacity || 1 }, line: null, opacity: 1 };
|
|
7864
|
+
let drawn = true;
|
|
7865
|
+
if (pointer && v) {
|
|
7866
|
+
if (has(/OFFSETMEAN|OFFSETMEDIAN|DEVIATION/) && Math.abs(v * step) > 50) {
|
|
7867
|
+
const h = Math.abs(v * step);
|
|
7868
|
+
step = (50 + (h - 50) / 10 + h % 50) / Math.abs(v);
|
|
7869
|
+
}
|
|
7870
|
+
const H = v * step;
|
|
7871
|
+
const arrow = has(/ARROW/);
|
|
7872
|
+
const style = { rgb: null, width: o.lineWidth ? o.lineWidth * Math.sqrt(sizer || 1) : 0.05, opacity: 0.5 };
|
|
7873
|
+
if (oy <= 0) {
|
|
7874
|
+
// positive: the shaft from the base up, a band as wide as the bar,
|
|
7875
|
+
// the head on it
|
|
7876
|
+
Object.assign(bar, arrow
|
|
7877
|
+
? { base: [posX + W / 2, oy - 0.9 * H / nC + 0.9 * H], len: 0.9 * H, sw: W / 6, band: W / 15, head: W / 1.5 }
|
|
7878
|
+
: { base: [posX + W / 2, oy - H / nC + H], len: H, sw: W * 5 / 14, band: W / 15, head: W / 2 });
|
|
7879
|
+
bar.line = style;
|
|
7880
|
+
} else if (!has(/NONEGATIVE/)) {
|
|
7881
|
+
// negative: from the zero line down, the head below
|
|
7882
|
+
Object.assign(bar, arrow
|
|
7883
|
+
? { base: [posX + W / 2, oy - H / nC], len: H, sw: W / 6, head: W / 2, dir: 2 }
|
|
7884
|
+
: { base: [posX + W / 2, oy - H / nC], len: H, sw: W * 5 / 14, head: W / 4, dir: 2 });
|
|
7885
|
+
bar.line = style;
|
|
7886
|
+
if (!has(/LEFT/)) {
|
|
7887
|
+
const fadeNeg = o.fadeNegative || 0.1;
|
|
7888
|
+
if (has(/ZOOM|NORMSIZE/)) {
|
|
7889
|
+
bar.fill.opacity = 0.5;
|
|
7890
|
+
bar.line = { rgb: 'color', width: 0.25, opacity: 1 };
|
|
7891
|
+
} else if (o.nClasses > 2) {
|
|
7892
|
+
bar.fill.opacity = fadeNeg;
|
|
7893
|
+
bar.line = { rgb: 'low', width: o.lineWidth ? o.lineWidth * 0.25 * sizer : 0.25, opacity: 0.8 };
|
|
7894
|
+
} else {
|
|
7895
|
+
bar.fill.opacity = fadeNeg;
|
|
7896
|
+
bar.line = { rgb: 'high', width: o.lineWidth ? o.lineWidth * Math.sqrt(sizer || 1) : 0.05, opacity: 1 };
|
|
7897
|
+
}
|
|
7898
|
+
}
|
|
7899
|
+
} else drawn = false;
|
|
7900
|
+
} else {
|
|
7901
|
+
// plain 2D bar (DOTTED, 3D, COLUMN are drawn as this too)
|
|
7902
|
+
if (!has(/NOZERO/)) v = v || 0.00001;
|
|
7903
|
+
const H = v * step;
|
|
7904
|
+
bar.value = v;
|
|
7905
|
+
if (H < 0) drawn = false;
|
|
7906
|
+
bar.base = [posX + W / 2, oy - H / nC + H];
|
|
7907
|
+
bar.len = H;
|
|
7908
|
+
if (oy <= 0 || has(/LEFT/)) {
|
|
7909
|
+
bar.line = { rgb: null, width: o.lineWidth ? o.lineWidth * 0.25 * sizer : 0.2, opacity: o.lineWidth ? 1 : 0.1 };
|
|
7910
|
+
} else {
|
|
7911
|
+
bar.fill.opacity = 0.3;
|
|
7912
|
+
bar.line = { rgb: 'color', width: 0.25, opacity: 1 };
|
|
7913
|
+
}
|
|
7914
|
+
}
|
|
7915
|
+
if (has(/FADENEGATIVE/) && tv < 0) bar.opacity = 0.5;
|
|
7916
|
+
if (has(/FADEIN/)) {
|
|
7917
|
+
if (n === 2 && i === 0) bar.opacity = 0.6;
|
|
7918
|
+
else if (!has(/NORMSIZE/)) {
|
|
7919
|
+
const p = has(/FADEINP4/) ? 10 : 3;
|
|
7920
|
+
bar.opacity = 0.1 + 0.9 * Math.pow(i + 1, p) / Math.pow(n, p);
|
|
7921
|
+
}
|
|
7922
|
+
}
|
|
7923
|
+
if (drawn) bars.push(bar);
|
|
7924
|
+
|
|
7925
|
+
// the value text
|
|
7926
|
+
const H = v * step;
|
|
7927
|
+
if (has(/VALUES/) && !(oy > 0 && has(/NONEGATIVE/)) && !o.hideValues
|
|
7928
|
+
&& f > 0 && !(o.gridX && o.nClasses / o.gridX > 2)) {
|
|
7929
|
+
let text = fmt(tv, o.valueDecimals != null ? o.valueDecimals : (o.themeMax <= 1 ? 1 : 0));
|
|
7930
|
+
if (pointer && tv > 0 && (o.themeMin < 0 || has(/OFFSETMEAN|OFFSETMEDIAN|\bSIGN\b/))) text = '+' + text;
|
|
7931
|
+
if (!has(/VOLUME/)) text += o.units ? ' ' + o.units + ' ' : '';
|
|
7932
|
+
if (stacked && tv !== 0 && !o.field100 && !o.gridX) text += ' (' + Math.round(tv / sum * 100) + '%)';
|
|
7933
|
+
if (stacked) f = (has(/ZOOM/) ? 18 : 5) * valueScale;
|
|
7934
|
+
// SVG collapses the blanks
|
|
7935
|
+
text = text.replace(/\s+/g, ' ').trim();
|
|
7936
|
+
const red = parseFloat(text) < 0;
|
|
7937
|
+
// createTextLabel's box: not with HORZ; with ZOOM … AXIS only
|
|
7938
|
+
// colored (VALUEBACKGROUND / CTEXT); colored for a positive value
|
|
7939
|
+
const colored = tv > 0 && has(/CTEXT|VALUEBACKGROUND/);
|
|
7940
|
+
let bgOn = true;
|
|
7941
|
+
if (has(/ZOOM|MENUSIZE|NORMSIZE|INFOSIZE|AXIS|SELECTION/)) bgOn = colored;
|
|
7942
|
+
if (has(/VALUEBACKGROUND/)) bgOn = true;
|
|
7943
|
+
if (horz) bgOn = false;
|
|
7944
|
+
const bg = bgOn ? (colored ? 'bar' : 'white') : null;
|
|
7945
|
+
// the text color: negative red (valuecolor), else textcolor; on the
|
|
7946
|
+
// bar's color its text color, else flat's values color
|
|
7947
|
+
const color = red ? (o.valueColor || 'red') : (o.textColor || (bg === 'bar' ? 'contrast' : 'value'));
|
|
7948
|
+
const textW = measure(text, f);
|
|
7949
|
+
const tLen = textW + (bgOn ? 0.7 * f : 0);
|
|
7950
|
+
const ptY = W / 2 + f * 0.15;
|
|
7951
|
+
const outside = (!has(/DOINLINETEXT/) || has(/ZOOM/) || has(/NORMSIZE/)) && has(/VALUE/) && !(has(/VOLUME/) && !has(/ZOOM/))
|
|
7952
|
+
&& (has(/THIN|NOINLINETEXT|DTEXT|SIZE|STACKED|NORMSIZE|ZOOM/) || tLen + 1 > H / nC);
|
|
7953
|
+
const base = { text, cls, part: idx, opacity: textOpacity, color, bg };
|
|
7954
|
+
if (outside && stacked) {
|
|
7955
|
+
if (!(v === 0 && !has(/ZEROISVALUE/)) && !(o.gridX && n / o.gridX > 2)) {
|
|
7956
|
+
const topDx = H / 2;
|
|
7957
|
+
minNext = Math.max(0, minNext + (f * 0.75 - topDx));
|
|
7958
|
+
const textPosX = minNext + topDx - f / 3;
|
|
7959
|
+
let indent = 0.5 * sizer;
|
|
7960
|
+
let ptYs = ptY;
|
|
7961
|
+
let x1 = posX + ptY + W / 3, x2 = posX + ptY + W * 7 / 8;
|
|
7962
|
+
const y1 = -topDx, y2 = -textPosX - f / 3;
|
|
7963
|
+
if (o.gridX && barsDrawn < o.gridX) {
|
|
7964
|
+
ptYs -= W * 2 + tLen;
|
|
7965
|
+
const tmp = x2;
|
|
7966
|
+
x2 = x1 - W * 1.4;
|
|
7967
|
+
x1 = tmp - W * 1.4;
|
|
7968
|
+
indent = -indent;
|
|
7969
|
+
}
|
|
7970
|
+
texts.push(Object.assign(base, { x: posX + ptYs + W + indent, y: oy - textPosX, angle: 0, font: f }));
|
|
7971
|
+
[[x1, y1, x1 + indent, y1], [x1 + indent, y1, x2, y2], [x2, y2, x2 + indent, y2]]
|
|
7972
|
+
.forEach(([a, b, c, d]) => lines.push({ x0: a, y0: oy + b, x1: c, y1: oy + d }));
|
|
7973
|
+
minNext = Math.max(0, minNext + (f * 2 / 3 - (H - topDx)));
|
|
7974
|
+
}
|
|
7975
|
+
} else if (outside) {
|
|
7976
|
+
const lift = pointer && tv > 0 ? W * 0.5 : 0;
|
|
7977
|
+
if (n === 1 && !has(/HORZ/) && !has(/VOLUME/)) {
|
|
7978
|
+
texts.push(Object.assign(base, { x: posX - W * 0.8 + f, y: oy - H / nC - f * 0.8 - lift + 0.25 * f, angle: 0, font: f,
|
|
7979
|
+
opacity: tv < 0 ? textOpacity * 0.75 : textOpacity }));
|
|
7980
|
+
} else {
|
|
7981
|
+
// rotate(270) scale(0.9): reading up, 0.9 font
|
|
7982
|
+
texts.push(Object.assign(base, { x: posX + ptY + 0.9 * 0.25 * f, y: oy - 0.5 - W * 0.25 - H / nC - 0.9 * f, angle: 90, font: 0.9 * f }));
|
|
7983
|
+
}
|
|
7984
|
+
} else {
|
|
7985
|
+
// inside the bar, in its text color, shrunk to fit
|
|
7986
|
+
const s = Math.min(1, H / tLen);
|
|
7987
|
+
if (s >= 0.33) {
|
|
7988
|
+
let ptX = -4;
|
|
7989
|
+
if (tv > 0) ptX += Math.max(1, H - (tLen + 3) * s);
|
|
7990
|
+
const dY = 0.5 / s;
|
|
7991
|
+
texts.push(Object.assign(base, { x: posX + ptY - dY + 0.25 * f, y: oy - ptX - f, angle: 90, font: f * s, color: 'contrast', bg: null }));
|
|
7992
|
+
}
|
|
7993
|
+
minNext = Math.max(0, minNext - H);
|
|
7994
|
+
}
|
|
7995
|
+
}
|
|
7996
|
+
|
|
7997
|
+
if (stacked) {
|
|
7998
|
+
if (!has(/CENTER/)) posY -= v * step;
|
|
7999
|
+
} else {
|
|
8000
|
+
posY = 0;
|
|
8001
|
+
posX += W;
|
|
8002
|
+
if (has(/SPACED/)) posX += W / 5;
|
|
8003
|
+
else if (!has(/\bUP\b/)) posX += 0.05;
|
|
8004
|
+
}
|
|
8005
|
+
barsDrawn++;
|
|
8006
|
+
if (stacked && !has(/MULTI/) && o.gridX && barsDrawn % o.gridX === 0) {
|
|
8007
|
+
posY = 0;
|
|
8008
|
+
posX += W + W / 20;
|
|
8009
|
+
}
|
|
8010
|
+
}
|
|
8011
|
+
|
|
8012
|
+
// the chart's position: HOR(Z) turned by 90° (bars to the right, the
|
|
8013
|
+
// first on top, the last centered on the position's height), else
|
|
8014
|
+
// the bars side by side centered on it (STACKED: the first column)
|
|
8015
|
+
const P = posX;
|
|
8016
|
+
let map, dirMap, angleMap;
|
|
8017
|
+
if (horz) {
|
|
8018
|
+
const dy = W / 2 + (has(/3D/) ? W / 3 : 0) - P;
|
|
8019
|
+
map = (x, y) => [-y, x + dy];
|
|
8020
|
+
dirMap = d => (d + 1) % 4;
|
|
8021
|
+
angleMap = a => a - 90;
|
|
8022
|
+
} else {
|
|
8023
|
+
const dx = stacked ? W / 2 : W * barsDrawn / 2;
|
|
8024
|
+
map = (x, y) => [x - dx, y];
|
|
8025
|
+
dirMap = d => d;
|
|
8026
|
+
angleMap = a => a;
|
|
8027
|
+
}
|
|
8028
|
+
const ext = [Infinity, Infinity, -Infinity, -Infinity];
|
|
8029
|
+
const grow = (x, y) => { if (x < ext[0]) ext[0] = x; if (y < ext[1]) ext[1] = y; if (x > ext[2]) ext[2] = x; if (y > ext[3]) ext[3] = y; };
|
|
8030
|
+
for (const b of bars) {
|
|
8031
|
+
b.base = map(b.base[0], b.base[1]);
|
|
8032
|
+
b.dir = dirMap(b.dir);
|
|
8033
|
+
const d = [[0, -1], [1, 0], [0, 1], [-1, 0]][b.dir], a = [-d[1], d[0]];
|
|
8034
|
+
const L = b.len + b.band + b.head;
|
|
8035
|
+
[[-b.w2, 0], [b.w2, 0], [-b.w2, L], [b.w2, L]].forEach(([u, v]) => grow(b.base[0] + a[0] * u + d[0] * v, b.base[1] + a[1] * u + d[1] * v));
|
|
8036
|
+
}
|
|
8037
|
+
for (const tx of texts) {
|
|
8038
|
+
[tx.x, tx.y] = map(tx.x, tx.y);
|
|
8039
|
+
tx.angle = angleMap(tx.angle);
|
|
8040
|
+
// arial: ascent 0.905, descent 0.212 of the font
|
|
8041
|
+
const w = measure(tx.text, tx.font) + (tx.bg ? 0.7 * tx.font : 0);
|
|
8042
|
+
const r = tx.angle * Math.PI / 180, c = Math.cos(r), s = -Math.sin(r);
|
|
8043
|
+
const xo = tx.bg ? -0.3 * tx.font : 0;
|
|
8044
|
+
[[xo, -0.905 * tx.font], [xo + w, -0.905 * tx.font], [xo, 0.212 * tx.font], [xo + w, 0.212 * tx.font]]
|
|
8045
|
+
.forEach(([u, v]) => grow(tx.x + u * c - v * s, tx.y + u * s + v * c));
|
|
8046
|
+
}
|
|
8047
|
+
for (const l of lines) {
|
|
8048
|
+
[l.x0, l.y0] = map(l.x0, l.y0);
|
|
8049
|
+
[l.x1, l.y1] = map(l.x1, l.y1);
|
|
8050
|
+
grow(l.x0, l.y0); grow(l.x1, l.y1);
|
|
8051
|
+
}
|
|
8052
|
+
return { bars, texts, lines, extent: ext[0] <= ext[2] ? ext : [0, 0, 0, 0], width: W, sizer };
|
|
8053
|
+
}
|
|
8054
|
+
|
|
8055
|
+
// flat's BOX and TITLE around a chart (maptheme.js 18758-18905), in the
|
|
8056
|
+
// chart's units: `extent` the chart's [x0, y0, x1, y1]; o: { margin
|
|
8057
|
+
// (boxmargin, 2), titleFont, title (text or ''), bottomTitle, alignRight,
|
|
8058
|
+
// measure }. The margin is min(2 m, m · width / 30); the title's first
|
|
8059
|
+
// baseline 0.7 font above the chart (BOTTOMTITLE: one font below), its
|
|
8060
|
+
// lines wrapped to the chart's width and stacked upward; the box encloses
|
|
8061
|
+
// chart and title, its top trimmed by 0.1 font when titled. Returns
|
|
8062
|
+
// { rect: [x0, y0, x1, y1], title: { lines, x, anchor, baseline, font } | null }
|
|
8063
|
+
function flatChartBox(extent, o) {
|
|
8064
|
+
const e = extent.slice();
|
|
8065
|
+
const w = e[2] - e[0];
|
|
8066
|
+
const margin = Math.min(2 * o.margin, o.margin * w / 30);
|
|
8067
|
+
let title = null;
|
|
8068
|
+
if (o.title) {
|
|
8069
|
+
const f = o.titleFont;
|
|
8070
|
+
const measure = o.measure || ((text, font) => text.length * font * 0.55);
|
|
8071
|
+
const lines = [];
|
|
8072
|
+
for (const word of String(o.title).split(/\s+/).filter(Boolean)) {
|
|
8073
|
+
const last = lines.length ? lines[lines.length - 1] + ' ' + word : null;
|
|
8074
|
+
if (last !== null && measure(last, f) <= w) lines[lines.length - 1] = last;
|
|
8075
|
+
else lines.push(word);
|
|
8076
|
+
}
|
|
8077
|
+
const width = Math.max(...lines.map(l => measure(l, f)));
|
|
8078
|
+
const baseline = o.bottomTitle ? e[3] + f : e[1] - 0.7 * f - (lines.length - 1) * f;
|
|
8079
|
+
const x = o.alignRight ? e[2] : e[0];
|
|
8080
|
+
const x0 = o.alignRight ? x - width : x;
|
|
8081
|
+
title = { lines, x, anchor: o.alignRight ? 'end' : 'start', baseline, font: f };
|
|
8082
|
+
e[0] = Math.min(e[0], x0);
|
|
8083
|
+
e[1] = Math.min(e[1], baseline - 0.905 * f) + 0.1 * f;
|
|
8084
|
+
e[2] = Math.max(e[2], x0 + width);
|
|
8085
|
+
e[3] = Math.max(e[3], baseline + (lines.length - 1) * f + 0.212 * f);
|
|
8086
|
+
}
|
|
8087
|
+
return { rect: [e[0] - margin, e[1] - margin, e[2] + margin, e[3] + margin], title, width: w };
|
|
8088
|
+
}
|
|
8089
|
+
|
|
7272
8090
|
// CHART|PIE / DONUT: one pie's slices as real ixmaps-flat lays them out —
|
|
7273
8091
|
// maptheme.js drawChart's PIE branch (19939-20330) and piechart.js
|
|
7274
8092
|
// DonutChart.realize (181-510). `values`: the item's part values (field
|
|
@@ -7456,6 +8274,322 @@ in vec4 vPieLineColor;
|
|
|
7456
8274
|
return out;
|
|
7457
8275
|
}
|
|
7458
8276
|
|
|
8277
|
+
// flat's value rules of a theme (MapTheme constructor, maptheme.js
|
|
8278
|
+
// 7740-7830), by substring of the type string as flat tests them:
|
|
8279
|
+
// nullIsValue (0 is a value: ZEROISNOTVALUE no, ZEROISVALUE yes, a CHART
|
|
8280
|
+
// with CATEGORICAL or several value fields always), negativeOk
|
|
8281
|
+
// (NEGATIVEISNOTVALUE no, NEGATIVEISVALUE yes, CATEGORICAL without
|
|
8282
|
+
// AGGREGATE no), undefinedOk (UNDEFINEDISNOTVALUE no), zeroExcludes
|
|
8283
|
+
// (ZEROISNOTVALUE: a 0 is left out of an aggregation's means)
|
|
8284
|
+
function flatValueRules(t, nFields) {
|
|
8285
|
+
t = String(t || '');
|
|
8286
|
+
let nullIsValue = true, negativeOk = true, undefinedOk = true;
|
|
8287
|
+
if (/ZEROISNOTVALUE/.test(t)) nullIsValue = false;
|
|
8288
|
+
if (/ZEROISVALUE/.test(t)) nullIsValue = true;
|
|
8289
|
+
if (/NEGATIVEISNOTVALUE/.test(t)) negativeOk = false;
|
|
8290
|
+
if (/NEGATIVEISVALUE/.test(t)) negativeOk = true;
|
|
8291
|
+
if (/UNDEFINEDISVALUE/.test(t)) undefinedOk = true;
|
|
8292
|
+
if (/UNDEFINEDISNOTVALUE/.test(t)) undefinedOk = false;
|
|
8293
|
+
if (/CHART/.test(t) && (/CATEGORICAL/.test(t) || nFields > 1)) nullIsValue = true;
|
|
8294
|
+
if (/CATEGORICAL/.test(t) && !/AGGREGATE/.test(t)) negativeOk = false;
|
|
8295
|
+
return { nullIsValue, negativeOk, undefinedOk, zeroExcludes: /ZEROISNOTVALUE/.test(t) };
|
|
8296
|
+
}
|
|
8297
|
+
|
|
8298
|
+
// An AGGREGATE or CATEGORICAL pie: one record's contribution to its item,
|
|
8299
|
+
// as flat's loadAndAggregateValuesOfTheme reads it (maptheme.js
|
|
8300
|
+
// 10465-10571, which every AGGREGATE theme goes through). `o`:
|
|
8301
|
+
// { rules (flatValueRules), sizeField, cat (CATEGORICAL: the record's
|
|
8302
|
+
// category, 0-based, -1 when it is none of them), fields (otherwise:
|
|
8303
|
+
// the value fields) }
|
|
8304
|
+
// Returns null when flat leaves the record out, else
|
|
8305
|
+
// { cat, size } (CATEGORICAL) or { parts, zeroNot, size }
|
|
8306
|
+
// - size: the size field as a number (1 without one); a record whose
|
|
8307
|
+
// size is no number, 0 (unless 0 is a value) or negative (unless
|
|
8308
|
+
// negatives are values) is left out
|
|
8309
|
+
// - CATEGORICAL: a value that is none of the categories (not in
|
|
8310
|
+
// style.values) is part -1 — no slice, but its size still counts in
|
|
8311
|
+
// the item's size (flat: nValuesA[-1]); left out only with
|
|
8312
|
+
// UNDEFINEDISNOTVALUE
|
|
8313
|
+
// - value fields: a value that is no number is 0 (UNDEFINEDISNOTVALUE:
|
|
8314
|
+
// the record is left out); a 0 is left out of the item's MEAN with
|
|
8315
|
+
// ZEROISNOTVALUE (zeroNot), else the record when 0 is no value; a
|
|
8316
|
+
// negative value when negatives are no values
|
|
8317
|
+
function flatPieRecord(raw, o) {
|
|
8318
|
+
const r = o.rules;
|
|
8319
|
+
const size = o.sizeField ? flatScanValue(raw[o.sizeField]) : 1;
|
|
8320
|
+
if (isNaN(size) || (size === 0 && !r.nullIsValue) || (size < 0 && !r.negativeOk)) return null;
|
|
8321
|
+
if (o.cat !== undefined) {
|
|
8322
|
+
if (o.cat < 0 && !r.undefinedOk) return null;
|
|
8323
|
+
return { cat: o.cat, size };
|
|
8324
|
+
}
|
|
8325
|
+
const parts = [], zeroNot = [];
|
|
8326
|
+
for (let k = 0; k < o.fields.length; k++) {
|
|
8327
|
+
let v = parseFloat(raw[o.fields[k]]);
|
|
8328
|
+
zeroNot[k] = false;
|
|
8329
|
+
if (isNaN(v)) {
|
|
8330
|
+
if (!r.undefinedOk) return null;
|
|
8331
|
+
v = 0;
|
|
8332
|
+
}
|
|
8333
|
+
if (v === 0) {
|
|
8334
|
+
if (r.zeroExcludes) { parts[k] = 0; zeroNot[k] = true; continue; }
|
|
8335
|
+
if (!r.nullIsValue) return null;
|
|
8336
|
+
}
|
|
8337
|
+
if (v < 0 && !r.negativeOk) return null;
|
|
8338
|
+
parts[k] = v;
|
|
8339
|
+
}
|
|
8340
|
+
return { parts, zeroNot, size };
|
|
8341
|
+
}
|
|
8342
|
+
|
|
8343
|
+
// Adds a record's flatPieRecord to its item (maptheme.js 10779-10943) —
|
|
8344
|
+
// `item` null for the first record of the item. Returns the item
|
|
8345
|
+
// { parts (sparse for CATEGORICAL: the category sums), counts (the
|
|
8346
|
+
// records per part, for MEAN), size (the item's size, flat's nSize),
|
|
8347
|
+
// n (its record count, flat's nCount) }
|
|
8348
|
+
// SUM (default): the parts and the |sizes| summed; MAX / MIN: their
|
|
8349
|
+
// maximum / minimum (a part missing so far counts 0, as in flat; the
|
|
8350
|
+
// record count stays 1); FIRST: the first record only; LAST: the last.
|
|
8351
|
+
// A CATEGORICAL record of no category (-1) adds its size only.
|
|
8352
|
+
function flatPieAccumulate(item, rec, flags) {
|
|
8353
|
+
const max = flags.has('MAX'), min = flags.has('MIN');
|
|
8354
|
+
if (item && flags.has('FIRST')) return item;
|
|
8355
|
+
if (!item || flags.has('LAST')) {
|
|
8356
|
+
if (rec.cat !== undefined) {
|
|
8357
|
+
const parts = [], counts = [];
|
|
8358
|
+
if (rec.cat >= 0) { parts[rec.cat] = rec.size; counts[rec.cat] = 1; }
|
|
8359
|
+
return { parts, counts, size: Math.abs(rec.size), n: 1 };
|
|
8360
|
+
}
|
|
8361
|
+
return { parts: rec.parts.slice(), counts: rec.zeroNot.map(z => (z ? 0 : 1)), size: Math.abs(rec.size), n: 1 };
|
|
8362
|
+
}
|
|
8363
|
+
const comb = (a, v) => (max ? Math.max(a || 0, v) : min ? Math.min(a || 0, v) : (a || 0) + v);
|
|
8364
|
+
if (rec.cat !== undefined) {
|
|
8365
|
+
if (rec.cat >= 0) {
|
|
8366
|
+
item.parts[rec.cat] = comb(item.parts[rec.cat], rec.size);
|
|
8367
|
+
item.counts[rec.cat] = (item.counts[rec.cat] || 0) + 1;
|
|
8368
|
+
}
|
|
8369
|
+
} else {
|
|
8370
|
+
for (let k = 0; k < rec.parts.length; k++) {
|
|
8371
|
+
item.parts[k] = comb(item.parts[k], rec.parts[k] || 0);
|
|
8372
|
+
if (!rec.zeroNot[k]) item.counts[k] = (item.counts[k] || 0) + 1;
|
|
8373
|
+
}
|
|
8374
|
+
}
|
|
8375
|
+
if (max) item.size = Math.max(item.size, rec.size);
|
|
8376
|
+
else if (min) item.size = Math.min(item.size, rec.size);
|
|
8377
|
+
else { item.size += Math.abs(rec.size); item.n++; }
|
|
8378
|
+
return item;
|
|
8379
|
+
}
|
|
8380
|
+
|
|
8381
|
+
// An aggregated pie item's parts and size after flat's post-aggregation
|
|
8382
|
+
// steps (maptheme.js 11056-11137): MEAN — a part divided by its record
|
|
8383
|
+
// count (CATEGORICAL: the category's records; value fields: the item's
|
|
8384
|
+
// records, or with ZEROISNOTVALUE the records with a value there), the
|
|
8385
|
+
// size by the item's record count unless SIZESUM; AUTO100 — the parts in
|
|
8386
|
+
// % of their sum. `nParts`: the number of parts (CATEGORICAL with
|
|
8387
|
+
// style.values: every category; without: up to the item's last one, as
|
|
8388
|
+
// flat's sparse nValuesA). Returns { parts, size }.
|
|
8389
|
+
function flatPieItemValues(item, t, o = {}) {
|
|
8390
|
+
t = String(t || '');
|
|
8391
|
+
const n = o.nParts != null ? o.nParts : item.parts.length;
|
|
8392
|
+
let parts = [];
|
|
8393
|
+
for (let k = 0; k < n; k++) parts[k] = item.parts[k] || 0;
|
|
8394
|
+
let size = item.size;
|
|
8395
|
+
if (/MEAN/.test(t)) {
|
|
8396
|
+
const perPart = o.categorical || /ZEROISNOTVALUE/.test(t);
|
|
8397
|
+
parts = parts.map((v, k) => (perPart ? (item.counts[k] > 0 ? v / item.counts[k] : 0) : v / item.n));
|
|
8398
|
+
if (!/SIZESUM/.test(t)) size /= item.n;
|
|
8399
|
+
}
|
|
8400
|
+
if (/AUTO100/.test(t)) {
|
|
8401
|
+
const sum = parts.reduce((a, v) => a + v, 0);
|
|
8402
|
+
parts = parts.map(v => v / (sum / 100));
|
|
8403
|
+
}
|
|
8404
|
+
return { parts, size };
|
|
8405
|
+
}
|
|
8406
|
+
|
|
8407
|
+
// CHART|VECTOR / CHART|BEZIER: the theme's items as real ixmaps-flat
|
|
8408
|
+
// builds them (maptheme.js loadAndAggregateValuesOfTheme, which every
|
|
8409
|
+
// VECTOR / BEZIER theme goes through, 9650: 10402-10450 the second
|
|
8410
|
+
// position, 10763 the item id, 10779-10985 the items). `records`:
|
|
8411
|
+
// { p1: [lng, lat], p2: [lng, lat] | null, cat (0-based category, -1
|
|
8412
|
+
// not one of them; null without CATEGORICAL), value (the value
|
|
8413
|
+
// field as a number, without CATEGORICAL), size (the size field as
|
|
8414
|
+
// a number, NaN when not one; 1 without a size field), raw }
|
|
8415
|
+
// `o`: { aggregate, vectorKey (the VECTOR flag: the item id has the
|
|
8416
|
+
// second position too), multiParts (CATEGORICAL: the category is part
|
|
8417
|
+
// of the id), max / min (MAX / MIN aggregation), zeroIsNotValue,
|
|
8418
|
+
// negativeIsNotValue }. Returns items
|
|
8419
|
+
// { p1, p2, cat, nSize, value, count, raw (the first record), raws }
|
|
8420
|
+
// - without AGGREGATE every record is an item, its nSize the size as
|
|
8421
|
+
// it is (negative too, 0 when not a number) — flat checks no value
|
|
8422
|
+
// there
|
|
8423
|
+
// - AGGREGATE: records of one id are one item: the id is the first
|
|
8424
|
+
// position (+ the category with CATEGORICAL, + the second position
|
|
8425
|
+
// with VECTOR — a BEZIER without VECTOR sums every flow of an origin
|
|
8426
|
+
// into its first record's flow, as flat); a record whose size is no
|
|
8427
|
+
// number (or 0 with ZEROISNOTVALUE, negative with
|
|
8428
|
+
// NEGATIVEISNOTVALUE) is left out; nSize the sum of the |sizes|
|
|
8429
|
+
// (MAX / MIN: their maximum / minimum), value the values' sum
|
|
8430
|
+
// - the first record of an item gives its category, second position,
|
|
8431
|
+
// and the record its tooltip reads
|
|
8432
|
+
function flatVectorItems(records, o = {}) {
|
|
8433
|
+
const items = [];
|
|
8434
|
+
if (!o.aggregate) {
|
|
8435
|
+
for (const r of records) {
|
|
8436
|
+
items.push({ p1: r.p1, p2: r.p2, cat: r.cat, nSize: r.size || 0, value: r.value, count: 1, raw: r.raw, raws: [r.raw] });
|
|
8437
|
+
}
|
|
8438
|
+
return items;
|
|
8439
|
+
}
|
|
8440
|
+
const byId = new Map();
|
|
8441
|
+
for (const r of records) {
|
|
8442
|
+
const s = r.size;
|
|
8443
|
+
if (isNaN(s) || (s === 0 && o.zeroIsNotValue) || (s < 0 && o.negativeIsNotValue)) continue;
|
|
8444
|
+
let id = r.p1[0] + ',' + r.p1[1];
|
|
8445
|
+
// flat's nValue for the id: the category number (1-based), 0 for a
|
|
8446
|
+
// value that is none of the categories
|
|
8447
|
+
if (o.multiParts) id += ',' + (r.cat != null && r.cat >= 0 ? r.cat + 1 : 0);
|
|
8448
|
+
if (o.vectorKey && r.p2) id += '&' + r.p2[0] + ',' + r.p2[1];
|
|
8449
|
+
const item = byId.get(id);
|
|
8450
|
+
if (!item) {
|
|
8451
|
+
const it = { p1: r.p1, p2: r.p2, cat: r.cat, nSize: Math.abs(s), value: r.value, count: 1, raw: r.raw, raws: [r.raw] };
|
|
8452
|
+
byId.set(id, it);
|
|
8453
|
+
items.push(it);
|
|
8454
|
+
continue;
|
|
8455
|
+
}
|
|
8456
|
+
if (o.max) {
|
|
8457
|
+
item.nSize = Math.max(item.nSize, s);
|
|
8458
|
+
item.value = Math.max(item.value || 0, r.value || 0);
|
|
8459
|
+
} else if (o.min) {
|
|
8460
|
+
item.nSize = Math.min(item.nSize, s);
|
|
8461
|
+
item.value = Math.min(item.value || 0, r.value || 0);
|
|
8462
|
+
} else {
|
|
8463
|
+
item.nSize += Math.abs(s);
|
|
8464
|
+
item.value = (item.value || 0) + (r.value || 0);
|
|
8465
|
+
}
|
|
8466
|
+
item.count++;
|
|
8467
|
+
item.raws.push(r.raw);
|
|
8468
|
+
}
|
|
8469
|
+
return items;
|
|
8470
|
+
}
|
|
8471
|
+
|
|
8472
|
+
// CHART|BEZIER: one flow's curve as flat draws it (maptheme.js
|
|
8473
|
+
// 18148-18375), in screen pixels relative to its first position (y
|
|
8474
|
+
// down — flat's SVG map coordinates, scaled). dx/dy: the second
|
|
8475
|
+
// position minus the first. o:
|
|
8476
|
+
// w the line width in flat's chart units (linewidth, else
|
|
8477
|
+
// 10 · (|nSize| / max)^(1/sizepow))
|
|
8478
|
+
// ll flat's arrow / gap length in chart units (w + 3, GAP: w +
|
|
8479
|
+
// gapsize — computed by the caller, see _buildVectorLayers)
|
|
8480
|
+
// unit pixels per chart unit (the object zoom × style.scale)
|
|
8481
|
+
// oz pixels per chart unit without style.scale
|
|
8482
|
+
// bow rangescale (flat's nBow; 5 when not given), RANDOM already
|
|
8483
|
+
// applied
|
|
8484
|
+
// t the type string (SHORT, POINTER / ARROW, GAP)
|
|
8485
|
+
// marker markersize (1)
|
|
8486
|
+
// gap gapsize (20)
|
|
8487
|
+
// Returns { start, c1, c2, end, vx, vy } — start/end/c1/c2 [x, y]
|
|
8488
|
+
// pixels; (vx, vy) the end after shortening, which decides the
|
|
8489
|
+
// gradient direction. The control points come from the unshortened
|
|
8490
|
+
// vector: offset by (dy, −dx) / 50 · bow (SHORT: the unit vector · 5
|
|
8491
|
+
// bow chart units), at 1/4 and 1/2 of the (shortened) vector. An arrow (POINTER
|
|
8492
|
+
// or a type word ending in ARROW) ends the curve ll · markersize units
|
|
8493
|
+
// short of the second position, room for the arrow head. GAP: the curve
|
|
8494
|
+
// starts gapsize units off the first position and ends ll / 500 of the
|
|
8495
|
+
// bow further on.
|
|
8496
|
+
function bezierVectorLayout(dx, dy, o) {
|
|
8497
|
+
const t = String(o.t || '');
|
|
8498
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
8499
|
+
const bow = o.bow || 5;
|
|
8500
|
+
let ox, oy;
|
|
8501
|
+
// (LONG's bow depends on flat's own map units — not drawn here, see
|
|
8502
|
+
// VECTOR_INERT_FLAGS: the default bow instead)
|
|
8503
|
+
if (/\bSHORT\b/.test(t)) { ox = dy / len * bow * 5 * o.oz; oy = dx / len * bow * 5 * o.oz; }
|
|
8504
|
+
else { ox = dy / 50 * bow; oy = dx / 50 * bow; }
|
|
8505
|
+
let x = dx, y = dy;
|
|
8506
|
+
// flat's /\bPOINTER|ARROW\b/: POINTER at a word start, or ARROW at a
|
|
8507
|
+
// word end (ARROW, SMALLARROW, LASTARROW ...)
|
|
8508
|
+
if (/\bPOINTER|ARROW\b/.test(t)) {
|
|
8509
|
+
const k = o.ll * o.unit * (o.marker || 1) / len;
|
|
8510
|
+
x -= dx * k; y -= dy * k;
|
|
8511
|
+
}
|
|
8512
|
+
let x0 = 0, y0 = 0;
|
|
8513
|
+
if (/\bGAP\b/.test(t)) {
|
|
8514
|
+
x += ox / 500 * o.ll;
|
|
8515
|
+
y -= oy / 500 * o.ll;
|
|
8516
|
+
x0 = x / len * (o.gap || 20) * o.oz;
|
|
8517
|
+
y0 = y / len * (o.gap || 20) * o.oz;
|
|
8518
|
+
}
|
|
8519
|
+
return { start: [x0, y0], c1: [x / 4 + ox, y / 4 - oy], c2: [x / 2 + ox, y / 2 - oy], end: [x, y], vx: x, vy: y };
|
|
8520
|
+
}
|
|
8521
|
+
|
|
8522
|
+
// flat's Themes.toArray for a style list (maptheme.js 999-1016): a list
|
|
8523
|
+
// as it is; a string split at "|" when it has one or names an RGB(…)
|
|
8524
|
+
// color (flat's /\RGB/: upper case only), else at ","
|
|
8525
|
+
function flatToArray(v) {
|
|
8526
|
+
if (Array.isArray(v)) return v;
|
|
8527
|
+
const str = String(v);
|
|
8528
|
+
return (/\|/.test(str) || /RGB/.test(str)) ? str.split('|') : str.split(',');
|
|
8529
|
+
}
|
|
8530
|
+
// a fixed number in [0, 1) for a string — stands in for flat's
|
|
8531
|
+
// Math.random() where the harness needs the same drawing every time
|
|
8532
|
+
function hashUnit(str) {
|
|
8533
|
+
let h = 2166136261;
|
|
8534
|
+
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
|
8535
|
+
return (h >>> 0) / 4294967296;
|
|
8536
|
+
}
|
|
8537
|
+
|
|
8538
|
+
// points of a cubic Bézier, n segments (n + 1 points)
|
|
8539
|
+
function cubicBezierPoints(p0, c1, c2, p3, n) {
|
|
8540
|
+
const out = [];
|
|
8541
|
+
for (let i = 0; i <= n; i++) {
|
|
8542
|
+
const s = i / n, u = 1 - s;
|
|
8543
|
+
const a = u * u * u, b = 3 * u * u * s, c = 3 * u * s * s, d = s * s * s;
|
|
8544
|
+
out.push([a * p0[0] + b * c1[0] + c * c2[0] + d * p3[0], a * p0[1] + b * c1[1] + c * c2[1] + d * p3[1]]);
|
|
8545
|
+
}
|
|
8546
|
+
return out;
|
|
8547
|
+
}
|
|
8548
|
+
|
|
8549
|
+
// flat's arrow marker (an SVG <marker>, markerUnits strokeWidth, orient
|
|
8550
|
+
// auto) at a line's end: the triangle M0,s L s,s/2 L0,0 in units of the
|
|
8551
|
+
// line width sw, its reference point (refX, refY) on the end point, its
|
|
8552
|
+
// x axis along the end's direction (tx, ty). BEZIER: s = min(5, 2.5 +
|
|
8553
|
+
// 5 / (w · scale)) · markersize, ref (s / 1.7, s / 2) (maptheme.js
|
|
8554
|
+
// 18309-18320); VECTOR: s = 4, ref (4, 2) — the tip on the end point
|
|
8555
|
+
// (18411-18424). A zero direction is orient 0: pointing right.
|
|
8556
|
+
// Returns the three corners [x, y] (pixels, y down).
|
|
8557
|
+
function arrowMarkerTriangle(end, tx, ty, sw, s, refX, refY) {
|
|
8558
|
+
const l = Math.sqrt(tx * tx + ty * ty);
|
|
8559
|
+
const ux = l ? tx / l : 1, uy = l ? ty / l : 0;
|
|
8560
|
+
const at = (mx, my) => {
|
|
8561
|
+
const a = (mx - refX) * sw, b = (my - refY) * sw;
|
|
8562
|
+
return [end[0] + a * ux - b * uy, end[1] + a * uy + b * ux];
|
|
8563
|
+
};
|
|
8564
|
+
return [at(0, s), at(s, s / 2), at(0, 0)];
|
|
8565
|
+
}
|
|
8566
|
+
|
|
8567
|
+
// BEZIER GRADIENT / FADEIN (maptheme.js 18233-18284): a linear gradient
|
|
8568
|
+
// over the curve's bounding box, vertical when the (shortened) vector
|
|
8569
|
+
// is more vertical than horizontal, else horizontal; the stop on the
|
|
8570
|
+
// first position's side has the fade opacity (FADEIN 0.2, GRADIENT 2),
|
|
8571
|
+
// the other one 2 — SVG clamps both to 1 — and the colors run from the
|
|
8572
|
+
// second color (linecolor's first entry) at the start to the line color
|
|
8573
|
+
// at the end. Returns per point { k: 0 … 1 from the start side's stop,
|
|
8574
|
+
// alpha } — the color mix k and opacity of each point.
|
|
8575
|
+
function fadeGradientStops(points, vx, vy, fade) {
|
|
8576
|
+
const vertical = Math.abs(vy) > Math.abs(vx);
|
|
8577
|
+
const axis = vertical ? 1 : 0;
|
|
8578
|
+
let min = Infinity, max = -Infinity;
|
|
8579
|
+
for (const p of points) { if (p[axis] < min) min = p[axis]; if (p[axis] > max) max = p[axis]; }
|
|
8580
|
+
const v = vertical ? vy : vx;
|
|
8581
|
+
const a = Math.min(1, Math.max(0, fade));
|
|
8582
|
+
// offset 0 is the box's left / top side: the start side unless the
|
|
8583
|
+
// vector points left / up
|
|
8584
|
+
const startAtZero = !(v < 0);
|
|
8585
|
+
return points.map(p => {
|
|
8586
|
+
let f = max > min ? (p[axis] - min) / (max - min) : 0;
|
|
8587
|
+
if (!startAtZero) f = 1 - f;
|
|
8588
|
+
// f: 0 on the start side, 1 on the end side
|
|
8589
|
+
return { k: f, alpha: a + (1 - a) * f };
|
|
8590
|
+
});
|
|
8591
|
+
}
|
|
8592
|
+
|
|
7459
8593
|
// CHART|SYMBOL|SEQUENCE (flat maptheme.js 21184-21300, 21790-21925,
|
|
7460
8594
|
// 22296-22395): one symbol per category part of a chart, in category
|
|
7461
8595
|
// order or sorted (SORT: DOWN biggest first, UP smallest first), parts
|
|
@@ -8511,6 +9645,9 @@ in vec4 vPieLineColor;
|
|
|
8511
9645
|
cell.value += f.properties.value;
|
|
8512
9646
|
if (f.properties.classValue !== undefined) cell.classValue += f.properties.classValue;
|
|
8513
9647
|
if (post) cell.value100 += f.properties.value100 || 0;
|
|
9648
|
+
// an AGGREGATE / CATEGORICAL pie's record (flatPieRecord): its parts
|
|
9649
|
+
// and size added to the cell's item as flat does (flatPieAccumulate)
|
|
9650
|
+
if (f.properties.pie) cell.pie = flatPieAccumulate(cell.pie || null, f.properties.pie, flags);
|
|
8514
9651
|
const series = f.properties.series;
|
|
8515
9652
|
if (series) {
|
|
8516
9653
|
if (!cell.series) cell.series = new Array(series.length).fill(0);
|
|
@@ -8532,6 +9669,7 @@ in vec4 vPieLineColor;
|
|
|
8532
9669
|
...(cell.raws && cell.raws.length ? { raws: cell.raws } : {}) }
|
|
8533
9670
|
: cell.first.properties;
|
|
8534
9671
|
if (cell.series && cell.n > 1) props = Object.assign({}, props, { series: cell.series });
|
|
9672
|
+
if (cell.pie) props = Object.assign({}, props, { pie: cell.pie });
|
|
8535
9673
|
// a field aggregation's cell key (the field value): the categories of
|
|
8536
9674
|
// one cell are merged by it (groupCoLocated), not by their positions
|
|
8537
9675
|
if (field) props = Object.assign({}, props, { aggKey: cell.key });
|
|
@@ -8709,7 +9847,10 @@ in vec4 vPieLineColor;
|
|
|
8709
9847
|
this._filterExpr = spec.filter || '';
|
|
8710
9848
|
this.mapOptions = mapOptions || {};
|
|
8711
9849
|
this._specBinding = spec.binding;
|
|
8712
|
-
|
|
9850
|
+
// VECTOR / BEZIER keep every record: flat checks their values only
|
|
9851
|
+
// when it aggregates them (flatVectorItems)
|
|
9852
|
+
const withDensity = this._withDensity(fc.features);
|
|
9853
|
+
this.features = applyField100(isVectorChart(this.flags) ? withDensity : filterFlatValues(withDensity, this.binding, this.flags), this.binding, this.flags, this.style);
|
|
8713
9854
|
this.binding = field100Binding(this.binding, this.flags);
|
|
8714
9855
|
this._iconCache = new Map();
|
|
8715
9856
|
this._glowIconCache = new Map();
|
|
@@ -8804,7 +9945,8 @@ in vec4 vPieLineColor;
|
|
|
8804
9945
|
this._gridRefLat = (minLat <= maxLat) ? (minLat + maxLat) / 2 : 0;
|
|
8805
9946
|
}
|
|
8806
9947
|
|
|
8807
|
-
|
|
9948
|
+
// a bar chart's parts are a pie's (its value fields or categories)
|
|
9949
|
+
if ((this._isPieChart() || this._isBarChart()) && this.binding.value) {
|
|
8808
9950
|
this._preparePie();
|
|
8809
9951
|
} else if (this.flags.has('DOMINANT') && this.binding.value && !isAggregatedCategoricalChoropleth(this)) {
|
|
8810
9952
|
// .type("CHOROPLETH|DOMINANT") — a MULTI-field bound value
|
|
@@ -8956,6 +10098,7 @@ in vec4 vPieLineColor;
|
|
|
8956
10098
|
if (this._usesAggregationIndex()) {
|
|
8957
10099
|
this._buildAggregationIndex(this.features);
|
|
8958
10100
|
}
|
|
10101
|
+
if (this._isVectorChart()) this._prepareVector();
|
|
8959
10102
|
|
|
8960
10103
|
this._prepareAlphaField();
|
|
8961
10104
|
}
|
|
@@ -9049,6 +10192,36 @@ in vec4 vPieLineColor;
|
|
|
9049
10192
|
_resolveAggregateValue(props) { return resolveAggregateValue(this.binding, this.flags, props); }
|
|
9050
10193
|
|
|
9051
10194
|
_buildAggregationIndex(sourceFeatures) {
|
|
10195
|
+
// an AGGREGATE / CATEGORICAL pie: one bucket, each record carrying its
|
|
10196
|
+
// contribution to its item (flatPieRecord), added up per position or
|
|
10197
|
+
// grid cell by aggregateOnGrid (flatPieAccumulate)
|
|
10198
|
+
if (this._isAggregatedPie()) {
|
|
10199
|
+
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
10200
|
+
const fields = this._pieCategorical ? null : String(this.binding.value || '').split('|');
|
|
10201
|
+
const rules = flatValueRules(t, fields ? fields.length : 1);
|
|
10202
|
+
const sizeField = this.binding.size && this.binding.size !== '$item$' ? this.binding.size : null;
|
|
10203
|
+
const feats = [];
|
|
10204
|
+
for (const f of sourceFeatures) {
|
|
10205
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
10206
|
+
if (!c || typeof c[0] !== 'number' || !isFinite(c[0]) || !isFinite(c[1])) continue;
|
|
10207
|
+
const p = f.properties || {};
|
|
10208
|
+
let cat;
|
|
10209
|
+
if (this._pieCategorical) {
|
|
10210
|
+
const k = this._pieCategoryKey(p[this.binding.value]);
|
|
10211
|
+
const i = k == null ? undefined : this._pieCategoryIndex.get(k);
|
|
10212
|
+
cat = i == null ? -1 : i;
|
|
10213
|
+
}
|
|
10214
|
+
const pie = flatPieRecord(p, { rules, sizeField, cat, fields });
|
|
10215
|
+
if (!pie) continue;
|
|
10216
|
+
feats.push({ type: 'Feature', geometry: f.geometry, properties: { value: pie.size, pie, raw: p } });
|
|
10217
|
+
}
|
|
10218
|
+
this._featuresByCategory = [feats];
|
|
10219
|
+
this._clusterIndices = null;
|
|
10220
|
+
this._clusterRadiusPx = null;
|
|
10221
|
+
this._aggregateStatsCache = null;
|
|
10222
|
+
this._pieStatsCache = null;
|
|
10223
|
+
return;
|
|
10224
|
+
}
|
|
9052
10225
|
// Range-classed (non-CATEGORICAL numeric) coloring: DON'T pre-split
|
|
9053
10226
|
// by class here — the real engine aggregates every record together
|
|
9054
10227
|
// first and classes color from the resulting CELL TOTAL afterward
|
|
@@ -9231,7 +10404,10 @@ in vec4 vPieLineColor;
|
|
|
9231
10404
|
this._timeMin = undefined;
|
|
9232
10405
|
const facetFilters = this.facetFilters, runtimeFilterExpr = this._runtimeFilterExpr, timeFrame = this._timeFrame;
|
|
9233
10406
|
this.binding = this._specBinding;
|
|
9234
|
-
|
|
10407
|
+
// VECTOR / BEZIER keep every record: flat checks their values only
|
|
10408
|
+
// when it aggregates them (flatVectorItems)
|
|
10409
|
+
const withDensity = this._withDensity(fc.features);
|
|
10410
|
+
this.features = applyField100(isVectorChart(this.flags) ? withDensity : filterFlatValues(withDensity, this.binding, this.flags), this.binding, this.flags, this.style);
|
|
9235
10411
|
this.binding = field100Binding(this.binding, this.flags);
|
|
9236
10412
|
this._iconCache.clear();
|
|
9237
10413
|
this._glowIconCache.clear();
|
|
@@ -9274,10 +10450,14 @@ in vec4 vPieLineColor;
|
|
|
9274
10450
|
}
|
|
9275
10451
|
|
|
9276
10452
|
_usesAggregationIndex() {
|
|
10453
|
+
// AGGREGATE / CATEGORICAL pies aggregate their records into items
|
|
10454
|
+
// (see _buildAggregationIndex)
|
|
10455
|
+
if (this._isAggregatedPie()) return true;
|
|
9277
10456
|
// GRIDSIZE bins its own grid (_ensureGridIndex) — PLOT curves and the
|
|
9278
10457
|
// plain mesh; a classed GRIDSIZE symbol chart is aggregated like any
|
|
9279
10458
|
// other (see _isPlainGridMesh)
|
|
9280
|
-
|
|
10459
|
+
// VECTOR / BEZIER aggregate by their positions (flatVectorItems)
|
|
10460
|
+
return (this.flags.has('AGGREGATE') || isSymbolChart(this.flags)) && !isVectorChart(this.flags)
|
|
9281
10461
|
&& (!this.flags.has('GRIDSIZE') || (!this.flags.has('PLOT') && !this._isPlainGridMesh()));
|
|
9282
10462
|
}
|
|
9283
10463
|
|
|
@@ -9562,7 +10742,15 @@ in vec4 vPieLineColor;
|
|
|
9562
10742
|
return this._glowIconCache.get(key);
|
|
9563
10743
|
}
|
|
9564
10744
|
|
|
9565
|
-
|
|
10745
|
+
// look (optional, a CATEGORICAL BUBBLE's flat look — see bubbleIconLook):
|
|
10746
|
+
// every part filled at look.fillOpacity and stroked like its own flat
|
|
10747
|
+
// bubble (look.stroke, else its color × 0.7; look.strokeOpacity), the
|
|
10748
|
+
// width look.width canvas px for a part of the icon's full radius,
|
|
10749
|
+
// √(share of it) for a smaller one (flat's width ∝ √r) — OUTLINE:
|
|
10750
|
+
// ∝ min(1, r / normal radius), look.outlineRatio being the full
|
|
10751
|
+
// radius / normal radius. Without it the older look: 0.9 fill, a 1 px
|
|
10752
|
+
// white stroke (the SYMBOL branch's packed icon).
|
|
10753
|
+
_buildBubbleIcon(counts, colors, marked = null, shapes = null, look = null) {
|
|
9566
10754
|
// The cache key quantizes each count's RATIO to the group's total,
|
|
9567
10755
|
// not the raw count — computeBubblePackLayout's radii are purely
|
|
9568
10756
|
// sqrt(c/total), so two clusters with wildly different absolute
|
|
@@ -9592,7 +10780,8 @@ in vec4 vPieLineColor;
|
|
|
9592
10780
|
// coarse share buckets that can put a small marked part at 0
|
|
9593
10781
|
const key = counts.map(c => Math.round((c / total) * RATIO_BUCKETS)).join('-') + (marked
|
|
9594
10782
|
? '|iso:' + counts.map((c, i) => (c > 0 && marked.has(i) ? i + ':' + Math.round(Math.sqrt(c / total) * 100) : null)).filter(Boolean).join(',')
|
|
9595
|
-
: '') + (shapes ? '|sym:' + shapes.map(v => String(v).toLowerCase()).join(',') : '')
|
|
10783
|
+
: '') + (shapes ? '|sym:' + shapes.map(v => String(v).toLowerCase()).join(',') : '')
|
|
10784
|
+
+ (look ? `|fb:${look.fillOpacity}:${look.strokeOpacity}:${look.stroke ? look.stroke.join(',') : look.noStroke ? 'none' : 'low'}:${look.width}${look.outlineRatio != null ? ':' + look.outlineRatio : ''}` : '');
|
|
9596
10785
|
if (this._iconCache.has(key)) return this._iconCache.get(key);
|
|
9597
10786
|
const size = BUBBLE_ICON_SIZE;
|
|
9598
10787
|
const canvas = document.createElement('canvas');
|
|
@@ -9607,13 +10796,33 @@ in vec4 vPieLineColor;
|
|
|
9607
10796
|
// _resolveSymbolShape; circles when the name isn't one GL draws
|
|
9608
10797
|
const shape = shapes ? normalizeSymbolShape(shapes[Math.min(p.i, shapes.length - 1)]) : 'circle';
|
|
9609
10798
|
const px = cx + offsets[i].x * fitScale, py = cy + offsets[i].y * fitScale;
|
|
10799
|
+
let pr = radii[i] * fitScale, lw = 0;
|
|
10800
|
+
if (look && !look.noStroke) {
|
|
10801
|
+
const k = pr / (size / 2);
|
|
10802
|
+
lw = Math.min(pr, look.outlineRatio != null
|
|
10803
|
+
? look.width * Math.min(1, k * look.outlineRatio) / Math.min(1, look.outlineRatio)
|
|
10804
|
+
: look.width * Math.sqrt(k));
|
|
10805
|
+
// inside the part, as _buildSingleIcon: the icon's edge is the bubble's
|
|
10806
|
+
pr = Math.max(0.5, pr - lw / 2);
|
|
10807
|
+
}
|
|
9610
10808
|
ctx.beginPath();
|
|
9611
|
-
if (shape === 'circle') ctx.arc(px, py,
|
|
9612
|
-
else drawSymbolPath(ctx, shape, px, py,
|
|
10809
|
+
if (shape === 'circle') ctx.arc(px, py, pr, 0, Math.PI * 2);
|
|
10810
|
+
else drawSymbolPath(ctx, shape, px, py, pr);
|
|
9613
10811
|
ctx.closePath();
|
|
9614
10812
|
ctx.fillStyle = `rgb(${colors[p.i].join(',')})`;
|
|
9615
|
-
|
|
9616
|
-
|
|
10813
|
+
if (!look) {
|
|
10814
|
+
ctx.globalAlpha = 0.9; ctx.fill(); ctx.globalAlpha = 1;
|
|
10815
|
+
ctx.lineWidth = 1; ctx.strokeStyle = '#fff'; ctx.stroke();
|
|
10816
|
+
return;
|
|
10817
|
+
}
|
|
10818
|
+
ctx.globalAlpha = look.fillOpacity; ctx.fill();
|
|
10819
|
+
if (lw > 0) {
|
|
10820
|
+
ctx.globalAlpha = look.strokeOpacity;
|
|
10821
|
+
ctx.lineWidth = lw;
|
|
10822
|
+
ctx.strokeStyle = `rgb(${(look.stroke || flatDerivateRgb(colors[p.i], 0.7)).join(',')})`;
|
|
10823
|
+
ctx.stroke();
|
|
10824
|
+
}
|
|
10825
|
+
ctx.globalAlpha = 1;
|
|
9617
10826
|
});
|
|
9618
10827
|
const icon = { url: canvas.toDataURL(), width: size, height: size, anchorX: size / 2, anchorY: size / 2, id: key };
|
|
9619
10828
|
return this._cacheIcon(key, icon);
|
|
@@ -9642,10 +10851,12 @@ in vec4 vPieLineColor;
|
|
|
9642
10851
|
// cached bubble icon here already accepts; the border scales with
|
|
9643
10852
|
// the bubble exactly as the fill already does, which reads as
|
|
9644
10853
|
// reasonable rather than wrong.
|
|
9645
|
-
|
|
10854
|
+
// borderOpacity (default 1): the border's own alpha (flat's
|
|
10855
|
+
// stroke-opacity, see flatBubbleLook)
|
|
10856
|
+
_buildSingleIcon(colorRgb, opacity, shape, borderColorRgb, borderWidth, borderOpacity = 1) {
|
|
9646
10857
|
shape = shape || 'circle';
|
|
9647
10858
|
borderWidth = borderWidth || 0;
|
|
9648
|
-
const key = `single-${shape}-${colorRgb.join(',')}-${opacity}-${borderColorRgb ? borderColorRgb.join(',') : 'none'}-${borderWidth}`;
|
|
10859
|
+
const key = `single-${shape}-${colorRgb.join(',')}-${opacity}-${borderColorRgb ? borderColorRgb.join(',') : 'none'}-${borderWidth}${borderOpacity !== 1 ? `-so${borderOpacity}` : ''}`;
|
|
9649
10860
|
if (this._iconCache.has(key)) return this._iconCache.get(key);
|
|
9650
10861
|
const size = BUBBLE_ICON_SIZE;
|
|
9651
10862
|
const canvas = document.createElement('canvas');
|
|
@@ -9667,7 +10878,7 @@ in vec4 vPieLineColor;
|
|
|
9667
10878
|
ctx.globalAlpha = shape === 'empty' ? 0 : opacity;
|
|
9668
10879
|
ctx.fill();
|
|
9669
10880
|
if (borderWidth > 0 && borderColorRgb) {
|
|
9670
|
-
ctx.globalAlpha =
|
|
10881
|
+
ctx.globalAlpha = borderOpacity;
|
|
9671
10882
|
ctx.strokeStyle = `rgb(${borderColorRgb.join(',')})`;
|
|
9672
10883
|
ctx.lineWidth = borderWidth;
|
|
9673
10884
|
ctx.stroke();
|
|
@@ -9765,18 +10976,35 @@ in vec4 vPieLineColor;
|
|
|
9765
10976
|
local: {}
|
|
9766
10977
|
};
|
|
9767
10978
|
|
|
10979
|
+
// a VECTOR / BEZIER flow (measured on the twin page): no item chart,
|
|
10980
|
+
// label or class; its value is flat's nValuesA[0] — with CATEGORICAL
|
|
10981
|
+
// the category's number (1 for the first), else the (summed) value
|
|
10982
|
+
// field; its count the records of the item
|
|
10983
|
+
if (this._isVectorChart() && props.vector) {
|
|
10984
|
+
const v = props.vector.itemValue;
|
|
10985
|
+
dataObj.theme.chart = dataObj.theme.item.chart = '';
|
|
10986
|
+
dataObj.theme.item.value = v == null || isNaN(v) ? '' : flatFormatValue(v, explicitValueDecimals(this.style) ?? (Math.abs(v) < 1 ? 2 : 0), 'ROUND');
|
|
10987
|
+
dataObj.theme.item.label = dataObj.theme.item.class = '';
|
|
10988
|
+
dataObj.theme.item.count = props.count;
|
|
10989
|
+
}
|
|
10990
|
+
|
|
9768
10991
|
// bare {{FIELD}}, {{raw.FIELD}}, {{local.FIELD}} — only meaningful
|
|
9769
|
-
// for a single unaggregated record (see class comment above)
|
|
9770
|
-
|
|
9771
|
-
|
|
10992
|
+
// for a single unaggregated record (see class comment above); an
|
|
10993
|
+
// aggregated pie's item gives its first record's, as flat (data[0])
|
|
10994
|
+
const fieldsRaw = isGroup ? props.firstRaw : props.raw;
|
|
10995
|
+
if (fieldsRaw) {
|
|
10996
|
+
Object.keys(fieldsRaw).forEach(field => {
|
|
9772
10997
|
if (field === 'geometry') return;
|
|
9773
|
-
const v =
|
|
10998
|
+
const v = fieldsRaw[field];
|
|
9774
10999
|
if (this._isNumericValue(v)) {
|
|
9775
11000
|
dataObj[field] = this._formatTooltipValue(v);
|
|
9776
11001
|
dataObj.raw[field] = v;
|
|
9777
11002
|
} else {
|
|
9778
11003
|
dataObj[field] = v;
|
|
9779
|
-
|
|
11004
|
+
// translated, as flat's tooltip (tooltip_mustache.js 446:
|
|
11005
|
+
// ixmaps.getLocalString(value)) — the migration page's ISO
|
|
11006
|
+
// code → country name in {{local.from}} → {{local.to}}
|
|
11007
|
+
dataObj.local[field] = makeLocalText([])(v);
|
|
9780
11008
|
}
|
|
9781
11009
|
});
|
|
9782
11010
|
}
|
|
@@ -10062,11 +11290,21 @@ in vec4 vPieLineColor;
|
|
|
10062
11290
|
// USER charts (a page's own chart function, style.userdraw) go the
|
|
10063
11291
|
// chart pipeline too, drawn by _buildUserChartLayers (bubbles when the
|
|
10064
11292
|
// page has no such function)
|
|
11293
|
+
// VECTOR / BEZIER (flat's chartMap draws them before it calls
|
|
11294
|
+
// drawChart, maptheme.js 18148, 18376)
|
|
10065
11295
|
// PIE / DONUT (flat's PIE branch comes before BUBBLE and SYMBOL in
|
|
10066
11296
|
// drawChart, maptheme.js 19939)
|
|
11297
|
+
if (this._isVectorChart()) return this._buildVectorLayers(zoom, bbox, liveZoom, globeCenter);
|
|
10067
11298
|
if (this._isPieChart()) return this._buildPieLayers(zoom, bbox, liveZoom, globeCenter);
|
|
11299
|
+
// BAR / BARS (flat's BAR branch, after BUBBLE and SYMBOL: a type
|
|
11300
|
+
// with those is not a bar chart, see isBarChart)
|
|
11301
|
+
if (this._isBarChart()) return this._buildBarLayers(zoom, bbox, liveZoom, globeCenter);
|
|
10068
11302
|
if (isSymbolChart(this.flags)) return this._buildChartLayers(zoom, bbox, liveZoom, globeCenter);
|
|
10069
|
-
|
|
11303
|
+
// once per theme, not on every redraw
|
|
11304
|
+
if (!this._warnedNoRenderer) {
|
|
11305
|
+
this._warnedNoRenderer = true;
|
|
11306
|
+
console.warn(`[ixmaps-gl] layer "${this.name}": type "${[...this.flags].join('|')}" has no implemented renderer`);
|
|
11307
|
+
}
|
|
10070
11308
|
return [];
|
|
10071
11309
|
}
|
|
10072
11310
|
|
|
@@ -10428,16 +11666,26 @@ in vec4 vPieLineColor;
|
|
|
10428
11666
|
return String(this.binding.value || '').split('|');
|
|
10429
11667
|
}
|
|
10430
11668
|
// CHART|PIE (and PIE|DONUT): one pie per record, its slices the value
|
|
10431
|
-
// fields
|
|
10432
|
-
// a
|
|
10433
|
-
//
|
|
11669
|
+
// fields; AGGREGATE and CATEGORICAL pies one pie per item — the records
|
|
11670
|
+
// of a position (or grid cell) — its slices the categories or the value
|
|
11671
|
+
// fields (_isAggregatedPie, _buildPieLayers)
|
|
11672
|
+
_isVectorChart() { return isVectorChart(this.flags); }
|
|
11673
|
+
// CHART|BAR / BARS: a bar chart per record or item, its bars a pie's
|
|
11674
|
+
// parts (_buildBarLayers)
|
|
11675
|
+
_isBarChart() { return isBarChart(this.flags); }
|
|
10434
11676
|
_isPieChart() {
|
|
10435
|
-
return this.flags.has('CHART') && this.flags.has('PIE') && !this.flags.has('
|
|
10436
|
-
|
|
11677
|
+
return !isVectorChart(this.flags) && this.flags.has('CHART') && this.flags.has('PIE') && !this.flags.has('DOT');
|
|
11678
|
+
}
|
|
11679
|
+
// a pie whose items flat aggregates (loadAndAggregateValuesOfTheme):
|
|
11680
|
+
// AGGREGATE, or CATEGORICAL (a part per category — flat sums the
|
|
11681
|
+
// records of an item's position by category even without AGGREGATE)
|
|
11682
|
+
// (a bar chart's items too)
|
|
11683
|
+
_isAggregatedPie() {
|
|
11684
|
+
return (this._isPieChart() || this._isBarChart()) && (this.flags.has('AGGREGATE') || this.flags.has('CATEGORICAL'));
|
|
10437
11685
|
}
|
|
10438
11686
|
// a per-item chart whose parts are its value fields (legend rows, sums)
|
|
10439
11687
|
_isMultiFieldChart() {
|
|
10440
|
-
return !!this._multiFields && (this._isMultiFieldSequence() || this._isPieChart());
|
|
11688
|
+
return !!this._multiFields && (this._isMultiFieldSequence() || this._isPieChart() || this._isBarChart());
|
|
10441
11689
|
}
|
|
10442
11690
|
// flat's theme statistics for a pie (maptheme.js 9119-9130): nMin/nMax
|
|
10443
11691
|
// over every part value of every item, the classes (partsA, flat's
|
|
@@ -10445,6 +11693,8 @@ in vec4 vPieLineColor;
|
|
|
10445
11693
|
// colors) — flat colors a 1-part pie (or any with CLASSES) by them
|
|
10446
11694
|
// (maptheme.js 20330-20341); several parts are colored by field.
|
|
10447
11695
|
_preparePie() {
|
|
11696
|
+
if (this.flags.has('CATEGORICAL')) { this._preparePieCategories(); return; }
|
|
11697
|
+
this._pieCategorical = false;
|
|
10448
11698
|
const fields = String(this.binding.value).split('|');
|
|
10449
11699
|
const pooled = [];
|
|
10450
11700
|
for (const f of this.features) {
|
|
@@ -10472,6 +11722,61 @@ in vec4 vPieLineColor;
|
|
|
10472
11722
|
}
|
|
10473
11723
|
}
|
|
10474
11724
|
|
|
11725
|
+
// a CATEGORICAL pie's categories (flat's getStringValueIndex, maptheme.js
|
|
11726
|
+
// 9204-9243): style.values in their order, else the values in the order
|
|
11727
|
+
// the records bring them; a value is trimmed (IGNORECASE: upper case),
|
|
11728
|
+
// an empty one is none (UNDEFINEDISVALUE: "undefined"). A slice per
|
|
11729
|
+
// category in its colorscheme color, labeled by style.label. flat's
|
|
11730
|
+
// classes are the category numbers (partsA: i + 1, the last one 0.001
|
|
11731
|
+
// wider) — its value range (nMin / nMax) follows the items
|
|
11732
|
+
// (_pieAggregateStats).
|
|
11733
|
+
_preparePieCategories() {
|
|
11734
|
+
const st = this.style;
|
|
11735
|
+
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
11736
|
+
const ignoreCase = /IGNORECASE/.test(t), undefinedIsValue = /UNDEFINEDISVALUE/.test(t);
|
|
11737
|
+
const key = v => {
|
|
11738
|
+
let k = v == null ? '' : String(v);
|
|
11739
|
+
if (k.length === 0 || k === ' ') { if (!undefinedIsValue) return null; k = 'undefined'; }
|
|
11740
|
+
if (ignoreCase) k = k.toUpperCase();
|
|
11741
|
+
return k.trim();
|
|
11742
|
+
};
|
|
11743
|
+
const index = new Map();
|
|
11744
|
+
const explicit = Array.isArray(st.values) ? st.values : null;
|
|
11745
|
+
const labels = [];
|
|
11746
|
+
if (explicit) {
|
|
11747
|
+
explicit.forEach(v => { const k = key(v); if (k != null && !index.has(k)) { index.set(k, labels.length); labels.push(String(v).trim()); } });
|
|
11748
|
+
} else {
|
|
11749
|
+
for (const f of this.features) {
|
|
11750
|
+
const k = key(f.properties[this.binding.value]);
|
|
11751
|
+
if (k != null && !index.has(k)) { index.set(k, labels.length); labels.push(k); }
|
|
11752
|
+
}
|
|
11753
|
+
}
|
|
11754
|
+
this._pieCategorical = true;
|
|
11755
|
+
this._pieExplicitValues = !!explicit;
|
|
11756
|
+
this._pieCategoryKey = key;
|
|
11757
|
+
this._pieCategoryIndex = index;
|
|
11758
|
+
this.categoryLabels = labels;
|
|
11759
|
+
const explicitLabel = Array.isArray(st.label) ? st.label.map(String) : null;
|
|
11760
|
+
this.categoryDisplayLabels = explicitLabel && explicitLabel.length === labels.length ? explicitLabel : labels;
|
|
11761
|
+
this.categoryColorsRgb = resolveClassColors(st.colorscheme, this.categoryDisplayLabels, st.classes);
|
|
11762
|
+
// the legend and the facets look a record's raw value up
|
|
11763
|
+
const byLabel = new Map();
|
|
11764
|
+
for (const f of this.features) {
|
|
11765
|
+
const v = f.properties[this.binding.value];
|
|
11766
|
+
if (byLabel.has(v)) continue;
|
|
11767
|
+
const k = key(v);
|
|
11768
|
+
if (k != null && index.has(k)) byLabel.set(v, index.get(k));
|
|
11769
|
+
}
|
|
11770
|
+
labels.forEach((l, i) => { if (!byLabel.has(l)) byLabel.set(l, i); });
|
|
11771
|
+
this.categoryIndexByLabel = byLabel;
|
|
11772
|
+
const n = labels.length;
|
|
11773
|
+
this.partsA = labels.map((l, i) => ({ min: i + 1, max: i + 1 + (i === n - 1 ? 0.001 : 0) + 1e-9 }));
|
|
11774
|
+
this._rangeClassed = false;
|
|
11775
|
+
this._multiFields = null;
|
|
11776
|
+
this._pieFields = null;
|
|
11777
|
+
this._partNoFill = new Set();
|
|
11778
|
+
}
|
|
11779
|
+
|
|
10475
11780
|
// x-axis labels of a per-item PLOT (style.xaxis / label, else the fields)
|
|
10476
11781
|
_itemPlotLabels() {
|
|
10477
11782
|
const fields = this._itemPlotFields();
|
|
@@ -11602,8 +12907,7 @@ in vec4 vPieLineColor;
|
|
|
11602
12907
|
const combined = individual.concat(groups).sort((a, b) => sizeValueOf(a) - sizeValueOf(b));
|
|
11603
12908
|
// maxcharts: like the flat engine (maptheme.js nMaxCharts), draw only the
|
|
11604
12909
|
// biggest N items — combined is ascending, so drop from the front
|
|
11605
|
-
|
|
11606
|
-
if (maxCharts > 0 && combined.length > maxCharts) combined.splice(0, combined.length - maxCharts);
|
|
12910
|
+
applyFlatMaxCharts(combined, this.style);
|
|
11607
12911
|
if (this.flags.has('USER')) {
|
|
11608
12912
|
const userLayers = this._buildUserChartLayers(combined, liveZoom, sizeValueOf);
|
|
11609
12913
|
if (userLayers) return layers.concat(userLayers);
|
|
@@ -11884,6 +13188,19 @@ in vec4 vPieLineColor;
|
|
|
11884
13188
|
// fillopacity and linecolor, as a single item (flat draws one
|
|
11885
13189
|
// symbol) — the packed multi-category icon is for CATEGORICAL cells
|
|
11886
13190
|
const singleSymbol = d => !d.properties.counts || (this._rangeClassed && d.properties.cat != null);
|
|
13191
|
+
// a CATEGORICAL BUBBLE (flat's BUBBLE branch): every bubble — a single
|
|
13192
|
+
// one and each part of a packed one — filled and stroked as flat's,
|
|
13193
|
+
// see flatBubbleLook; the icon carries both opacities
|
|
13194
|
+
const flatLook = flatChartBranch(this.flags) === 'bubble' && this.flags.has('CATEGORICAL')
|
|
13195
|
+
? d => bubbleIconLook(radiusOf(d), objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1), this.style, this.flags, BUBBLE_ICON_SIZE)
|
|
13196
|
+
: null;
|
|
13197
|
+
const singleIcon = d => {
|
|
13198
|
+
const rgb = (this.categoryColorsRgb || [])[d.properties.cat];
|
|
13199
|
+
if (!flatLook) return this._buildSingleIcon(rgb, fillOpacity, shapeOf(d.properties), singleBorderColorRgb, singleBorderWidthPx);
|
|
13200
|
+
const look = flatLook(d);
|
|
13201
|
+
return this._buildSingleIcon(rgb, look.fillOpacity, shapeOf(d.properties),
|
|
13202
|
+
look.noStroke || !rgb ? null : (look.stroke || flatDerivateRgb(rgb, 0.7)), look.width, look.strokeOpacity);
|
|
13203
|
+
};
|
|
11887
13204
|
// flat's symbol outline (maptheme.js 21595-21610 and 21950-21997,
|
|
11888
13205
|
// drawn 21625-21634 / 21995-22035): every SYMBOL shape is stroked,
|
|
11889
13206
|
// (linewidth || 1) · normalX(min(2r / rMax, 0.2)) with rMax the
|
|
@@ -11919,17 +13236,21 @@ in vec4 vPieLineColor;
|
|
|
11919
13236
|
// icon's look, inside the radius
|
|
11920
13237
|
if (combined.length && !alignOf.active && combined.every(d => singleSymbol(d) && shapeOf(d.properties) === 'circle')) {
|
|
11921
13238
|
const radiusPx = d => vRadius(sizeValueOf(d));
|
|
11922
|
-
|
|
13239
|
+
// a CATEGORICAL BUBBLE: flat's look (flatBubbleLook) in screen px,
|
|
13240
|
+
// its stroke centered on the radius as flat's SVG circle
|
|
13241
|
+
const lookUnit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1);
|
|
13242
|
+
const circleLook = flatLook ? d => flatBubbleLook((this.categoryColorsRgb || [])[d.properties.cat] || [128, 128, 128], radiusPx(d), lookUnit, this.style, this.flags) : null;
|
|
13243
|
+
const lineRgb = d => (circleLook ? circleLook(d).stroke : outlineRgb(d)) || [0, 0, 0];
|
|
11923
13244
|
layers.push(new ScatterplotLayer({
|
|
11924
13245
|
id: `ix-bubbles-${this.name}-circles-g${this._iconGeneration}`,
|
|
11925
13246
|
data: combined, pickable: true,
|
|
11926
13247
|
getPosition: d => d.geometry.coordinates,
|
|
11927
|
-
getRadius: d => radiusPx(d) - (symbolOutline ? 0 : outlinePx(d, radiusPx(d)) / 2),
|
|
13248
|
+
getRadius: d => radiusPx(d) - (symbolOutline || circleLook ? 0 : outlinePx(d, radiusPx(d)) / 2),
|
|
11928
13249
|
radiusUnits: 'pixels',
|
|
11929
|
-
getFillColor: d => [...((this.categoryColorsRgb || [])[d.properties.cat] || [128, 128, 128]).slice(0, 3), Math.round(this._iconAlpha(d) * fillOpacity)],
|
|
11930
|
-
stroked: combined.some(d => outlineRgb(d)),
|
|
11931
|
-
getLineColor: d => [...lineRgb(d).slice(0, 3), this._iconAlpha(d)],
|
|
11932
|
-
getLineWidth: d => outlinePx(d, radiusPx(d)),
|
|
13250
|
+
getFillColor: d => [...((this.categoryColorsRgb || [])[d.properties.cat] || [128, 128, 128]).slice(0, 3), Math.round(this._iconAlpha(d) * (circleLook ? circleLook(d).fillOpacity : fillOpacity))],
|
|
13251
|
+
stroked: circleLook ? combined.some(d => circleLook(d).stroke) : combined.some(d => outlineRgb(d)),
|
|
13252
|
+
getLineColor: d => [...lineRgb(d).slice(0, 3), Math.round(this._iconAlpha(d) * (circleLook ? circleLook(d).strokeOpacity : 1))],
|
|
13253
|
+
getLineWidth: d => (circleLook ? (circleLook(d).stroke ? circleLook(d).width : 0) : outlinePx(d, radiusPx(d))),
|
|
11933
13254
|
lineWidthUnits: 'pixels',
|
|
11934
13255
|
billboard: true,
|
|
11935
13256
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
@@ -11939,7 +13260,8 @@ in vec4 vPieLineColor;
|
|
|
11939
13260
|
data: combined, pickable: true,
|
|
11940
13261
|
getPosition: d => d.geometry.coordinates,
|
|
11941
13262
|
getIcon: d => {
|
|
11942
|
-
if (!singleSymbol(d)) return this._buildBubbleIcon(d.properties.counts, this.categoryColorsRgb, this._groupIsolation(d), Array.isArray(this.style.symbols) ? this.style.symbols : null);
|
|
13263
|
+
if (!singleSymbol(d)) return this._buildBubbleIcon(d.properties.counts, this.categoryColorsRgb, this._groupIsolation(d), Array.isArray(this.style.symbols) ? this.style.symbols : null, flatLook && flatLook(d));
|
|
13264
|
+
if (flatLook) return singleIcon(d);
|
|
11943
13265
|
const shape = shapeOf(d.properties), rgb = outlineRgb(d);
|
|
11944
13266
|
if (!symbolOutline || !rgb) return this._buildSingleIcon((this.categoryColorsRgb || [])[d.properties.cat], fillOpacity, shape, rgb, rgb ? singleBorderWidthPx : 0);
|
|
11945
13267
|
// the outline in the icon's own pixels, inside its edge; quarter
|
|
@@ -11952,7 +13274,8 @@ in vec4 vPieLineColor;
|
|
|
11952
13274
|
getSize: d => vRadius(sizeValueOf(d)) * 2,
|
|
11953
13275
|
// group icons bake a fixed 0.9 alpha; an explicit fillopacity scales it
|
|
11954
13276
|
// so a near-transparent theme doesn't show opaque 1-px-cell clusters
|
|
11955
|
-
|
|
13277
|
+
// (a flat-look icon carries its own fill and stroke opacity)
|
|
13278
|
+
getColor: d => [255, 255, 255, Math.round(this._iconAlpha(d) * (!flatLook && !singleSymbol(d) && this.style.fillopacity != null ? fillOpacity : 1))],
|
|
11956
13279
|
...(alignOf.active ? { getPixelOffset: alignOf } : {}),
|
|
11957
13280
|
sizeUnits: 'pixels',
|
|
11958
13281
|
billboard: true,
|
|
@@ -12361,7 +13684,18 @@ in vec4 vPieLineColor;
|
|
|
12361
13684
|
// unit.
|
|
12362
13685
|
// - draw order: flat's sort before drawing (by the size value, the
|
|
12363
13686
|
// sum or the first value; the biggest on top)
|
|
12364
|
-
//
|
|
13687
|
+
// - AGGREGATE / CATEGORICAL (_isAggregatedPie): a pie per item — the
|
|
13688
|
+
// records of a position, or of an aggregation grid cell —
|
|
13689
|
+
// (_pieAggregatedItems; flatPieRecord / flatPieAccumulate /
|
|
13690
|
+
// flatPieItemValues), a slice per category (CATEGORICAL, in its
|
|
13691
|
+
// category color) or per value field; its size the item's summed
|
|
13692
|
+
// size field (flat's nSize, every record of the item counted), the
|
|
13693
|
+
// theme's nMin / nMax / nMaxSize those of the items
|
|
13694
|
+
// (_pieAggregateStats); draw order by the item's size with a size
|
|
13695
|
+
// field or AGGREGATE|SUM; COUNT prints a part's record count;
|
|
13696
|
+
// CENTERVALUE the item's record count
|
|
13697
|
+
// Not drawn here (a console note at type parsing, PIE_INERT_FLAGS,
|
|
13698
|
+
// PIE_AGGREGATE_INERT_FLAGS):
|
|
12365
13699
|
// 3D, VOLUME, HEIGHT, STARBURST, FLOWER/RAYS, BOW, ZOOM, DIRECTION /
|
|
12366
13700
|
// DIRECTED, POLAR — the pie is drawn flat and plain instead. Nor
|
|
12367
13701
|
// style.clipparts, showparts, gridx (flat's several pies per item).
|
|
@@ -12375,15 +13709,20 @@ in vec4 vPieLineColor;
|
|
|
12375
13709
|
const st = this.style;
|
|
12376
13710
|
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
12377
13711
|
const has = re => re.test(t);
|
|
12378
|
-
const
|
|
12379
|
-
const
|
|
13712
|
+
const agg = this._isAggregatedPie();
|
|
13713
|
+
const cat = !!this._pieCategorical;
|
|
13714
|
+
const fields = cat ? null : (this._pieFields || String(this.binding.value || '').split('|'));
|
|
13715
|
+
const multi = !cat && fields.length > 1;
|
|
13716
|
+
// AGGREGATE / CATEGORICAL: the statistics of the items (sets
|
|
13717
|
+
// _valueMin / _valueMax), their biggest size flat's nMaxSize
|
|
13718
|
+
const stats = agg ? this._pieAggregateStats(zoom) : null;
|
|
12380
13719
|
const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(st.scale) || 1);
|
|
12381
13720
|
const sizePow = resolveSizePow(st, this.flags);
|
|
12382
13721
|
// flat tests /SIZE/: also NOSIZE and GRIDSIZE
|
|
12383
13722
|
const sizeFlag = has(/SIZE/) && !has(/NORMSIZE/);
|
|
12384
13723
|
const sizeLog = has(/SIZELOG/);
|
|
12385
13724
|
const nsv = styleNum(st.normalsizevalue);
|
|
12386
|
-
const maxSize = nsv || this._maxSizeValue;
|
|
13725
|
+
const maxSize = nsv || (stats ? stats.maxSize : this._maxSizeValue);
|
|
12387
13726
|
const range = nsv || ((this._valueMax || 0) - (this._valueMin || 0));
|
|
12388
13727
|
const nSizeOf = (sum, size) => {
|
|
12389
13728
|
const n = NORMAL_RADIUS_PX;
|
|
@@ -12398,14 +13737,17 @@ in vec4 vPieLineColor;
|
|
|
12398
13737
|
const colors = this.categoryColorsRgb || [];
|
|
12399
13738
|
const scheme = Array.isArray(st.colorscheme) ? st.colorscheme : [];
|
|
12400
13739
|
const noFill = this._partNoFill || new Set();
|
|
12401
|
-
const byClass = has(/CLASSES/) || !multi;
|
|
13740
|
+
const byClass = has(/CLASSES/) || (!cat && !multi);
|
|
13741
|
+
// flat (maptheme.js 20327-20336): a pie of one part (or CLASSES) takes
|
|
13742
|
+
// the color of the class its value falls in — CATEGORICAL: the class
|
|
13743
|
+
// whose number equals the value (sl.single: the item's only part)
|
|
12402
13744
|
const classColor = v => {
|
|
12403
13745
|
const parts = this.partsA || [];
|
|
12404
|
-
for (let x = 0; x < parts.length; x++) if (v < parts[x].max) return colors[x];
|
|
13746
|
+
for (let x = 0; x < parts.length; x++) if (cat ? v === parts[x].min : v < parts[x].max) return colors[x];
|
|
12405
13747
|
return null;
|
|
12406
13748
|
};
|
|
12407
13749
|
const sliceRgb = sl => (sl.complement ? (parseCssColor(String(st.nodatacolor || '')) || [0xee, 0xee, 0xee])
|
|
12408
|
-
: (byClass && classColor(sl.value)) || colors[sl.i] || [128, 128, 128]);
|
|
13750
|
+
: ((byClass || sl.single) && classColor(sl.value)) || colors[sl.i] || [128, 128, 128]);
|
|
12409
13751
|
const fo = styleNum(st.fillopacity), op = styleNum(st.opacity);
|
|
12410
13752
|
// flat sets the group's fill-opacity only for a truthy value
|
|
12411
13753
|
const fillAlpha = op > 0 ? op : fo > 0 ? fo : 1;
|
|
@@ -12415,13 +13757,39 @@ in vec4 vPieLineColor;
|
|
|
12415
13757
|
if (has(/WHITELINES/)) line = 'white';
|
|
12416
13758
|
const lc = st.linecolor != null && st.linecolor !== '' ? styleLineColor(st.linecolor) : null;
|
|
12417
13759
|
if (lc) line = lc;
|
|
12418
|
-
if (scheme.slice(0, fields.length).some(c => String(c).trim().toLowerCase() === 'none')) line = lc || 'black';
|
|
13760
|
+
if (!cat && scheme.slice(0, fields.length).some(c => String(c).trim().toLowerCase() === 'none')) line = lc || 'black';
|
|
12419
13761
|
const lineRgb = line === 'none' ? null : hexOrNamedToRgb(line);
|
|
12420
13762
|
const lineWidthOf = r => (lineRgb ? (styleNum(st.linewidth) || 0.1) * r / 15 : 0);
|
|
12421
13763
|
const lineRgba = lineRgb ? [...lineRgb.slice(0, 3), Math.round(255 * lineAlpha)] : [0, 0, 0, 0];
|
|
12422
13764
|
|
|
12423
13765
|
const charts = [];
|
|
12424
|
-
|
|
13766
|
+
if (agg) {
|
|
13767
|
+
// one pie per item (_pieAggregatedItems), its parts and size as flat
|
|
13768
|
+
// has them after aggregation (flatPieItemValues)
|
|
13769
|
+
for (const f of this._pieAggregatedItems(zoom, bbox)) {
|
|
13770
|
+
const c = f.geometry.coordinates;
|
|
13771
|
+
if (globeCenter && !isOnVisibleHemisphere(c[0], c[1], globeCenter)) continue;
|
|
13772
|
+
const pie = f.properties.pie;
|
|
13773
|
+
const { parts: values, size } = this._pieItemValues(pie);
|
|
13774
|
+
const layout = pieSliceLayout(values, t, { centerPart: st.centerpart, itemCount: pie.n });
|
|
13775
|
+
if (!layout) continue;
|
|
13776
|
+
if (cat) layout.slices.forEach(sl => { sl.single = values.length === 1; });
|
|
13777
|
+
const sum = values.reduce((a, v) => a + (isNaN(v) ? 0 : v), 0);
|
|
13778
|
+
const r = nSizeOf(sum, size) * unit;
|
|
13779
|
+
if (!(r > 0) || !isFinite(r)) continue;
|
|
13780
|
+
// flat's draw-order key (maptheme.js 16812-16860): with a size
|
|
13781
|
+
// field or AGGREGATE|SUM the item's size, else its first part
|
|
13782
|
+
const key = this.binding.size || (has(/AGGREGATE/) && has(/SUM/)) ? size : values[0];
|
|
13783
|
+
// the tooltip: a row per part (counts), its value the parts' sum
|
|
13784
|
+
// (flat: the sum of nValuesA), its records (theme.item.data, the
|
|
13785
|
+
// page's own {{field}}s from the first, as flat's data[0])
|
|
13786
|
+
const raws = f.properties.raws || (f.properties.raw ? [f.properties.raw] : []);
|
|
13787
|
+
const properties = { counts: values, total: sum, recordCounts: [pie.n], raws, firstRaw: raws[0] || null,
|
|
13788
|
+
titleRaw: f.properties.titleRaw || f.properties.raw || null };
|
|
13789
|
+
charts.push({ c, r, layout, size, count: pie.n, partCounts: pie.counts, nParts: values.length, key, properties });
|
|
13790
|
+
}
|
|
13791
|
+
}
|
|
13792
|
+
for (const f of agg ? [] : this._activeFeatures || this.features) {
|
|
12425
13793
|
const c = f.geometry && f.geometry.coordinates;
|
|
12426
13794
|
if (!c || typeof c[0] !== 'number') continue;
|
|
12427
13795
|
const [lng, lat] = c;
|
|
@@ -12442,16 +13810,19 @@ in vec4 vPieLineColor;
|
|
|
12442
13810
|
// 1-field pie its class (cat) like a range-classed bubble
|
|
12443
13811
|
const properties = multi ? { value: values[0], parts: values, raw }
|
|
12444
13812
|
: { value: values[0], cat: this.partsA ? this._resolvePartsClass(values[0]) : 0, raw };
|
|
12445
|
-
charts.push({ c, r, layout, size, key, properties });
|
|
13813
|
+
charts.push({ c, r, layout, size, nParts: values.length, key, properties });
|
|
12446
13814
|
}
|
|
12447
13815
|
// flat sorts the charts before drawing (maptheme.js 16774-16870),
|
|
12448
13816
|
// ascending — the last drawn on top —, SORT|UP descending; not with
|
|
12449
|
-
// NOSRT, NOSIZE (unless SORT) or when all values are equal
|
|
13817
|
+
// NOSRT, NOSIZE (unless SORT) or when all values are equal, nor a
|
|
13818
|
+
// CATEGORICAL pie without AGGREGATE, size field or SORT
|
|
12450
13819
|
if (((this._valueMin !== this._valueMax) || this.binding.size) && !has(/NOSRT/)
|
|
12451
|
-
&& (!has(/NOSIZE/) || has(/\bSORT\b/))
|
|
13820
|
+
&& (!has(/NOSIZE/) || has(/\bSORT\b/))
|
|
13821
|
+
&& (!has(/CATEGORICAL/) || has(/AGGREGATE/) || this.binding.size || has(/\bSORT\b/))) {
|
|
12452
13822
|
const down = has(/\bSORT\b/) && has(/\bUP\b/);
|
|
12453
13823
|
charts.sort((a, b) => (down ? b.key - a.key : a.key - b.key) || 0);
|
|
12454
13824
|
}
|
|
13825
|
+
applyFlatMaxCharts(charts, st);
|
|
12455
13826
|
// align / offsetx / offsety (flatChartAlignOffset) — flat's PIE
|
|
12456
13827
|
// ptNull is (0, r + 5 units); the chart's half size taken as r, as
|
|
12457
13828
|
// for BUBBLE (ASSUMPTION: not measured on flat for PIE)
|
|
@@ -12508,6 +13879,74 @@ in vec4 vPieLineColor;
|
|
|
12508
13879
|
return layers.concat(this._buildPieTextLayers(charts, zoom, liveZoom, unit, sliceRgb, has));
|
|
12509
13880
|
}
|
|
12510
13881
|
|
|
13882
|
+
// the items of an AGGREGATE / CATEGORICAL pie within bbox: its records
|
|
13883
|
+
// (_buildAggregationIndex) added up per item by aggregateOnGrid — with
|
|
13884
|
+
// AGGREGATE on the theme's aggregation grid (gridwidth, aggregation …,
|
|
13885
|
+
// see _ensureClusterIndices; none: per position), else per position
|
|
13886
|
+
// (flat sums a CATEGORICAL theme's records of one shape by category
|
|
13887
|
+
// without AGGREGATE too). Each item's properties.pie: flatPieAccumulate's.
|
|
13888
|
+
_pieAggregatedItems(zoom, bbox) {
|
|
13889
|
+
if (!this._featuresByCategory) return [];
|
|
13890
|
+
if (this.flags.has('AGGREGATE')) {
|
|
13891
|
+
this._ensureClusterIndices(zoom);
|
|
13892
|
+
return this._clusterIndices[0].getClusters(bbox, this._clusterUsesFixedZoom ? GRIDWIDTH_METERS_REFERENCE_ZOOM : zoom);
|
|
13893
|
+
}
|
|
13894
|
+
const feats = this._featuresByCategory[0];
|
|
13895
|
+
if (!this._pieExactIndex || this._pieExactIndex.features !== feats) {
|
|
13896
|
+
this._pieExactIndex = new GridAggregateIndex(null, this.flags, undefined, null).load(feats);
|
|
13897
|
+
}
|
|
13898
|
+
return this._pieExactIndex.getClusters(bbox, zoom);
|
|
13899
|
+
}
|
|
13900
|
+
|
|
13901
|
+
// an aggregated pie item's parts (a CATEGORICAL pie with style.values:
|
|
13902
|
+
// one per category) and size (flatPieItemValues)
|
|
13903
|
+
_pieItemValues(pie) {
|
|
13904
|
+
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
13905
|
+
const nParts = this._pieCategorical ? (this._pieExplicitValues ? this.categoryLabels.length : null) : (this._pieFields || []).length;
|
|
13906
|
+
return flatPieItemValues(pie, t, { categorical: !!this._pieCategorical, nParts });
|
|
13907
|
+
}
|
|
13908
|
+
|
|
13909
|
+
// flat's statistics of an aggregated pie theme (maptheme.js
|
|
13910
|
+
// 11180-11241), over every item (at this zoom when a grid aggregates):
|
|
13911
|
+
// nMin / nMax over the items' sizes (CATEGORICAL or a size field), else
|
|
13912
|
+
// over their parts; nMaxSize the biggest size; the classes of a
|
|
13913
|
+
// value-field pie (partsA) over the items' parts, as for a per-record
|
|
13914
|
+
// pie (_preparePie). Sets the theme's _valueMin / _valueMax / partsA.
|
|
13915
|
+
_pieAggregateStats(zoom) {
|
|
13916
|
+
const feats = this._featuresByCategory && this._featuresByCategory[0];
|
|
13917
|
+
const zkey = this.flags.has('AGGREGATE') ? zoom : null;
|
|
13918
|
+
const cache = this._pieStatsCache;
|
|
13919
|
+
if (cache && cache.feats === feats && cache.zoom === zkey) return cache;
|
|
13920
|
+
let nMin = Infinity, nMax = -Infinity, maxSize = -Infinity;
|
|
13921
|
+
const pooled = [];
|
|
13922
|
+
const bySize = !!this._pieCategorical || !!(this.binding.size && this.binding.size !== '$item$');
|
|
13923
|
+
for (const f of this._pieAggregatedItems(zoom, [-180, -90, 180, 90])) {
|
|
13924
|
+
const { parts, size } = this._pieItemValues(f.properties.pie);
|
|
13925
|
+
if (isFinite(size) && size > maxSize) maxSize = size;
|
|
13926
|
+
for (const v of parts) if (isFinite(v)) pooled.push(v);
|
|
13927
|
+
if (bySize) {
|
|
13928
|
+
if (isFinite(size)) { if (size < nMin) nMin = size; if (size > nMax) nMax = size; }
|
|
13929
|
+
} else {
|
|
13930
|
+
for (const v of parts) if (isFinite(v)) { if (v < nMin) nMin = v; if (v > nMax) nMax = v; }
|
|
13931
|
+
}
|
|
13932
|
+
}
|
|
13933
|
+
if (nMin <= nMax) {
|
|
13934
|
+
this._valueMin = nMin;
|
|
13935
|
+
this._valueMax = nMax;
|
|
13936
|
+
if (!this._pieCategorical && pooled.length) {
|
|
13937
|
+
const nParts = Math.trunc(styleNum(this.style.classes)) || colorSchemeClassCount(this.style.colorscheme) || DEFAULT_RANGE_CLASSES;
|
|
13938
|
+
this.partsA = flatRangeParts(pooled, nMin, nMax, nParts, this.flags, styleNum(this.style.rangecentervalue));
|
|
13939
|
+
// a 1-field pie's legend rows are these classes (in place, as
|
|
13940
|
+
// _ensureAggregateStats: the legend holds the array)
|
|
13941
|
+
if (!this._multiFields && Array.isArray(this.categoryLabels)) {
|
|
13942
|
+
this.categoryLabels.splice(0, this.categoryLabels.length, ...this.partsA.map(p => `${this._formatTooltipValue(p.min)} - ${this._formatTooltipValue(p.max)}`));
|
|
13943
|
+
}
|
|
13944
|
+
}
|
|
13945
|
+
}
|
|
13946
|
+
this._pieStatsCache = { feats, zoom: zkey, maxSize: maxSize > -Infinity ? maxSize : undefined };
|
|
13947
|
+
return this._pieStatsCache;
|
|
13948
|
+
}
|
|
13949
|
+
|
|
12511
13950
|
// the value texts of _buildPieLayers (see there)
|
|
12512
13951
|
_buildPieTextLayers(charts, zoom, liveZoom, unit, sliceRgb, has) {
|
|
12513
13952
|
const st = this.style;
|
|
@@ -12518,12 +13957,16 @@ in vec4 vPieLineColor;
|
|
|
12518
13957
|
const noBreaks = flatNoBreaks(this._valueMin, this._valueMax);
|
|
12519
13958
|
const dec = explicitValueDecimals(st);
|
|
12520
13959
|
const fmt = (v, d) => flatFormatValue(v, d, noBreaks ? 'ROUND|NOBREAKS' : 'ROUND');
|
|
12521
|
-
|
|
12522
|
-
|
|
13960
|
+
// COUNT on an aggregated pie: the part's record count (flat's nCountA)
|
|
13961
|
+
const countText = has(/COUNT/);
|
|
13962
|
+
const partText = (sl, ch) => {
|
|
13963
|
+
if (sl.complement) return Math.round(sl.percent * 10) / 10 + ' % ';
|
|
13964
|
+
const v = countText && ch && ch.partCounts ? ch.partCounts[sl.i] || 0 : sl.value;
|
|
13965
|
+
return fmt(v, dec != null ? dec : (v < 1 ? 2 : 0)) + partUnit;
|
|
13966
|
+
};
|
|
12523
13967
|
const valuesOn = has(/VALUES/) && !has(/STACKED/) && !valuesHiddenByScale(st, zoom);
|
|
12524
13968
|
const textOverride = st.valuecolor || st.textcolor;
|
|
12525
13969
|
const inline = [], boxed = [], centerTexts = [], lines = [];
|
|
12526
|
-
const nFields = (this._pieFields || []).length;
|
|
12527
13970
|
for (const ch of charts) {
|
|
12528
13971
|
const o = ch.off;
|
|
12529
13972
|
// CENTER: the center part's value (maptheme.js 20402-20410)
|
|
@@ -12533,10 +13976,10 @@ in vec4 vPieLineColor;
|
|
|
12533
13976
|
const fontSize = Math.min(ch.centerR * 0.8, ch.centerR * (3.3 / Math.max(1, text.length))) * valueScale;
|
|
12534
13977
|
const rgb = (this.categoryColorsRgb || [])[ch.layout.center.i] || [128, 128, 128];
|
|
12535
13978
|
if (fontSize > VALUES_MIN_FONT_PX) centerTexts.push({ position: ch.c, text, fontSize, off: o, color: flatChartTextRgb(rgb), bold: true });
|
|
12536
|
-
} else if (!ch.layout.center && has(/CENTERVALUE/) && ch.size !== undefined && !isNaN(ch.size)) {
|
|
13979
|
+
} else if (!ch.layout.center && has(/CENTERVALUE/) && (ch.count || (ch.size !== undefined && !isNaN(ch.size)))) {
|
|
12537
13980
|
// flat prints String(nCount || nSize): a record's item has no
|
|
12538
|
-
// nCount, so its size value
|
|
12539
|
-
const text = String(ch.size);
|
|
13981
|
+
// nCount, so its size value; an aggregated item its record count
|
|
13982
|
+
const text = String(ch.count || ch.size);
|
|
12540
13983
|
const fontSize = Math.min(ch.r * 0.5, ch.r * (2 / Math.max(1, text.length)));
|
|
12541
13984
|
if (fontSize > VALUES_MIN_FONT_PX) {
|
|
12542
13985
|
centerTexts.push({ position: ch.c, text, fontSize, off: o, bold: false,
|
|
@@ -12544,20 +13987,20 @@ in vec4 vPieLineColor;
|
|
|
12544
13987
|
}
|
|
12545
13988
|
}
|
|
12546
13989
|
if (!valuesOn) continue;
|
|
12547
|
-
if (
|
|
13990
|
+
if (ch.nParts > 1 || has(/NOINLINETEXT/)) {
|
|
12548
13991
|
const fontSize = 5 * unit * valueScale;
|
|
12549
13992
|
if (fontSize <= VALUES_MIN_FONT_PX) continue;
|
|
12550
13993
|
for (const lab of pieValueLabelLayout(ch.layout.slices, ch.r, fontSize, unit, (this.partsA || []).length)) {
|
|
12551
13994
|
const bg = has(/VALUEBACKGROUND/) ? sliceRgb(lab.slice) : [255, 255, 255];
|
|
12552
13995
|
const textRgb = textOverride ? hexOrNamedToRgb(textOverride) : flatDerivateRgb(bg, bg[0] + bg[1] + bg[2] > 450 ? 0.6 : 3);
|
|
12553
|
-
boxed.push({ position: ch.c, text: partText(lab.slice), fontSize, anchor: lab.anchor,
|
|
13996
|
+
boxed.push({ position: ch.c, text: partText(lab.slice, ch), fontSize, anchor: lab.anchor,
|
|
12554
13997
|
// the text's baseline → its center (arial: 0.35 font above)
|
|
12555
13998
|
off: [o[0] + lab.x, o[1] + lab.y - 0.35 * fontSize], color: textRgb, bg: [...bg.slice(0, 3), 128] });
|
|
12556
13999
|
lab.segments.forEach(sg => lines.push({ c: ch.c, o, sg }));
|
|
12557
14000
|
}
|
|
12558
14001
|
} else if (ch.layout.slices.length) {
|
|
12559
14002
|
const sl = ch.layout.slices[ch.layout.slices.length - 1];
|
|
12560
|
-
const text = partText(sl);
|
|
14003
|
+
const text = partText(sl, ch);
|
|
12561
14004
|
const fontSize = Math.min(ch.r * 0.8, ch.r * (3.4 / Math.max(1, text.length)));
|
|
12562
14005
|
const color = textOverride ? hexOrNamedToRgb(textOverride) : flatChartTextRgb(sliceRgb(sl));
|
|
12563
14006
|
// baseline at −0.6 R + R / 2 + 0.45 font (maptheme.js 20434)
|
|
@@ -12611,6 +14054,594 @@ in vec4 vPieLineColor;
|
|
|
12611
14054
|
return layers;
|
|
12612
14055
|
}
|
|
12613
14056
|
|
|
14057
|
+
// CHART|BAR / BARS (_isBarChart) — flat's bar chart per item
|
|
14058
|
+
// (flatBarLayout, maptheme.js drawChart's BAR branch): its items and
|
|
14059
|
+
// theme statistics as a pie's (one per record, or AGGREGATE /
|
|
14060
|
+
// CATEGORICAL items via _pieAggregatedItems / _pieAggregateStats), a bar
|
|
14061
|
+
// per value field or category:
|
|
14062
|
+
// - size: SIZE by the size field (or the values' sum) with sizepow, the
|
|
14063
|
+
// width chart size / parts · 0.66, the length value · 30 u / nMax
|
|
14064
|
+
// (normalsizevalue, else the theme's max) · rangescale, with SIZE
|
|
14065
|
+
// and several parts / sizer (SIZEP4: sizer²), COMPRESS / EXPAND,
|
|
14066
|
+
// POINTER · 1.2
|
|
14067
|
+
// - HORZ: the bars to the right, the first on top; else upward side
|
|
14068
|
+
// by side; STACKED: one column; SORT, UP, CENTER, SPACED, THIN /
|
|
14069
|
+
// THICK, NOZERO, NONEGATIVE, ONLYNEGATIVE, LEFT
|
|
14070
|
+
// - colors: the part's (categories, fields), a single part or
|
|
14071
|
+
// SEQUENCE the value's class; outline linecolor (black) at
|
|
14072
|
+
// linewidth · 0.25 (0.2 u) and opacity 1 (0.1 without linewidth);
|
|
14073
|
+
// a negative bar fill 0.3, outlined in its color; fillopacity
|
|
14074
|
+
// - POINTER / ARROW (SMALLARROW drawn as ARROW): flat's pointer path,
|
|
14075
|
+
// negative values pointing the other way (fadenegative fill, the
|
|
14076
|
+
// outline lighter or darker), SIZE: its width by the value;
|
|
14077
|
+
// OFFSETMEAN / OFFSETMEDIAN / DEVIATION: the value's deviation from
|
|
14078
|
+
// the part's mean / median (in %) or in standard deviations
|
|
14079
|
+
// - VALUES: the value (valuedecimals, else 1 decimal with a max ≤ 1)
|
|
14080
|
+
// and units, POINTER a "+", STACKED its share; beyond the bar's end
|
|
14081
|
+
// (in a white box unless HORZ; VALUEBACKGROUND / CTEXT: the bar's
|
|
14082
|
+
// color), inside it when it fits (without SIZE, THIN, DTEXT …),
|
|
14083
|
+
// STACKED beside the column on leader lines; flat's values color
|
|
14084
|
+
// #798697 (textcolor), negative red (valuecolor); DTEXT and a sized
|
|
14085
|
+
// POINTER fade small values
|
|
14086
|
+
// - BOX / TITLE (flatChartBox): a box around chart and title, the
|
|
14087
|
+
// item's title above it — boxcolor (white) at boxopacity, bordercolor
|
|
14088
|
+
// (#bbbbbb) at 0.3, borderwidth · 0.2 · w / 30; title 5 u ·
|
|
14089
|
+
// textscale in textcolor (#888888) over a white halo
|
|
14090
|
+
// - draw order: flat's (_buildPieLayers); each chart's box under its
|
|
14091
|
+
// bars, a later chart over an earlier one
|
|
14092
|
+
// Not drawn (BAR_INERT_FLAGS, a console note): see flatBarLayout.
|
|
14093
|
+
// Known divergence: every value text lies above every bar (flat draws
|
|
14094
|
+
// them in the chart's own group); the leader lines are drawn in map
|
|
14095
|
+
// coordinates (approximate on the globe).
|
|
14096
|
+
_buildBarLayers(zoom, bbox, liveZoom = zoom, globeCenter = null) {
|
|
14097
|
+
const st = this.style;
|
|
14098
|
+
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
14099
|
+
const has = re => re.test(t);
|
|
14100
|
+
const agg = this._isAggregatedPie();
|
|
14101
|
+
const cat = !!this._pieCategorical;
|
|
14102
|
+
const fields = cat ? null : (this._pieFields || String(this.binding.value || '').split('|'));
|
|
14103
|
+
const stats = agg ? this._pieAggregateStats(zoom) : null;
|
|
14104
|
+
const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(st.scale) || 1);
|
|
14105
|
+
const sizeField = !!(this.binding.size && this.binding.size !== '$item$');
|
|
14106
|
+
const nsv = styleNum(st.normalsizevalue);
|
|
14107
|
+
const colors = this.categoryColorsRgb || [];
|
|
14108
|
+
const num = v => { const x = styleNum(v); return isNaN(x) ? undefined : x; };
|
|
14109
|
+
const measure = (text, font) => titleTextWidth(text, font);
|
|
14110
|
+
// the charts' values (and, for OFFSETMEAN …, every item's)
|
|
14111
|
+
const items = [];
|
|
14112
|
+
const itemValues = f => {
|
|
14113
|
+
if (agg) {
|
|
14114
|
+
const { parts, size } = this._pieItemValues(f.properties.pie);
|
|
14115
|
+
return { values: parts, size };
|
|
14116
|
+
}
|
|
14117
|
+
const raw = f.properties || {};
|
|
14118
|
+
return { values: fields.map(k => flatNumber(raw[k], this.flags)), size: sizeField ? parseFloat(raw[this.binding.size]) : undefined };
|
|
14119
|
+
};
|
|
14120
|
+
const all = agg ? this._pieAggregatedItems(zoom, [-180, -90, 180, 90]) : (this._activeFeatures || this.features);
|
|
14121
|
+
// flat's per-part mean, median (getMeanMedianQuantile) and standard
|
|
14122
|
+
// deviation over every item
|
|
14123
|
+
let means, medians, devs;
|
|
14124
|
+
if (has(/OFFSETMEAN|OFFSETMEDIAN|DEVIATION/)) {
|
|
14125
|
+
const cache = this._barStatsCache;
|
|
14126
|
+
if (cache && cache.all === all && (!agg || cache.zoom === zoom)) ({ means, medians, devs } = cache);
|
|
14127
|
+
else {
|
|
14128
|
+
const cols = [];
|
|
14129
|
+
for (const f of all) itemValues(f).values.forEach((v, k) => { (cols[k] ||= []).push(v); });
|
|
14130
|
+
means = cols.map(c => c.reduce((a, v) => a + v, 0) / c.length);
|
|
14131
|
+
medians = cols.map(c => c.slice().sort((a, b) => a - b)[Math.round(c.length / 2)]);
|
|
14132
|
+
devs = cols.map((c, k) => Math.sqrt(c.reduce((a, v) => a + (v - means[k]) * (v - means[k]), 0) / c.length));
|
|
14133
|
+
this._barStatsCache = { all, zoom, means, medians, devs };
|
|
14134
|
+
}
|
|
14135
|
+
}
|
|
14136
|
+
const lo = {
|
|
14137
|
+
themeMin: this._valueMin, themeMax: this._valueMax,
|
|
14138
|
+
maxSize: stats ? stats.maxSize : this._maxSizeValue, sizeField,
|
|
14139
|
+
normalSizeValue: nsv > 0 ? nsv : undefined, sizePow: resolveSizePow(st, this.flags),
|
|
14140
|
+
rangeScale: num(st.rangescale), valueScale: num(st.valuescale), units: st.units ? String(st.units) : '',
|
|
14141
|
+
valueDecimals: explicitValueDecimals(st), noBreaks: flatNoBreaks(this._valueMin, this._valueMax),
|
|
14142
|
+
gridX: num(st.gridx), nClasses: (this.partsA || []).length, classMax: (this.partsA || []).map(p => p.max),
|
|
14143
|
+
nColors: colors.length, fillOpacity: num(st.fillopacity), lineWidth: num(st.linewidth),
|
|
14144
|
+
fadeNegative: num(st.fadenegative), fadeValuePow: num(st.fadevaluepow),
|
|
14145
|
+
textColor: st.textcolor || undefined, valueColor: st.valuecolor || undefined,
|
|
14146
|
+
hideValues: valuesHiddenByScale(st, zoom), means, medians, devs, measure
|
|
14147
|
+
};
|
|
14148
|
+
const inView = c => c[0] >= bbox[0] && c[0] <= bbox[2] && c[1] >= bbox[1] && c[1] <= bbox[3]
|
|
14149
|
+
&& (!globeCenter || isOnVisibleHemisphere(c[0], c[1], globeCenter));
|
|
14150
|
+
const charts = [];
|
|
14151
|
+
for (const f of agg ? this._pieAggregatedItems(zoom, bbox) : all) {
|
|
14152
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
14153
|
+
if (!c || typeof c[0] !== 'number' || !inView(c)) continue;
|
|
14154
|
+
const { values, size } = itemValues(f);
|
|
14155
|
+
const layout = flatBarLayout(values, t, Object.assign({ itemSize: size }, lo));
|
|
14156
|
+
if (!layout || (!layout.bars.length && !layout.texts.length)) continue;
|
|
14157
|
+
const sum = values.reduce((a, v) => a + (isFinite(v) ? v : 0), 0);
|
|
14158
|
+
let properties;
|
|
14159
|
+
if (agg) {
|
|
14160
|
+
const pie = f.properties.pie;
|
|
14161
|
+
const raws = f.properties.raws || (f.properties.raw ? [f.properties.raw] : []);
|
|
14162
|
+
properties = { counts: values, total: sum, recordCounts: [pie.n], raws, firstRaw: raws[0] || null,
|
|
14163
|
+
titleRaw: f.properties.titleRaw || f.properties.raw || null };
|
|
14164
|
+
} else {
|
|
14165
|
+
const raw = f.properties || {};
|
|
14166
|
+
properties = values.length > 1 ? { value: values[0], parts: values, raw }
|
|
14167
|
+
: { value: values[0], cat: this.partsA ? this._resolvePartsClass(values[0]) : 0, raw };
|
|
14168
|
+
}
|
|
14169
|
+
// flat's draw-order key (as for pies, maptheme.js 16812-16860)
|
|
14170
|
+
const key = agg ? (this.binding.size || (has(/AGGREGATE/) && has(/SUM/)) ? size : values[0])
|
|
14171
|
+
: (this.binding.size ? size : has(/SIZE/) ? sum : values[0]);
|
|
14172
|
+
charts.push({ c, layout, key, properties });
|
|
14173
|
+
}
|
|
14174
|
+
if (((this._valueMin !== this._valueMax) || this.binding.size) && !has(/NOSRT/)
|
|
14175
|
+
&& (!has(/NOSIZE/) || has(/\bSORT\b/))
|
|
14176
|
+
&& (!has(/CATEGORICAL/) || has(/AGGREGATE/) || this.binding.size || has(/\bSORT\b/))) {
|
|
14177
|
+
const down = has(/\bSORT\b/) && has(/\bUP\b/);
|
|
14178
|
+
charts.sort((a, b) => (down ? b.key - a.key : a.key - b.key) || 0);
|
|
14179
|
+
}
|
|
14180
|
+
applyFlatMaxCharts(charts, st);
|
|
14181
|
+
|
|
14182
|
+
const boxShown = flatFlag(this.flags, 'BOX') && !boxHiddenByScale(st, liveZoom);
|
|
14183
|
+
const titleShown = boxShown && flatFlag(this.flags, 'TITLE') && !!this.binding.title;
|
|
14184
|
+
const titleFont = 5 * (num(st.textscale) || num(st.valuescale) || 1);
|
|
14185
|
+
const boxOpacity = isNaN(styleNum(st.boxopacity)) ? 1 : styleNum(st.boxopacity);
|
|
14186
|
+
const borderStyle = String(st.borderstyle || '');
|
|
14187
|
+
const boxFill = [...hexOrNamedToRgb(st.boxcolor || 'white').slice(0, 3), Math.round(255 * boxOpacity)];
|
|
14188
|
+
const boxLine = borderStyle === 'none' ? [0, 0, 0, 0]
|
|
14189
|
+
: [...hexOrNamedToRgb(styleLineColor(st.bordercolor) || '#bbbbbb').slice(0, 3), Math.round(255 * (/^(solid|dotted|dashed)$/.test(borderStyle) ? 1 : 0.3))];
|
|
14190
|
+
const borderWidth = num(st.borderwidth) || 1;
|
|
14191
|
+
const lc = st.linecolor != null && st.linecolor !== '' ? styleLineColor(st.linecolor) : null;
|
|
14192
|
+
const lineRgbOf = (spec, rgb) => (spec.rgb === 'color' ? rgb : spec.rgb === 'low' ? flatDerivateRgb(rgb, 0.7)
|
|
14193
|
+
: spec.rgb === 'high' ? flatDerivateRgb(rgb, 1.3) : (lc && lc !== 'none' ? hexOrNamedToRgb(lc) : [0, 0, 0]));
|
|
14194
|
+
const align = !!(st.align || styleNum(st.offsetx) || styleNum(st.offsety));
|
|
14195
|
+
|
|
14196
|
+
const shapes = [], texts = [], titles = [], lines = [];
|
|
14197
|
+
// a shape in chart units → the instance (px; the circle around it)
|
|
14198
|
+
const shape = (ch, s, fill, line, width, props, noPick) => {
|
|
14199
|
+
const L = s.len + s.band + s.head;
|
|
14200
|
+
const d = [[0, -1], [1, 0], [0, 1], [-1, 0]][s.dir];
|
|
14201
|
+
const cx = (s.base[0] + d[0] * L / 2) * unit + ch.off[0], cy = (s.base[1] + d[1] * L / 2) * unit + ch.off[1];
|
|
14202
|
+
const lw = line[3] > 0 ? width * unit : 0;
|
|
14203
|
+
shapes.push({
|
|
14204
|
+
position: ch.c, off: [cx, -cy], properties: props, fill, line,
|
|
14205
|
+
r: Math.hypot(s.w2 * unit, L * unit / 2) + lw + 2,
|
|
14206
|
+
shape: [s.w2 * unit, s.sw * unit, s.len * unit, s.band * unit],
|
|
14207
|
+
shape2: [s.head * unit, s.dir + (noPick ? 4 : 0), lw, L * unit / 2]
|
|
14208
|
+
});
|
|
14209
|
+
};
|
|
14210
|
+
for (const ch of charts) {
|
|
14211
|
+
const L = ch.layout;
|
|
14212
|
+
let box = null;
|
|
14213
|
+
if (boxShown) {
|
|
14214
|
+
const title = titleShown ? this._itemTitle(ch.properties) : '';
|
|
14215
|
+
box = flatChartBox(L.extent, { margin: num(st.boxmargin) || 2, titleFont, title: title ? title + (st.titleunits || '') : '',
|
|
14216
|
+
bottomTitle: flatFlag(this.flags, 'BOTTOMTITLE'), alignRight: /right/.test(String(st.align || '')), measure });
|
|
14217
|
+
}
|
|
14218
|
+
const ext = box ? box.rect : L.extent;
|
|
14219
|
+
// align / offsetx / offsety (flatChartAlignOffset), the chart's
|
|
14220
|
+
// half height as its size (ASSUMPTION: not measured on flat for BAR)
|
|
14221
|
+
ch.off = align ? flatChartAlignOffset(st, { unit, symbolScale: 1, half: (ext[3] - ext[1]) / 2 * unit }) : [0, 0];
|
|
14222
|
+
if (box) {
|
|
14223
|
+
const r = box.rect;
|
|
14224
|
+
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 },
|
|
14225
|
+
boxFill, boxLine, borderWidth * 0.2 * box.width / 30, null, true);
|
|
14226
|
+
if (box.title) titles.push({ position: ch.c, text: box.title.lines.join('\n'), font: box.title.font * unit, anchor: box.title.anchor,
|
|
14227
|
+
off: [box.title.x * unit + ch.off[0], (box.title.baseline - 0.8 * box.title.font) * unit + ch.off[1]] });
|
|
14228
|
+
}
|
|
14229
|
+
for (const b of L.bars) {
|
|
14230
|
+
const rgb = colors[b.cls] || [128, 128, 128];
|
|
14231
|
+
const a = Math.round(255 * b.fill.opacity * b.opacity);
|
|
14232
|
+
const line = [...lineRgbOf(b.line, rgb).slice(0, 3), Math.round(255 * b.line.opacity * b.opacity)];
|
|
14233
|
+
shape(ch, b, [...rgb.slice(0, 3), a], line, b.line.width, ch.properties, false);
|
|
14234
|
+
}
|
|
14235
|
+
for (const tx of L.texts) {
|
|
14236
|
+
const font = tx.font * unit;
|
|
14237
|
+
if (font <= VALUES_MIN_FONT_PX) continue;
|
|
14238
|
+
const rgb = colors[tx.cls] || [128, 128, 128];
|
|
14239
|
+
const color = tx.color === 'value' ? [0x79, 0x86, 0x97] : tx.color === 'contrast' ? flatChartTextRgb(rgb) : hexOrNamedToRgb(tx.color);
|
|
14240
|
+
// the baseline's start → the text's middle-left (arial: 0.35 font
|
|
14241
|
+
// above the baseline), perpendicular to its reading direction
|
|
14242
|
+
const r = tx.angle * Math.PI / 180;
|
|
14243
|
+
const mx = tx.x * unit - 0.35 * font * Math.sin(r), my = tx.y * unit - 0.35 * font * Math.cos(r);
|
|
14244
|
+
texts.push({ position: ch.c, text: tx.text, font, angle: tx.angle, off: [mx + ch.off[0], my + ch.off[1]],
|
|
14245
|
+
color: [...color.slice(0, 3), Math.round(255 * Math.max(0, Math.min(1, tx.opacity)))],
|
|
14246
|
+
bg: tx.bg === 'bar' ? [...rgb.slice(0, 3), 128] : tx.bg === 'white' ? [254, 254, 255, 255] : null });
|
|
14247
|
+
}
|
|
14248
|
+
for (const l of L.lines) lines.push({ c: ch.c, o: ch.off, sg: [l.x0 * unit, l.y0 * unit, l.x1 * unit, l.y1 * unit] });
|
|
14249
|
+
}
|
|
14250
|
+
|
|
14251
|
+
const layers = [new BarShapeLayer({
|
|
14252
|
+
id: `ix-bar-${this.name}`,
|
|
14253
|
+
data: shapes, pickable: true,
|
|
14254
|
+
getPosition: d => d.position,
|
|
14255
|
+
getRadius: d => d.r,
|
|
14256
|
+
getPixelOffset: d => d.off,
|
|
14257
|
+
getFillColor: d => d.fill,
|
|
14258
|
+
getLineColor: d => d.line,
|
|
14259
|
+
getBarShape: d => d.shape,
|
|
14260
|
+
getBarShape2: d => d.shape2,
|
|
14261
|
+
radiusUnits: 'pixels', billboard: true, stroked: false, filled: true, antialiasing: true,
|
|
14262
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14263
|
+
})];
|
|
14264
|
+
if (lines.length) {
|
|
14265
|
+
const feature = d => ({ type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: [
|
|
14266
|
+
pixelOffsetLngLat(d.c, [d.o[0] + d.sg[0], d.o[1] + d.sg[1]], liveZoom),
|
|
14267
|
+
pixelOffsetLngLat(d.c, [d.o[0] + d.sg[2], d.o[1] + d.sg[3]], liveZoom)] } });
|
|
14268
|
+
const fc = { type: 'FeatureCollection', features: lines.map(feature) };
|
|
14269
|
+
[['bg', [255, 255, 255, Math.round(255 * 0.2)], 0.15], ['fg', [0x88, 0x88, 0x88, 255], 0.05]].forEach(([k, color, w]) => {
|
|
14270
|
+
layers.push(new GeoJsonLayer({
|
|
14271
|
+
id: `ix-bar-lines-${k}-${this.name}`, data: fc, pickable: false,
|
|
14272
|
+
stroked: true, filled: false, getLineColor: color, getLineWidth: w * unit, lineWidthUnits: 'pixels'
|
|
14273
|
+
}));
|
|
14274
|
+
});
|
|
14275
|
+
}
|
|
14276
|
+
const textBase = {
|
|
14277
|
+
getPosition: d => d.position, getText: d => d.text, getSize: d => d.font,
|
|
14278
|
+
getPixelOffset: d => d.off, getColor: d => d.color, getAngle: d => d.angle,
|
|
14279
|
+
sizeUnits: 'pixels', fontFamily: 'arial', characterSet: 'auto', fontWeight: 'normal',
|
|
14280
|
+
getTextAnchor: 'start', getAlignmentBaseline: 'center', pickable: false,
|
|
14281
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14282
|
+
};
|
|
14283
|
+
const boxed = texts.filter(d => d.bg), plain = texts.filter(d => !d.bg);
|
|
14284
|
+
if (plain.length) layers.push(new TextLayer(Object.assign({}, textBase, { id: `ix-bar-values-${this.name}`, data: plain })));
|
|
14285
|
+
if (boxed.length) {
|
|
14286
|
+
// flat's box (createTextLabel): 0.3 font left of the text, 0.4 font
|
|
14287
|
+
// right of it, 0.1 font taller
|
|
14288
|
+
layers.push(new TextLayer(Object.assign({}, textBase, {
|
|
14289
|
+
id: `ix-bar-values-boxed-${this.name}`, data: boxed,
|
|
14290
|
+
background: true, getBackgroundColor: d => d.bg,
|
|
14291
|
+
// one padding per layer: of the texts' mean font
|
|
14292
|
+
backgroundPadding: [0.3, 0, 0.4, 0.1].map(k => k * boxed.reduce((a, d) => a + d.font, 0) / boxed.length)
|
|
14293
|
+
})));
|
|
14294
|
+
}
|
|
14295
|
+
if (titles.length) {
|
|
14296
|
+
layers.push(new TextLayer({
|
|
14297
|
+
id: `ix-bar-titles-${this.name}`, data: titles, pickable: false,
|
|
14298
|
+
getPosition: d => d.position, getText: d => d.text, getSize: d => d.font,
|
|
14299
|
+
sizeUnits: 'pixels', fontFamily: 'arial', characterSet: 'auto', lineHeight: 1,
|
|
14300
|
+
getColor: [...hexOrNamedToRgb(st.textcolor || '#888888').slice(0, 3), 255],
|
|
14301
|
+
fontSettings: { sdf: true }, outlineWidth: 1 / 7, outlineColor: [255, 255, 255, 128],
|
|
14302
|
+
getTextAnchor: d => d.anchor, getAlignmentBaseline: 'top',
|
|
14303
|
+
getPixelOffset: d => d.off,
|
|
14304
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14305
|
+
}));
|
|
14306
|
+
}
|
|
14307
|
+
return layers;
|
|
14308
|
+
}
|
|
14309
|
+
|
|
14310
|
+
// CHART|VECTOR / CHART|BEZIER — the theme's classes as flat has them:
|
|
14311
|
+
// with CATEGORICAL (or EXACT) the categories, flat's string values
|
|
14312
|
+
// trimmed (getStringValueIndex, maptheme.js 9204), its partsA one
|
|
14313
|
+
// [k, k + 1e-9] per category (the last + 0.001) and the colorscheme's
|
|
14314
|
+
// colors per category; the items' statistics follow in _vectorItems
|
|
14315
|
+
_prepareVector() {
|
|
14316
|
+
const st = this.style;
|
|
14317
|
+
this._vectorCategorical = this.flags.has('CATEGORICAL') && !!this.binding.value;
|
|
14318
|
+
if (this._vectorCategorical) {
|
|
14319
|
+
const explicit = Array.isArray(st.values) ? st.values.map(v => String(v).trim()) : null;
|
|
14320
|
+
const index = new Map();
|
|
14321
|
+
if (explicit) explicit.forEach((v, i) => { if (!index.has(v)) index.set(v, i); });
|
|
14322
|
+
else {
|
|
14323
|
+
for (const f of this.features) {
|
|
14324
|
+
const v = f.properties[this.binding.value];
|
|
14325
|
+
if (v == null) continue;
|
|
14326
|
+
const k = String(v).trim();
|
|
14327
|
+
if (k !== '' && !index.has(k)) index.set(k, index.size);
|
|
14328
|
+
}
|
|
14329
|
+
this.categoryLabels = Array.from(index.keys());
|
|
14330
|
+
const explicitLabel = Array.isArray(st.label) ? st.label.map(String) : null;
|
|
14331
|
+
this.categoryDisplayLabels = explicitLabel && explicitLabel.length === this.categoryLabels.length ? explicitLabel : this.categoryLabels;
|
|
14332
|
+
this.categoryColorsRgb = resolveClassColors(st.colorscheme, this.categoryDisplayLabels, st.classes);
|
|
14333
|
+
}
|
|
14334
|
+
this._vectorCategoryIndex = index;
|
|
14335
|
+
// the legend counts records by their raw value
|
|
14336
|
+
const byLabel = new Map(index);
|
|
14337
|
+
for (const f of this.features) {
|
|
14338
|
+
const v = f.properties[this.binding.value];
|
|
14339
|
+
if (v != null && !byLabel.has(v) && index.has(String(v).trim())) byLabel.set(v, index.get(String(v).trim()));
|
|
14340
|
+
}
|
|
14341
|
+
this.categoryIndexByLabel = byLabel;
|
|
14342
|
+
const n = this.categoryLabels.length;
|
|
14343
|
+
this.partsA = this.categoryLabels.map((l, i) => ({ min: i + 1, max: i + 1 + (i === n - 1 ? 0.001 : 0) + 1e-9 }));
|
|
14344
|
+
this._rangeClassed = false;
|
|
14345
|
+
}
|
|
14346
|
+
// colorfield (flat 12529-12597, 12644-12652): an item's color is its
|
|
14347
|
+
// color value's — the colorvalues, else values, else the items'
|
|
14348
|
+
// values in order — n-th color of the colorscheme realized for n
|
|
14349
|
+
const colorField = st.colorfield || (this.binding && this.binding.color);
|
|
14350
|
+
this._vectorColorField = colorField || null;
|
|
14351
|
+
if (colorField) {
|
|
14352
|
+
const list = Array.isArray(st.colorvalues) ? st.colorvalues.map(String)
|
|
14353
|
+
: Array.isArray(st.values) ? st.values.map(String)
|
|
14354
|
+
: [...new Set(this.features.map(f => String(f.properties[colorField])))];
|
|
14355
|
+
const colors = resolveClassColors(st.colorscheme, list, st.classes) || [];
|
|
14356
|
+
this._vectorColorOf = new Map(list.map((v, i) => [v, colors[i]]));
|
|
14357
|
+
}
|
|
14358
|
+
this._vectorItemsOf = null;
|
|
14359
|
+
}
|
|
14360
|
+
|
|
14361
|
+
// the records of flatVectorItems from this theme's (active) features:
|
|
14362
|
+
// the second position joined by key (joinChartPositions: f._pos2) or
|
|
14363
|
+
// read from lat/lon fields ("lat|lon")
|
|
14364
|
+
_vectorRecords(features) {
|
|
14365
|
+
const b = this.binding;
|
|
14366
|
+
const l2 = b.lookup2 != null ? String(b.lookup2) : '';
|
|
14367
|
+
const latlon2 = l2.includes('|') ? l2.split('|') : null;
|
|
14368
|
+
const sizeField = b.size && b.size !== '$item$' ? b.size : null;
|
|
14369
|
+
const records = [];
|
|
14370
|
+
for (const f of features) {
|
|
14371
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
14372
|
+
if (!c || typeof c[0] !== 'number') continue;
|
|
14373
|
+
const p = f.properties || {};
|
|
14374
|
+
let p2 = null;
|
|
14375
|
+
if (latlon2) {
|
|
14376
|
+
const lat = flatScanValue(p[latlon2[0]]), lng = flatScanValue(p[latlon2[1]]);
|
|
14377
|
+
if (isFinite(lat) && isFinite(lng)) p2 = [lng, lat];
|
|
14378
|
+
} else if (f._pos2) p2 = f._pos2;
|
|
14379
|
+
let cat = null, value;
|
|
14380
|
+
if (this._vectorCategorical) {
|
|
14381
|
+
const v = p[b.value];
|
|
14382
|
+
const i = v == null ? undefined : this._vectorCategoryIndex.get(String(v).trim());
|
|
14383
|
+
cat = i == null ? -1 : i;
|
|
14384
|
+
} else if (b.value) {
|
|
14385
|
+
value = flatScanValue(p[b.value]);
|
|
14386
|
+
}
|
|
14387
|
+
records.push({ p1: c, p2, cat, value, size: sizeField ? flatScanValue(p[sizeField]) : 1, raw: p });
|
|
14388
|
+
}
|
|
14389
|
+
return records;
|
|
14390
|
+
}
|
|
14391
|
+
|
|
14392
|
+
// the items (flatVectorItems) of the active features, once per feature
|
|
14393
|
+
// set; also flat's statistics of them (maptheme.js 11180-11240): nMin /
|
|
14394
|
+
// nMax over the items' sizes (CATEGORICAL or a size field, else their
|
|
14395
|
+
// values), nMaxSize the biggest size
|
|
14396
|
+
_vectorItems() {
|
|
14397
|
+
const features = this._activeFeatures || this.features;
|
|
14398
|
+
if (this._vectorItemsOf === features) return this._vectorItemsCache;
|
|
14399
|
+
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
14400
|
+
const items = flatVectorItems(this._vectorRecords(features), {
|
|
14401
|
+
aggregate: this.flags.has('AGGREGATE'),
|
|
14402
|
+
vectorKey: /VECTOR/.test(t),
|
|
14403
|
+
multiParts: this.flags.has('CATEGORICAL'),
|
|
14404
|
+
max: /\bMAX\b/.test(t), min: /\bMIN\b/.test(t),
|
|
14405
|
+
zeroIsNotValue: /ZEROISNOTVALUE/.test(t), negativeIsNotValue: /NEGATIVEISNOTVALUE/.test(t)
|
|
14406
|
+
});
|
|
14407
|
+
let min = Infinity, max = -Infinity, maxSize = -Infinity;
|
|
14408
|
+
const bySize = this._vectorCategorical || !!this.binding.size;
|
|
14409
|
+
for (const it of items) {
|
|
14410
|
+
if (isFinite(it.nSize) && it.nSize > maxSize) maxSize = it.nSize;
|
|
14411
|
+
const v = bySize ? it.nSize : it.value;
|
|
14412
|
+
if (isFinite(v)) { if (v < min) min = v; if (v > max) max = v; }
|
|
14413
|
+
}
|
|
14414
|
+
if (min <= max) { this._valueMin = min; this._valueMax = max; }
|
|
14415
|
+
this._vectorMaxSize = maxSize;
|
|
14416
|
+
this._vectorItemsOf = features;
|
|
14417
|
+
this._vectorItemsCache = items;
|
|
14418
|
+
return items;
|
|
14419
|
+
}
|
|
14420
|
+
|
|
14421
|
+
// CHART|VECTOR / CHART|BEZIER — real ixmaps-flat's flow lines
|
|
14422
|
+
// (maptheme.js chartMap 18148-18435): one line per item
|
|
14423
|
+
// (_vectorItems) from its first to its second position — the
|
|
14424
|
+
// positions joined to FEATURE shapes by key (binding position /
|
|
14425
|
+
// position2, style lookupfield / lookupfield2) or read from lat/lon
|
|
14426
|
+
// fields. An item without a second position, or with a size under
|
|
14427
|
+
// style.minvalue, draws nothing; one is drawn when either end lies in
|
|
14428
|
+
// the view (flat 16590-16620).
|
|
14429
|
+
// - BEZIER (also VECTOR|BEZIER — the BEZIER branch comes first): a
|
|
14430
|
+
// cubic curve (bezierVectorLayout), width (linewidth, else 10 ·
|
|
14431
|
+
// (|nSize| / normalsizevalue or the biggest size)^(1/sizepow)) ·
|
|
14432
|
+
// unit px — unit: the object zoom × style.scale; a width ≤ 0 is not
|
|
14433
|
+
// drawn; stroke opacity fillopacity (else 0.3); a negative size or
|
|
14434
|
+
// REVERSE swaps the ends; RANDOM lowers the bow by up to 0.66 ·
|
|
14435
|
+
// rangescale (flat's Math.random, here a fixed hash of the item);
|
|
14436
|
+
// POINTER / ARROW an arrow head (arrowMarkerTriangle) at the
|
|
14437
|
+
// shortened end, its opacity 2 · fillopacity; GRADIENT / FADEIN a
|
|
14438
|
+
// gradient (fadeGradientStops) from the second color (linecolor's
|
|
14439
|
+
// first entry) at the start — FADEIN at 0.2 opacity — to the line
|
|
14440
|
+
// color at the end; DOPACITY (DOPACITYMAX) / DOPACITYMIN the line
|
|
14441
|
+
// and arrow opacity by the size within nMin … nMax (dopacitypow,
|
|
14442
|
+
// dopacityscale); DASH dashes of 50 / 5 + width units, round caps
|
|
14443
|
+
// - VECTOR alone: a straight line, width (linewidth, else 2 · nSize /
|
|
14444
|
+
// normalsizevalue / the biggest size, else 0.1) · the object zoom px
|
|
14445
|
+
// (no style.scale), opacity fillopacity (0.3); POINTER / ARROW a 4 ×
|
|
14446
|
+
// width arrow head with its tip on the second position; DASH dashes
|
|
14447
|
+
// of 50 / 15 units
|
|
14448
|
+
// - color: linecolor (its last entry), else the colorfield's color,
|
|
14449
|
+
// else the category's (CATEGORICAL) or the value's n-th color; a
|
|
14450
|
+
// record whose category is none of them is black (flat: an
|
|
14451
|
+
// "undefined" stroke — szNoDataColor is unset)
|
|
14452
|
+
// - draw order: flat's sort before drawing (16774-16880), the
|
|
14453
|
+
// biggest last (on top) when sized or AGGREGATE|SUM
|
|
14454
|
+
// - lines are densified in Web Mercator pixels (flat draws them on its
|
|
14455
|
+
// Mercator map), so under the globe projection they follow the
|
|
14456
|
+
// surface; their far-side parts are left out
|
|
14457
|
+
// Not drawn here (console note at type parsing, VECTOR_INERT_FLAGS):
|
|
14458
|
+
// VALUES (text along the curve), DYNAMICWIDTH, LONG, the DASH
|
|
14459
|
+
// animation; nor the second position from the category label (flat's
|
|
14460
|
+
// fallback without position2), a grid aggregation (gridwidth), the
|
|
14461
|
+
// VECTOR arrow's thin white outline, the legend's class marking.
|
|
14462
|
+
// Known divergence: all arrow heads lie above all lines (flat draws
|
|
14463
|
+
// each line with its head); a line thinner than a pixel is drawn one
|
|
14464
|
+
// pixel wide at its width as opacity (SVG's coverage).
|
|
14465
|
+
_buildVectorLayers(zoom, bbox, liveZoom = zoom, globeCenter = null) {
|
|
14466
|
+
const st = this.style;
|
|
14467
|
+
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
14468
|
+
const has = re => re.test(t);
|
|
14469
|
+
const bezier = has(/\bBEZIER\b/);
|
|
14470
|
+
if (!this.binding.lookup2) {
|
|
14471
|
+
if (!this._warnedNoPos2) {
|
|
14472
|
+
this._warnedNoPos2 = true;
|
|
14473
|
+
console.info(`[ixmaps-gl] layer "${this.name}": a VECTOR/BEZIER chart without position2 (lookupfield2) — flat's second position from the category label is not ported, nothing drawn`);
|
|
14474
|
+
}
|
|
14475
|
+
return [];
|
|
14476
|
+
}
|
|
14477
|
+
const items = this._vectorItems();
|
|
14478
|
+
const oz = objectZoomFactor(liveZoom, this.mapOptions);
|
|
14479
|
+
const scale = styleNum(st.scale) || 1;
|
|
14480
|
+
const unit = oz * scale;
|
|
14481
|
+
const world = 512 * Math.pow(2, liveZoom);
|
|
14482
|
+
const toPx = ([lng, lat]) => {
|
|
14483
|
+
const s = Math.sin(Math.max(-85.0511, Math.min(85.0511, lat)) * Math.PI / 180);
|
|
14484
|
+
return [(lng + 180) / 360 * world, (0.5 - Math.log((1 + s) / (1 - s)) / (4 * Math.PI)) * world];
|
|
14485
|
+
};
|
|
14486
|
+
const toLngLat = ([x, y]) => [x / world * 360 - 180, (2 * Math.atan(Math.exp((0.5 - y / world) * 2 * Math.PI)) - Math.PI / 2) * 180 / Math.PI];
|
|
14487
|
+
const inBox = c => c && c[0] >= bbox[0] && c[0] <= bbox[2] && c[1] >= bbox[1] && c[1] <= bbox[3];
|
|
14488
|
+
|
|
14489
|
+
const sizePow = resolveSizePow(st, this.flags);
|
|
14490
|
+
const nsv = styleNum(st.normalsizevalue);
|
|
14491
|
+
const maxSize = this._vectorMaxSize;
|
|
14492
|
+
// style.linewidth as flat holds it: the raw style value — flat's
|
|
14493
|
+
// `nLineWidth + 3` (the arrow length) joins a string ("0.6" + 3 =
|
|
14494
|
+
// "0.63") but adds a number
|
|
14495
|
+
const rawLw = this._definition && this._definition.style ? this._definition.style.linewidth : st.linewidth;
|
|
14496
|
+
const lwNum = styleNum(st.linewidth);
|
|
14497
|
+
const markerSize = styleNum(st.markersize) || 1;
|
|
14498
|
+
const gapSize = styleNum(st.gapsize) || 20;
|
|
14499
|
+
const fo = styleNum(st.fillopacity);
|
|
14500
|
+
const groupAlpha = styleNum(st.opacity) > 0 ? Math.min(1, styleNum(st.opacity)) : 1;
|
|
14501
|
+
const minValue = styleNum(st.minvalue);
|
|
14502
|
+
const rs = styleNum(st.rangescale);
|
|
14503
|
+
const dopMin = has(/DOPACITYMIN/), dop = !dopMin && has(/DOPACITY/);
|
|
14504
|
+
const dopPow = styleNum(st.dopacitypow) || 1, dopScale = styleNum(st.dopacityscale) || 1;
|
|
14505
|
+
const nMin = this._valueMin, nMax = this._valueMax;
|
|
14506
|
+
const lineColors = st.linecolor != null && st.linecolor !== '' ? flatToArray(st.linecolor) : null;
|
|
14507
|
+
const lineRgb = lineColors ? hexOrNamedToRgb(String(lineColors[lineColors.length - 1]).trim()) : null;
|
|
14508
|
+
const colors = this.categoryColorsRgb || [];
|
|
14509
|
+
const colorOf = it => {
|
|
14510
|
+
if (lineRgb) return lineRgb;
|
|
14511
|
+
if (this._vectorColorField) {
|
|
14512
|
+
const c = this._vectorColorOf.get(String(it.raw[this._vectorColorField]));
|
|
14513
|
+
if (c) return c;
|
|
14514
|
+
}
|
|
14515
|
+
if (it.cat != null) return it.cat >= 0 && colors[it.cat] ? colors[it.cat] : [0, 0, 0];
|
|
14516
|
+
return Number.isInteger(it.value) && colors[it.value - 1] ? colors[it.value - 1] : [0, 0, 0];
|
|
14517
|
+
};
|
|
14518
|
+
const fade = has(/\bFADEIN\b/) ? 0.2 : 2;
|
|
14519
|
+
const gradient = bezier && has(/\bGRADIENT\b|\bFADEIN\b/);
|
|
14520
|
+
const arrow = has(/\bPOINTER|ARROW\b/);
|
|
14521
|
+
const dash = has(/\bDASH\b/);
|
|
14522
|
+
const dopacity = it => {
|
|
14523
|
+
const range = Math.pow(nMax - nMin, 1 / dopPow);
|
|
14524
|
+
return Math.pow(dopMin ? nMax - it.nSize : it.nSize - nMin, 1 / dopPow) / range * (fo || 1) * dopScale;
|
|
14525
|
+
};
|
|
14526
|
+
|
|
14527
|
+
// flat's sort before drawing (maptheme.js 16774-16880)
|
|
14528
|
+
let order = items;
|
|
14529
|
+
const sized = !!this.binding.size;
|
|
14530
|
+
if (((nMin !== nMax) || sized) && !has(/NOSRT/) && (!has(/NOSIZE/) || has(/\bSORT\b/))
|
|
14531
|
+
&& (!this.flags.has('CATEGORICAL') || this.flags.has('AGGREGATE') || sized || has(/\bSORT\b/))) {
|
|
14532
|
+
const key = (sized || (this.flags.has('AGGREGATE') && has(/SUM/))) ? (it => it.nSize)
|
|
14533
|
+
: (this._vectorCategorical ? (it => (it.cat != null && it.cat >= 0 ? it.cat + 1 : 0)) : (it => it.value));
|
|
14534
|
+
const keyed = items.map(it => ({ it, y: key(it) })).filter(d => !isNaN(d.y));
|
|
14535
|
+
const down = has(/\bSORT\b/) && has(/\bUP\b/);
|
|
14536
|
+
keyed.sort((a, b) => (down ? b.y - a.y : a.y - b.y));
|
|
14537
|
+
order = keyed.map(d => d.it);
|
|
14538
|
+
}
|
|
14539
|
+
// the flows that would be drawn (as the loop below draws them), of
|
|
14540
|
+
// which flat keeps the biggest maxcharts
|
|
14541
|
+
if (Math.round(styleNum(st.maxcharts)) > 0) {
|
|
14542
|
+
order = applyFlatMaxCharts(order.filter(it => it.p2 && !(minValue && it.nSize < minValue) && (inBox(it.p1) || inBox(it.p2))), st);
|
|
14543
|
+
}
|
|
14544
|
+
|
|
14545
|
+
const paths = [], heads = [];
|
|
14546
|
+
const N = 32;
|
|
14547
|
+
const visible = c => !globeCenter || isOnVisibleHemisphere(c[0], c[1], globeCenter);
|
|
14548
|
+
for (const it of order) {
|
|
14549
|
+
let p1 = it.p1, p2 = it.p2;
|
|
14550
|
+
if (!p2) continue;
|
|
14551
|
+
if (minValue && it.nSize < minValue) continue;
|
|
14552
|
+
if (!inBox(p1) && !inBox(p2)) continue;
|
|
14553
|
+
const rgb = colorOf(it);
|
|
14554
|
+
let pts, end, tan, sw, headS, refX, refY, headAlpha, lineAlpha, stops = null, dashArr = null;
|
|
14555
|
+
if (bezier) {
|
|
14556
|
+
if (p1[0] === p2[0] && p1[1] === p2[1]) continue;
|
|
14557
|
+
const w = rawLw != null && rawLw !== '' && lwNum ? lwNum
|
|
14558
|
+
: 10 / Math.pow(nsv || maxSize, 1 / sizePow) * Math.pow(Math.abs(it.nSize), 1 / sizePow);
|
|
14559
|
+
sw = w * unit;
|
|
14560
|
+
if (!(sw > 0)) continue;
|
|
14561
|
+
if (has(/\bREVERSE\b/) || it.nSize < 0) [p1, p2] = [p2, p1];
|
|
14562
|
+
const a = toPx(p1), b = toPx(p2);
|
|
14563
|
+
const dx = b[0] - a[0], dy = b[1] - a[1];
|
|
14564
|
+
if (!(dx * dx + dy * dy > 0)) continue;
|
|
14565
|
+
let bow = rs;
|
|
14566
|
+
if (has(/\bRANDOM\b/)) bow -= rs * 0.66 * hashUnit(p1[0] + ',' + p1[1] + '&' + p2[0] + ',' + p2[1]);
|
|
14567
|
+
const gapAdd = has(/\bGAP\b/) ? gapSize : 3;
|
|
14568
|
+
const ll = typeof rawLw === 'string' && lwNum ? Number(rawLw + String(gapAdd)) : w + gapAdd;
|
|
14569
|
+
const lay = bezierVectorLayout(dx, dy, { w, ll, unit, oz, bow, t, marker: markerSize, gap: gapSize });
|
|
14570
|
+
pts = cubicBezierPoints(lay.start, lay.c1, lay.c2, lay.end, N);
|
|
14571
|
+
end = lay.end;
|
|
14572
|
+
tan = [lay.end[0] - lay.c2[0], lay.end[1] - lay.c2[1]];
|
|
14573
|
+
headS = Math.min(5, 2.5 + 5 / (w * scale)) * markerSize;
|
|
14574
|
+
refX = headS / 1.7; refY = headS / 2;
|
|
14575
|
+
lineAlpha = fo || 0.3;
|
|
14576
|
+
headAlpha = Math.min(1, (fo || 0.3) * 2);
|
|
14577
|
+
// (flat sets the computed opacity as is; one that is no number
|
|
14578
|
+
// — every size equal — SVG ignores: the opacity stays)
|
|
14579
|
+
if (dop || dopMin) { const d = dopacity(it); if (isFinite(d)) { lineAlpha = d; headAlpha = d; } }
|
|
14580
|
+
if (gradient) stops = fadeGradientStops(pts, lay.vx, lay.vy, fade);
|
|
14581
|
+
if (dash) dashArr = [50 * oz, 5 * oz + sw];
|
|
14582
|
+
pts = pts.map(p => [p[0] + a[0], p[1] + a[1]]);
|
|
14583
|
+
end = [end[0] + a[0], end[1] + a[1]];
|
|
14584
|
+
} else {
|
|
14585
|
+
const w = (rawLw != null && rawLw !== '' && lwNum) || (2 / (nsv || 1) / maxSize * it.nSize) || 0.1;
|
|
14586
|
+
sw = w * oz;
|
|
14587
|
+
const a = toPx(p1), b = toPx(p2);
|
|
14588
|
+
pts = [];
|
|
14589
|
+
for (let i = 0; i <= N; i++) pts.push([a[0] + (b[0] - a[0]) * i / N, a[1] + (b[1] - a[1]) * i / N]);
|
|
14590
|
+
end = b;
|
|
14591
|
+
tan = [b[0] - a[0], b[1] - a[1]];
|
|
14592
|
+
headS = 4; refX = 4; refY = 2;
|
|
14593
|
+
lineAlpha = fo || 0.3;
|
|
14594
|
+
headAlpha = Math.min(1, (fo || 0.3) * 2);
|
|
14595
|
+
if (dash) dashArr = [50 * oz, 15 * oz];
|
|
14596
|
+
}
|
|
14597
|
+
// a line under a pixel: one pixel at its width as opacity
|
|
14598
|
+
const drawW = Math.max(1, sw);
|
|
14599
|
+
const thin = Math.min(1, sw);
|
|
14600
|
+
const color2 = lineColors && lineColors.length > 1 ? hexOrNamedToRgb(String(lineColors[0]).trim()) : rgb;
|
|
14601
|
+
const vertexColor = i => {
|
|
14602
|
+
const s = stops ? stops[i] : null;
|
|
14603
|
+
const k = s ? s.k : 1;
|
|
14604
|
+
const c = s ? [0, 1, 2].map(j => Math.round(color2[j] + (rgb[j] - color2[j]) * k)) : rgb.slice(0, 3);
|
|
14605
|
+
return [...c, Math.round(255 * Math.min(1, lineAlpha) * (s ? s.alpha : 1) * thin * groupAlpha)];
|
|
14606
|
+
};
|
|
14607
|
+
const properties = { value: it.nSize, cat: it.cat, raw: it.raw, count: it.count,
|
|
14608
|
+
vector: { itemValue: it.cat != null ? (it.cat >= 0 ? it.cat + 1 : 0) : it.value } };
|
|
14609
|
+
// the line in map coordinates; on the globe only its visible runs
|
|
14610
|
+
const ll = pts.map(toLngLat);
|
|
14611
|
+
let run = [], runColors = [];
|
|
14612
|
+
const flush = () => {
|
|
14613
|
+
if (run.length > 1) paths.push({ path: run, colors: runColors, width: drawW, dash: dashArr ? [dashArr[0] / drawW, dashArr[1] / drawW] : [0, 0], properties, item: it });
|
|
14614
|
+
run = []; runColors = [];
|
|
14615
|
+
};
|
|
14616
|
+
ll.forEach((c, i) => { if (visible(c)) { run.push(c); runColors.push(vertexColor(i)); } else flush(); });
|
|
14617
|
+
flush();
|
|
14618
|
+
if (arrow && visible(toLngLat(end))) {
|
|
14619
|
+
const tri = arrowMarkerTriangle(end, tan[0], tan[1], sw, headS, refX, refY).map(toLngLat);
|
|
14620
|
+
heads.push({ type: 'Feature', properties, geometry: { type: 'Polygon', coordinates: [[...tri, tri[0]]] },
|
|
14621
|
+
fill: [...rgb.slice(0, 3), Math.round(255 * Math.min(1, headAlpha) * groupAlpha)] });
|
|
14622
|
+
}
|
|
14623
|
+
}
|
|
14624
|
+
const layers = [new PathLayer({
|
|
14625
|
+
id: `ix-vector-${this.name}`, data: paths, pickable: true,
|
|
14626
|
+
getPath: d => d.path, getColor: d => d.colors, getWidth: d => d.width,
|
|
14627
|
+
widthUnits: 'pixels', capRounded: dash && bezier, jointRounded: false, billboard: false,
|
|
14628
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS,
|
|
14629
|
+
...(dash ? { getDashArray: d => d.dash, extensions: [new PathStyleExtension({ dash: true })] } : {})
|
|
14630
|
+
})];
|
|
14631
|
+
if (heads.length) {
|
|
14632
|
+
// the head count in the id: deck kept a same-id polygon layer's
|
|
14633
|
+
// larger buffers when a filter cut the heads (the Zeit page's "to
|
|
14634
|
+
// Africa": bow-tie triangles from the unfiltered heads' vertices).
|
|
14635
|
+
// Every head has the same vertex count, so an equal count — a pan —
|
|
14636
|
+
// reuses the layer; a changed one gets fresh buffers
|
|
14637
|
+
layers.push(new GeoJsonLayer({
|
|
14638
|
+
id: `ix-vector-heads-${this.name}-n${heads.length}`, data: { type: 'FeatureCollection', features: heads }, pickable: true,
|
|
14639
|
+
filled: true, stroked: false, getFillColor: d => d.fill, parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14640
|
+
}));
|
|
14641
|
+
}
|
|
14642
|
+
return layers;
|
|
14643
|
+
}
|
|
14644
|
+
|
|
12614
14645
|
// flat's chart position for this build (flatChartAlignOffset): a
|
|
12615
14646
|
// function item → pixel offset, the same for every part of one chart
|
|
12616
14647
|
// (symbol, glow, shadow, box, title, value text). unit is flat's
|
|
@@ -13755,11 +15786,12 @@ in vec4 vPieLineColor;
|
|
|
13755
15786
|
const map = _lastMapApi.map;
|
|
13756
15787
|
const center = map.getCenter();
|
|
13757
15788
|
const proj = (typeof map.getProjection === 'function' && map.getProjection()) || { type: 'mercator' };
|
|
15789
|
+
const svgProjection = mapSvgProjection(map);
|
|
13758
15790
|
return JSON.stringify({
|
|
13759
15791
|
map: {
|
|
13760
|
-
map: proj.type === 'globe' ? 'maps/svg/maps/generic/orthographic.svg' : 'maps/svg/maps/generic/mercator.svg',
|
|
15792
|
+
map: proj.type === 'globe' ? 'maps/svg/maps/generic/orthographic.svg' : svgProjection ? svgProjection.svg : 'maps/svg/maps/generic/mercator.svg',
|
|
13761
15793
|
center: { lat: center.lat, lng: center.lng },
|
|
13762
|
-
zoom:
|
|
15794
|
+
zoom: flatViewZoomOf(map)
|
|
13763
15795
|
}
|
|
13764
15796
|
});
|
|
13765
15797
|
}
|
|
@@ -13830,6 +15862,13 @@ in vec4 vPieLineColor;
|
|
|
13830
15862
|
htmlgui_getAttributionString: () => (_lastMapApi && _lastMapApi.getAttribution ? _lastMapApi.getAttribution() : ''),
|
|
13831
15863
|
changeThemeStyle: compatChangeThemeStyle,
|
|
13832
15864
|
removeTheme: compatRemoveTheme,
|
|
15865
|
+
// flat's ixmaps.setLocalString(orig, local) (htmlgui.js 3814 →
|
|
15866
|
+
// Api.setLocalString → Dictionary.replace): a page-wide translation,
|
|
15867
|
+
// e.g. the migration page's ISO code → country name; usable before
|
|
15868
|
+
// the map exists. getLocalString: its translation (flat's never
|
|
15869
|
+
// returns it — a missing return there)
|
|
15870
|
+
setLocalString: (szOrig, szLocal) => { _localStrings[szOrig] = szLocal; },
|
|
15871
|
+
getLocalString: (szOrig) => makeLocalText([])(szOrig),
|
|
13833
15872
|
// flat's ixmaps.newTheme(title, theme, flag) — on the last built map
|
|
13834
15873
|
newTheme: (title, theme, flag) => _mapHandle.newTheme(title, theme, flag),
|
|
13835
15874
|
// flat's time slider: show the records of a theme (null = all themes) whose timefield lies in [min, max)
|
|
@@ -13858,16 +15897,16 @@ in vec4 vPieLineColor;
|
|
|
13858
15897
|
// test-only: lets test/unit/*.test.mjs call pure internals directly (the
|
|
13859
15898
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
13860
15899
|
global.__ixmapsGlInternals = {
|
|
13861
|
-
IXMAPS_GL_VERSION, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout,
|
|
15900
|
+
IXMAPS_GL_VERSION, applyFlatMaxCharts, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout, ringsLayout, pieSliceLayout, pieValueLabelLayout, isBarChart, flatBarLayout, flatChartBox, flatValueRules, flatPieRecord, flatPieAccumulate, flatPieItemValues, flatVectorItems, bezierVectorLayout, cubicBezierPoints, arrowMarkerTriangle, fadeGradientStops, flatToArray, hashUnit, multiQuadOffsets, pixelOffsetLngLat, chartTextTransmittance, insideSymbolShape,
|
|
13862
15901
|
normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
13863
15902
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
13864
15903
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|
|
13865
15904
|
computeAlphaStats, computeMultiFieldClasses, computeDominantStats, resolveDominantClass, computeComposeColorStats,
|
|
13866
15905
|
resolveComposedColor, computeRangeClasses, colorSchemeClassCount, resolveDopacityAlpha,
|
|
13867
15906
|
flatColorSweep, applyClassesToColorScheme, resolveClassColors, flatOutlierStats, parseCssColor, flatLegendLook,
|
|
13868
|
-
flatToMapLibreZoom, mapLibreToFlatZoom, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
|
|
15907
|
+
flatToMapLibreZoom, mapLibreToFlatZoom, flatViewToMapLibreZoom, mapLibreToFlatViewZoom, flatSvgProjectionOf, flatSvgProjectionByOptions, normalSizeScaleOf, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
|
|
13869
15908
|
fetchLayerData, parseCsvText, dataTableRows, geometryRowsToFeatureCollection, filterFlatValues, applyField100, field100Binding, rangeClassLegendTotals, resolveAggregateValue, classValueSeparate, cellAggregatedValues, oneHot, groupCoLocated, aggregateOnGrid, GridAggregateIndex, flatRangeParts, aggregateField100, valueRadius, itemPlotGeometry, itemAnchor, objectZoomFactor, resolveZoomReference, defaultNormalSizeValue,
|
|
13870
|
-
applyWhereFilter, joinChartPositions, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
15909
|
+
applyWhereFilter, joinChartPositions, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatBubbleLook, bubbleIconLook, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
13871
15910
|
drawSymbolPath, normalizeSymbolShape,
|
|
13872
15911
|
flatFormatValue, themeStyleArgs, flatChangedStyleValue, explicitValueDecimals, legendDecimals,
|
|
13873
15912
|
dataCacheKey, dataCacheDisabled, cachedLayerData, featuresBounds, wantsZoomToExtent, legendIsOn,
|