ixmaps-gl 0.2.20 → 0.2.22
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 +2200 -104
- 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.22';
|
|
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() : '?';
|
|
@@ -1430,6 +1542,16 @@ in vec4 vPieLineColor;
|
|
|
1430
1542
|
_filterCache.set(expr, pred);
|
|
1431
1543
|
return pred;
|
|
1432
1544
|
}
|
|
1545
|
+
// a load-time .filter() drops records for good, so a later changeThemeStyle
|
|
1546
|
+
// "filter" must re-filter the unfiltered data (flat re-processes the theme
|
|
1547
|
+
// with the new filter) instead of narrowing what the first filter left
|
|
1548
|
+
function refilterFrom(raw, spec, runtimes) {
|
|
1549
|
+
return expr => {
|
|
1550
|
+
const f = applyWhereFilter(raw, expr);
|
|
1551
|
+
return f.type === 'Table' ? joinTableFeatures(spec, f, runtimes) : f;
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1433
1555
|
function applyWhereFilter(fc, filterExpr) {
|
|
1434
1556
|
if (!filterExpr) return fc;
|
|
1435
1557
|
const pred = flatFilterPredicate(String(filterExpr));
|
|
@@ -1547,13 +1669,17 @@ in vec4 vPieLineColor;
|
|
|
1547
1669
|
const lookupField = spec.binding.lookup;
|
|
1548
1670
|
let layers = String(spec.name).split('|');
|
|
1549
1671
|
if (layers.length > 1 && spec.flags && spec.flags.has('DIFFERENCE')) layers = layers.slice(-1);
|
|
1672
|
+
const lookup2 = spec.binding.lookup2;
|
|
1550
1673
|
const features = [];
|
|
1551
|
-
|
|
1674
|
+
const indexes = layers.map(layer => {
|
|
1552
1675
|
const index = new Map();
|
|
1553
1676
|
runtimes.filter(r => r.name === layer && (r.flags.has('FEATURE') || r.flags.has('FEATURES'))).forEach(r => {
|
|
1554
1677
|
// in theme order, as flat's layer holds the themes' shapes
|
|
1555
1678
|
for (const [k, v] of featurePositionIndex(r)) { if (flatShapeWins(index.get(k), v.area)) index.set(k, v); }
|
|
1556
1679
|
});
|
|
1680
|
+
return index;
|
|
1681
|
+
});
|
|
1682
|
+
for (const index of indexes) {
|
|
1557
1683
|
if (!index.size) continue;
|
|
1558
1684
|
for (const row of table.rows) {
|
|
1559
1685
|
const v = row[lookupField];
|
|
@@ -1563,7 +1689,18 @@ in vec4 vPieLineColor;
|
|
|
1563
1689
|
// joined shape, as flat) needs the shape's km² area carried along
|
|
1564
1690
|
// (featureAreaKm2 prefers this marker), exactly as the choropleth
|
|
1565
1691
|
// join stamps it
|
|
1566
|
-
if (hit)
|
|
1692
|
+
if (!hit) continue;
|
|
1693
|
+
const f = { type: 'Feature', geometry: { type: 'Point', coordinates: hit.pos }, properties: row, _flatAreaKm2: hit.areaKm2 };
|
|
1694
|
+
// VECTOR / BEZIER: the second position by its own key (flat looks
|
|
1695
|
+
// it up in every layer of the theme, maptheme.js 10431-10436); a
|
|
1696
|
+
// record without one stays an item that draws no line (null)
|
|
1697
|
+
if (lookup2 && !String(lookup2).includes('|')) {
|
|
1698
|
+
const v2 = row[lookup2];
|
|
1699
|
+
let hit2 = null;
|
|
1700
|
+
if (v2 != null && v2 !== '') for (const idx of indexes) { hit2 = idx.get(flatLookupKey(v2, spec.style)); if (hit2) break; }
|
|
1701
|
+
f._pos2 = hit2 ? hit2.pos : null;
|
|
1702
|
+
}
|
|
1703
|
+
features.push(f);
|
|
1567
1704
|
}
|
|
1568
1705
|
}
|
|
1569
1706
|
return { type: 'FeatureCollection', features };
|
|
@@ -2064,6 +2201,26 @@ in vec4 vPieLineColor;
|
|
|
2064
2201
|
'POW2', 'POW3', 'NOSORT'];
|
|
2065
2202
|
// flat's PIE / DONUT variants with no rendering here (see _buildPieLayers)
|
|
2066
2203
|
const PIE_INERT_FLAGS = [/3D/, /VOLUME/, /HEIGHT/, /STARBURST/, /FLOWER/, /RAYS/, /BOW/, /ZOOM/, /DIRECTION/, /DIRECTED/, /POLAR/];
|
|
2204
|
+
// ... and of AGGREGATE / CATEGORICAL pies: GRIDSIZE's pie the size of its
|
|
2205
|
+
// grid cell (drawn by the SIZE rule instead), AUTOSIZE, DOPACITY, the
|
|
2206
|
+
// chart BOX and TITLE, GAUGE, MULTIPLE / DIFFUSE, the post-aggregation
|
|
2207
|
+
// value arithmetic (DIFFERENCE, FRACTION, PERCENT …, field100)
|
|
2208
|
+
const PIE_AGGREGATE_INERT_FLAGS = [/^GRIDSIZE$/, /AUTOSIZE/, /DOPACITY/, /BOX/, /TITLE/, /^GAUGE$/, /^MULTIPLE$/, /^DIFFUSE$/,
|
|
2209
|
+
/^DIFFERENCE$/, /^FRACTION$/, /^PERCENT$/, /^PERMILLE$/, /^PRODUCT$/, /^CALCVAL$/, /^RELATIVE$/];
|
|
2210
|
+
// flat's BAR / BARS variants with no rendering here (see flatBarLayout):
|
|
2211
|
+
// the bar is drawn plain (SMALLARROW as ARROW) — tested as substrings,
|
|
2212
|
+
// as flat does
|
|
2213
|
+
const BAR_INERT_FLAGS = [/3D/, /VOLUME/, /COLUMN/, /DOTTED/, /SMALLARROW/, /TRENDLINE/, /AXIS/, /^CLIP$/, /MORPH/,
|
|
2214
|
+
/ZOOM/, /NORMSIZE/, /MENUSIZE/, /INVERT/, /DIAGLEFT/, /DIAGRIGHT/, /^WIDTH$/, /^MULTIPLE$/, /^DIFFUSE$/,
|
|
2215
|
+
/^DIFFERENCE$/, /^FRACTION$/, /^PERCENT$/, /^PERMILLE$/, /^PRODUCT$/, /^CALCVAL$/, /^RELATIVE$/];
|
|
2216
|
+
// flat's VECTOR / BEZIER modifiers with no rendering here (see
|
|
2217
|
+
// _buildVectorLayers), the word → what happens instead
|
|
2218
|
+
const VECTOR_INERT_FLAGS = {
|
|
2219
|
+
VALUES: 'the value text along the curve is not drawn yet',
|
|
2220
|
+
DYNAMICWIDTH: 'not drawn yet (flat widens by the log of the length in its own map units) — the plain width instead',
|
|
2221
|
+
LONG: 'not drawn yet (its bow depends on flat\'s own map units) — the default bow instead',
|
|
2222
|
+
DASH: 'drawn as static dashes — flat\'s dash animation is not ported'
|
|
2223
|
+
};
|
|
2067
2224
|
const _warnedFlags = new Set();
|
|
2068
2225
|
|
|
2069
2226
|
// GL-PORT COMPAT: real ixmaps-flat's global ixmaps.getThemeObj(szId) /
|
|
@@ -2079,6 +2236,19 @@ in vec4 vPieLineColor;
|
|
|
2079
2236
|
// build()'s main loop and defineLayer(), the two places a runtime is
|
|
2080
2237
|
// ever created, for where this gets populated.
|
|
2081
2238
|
const _globalThemeRegistry = new Map();
|
|
2239
|
+
// flat's theme index (maptheme.js getTheme, "theme 0,1,2,..."): a number or
|
|
2240
|
+
// numeric string below the theme count addresses the n-th theme
|
|
2241
|
+
function isThemeIndex(id, count) {
|
|
2242
|
+
return id != null && id !== '' && typeof id !== 'boolean' && !isNaN(Number(id)) && Number.isInteger(Number(id)) && Number(id) >= 0 && Number(id) < count;
|
|
2243
|
+
}
|
|
2244
|
+
// the runtime of a theme id for the module-level globals: by name, else the
|
|
2245
|
+
// n-th theme of the last built map
|
|
2246
|
+
function registryTheme(id) {
|
|
2247
|
+
const rt = _globalThemeRegistry.get(id);
|
|
2248
|
+
if (rt) return rt;
|
|
2249
|
+
const list = _lastMapApi && _lastMapApi.runtimes;
|
|
2250
|
+
return list && isThemeIndex(id, list.length) ? list[Number(id)] : undefined;
|
|
2251
|
+
}
|
|
2082
2252
|
|
|
2083
2253
|
// Same LAST-DEFINITION-WINS rationale as _globalThemeRegistry above, for
|
|
2084
2254
|
// real ixmaps-flat's map-LEVEL globals (ixmaps.getProjectString/
|
|
@@ -2401,6 +2571,9 @@ in vec4 vPieLineColor;
|
|
|
2401
2571
|
'style.valuefield': ['style', 'valuefield'],
|
|
2402
2572
|
'style.timefield': ['binding', 'time'], // setThemeTimeFrame, see LayerRuntime.setTimeFrame
|
|
2403
2573
|
'style.titlefield': ['binding', 'title'], // {{theme.item.title}}, see _buildTooltipContext
|
|
2574
|
+
// VECTOR / BEZIER's second position: a join key ("a") or lat/lon
|
|
2575
|
+
// fields ("a|b"), see joinChartPositions and _vectorRecords
|
|
2576
|
+
'style.lookupfield2': ['binding', 'lookup2'],
|
|
2404
2577
|
// the lookup key's normalization (flatLookupKey): flat's newTheme moves
|
|
2405
2578
|
// binding.tonumber / digits / … into these style keys (htmlgui.js 1712)
|
|
2406
2579
|
'style.lookupdigits': ['style', 'lookupdigits'],
|
|
@@ -2633,7 +2806,10 @@ in vec4 vPieLineColor;
|
|
|
2633
2806
|
// real ixmaps-flat (htmlgui.js newTheme) merges .meta() INTO style first
|
|
2634
2807
|
// — meta wins — and only then reads type, bindings and style properties
|
|
2635
2808
|
const style = Object.assign({}, def.style, def.meta);
|
|
2636
|
-
|
|
2809
|
+
let typeStr = style.type != null ? String(style.type) : '';
|
|
2810
|
+
// flat (htmlgui-side theme parse, maptheme.js 1192): the old type
|
|
2811
|
+
// word EXACT is CATEGORICAL
|
|
2812
|
+
if (/\bEXACT\b/.test(typeStr) && !/\bCATEGORICAL\b/.test(typeStr)) typeStr += '|CATEGORICAL';
|
|
2637
2813
|
const filter = style.filter;
|
|
2638
2814
|
const title = style.title;
|
|
2639
2815
|
delete style.type;
|
|
@@ -2664,6 +2840,37 @@ in vec4 vPieLineColor;
|
|
|
2664
2840
|
console.info(`[ixmaps-gl] type flag "${flag}" on a PIE chart recognized, not drawn yet — the pie is drawn flat and plain`);
|
|
2665
2841
|
}
|
|
2666
2842
|
});
|
|
2843
|
+
// AGGREGATE / CATEGORICAL pies (_isAggregatedPie): what is not ported
|
|
2844
|
+
if (flags.has('AGGREGATE') || flags.has('CATEGORICAL')) {
|
|
2845
|
+
flags.forEach(flag => {
|
|
2846
|
+
const inert = PIE_AGGREGATE_INERT_FLAGS.find(re => re.test(flag));
|
|
2847
|
+
if (inert && !_warnedFlags.has('PIEAGG:' + flag)) {
|
|
2848
|
+
_warnedFlags.add('PIEAGG:' + flag);
|
|
2849
|
+
console.info(`[ixmaps-gl] type flag "${flag}" on an aggregated PIE chart recognized, not ported yet — ignored`);
|
|
2850
|
+
}
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
// BAR variants _buildBarLayers doesn't draw (the bars are drawn plain)
|
|
2856
|
+
if (isBarChart(flags)) {
|
|
2857
|
+
flags.forEach(flag => {
|
|
2858
|
+
if (BAR_INERT_FLAGS.some(re => re.test(flag)) && !_warnedFlags.has('BAR:' + flag)) {
|
|
2859
|
+
_warnedFlags.add('BAR:' + flag);
|
|
2860
|
+
console.info(`[ixmaps-gl] type flag "${flag}" on a BAR chart recognized, not ported yet — the bars are drawn plain`);
|
|
2861
|
+
}
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
// VECTOR / BEZIER modifiers _buildVectorLayers doesn't draw
|
|
2866
|
+
if (isVectorChart(flags)) {
|
|
2867
|
+
flags.forEach(flag => {
|
|
2868
|
+
const why = VECTOR_INERT_FLAGS[flag];
|
|
2869
|
+
if (why && !_warnedFlags.has('VECTOR:' + flag)) {
|
|
2870
|
+
_warnedFlags.add('VECTOR:' + flag);
|
|
2871
|
+
console.info(`[ixmaps-gl] type flag "${flag}" on a VECTOR/BEZIER chart recognized, ${why}`);
|
|
2872
|
+
}
|
|
2873
|
+
});
|
|
2667
2874
|
}
|
|
2668
2875
|
|
|
2669
2876
|
const rawBinding = Object.assign({}, def.binding);
|
|
@@ -2726,6 +2933,9 @@ in vec4 vPieLineColor;
|
|
|
2726
2933
|
opacity: 1; transition: opacity 0.4s ease;
|
|
2727
2934
|
}
|
|
2728
2935
|
.ixmaps-splash.ixmaps-splash-hidden { opacity: 0; pointer-events: none; }
|
|
2936
|
+
.ixmaps-splash.ixmaps-splash-dark { background: #1a1a1a; }
|
|
2937
|
+
.ixmaps-splash-dark .ixmaps-splash-word { color: #e8e8e8; }
|
|
2938
|
+
.ixmaps-splash-dark .ixmaps-splash-text { color: #9a9a9a; }
|
|
2729
2939
|
.ixmaps-splash-mark { position: relative; width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; }
|
|
2730
2940
|
.ixmaps-splash-ping {
|
|
2731
2941
|
position: absolute; width: 16px; height: 16px; border-radius: 50%;
|
|
@@ -2761,15 +2971,21 @@ in vec4 vPieLineColor;
|
|
|
2761
2971
|
// flat's Dictionary.getLocalText (mapscript.js 5631-5648) over the page's
|
|
2762
2972
|
// .local(text, translation) pairs: the whole text, else word by word
|
|
2763
2973
|
// (a word's entry is written in quotes, 'word')
|
|
2974
|
+
// plus the page-wide entries of ixmaps.setLocalString (flat's
|
|
2975
|
+
// Api.setLocalString → Dictionary.replace, mapapi.js 123), read at call
|
|
2976
|
+
// time so strings set after the map is built count; the later
|
|
2977
|
+
// setLocalString wins over a .local() pair of the same text
|
|
2978
|
+
const _localStrings = Object.create(null);
|
|
2764
2979
|
function makeLocalText(locals) {
|
|
2765
2980
|
const dict = {};
|
|
2766
2981
|
(locals || []).forEach(a => {
|
|
2767
2982
|
if (a && typeof a[0] === 'object' && a[0]) Object.assign(dict, a[0]);
|
|
2768
2983
|
else if (a && a.length >= 2) dict[a[0]] = a[1];
|
|
2769
2984
|
});
|
|
2985
|
+
const entry = k => (_localStrings[k] !== undefined ? _localStrings[k] : dict[k]);
|
|
2770
2986
|
return text => {
|
|
2771
|
-
if (
|
|
2772
|
-
return String(text).split(' ').map(w => (
|
|
2987
|
+
if (entry(text) !== undefined) return String(entry(text));
|
|
2988
|
+
return String(text).split(' ').map(w => (entry("'" + w + "'") !== undefined ? String(entry("'" + w + "'")) : w)).join(' ');
|
|
2773
2989
|
};
|
|
2774
2990
|
}
|
|
2775
2991
|
// make the map container the positioning context of what the engine
|
|
@@ -2782,7 +2998,7 @@ in vec4 vPieLineColor;
|
|
|
2782
2998
|
function anchorInside(el) {
|
|
2783
2999
|
if (el && getComputedStyle(el).position === 'static') el.style.position = 'relative';
|
|
2784
3000
|
}
|
|
2785
|
-
function showSplash(el, text) {
|
|
3001
|
+
function showSplash(el, text, dark) {
|
|
2786
3002
|
ensureSplashStyle();
|
|
2787
3003
|
// Checks the actual RENDERED position, not el.style (the inline
|
|
2788
3004
|
// attribute only) — every example page's own #map_div already sets
|
|
@@ -2797,7 +3013,7 @@ in vec4 vPieLineColor;
|
|
|
2797
3013
|
// this at all.
|
|
2798
3014
|
anchorInside(el);
|
|
2799
3015
|
const splash = document.createElement('div');
|
|
2800
|
-
splash.className = 'ixmaps-splash';
|
|
3016
|
+
splash.className = 'ixmaps-splash' + (dark ? ' ixmaps-splash-dark' : '');
|
|
2801
3017
|
splash.innerHTML = '<div class="ixmaps-splash-mark"><span class="ixmaps-splash-ping"></span><span class="ixmaps-splash-dot"></span></div>' +
|
|
2802
3018
|
'<div class="ixmaps-splash-word">ixmaps<sup>gl</sup></div>' +
|
|
2803
3019
|
'<div class="ixmaps-splash-text"></div>';
|
|
@@ -3569,7 +3785,10 @@ in vec4 vPieLineColor;
|
|
|
3569
3785
|
};
|
|
3570
3786
|
// a plain FEATURE(S) base layer has one flat fill (no classes); FEATURE|CATEGORICAL
|
|
3571
3787
|
// is coloured per category (see _buildFeaturesLayers), so it gets a legend
|
|
3572
|
-
|
|
3788
|
+
// a CATEGORICAL theme with no value binding (a count per aggregation cell) has no
|
|
3789
|
+
// category rows: its panel is the text legend (title / snippet / description)
|
|
3790
|
+
const countOnlyLegend = rt => !rt.categoryLabels && rt.flags.has('CATEGORICAL') && !rt.binding.value;
|
|
3791
|
+
const legendApplies = rt => (countOnlyLegend(rt) || (rt.categoryLabels && rt.categoryLabels.length))
|
|
3573
3792
|
&& (!(rt.flags.has('FEATURE') || rt.flags.has('FEATURES')) || rt.flags.has('CATEGORICAL')) && !rt.flags.has('NOLEGEND');
|
|
3574
3793
|
// One theme's panel — at build time for every theme, and again for a
|
|
3575
3794
|
// theme defined later (map.layer(...) in a myMap.then(...) chain,
|
|
@@ -3663,7 +3882,9 @@ in vec4 vPieLineColor;
|
|
|
3663
3882
|
// aggregation" reading (style.valuefield, falling back to
|
|
3664
3883
|
// the bound size field); anything else (plain CATEGORICAL,
|
|
3665
3884
|
// no SUM) falls back to a per-category record COUNT.
|
|
3666
|
-
|
|
3885
|
+
// a VECTOR / BEZIER flow legend sums its size field (flat's rows
|
|
3886
|
+
// show the flows' summed sizes, measured on the twin page)
|
|
3887
|
+
const useSum = flatFlag(rt.flags, 'SUM') && (rt.style.valuefield || ((rt._isVectorChart() || rt._isAggregatedPie()) && rt.binding.size));
|
|
3667
3888
|
const valueField = rt.style.valuefield || rt.binding.size;
|
|
3668
3889
|
// legendunits wins over the theme's general-purpose units
|
|
3669
3890
|
// (used elsewhere for tooltips, e.g. _renderItemChartHtml) —
|
|
@@ -3672,13 +3893,18 @@ in vec4 vPieLineColor;
|
|
|
3672
3893
|
// space injected), matching how style.units/legendunits are
|
|
3673
3894
|
// themselves authored with their own leading space (e.g.
|
|
3674
3895
|
// " MW") in real pages.
|
|
3675
|
-
|
|
3896
|
+
// (a VECTOR / BEZIER or aggregated PIE legend: flat's szUnit, the
|
|
3897
|
+
// units after a space unless they start with ".", measured on the
|
|
3898
|
+
// twin pages)
|
|
3899
|
+
const flatUnitLegend = rt._isVectorChart() || rt._isAggregatedPie();
|
|
3900
|
+
const rawLegendUnit = rt.style.legendunits || rt.style.units || '';
|
|
3901
|
+
const legendUnit = flatUnitLegend && rawLegendUnit && rawLegendUnit[0] !== '.' && rawLegendUnit[0] !== ' ' ? ' ' + rawLegendUnit : rawLegendUnit;
|
|
3676
3902
|
// a per-item chart over several value fields: the row values as
|
|
3677
3903
|
// flat's legend prints them (legend.js 610, 810: __formatValue
|
|
3678
3904
|
// with valuedecimals, else 2, "BLANK" — whole numbers without
|
|
3679
3905
|
// decimals), a colorscheme "none" part as an outline in linecolor
|
|
3680
3906
|
const multiFieldLegend = rt._isMultiFieldChart();
|
|
3681
|
-
const legendTotalText = v => (multiFieldLegend
|
|
3907
|
+
const legendTotalText = v => ((multiFieldLegend || flatUnitLegend)
|
|
3682
3908
|
? flatFormatValue(v, legendDecimals(rt.style), 'BLANK')
|
|
3683
3909
|
: rt._formatTooltipValue(v));
|
|
3684
3910
|
const outlineRgb = rt.style.linecolor && styleLineColor(rt.style.linecolor) !== 'none' ? hexOrNamedToRgb(styleLineColor(rt.style.linecolor)) : [128, 128, 128];
|
|
@@ -3698,7 +3924,7 @@ in vec4 vPieLineColor;
|
|
|
3698
3924
|
// dropdown and Chart-size slider stay: neither is a
|
|
3699
3925
|
// "categorical item" or "colorscheme swatch", both are
|
|
3700
3926
|
// independent controls unrelated to the row list.
|
|
3701
|
-
const isTextOnly = rt.flags.has('TEXTLEGEND');
|
|
3927
|
+
const isTextOnly = rt.flags.has('TEXTLEGEND') || countOnlyLegend(rt);
|
|
3702
3928
|
|
|
3703
3929
|
// Map-view-aware per explicit request: totals reflect only
|
|
3704
3930
|
// what's CURRENTLY on screen (the map's own bounds, plus the
|
|
@@ -4238,6 +4464,11 @@ in vec4 vPieLineColor;
|
|
|
4238
4464
|
await null;
|
|
4239
4465
|
const el = document.getElementById(this.containerId);
|
|
4240
4466
|
if (!el) throw new Error(`[ixmaps-gl] container #${this.containerId} not found`);
|
|
4467
|
+
// flat's init replaces the container's content with its own map page
|
|
4468
|
+
// (mappage.html via innerHTML), so a page's placeholder inside the
|
|
4469
|
+
// map div ("connecting to Internet ...", the fuel-price page) goes
|
|
4470
|
+
// away; here it would stay on top of the map — remove it the same way
|
|
4471
|
+
while (el.firstChild) el.removeChild(el.firstChild);
|
|
4241
4472
|
// GL-PORT COMPAT: ixmaps.Map(id, {width, height}) — flat sizes its
|
|
4242
4473
|
// inner map div with these and gives PIXEL values to the page's own
|
|
4243
4474
|
// container too (htmlgui_flat.js:866-876, parent().css(...)); here
|
|
@@ -4248,6 +4479,11 @@ in vec4 vPieLineColor;
|
|
|
4248
4479
|
const v = this.mapOptions && this.mapOptions[dim];
|
|
4249
4480
|
if (typeof v === 'string' && /^\s*\d+(\.\d+)?\s*px\s*$/i.test(v)) el.style[dim] = v.trim();
|
|
4250
4481
|
else if (typeof v === 'number' && v > 0) el.style[dim] = v + 'px';
|
|
4482
|
+
// a percentage stays the page's CSS — unless that CSS left the
|
|
4483
|
+
// container collapsed on this side (e.g. an empty flex item, the
|
|
4484
|
+
// UNHCR page): flat's own pixel-sized map div fills it there
|
|
4485
|
+
else if (typeof v === 'string' && /^\s*\d+(\.\d+)?\s*%\s*$/.test(v)
|
|
4486
|
+
&& !(dim === 'width' ? el.clientWidth : el.clientHeight)) el.style[dim] = v.trim();
|
|
4251
4487
|
}
|
|
4252
4488
|
// without width/height flat's map is "fullscreen" (htmlgui_flat.js
|
|
4253
4489
|
// 847-876): it fills the window, whatever the page's CSS gives the
|
|
@@ -4263,15 +4499,29 @@ in vec4 vPieLineColor;
|
|
|
4263
4499
|
// overrides the default message, .options({splash:false}) skips it
|
|
4264
4500
|
// entirely (e.g. for a map embedded somewhere a full-cover overlay
|
|
4265
4501
|
// would be wrong, or one that's expected to load near-instantly).
|
|
4502
|
+
// dark on a dark map (no basemap style yet: flatLegendLook's name/
|
|
4503
|
+
// color rule, or a basemap faded to <= 0.5 over a dark page, as
|
|
4504
|
+
// effectiveMapDark) — a light sheet flashed between a dark page and
|
|
4505
|
+
// a dark map (the UNHCR page)
|
|
4506
|
+
const splashOpacity = parseFloat(this._engineOptions.basemapopacity);
|
|
4507
|
+
const splashLegendBg = this.mapOptions.legendBackground || this.mapOptions.legendbackground;
|
|
4508
|
+
const splashDark = flatLegendLook(this.mapOptions.mapType, isNaN(splashOpacity) ? 1 : splashOpacity, splashLegendBg).dark
|
|
4509
|
+
|| (splashOpacity <= 0.5 && flatIsDarkColor('rgb(' + pageBackgroundColorOf(el).map(Math.round).join(',') + ')'));
|
|
4266
4510
|
const splash = this._engineOptions.splash === false ? null
|
|
4267
|
-
: showSplash(el, this._engineOptions.splashText || 'loading…');
|
|
4511
|
+
: showSplash(el, this._engineOptions.splashText || 'loading…', splashDark);
|
|
4268
4512
|
|
|
4269
4513
|
// ixmaps.getBoundingBox()/setTitle() for brokers called below, before
|
|
4270
4514
|
// the MapLibre map exists (same default view as the map gets)
|
|
4271
4515
|
_titleHost = el;
|
|
4272
|
-
|
|
4516
|
+
// a flat SVG map in another projection (equalearth.svg): its view
|
|
4517
|
+
// framing and map scale (see FLAT_SVG_PROJECTIONS)
|
|
4518
|
+
const svgProjection = flatSvgProjectionOf(this.mapOptions.map);
|
|
4519
|
+
if (svgProjection) flatSvgProjectionByOptions.set(this._engineOptions, svgProjection);
|
|
4520
|
+
const [vLat, vLng] = this._viewCenter || [45.5, 9.2];
|
|
4521
|
+
const initialZoom = this._viewZoom != null && this._viewZoom !== ''
|
|
4522
|
+
? flatViewToMapLibreZoom(Number(this._viewZoom), Number(vLat), Number(vLng), el.clientWidth, el.clientHeight, svgProjection)
|
|
4523
|
+
: 8;
|
|
4273
4524
|
{
|
|
4274
|
-
const [vLat, vLng] = this._viewCenter || [45.5, 9.2];
|
|
4275
4525
|
_boundsSource = () => viewBounds(Number(vLat), Number(vLng), initialZoom, el.clientWidth, el.clientHeight);
|
|
4276
4526
|
}
|
|
4277
4527
|
|
|
@@ -4353,6 +4603,7 @@ in vec4 vPieLineColor;
|
|
|
4353
4603
|
const filtered = applyWhereFilter(raw, spec.filter);
|
|
4354
4604
|
const fc = filtered.type === 'Table' ? joinTableFeatures(spec, filtered, runtimes) : filtered;
|
|
4355
4605
|
const rt = new LayerRuntime(spec, fc, this._engineOptions);
|
|
4606
|
+
if (spec.filter) rt._refilter = refilterFrom(raw, spec, runtimes);
|
|
4356
4607
|
// tags which underlying data source this runtime came from — see
|
|
4357
4608
|
// setFacetFilter/clearFacetFilter/clearAllFacetFilters below,
|
|
4358
4609
|
// which use this to propagate a facet filter to every theme
|
|
@@ -4390,7 +4641,7 @@ in vec4 vPieLineColor;
|
|
|
4390
4641
|
style: mapTypeColor ? buildBlankBackgroundStyle(mapTypeColor)
|
|
4391
4642
|
: resolveBasemapStyleUrl(this.mapOptions.mapType),
|
|
4392
4643
|
center: [lon, lat],
|
|
4393
|
-
zoom:
|
|
4644
|
+
zoom: initialZoom,
|
|
4394
4645
|
// MapLibre's own AttributionControl (bottom right) keeps the
|
|
4395
4646
|
// basemap's required CARTO/OpenStreetMap credit; the page's own
|
|
4396
4647
|
// .attribution() goes bottom left, as flat shows it
|
|
@@ -4414,6 +4665,7 @@ in vec4 vPieLineColor;
|
|
|
4414
4665
|
// world once, at the cost of that zoom limit.
|
|
4415
4666
|
renderWorldCopies: this._engineOptions.worldcopies !== false
|
|
4416
4667
|
});
|
|
4668
|
+
engineOptionsByMap.set(map, this._engineOptions);
|
|
4417
4669
|
// the live-SVG pane USER charts render into (see chartHostFor) —
|
|
4418
4670
|
// registered here, keyed by this map's options object, so every
|
|
4419
4671
|
// runtime (including later loadProject/map.layer ones) finds it
|
|
@@ -4461,7 +4713,7 @@ in vec4 vPieLineColor;
|
|
|
4461
4713
|
if (typeof hook !== 'function' || !prev) return;
|
|
4462
4714
|
const p = map.project(c), q = map.project(prev.c);
|
|
4463
4715
|
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,
|
|
4716
|
+
try { hook.call(global.ixmaps, flatViewZoomOf(map, z)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onZoomAndPan:', e); }
|
|
4465
4717
|
});
|
|
4466
4718
|
notifyZoomAndPanMidGesture = () => {
|
|
4467
4719
|
const hook = pageIxmaps() && pageIxmaps().htmlgui_onZoomAndPan;
|
|
@@ -4469,13 +4721,13 @@ in vec4 vPieLineColor;
|
|
|
4469
4721
|
const z = map.getZoom();
|
|
4470
4722
|
if (z === last.z) return; // a pan alone waits for 'moveend' (and its 10 px rule)
|
|
4471
4723
|
last = { z, c: map.getCenter() };
|
|
4472
|
-
try { hook.call(global.ixmaps,
|
|
4724
|
+
try { hook.call(global.ixmaps, flatViewZoomOf(map, z)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onZoomAndPan:', e); }
|
|
4473
4725
|
};
|
|
4474
4726
|
// flat calls it on the first draw too (its old zoom is unset then)
|
|
4475
4727
|
map.once('load', () => {
|
|
4476
4728
|
last = { z: map.getZoom(), c: map.getCenter() };
|
|
4477
4729
|
const hook = pageIxmaps() && pageIxmaps().htmlgui_onZoomAndPan;
|
|
4478
|
-
if (typeof hook === 'function') { try { hook.call(global.ixmaps,
|
|
4730
|
+
if (typeof hook === 'function') { try { hook.call(global.ixmaps, flatViewZoomOf(map, last.z)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onZoomAndPan:', e); } }
|
|
4479
4731
|
});
|
|
4480
4732
|
}
|
|
4481
4733
|
|
|
@@ -4505,10 +4757,12 @@ in vec4 vPieLineColor;
|
|
|
4505
4757
|
// carry the suffix; stripping a pattern that isn't there is a
|
|
4506
4758
|
// no-op. A theme's single circles (ix-bubbles-<name>-circles-gN,
|
|
4507
4759
|
// a ScatterplotLayer) route like its bubbles.
|
|
4508
|
-
const base = layerId.replace(/-g\d+$/, '').replace(/-circles$/, '');
|
|
4760
|
+
const base = layerId.replace(/-g\d+$/, '').replace(/-circles$/, '').replace(/^(ix-vector-heads-.*)-n\d+$/, '$1');
|
|
4509
4761
|
return runtimes.find(r => {
|
|
4510
4762
|
if (base === `ix-bubbles-${r.name}`) return isSymbolChart(r.flags);
|
|
4511
4763
|
if (base === `ix-pie-${r.name}`) return r._isPieChart();
|
|
4764
|
+
if (base === `ix-bar-${r.name}`) return r._isBarChart();
|
|
4765
|
+
if (base === `ix-vector-${r.name}` || base === `ix-vector-heads-${r.name}`) return r._isVectorChart();
|
|
4512
4766
|
if (base === `ix-dot-${r.name}`) return r.flags.has('DOT');
|
|
4513
4767
|
if (base === `ix-choropleth-${r.name}`) return r.flags.has('CHOROPLETH');
|
|
4514
4768
|
if (base === `ix-features-${r.name}`) return r.flags.has('FEATURE') || r.flags.has('FEATURES');
|
|
@@ -4578,6 +4832,9 @@ in vec4 vPieLineColor;
|
|
|
4578
4832
|
// verified conventions' own priority (layer name, then style.name).
|
|
4579
4833
|
function findRuntime(themeId) {
|
|
4580
4834
|
const matches = runtimes.filter(r => r.name === themeId || (r.style && r.style.name === themeId) || (r.meta && r.meta.name === themeId));
|
|
4835
|
+
// a number (or "1", "2" ...) naming no theme is flat's theme index
|
|
4836
|
+
// (maptheme.js getTheme): 0-based, base layers counted
|
|
4837
|
+
if (!matches.length && isThemeIndex(themeId, runtimes.length)) return runtimes[Number(themeId)];
|
|
4581
4838
|
if (!matches.length) { console.warn(`[ixmaps-gl] no layer named "${themeId}"`); return undefined; }
|
|
4582
4839
|
return matches.find(r => !r.flags.has('FEATURE') && !r.flags.has('FEATURES')) || matches[0];
|
|
4583
4840
|
}
|
|
@@ -4665,7 +4922,7 @@ in vec4 vPieLineColor;
|
|
|
4665
4922
|
resize: () => { map.resize(); return engineApi; },
|
|
4666
4923
|
// broker calls in flight (themes whose data comes by name)
|
|
4667
4924
|
pendingLoads: () => pendingLoads,
|
|
4668
|
-
getZoom: () =>
|
|
4925
|
+
getZoom: () => flatViewZoomOf(map),
|
|
4669
4926
|
require: (url) => { loadRequiredScripts([url]); return engineApi; },
|
|
4670
4927
|
// flat's theme-level calls by theme id (style.name / meta.name, else
|
|
4671
4928
|
// the layer name): add(theme, flag) — "replace" swaps a theme of the
|
|
@@ -4860,7 +5117,13 @@ in vec4 vPieLineColor;
|
|
|
4860
5117
|
targets.forEach(rt => {
|
|
4861
5118
|
if (validation) validation.style(themeIdOf(rt), { [key]: value });
|
|
4862
5119
|
if (key === 'filter') {
|
|
4863
|
-
|
|
5120
|
+
const expr = action === 'remove' ? '' : value;
|
|
5121
|
+
if (rt._refilter) {
|
|
5122
|
+
// the theme was loaded with a filter: re-filter its full data
|
|
5123
|
+
rt._runtimeFilterExpr = '';
|
|
5124
|
+
rt._filterExpr = expr;
|
|
5125
|
+
rt.replaceFeatures(rt._refilter(expr));
|
|
5126
|
+
} else rt.setRuntimeFilter(expr);
|
|
4864
5127
|
changed = true;
|
|
4865
5128
|
return;
|
|
4866
5129
|
}
|
|
@@ -4938,7 +5201,7 @@ in vec4 vPieLineColor;
|
|
|
4938
5201
|
const z = isOpts ? latlonOrOpts.zoom : zoom;
|
|
4939
5202
|
if (c) {
|
|
4940
5203
|
const lngLat = Array.isArray(c) ? [c[1], c[0]] : [c.lng, c.lat];
|
|
4941
|
-
jumpLive(Object.assign({ center: lngLat }, z != null ? { zoom:
|
|
5204
|
+
jumpLive(Object.assign({ center: lngLat }, z != null ? { zoom: mapLibreZoomOfFlatView(map, Number(z), lngLat[1], lngLat[0]) } : {}));
|
|
4942
5205
|
}
|
|
4943
5206
|
return engineApi;
|
|
4944
5207
|
},
|
|
@@ -5198,6 +5461,7 @@ in vec4 vPieLineColor;
|
|
|
5198
5461
|
spec = pspec;
|
|
5199
5462
|
}
|
|
5200
5463
|
rt.replaceFeatures(namedThemeFeatures(spec, rows, rt._specBinding));
|
|
5464
|
+
rt._refilter = spec.filter ? expr => namedThemeFeatures(Object.assign({}, spec, { filter: expr }), rows, rt._specBinding) : null;
|
|
5201
5465
|
if (glDebug()) console.info(`[ixmaps-gl debug] data for ${themeIdOf(rt)}: ${rows.length} rows → ${rt.features.length} features`);
|
|
5202
5466
|
// the legend is built from the theme's classes: build it again
|
|
5203
5467
|
if (rt._legendPanel && rt._legendPanel.parentNode) rt._legendPanel.parentNode.removeChild(rt._legendPanel);
|
|
@@ -5273,6 +5537,7 @@ in vec4 vPieLineColor;
|
|
|
5273
5537
|
const filtered = applyWhereFilter(raw, spec.filter);
|
|
5274
5538
|
const fc = filtered.type === 'Table' ? joinTableFeatures(spec, filtered, runtimes) : filtered;
|
|
5275
5539
|
const rt = new LayerRuntime(spec, fc, builder._engineOptions);
|
|
5540
|
+
if (spec.filter) rt._refilter = refilterFrom(raw, spec, runtimes);
|
|
5276
5541
|
rt._definition = pdef; // for refreshTheme
|
|
5277
5542
|
rt._deferredLoad = deferred;
|
|
5278
5543
|
rt._named = named;
|
|
@@ -5345,6 +5610,12 @@ in vec4 vPieLineColor;
|
|
|
5345
5610
|
|
|
5346
5611
|
function applyProjectMap(m, f, report) {
|
|
5347
5612
|
const svg = String(m.map || '');
|
|
5613
|
+
// its view and map scale follow the project's map file, as on flat
|
|
5614
|
+
if (svg) {
|
|
5615
|
+
const svgProjection = flatSvgProjectionOf(svg);
|
|
5616
|
+
if (svgProjection) flatSvgProjectionByOptions.set(builder._engineOptions, svgProjection);
|
|
5617
|
+
else flatSvgProjectionByOptions.delete(builder._engineOptions);
|
|
5618
|
+
}
|
|
5348
5619
|
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
5620
|
if (typeof map.setProjection === 'function') {
|
|
5350
5621
|
try {
|
|
@@ -5365,8 +5636,8 @@ in vec4 vPieLineColor;
|
|
|
5365
5636
|
const c = m.center || {};
|
|
5366
5637
|
const lat = Number(c.lat), lng = Number(c.lng);
|
|
5367
5638
|
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:
|
|
5639
|
+
if (Number.isFinite(lat) && Number.isFinite(lng)) jumpLive(Object.assign({ center: [lng, lat] }, Number.isFinite(z) ? { zoom: mapLibreZoomOfFlatView(map, z, lat, lng) } : {}));
|
|
5640
|
+
else if (Number.isFinite(z)) jumpLive({ zoom: mapLibreZoomOfFlatView(map, z, map.getCenter().lat, map.getCenter().lng) });
|
|
5370
5641
|
}
|
|
5371
5642
|
// a project can't authorize its own scripts — trustedscripts is the page's
|
|
5372
5643
|
if (m.options && typeof m.options === 'object') {
|
|
@@ -6040,6 +6311,110 @@ in vec4 vPieLineColor;
|
|
|
6040
6311
|
const FLAT_ZOOM_OFFSET = 1;
|
|
6041
6312
|
const flatToMapLibreZoom = z => z - FLAT_ZOOM_OFFSET;
|
|
6042
6313
|
const mapLibreToFlatZoom = z => z + FLAT_ZOOM_OFFSET;
|
|
6314
|
+
|
|
6315
|
+
// flat's world SVG maps in a projection other than Mercator
|
|
6316
|
+
// (map:".../generic/equalearth.svg"), which this engine draws in Web
|
|
6317
|
+
// Mercator. Flat syncs such a map to its Leaflet view by fitting the
|
|
6318
|
+
// Leaflet bounds' SW/NE corners, projected, into the map
|
|
6319
|
+
// (htmlgui_sync.js setBoundsLatLon → mapscript2.js doSetMapToGeoBounds /
|
|
6320
|
+
// doZoomMapToArea): both axes fit (min) while the bounds span ≥ 180° of
|
|
6321
|
+
// longitude, else the x axis fits the width on its own (mapapi.js
|
|
6322
|
+
// doSetMapToGeoBounds drops fPreserveMapRatio) — so the same view zoom
|
|
6323
|
+
// frames these maps differently from Mercator (Equal Earth at 2 on
|
|
6324
|
+
// 1024×748: 1.55× the Mercator scale; at 4: 1.09×).
|
|
6325
|
+
// `project` is flat's own forward projection (mapscript.js
|
|
6326
|
+
// _LLtoEqualEarth, _LLtoWinkelTripel — Winkel's cos(50.467) in radians
|
|
6327
|
+
// included), in units of the earth radius.
|
|
6328
|
+
// `scaleConstant`: flat's map scale (nTrueMapScale · nZoomScale, the
|
|
6329
|
+
// scale its dynamic object scaling and gates use) × the screen pixels
|
|
6330
|
+
// per degree of longitude on the equator — constant per map file,
|
|
6331
|
+
// measured on flat (2026-10-09, view zooms 2-5): Equal Earth 170.1e6,
|
|
6332
|
+
// Winkel 195.45e6; for flat's Mercator map 442 913 385 · 256 / 360 =
|
|
6333
|
+
// 314.96e6 (FLAT_OBJECT_SCALE_CONSTANT). These SVG maps carry the
|
|
6334
|
+
// Mercator map's mapscale (177 165 354), so at the same on-screen size
|
|
6335
|
+
// flat calls their scale 0.54× (0.62×) the Mercator one, and its
|
|
6336
|
+
// symbols are drawn larger by that ratio^(1/dynamicScalePow). Framing
|
|
6337
|
+
// matches flat's pixels per degree of longitude at the view's center;
|
|
6338
|
+
// the scale gates (scaleDenominatorAt) still use the Mercator scale.
|
|
6339
|
+
const FLAT_MERCATOR_SCALE_CONSTANT = FLAT_OBJECT_SCALE_CONSTANT * 256 / 360;
|
|
6340
|
+
const FLAT_SVG_PROJECTIONS = {
|
|
6341
|
+
equalearth: {
|
|
6342
|
+
svg: 'maps/svg/maps/generic/equalearth.svg',
|
|
6343
|
+
scaleConstant: 170.1e6,
|
|
6344
|
+
project: (lat, lon) => {
|
|
6345
|
+
const M = Math.sqrt(3) / 2, A1 = 1.340264, A2 = -0.081106, A3 = 0.000893, A4 = 0.003796;
|
|
6346
|
+
const l = Math.asin(M * Math.sin(lat * Math.PI / 180)), l2 = l * l, l6 = l2 * l2 * l2;
|
|
6347
|
+
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))];
|
|
6348
|
+
}
|
|
6349
|
+
},
|
|
6350
|
+
winkel: {
|
|
6351
|
+
svg: 'maps/svg/maps/generic/winkel.svg',
|
|
6352
|
+
scaleConstant: 195.45e6,
|
|
6353
|
+
project: (lat, lon) => {
|
|
6354
|
+
const p = lat * Math.PI / 180, q = Math.max(-180, Math.min(180, lon)) * Math.PI / 360;
|
|
6355
|
+
const D = Math.acos(Math.cos(p) * Math.cos(q));
|
|
6356
|
+
let x1 = 0, y1 = 0;
|
|
6357
|
+
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; }
|
|
6358
|
+
return [0.5 * (x1 + 2 * q * Math.cos(50.467)), 0.5 * (y1 + p)];
|
|
6359
|
+
}
|
|
6360
|
+
}
|
|
6361
|
+
};
|
|
6362
|
+
// the flat SVG map projection of a map:/project map URL (generic file
|
|
6363
|
+
// name), null for Mercator and every map drawn as is (orthographic → globe)
|
|
6364
|
+
function flatSvgProjectionOf(svgUrl) {
|
|
6365
|
+
const m = /(?:^|\/)(equalearth|winkel)\.svgz?(?:[?#].*)?$/i.exec(String(svgUrl || ''));
|
|
6366
|
+
return m ? FLAT_SVG_PROJECTIONS[m[1].toLowerCase()] : null;
|
|
6367
|
+
}
|
|
6368
|
+
// the flat SVG projection a map's options object belongs to (set by the
|
|
6369
|
+
// map's init and loadProject; the runtimes only see the options object)
|
|
6370
|
+
const flatSvgProjectionByOptions = new WeakMap();
|
|
6371
|
+
// flat SVG map scale ÷ the Mercator one at the same on-screen size (1
|
|
6372
|
+
// without a flat SVG projection)
|
|
6373
|
+
function flatMapScaleRatio(mapOptions) {
|
|
6374
|
+
const proj = mapOptions && typeof mapOptions === 'object' ? flatSvgProjectionByOptions.get(mapOptions) : null;
|
|
6375
|
+
return proj ? proj.scaleConstant / FLAT_MERCATOR_SCALE_CONSTANT : 1;
|
|
6376
|
+
}
|
|
6377
|
+
// a flat view zoom → the MapLibre zoom showing what flat shows at it:
|
|
6378
|
+
// under a flat SVG projection the same screen pixels per degree of
|
|
6379
|
+
// longitude at the center latitude as flat's fitted map (see
|
|
6380
|
+
// FLAT_SVG_PROJECTIONS), else flatToMapLibreZoom
|
|
6381
|
+
function flatViewToMapLibreZoom(flatZoom, lat, lng, width, height, proj) {
|
|
6382
|
+
if (!proj || !(width > 0) || !(height > 0) || !Number.isFinite(lat) || !Number.isFinite(lng)) return flatToMapLibreZoom(flatZoom);
|
|
6383
|
+
// flat's Leaflet bounds (256 px tiles) at this zoom
|
|
6384
|
+
const [sw, ne] = viewBounds(lat, lng, flatToMapLibreZoom(flatZoom), width, height);
|
|
6385
|
+
const a = proj.project(sw.lat, sw.lng), b = proj.project(ne.lat, ne.lng);
|
|
6386
|
+
const kx = width / Math.abs(b[0] - a[0]), ky = height / Math.abs(b[1] - a[1]);
|
|
6387
|
+
const k = ne.lng - sw.lng < 180 ? kx : Math.min(kx, ky);
|
|
6388
|
+
const pxPerDegree = k * (proj.project(lat, lng + 0.5)[0] - proj.project(lat, lng - 0.5)[0]);
|
|
6389
|
+
return Math.log2(pxPerDegree * 360 / 512);
|
|
6390
|
+
}
|
|
6391
|
+
// the inverse: the flat view zoom of a MapLibre zoom (bisection — the
|
|
6392
|
+
// fit above is monotonic in the zoom)
|
|
6393
|
+
function mapLibreToFlatViewZoom(mapLibreZoom, lat, lng, width, height, proj) {
|
|
6394
|
+
if (!proj || !(width > 0) || !(height > 0) || !Number.isFinite(lat) || !Number.isFinite(lng)) return mapLibreToFlatZoom(mapLibreZoom);
|
|
6395
|
+
let lo = mapLibreToFlatZoom(mapLibreZoom) - 4, hi = mapLibreToFlatZoom(mapLibreZoom) + 4;
|
|
6396
|
+
for (let i = 0; i < 40; i++) {
|
|
6397
|
+
const mid = (lo + hi) / 2;
|
|
6398
|
+
if (flatViewToMapLibreZoom(mid, lat, lng, width, height, proj) < mapLibreZoom) lo = mid; else hi = mid;
|
|
6399
|
+
}
|
|
6400
|
+
return (lo + hi) / 2;
|
|
6401
|
+
}
|
|
6402
|
+
// a live MapLibre map's options object (its runtimes' mapOptions), for
|
|
6403
|
+
// the conversions below
|
|
6404
|
+
const engineOptionsByMap = new WeakMap();
|
|
6405
|
+
function mapSvgProjection(map) {
|
|
6406
|
+
const options = engineOptionsByMap.get(map);
|
|
6407
|
+
return (options && flatSvgProjectionByOptions.get(options)) || null;
|
|
6408
|
+
}
|
|
6409
|
+
// the flat view zoom a live map shows / the MapLibre zoom of a flat view
|
|
6410
|
+
function flatViewZoomOf(map, mapLibreZoom) {
|
|
6411
|
+
const c = map.getCenter(), el = map.getContainer();
|
|
6412
|
+
return mapLibreToFlatViewZoom(mapLibreZoom == null ? map.getZoom() : mapLibreZoom, c.lat, c.lng, el.clientWidth, el.clientHeight, mapSvgProjection(map));
|
|
6413
|
+
}
|
|
6414
|
+
function mapLibreZoomOfFlatView(map, flatZoom, lat, lng) {
|
|
6415
|
+
const el = map.getContainer();
|
|
6416
|
+
return flatViewToMapLibreZoom(flatZoom, lat, lng, el.clientWidth, el.clientHeight, mapSvgProjection(map));
|
|
6417
|
+
}
|
|
6043
6418
|
// map scale denominator at a MapLibre zoom (null → the default reference):
|
|
6044
6419
|
// flat's own map scale, the one its scale bar shows and every scale gate
|
|
6045
6420
|
// (featureupper, chartupper, boxupper, valueupper, aggregation brackets)
|
|
@@ -6064,9 +6439,20 @@ in vec4 vPieLineColor;
|
|
|
6064
6439
|
// (e.g. "fixed") means they don't — same pixel size at every zoom.
|
|
6065
6440
|
// the zoom at which flat's object scale equals normalSizeScale (symbols
|
|
6066
6441
|
// at their normal size) — see FLAT_OBJECT_SCALE_CONSTANT
|
|
6442
|
+
// (under a flat SVG projection, flat's scale at that zoom is
|
|
6443
|
+
// flatMapScaleRatio × the Mercator one)
|
|
6067
6444
|
function resolveZoomReference(mapOptions) {
|
|
6068
|
-
const scaleDenominator = parseFloat(mapOptions
|
|
6069
|
-
return flatToMapLibreZoom(Math.log2(FLAT_OBJECT_SCALE_CONSTANT / scaleDenominator));
|
|
6445
|
+
const scaleDenominator = parseFloat(normalSizeScaleOf(mapOptions)) || FLAT_DEFAULT_NORMAL_SIZE_SCALE;
|
|
6446
|
+
return flatToMapLibreZoom(Math.log2(FLAT_OBJECT_SCALE_CONSTANT * flatMapScaleRatio(mapOptions) / scaleDenominator));
|
|
6447
|
+
}
|
|
6448
|
+
// .options({normalSizeScale}) under any spelling: flat matches the key
|
|
6449
|
+
// case-insensitively (htmlgui.js, /normalSizeScale/i → setScaleParam;
|
|
6450
|
+
// the last matching key wins), e.g. "normalsizescale"
|
|
6451
|
+
function normalSizeScaleOf(mapOptions) {
|
|
6452
|
+
if (!mapOptions || typeof mapOptions !== 'object') return undefined;
|
|
6453
|
+
let value;
|
|
6454
|
+
for (const key of Object.keys(mapOptions)) if (/normalSizeScale/i.test(key)) value = mapOptions[key];
|
|
6455
|
+
return value;
|
|
6070
6456
|
}
|
|
6071
6457
|
|
|
6072
6458
|
// .options({dynamicScalePow}) — the REAL parameter name (confirmed in
|
|
@@ -6139,7 +6525,7 @@ in vec4 vPieLineColor;
|
|
|
6139
6525
|
// scale (resolveZoomReference); flat's real rule (maptheme.js 13408,
|
|
6140
6526
|
// 0.3 + 0.7 / ln(nZoom / dx)) is not ported yet
|
|
6141
6527
|
function autoOpacityZoomReference(mapOptions) {
|
|
6142
|
-
const scaleDenominator = parseFloat(mapOptions
|
|
6528
|
+
const scaleDenominator = parseFloat(normalSizeScaleOf(mapOptions));
|
|
6143
6529
|
if (!scaleDenominator) return flatToMapLibreZoom(DEFAULT_ZOOM_REFERENCE);
|
|
6144
6530
|
return flatToMapLibreZoom(Math.log2(WEBMERCATOR_SCALE_CONSTANT / scaleDenominator));
|
|
6145
6531
|
}
|
|
@@ -6474,6 +6860,12 @@ in vec4 vPieLineColor;
|
|
|
6474
6860
|
|
|
6475
6861
|
// the themes drawn by the symbol chart pipeline (_buildChartLayers): flat's
|
|
6476
6862
|
// BUBBLE/SQUARE/LABEL branch (maptheme.js 20647-20652), symbols, user charts
|
|
6863
|
+
// CHART|VECTOR / CHART|BEZIER — flat's flow lines (maptheme.js chartMap
|
|
6864
|
+
// 18148-18435, ahead of drawChart: they take precedence over PIE,
|
|
6865
|
+
// BUBBLE, SYMBOL ...; DOT's branch comes first)
|
|
6866
|
+
function isVectorChart(flags) {
|
|
6867
|
+
return flags.has('CHART') && (flags.has('VECTOR') || flags.has('BEZIER')) && !flags.has('DOT');
|
|
6868
|
+
}
|
|
6477
6869
|
function isSymbolChart(flags) {
|
|
6478
6870
|
return flags.has('CHART') && (flags.has('SYMBOL') || flags.has('USER') || flags.has('LABEL'));
|
|
6479
6871
|
}
|
|
@@ -6509,6 +6901,17 @@ in vec4 vPieLineColor;
|
|
|
6509
6901
|
// the item's radius, SYMBOL: the theme's normal radius)
|
|
6510
6902
|
// So "left" puts the chart's left edge o to the right of the point (and
|
|
6511
6903
|
// drops offsetx), "2left" one more h. Returns deck.gl's pixel offset.
|
|
6904
|
+
// flat's maxcharts (maptheme.js 16879-16885): chartMap sorts the charts
|
|
6905
|
+
// ascending (the biggest drawn last, on top) and skips the first
|
|
6906
|
+
// nToDraw − nMaxCharts, so only the biggest maxcharts are drawn — for
|
|
6907
|
+
// every chart type (symbols, pies, bars, VECTOR/BEZIER). In place, on the
|
|
6908
|
+
// already sorted list of charts to draw
|
|
6909
|
+
function applyFlatMaxCharts(list, style) {
|
|
6910
|
+
const n = Math.round(styleNum(style && style.maxcharts)) || 0;
|
|
6911
|
+
if (n > 0 && list.length > n) list.splice(0, list.length - n);
|
|
6912
|
+
return list;
|
|
6913
|
+
}
|
|
6914
|
+
|
|
6512
6915
|
function flatChartAlignOffset(style, g) {
|
|
6513
6916
|
const num = v => (isNaN(styleNum(v)) ? 0 : styleNum(v));
|
|
6514
6917
|
const sym = g.symbolScale || 1;
|
|
@@ -6551,6 +6954,53 @@ in vec4 vPieLineColor;
|
|
|
6551
6954
|
}
|
|
6552
6955
|
return [r, g, b].map(c => Math.min(255, Math.floor(c * f)));
|
|
6553
6956
|
}
|
|
6957
|
+
// A BUBBLE chart's look in flat (maptheme.js, the BUBBLE branch), for a
|
|
6958
|
+
// circle of screen radius r in the class color rgb, `unit` = flat's
|
|
6959
|
+
// normalX(1) on screen (the object zoom factor × style.scale):
|
|
6960
|
+
// - stroke (20740-20780, 20895): style.linecolor, else ChartColors.lowColor
|
|
6961
|
+
// (the color × 0.7, __maptheme_getChartColors) — none with NOLINES or
|
|
6962
|
+
// linecolor "none"
|
|
6963
|
+
// - width (20898-20903): linewidth (0.1) · unit · √(r / normal radius),
|
|
6964
|
+
// the normal radius 15 · unit; OUTLINE: unit · min(1, r / normal radius)
|
|
6965
|
+
// - fill-opacity fillopacity (1); stroke-opacity (21050-21056) 1 with
|
|
6966
|
+
// OUTLINE or a linecolor, else 1 below fillopacity 0.5, 0.3 above
|
|
6967
|
+
// (_buildLabelChartLayers spells the same rule out for CHART|LABEL's box.)
|
|
6968
|
+
function flatBubbleLook(rgb, r, unit, style, flags) {
|
|
6969
|
+
const fillOpacity = styleNum(style.fillopacity) || 1;
|
|
6970
|
+
const outline = flatFlag(flags, 'OUTLINE');
|
|
6971
|
+
const lineColor = style.linecolor ? styleLineColor(style.linecolor) : null;
|
|
6972
|
+
const stroke = lineColor ? (lineColor === 'none' ? null : hexOrNamedToRgb(lineColor))
|
|
6973
|
+
: flatFlag(flags, 'NOLINES') ? null : flatDerivateRgb(rgb, 0.7);
|
|
6974
|
+
const ratio = r / (NORMAL_RADIUS_PX * unit);
|
|
6975
|
+
const width = outline ? unit * Math.min(1, ratio) : unit * (styleNum(style.linewidth) || 0.1) * Math.sqrt(ratio);
|
|
6976
|
+
const strokeOpacity = outline || lineColor ? 1 : (fillOpacity < 0.5 ? 1 : 0.3);
|
|
6977
|
+
return { fillOpacity, stroke, strokeOpacity, width };
|
|
6978
|
+
}
|
|
6979
|
+
|
|
6980
|
+
// flatBubbleLook for a cached icon raster `size` canvas px wide, drawn 2r
|
|
6981
|
+
// screen px wide: the stroke width in canvas px, in quarter-octave steps
|
|
6982
|
+
// (≤ 19 % apart, at most the icon's radius) — every distinct width is
|
|
6983
|
+
// another icon in deck.gl's shared atlas (see ICON_ATLAS_RESET_AFTER), and
|
|
6984
|
+
// a continuous width would make one per bubble. It hardly depends on the
|
|
6985
|
+
// zoom: r and the width both scale with the object zoom factor. `stroke`
|
|
6986
|
+
// only when it is style.linecolor (a packed icon derives each part's own,
|
|
6987
|
+
// see _buildBubbleIcon), noStroke for NOLINES / linecolor "none";
|
|
6988
|
+
// outlineRatio (OUTLINE): r / normal radius, also stepped, as 1 from 1 up
|
|
6989
|
+
// (where flat's OUTLINE width stops growing).
|
|
6990
|
+
function bubbleIconLook(r, unit, style, flags, size) {
|
|
6991
|
+
const look = flatBubbleLook([0, 0, 0], r, unit, style, flags);
|
|
6992
|
+
const step = v => (v > 0 ? Math.round(Math.pow(2, Math.round(Math.log2(v) * 4) / 4) * 1000) / 1000 : 0);
|
|
6993
|
+
const out = {
|
|
6994
|
+
fillOpacity: look.fillOpacity,
|
|
6995
|
+
strokeOpacity: look.strokeOpacity,
|
|
6996
|
+
stroke: style.linecolor ? look.stroke : null,
|
|
6997
|
+
noStroke: !look.stroke,
|
|
6998
|
+
width: r > 0 ? Math.min(size / 2, step(look.width * size / (2 * r))) : 0,
|
|
6999
|
+
};
|
|
7000
|
+
if (flatFlag(flags, 'OUTLINE')) out.outlineRatio = Math.min(1, step(r / (NORMAL_RADIUS_PX * unit)));
|
|
7001
|
+
return out;
|
|
7002
|
+
}
|
|
7003
|
+
|
|
6554
7004
|
// ChartColors.textColor (colorscheme.js 104-116): darker on light colors
|
|
6555
7005
|
function flatChartTextRgb(rgb) {
|
|
6556
7006
|
return flatDerivateRgb(rgb, rgb[0] + rgb[1] + rgb[2] > 450 ? 0.6 : 3);
|
|
@@ -7269,6 +7719,412 @@ in vec4 vPieLineColor;
|
|
|
7269
7719
|
return [lngLat[0] + offset[0] * 360 / world, lat];
|
|
7270
7720
|
}
|
|
7271
7721
|
|
|
7722
|
+
// CHART|BAR / BARS: which flat draws as a bar chart — drawChart's BAR
|
|
7723
|
+
// branch (maptheme.js 23283) comes after USER, PIE, WAFFLE, BUBBLE,
|
|
7724
|
+
// SYMBOL and BUFFER; flat tests /BAR/ as a substring (BARS too)
|
|
7725
|
+
function isBarChart(flags) {
|
|
7726
|
+
if (!flags.has('CHART') || isVectorChart(flags) || flags.has('DOT')) return false;
|
|
7727
|
+
const t = flags.typeString != null ? flags.typeString : [...flags].join('|');
|
|
7728
|
+
return /BAR/.test(t) && !/\bUSER\b|PIE|WAFFLE|BUBBLE|SYMBOL|BUFFER/.test(t);
|
|
7729
|
+
}
|
|
7730
|
+
|
|
7731
|
+
// One item's bar chart as real ixmaps-flat draws it — maptheme.js
|
|
7732
|
+
// drawChart's BAR branch (23283-24425) — in flat's normal units (u:
|
|
7733
|
+
// normalX(1), the chart size is 30 u; px = u · the object zoom factor ×
|
|
7734
|
+
// style.scale), y down, relative to the item's position. `values`: the
|
|
7735
|
+
// item's parts (nValuesA, after aggregation); `t`: the type string,
|
|
7736
|
+
// tested as flat does, by substring. `o`:
|
|
7737
|
+
// themeMin / themeMax the theme's nMin / nMax (the items' sizes with a
|
|
7738
|
+
// size field or CATEGORICAL, else their parts)
|
|
7739
|
+
// maxSize, itemSize the theme's nMaxSize, the item's nSize (size field)
|
|
7740
|
+
// sizeField a size field is bound
|
|
7741
|
+
// normalSizeValue, sizePow, rangeScale, valueScale, units,
|
|
7742
|
+
// valueDecimals (explicit, else undefined), noBreaks, gridX, nClasses
|
|
7743
|
+
// (partsA.length), classMax (partsA[k].max), nColors, fillOpacity,
|
|
7744
|
+
// lineWidth, lineColor (linecolor, else undefined), fadeNegative,
|
|
7745
|
+
// fadeValuePow, hideValues (valueupper/lower), means / medians / devs
|
|
7746
|
+
// (per part, OFFSETMEAN / OFFSETMEDIAN / DEVIATION)
|
|
7747
|
+
// measure(text, font) the text's width at that font size (u)
|
|
7748
|
+
// Returns null when flat draws no chart (no size, or a single 0 part),
|
|
7749
|
+
// else { bars, texts, lines, extent: [x0, y0, x1, y1] } —
|
|
7750
|
+
// bars: { part (the value's index), cls (its color index), base
|
|
7751
|
+
// [x, y] (the middle of the shaft's foot), dir (0 up, 1 right,
|
|
7752
|
+
// 2 down, 3 left), w2 (half width), sw (shaft half width), len
|
|
7753
|
+
// (shaft), band, head, value, textValue, fill: { opacity },
|
|
7754
|
+
// line: { rgb: 'color' (the bar's) | null (lineColor or black),
|
|
7755
|
+
// width, opacity }, opacity (FADEIN / FADENEGATIVE) }
|
|
7756
|
+
// texts: { text, x, y (the baseline's start), angle (0 reading right,
|
|
7757
|
+
// 90 up, -90 down), font, opacity, color ('value' — flat's
|
|
7758
|
+
// #798697 values color, 'red', 'contrast' — ChartColors
|
|
7759
|
+
// .textColor of the bar's color —, or a CSS color), bg (null,
|
|
7760
|
+
// 'white' — #fefeff —, or 'bar' — the bar's color at 0.5),
|
|
7761
|
+
// cls, part }
|
|
7762
|
+
// lines: STACKED leader lines { x0, y0, x1, y1 }
|
|
7763
|
+
// Divergences (not drawn here): 3D, VOLUME, COLUMN's cylinders, DOTTED,
|
|
7764
|
+
// SMALLARROW (drawn as ARROW), TRENDLINE, the XAXIS/AXIS labels, the
|
|
7765
|
+
// mean/median marks, CLIP / MORPH frames, showparts / clipparts, ZOOM /
|
|
7766
|
+
// NORMSIZE / MENUSIZE, INVERT, DIAGLEFT / DIAGRIGHT — bars are drawn
|
|
7767
|
+
// plain instead (see BAR_INERT_FLAGS).
|
|
7768
|
+
function flatBarLayout(values, t, o) {
|
|
7769
|
+
t = String(t || '');
|
|
7770
|
+
const has = re => re.test(t);
|
|
7771
|
+
const n = values.length;
|
|
7772
|
+
if (!n) return null;
|
|
7773
|
+
const parts = values.map(v => (isFinite(v) ? v : 0));
|
|
7774
|
+
const sum = parts.reduce((a, v) => a + v, 0);
|
|
7775
|
+
const stacked = has(/STACKED/), pointer = has(/POINTER/), horz = has(/HOR/);
|
|
7776
|
+
const sizeFlag = has(/SIZE/) && !has(/NORMSIZE/);
|
|
7777
|
+
const sizePow = o.sizePow || 2;
|
|
7778
|
+
const CHART = 30;
|
|
7779
|
+
let nMax = Math.max(o.themeMax, Math.abs(o.themeMin));
|
|
7780
|
+
let nRange = o.themeMax - o.themeMin;
|
|
7781
|
+
let maxSize = o.maxSize;
|
|
7782
|
+
if (o.normalSizeValue) nRange = maxSize = o.normalSizeValue;
|
|
7783
|
+
// flat's getChartSize (no chart when 0) and its "a single 0" rule
|
|
7784
|
+
if (sizeFlag && !has(/NOSIZE/)) {
|
|
7785
|
+
const s = o.sizeField ? o.itemSize : Math.abs(sum);
|
|
7786
|
+
const ref = o.sizeField ? maxSize : nRange;
|
|
7787
|
+
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);
|
|
7788
|
+
if (isNaN(size) || size === 0) return null;
|
|
7789
|
+
}
|
|
7790
|
+
if (n === 1 && parts[0] === 0 && !has(/ZEROISVALUE|AUTOCOMPLETE|CATEGORICAL/)) return null;
|
|
7791
|
+
|
|
7792
|
+
// the sizer: the chart's size by the size field or by the values' sum
|
|
7793
|
+
let sizer = 1;
|
|
7794
|
+
if (sizeFlag && o.sizeField && o.itemSize != null) {
|
|
7795
|
+
sizer = has(/SIZELOG/) ? 1 / Math.max(1, Math.log(maxSize) * Math.log(o.itemSize))
|
|
7796
|
+
: Math.pow(o.itemSize, 1 / sizePow) / Math.pow(maxSize, 1 / sizePow);
|
|
7797
|
+
} else if (sizeFlag) {
|
|
7798
|
+
const s = pointer ? sum / n : sum;
|
|
7799
|
+
sizer = has(/SIZELOG/) ? 1 / Math.max(1, Math.log(nRange) * Math.log(Math.abs(s)))
|
|
7800
|
+
: Math.pow(Math.abs(s), 1 / sizePow) / Math.pow(nRange, 1 / sizePow);
|
|
7801
|
+
}
|
|
7802
|
+
if (sizer === 0 && sum !== 0) sizer = 1;
|
|
7803
|
+
const chartSize = CHART * sizer;
|
|
7804
|
+
|
|
7805
|
+
// bar width and value text size
|
|
7806
|
+
let W = chartSize / n * 0.66;
|
|
7807
|
+
if (stacked) {
|
|
7808
|
+
W = chartSize / 3;
|
|
7809
|
+
if (o.gridX && !has(/MULTI/)) W /= 2;
|
|
7810
|
+
}
|
|
7811
|
+
let f = W < 5 ? W * 4 / 5 : W;
|
|
7812
|
+
let textOpacity = 1;
|
|
7813
|
+
if (has(/COLUMN/)) W *= has(/THICK/) ? 2 : has(/THIN/) ? 1 : 1.5;
|
|
7814
|
+
if (has(/THIN/)) { W *= 0.6; f *= 0.75; }
|
|
7815
|
+
if (has(/THICK/)) { W *= 1.5; f *= 1.5; }
|
|
7816
|
+
if (stacked) f = 5;
|
|
7817
|
+
if (stacked && !has(/ZOOM/)) f *= sizer;
|
|
7818
|
+
const valueScale = o.valueScale || 1;
|
|
7819
|
+
f *= valueScale;
|
|
7820
|
+
|
|
7821
|
+
// the bar length per value unit (nStep)
|
|
7822
|
+
if (o.normalSizeValue && (!o.field100 || pointer)) nMax = o.normalSizeValue;
|
|
7823
|
+
let step0 = CHART / nMax;
|
|
7824
|
+
if (has(/OFFSETMEAN|OFFSETMEDIAN|DEVIATION/)) step0 = chartSize / 300;
|
|
7825
|
+
if (sizeFlag && has(/SEQUENCE/) && n > 1 && !o.sizeField && !o.field100) step0 = step0 * n / sizer;
|
|
7826
|
+
else if (sizeFlag && n > 1 && !has(/EXPAND/)) {
|
|
7827
|
+
if (o.field100 || pointer) step0 *= sizer;
|
|
7828
|
+
else step0 /= has(/SIZEP4/) ? sizer * sizer : sizer;
|
|
7829
|
+
}
|
|
7830
|
+
if (o.rangeScale) step0 *= o.rangeScale;
|
|
7831
|
+
if (has(/COMPRESSMAX/)) step0 /= 4;
|
|
7832
|
+
else if (has(/COMPRESSMORE/)) step0 /= 3;
|
|
7833
|
+
else if (has(/COMPRESS/)) step0 /= 2;
|
|
7834
|
+
else if (has(/EXPANDMAX/)) step0 *= 4;
|
|
7835
|
+
else if (has(/EXPANDMORE/)) step0 *= 3;
|
|
7836
|
+
else if (has(/EXPAND/)) step0 *= 2;
|
|
7837
|
+
if (pointer && !has(/NORMSIZE/)) step0 *= 1.2;
|
|
7838
|
+
|
|
7839
|
+
// the bar order: SORT descending (STACKED ascending), UP reversed
|
|
7840
|
+
let sorted = null;
|
|
7841
|
+
if (has(/\bSORT\b/)) {
|
|
7842
|
+
sorted = parts.map((v, i) => ({ i, v }));
|
|
7843
|
+
sorted.sort(stacked ? (a, b) => a.v - b.v : (a, b) => b.v - a.v);
|
|
7844
|
+
sorted = sorted.map(s => s.i);
|
|
7845
|
+
}
|
|
7846
|
+
const startI = has(/\bUP\b/) ? n - 1 : 0;
|
|
7847
|
+
const nC = has(/CENTER/) ? 2 : 1;
|
|
7848
|
+
const fmt = (v, d) => flatGroupedValue(v, d, o.noBreaks);
|
|
7849
|
+
const measure = o.measure || ((text, font) => text.length * font * 0.55);
|
|
7850
|
+
|
|
7851
|
+
const bars = [], texts = [], lines = [];
|
|
7852
|
+
let posX = 0, posY = 0, minNext = 0, barsDrawn = 0, step = step0;
|
|
7853
|
+
for (let i = 0; i < n; i++) {
|
|
7854
|
+
step = step0;
|
|
7855
|
+
let idx = Math.abs(startI - i);
|
|
7856
|
+
if (sorted) idx = sorted[idx];
|
|
7857
|
+
let v = parts[idx];
|
|
7858
|
+
let tv = v;
|
|
7859
|
+
if (has(/OFFSETMEAN|OFFSETMEDIAN/)) {
|
|
7860
|
+
const ref = has(/OFFSETMEDIAN/) ? (o.medians || [])[idx] : (o.means || [])[idx];
|
|
7861
|
+
v = 100 / ref * v - 100;
|
|
7862
|
+
v = isFinite(v) ? v : 0;
|
|
7863
|
+
tv = v;
|
|
7864
|
+
nMax = 100;
|
|
7865
|
+
} else if (has(/DEVIATION/)) {
|
|
7866
|
+
v = (v - (o.means || [])[idx]) / (o.devs || [])[idx];
|
|
7867
|
+
v = isFinite(v) ? v : 0;
|
|
7868
|
+
tv = v;
|
|
7869
|
+
v *= 100;
|
|
7870
|
+
}
|
|
7871
|
+
// the color: the part's, or (SEQUENCE, a single part) the class of
|
|
7872
|
+
// the value
|
|
7873
|
+
let cls = o.nColors ? (idx < o.nColors ? idx : idx % (o.gridX || 1000000)) : idx;
|
|
7874
|
+
if ((has(/SEQUENCE|CLIP/) || n === 1) && o.nColors >= 2 && o.classMax && o.classMax.length) {
|
|
7875
|
+
cls = o.classMax.length - 1;
|
|
7876
|
+
for (let k = 0; k < o.classMax.length; k++) if (v < o.classMax[k]) { cls = k; break; }
|
|
7877
|
+
}
|
|
7878
|
+
if (has(/LEFT/) && has(/HORZ/)) v = -v;
|
|
7879
|
+
if (v < 0 && !stacked) { posY = -v * step; v = -v; }
|
|
7880
|
+
if (pointer && sizeFlag) {
|
|
7881
|
+
// flat's sized pointer: its width by the value (the WIDTH variant
|
|
7882
|
+
// by value100 is not ported)
|
|
7883
|
+
W = chartSize * Math.pow(Math.abs(v / nMax), 1 / 3);
|
|
7884
|
+
f = W * 0.6;
|
|
7885
|
+
if (!has(/ZOOM|XAXIS/)) {
|
|
7886
|
+
textOpacity = Math.pow(Math.abs(v), 1 / 2) / Math.pow(o.themeMax, 1 / 2);
|
|
7887
|
+
if (o.fadeValuePow) textOpacity = Math.pow(Math.abs(v), o.fadeValuePow) / Math.pow(nMax, o.fadeValuePow);
|
|
7888
|
+
}
|
|
7889
|
+
}
|
|
7890
|
+
if (has(/DTEXT/) && !has(/ZOOM/)) {
|
|
7891
|
+
f = (chartSize * 2 / 3) * Math.pow(Math.abs(v / nMax), 1 / 2) * 0.75;
|
|
7892
|
+
textOpacity = Math.pow(Math.abs(v), 1 / 3) / Math.pow(nMax, 1 / 3);
|
|
7893
|
+
}
|
|
7894
|
+
v = isFinite(v) ? v : 0;
|
|
7895
|
+
if (v === 0 && has(/NOZERO/)) { posY = 0; continue; }
|
|
7896
|
+
if (tv < 0 && has(/NONEGATIVE/)) { posY = 0; continue; }
|
|
7897
|
+
if (tv > 0 && has(/ONLYNEGATIVE/)) { posY = 0; continue; }
|
|
7898
|
+
|
|
7899
|
+
const oy = posY;
|
|
7900
|
+
const bar = { part: idx, cls, value: v, textValue: tv, dir: 0, w2: W / 2, sw: W / 2, band: 0, head: 0, len: 0,
|
|
7901
|
+
fill: { opacity: o.fillOpacity || 1 }, line: null, opacity: 1 };
|
|
7902
|
+
let drawn = true;
|
|
7903
|
+
if (pointer && v) {
|
|
7904
|
+
if (has(/OFFSETMEAN|OFFSETMEDIAN|DEVIATION/) && Math.abs(v * step) > 50) {
|
|
7905
|
+
const h = Math.abs(v * step);
|
|
7906
|
+
step = (50 + (h - 50) / 10 + h % 50) / Math.abs(v);
|
|
7907
|
+
}
|
|
7908
|
+
const H = v * step;
|
|
7909
|
+
const arrow = has(/ARROW/);
|
|
7910
|
+
const style = { rgb: null, width: o.lineWidth ? o.lineWidth * Math.sqrt(sizer || 1) : 0.05, opacity: 0.5 };
|
|
7911
|
+
if (oy <= 0) {
|
|
7912
|
+
// positive: the shaft from the base up, a band as wide as the bar,
|
|
7913
|
+
// the head on it
|
|
7914
|
+
Object.assign(bar, arrow
|
|
7915
|
+
? { 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 }
|
|
7916
|
+
: { base: [posX + W / 2, oy - H / nC + H], len: H, sw: W * 5 / 14, band: W / 15, head: W / 2 });
|
|
7917
|
+
bar.line = style;
|
|
7918
|
+
} else if (!has(/NONEGATIVE/)) {
|
|
7919
|
+
// negative: from the zero line down, the head below
|
|
7920
|
+
Object.assign(bar, arrow
|
|
7921
|
+
? { base: [posX + W / 2, oy - H / nC], len: H, sw: W / 6, head: W / 2, dir: 2 }
|
|
7922
|
+
: { base: [posX + W / 2, oy - H / nC], len: H, sw: W * 5 / 14, head: W / 4, dir: 2 });
|
|
7923
|
+
bar.line = style;
|
|
7924
|
+
if (!has(/LEFT/)) {
|
|
7925
|
+
const fadeNeg = o.fadeNegative || 0.1;
|
|
7926
|
+
if (has(/ZOOM|NORMSIZE/)) {
|
|
7927
|
+
bar.fill.opacity = 0.5;
|
|
7928
|
+
bar.line = { rgb: 'color', width: 0.25, opacity: 1 };
|
|
7929
|
+
} else if (o.nClasses > 2) {
|
|
7930
|
+
bar.fill.opacity = fadeNeg;
|
|
7931
|
+
bar.line = { rgb: 'low', width: o.lineWidth ? o.lineWidth * 0.25 * sizer : 0.25, opacity: 0.8 };
|
|
7932
|
+
} else {
|
|
7933
|
+
bar.fill.opacity = fadeNeg;
|
|
7934
|
+
bar.line = { rgb: 'high', width: o.lineWidth ? o.lineWidth * Math.sqrt(sizer || 1) : 0.05, opacity: 1 };
|
|
7935
|
+
}
|
|
7936
|
+
}
|
|
7937
|
+
} else drawn = false;
|
|
7938
|
+
} else {
|
|
7939
|
+
// plain 2D bar (DOTTED, 3D, COLUMN are drawn as this too)
|
|
7940
|
+
if (!has(/NOZERO/)) v = v || 0.00001;
|
|
7941
|
+
const H = v * step;
|
|
7942
|
+
bar.value = v;
|
|
7943
|
+
if (H < 0) drawn = false;
|
|
7944
|
+
bar.base = [posX + W / 2, oy - H / nC + H];
|
|
7945
|
+
bar.len = H;
|
|
7946
|
+
if (oy <= 0 || has(/LEFT/)) {
|
|
7947
|
+
bar.line = { rgb: null, width: o.lineWidth ? o.lineWidth * 0.25 * sizer : 0.2, opacity: o.lineWidth ? 1 : 0.1 };
|
|
7948
|
+
} else {
|
|
7949
|
+
bar.fill.opacity = 0.3;
|
|
7950
|
+
bar.line = { rgb: 'color', width: 0.25, opacity: 1 };
|
|
7951
|
+
}
|
|
7952
|
+
}
|
|
7953
|
+
if (has(/FADENEGATIVE/) && tv < 0) bar.opacity = 0.5;
|
|
7954
|
+
if (has(/FADEIN/)) {
|
|
7955
|
+
if (n === 2 && i === 0) bar.opacity = 0.6;
|
|
7956
|
+
else if (!has(/NORMSIZE/)) {
|
|
7957
|
+
const p = has(/FADEINP4/) ? 10 : 3;
|
|
7958
|
+
bar.opacity = 0.1 + 0.9 * Math.pow(i + 1, p) / Math.pow(n, p);
|
|
7959
|
+
}
|
|
7960
|
+
}
|
|
7961
|
+
if (drawn) bars.push(bar);
|
|
7962
|
+
|
|
7963
|
+
// the value text
|
|
7964
|
+
const H = v * step;
|
|
7965
|
+
if (has(/VALUES/) && !(oy > 0 && has(/NONEGATIVE/)) && !o.hideValues
|
|
7966
|
+
&& f > 0 && !(o.gridX && o.nClasses / o.gridX > 2)) {
|
|
7967
|
+
let text = fmt(tv, o.valueDecimals != null ? o.valueDecimals : (o.themeMax <= 1 ? 1 : 0));
|
|
7968
|
+
if (pointer && tv > 0 && (o.themeMin < 0 || has(/OFFSETMEAN|OFFSETMEDIAN|\bSIGN\b/))) text = '+' + text;
|
|
7969
|
+
if (!has(/VOLUME/)) text += o.units ? ' ' + o.units + ' ' : '';
|
|
7970
|
+
if (stacked && tv !== 0 && !o.field100 && !o.gridX) text += ' (' + Math.round(tv / sum * 100) + '%)';
|
|
7971
|
+
if (stacked) f = (has(/ZOOM/) ? 18 : 5) * valueScale;
|
|
7972
|
+
// SVG collapses the blanks
|
|
7973
|
+
text = text.replace(/\s+/g, ' ').trim();
|
|
7974
|
+
const red = parseFloat(text) < 0;
|
|
7975
|
+
// createTextLabel's box: not with HORZ; with ZOOM … AXIS only
|
|
7976
|
+
// colored (VALUEBACKGROUND / CTEXT); colored for a positive value
|
|
7977
|
+
const colored = tv > 0 && has(/CTEXT|VALUEBACKGROUND/);
|
|
7978
|
+
let bgOn = true;
|
|
7979
|
+
if (has(/ZOOM|MENUSIZE|NORMSIZE|INFOSIZE|AXIS|SELECTION/)) bgOn = colored;
|
|
7980
|
+
if (has(/VALUEBACKGROUND/)) bgOn = true;
|
|
7981
|
+
if (horz) bgOn = false;
|
|
7982
|
+
const bg = bgOn ? (colored ? 'bar' : 'white') : null;
|
|
7983
|
+
// the text color: negative red (valuecolor), else textcolor; on the
|
|
7984
|
+
// bar's color its text color, else flat's values color
|
|
7985
|
+
const color = red ? (o.valueColor || 'red') : (o.textColor || (bg === 'bar' ? 'contrast' : 'value'));
|
|
7986
|
+
const textW = measure(text, f);
|
|
7987
|
+
const tLen = textW + (bgOn ? 0.7 * f : 0);
|
|
7988
|
+
const ptY = W / 2 + f * 0.15;
|
|
7989
|
+
const outside = (!has(/DOINLINETEXT/) || has(/ZOOM/) || has(/NORMSIZE/)) && has(/VALUE/) && !(has(/VOLUME/) && !has(/ZOOM/))
|
|
7990
|
+
&& (has(/THIN|NOINLINETEXT|DTEXT|SIZE|STACKED|NORMSIZE|ZOOM/) || tLen + 1 > H / nC);
|
|
7991
|
+
const base = { text, cls, part: idx, opacity: textOpacity, color, bg };
|
|
7992
|
+
if (outside && stacked) {
|
|
7993
|
+
if (!(v === 0 && !has(/ZEROISVALUE/)) && !(o.gridX && n / o.gridX > 2)) {
|
|
7994
|
+
const topDx = H / 2;
|
|
7995
|
+
minNext = Math.max(0, minNext + (f * 0.75 - topDx));
|
|
7996
|
+
const textPosX = minNext + topDx - f / 3;
|
|
7997
|
+
let indent = 0.5 * sizer;
|
|
7998
|
+
let ptYs = ptY;
|
|
7999
|
+
let x1 = posX + ptY + W / 3, x2 = posX + ptY + W * 7 / 8;
|
|
8000
|
+
const y1 = -topDx, y2 = -textPosX - f / 3;
|
|
8001
|
+
if (o.gridX && barsDrawn < o.gridX) {
|
|
8002
|
+
ptYs -= W * 2 + tLen;
|
|
8003
|
+
const tmp = x2;
|
|
8004
|
+
x2 = x1 - W * 1.4;
|
|
8005
|
+
x1 = tmp - W * 1.4;
|
|
8006
|
+
indent = -indent;
|
|
8007
|
+
}
|
|
8008
|
+
texts.push(Object.assign(base, { x: posX + ptYs + W + indent, y: oy - textPosX, angle: 0, font: f }));
|
|
8009
|
+
[[x1, y1, x1 + indent, y1], [x1 + indent, y1, x2, y2], [x2, y2, x2 + indent, y2]]
|
|
8010
|
+
.forEach(([a, b, c, d]) => lines.push({ x0: a, y0: oy + b, x1: c, y1: oy + d }));
|
|
8011
|
+
minNext = Math.max(0, minNext + (f * 2 / 3 - (H - topDx)));
|
|
8012
|
+
}
|
|
8013
|
+
} else if (outside) {
|
|
8014
|
+
const lift = pointer && tv > 0 ? W * 0.5 : 0;
|
|
8015
|
+
if (n === 1 && !has(/HORZ/) && !has(/VOLUME/)) {
|
|
8016
|
+
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,
|
|
8017
|
+
opacity: tv < 0 ? textOpacity * 0.75 : textOpacity }));
|
|
8018
|
+
} else {
|
|
8019
|
+
// rotate(270) scale(0.9): reading up, 0.9 font
|
|
8020
|
+
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 }));
|
|
8021
|
+
}
|
|
8022
|
+
} else {
|
|
8023
|
+
// inside the bar, in its text color, shrunk to fit
|
|
8024
|
+
const s = Math.min(1, H / tLen);
|
|
8025
|
+
if (s >= 0.33) {
|
|
8026
|
+
let ptX = -4;
|
|
8027
|
+
if (tv > 0) ptX += Math.max(1, H - (tLen + 3) * s);
|
|
8028
|
+
const dY = 0.5 / s;
|
|
8029
|
+
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 }));
|
|
8030
|
+
}
|
|
8031
|
+
minNext = Math.max(0, minNext - H);
|
|
8032
|
+
}
|
|
8033
|
+
}
|
|
8034
|
+
|
|
8035
|
+
if (stacked) {
|
|
8036
|
+
if (!has(/CENTER/)) posY -= v * step;
|
|
8037
|
+
} else {
|
|
8038
|
+
posY = 0;
|
|
8039
|
+
posX += W;
|
|
8040
|
+
if (has(/SPACED/)) posX += W / 5;
|
|
8041
|
+
else if (!has(/\bUP\b/)) posX += 0.05;
|
|
8042
|
+
}
|
|
8043
|
+
barsDrawn++;
|
|
8044
|
+
if (stacked && !has(/MULTI/) && o.gridX && barsDrawn % o.gridX === 0) {
|
|
8045
|
+
posY = 0;
|
|
8046
|
+
posX += W + W / 20;
|
|
8047
|
+
}
|
|
8048
|
+
}
|
|
8049
|
+
|
|
8050
|
+
// the chart's position: HOR(Z) turned by 90° (bars to the right, the
|
|
8051
|
+
// first on top, the last centered on the position's height), else
|
|
8052
|
+
// the bars side by side centered on it (STACKED: the first column)
|
|
8053
|
+
const P = posX;
|
|
8054
|
+
let map, dirMap, angleMap;
|
|
8055
|
+
if (horz) {
|
|
8056
|
+
const dy = W / 2 + (has(/3D/) ? W / 3 : 0) - P;
|
|
8057
|
+
map = (x, y) => [-y, x + dy];
|
|
8058
|
+
dirMap = d => (d + 1) % 4;
|
|
8059
|
+
angleMap = a => a - 90;
|
|
8060
|
+
} else {
|
|
8061
|
+
const dx = stacked ? W / 2 : W * barsDrawn / 2;
|
|
8062
|
+
map = (x, y) => [x - dx, y];
|
|
8063
|
+
dirMap = d => d;
|
|
8064
|
+
angleMap = a => a;
|
|
8065
|
+
}
|
|
8066
|
+
const ext = [Infinity, Infinity, -Infinity, -Infinity];
|
|
8067
|
+
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; };
|
|
8068
|
+
for (const b of bars) {
|
|
8069
|
+
b.base = map(b.base[0], b.base[1]);
|
|
8070
|
+
b.dir = dirMap(b.dir);
|
|
8071
|
+
const d = [[0, -1], [1, 0], [0, 1], [-1, 0]][b.dir], a = [-d[1], d[0]];
|
|
8072
|
+
const L = b.len + b.band + b.head;
|
|
8073
|
+
[[-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));
|
|
8074
|
+
}
|
|
8075
|
+
for (const tx of texts) {
|
|
8076
|
+
[tx.x, tx.y] = map(tx.x, tx.y);
|
|
8077
|
+
tx.angle = angleMap(tx.angle);
|
|
8078
|
+
// arial: ascent 0.905, descent 0.212 of the font
|
|
8079
|
+
const w = measure(tx.text, tx.font) + (tx.bg ? 0.7 * tx.font : 0);
|
|
8080
|
+
const r = tx.angle * Math.PI / 180, c = Math.cos(r), s = -Math.sin(r);
|
|
8081
|
+
const xo = tx.bg ? -0.3 * tx.font : 0;
|
|
8082
|
+
[[xo, -0.905 * tx.font], [xo + w, -0.905 * tx.font], [xo, 0.212 * tx.font], [xo + w, 0.212 * tx.font]]
|
|
8083
|
+
.forEach(([u, v]) => grow(tx.x + u * c - v * s, tx.y + u * s + v * c));
|
|
8084
|
+
}
|
|
8085
|
+
for (const l of lines) {
|
|
8086
|
+
[l.x0, l.y0] = map(l.x0, l.y0);
|
|
8087
|
+
[l.x1, l.y1] = map(l.x1, l.y1);
|
|
8088
|
+
grow(l.x0, l.y0); grow(l.x1, l.y1);
|
|
8089
|
+
}
|
|
8090
|
+
return { bars, texts, lines, extent: ext[0] <= ext[2] ? ext : [0, 0, 0, 0], width: W, sizer };
|
|
8091
|
+
}
|
|
8092
|
+
|
|
8093
|
+
// flat's BOX and TITLE around a chart (maptheme.js 18758-18905), in the
|
|
8094
|
+
// chart's units: `extent` the chart's [x0, y0, x1, y1]; o: { margin
|
|
8095
|
+
// (boxmargin, 2), titleFont, title (text or ''), bottomTitle, alignRight,
|
|
8096
|
+
// measure }. The margin is min(2 m, m · width / 30); the title's first
|
|
8097
|
+
// baseline 0.7 font above the chart (BOTTOMTITLE: one font below), its
|
|
8098
|
+
// lines wrapped to the chart's width and stacked upward; the box encloses
|
|
8099
|
+
// chart and title, its top trimmed by 0.1 font when titled. Returns
|
|
8100
|
+
// { rect: [x0, y0, x1, y1], title: { lines, x, anchor, baseline, font } | null }
|
|
8101
|
+
function flatChartBox(extent, o) {
|
|
8102
|
+
const e = extent.slice();
|
|
8103
|
+
const w = e[2] - e[0];
|
|
8104
|
+
const margin = Math.min(2 * o.margin, o.margin * w / 30);
|
|
8105
|
+
let title = null;
|
|
8106
|
+
if (o.title) {
|
|
8107
|
+
const f = o.titleFont;
|
|
8108
|
+
const measure = o.measure || ((text, font) => text.length * font * 0.55);
|
|
8109
|
+
const lines = [];
|
|
8110
|
+
for (const word of String(o.title).split(/\s+/).filter(Boolean)) {
|
|
8111
|
+
const last = lines.length ? lines[lines.length - 1] + ' ' + word : null;
|
|
8112
|
+
if (last !== null && measure(last, f) <= w) lines[lines.length - 1] = last;
|
|
8113
|
+
else lines.push(word);
|
|
8114
|
+
}
|
|
8115
|
+
const width = Math.max(...lines.map(l => measure(l, f)));
|
|
8116
|
+
const baseline = o.bottomTitle ? e[3] + f : e[1] - 0.7 * f - (lines.length - 1) * f;
|
|
8117
|
+
const x = o.alignRight ? e[2] : e[0];
|
|
8118
|
+
const x0 = o.alignRight ? x - width : x;
|
|
8119
|
+
title = { lines, x, anchor: o.alignRight ? 'end' : 'start', baseline, font: f };
|
|
8120
|
+
e[0] = Math.min(e[0], x0);
|
|
8121
|
+
e[1] = Math.min(e[1], baseline - 0.905 * f) + 0.1 * f;
|
|
8122
|
+
e[2] = Math.max(e[2], x0 + width);
|
|
8123
|
+
e[3] = Math.max(e[3], baseline + (lines.length - 1) * f + 0.212 * f);
|
|
8124
|
+
}
|
|
8125
|
+
return { rect: [e[0] - margin, e[1] - margin, e[2] + margin, e[3] + margin], title, width: w };
|
|
8126
|
+
}
|
|
8127
|
+
|
|
7272
8128
|
// CHART|PIE / DONUT: one pie's slices as real ixmaps-flat lays them out —
|
|
7273
8129
|
// maptheme.js drawChart's PIE branch (19939-20330) and piechart.js
|
|
7274
8130
|
// DonutChart.realize (181-510). `values`: the item's part values (field
|
|
@@ -7456,6 +8312,322 @@ in vec4 vPieLineColor;
|
|
|
7456
8312
|
return out;
|
|
7457
8313
|
}
|
|
7458
8314
|
|
|
8315
|
+
// flat's value rules of a theme (MapTheme constructor, maptheme.js
|
|
8316
|
+
// 7740-7830), by substring of the type string as flat tests them:
|
|
8317
|
+
// nullIsValue (0 is a value: ZEROISNOTVALUE no, ZEROISVALUE yes, a CHART
|
|
8318
|
+
// with CATEGORICAL or several value fields always), negativeOk
|
|
8319
|
+
// (NEGATIVEISNOTVALUE no, NEGATIVEISVALUE yes, CATEGORICAL without
|
|
8320
|
+
// AGGREGATE no), undefinedOk (UNDEFINEDISNOTVALUE no), zeroExcludes
|
|
8321
|
+
// (ZEROISNOTVALUE: a 0 is left out of an aggregation's means)
|
|
8322
|
+
function flatValueRules(t, nFields) {
|
|
8323
|
+
t = String(t || '');
|
|
8324
|
+
let nullIsValue = true, negativeOk = true, undefinedOk = true;
|
|
8325
|
+
if (/ZEROISNOTVALUE/.test(t)) nullIsValue = false;
|
|
8326
|
+
if (/ZEROISVALUE/.test(t)) nullIsValue = true;
|
|
8327
|
+
if (/NEGATIVEISNOTVALUE/.test(t)) negativeOk = false;
|
|
8328
|
+
if (/NEGATIVEISVALUE/.test(t)) negativeOk = true;
|
|
8329
|
+
if (/UNDEFINEDISVALUE/.test(t)) undefinedOk = true;
|
|
8330
|
+
if (/UNDEFINEDISNOTVALUE/.test(t)) undefinedOk = false;
|
|
8331
|
+
if (/CHART/.test(t) && (/CATEGORICAL/.test(t) || nFields > 1)) nullIsValue = true;
|
|
8332
|
+
if (/CATEGORICAL/.test(t) && !/AGGREGATE/.test(t)) negativeOk = false;
|
|
8333
|
+
return { nullIsValue, negativeOk, undefinedOk, zeroExcludes: /ZEROISNOTVALUE/.test(t) };
|
|
8334
|
+
}
|
|
8335
|
+
|
|
8336
|
+
// An AGGREGATE or CATEGORICAL pie: one record's contribution to its item,
|
|
8337
|
+
// as flat's loadAndAggregateValuesOfTheme reads it (maptheme.js
|
|
8338
|
+
// 10465-10571, which every AGGREGATE theme goes through). `o`:
|
|
8339
|
+
// { rules (flatValueRules), sizeField, cat (CATEGORICAL: the record's
|
|
8340
|
+
// category, 0-based, -1 when it is none of them), fields (otherwise:
|
|
8341
|
+
// the value fields) }
|
|
8342
|
+
// Returns null when flat leaves the record out, else
|
|
8343
|
+
// { cat, size } (CATEGORICAL) or { parts, zeroNot, size }
|
|
8344
|
+
// - size: the size field as a number (1 without one); a record whose
|
|
8345
|
+
// size is no number, 0 (unless 0 is a value) or negative (unless
|
|
8346
|
+
// negatives are values) is left out
|
|
8347
|
+
// - CATEGORICAL: a value that is none of the categories (not in
|
|
8348
|
+
// style.values) is part -1 — no slice, but its size still counts in
|
|
8349
|
+
// the item's size (flat: nValuesA[-1]); left out only with
|
|
8350
|
+
// UNDEFINEDISNOTVALUE
|
|
8351
|
+
// - value fields: a value that is no number is 0 (UNDEFINEDISNOTVALUE:
|
|
8352
|
+
// the record is left out); a 0 is left out of the item's MEAN with
|
|
8353
|
+
// ZEROISNOTVALUE (zeroNot), else the record when 0 is no value; a
|
|
8354
|
+
// negative value when negatives are no values
|
|
8355
|
+
function flatPieRecord(raw, o) {
|
|
8356
|
+
const r = o.rules;
|
|
8357
|
+
const size = o.sizeField ? flatScanValue(raw[o.sizeField]) : 1;
|
|
8358
|
+
if (isNaN(size) || (size === 0 && !r.nullIsValue) || (size < 0 && !r.negativeOk)) return null;
|
|
8359
|
+
if (o.cat !== undefined) {
|
|
8360
|
+
if (o.cat < 0 && !r.undefinedOk) return null;
|
|
8361
|
+
return { cat: o.cat, size };
|
|
8362
|
+
}
|
|
8363
|
+
const parts = [], zeroNot = [];
|
|
8364
|
+
for (let k = 0; k < o.fields.length; k++) {
|
|
8365
|
+
let v = parseFloat(raw[o.fields[k]]);
|
|
8366
|
+
zeroNot[k] = false;
|
|
8367
|
+
if (isNaN(v)) {
|
|
8368
|
+
if (!r.undefinedOk) return null;
|
|
8369
|
+
v = 0;
|
|
8370
|
+
}
|
|
8371
|
+
if (v === 0) {
|
|
8372
|
+
if (r.zeroExcludes) { parts[k] = 0; zeroNot[k] = true; continue; }
|
|
8373
|
+
if (!r.nullIsValue) return null;
|
|
8374
|
+
}
|
|
8375
|
+
if (v < 0 && !r.negativeOk) return null;
|
|
8376
|
+
parts[k] = v;
|
|
8377
|
+
}
|
|
8378
|
+
return { parts, zeroNot, size };
|
|
8379
|
+
}
|
|
8380
|
+
|
|
8381
|
+
// Adds a record's flatPieRecord to its item (maptheme.js 10779-10943) —
|
|
8382
|
+
// `item` null for the first record of the item. Returns the item
|
|
8383
|
+
// { parts (sparse for CATEGORICAL: the category sums), counts (the
|
|
8384
|
+
// records per part, for MEAN), size (the item's size, flat's nSize),
|
|
8385
|
+
// n (its record count, flat's nCount) }
|
|
8386
|
+
// SUM (default): the parts and the |sizes| summed; MAX / MIN: their
|
|
8387
|
+
// maximum / minimum (a part missing so far counts 0, as in flat; the
|
|
8388
|
+
// record count stays 1); FIRST: the first record only; LAST: the last.
|
|
8389
|
+
// A CATEGORICAL record of no category (-1) adds its size only.
|
|
8390
|
+
function flatPieAccumulate(item, rec, flags) {
|
|
8391
|
+
const max = flags.has('MAX'), min = flags.has('MIN');
|
|
8392
|
+
if (item && flags.has('FIRST')) return item;
|
|
8393
|
+
if (!item || flags.has('LAST')) {
|
|
8394
|
+
if (rec.cat !== undefined) {
|
|
8395
|
+
const parts = [], counts = [];
|
|
8396
|
+
if (rec.cat >= 0) { parts[rec.cat] = rec.size; counts[rec.cat] = 1; }
|
|
8397
|
+
return { parts, counts, size: Math.abs(rec.size), n: 1 };
|
|
8398
|
+
}
|
|
8399
|
+
return { parts: rec.parts.slice(), counts: rec.zeroNot.map(z => (z ? 0 : 1)), size: Math.abs(rec.size), n: 1 };
|
|
8400
|
+
}
|
|
8401
|
+
const comb = (a, v) => (max ? Math.max(a || 0, v) : min ? Math.min(a || 0, v) : (a || 0) + v);
|
|
8402
|
+
if (rec.cat !== undefined) {
|
|
8403
|
+
if (rec.cat >= 0) {
|
|
8404
|
+
item.parts[rec.cat] = comb(item.parts[rec.cat], rec.size);
|
|
8405
|
+
item.counts[rec.cat] = (item.counts[rec.cat] || 0) + 1;
|
|
8406
|
+
}
|
|
8407
|
+
} else {
|
|
8408
|
+
for (let k = 0; k < rec.parts.length; k++) {
|
|
8409
|
+
item.parts[k] = comb(item.parts[k], rec.parts[k] || 0);
|
|
8410
|
+
if (!rec.zeroNot[k]) item.counts[k] = (item.counts[k] || 0) + 1;
|
|
8411
|
+
}
|
|
8412
|
+
}
|
|
8413
|
+
if (max) item.size = Math.max(item.size, rec.size);
|
|
8414
|
+
else if (min) item.size = Math.min(item.size, rec.size);
|
|
8415
|
+
else { item.size += Math.abs(rec.size); item.n++; }
|
|
8416
|
+
return item;
|
|
8417
|
+
}
|
|
8418
|
+
|
|
8419
|
+
// An aggregated pie item's parts and size after flat's post-aggregation
|
|
8420
|
+
// steps (maptheme.js 11056-11137): MEAN — a part divided by its record
|
|
8421
|
+
// count (CATEGORICAL: the category's records; value fields: the item's
|
|
8422
|
+
// records, or with ZEROISNOTVALUE the records with a value there), the
|
|
8423
|
+
// size by the item's record count unless SIZESUM; AUTO100 — the parts in
|
|
8424
|
+
// % of their sum. `nParts`: the number of parts (CATEGORICAL with
|
|
8425
|
+
// style.values: every category; without: up to the item's last one, as
|
|
8426
|
+
// flat's sparse nValuesA). Returns { parts, size }.
|
|
8427
|
+
function flatPieItemValues(item, t, o = {}) {
|
|
8428
|
+
t = String(t || '');
|
|
8429
|
+
const n = o.nParts != null ? o.nParts : item.parts.length;
|
|
8430
|
+
let parts = [];
|
|
8431
|
+
for (let k = 0; k < n; k++) parts[k] = item.parts[k] || 0;
|
|
8432
|
+
let size = item.size;
|
|
8433
|
+
if (/MEAN/.test(t)) {
|
|
8434
|
+
const perPart = o.categorical || /ZEROISNOTVALUE/.test(t);
|
|
8435
|
+
parts = parts.map((v, k) => (perPart ? (item.counts[k] > 0 ? v / item.counts[k] : 0) : v / item.n));
|
|
8436
|
+
if (!/SIZESUM/.test(t)) size /= item.n;
|
|
8437
|
+
}
|
|
8438
|
+
if (/AUTO100/.test(t)) {
|
|
8439
|
+
const sum = parts.reduce((a, v) => a + v, 0);
|
|
8440
|
+
parts = parts.map(v => v / (sum / 100));
|
|
8441
|
+
}
|
|
8442
|
+
return { parts, size };
|
|
8443
|
+
}
|
|
8444
|
+
|
|
8445
|
+
// CHART|VECTOR / CHART|BEZIER: the theme's items as real ixmaps-flat
|
|
8446
|
+
// builds them (maptheme.js loadAndAggregateValuesOfTheme, which every
|
|
8447
|
+
// VECTOR / BEZIER theme goes through, 9650: 10402-10450 the second
|
|
8448
|
+
// position, 10763 the item id, 10779-10985 the items). `records`:
|
|
8449
|
+
// { p1: [lng, lat], p2: [lng, lat] | null, cat (0-based category, -1
|
|
8450
|
+
// not one of them; null without CATEGORICAL), value (the value
|
|
8451
|
+
// field as a number, without CATEGORICAL), size (the size field as
|
|
8452
|
+
// a number, NaN when not one; 1 without a size field), raw }
|
|
8453
|
+
// `o`: { aggregate, vectorKey (the VECTOR flag: the item id has the
|
|
8454
|
+
// second position too), multiParts (CATEGORICAL: the category is part
|
|
8455
|
+
// of the id), max / min (MAX / MIN aggregation), zeroIsNotValue,
|
|
8456
|
+
// negativeIsNotValue }. Returns items
|
|
8457
|
+
// { p1, p2, cat, nSize, value, count, raw (the first record), raws }
|
|
8458
|
+
// - without AGGREGATE every record is an item, its nSize the size as
|
|
8459
|
+
// it is (negative too, 0 when not a number) — flat checks no value
|
|
8460
|
+
// there
|
|
8461
|
+
// - AGGREGATE: records of one id are one item: the id is the first
|
|
8462
|
+
// position (+ the category with CATEGORICAL, + the second position
|
|
8463
|
+
// with VECTOR — a BEZIER without VECTOR sums every flow of an origin
|
|
8464
|
+
// into its first record's flow, as flat); a record whose size is no
|
|
8465
|
+
// number (or 0 with ZEROISNOTVALUE, negative with
|
|
8466
|
+
// NEGATIVEISNOTVALUE) is left out; nSize the sum of the |sizes|
|
|
8467
|
+
// (MAX / MIN: their maximum / minimum), value the values' sum
|
|
8468
|
+
// - the first record of an item gives its category, second position,
|
|
8469
|
+
// and the record its tooltip reads
|
|
8470
|
+
function flatVectorItems(records, o = {}) {
|
|
8471
|
+
const items = [];
|
|
8472
|
+
if (!o.aggregate) {
|
|
8473
|
+
for (const r of records) {
|
|
8474
|
+
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] });
|
|
8475
|
+
}
|
|
8476
|
+
return items;
|
|
8477
|
+
}
|
|
8478
|
+
const byId = new Map();
|
|
8479
|
+
for (const r of records) {
|
|
8480
|
+
const s = r.size;
|
|
8481
|
+
if (isNaN(s) || (s === 0 && o.zeroIsNotValue) || (s < 0 && o.negativeIsNotValue)) continue;
|
|
8482
|
+
let id = r.p1[0] + ',' + r.p1[1];
|
|
8483
|
+
// flat's nValue for the id: the category number (1-based), 0 for a
|
|
8484
|
+
// value that is none of the categories
|
|
8485
|
+
if (o.multiParts) id += ',' + (r.cat != null && r.cat >= 0 ? r.cat + 1 : 0);
|
|
8486
|
+
if (o.vectorKey && r.p2) id += '&' + r.p2[0] + ',' + r.p2[1];
|
|
8487
|
+
const item = byId.get(id);
|
|
8488
|
+
if (!item) {
|
|
8489
|
+
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] };
|
|
8490
|
+
byId.set(id, it);
|
|
8491
|
+
items.push(it);
|
|
8492
|
+
continue;
|
|
8493
|
+
}
|
|
8494
|
+
if (o.max) {
|
|
8495
|
+
item.nSize = Math.max(item.nSize, s);
|
|
8496
|
+
item.value = Math.max(item.value || 0, r.value || 0);
|
|
8497
|
+
} else if (o.min) {
|
|
8498
|
+
item.nSize = Math.min(item.nSize, s);
|
|
8499
|
+
item.value = Math.min(item.value || 0, r.value || 0);
|
|
8500
|
+
} else {
|
|
8501
|
+
item.nSize += Math.abs(s);
|
|
8502
|
+
item.value = (item.value || 0) + (r.value || 0);
|
|
8503
|
+
}
|
|
8504
|
+
item.count++;
|
|
8505
|
+
item.raws.push(r.raw);
|
|
8506
|
+
}
|
|
8507
|
+
return items;
|
|
8508
|
+
}
|
|
8509
|
+
|
|
8510
|
+
// CHART|BEZIER: one flow's curve as flat draws it (maptheme.js
|
|
8511
|
+
// 18148-18375), in screen pixels relative to its first position (y
|
|
8512
|
+
// down — flat's SVG map coordinates, scaled). dx/dy: the second
|
|
8513
|
+
// position minus the first. o:
|
|
8514
|
+
// w the line width in flat's chart units (linewidth, else
|
|
8515
|
+
// 10 · (|nSize| / max)^(1/sizepow))
|
|
8516
|
+
// ll flat's arrow / gap length in chart units (w + 3, GAP: w +
|
|
8517
|
+
// gapsize — computed by the caller, see _buildVectorLayers)
|
|
8518
|
+
// unit pixels per chart unit (the object zoom × style.scale)
|
|
8519
|
+
// oz pixels per chart unit without style.scale
|
|
8520
|
+
// bow rangescale (flat's nBow; 5 when not given), RANDOM already
|
|
8521
|
+
// applied
|
|
8522
|
+
// t the type string (SHORT, POINTER / ARROW, GAP)
|
|
8523
|
+
// marker markersize (1)
|
|
8524
|
+
// gap gapsize (20)
|
|
8525
|
+
// Returns { start, c1, c2, end, vx, vy } — start/end/c1/c2 [x, y]
|
|
8526
|
+
// pixels; (vx, vy) the end after shortening, which decides the
|
|
8527
|
+
// gradient direction. The control points come from the unshortened
|
|
8528
|
+
// vector: offset by (dy, −dx) / 50 · bow (SHORT: the unit vector · 5
|
|
8529
|
+
// bow chart units), at 1/4 and 1/2 of the (shortened) vector. An arrow (POINTER
|
|
8530
|
+
// or a type word ending in ARROW) ends the curve ll · markersize units
|
|
8531
|
+
// short of the second position, room for the arrow head. GAP: the curve
|
|
8532
|
+
// starts gapsize units off the first position and ends ll / 500 of the
|
|
8533
|
+
// bow further on.
|
|
8534
|
+
function bezierVectorLayout(dx, dy, o) {
|
|
8535
|
+
const t = String(o.t || '');
|
|
8536
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
8537
|
+
const bow = o.bow || 5;
|
|
8538
|
+
let ox, oy;
|
|
8539
|
+
// (LONG's bow depends on flat's own map units — not drawn here, see
|
|
8540
|
+
// VECTOR_INERT_FLAGS: the default bow instead)
|
|
8541
|
+
if (/\bSHORT\b/.test(t)) { ox = dy / len * bow * 5 * o.oz; oy = dx / len * bow * 5 * o.oz; }
|
|
8542
|
+
else { ox = dy / 50 * bow; oy = dx / 50 * bow; }
|
|
8543
|
+
let x = dx, y = dy;
|
|
8544
|
+
// flat's /\bPOINTER|ARROW\b/: POINTER at a word start, or ARROW at a
|
|
8545
|
+
// word end (ARROW, SMALLARROW, LASTARROW ...)
|
|
8546
|
+
if (/\bPOINTER|ARROW\b/.test(t)) {
|
|
8547
|
+
const k = o.ll * o.unit * (o.marker || 1) / len;
|
|
8548
|
+
x -= dx * k; y -= dy * k;
|
|
8549
|
+
}
|
|
8550
|
+
let x0 = 0, y0 = 0;
|
|
8551
|
+
if (/\bGAP\b/.test(t)) {
|
|
8552
|
+
x += ox / 500 * o.ll;
|
|
8553
|
+
y -= oy / 500 * o.ll;
|
|
8554
|
+
x0 = x / len * (o.gap || 20) * o.oz;
|
|
8555
|
+
y0 = y / len * (o.gap || 20) * o.oz;
|
|
8556
|
+
}
|
|
8557
|
+
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 };
|
|
8558
|
+
}
|
|
8559
|
+
|
|
8560
|
+
// flat's Themes.toArray for a style list (maptheme.js 999-1016): a list
|
|
8561
|
+
// as it is; a string split at "|" when it has one or names an RGB(…)
|
|
8562
|
+
// color (flat's /\RGB/: upper case only), else at ","
|
|
8563
|
+
function flatToArray(v) {
|
|
8564
|
+
if (Array.isArray(v)) return v;
|
|
8565
|
+
const str = String(v);
|
|
8566
|
+
return (/\|/.test(str) || /RGB/.test(str)) ? str.split('|') : str.split(',');
|
|
8567
|
+
}
|
|
8568
|
+
// a fixed number in [0, 1) for a string — stands in for flat's
|
|
8569
|
+
// Math.random() where the harness needs the same drawing every time
|
|
8570
|
+
function hashUnit(str) {
|
|
8571
|
+
let h = 2166136261;
|
|
8572
|
+
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
|
8573
|
+
return (h >>> 0) / 4294967296;
|
|
8574
|
+
}
|
|
8575
|
+
|
|
8576
|
+
// points of a cubic Bézier, n segments (n + 1 points)
|
|
8577
|
+
function cubicBezierPoints(p0, c1, c2, p3, n) {
|
|
8578
|
+
const out = [];
|
|
8579
|
+
for (let i = 0; i <= n; i++) {
|
|
8580
|
+
const s = i / n, u = 1 - s;
|
|
8581
|
+
const a = u * u * u, b = 3 * u * u * s, c = 3 * u * s * s, d = s * s * s;
|
|
8582
|
+
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]]);
|
|
8583
|
+
}
|
|
8584
|
+
return out;
|
|
8585
|
+
}
|
|
8586
|
+
|
|
8587
|
+
// flat's arrow marker (an SVG <marker>, markerUnits strokeWidth, orient
|
|
8588
|
+
// auto) at a line's end: the triangle M0,s L s,s/2 L0,0 in units of the
|
|
8589
|
+
// line width sw, its reference point (refX, refY) on the end point, its
|
|
8590
|
+
// x axis along the end's direction (tx, ty). BEZIER: s = min(5, 2.5 +
|
|
8591
|
+
// 5 / (w · scale)) · markersize, ref (s / 1.7, s / 2) (maptheme.js
|
|
8592
|
+
// 18309-18320); VECTOR: s = 4, ref (4, 2) — the tip on the end point
|
|
8593
|
+
// (18411-18424). A zero direction is orient 0: pointing right.
|
|
8594
|
+
// Returns the three corners [x, y] (pixels, y down).
|
|
8595
|
+
function arrowMarkerTriangle(end, tx, ty, sw, s, refX, refY) {
|
|
8596
|
+
const l = Math.sqrt(tx * tx + ty * ty);
|
|
8597
|
+
const ux = l ? tx / l : 1, uy = l ? ty / l : 0;
|
|
8598
|
+
const at = (mx, my) => {
|
|
8599
|
+
const a = (mx - refX) * sw, b = (my - refY) * sw;
|
|
8600
|
+
return [end[0] + a * ux - b * uy, end[1] + a * uy + b * ux];
|
|
8601
|
+
};
|
|
8602
|
+
return [at(0, s), at(s, s / 2), at(0, 0)];
|
|
8603
|
+
}
|
|
8604
|
+
|
|
8605
|
+
// BEZIER GRADIENT / FADEIN (maptheme.js 18233-18284): a linear gradient
|
|
8606
|
+
// over the curve's bounding box, vertical when the (shortened) vector
|
|
8607
|
+
// is more vertical than horizontal, else horizontal; the stop on the
|
|
8608
|
+
// first position's side has the fade opacity (FADEIN 0.2, GRADIENT 2),
|
|
8609
|
+
// the other one 2 — SVG clamps both to 1 — and the colors run from the
|
|
8610
|
+
// second color (linecolor's first entry) at the start to the line color
|
|
8611
|
+
// at the end. Returns per point { k: 0 … 1 from the start side's stop,
|
|
8612
|
+
// alpha } — the color mix k and opacity of each point.
|
|
8613
|
+
function fadeGradientStops(points, vx, vy, fade) {
|
|
8614
|
+
const vertical = Math.abs(vy) > Math.abs(vx);
|
|
8615
|
+
const axis = vertical ? 1 : 0;
|
|
8616
|
+
let min = Infinity, max = -Infinity;
|
|
8617
|
+
for (const p of points) { if (p[axis] < min) min = p[axis]; if (p[axis] > max) max = p[axis]; }
|
|
8618
|
+
const v = vertical ? vy : vx;
|
|
8619
|
+
const a = Math.min(1, Math.max(0, fade));
|
|
8620
|
+
// offset 0 is the box's left / top side: the start side unless the
|
|
8621
|
+
// vector points left / up
|
|
8622
|
+
const startAtZero = !(v < 0);
|
|
8623
|
+
return points.map(p => {
|
|
8624
|
+
let f = max > min ? (p[axis] - min) / (max - min) : 0;
|
|
8625
|
+
if (!startAtZero) f = 1 - f;
|
|
8626
|
+
// f: 0 on the start side, 1 on the end side
|
|
8627
|
+
return { k: f, alpha: a + (1 - a) * f };
|
|
8628
|
+
});
|
|
8629
|
+
}
|
|
8630
|
+
|
|
7459
8631
|
// CHART|SYMBOL|SEQUENCE (flat maptheme.js 21184-21300, 21790-21925,
|
|
7460
8632
|
// 22296-22395): one symbol per category part of a chart, in category
|
|
7461
8633
|
// order or sorted (SORT: DOWN biggest first, UP smallest first), parts
|
|
@@ -8511,6 +9683,9 @@ in vec4 vPieLineColor;
|
|
|
8511
9683
|
cell.value += f.properties.value;
|
|
8512
9684
|
if (f.properties.classValue !== undefined) cell.classValue += f.properties.classValue;
|
|
8513
9685
|
if (post) cell.value100 += f.properties.value100 || 0;
|
|
9686
|
+
// an AGGREGATE / CATEGORICAL pie's record (flatPieRecord): its parts
|
|
9687
|
+
// and size added to the cell's item as flat does (flatPieAccumulate)
|
|
9688
|
+
if (f.properties.pie) cell.pie = flatPieAccumulate(cell.pie || null, f.properties.pie, flags);
|
|
8514
9689
|
const series = f.properties.series;
|
|
8515
9690
|
if (series) {
|
|
8516
9691
|
if (!cell.series) cell.series = new Array(series.length).fill(0);
|
|
@@ -8532,6 +9707,7 @@ in vec4 vPieLineColor;
|
|
|
8532
9707
|
...(cell.raws && cell.raws.length ? { raws: cell.raws } : {}) }
|
|
8533
9708
|
: cell.first.properties;
|
|
8534
9709
|
if (cell.series && cell.n > 1) props = Object.assign({}, props, { series: cell.series });
|
|
9710
|
+
if (cell.pie) props = Object.assign({}, props, { pie: cell.pie });
|
|
8535
9711
|
// a field aggregation's cell key (the field value): the categories of
|
|
8536
9712
|
// one cell are merged by it (groupCoLocated), not by their positions
|
|
8537
9713
|
if (field) props = Object.assign({}, props, { aggKey: cell.key });
|
|
@@ -8709,7 +9885,10 @@ in vec4 vPieLineColor;
|
|
|
8709
9885
|
this._filterExpr = spec.filter || '';
|
|
8710
9886
|
this.mapOptions = mapOptions || {};
|
|
8711
9887
|
this._specBinding = spec.binding;
|
|
8712
|
-
|
|
9888
|
+
// VECTOR / BEZIER keep every record: flat checks their values only
|
|
9889
|
+
// when it aggregates them (flatVectorItems)
|
|
9890
|
+
const withDensity = this._withDensity(fc.features);
|
|
9891
|
+
this.features = applyField100(isVectorChart(this.flags) ? withDensity : filterFlatValues(withDensity, this.binding, this.flags), this.binding, this.flags, this.style);
|
|
8713
9892
|
this.binding = field100Binding(this.binding, this.flags);
|
|
8714
9893
|
this._iconCache = new Map();
|
|
8715
9894
|
this._glowIconCache = new Map();
|
|
@@ -8804,7 +9983,8 @@ in vec4 vPieLineColor;
|
|
|
8804
9983
|
this._gridRefLat = (minLat <= maxLat) ? (minLat + maxLat) / 2 : 0;
|
|
8805
9984
|
}
|
|
8806
9985
|
|
|
8807
|
-
|
|
9986
|
+
// a bar chart's parts are a pie's (its value fields or categories)
|
|
9987
|
+
if ((this._isPieChart() || this._isBarChart()) && this.binding.value) {
|
|
8808
9988
|
this._preparePie();
|
|
8809
9989
|
} else if (this.flags.has('DOMINANT') && this.binding.value && !isAggregatedCategoricalChoropleth(this)) {
|
|
8810
9990
|
// .type("CHOROPLETH|DOMINANT") — a MULTI-field bound value
|
|
@@ -8956,6 +10136,7 @@ in vec4 vPieLineColor;
|
|
|
8956
10136
|
if (this._usesAggregationIndex()) {
|
|
8957
10137
|
this._buildAggregationIndex(this.features);
|
|
8958
10138
|
}
|
|
10139
|
+
if (this._isVectorChart()) this._prepareVector();
|
|
8959
10140
|
|
|
8960
10141
|
this._prepareAlphaField();
|
|
8961
10142
|
}
|
|
@@ -9049,6 +10230,36 @@ in vec4 vPieLineColor;
|
|
|
9049
10230
|
_resolveAggregateValue(props) { return resolveAggregateValue(this.binding, this.flags, props); }
|
|
9050
10231
|
|
|
9051
10232
|
_buildAggregationIndex(sourceFeatures) {
|
|
10233
|
+
// an AGGREGATE / CATEGORICAL pie: one bucket, each record carrying its
|
|
10234
|
+
// contribution to its item (flatPieRecord), added up per position or
|
|
10235
|
+
// grid cell by aggregateOnGrid (flatPieAccumulate)
|
|
10236
|
+
if (this._isAggregatedPie()) {
|
|
10237
|
+
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
10238
|
+
const fields = this._pieCategorical ? null : String(this.binding.value || '').split('|');
|
|
10239
|
+
const rules = flatValueRules(t, fields ? fields.length : 1);
|
|
10240
|
+
const sizeField = this.binding.size && this.binding.size !== '$item$' ? this.binding.size : null;
|
|
10241
|
+
const feats = [];
|
|
10242
|
+
for (const f of sourceFeatures) {
|
|
10243
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
10244
|
+
if (!c || typeof c[0] !== 'number' || !isFinite(c[0]) || !isFinite(c[1])) continue;
|
|
10245
|
+
const p = f.properties || {};
|
|
10246
|
+
let cat;
|
|
10247
|
+
if (this._pieCategorical) {
|
|
10248
|
+
const k = this._pieCategoryKey(p[this.binding.value]);
|
|
10249
|
+
const i = k == null ? undefined : this._pieCategoryIndex.get(k);
|
|
10250
|
+
cat = i == null ? -1 : i;
|
|
10251
|
+
}
|
|
10252
|
+
const pie = flatPieRecord(p, { rules, sizeField, cat, fields });
|
|
10253
|
+
if (!pie) continue;
|
|
10254
|
+
feats.push({ type: 'Feature', geometry: f.geometry, properties: { value: pie.size, pie, raw: p } });
|
|
10255
|
+
}
|
|
10256
|
+
this._featuresByCategory = [feats];
|
|
10257
|
+
this._clusterIndices = null;
|
|
10258
|
+
this._clusterRadiusPx = null;
|
|
10259
|
+
this._aggregateStatsCache = null;
|
|
10260
|
+
this._pieStatsCache = null;
|
|
10261
|
+
return;
|
|
10262
|
+
}
|
|
9052
10263
|
// Range-classed (non-CATEGORICAL numeric) coloring: DON'T pre-split
|
|
9053
10264
|
// by class here — the real engine aggregates every record together
|
|
9054
10265
|
// first and classes color from the resulting CELL TOTAL afterward
|
|
@@ -9231,7 +10442,10 @@ in vec4 vPieLineColor;
|
|
|
9231
10442
|
this._timeMin = undefined;
|
|
9232
10443
|
const facetFilters = this.facetFilters, runtimeFilterExpr = this._runtimeFilterExpr, timeFrame = this._timeFrame;
|
|
9233
10444
|
this.binding = this._specBinding;
|
|
9234
|
-
|
|
10445
|
+
// VECTOR / BEZIER keep every record: flat checks their values only
|
|
10446
|
+
// when it aggregates them (flatVectorItems)
|
|
10447
|
+
const withDensity = this._withDensity(fc.features);
|
|
10448
|
+
this.features = applyField100(isVectorChart(this.flags) ? withDensity : filterFlatValues(withDensity, this.binding, this.flags), this.binding, this.flags, this.style);
|
|
9235
10449
|
this.binding = field100Binding(this.binding, this.flags);
|
|
9236
10450
|
this._iconCache.clear();
|
|
9237
10451
|
this._glowIconCache.clear();
|
|
@@ -9274,10 +10488,14 @@ in vec4 vPieLineColor;
|
|
|
9274
10488
|
}
|
|
9275
10489
|
|
|
9276
10490
|
_usesAggregationIndex() {
|
|
10491
|
+
// AGGREGATE / CATEGORICAL pies aggregate their records into items
|
|
10492
|
+
// (see _buildAggregationIndex)
|
|
10493
|
+
if (this._isAggregatedPie()) return true;
|
|
9277
10494
|
// GRIDSIZE bins its own grid (_ensureGridIndex) — PLOT curves and the
|
|
9278
10495
|
// plain mesh; a classed GRIDSIZE symbol chart is aggregated like any
|
|
9279
10496
|
// other (see _isPlainGridMesh)
|
|
9280
|
-
|
|
10497
|
+
// VECTOR / BEZIER aggregate by their positions (flatVectorItems)
|
|
10498
|
+
return (this.flags.has('AGGREGATE') || isSymbolChart(this.flags)) && !isVectorChart(this.flags)
|
|
9281
10499
|
&& (!this.flags.has('GRIDSIZE') || (!this.flags.has('PLOT') && !this._isPlainGridMesh()));
|
|
9282
10500
|
}
|
|
9283
10501
|
|
|
@@ -9562,7 +10780,15 @@ in vec4 vPieLineColor;
|
|
|
9562
10780
|
return this._glowIconCache.get(key);
|
|
9563
10781
|
}
|
|
9564
10782
|
|
|
9565
|
-
|
|
10783
|
+
// look (optional, a CATEGORICAL BUBBLE's flat look — see bubbleIconLook):
|
|
10784
|
+
// every part filled at look.fillOpacity and stroked like its own flat
|
|
10785
|
+
// bubble (look.stroke, else its color × 0.7; look.strokeOpacity), the
|
|
10786
|
+
// width look.width canvas px for a part of the icon's full radius,
|
|
10787
|
+
// √(share of it) for a smaller one (flat's width ∝ √r) — OUTLINE:
|
|
10788
|
+
// ∝ min(1, r / normal radius), look.outlineRatio being the full
|
|
10789
|
+
// radius / normal radius. Without it the older look: 0.9 fill, a 1 px
|
|
10790
|
+
// white stroke (the SYMBOL branch's packed icon).
|
|
10791
|
+
_buildBubbleIcon(counts, colors, marked = null, shapes = null, look = null) {
|
|
9566
10792
|
// The cache key quantizes each count's RATIO to the group's total,
|
|
9567
10793
|
// not the raw count — computeBubblePackLayout's radii are purely
|
|
9568
10794
|
// sqrt(c/total), so two clusters with wildly different absolute
|
|
@@ -9592,7 +10818,8 @@ in vec4 vPieLineColor;
|
|
|
9592
10818
|
// coarse share buckets that can put a small marked part at 0
|
|
9593
10819
|
const key = counts.map(c => Math.round((c / total) * RATIO_BUCKETS)).join('-') + (marked
|
|
9594
10820
|
? '|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(',') : '')
|
|
10821
|
+
: '') + (shapes ? '|sym:' + shapes.map(v => String(v).toLowerCase()).join(',') : '')
|
|
10822
|
+
+ (look ? `|fb:${look.fillOpacity}:${look.strokeOpacity}:${look.stroke ? look.stroke.join(',') : look.noStroke ? 'none' : 'low'}:${look.width}${look.outlineRatio != null ? ':' + look.outlineRatio : ''}` : '');
|
|
9596
10823
|
if (this._iconCache.has(key)) return this._iconCache.get(key);
|
|
9597
10824
|
const size = BUBBLE_ICON_SIZE;
|
|
9598
10825
|
const canvas = document.createElement('canvas');
|
|
@@ -9607,13 +10834,33 @@ in vec4 vPieLineColor;
|
|
|
9607
10834
|
// _resolveSymbolShape; circles when the name isn't one GL draws
|
|
9608
10835
|
const shape = shapes ? normalizeSymbolShape(shapes[Math.min(p.i, shapes.length - 1)]) : 'circle';
|
|
9609
10836
|
const px = cx + offsets[i].x * fitScale, py = cy + offsets[i].y * fitScale;
|
|
10837
|
+
let pr = radii[i] * fitScale, lw = 0;
|
|
10838
|
+
if (look && !look.noStroke) {
|
|
10839
|
+
const k = pr / (size / 2);
|
|
10840
|
+
lw = Math.min(pr, look.outlineRatio != null
|
|
10841
|
+
? look.width * Math.min(1, k * look.outlineRatio) / Math.min(1, look.outlineRatio)
|
|
10842
|
+
: look.width * Math.sqrt(k));
|
|
10843
|
+
// inside the part, as _buildSingleIcon: the icon's edge is the bubble's
|
|
10844
|
+
pr = Math.max(0.5, pr - lw / 2);
|
|
10845
|
+
}
|
|
9610
10846
|
ctx.beginPath();
|
|
9611
|
-
if (shape === 'circle') ctx.arc(px, py,
|
|
9612
|
-
else drawSymbolPath(ctx, shape, px, py,
|
|
10847
|
+
if (shape === 'circle') ctx.arc(px, py, pr, 0, Math.PI * 2);
|
|
10848
|
+
else drawSymbolPath(ctx, shape, px, py, pr);
|
|
9613
10849
|
ctx.closePath();
|
|
9614
10850
|
ctx.fillStyle = `rgb(${colors[p.i].join(',')})`;
|
|
9615
|
-
|
|
9616
|
-
|
|
10851
|
+
if (!look) {
|
|
10852
|
+
ctx.globalAlpha = 0.9; ctx.fill(); ctx.globalAlpha = 1;
|
|
10853
|
+
ctx.lineWidth = 1; ctx.strokeStyle = '#fff'; ctx.stroke();
|
|
10854
|
+
return;
|
|
10855
|
+
}
|
|
10856
|
+
ctx.globalAlpha = look.fillOpacity; ctx.fill();
|
|
10857
|
+
if (lw > 0) {
|
|
10858
|
+
ctx.globalAlpha = look.strokeOpacity;
|
|
10859
|
+
ctx.lineWidth = lw;
|
|
10860
|
+
ctx.strokeStyle = `rgb(${(look.stroke || flatDerivateRgb(colors[p.i], 0.7)).join(',')})`;
|
|
10861
|
+
ctx.stroke();
|
|
10862
|
+
}
|
|
10863
|
+
ctx.globalAlpha = 1;
|
|
9617
10864
|
});
|
|
9618
10865
|
const icon = { url: canvas.toDataURL(), width: size, height: size, anchorX: size / 2, anchorY: size / 2, id: key };
|
|
9619
10866
|
return this._cacheIcon(key, icon);
|
|
@@ -9642,10 +10889,19 @@ in vec4 vPieLineColor;
|
|
|
9642
10889
|
// cached bubble icon here already accepts; the border scales with
|
|
9643
10890
|
// the bubble exactly as the fill already does, which reads as
|
|
9644
10891
|
// reasonable rather than wrong.
|
|
9645
|
-
|
|
10892
|
+
// borderOpacity (default 1): the border's own alpha (flat's
|
|
10893
|
+
// stroke-opacity, see flatBubbleLook)
|
|
10894
|
+
// colorscheme ["none"] — flat's explicit no-fill symbol
|
|
10895
|
+
_noFillScheme() {
|
|
10896
|
+
const cs = this.style.colorscheme;
|
|
10897
|
+
return Array.isArray(cs) && cs.length === 1 && String(cs[0]).trim().toLowerCase() === 'none';
|
|
10898
|
+
}
|
|
10899
|
+
|
|
10900
|
+
_buildSingleIcon(colorRgb, opacity, shape, borderColorRgb, borderWidth, borderOpacity = 1) {
|
|
10901
|
+
if (!colorRgb) { colorRgb = [128, 128, 128]; opacity = 0; } // no class colour (["none"]): unfilled
|
|
9646
10902
|
shape = shape || 'circle';
|
|
9647
10903
|
borderWidth = borderWidth || 0;
|
|
9648
|
-
const key = `single-${shape}-${colorRgb.join(',')}-${opacity}-${borderColorRgb ? borderColorRgb.join(',') : 'none'}-${borderWidth}`;
|
|
10904
|
+
const key = `single-${shape}-${colorRgb.join(',')}-${opacity}-${borderColorRgb ? borderColorRgb.join(',') : 'none'}-${borderWidth}${borderOpacity !== 1 ? `-so${borderOpacity}` : ''}`;
|
|
9649
10905
|
if (this._iconCache.has(key)) return this._iconCache.get(key);
|
|
9650
10906
|
const size = BUBBLE_ICON_SIZE;
|
|
9651
10907
|
const canvas = document.createElement('canvas');
|
|
@@ -9667,7 +10923,7 @@ in vec4 vPieLineColor;
|
|
|
9667
10923
|
ctx.globalAlpha = shape === 'empty' ? 0 : opacity;
|
|
9668
10924
|
ctx.fill();
|
|
9669
10925
|
if (borderWidth > 0 && borderColorRgb) {
|
|
9670
|
-
ctx.globalAlpha =
|
|
10926
|
+
ctx.globalAlpha = borderOpacity;
|
|
9671
10927
|
ctx.strokeStyle = `rgb(${borderColorRgb.join(',')})`;
|
|
9672
10928
|
ctx.lineWidth = borderWidth;
|
|
9673
10929
|
ctx.stroke();
|
|
@@ -9765,18 +11021,35 @@ in vec4 vPieLineColor;
|
|
|
9765
11021
|
local: {}
|
|
9766
11022
|
};
|
|
9767
11023
|
|
|
11024
|
+
// a VECTOR / BEZIER flow (measured on the twin page): no item chart,
|
|
11025
|
+
// label or class; its value is flat's nValuesA[0] — with CATEGORICAL
|
|
11026
|
+
// the category's number (1 for the first), else the (summed) value
|
|
11027
|
+
// field; its count the records of the item
|
|
11028
|
+
if (this._isVectorChart() && props.vector) {
|
|
11029
|
+
const v = props.vector.itemValue;
|
|
11030
|
+
dataObj.theme.chart = dataObj.theme.item.chart = '';
|
|
11031
|
+
dataObj.theme.item.value = v == null || isNaN(v) ? '' : flatFormatValue(v, explicitValueDecimals(this.style) ?? (Math.abs(v) < 1 ? 2 : 0), 'ROUND');
|
|
11032
|
+
dataObj.theme.item.label = dataObj.theme.item.class = '';
|
|
11033
|
+
dataObj.theme.item.count = props.count;
|
|
11034
|
+
}
|
|
11035
|
+
|
|
9768
11036
|
// bare {{FIELD}}, {{raw.FIELD}}, {{local.FIELD}} — only meaningful
|
|
9769
|
-
// for a single unaggregated record (see class comment above)
|
|
9770
|
-
|
|
9771
|
-
|
|
11037
|
+
// for a single unaggregated record (see class comment above); an
|
|
11038
|
+
// aggregated pie's item gives its first record's, as flat (data[0])
|
|
11039
|
+
const fieldsRaw = isGroup ? props.firstRaw : props.raw;
|
|
11040
|
+
if (fieldsRaw) {
|
|
11041
|
+
Object.keys(fieldsRaw).forEach(field => {
|
|
9772
11042
|
if (field === 'geometry') return;
|
|
9773
|
-
const v =
|
|
11043
|
+
const v = fieldsRaw[field];
|
|
9774
11044
|
if (this._isNumericValue(v)) {
|
|
9775
11045
|
dataObj[field] = this._formatTooltipValue(v);
|
|
9776
11046
|
dataObj.raw[field] = v;
|
|
9777
11047
|
} else {
|
|
9778
11048
|
dataObj[field] = v;
|
|
9779
|
-
|
|
11049
|
+
// translated, as flat's tooltip (tooltip_mustache.js 446:
|
|
11050
|
+
// ixmaps.getLocalString(value)) — the migration page's ISO
|
|
11051
|
+
// code → country name in {{local.from}} → {{local.to}}
|
|
11052
|
+
dataObj.local[field] = makeLocalText([])(v);
|
|
9780
11053
|
}
|
|
9781
11054
|
});
|
|
9782
11055
|
}
|
|
@@ -10062,11 +11335,21 @@ in vec4 vPieLineColor;
|
|
|
10062
11335
|
// USER charts (a page's own chart function, style.userdraw) go the
|
|
10063
11336
|
// chart pipeline too, drawn by _buildUserChartLayers (bubbles when the
|
|
10064
11337
|
// page has no such function)
|
|
11338
|
+
// VECTOR / BEZIER (flat's chartMap draws them before it calls
|
|
11339
|
+
// drawChart, maptheme.js 18148, 18376)
|
|
10065
11340
|
// PIE / DONUT (flat's PIE branch comes before BUBBLE and SYMBOL in
|
|
10066
11341
|
// drawChart, maptheme.js 19939)
|
|
11342
|
+
if (this._isVectorChart()) return this._buildVectorLayers(zoom, bbox, liveZoom, globeCenter);
|
|
10067
11343
|
if (this._isPieChart()) return this._buildPieLayers(zoom, bbox, liveZoom, globeCenter);
|
|
11344
|
+
// BAR / BARS (flat's BAR branch, after BUBBLE and SYMBOL: a type
|
|
11345
|
+
// with those is not a bar chart, see isBarChart)
|
|
11346
|
+
if (this._isBarChart()) return this._buildBarLayers(zoom, bbox, liveZoom, globeCenter);
|
|
10068
11347
|
if (isSymbolChart(this.flags)) return this._buildChartLayers(zoom, bbox, liveZoom, globeCenter);
|
|
10069
|
-
|
|
11348
|
+
// once per theme, not on every redraw
|
|
11349
|
+
if (!this._warnedNoRenderer) {
|
|
11350
|
+
this._warnedNoRenderer = true;
|
|
11351
|
+
console.warn(`[ixmaps-gl] layer "${this.name}": type "${[...this.flags].join('|')}" has no implemented renderer`);
|
|
11352
|
+
}
|
|
10070
11353
|
return [];
|
|
10071
11354
|
}
|
|
10072
11355
|
|
|
@@ -10428,16 +11711,26 @@ in vec4 vPieLineColor;
|
|
|
10428
11711
|
return String(this.binding.value || '').split('|');
|
|
10429
11712
|
}
|
|
10430
11713
|
// CHART|PIE (and PIE|DONUT): one pie per record, its slices the value
|
|
10431
|
-
// fields
|
|
10432
|
-
// a
|
|
10433
|
-
//
|
|
11714
|
+
// fields; AGGREGATE and CATEGORICAL pies one pie per item — the records
|
|
11715
|
+
// of a position (or grid cell) — its slices the categories or the value
|
|
11716
|
+
// fields (_isAggregatedPie, _buildPieLayers)
|
|
11717
|
+
_isVectorChart() { return isVectorChart(this.flags); }
|
|
11718
|
+
// CHART|BAR / BARS: a bar chart per record or item, its bars a pie's
|
|
11719
|
+
// parts (_buildBarLayers)
|
|
11720
|
+
_isBarChart() { return isBarChart(this.flags); }
|
|
10434
11721
|
_isPieChart() {
|
|
10435
|
-
return this.flags.has('CHART') && this.flags.has('PIE') && !this.flags.has('
|
|
10436
|
-
|
|
11722
|
+
return !isVectorChart(this.flags) && this.flags.has('CHART') && this.flags.has('PIE') && !this.flags.has('DOT');
|
|
11723
|
+
}
|
|
11724
|
+
// a pie whose items flat aggregates (loadAndAggregateValuesOfTheme):
|
|
11725
|
+
// AGGREGATE, or CATEGORICAL (a part per category — flat sums the
|
|
11726
|
+
// records of an item's position by category even without AGGREGATE)
|
|
11727
|
+
// (a bar chart's items too)
|
|
11728
|
+
_isAggregatedPie() {
|
|
11729
|
+
return (this._isPieChart() || this._isBarChart()) && (this.flags.has('AGGREGATE') || this.flags.has('CATEGORICAL'));
|
|
10437
11730
|
}
|
|
10438
11731
|
// a per-item chart whose parts are its value fields (legend rows, sums)
|
|
10439
11732
|
_isMultiFieldChart() {
|
|
10440
|
-
return !!this._multiFields && (this._isMultiFieldSequence() || this._isPieChart());
|
|
11733
|
+
return !!this._multiFields && (this._isMultiFieldSequence() || this._isPieChart() || this._isBarChart());
|
|
10441
11734
|
}
|
|
10442
11735
|
// flat's theme statistics for a pie (maptheme.js 9119-9130): nMin/nMax
|
|
10443
11736
|
// over every part value of every item, the classes (partsA, flat's
|
|
@@ -10445,6 +11738,8 @@ in vec4 vPieLineColor;
|
|
|
10445
11738
|
// colors) — flat colors a 1-part pie (or any with CLASSES) by them
|
|
10446
11739
|
// (maptheme.js 20330-20341); several parts are colored by field.
|
|
10447
11740
|
_preparePie() {
|
|
11741
|
+
if (this.flags.has('CATEGORICAL')) { this._preparePieCategories(); return; }
|
|
11742
|
+
this._pieCategorical = false;
|
|
10448
11743
|
const fields = String(this.binding.value).split('|');
|
|
10449
11744
|
const pooled = [];
|
|
10450
11745
|
for (const f of this.features) {
|
|
@@ -10472,6 +11767,61 @@ in vec4 vPieLineColor;
|
|
|
10472
11767
|
}
|
|
10473
11768
|
}
|
|
10474
11769
|
|
|
11770
|
+
// a CATEGORICAL pie's categories (flat's getStringValueIndex, maptheme.js
|
|
11771
|
+
// 9204-9243): style.values in their order, else the values in the order
|
|
11772
|
+
// the records bring them; a value is trimmed (IGNORECASE: upper case),
|
|
11773
|
+
// an empty one is none (UNDEFINEDISVALUE: "undefined"). A slice per
|
|
11774
|
+
// category in its colorscheme color, labeled by style.label. flat's
|
|
11775
|
+
// classes are the category numbers (partsA: i + 1, the last one 0.001
|
|
11776
|
+
// wider) — its value range (nMin / nMax) follows the items
|
|
11777
|
+
// (_pieAggregateStats).
|
|
11778
|
+
_preparePieCategories() {
|
|
11779
|
+
const st = this.style;
|
|
11780
|
+
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
11781
|
+
const ignoreCase = /IGNORECASE/.test(t), undefinedIsValue = /UNDEFINEDISVALUE/.test(t);
|
|
11782
|
+
const key = v => {
|
|
11783
|
+
let k = v == null ? '' : String(v);
|
|
11784
|
+
if (k.length === 0 || k === ' ') { if (!undefinedIsValue) return null; k = 'undefined'; }
|
|
11785
|
+
if (ignoreCase) k = k.toUpperCase();
|
|
11786
|
+
return k.trim();
|
|
11787
|
+
};
|
|
11788
|
+
const index = new Map();
|
|
11789
|
+
const explicit = Array.isArray(st.values) ? st.values : null;
|
|
11790
|
+
const labels = [];
|
|
11791
|
+
if (explicit) {
|
|
11792
|
+
explicit.forEach(v => { const k = key(v); if (k != null && !index.has(k)) { index.set(k, labels.length); labels.push(String(v).trim()); } });
|
|
11793
|
+
} else {
|
|
11794
|
+
for (const f of this.features) {
|
|
11795
|
+
const k = key(f.properties[this.binding.value]);
|
|
11796
|
+
if (k != null && !index.has(k)) { index.set(k, labels.length); labels.push(k); }
|
|
11797
|
+
}
|
|
11798
|
+
}
|
|
11799
|
+
this._pieCategorical = true;
|
|
11800
|
+
this._pieExplicitValues = !!explicit;
|
|
11801
|
+
this._pieCategoryKey = key;
|
|
11802
|
+
this._pieCategoryIndex = index;
|
|
11803
|
+
this.categoryLabels = labels;
|
|
11804
|
+
const explicitLabel = Array.isArray(st.label) ? st.label.map(String) : null;
|
|
11805
|
+
this.categoryDisplayLabels = explicitLabel && explicitLabel.length === labels.length ? explicitLabel : labels;
|
|
11806
|
+
this.categoryColorsRgb = resolveClassColors(st.colorscheme, this.categoryDisplayLabels, st.classes);
|
|
11807
|
+
// the legend and the facets look a record's raw value up
|
|
11808
|
+
const byLabel = new Map();
|
|
11809
|
+
for (const f of this.features) {
|
|
11810
|
+
const v = f.properties[this.binding.value];
|
|
11811
|
+
if (byLabel.has(v)) continue;
|
|
11812
|
+
const k = key(v);
|
|
11813
|
+
if (k != null && index.has(k)) byLabel.set(v, index.get(k));
|
|
11814
|
+
}
|
|
11815
|
+
labels.forEach((l, i) => { if (!byLabel.has(l)) byLabel.set(l, i); });
|
|
11816
|
+
this.categoryIndexByLabel = byLabel;
|
|
11817
|
+
const n = labels.length;
|
|
11818
|
+
this.partsA = labels.map((l, i) => ({ min: i + 1, max: i + 1 + (i === n - 1 ? 0.001 : 0) + 1e-9 }));
|
|
11819
|
+
this._rangeClassed = false;
|
|
11820
|
+
this._multiFields = null;
|
|
11821
|
+
this._pieFields = null;
|
|
11822
|
+
this._partNoFill = new Set();
|
|
11823
|
+
}
|
|
11824
|
+
|
|
10475
11825
|
// x-axis labels of a per-item PLOT (style.xaxis / label, else the fields)
|
|
10476
11826
|
_itemPlotLabels() {
|
|
10477
11827
|
const fields = this._itemPlotFields();
|
|
@@ -11600,10 +12950,18 @@ in vec4 vPieLineColor;
|
|
|
11600
12950
|
// _buildTooltipContext's own `isGroup` check already uses).
|
|
11601
12951
|
const sizeValueOf = d => d.properties.counts ? d.properties.total : d.properties.value;
|
|
11602
12952
|
const combined = individual.concat(groups).sort((a, b) => sizeValueOf(a) - sizeValueOf(b));
|
|
12953
|
+
// .style({minvalue}) on a bubble: flat skips an item whose size value is below it
|
|
12954
|
+
// (maptheme.js 19826 / 20698, `this.nMinValue && nSizeValue < this.nMinValue`).
|
|
12955
|
+
// combined is ascending, so the items below it are a prefix.
|
|
12956
|
+
const bubbleMin = this.flags.has('SYMBOL') ? styleNum(this.style.minvalue) : NaN;
|
|
12957
|
+
if (bubbleMin) {
|
|
12958
|
+
let cut = 0;
|
|
12959
|
+
while (cut < combined.length && sizeValueOf(combined[cut]) < bubbleMin) cut++;
|
|
12960
|
+
combined.splice(0, cut);
|
|
12961
|
+
}
|
|
11603
12962
|
// maxcharts: like the flat engine (maptheme.js nMaxCharts), draw only the
|
|
11604
12963
|
// 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);
|
|
12964
|
+
applyFlatMaxCharts(combined, this.style);
|
|
11607
12965
|
if (this.flags.has('USER')) {
|
|
11608
12966
|
const userLayers = this._buildUserChartLayers(combined, liveZoom, sizeValueOf);
|
|
11609
12967
|
if (userLayers) return layers.concat(userLayers);
|
|
@@ -11884,6 +13242,19 @@ in vec4 vPieLineColor;
|
|
|
11884
13242
|
// fillopacity and linecolor, as a single item (flat draws one
|
|
11885
13243
|
// symbol) — the packed multi-category icon is for CATEGORICAL cells
|
|
11886
13244
|
const singleSymbol = d => !d.properties.counts || (this._rangeClassed && d.properties.cat != null);
|
|
13245
|
+
// a CATEGORICAL BUBBLE (flat's BUBBLE branch): every bubble — a single
|
|
13246
|
+
// one and each part of a packed one — filled and stroked as flat's,
|
|
13247
|
+
// see flatBubbleLook; the icon carries both opacities
|
|
13248
|
+
const flatLook = flatChartBranch(this.flags) === 'bubble' && this.flags.has('CATEGORICAL')
|
|
13249
|
+
? d => bubbleIconLook(radiusOf(d), objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1), this.style, this.flags, BUBBLE_ICON_SIZE)
|
|
13250
|
+
: null;
|
|
13251
|
+
const singleIcon = d => {
|
|
13252
|
+
const rgb = (this.categoryColorsRgb || [])[d.properties.cat];
|
|
13253
|
+
if (!flatLook) return this._buildSingleIcon(rgb, fillOpacity, shapeOf(d.properties), singleBorderColorRgb, singleBorderWidthPx);
|
|
13254
|
+
const look = flatLook(d);
|
|
13255
|
+
return this._buildSingleIcon(rgb, look.fillOpacity, shapeOf(d.properties),
|
|
13256
|
+
look.noStroke || !rgb ? null : (look.stroke || flatDerivateRgb(rgb, 0.7)), look.width, look.strokeOpacity);
|
|
13257
|
+
};
|
|
11887
13258
|
// flat's symbol outline (maptheme.js 21595-21610 and 21950-21997,
|
|
11888
13259
|
// drawn 21625-21634 / 21995-22035): every SYMBOL shape is stroked,
|
|
11889
13260
|
// (linewidth || 1) · normalX(min(2r / rMax, 0.2)) with rMax the
|
|
@@ -11919,17 +13290,23 @@ in vec4 vPieLineColor;
|
|
|
11919
13290
|
// icon's look, inside the radius
|
|
11920
13291
|
if (combined.length && !alignOf.active && combined.every(d => singleSymbol(d) && shapeOf(d.properties) === 'circle')) {
|
|
11921
13292
|
const radiusPx = d => vRadius(sizeValueOf(d));
|
|
11922
|
-
|
|
13293
|
+
// a CATEGORICAL BUBBLE: flat's look (flatBubbleLook) in screen px,
|
|
13294
|
+
// its stroke centered on the radius as flat's SVG circle
|
|
13295
|
+
const lookUnit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1);
|
|
13296
|
+
// colorscheme ["none"]: flat draws the symbol unfilled (only its values / box / title)
|
|
13297
|
+
const noFill = this._noFillScheme();
|
|
13298
|
+
const circleLook = flatLook ? d => flatBubbleLook((this.categoryColorsRgb || [])[d.properties.cat] || [128, 128, 128], radiusPx(d), lookUnit, this.style, this.flags) : null;
|
|
13299
|
+
const lineRgb = d => (circleLook ? circleLook(d).stroke : outlineRgb(d)) || [0, 0, 0];
|
|
11923
13300
|
layers.push(new ScatterplotLayer({
|
|
11924
13301
|
id: `ix-bubbles-${this.name}-circles-g${this._iconGeneration}`,
|
|
11925
13302
|
data: combined, pickable: true,
|
|
11926
13303
|
getPosition: d => d.geometry.coordinates,
|
|
11927
|
-
getRadius: d => radiusPx(d) - (symbolOutline ? 0 : outlinePx(d, radiusPx(d)) / 2),
|
|
13304
|
+
getRadius: d => radiusPx(d) - (symbolOutline || circleLook ? 0 : outlinePx(d, radiusPx(d)) / 2),
|
|
11928
13305
|
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)),
|
|
13306
|
+
getFillColor: d => [...((this.categoryColorsRgb || [])[d.properties.cat] || [128, 128, 128]).slice(0, 3), noFill ? 0 : Math.round(this._iconAlpha(d) * (circleLook ? circleLook(d).fillOpacity : fillOpacity))],
|
|
13307
|
+
stroked: circleLook ? combined.some(d => circleLook(d).stroke) : combined.some(d => outlineRgb(d)),
|
|
13308
|
+
getLineColor: d => [...lineRgb(d).slice(0, 3), Math.round(this._iconAlpha(d) * (circleLook ? circleLook(d).strokeOpacity : 1))],
|
|
13309
|
+
getLineWidth: d => (circleLook ? (circleLook(d).stroke ? circleLook(d).width : 0) : outlinePx(d, radiusPx(d))),
|
|
11933
13310
|
lineWidthUnits: 'pixels',
|
|
11934
13311
|
billboard: true,
|
|
11935
13312
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
@@ -11939,7 +13316,8 @@ in vec4 vPieLineColor;
|
|
|
11939
13316
|
data: combined, pickable: true,
|
|
11940
13317
|
getPosition: d => d.geometry.coordinates,
|
|
11941
13318
|
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);
|
|
13319
|
+
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));
|
|
13320
|
+
if (flatLook) return singleIcon(d);
|
|
11943
13321
|
const shape = shapeOf(d.properties), rgb = outlineRgb(d);
|
|
11944
13322
|
if (!symbolOutline || !rgb) return this._buildSingleIcon((this.categoryColorsRgb || [])[d.properties.cat], fillOpacity, shape, rgb, rgb ? singleBorderWidthPx : 0);
|
|
11945
13323
|
// the outline in the icon's own pixels, inside its edge; quarter
|
|
@@ -11952,7 +13330,8 @@ in vec4 vPieLineColor;
|
|
|
11952
13330
|
getSize: d => vRadius(sizeValueOf(d)) * 2,
|
|
11953
13331
|
// group icons bake a fixed 0.9 alpha; an explicit fillopacity scales it
|
|
11954
13332
|
// so a near-transparent theme doesn't show opaque 1-px-cell clusters
|
|
11955
|
-
|
|
13333
|
+
// (a flat-look icon carries its own fill and stroke opacity)
|
|
13334
|
+
getColor: d => [255, 255, 255, Math.round(this._iconAlpha(d) * (!flatLook && !singleSymbol(d) && this.style.fillopacity != null ? fillOpacity : 1))],
|
|
11956
13335
|
...(alignOf.active ? { getPixelOffset: alignOf } : {}),
|
|
11957
13336
|
sizeUnits: 'pixels',
|
|
11958
13337
|
billboard: true,
|
|
@@ -12361,7 +13740,18 @@ in vec4 vPieLineColor;
|
|
|
12361
13740
|
// unit.
|
|
12362
13741
|
// - draw order: flat's sort before drawing (by the size value, the
|
|
12363
13742
|
// sum or the first value; the biggest on top)
|
|
12364
|
-
//
|
|
13743
|
+
// - AGGREGATE / CATEGORICAL (_isAggregatedPie): a pie per item — the
|
|
13744
|
+
// records of a position, or of an aggregation grid cell —
|
|
13745
|
+
// (_pieAggregatedItems; flatPieRecord / flatPieAccumulate /
|
|
13746
|
+
// flatPieItemValues), a slice per category (CATEGORICAL, in its
|
|
13747
|
+
// category color) or per value field; its size the item's summed
|
|
13748
|
+
// size field (flat's nSize, every record of the item counted), the
|
|
13749
|
+
// theme's nMin / nMax / nMaxSize those of the items
|
|
13750
|
+
// (_pieAggregateStats); draw order by the item's size with a size
|
|
13751
|
+
// field or AGGREGATE|SUM; COUNT prints a part's record count;
|
|
13752
|
+
// CENTERVALUE the item's record count
|
|
13753
|
+
// Not drawn here (a console note at type parsing, PIE_INERT_FLAGS,
|
|
13754
|
+
// PIE_AGGREGATE_INERT_FLAGS):
|
|
12365
13755
|
// 3D, VOLUME, HEIGHT, STARBURST, FLOWER/RAYS, BOW, ZOOM, DIRECTION /
|
|
12366
13756
|
// DIRECTED, POLAR — the pie is drawn flat and plain instead. Nor
|
|
12367
13757
|
// style.clipparts, showparts, gridx (flat's several pies per item).
|
|
@@ -12375,15 +13765,20 @@ in vec4 vPieLineColor;
|
|
|
12375
13765
|
const st = this.style;
|
|
12376
13766
|
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
12377
13767
|
const has = re => re.test(t);
|
|
12378
|
-
const
|
|
12379
|
-
const
|
|
13768
|
+
const agg = this._isAggregatedPie();
|
|
13769
|
+
const cat = !!this._pieCategorical;
|
|
13770
|
+
const fields = cat ? null : (this._pieFields || String(this.binding.value || '').split('|'));
|
|
13771
|
+
const multi = !cat && fields.length > 1;
|
|
13772
|
+
// AGGREGATE / CATEGORICAL: the statistics of the items (sets
|
|
13773
|
+
// _valueMin / _valueMax), their biggest size flat's nMaxSize
|
|
13774
|
+
const stats = agg ? this._pieAggregateStats(zoom) : null;
|
|
12380
13775
|
const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(st.scale) || 1);
|
|
12381
13776
|
const sizePow = resolveSizePow(st, this.flags);
|
|
12382
13777
|
// flat tests /SIZE/: also NOSIZE and GRIDSIZE
|
|
12383
13778
|
const sizeFlag = has(/SIZE/) && !has(/NORMSIZE/);
|
|
12384
13779
|
const sizeLog = has(/SIZELOG/);
|
|
12385
13780
|
const nsv = styleNum(st.normalsizevalue);
|
|
12386
|
-
const maxSize = nsv || this._maxSizeValue;
|
|
13781
|
+
const maxSize = nsv || (stats ? stats.maxSize : this._maxSizeValue);
|
|
12387
13782
|
const range = nsv || ((this._valueMax || 0) - (this._valueMin || 0));
|
|
12388
13783
|
const nSizeOf = (sum, size) => {
|
|
12389
13784
|
const n = NORMAL_RADIUS_PX;
|
|
@@ -12398,14 +13793,17 @@ in vec4 vPieLineColor;
|
|
|
12398
13793
|
const colors = this.categoryColorsRgb || [];
|
|
12399
13794
|
const scheme = Array.isArray(st.colorscheme) ? st.colorscheme : [];
|
|
12400
13795
|
const noFill = this._partNoFill || new Set();
|
|
12401
|
-
const byClass = has(/CLASSES/) || !multi;
|
|
13796
|
+
const byClass = has(/CLASSES/) || (!cat && !multi);
|
|
13797
|
+
// flat (maptheme.js 20327-20336): a pie of one part (or CLASSES) takes
|
|
13798
|
+
// the color of the class its value falls in — CATEGORICAL: the class
|
|
13799
|
+
// whose number equals the value (sl.single: the item's only part)
|
|
12402
13800
|
const classColor = v => {
|
|
12403
13801
|
const parts = this.partsA || [];
|
|
12404
|
-
for (let x = 0; x < parts.length; x++) if (v < parts[x].max) return colors[x];
|
|
13802
|
+
for (let x = 0; x < parts.length; x++) if (cat ? v === parts[x].min : v < parts[x].max) return colors[x];
|
|
12405
13803
|
return null;
|
|
12406
13804
|
};
|
|
12407
13805
|
const sliceRgb = sl => (sl.complement ? (parseCssColor(String(st.nodatacolor || '')) || [0xee, 0xee, 0xee])
|
|
12408
|
-
: (byClass && classColor(sl.value)) || colors[sl.i] || [128, 128, 128]);
|
|
13806
|
+
: ((byClass || sl.single) && classColor(sl.value)) || colors[sl.i] || [128, 128, 128]);
|
|
12409
13807
|
const fo = styleNum(st.fillopacity), op = styleNum(st.opacity);
|
|
12410
13808
|
// flat sets the group's fill-opacity only for a truthy value
|
|
12411
13809
|
const fillAlpha = op > 0 ? op : fo > 0 ? fo : 1;
|
|
@@ -12415,13 +13813,39 @@ in vec4 vPieLineColor;
|
|
|
12415
13813
|
if (has(/WHITELINES/)) line = 'white';
|
|
12416
13814
|
const lc = st.linecolor != null && st.linecolor !== '' ? styleLineColor(st.linecolor) : null;
|
|
12417
13815
|
if (lc) line = lc;
|
|
12418
|
-
if (scheme.slice(0, fields.length).some(c => String(c).trim().toLowerCase() === 'none')) line = lc || 'black';
|
|
13816
|
+
if (!cat && scheme.slice(0, fields.length).some(c => String(c).trim().toLowerCase() === 'none')) line = lc || 'black';
|
|
12419
13817
|
const lineRgb = line === 'none' ? null : hexOrNamedToRgb(line);
|
|
12420
13818
|
const lineWidthOf = r => (lineRgb ? (styleNum(st.linewidth) || 0.1) * r / 15 : 0);
|
|
12421
13819
|
const lineRgba = lineRgb ? [...lineRgb.slice(0, 3), Math.round(255 * lineAlpha)] : [0, 0, 0, 0];
|
|
12422
13820
|
|
|
12423
13821
|
const charts = [];
|
|
12424
|
-
|
|
13822
|
+
if (agg) {
|
|
13823
|
+
// one pie per item (_pieAggregatedItems), its parts and size as flat
|
|
13824
|
+
// has them after aggregation (flatPieItemValues)
|
|
13825
|
+
for (const f of this._pieAggregatedItems(zoom, bbox)) {
|
|
13826
|
+
const c = f.geometry.coordinates;
|
|
13827
|
+
if (globeCenter && !isOnVisibleHemisphere(c[0], c[1], globeCenter)) continue;
|
|
13828
|
+
const pie = f.properties.pie;
|
|
13829
|
+
const { parts: values, size } = this._pieItemValues(pie);
|
|
13830
|
+
const layout = pieSliceLayout(values, t, { centerPart: st.centerpart, itemCount: pie.n });
|
|
13831
|
+
if (!layout) continue;
|
|
13832
|
+
if (cat) layout.slices.forEach(sl => { sl.single = values.length === 1; });
|
|
13833
|
+
const sum = values.reduce((a, v) => a + (isNaN(v) ? 0 : v), 0);
|
|
13834
|
+
const r = nSizeOf(sum, size) * unit;
|
|
13835
|
+
if (!(r > 0) || !isFinite(r)) continue;
|
|
13836
|
+
// flat's draw-order key (maptheme.js 16812-16860): with a size
|
|
13837
|
+
// field or AGGREGATE|SUM the item's size, else its first part
|
|
13838
|
+
const key = this.binding.size || (has(/AGGREGATE/) && has(/SUM/)) ? size : values[0];
|
|
13839
|
+
// the tooltip: a row per part (counts), its value the parts' sum
|
|
13840
|
+
// (flat: the sum of nValuesA), its records (theme.item.data, the
|
|
13841
|
+
// page's own {{field}}s from the first, as flat's data[0])
|
|
13842
|
+
const raws = f.properties.raws || (f.properties.raw ? [f.properties.raw] : []);
|
|
13843
|
+
const properties = { counts: values, total: sum, recordCounts: [pie.n], raws, firstRaw: raws[0] || null,
|
|
13844
|
+
titleRaw: f.properties.titleRaw || f.properties.raw || null };
|
|
13845
|
+
charts.push({ c, r, layout, size, count: pie.n, partCounts: pie.counts, nParts: values.length, key, properties });
|
|
13846
|
+
}
|
|
13847
|
+
}
|
|
13848
|
+
for (const f of agg ? [] : this._activeFeatures || this.features) {
|
|
12425
13849
|
const c = f.geometry && f.geometry.coordinates;
|
|
12426
13850
|
if (!c || typeof c[0] !== 'number') continue;
|
|
12427
13851
|
const [lng, lat] = c;
|
|
@@ -12442,16 +13866,19 @@ in vec4 vPieLineColor;
|
|
|
12442
13866
|
// 1-field pie its class (cat) like a range-classed bubble
|
|
12443
13867
|
const properties = multi ? { value: values[0], parts: values, raw }
|
|
12444
13868
|
: { value: values[0], cat: this.partsA ? this._resolvePartsClass(values[0]) : 0, raw };
|
|
12445
|
-
charts.push({ c, r, layout, size, key, properties });
|
|
13869
|
+
charts.push({ c, r, layout, size, nParts: values.length, key, properties });
|
|
12446
13870
|
}
|
|
12447
13871
|
// flat sorts the charts before drawing (maptheme.js 16774-16870),
|
|
12448
13872
|
// ascending — the last drawn on top —, SORT|UP descending; not with
|
|
12449
|
-
// NOSRT, NOSIZE (unless SORT) or when all values are equal
|
|
13873
|
+
// NOSRT, NOSIZE (unless SORT) or when all values are equal, nor a
|
|
13874
|
+
// CATEGORICAL pie without AGGREGATE, size field or SORT
|
|
12450
13875
|
if (((this._valueMin !== this._valueMax) || this.binding.size) && !has(/NOSRT/)
|
|
12451
|
-
&& (!has(/NOSIZE/) || has(/\bSORT\b/))
|
|
13876
|
+
&& (!has(/NOSIZE/) || has(/\bSORT\b/))
|
|
13877
|
+
&& (!has(/CATEGORICAL/) || has(/AGGREGATE/) || this.binding.size || has(/\bSORT\b/))) {
|
|
12452
13878
|
const down = has(/\bSORT\b/) && has(/\bUP\b/);
|
|
12453
13879
|
charts.sort((a, b) => (down ? b.key - a.key : a.key - b.key) || 0);
|
|
12454
13880
|
}
|
|
13881
|
+
applyFlatMaxCharts(charts, st);
|
|
12455
13882
|
// align / offsetx / offsety (flatChartAlignOffset) — flat's PIE
|
|
12456
13883
|
// ptNull is (0, r + 5 units); the chart's half size taken as r, as
|
|
12457
13884
|
// for BUBBLE (ASSUMPTION: not measured on flat for PIE)
|
|
@@ -12508,6 +13935,74 @@ in vec4 vPieLineColor;
|
|
|
12508
13935
|
return layers.concat(this._buildPieTextLayers(charts, zoom, liveZoom, unit, sliceRgb, has));
|
|
12509
13936
|
}
|
|
12510
13937
|
|
|
13938
|
+
// the items of an AGGREGATE / CATEGORICAL pie within bbox: its records
|
|
13939
|
+
// (_buildAggregationIndex) added up per item by aggregateOnGrid — with
|
|
13940
|
+
// AGGREGATE on the theme's aggregation grid (gridwidth, aggregation …,
|
|
13941
|
+
// see _ensureClusterIndices; none: per position), else per position
|
|
13942
|
+
// (flat sums a CATEGORICAL theme's records of one shape by category
|
|
13943
|
+
// without AGGREGATE too). Each item's properties.pie: flatPieAccumulate's.
|
|
13944
|
+
_pieAggregatedItems(zoom, bbox) {
|
|
13945
|
+
if (!this._featuresByCategory) return [];
|
|
13946
|
+
if (this.flags.has('AGGREGATE')) {
|
|
13947
|
+
this._ensureClusterIndices(zoom);
|
|
13948
|
+
return this._clusterIndices[0].getClusters(bbox, this._clusterUsesFixedZoom ? GRIDWIDTH_METERS_REFERENCE_ZOOM : zoom);
|
|
13949
|
+
}
|
|
13950
|
+
const feats = this._featuresByCategory[0];
|
|
13951
|
+
if (!this._pieExactIndex || this._pieExactIndex.features !== feats) {
|
|
13952
|
+
this._pieExactIndex = new GridAggregateIndex(null, this.flags, undefined, null).load(feats);
|
|
13953
|
+
}
|
|
13954
|
+
return this._pieExactIndex.getClusters(bbox, zoom);
|
|
13955
|
+
}
|
|
13956
|
+
|
|
13957
|
+
// an aggregated pie item's parts (a CATEGORICAL pie with style.values:
|
|
13958
|
+
// one per category) and size (flatPieItemValues)
|
|
13959
|
+
_pieItemValues(pie) {
|
|
13960
|
+
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
13961
|
+
const nParts = this._pieCategorical ? (this._pieExplicitValues ? this.categoryLabels.length : null) : (this._pieFields || []).length;
|
|
13962
|
+
return flatPieItemValues(pie, t, { categorical: !!this._pieCategorical, nParts });
|
|
13963
|
+
}
|
|
13964
|
+
|
|
13965
|
+
// flat's statistics of an aggregated pie theme (maptheme.js
|
|
13966
|
+
// 11180-11241), over every item (at this zoom when a grid aggregates):
|
|
13967
|
+
// nMin / nMax over the items' sizes (CATEGORICAL or a size field), else
|
|
13968
|
+
// over their parts; nMaxSize the biggest size; the classes of a
|
|
13969
|
+
// value-field pie (partsA) over the items' parts, as for a per-record
|
|
13970
|
+
// pie (_preparePie). Sets the theme's _valueMin / _valueMax / partsA.
|
|
13971
|
+
_pieAggregateStats(zoom) {
|
|
13972
|
+
const feats = this._featuresByCategory && this._featuresByCategory[0];
|
|
13973
|
+
const zkey = this.flags.has('AGGREGATE') ? zoom : null;
|
|
13974
|
+
const cache = this._pieStatsCache;
|
|
13975
|
+
if (cache && cache.feats === feats && cache.zoom === zkey) return cache;
|
|
13976
|
+
let nMin = Infinity, nMax = -Infinity, maxSize = -Infinity;
|
|
13977
|
+
const pooled = [];
|
|
13978
|
+
const bySize = !!this._pieCategorical || !!(this.binding.size && this.binding.size !== '$item$');
|
|
13979
|
+
for (const f of this._pieAggregatedItems(zoom, [-180, -90, 180, 90])) {
|
|
13980
|
+
const { parts, size } = this._pieItemValues(f.properties.pie);
|
|
13981
|
+
if (isFinite(size) && size > maxSize) maxSize = size;
|
|
13982
|
+
for (const v of parts) if (isFinite(v)) pooled.push(v);
|
|
13983
|
+
if (bySize) {
|
|
13984
|
+
if (isFinite(size)) { if (size < nMin) nMin = size; if (size > nMax) nMax = size; }
|
|
13985
|
+
} else {
|
|
13986
|
+
for (const v of parts) if (isFinite(v)) { if (v < nMin) nMin = v; if (v > nMax) nMax = v; }
|
|
13987
|
+
}
|
|
13988
|
+
}
|
|
13989
|
+
if (nMin <= nMax) {
|
|
13990
|
+
this._valueMin = nMin;
|
|
13991
|
+
this._valueMax = nMax;
|
|
13992
|
+
if (!this._pieCategorical && pooled.length) {
|
|
13993
|
+
const nParts = Math.trunc(styleNum(this.style.classes)) || colorSchemeClassCount(this.style.colorscheme) || DEFAULT_RANGE_CLASSES;
|
|
13994
|
+
this.partsA = flatRangeParts(pooled, nMin, nMax, nParts, this.flags, styleNum(this.style.rangecentervalue));
|
|
13995
|
+
// a 1-field pie's legend rows are these classes (in place, as
|
|
13996
|
+
// _ensureAggregateStats: the legend holds the array)
|
|
13997
|
+
if (!this._multiFields && Array.isArray(this.categoryLabels)) {
|
|
13998
|
+
this.categoryLabels.splice(0, this.categoryLabels.length, ...this.partsA.map(p => `${this._formatTooltipValue(p.min)} - ${this._formatTooltipValue(p.max)}`));
|
|
13999
|
+
}
|
|
14000
|
+
}
|
|
14001
|
+
}
|
|
14002
|
+
this._pieStatsCache = { feats, zoom: zkey, maxSize: maxSize > -Infinity ? maxSize : undefined };
|
|
14003
|
+
return this._pieStatsCache;
|
|
14004
|
+
}
|
|
14005
|
+
|
|
12511
14006
|
// the value texts of _buildPieLayers (see there)
|
|
12512
14007
|
_buildPieTextLayers(charts, zoom, liveZoom, unit, sliceRgb, has) {
|
|
12513
14008
|
const st = this.style;
|
|
@@ -12518,12 +14013,16 @@ in vec4 vPieLineColor;
|
|
|
12518
14013
|
const noBreaks = flatNoBreaks(this._valueMin, this._valueMax);
|
|
12519
14014
|
const dec = explicitValueDecimals(st);
|
|
12520
14015
|
const fmt = (v, d) => flatFormatValue(v, d, noBreaks ? 'ROUND|NOBREAKS' : 'ROUND');
|
|
12521
|
-
|
|
12522
|
-
|
|
14016
|
+
// COUNT on an aggregated pie: the part's record count (flat's nCountA)
|
|
14017
|
+
const countText = has(/COUNT/);
|
|
14018
|
+
const partText = (sl, ch) => {
|
|
14019
|
+
if (sl.complement) return Math.round(sl.percent * 10) / 10 + ' % ';
|
|
14020
|
+
const v = countText && ch && ch.partCounts ? ch.partCounts[sl.i] || 0 : sl.value;
|
|
14021
|
+
return fmt(v, dec != null ? dec : (v < 1 ? 2 : 0)) + partUnit;
|
|
14022
|
+
};
|
|
12523
14023
|
const valuesOn = has(/VALUES/) && !has(/STACKED/) && !valuesHiddenByScale(st, zoom);
|
|
12524
14024
|
const textOverride = st.valuecolor || st.textcolor;
|
|
12525
14025
|
const inline = [], boxed = [], centerTexts = [], lines = [];
|
|
12526
|
-
const nFields = (this._pieFields || []).length;
|
|
12527
14026
|
for (const ch of charts) {
|
|
12528
14027
|
const o = ch.off;
|
|
12529
14028
|
// CENTER: the center part's value (maptheme.js 20402-20410)
|
|
@@ -12533,10 +14032,10 @@ in vec4 vPieLineColor;
|
|
|
12533
14032
|
const fontSize = Math.min(ch.centerR * 0.8, ch.centerR * (3.3 / Math.max(1, text.length))) * valueScale;
|
|
12534
14033
|
const rgb = (this.categoryColorsRgb || [])[ch.layout.center.i] || [128, 128, 128];
|
|
12535
14034
|
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)) {
|
|
14035
|
+
} else if (!ch.layout.center && has(/CENTERVALUE/) && (ch.count || (ch.size !== undefined && !isNaN(ch.size)))) {
|
|
12537
14036
|
// flat prints String(nCount || nSize): a record's item has no
|
|
12538
|
-
// nCount, so its size value
|
|
12539
|
-
const text = String(ch.size);
|
|
14037
|
+
// nCount, so its size value; an aggregated item its record count
|
|
14038
|
+
const text = String(ch.count || ch.size);
|
|
12540
14039
|
const fontSize = Math.min(ch.r * 0.5, ch.r * (2 / Math.max(1, text.length)));
|
|
12541
14040
|
if (fontSize > VALUES_MIN_FONT_PX) {
|
|
12542
14041
|
centerTexts.push({ position: ch.c, text, fontSize, off: o, bold: false,
|
|
@@ -12544,20 +14043,20 @@ in vec4 vPieLineColor;
|
|
|
12544
14043
|
}
|
|
12545
14044
|
}
|
|
12546
14045
|
if (!valuesOn) continue;
|
|
12547
|
-
if (
|
|
14046
|
+
if (ch.nParts > 1 || has(/NOINLINETEXT/)) {
|
|
12548
14047
|
const fontSize = 5 * unit * valueScale;
|
|
12549
14048
|
if (fontSize <= VALUES_MIN_FONT_PX) continue;
|
|
12550
14049
|
for (const lab of pieValueLabelLayout(ch.layout.slices, ch.r, fontSize, unit, (this.partsA || []).length)) {
|
|
12551
14050
|
const bg = has(/VALUEBACKGROUND/) ? sliceRgb(lab.slice) : [255, 255, 255];
|
|
12552
14051
|
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,
|
|
14052
|
+
boxed.push({ position: ch.c, text: partText(lab.slice, ch), fontSize, anchor: lab.anchor,
|
|
12554
14053
|
// the text's baseline → its center (arial: 0.35 font above)
|
|
12555
14054
|
off: [o[0] + lab.x, o[1] + lab.y - 0.35 * fontSize], color: textRgb, bg: [...bg.slice(0, 3), 128] });
|
|
12556
14055
|
lab.segments.forEach(sg => lines.push({ c: ch.c, o, sg }));
|
|
12557
14056
|
}
|
|
12558
14057
|
} else if (ch.layout.slices.length) {
|
|
12559
14058
|
const sl = ch.layout.slices[ch.layout.slices.length - 1];
|
|
12560
|
-
const text = partText(sl);
|
|
14059
|
+
const text = partText(sl, ch);
|
|
12561
14060
|
const fontSize = Math.min(ch.r * 0.8, ch.r * (3.4 / Math.max(1, text.length)));
|
|
12562
14061
|
const color = textOverride ? hexOrNamedToRgb(textOverride) : flatChartTextRgb(sliceRgb(sl));
|
|
12563
14062
|
// baseline at −0.6 R + R / 2 + 0.45 font (maptheme.js 20434)
|
|
@@ -12611,6 +14110,594 @@ in vec4 vPieLineColor;
|
|
|
12611
14110
|
return layers;
|
|
12612
14111
|
}
|
|
12613
14112
|
|
|
14113
|
+
// CHART|BAR / BARS (_isBarChart) — flat's bar chart per item
|
|
14114
|
+
// (flatBarLayout, maptheme.js drawChart's BAR branch): its items and
|
|
14115
|
+
// theme statistics as a pie's (one per record, or AGGREGATE /
|
|
14116
|
+
// CATEGORICAL items via _pieAggregatedItems / _pieAggregateStats), a bar
|
|
14117
|
+
// per value field or category:
|
|
14118
|
+
// - size: SIZE by the size field (or the values' sum) with sizepow, the
|
|
14119
|
+
// width chart size / parts · 0.66, the length value · 30 u / nMax
|
|
14120
|
+
// (normalsizevalue, else the theme's max) · rangescale, with SIZE
|
|
14121
|
+
// and several parts / sizer (SIZEP4: sizer²), COMPRESS / EXPAND,
|
|
14122
|
+
// POINTER · 1.2
|
|
14123
|
+
// - HORZ: the bars to the right, the first on top; else upward side
|
|
14124
|
+
// by side; STACKED: one column; SORT, UP, CENTER, SPACED, THIN /
|
|
14125
|
+
// THICK, NOZERO, NONEGATIVE, ONLYNEGATIVE, LEFT
|
|
14126
|
+
// - colors: the part's (categories, fields), a single part or
|
|
14127
|
+
// SEQUENCE the value's class; outline linecolor (black) at
|
|
14128
|
+
// linewidth · 0.25 (0.2 u) and opacity 1 (0.1 without linewidth);
|
|
14129
|
+
// a negative bar fill 0.3, outlined in its color; fillopacity
|
|
14130
|
+
// - POINTER / ARROW (SMALLARROW drawn as ARROW): flat's pointer path,
|
|
14131
|
+
// negative values pointing the other way (fadenegative fill, the
|
|
14132
|
+
// outline lighter or darker), SIZE: its width by the value;
|
|
14133
|
+
// OFFSETMEAN / OFFSETMEDIAN / DEVIATION: the value's deviation from
|
|
14134
|
+
// the part's mean / median (in %) or in standard deviations
|
|
14135
|
+
// - VALUES: the value (valuedecimals, else 1 decimal with a max ≤ 1)
|
|
14136
|
+
// and units, POINTER a "+", STACKED its share; beyond the bar's end
|
|
14137
|
+
// (in a white box unless HORZ; VALUEBACKGROUND / CTEXT: the bar's
|
|
14138
|
+
// color), inside it when it fits (without SIZE, THIN, DTEXT …),
|
|
14139
|
+
// STACKED beside the column on leader lines; flat's values color
|
|
14140
|
+
// #798697 (textcolor), negative red (valuecolor); DTEXT and a sized
|
|
14141
|
+
// POINTER fade small values
|
|
14142
|
+
// - BOX / TITLE (flatChartBox): a box around chart and title, the
|
|
14143
|
+
// item's title above it — boxcolor (white) at boxopacity, bordercolor
|
|
14144
|
+
// (#bbbbbb) at 0.3, borderwidth · 0.2 · w / 30; title 5 u ·
|
|
14145
|
+
// textscale in textcolor (#888888) over a white halo
|
|
14146
|
+
// - draw order: flat's (_buildPieLayers); each chart's box under its
|
|
14147
|
+
// bars, a later chart over an earlier one
|
|
14148
|
+
// Not drawn (BAR_INERT_FLAGS, a console note): see flatBarLayout.
|
|
14149
|
+
// Known divergence: every value text lies above every bar (flat draws
|
|
14150
|
+
// them in the chart's own group); the leader lines are drawn in map
|
|
14151
|
+
// coordinates (approximate on the globe).
|
|
14152
|
+
_buildBarLayers(zoom, bbox, liveZoom = zoom, globeCenter = null) {
|
|
14153
|
+
const st = this.style;
|
|
14154
|
+
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
14155
|
+
const has = re => re.test(t);
|
|
14156
|
+
const agg = this._isAggregatedPie();
|
|
14157
|
+
const cat = !!this._pieCategorical;
|
|
14158
|
+
const fields = cat ? null : (this._pieFields || String(this.binding.value || '').split('|'));
|
|
14159
|
+
const stats = agg ? this._pieAggregateStats(zoom) : null;
|
|
14160
|
+
const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(st.scale) || 1);
|
|
14161
|
+
const sizeField = !!(this.binding.size && this.binding.size !== '$item$');
|
|
14162
|
+
const nsv = styleNum(st.normalsizevalue);
|
|
14163
|
+
const colors = this.categoryColorsRgb || [];
|
|
14164
|
+
const num = v => { const x = styleNum(v); return isNaN(x) ? undefined : x; };
|
|
14165
|
+
const measure = (text, font) => titleTextWidth(text, font);
|
|
14166
|
+
// the charts' values (and, for OFFSETMEAN …, every item's)
|
|
14167
|
+
const items = [];
|
|
14168
|
+
const itemValues = f => {
|
|
14169
|
+
if (agg) {
|
|
14170
|
+
const { parts, size } = this._pieItemValues(f.properties.pie);
|
|
14171
|
+
return { values: parts, size };
|
|
14172
|
+
}
|
|
14173
|
+
const raw = f.properties || {};
|
|
14174
|
+
return { values: fields.map(k => flatNumber(raw[k], this.flags)), size: sizeField ? parseFloat(raw[this.binding.size]) : undefined };
|
|
14175
|
+
};
|
|
14176
|
+
const all = agg ? this._pieAggregatedItems(zoom, [-180, -90, 180, 90]) : (this._activeFeatures || this.features);
|
|
14177
|
+
// flat's per-part mean, median (getMeanMedianQuantile) and standard
|
|
14178
|
+
// deviation over every item
|
|
14179
|
+
let means, medians, devs;
|
|
14180
|
+
if (has(/OFFSETMEAN|OFFSETMEDIAN|DEVIATION/)) {
|
|
14181
|
+
const cache = this._barStatsCache;
|
|
14182
|
+
if (cache && cache.all === all && (!agg || cache.zoom === zoom)) ({ means, medians, devs } = cache);
|
|
14183
|
+
else {
|
|
14184
|
+
const cols = [];
|
|
14185
|
+
for (const f of all) itemValues(f).values.forEach((v, k) => { (cols[k] ||= []).push(v); });
|
|
14186
|
+
means = cols.map(c => c.reduce((a, v) => a + v, 0) / c.length);
|
|
14187
|
+
medians = cols.map(c => c.slice().sort((a, b) => a - b)[Math.round(c.length / 2)]);
|
|
14188
|
+
devs = cols.map((c, k) => Math.sqrt(c.reduce((a, v) => a + (v - means[k]) * (v - means[k]), 0) / c.length));
|
|
14189
|
+
this._barStatsCache = { all, zoom, means, medians, devs };
|
|
14190
|
+
}
|
|
14191
|
+
}
|
|
14192
|
+
const lo = {
|
|
14193
|
+
themeMin: this._valueMin, themeMax: this._valueMax,
|
|
14194
|
+
maxSize: stats ? stats.maxSize : this._maxSizeValue, sizeField,
|
|
14195
|
+
normalSizeValue: nsv > 0 ? nsv : undefined, sizePow: resolveSizePow(st, this.flags),
|
|
14196
|
+
rangeScale: num(st.rangescale), valueScale: num(st.valuescale), units: st.units ? String(st.units) : '',
|
|
14197
|
+
valueDecimals: explicitValueDecimals(st), noBreaks: flatNoBreaks(this._valueMin, this._valueMax),
|
|
14198
|
+
gridX: num(st.gridx), nClasses: (this.partsA || []).length, classMax: (this.partsA || []).map(p => p.max),
|
|
14199
|
+
nColors: colors.length, fillOpacity: num(st.fillopacity), lineWidth: num(st.linewidth),
|
|
14200
|
+
fadeNegative: num(st.fadenegative), fadeValuePow: num(st.fadevaluepow),
|
|
14201
|
+
textColor: st.textcolor || undefined, valueColor: st.valuecolor || undefined,
|
|
14202
|
+
hideValues: valuesHiddenByScale(st, zoom), means, medians, devs, measure
|
|
14203
|
+
};
|
|
14204
|
+
const inView = c => c[0] >= bbox[0] && c[0] <= bbox[2] && c[1] >= bbox[1] && c[1] <= bbox[3]
|
|
14205
|
+
&& (!globeCenter || isOnVisibleHemisphere(c[0], c[1], globeCenter));
|
|
14206
|
+
const charts = [];
|
|
14207
|
+
for (const f of agg ? this._pieAggregatedItems(zoom, bbox) : all) {
|
|
14208
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
14209
|
+
if (!c || typeof c[0] !== 'number' || !inView(c)) continue;
|
|
14210
|
+
const { values, size } = itemValues(f);
|
|
14211
|
+
const layout = flatBarLayout(values, t, Object.assign({ itemSize: size }, lo));
|
|
14212
|
+
if (!layout || (!layout.bars.length && !layout.texts.length)) continue;
|
|
14213
|
+
const sum = values.reduce((a, v) => a + (isFinite(v) ? v : 0), 0);
|
|
14214
|
+
let properties;
|
|
14215
|
+
if (agg) {
|
|
14216
|
+
const pie = f.properties.pie;
|
|
14217
|
+
const raws = f.properties.raws || (f.properties.raw ? [f.properties.raw] : []);
|
|
14218
|
+
properties = { counts: values, total: sum, recordCounts: [pie.n], raws, firstRaw: raws[0] || null,
|
|
14219
|
+
titleRaw: f.properties.titleRaw || f.properties.raw || null };
|
|
14220
|
+
} else {
|
|
14221
|
+
const raw = f.properties || {};
|
|
14222
|
+
properties = values.length > 1 ? { value: values[0], parts: values, raw }
|
|
14223
|
+
: { value: values[0], cat: this.partsA ? this._resolvePartsClass(values[0]) : 0, raw };
|
|
14224
|
+
}
|
|
14225
|
+
// flat's draw-order key (as for pies, maptheme.js 16812-16860)
|
|
14226
|
+
const key = agg ? (this.binding.size || (has(/AGGREGATE/) && has(/SUM/)) ? size : values[0])
|
|
14227
|
+
: (this.binding.size ? size : has(/SIZE/) ? sum : values[0]);
|
|
14228
|
+
charts.push({ c, layout, key, properties });
|
|
14229
|
+
}
|
|
14230
|
+
if (((this._valueMin !== this._valueMax) || this.binding.size) && !has(/NOSRT/)
|
|
14231
|
+
&& (!has(/NOSIZE/) || has(/\bSORT\b/))
|
|
14232
|
+
&& (!has(/CATEGORICAL/) || has(/AGGREGATE/) || this.binding.size || has(/\bSORT\b/))) {
|
|
14233
|
+
const down = has(/\bSORT\b/) && has(/\bUP\b/);
|
|
14234
|
+
charts.sort((a, b) => (down ? b.key - a.key : a.key - b.key) || 0);
|
|
14235
|
+
}
|
|
14236
|
+
applyFlatMaxCharts(charts, st);
|
|
14237
|
+
|
|
14238
|
+
const boxShown = flatFlag(this.flags, 'BOX') && !boxHiddenByScale(st, liveZoom);
|
|
14239
|
+
const titleShown = boxShown && flatFlag(this.flags, 'TITLE') && !!this.binding.title;
|
|
14240
|
+
const titleFont = 5 * (num(st.textscale) || num(st.valuescale) || 1);
|
|
14241
|
+
const boxOpacity = isNaN(styleNum(st.boxopacity)) ? 1 : styleNum(st.boxopacity);
|
|
14242
|
+
const borderStyle = String(st.borderstyle || '');
|
|
14243
|
+
const boxFill = [...hexOrNamedToRgb(st.boxcolor || 'white').slice(0, 3), Math.round(255 * boxOpacity)];
|
|
14244
|
+
const boxLine = borderStyle === 'none' ? [0, 0, 0, 0]
|
|
14245
|
+
: [...hexOrNamedToRgb(styleLineColor(st.bordercolor) || '#bbbbbb').slice(0, 3), Math.round(255 * (/^(solid|dotted|dashed)$/.test(borderStyle) ? 1 : 0.3))];
|
|
14246
|
+
const borderWidth = num(st.borderwidth) || 1;
|
|
14247
|
+
const lc = st.linecolor != null && st.linecolor !== '' ? styleLineColor(st.linecolor) : null;
|
|
14248
|
+
const lineRgbOf = (spec, rgb) => (spec.rgb === 'color' ? rgb : spec.rgb === 'low' ? flatDerivateRgb(rgb, 0.7)
|
|
14249
|
+
: spec.rgb === 'high' ? flatDerivateRgb(rgb, 1.3) : (lc && lc !== 'none' ? hexOrNamedToRgb(lc) : [0, 0, 0]));
|
|
14250
|
+
const align = !!(st.align || styleNum(st.offsetx) || styleNum(st.offsety));
|
|
14251
|
+
|
|
14252
|
+
const shapes = [], texts = [], titles = [], lines = [];
|
|
14253
|
+
// a shape in chart units → the instance (px; the circle around it)
|
|
14254
|
+
const shape = (ch, s, fill, line, width, props, noPick) => {
|
|
14255
|
+
const L = s.len + s.band + s.head;
|
|
14256
|
+
const d = [[0, -1], [1, 0], [0, 1], [-1, 0]][s.dir];
|
|
14257
|
+
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];
|
|
14258
|
+
const lw = line[3] > 0 ? width * unit : 0;
|
|
14259
|
+
shapes.push({
|
|
14260
|
+
position: ch.c, off: [cx, -cy], properties: props, fill, line,
|
|
14261
|
+
r: Math.hypot(s.w2 * unit, L * unit / 2) + lw + 2,
|
|
14262
|
+
shape: [s.w2 * unit, s.sw * unit, s.len * unit, s.band * unit],
|
|
14263
|
+
shape2: [s.head * unit, s.dir + (noPick ? 4 : 0), lw, L * unit / 2]
|
|
14264
|
+
});
|
|
14265
|
+
};
|
|
14266
|
+
for (const ch of charts) {
|
|
14267
|
+
const L = ch.layout;
|
|
14268
|
+
let box = null;
|
|
14269
|
+
if (boxShown) {
|
|
14270
|
+
const title = titleShown ? this._itemTitle(ch.properties) : '';
|
|
14271
|
+
box = flatChartBox(L.extent, { margin: num(st.boxmargin) || 2, titleFont, title: title ? title + (st.titleunits || '') : '',
|
|
14272
|
+
bottomTitle: flatFlag(this.flags, 'BOTTOMTITLE'), alignRight: /right/.test(String(st.align || '')), measure });
|
|
14273
|
+
}
|
|
14274
|
+
const ext = box ? box.rect : L.extent;
|
|
14275
|
+
// align / offsetx / offsety (flatChartAlignOffset), the chart's
|
|
14276
|
+
// half height as its size (ASSUMPTION: not measured on flat for BAR)
|
|
14277
|
+
ch.off = align ? flatChartAlignOffset(st, { unit, symbolScale: 1, half: (ext[3] - ext[1]) / 2 * unit }) : [0, 0];
|
|
14278
|
+
if (box) {
|
|
14279
|
+
const r = box.rect;
|
|
14280
|
+
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 },
|
|
14281
|
+
boxFill, boxLine, borderWidth * 0.2 * box.width / 30, null, true);
|
|
14282
|
+
if (box.title) titles.push({ position: ch.c, text: box.title.lines.join('\n'), font: box.title.font * unit, anchor: box.title.anchor,
|
|
14283
|
+
off: [box.title.x * unit + ch.off[0], (box.title.baseline - 0.8 * box.title.font) * unit + ch.off[1]] });
|
|
14284
|
+
}
|
|
14285
|
+
for (const b of L.bars) {
|
|
14286
|
+
const rgb = colors[b.cls] || [128, 128, 128];
|
|
14287
|
+
const a = Math.round(255 * b.fill.opacity * b.opacity);
|
|
14288
|
+
const line = [...lineRgbOf(b.line, rgb).slice(0, 3), Math.round(255 * b.line.opacity * b.opacity)];
|
|
14289
|
+
shape(ch, b, [...rgb.slice(0, 3), a], line, b.line.width, ch.properties, false);
|
|
14290
|
+
}
|
|
14291
|
+
for (const tx of L.texts) {
|
|
14292
|
+
const font = tx.font * unit;
|
|
14293
|
+
if (font <= VALUES_MIN_FONT_PX) continue;
|
|
14294
|
+
const rgb = colors[tx.cls] || [128, 128, 128];
|
|
14295
|
+
const color = tx.color === 'value' ? [0x79, 0x86, 0x97] : tx.color === 'contrast' ? flatChartTextRgb(rgb) : hexOrNamedToRgb(tx.color);
|
|
14296
|
+
// the baseline's start → the text's middle-left (arial: 0.35 font
|
|
14297
|
+
// above the baseline), perpendicular to its reading direction
|
|
14298
|
+
const r = tx.angle * Math.PI / 180;
|
|
14299
|
+
const mx = tx.x * unit - 0.35 * font * Math.sin(r), my = tx.y * unit - 0.35 * font * Math.cos(r);
|
|
14300
|
+
texts.push({ position: ch.c, text: tx.text, font, angle: tx.angle, off: [mx + ch.off[0], my + ch.off[1]],
|
|
14301
|
+
color: [...color.slice(0, 3), Math.round(255 * Math.max(0, Math.min(1, tx.opacity)))],
|
|
14302
|
+
bg: tx.bg === 'bar' ? [...rgb.slice(0, 3), 128] : tx.bg === 'white' ? [254, 254, 255, 255] : null });
|
|
14303
|
+
}
|
|
14304
|
+
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] });
|
|
14305
|
+
}
|
|
14306
|
+
|
|
14307
|
+
const layers = [new BarShapeLayer({
|
|
14308
|
+
id: `ix-bar-${this.name}`,
|
|
14309
|
+
data: shapes, pickable: true,
|
|
14310
|
+
getPosition: d => d.position,
|
|
14311
|
+
getRadius: d => d.r,
|
|
14312
|
+
getPixelOffset: d => d.off,
|
|
14313
|
+
getFillColor: d => d.fill,
|
|
14314
|
+
getLineColor: d => d.line,
|
|
14315
|
+
getBarShape: d => d.shape,
|
|
14316
|
+
getBarShape2: d => d.shape2,
|
|
14317
|
+
radiusUnits: 'pixels', billboard: true, stroked: false, filled: true, antialiasing: true,
|
|
14318
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14319
|
+
})];
|
|
14320
|
+
if (lines.length) {
|
|
14321
|
+
const feature = d => ({ type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: [
|
|
14322
|
+
pixelOffsetLngLat(d.c, [d.o[0] + d.sg[0], d.o[1] + d.sg[1]], liveZoom),
|
|
14323
|
+
pixelOffsetLngLat(d.c, [d.o[0] + d.sg[2], d.o[1] + d.sg[3]], liveZoom)] } });
|
|
14324
|
+
const fc = { type: 'FeatureCollection', features: lines.map(feature) };
|
|
14325
|
+
[['bg', [255, 255, 255, Math.round(255 * 0.2)], 0.15], ['fg', [0x88, 0x88, 0x88, 255], 0.05]].forEach(([k, color, w]) => {
|
|
14326
|
+
layers.push(new GeoJsonLayer({
|
|
14327
|
+
id: `ix-bar-lines-${k}-${this.name}`, data: fc, pickable: false,
|
|
14328
|
+
stroked: true, filled: false, getLineColor: color, getLineWidth: w * unit, lineWidthUnits: 'pixels'
|
|
14329
|
+
}));
|
|
14330
|
+
});
|
|
14331
|
+
}
|
|
14332
|
+
const textBase = {
|
|
14333
|
+
getPosition: d => d.position, getText: d => d.text, getSize: d => d.font,
|
|
14334
|
+
getPixelOffset: d => d.off, getColor: d => d.color, getAngle: d => d.angle,
|
|
14335
|
+
sizeUnits: 'pixels', fontFamily: 'arial', characterSet: 'auto', fontWeight: 'normal',
|
|
14336
|
+
getTextAnchor: 'start', getAlignmentBaseline: 'center', pickable: false,
|
|
14337
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14338
|
+
};
|
|
14339
|
+
const boxed = texts.filter(d => d.bg), plain = texts.filter(d => !d.bg);
|
|
14340
|
+
if (plain.length) layers.push(new TextLayer(Object.assign({}, textBase, { id: `ix-bar-values-${this.name}`, data: plain })));
|
|
14341
|
+
if (boxed.length) {
|
|
14342
|
+
// flat's box (createTextLabel): 0.3 font left of the text, 0.4 font
|
|
14343
|
+
// right of it, 0.1 font taller
|
|
14344
|
+
layers.push(new TextLayer(Object.assign({}, textBase, {
|
|
14345
|
+
id: `ix-bar-values-boxed-${this.name}`, data: boxed,
|
|
14346
|
+
background: true, getBackgroundColor: d => d.bg,
|
|
14347
|
+
// one padding per layer: of the texts' mean font
|
|
14348
|
+
backgroundPadding: [0.3, 0, 0.4, 0.1].map(k => k * boxed.reduce((a, d) => a + d.font, 0) / boxed.length)
|
|
14349
|
+
})));
|
|
14350
|
+
}
|
|
14351
|
+
if (titles.length) {
|
|
14352
|
+
layers.push(new TextLayer({
|
|
14353
|
+
id: `ix-bar-titles-${this.name}`, data: titles, pickable: false,
|
|
14354
|
+
getPosition: d => d.position, getText: d => d.text, getSize: d => d.font,
|
|
14355
|
+
sizeUnits: 'pixels', fontFamily: 'arial', characterSet: 'auto', lineHeight: 1,
|
|
14356
|
+
getColor: [...hexOrNamedToRgb(st.textcolor || '#888888').slice(0, 3), 255],
|
|
14357
|
+
fontSettings: { sdf: true }, outlineWidth: 1 / 7, outlineColor: [255, 255, 255, 128],
|
|
14358
|
+
getTextAnchor: d => d.anchor, getAlignmentBaseline: 'top',
|
|
14359
|
+
getPixelOffset: d => d.off,
|
|
14360
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14361
|
+
}));
|
|
14362
|
+
}
|
|
14363
|
+
return layers;
|
|
14364
|
+
}
|
|
14365
|
+
|
|
14366
|
+
// CHART|VECTOR / CHART|BEZIER — the theme's classes as flat has them:
|
|
14367
|
+
// with CATEGORICAL (or EXACT) the categories, flat's string values
|
|
14368
|
+
// trimmed (getStringValueIndex, maptheme.js 9204), its partsA one
|
|
14369
|
+
// [k, k + 1e-9] per category (the last + 0.001) and the colorscheme's
|
|
14370
|
+
// colors per category; the items' statistics follow in _vectorItems
|
|
14371
|
+
_prepareVector() {
|
|
14372
|
+
const st = this.style;
|
|
14373
|
+
this._vectorCategorical = this.flags.has('CATEGORICAL') && !!this.binding.value;
|
|
14374
|
+
if (this._vectorCategorical) {
|
|
14375
|
+
const explicit = Array.isArray(st.values) ? st.values.map(v => String(v).trim()) : null;
|
|
14376
|
+
const index = new Map();
|
|
14377
|
+
if (explicit) explicit.forEach((v, i) => { if (!index.has(v)) index.set(v, i); });
|
|
14378
|
+
else {
|
|
14379
|
+
for (const f of this.features) {
|
|
14380
|
+
const v = f.properties[this.binding.value];
|
|
14381
|
+
if (v == null) continue;
|
|
14382
|
+
const k = String(v).trim();
|
|
14383
|
+
if (k !== '' && !index.has(k)) index.set(k, index.size);
|
|
14384
|
+
}
|
|
14385
|
+
this.categoryLabels = Array.from(index.keys());
|
|
14386
|
+
const explicitLabel = Array.isArray(st.label) ? st.label.map(String) : null;
|
|
14387
|
+
this.categoryDisplayLabels = explicitLabel && explicitLabel.length === this.categoryLabels.length ? explicitLabel : this.categoryLabels;
|
|
14388
|
+
this.categoryColorsRgb = resolveClassColors(st.colorscheme, this.categoryDisplayLabels, st.classes);
|
|
14389
|
+
}
|
|
14390
|
+
this._vectorCategoryIndex = index;
|
|
14391
|
+
// the legend counts records by their raw value
|
|
14392
|
+
const byLabel = new Map(index);
|
|
14393
|
+
for (const f of this.features) {
|
|
14394
|
+
const v = f.properties[this.binding.value];
|
|
14395
|
+
if (v != null && !byLabel.has(v) && index.has(String(v).trim())) byLabel.set(v, index.get(String(v).trim()));
|
|
14396
|
+
}
|
|
14397
|
+
this.categoryIndexByLabel = byLabel;
|
|
14398
|
+
const n = this.categoryLabels.length;
|
|
14399
|
+
this.partsA = this.categoryLabels.map((l, i) => ({ min: i + 1, max: i + 1 + (i === n - 1 ? 0.001 : 0) + 1e-9 }));
|
|
14400
|
+
this._rangeClassed = false;
|
|
14401
|
+
}
|
|
14402
|
+
// colorfield (flat 12529-12597, 12644-12652): an item's color is its
|
|
14403
|
+
// color value's — the colorvalues, else values, else the items'
|
|
14404
|
+
// values in order — n-th color of the colorscheme realized for n
|
|
14405
|
+
const colorField = st.colorfield || (this.binding && this.binding.color);
|
|
14406
|
+
this._vectorColorField = colorField || null;
|
|
14407
|
+
if (colorField) {
|
|
14408
|
+
const list = Array.isArray(st.colorvalues) ? st.colorvalues.map(String)
|
|
14409
|
+
: Array.isArray(st.values) ? st.values.map(String)
|
|
14410
|
+
: [...new Set(this.features.map(f => String(f.properties[colorField])))];
|
|
14411
|
+
const colors = resolveClassColors(st.colorscheme, list, st.classes) || [];
|
|
14412
|
+
this._vectorColorOf = new Map(list.map((v, i) => [v, colors[i]]));
|
|
14413
|
+
}
|
|
14414
|
+
this._vectorItemsOf = null;
|
|
14415
|
+
}
|
|
14416
|
+
|
|
14417
|
+
// the records of flatVectorItems from this theme's (active) features:
|
|
14418
|
+
// the second position joined by key (joinChartPositions: f._pos2) or
|
|
14419
|
+
// read from lat/lon fields ("lat|lon")
|
|
14420
|
+
_vectorRecords(features) {
|
|
14421
|
+
const b = this.binding;
|
|
14422
|
+
const l2 = b.lookup2 != null ? String(b.lookup2) : '';
|
|
14423
|
+
const latlon2 = l2.includes('|') ? l2.split('|') : null;
|
|
14424
|
+
const sizeField = b.size && b.size !== '$item$' ? b.size : null;
|
|
14425
|
+
const records = [];
|
|
14426
|
+
for (const f of features) {
|
|
14427
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
14428
|
+
if (!c || typeof c[0] !== 'number') continue;
|
|
14429
|
+
const p = f.properties || {};
|
|
14430
|
+
let p2 = null;
|
|
14431
|
+
if (latlon2) {
|
|
14432
|
+
const lat = flatScanValue(p[latlon2[0]]), lng = flatScanValue(p[latlon2[1]]);
|
|
14433
|
+
if (isFinite(lat) && isFinite(lng)) p2 = [lng, lat];
|
|
14434
|
+
} else if (f._pos2) p2 = f._pos2;
|
|
14435
|
+
let cat = null, value;
|
|
14436
|
+
if (this._vectorCategorical) {
|
|
14437
|
+
const v = p[b.value];
|
|
14438
|
+
const i = v == null ? undefined : this._vectorCategoryIndex.get(String(v).trim());
|
|
14439
|
+
cat = i == null ? -1 : i;
|
|
14440
|
+
} else if (b.value) {
|
|
14441
|
+
value = flatScanValue(p[b.value]);
|
|
14442
|
+
}
|
|
14443
|
+
records.push({ p1: c, p2, cat, value, size: sizeField ? flatScanValue(p[sizeField]) : 1, raw: p });
|
|
14444
|
+
}
|
|
14445
|
+
return records;
|
|
14446
|
+
}
|
|
14447
|
+
|
|
14448
|
+
// the items (flatVectorItems) of the active features, once per feature
|
|
14449
|
+
// set; also flat's statistics of them (maptheme.js 11180-11240): nMin /
|
|
14450
|
+
// nMax over the items' sizes (CATEGORICAL or a size field, else their
|
|
14451
|
+
// values), nMaxSize the biggest size
|
|
14452
|
+
_vectorItems() {
|
|
14453
|
+
const features = this._activeFeatures || this.features;
|
|
14454
|
+
if (this._vectorItemsOf === features) return this._vectorItemsCache;
|
|
14455
|
+
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
14456
|
+
const items = flatVectorItems(this._vectorRecords(features), {
|
|
14457
|
+
aggregate: this.flags.has('AGGREGATE'),
|
|
14458
|
+
vectorKey: /VECTOR/.test(t),
|
|
14459
|
+
multiParts: this.flags.has('CATEGORICAL'),
|
|
14460
|
+
max: /\bMAX\b/.test(t), min: /\bMIN\b/.test(t),
|
|
14461
|
+
zeroIsNotValue: /ZEROISNOTVALUE/.test(t), negativeIsNotValue: /NEGATIVEISNOTVALUE/.test(t)
|
|
14462
|
+
});
|
|
14463
|
+
let min = Infinity, max = -Infinity, maxSize = -Infinity;
|
|
14464
|
+
const bySize = this._vectorCategorical || !!this.binding.size;
|
|
14465
|
+
for (const it of items) {
|
|
14466
|
+
if (isFinite(it.nSize) && it.nSize > maxSize) maxSize = it.nSize;
|
|
14467
|
+
const v = bySize ? it.nSize : it.value;
|
|
14468
|
+
if (isFinite(v)) { if (v < min) min = v; if (v > max) max = v; }
|
|
14469
|
+
}
|
|
14470
|
+
if (min <= max) { this._valueMin = min; this._valueMax = max; }
|
|
14471
|
+
this._vectorMaxSize = maxSize;
|
|
14472
|
+
this._vectorItemsOf = features;
|
|
14473
|
+
this._vectorItemsCache = items;
|
|
14474
|
+
return items;
|
|
14475
|
+
}
|
|
14476
|
+
|
|
14477
|
+
// CHART|VECTOR / CHART|BEZIER — real ixmaps-flat's flow lines
|
|
14478
|
+
// (maptheme.js chartMap 18148-18435): one line per item
|
|
14479
|
+
// (_vectorItems) from its first to its second position — the
|
|
14480
|
+
// positions joined to FEATURE shapes by key (binding position /
|
|
14481
|
+
// position2, style lookupfield / lookupfield2) or read from lat/lon
|
|
14482
|
+
// fields. An item without a second position, or with a size under
|
|
14483
|
+
// style.minvalue, draws nothing; one is drawn when either end lies in
|
|
14484
|
+
// the view (flat 16590-16620).
|
|
14485
|
+
// - BEZIER (also VECTOR|BEZIER — the BEZIER branch comes first): a
|
|
14486
|
+
// cubic curve (bezierVectorLayout), width (linewidth, else 10 ·
|
|
14487
|
+
// (|nSize| / normalsizevalue or the biggest size)^(1/sizepow)) ·
|
|
14488
|
+
// unit px — unit: the object zoom × style.scale; a width ≤ 0 is not
|
|
14489
|
+
// drawn; stroke opacity fillopacity (else 0.3); a negative size or
|
|
14490
|
+
// REVERSE swaps the ends; RANDOM lowers the bow by up to 0.66 ·
|
|
14491
|
+
// rangescale (flat's Math.random, here a fixed hash of the item);
|
|
14492
|
+
// POINTER / ARROW an arrow head (arrowMarkerTriangle) at the
|
|
14493
|
+
// shortened end, its opacity 2 · fillopacity; GRADIENT / FADEIN a
|
|
14494
|
+
// gradient (fadeGradientStops) from the second color (linecolor's
|
|
14495
|
+
// first entry) at the start — FADEIN at 0.2 opacity — to the line
|
|
14496
|
+
// color at the end; DOPACITY (DOPACITYMAX) / DOPACITYMIN the line
|
|
14497
|
+
// and arrow opacity by the size within nMin … nMax (dopacitypow,
|
|
14498
|
+
// dopacityscale); DASH dashes of 50 / 5 + width units, round caps
|
|
14499
|
+
// - VECTOR alone: a straight line, width (linewidth, else 2 · nSize /
|
|
14500
|
+
// normalsizevalue / the biggest size, else 0.1) · the object zoom px
|
|
14501
|
+
// (no style.scale), opacity fillopacity (0.3); POINTER / ARROW a 4 ×
|
|
14502
|
+
// width arrow head with its tip on the second position; DASH dashes
|
|
14503
|
+
// of 50 / 15 units
|
|
14504
|
+
// - color: linecolor (its last entry), else the colorfield's color,
|
|
14505
|
+
// else the category's (CATEGORICAL) or the value's n-th color; a
|
|
14506
|
+
// record whose category is none of them is black (flat: an
|
|
14507
|
+
// "undefined" stroke — szNoDataColor is unset)
|
|
14508
|
+
// - draw order: flat's sort before drawing (16774-16880), the
|
|
14509
|
+
// biggest last (on top) when sized or AGGREGATE|SUM
|
|
14510
|
+
// - lines are densified in Web Mercator pixels (flat draws them on its
|
|
14511
|
+
// Mercator map), so under the globe projection they follow the
|
|
14512
|
+
// surface; their far-side parts are left out
|
|
14513
|
+
// Not drawn here (console note at type parsing, VECTOR_INERT_FLAGS):
|
|
14514
|
+
// VALUES (text along the curve), DYNAMICWIDTH, LONG, the DASH
|
|
14515
|
+
// animation; nor the second position from the category label (flat's
|
|
14516
|
+
// fallback without position2), a grid aggregation (gridwidth), the
|
|
14517
|
+
// VECTOR arrow's thin white outline, the legend's class marking.
|
|
14518
|
+
// Known divergence: all arrow heads lie above all lines (flat draws
|
|
14519
|
+
// each line with its head); a line thinner than a pixel is drawn one
|
|
14520
|
+
// pixel wide at its width as opacity (SVG's coverage).
|
|
14521
|
+
_buildVectorLayers(zoom, bbox, liveZoom = zoom, globeCenter = null) {
|
|
14522
|
+
const st = this.style;
|
|
14523
|
+
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
14524
|
+
const has = re => re.test(t);
|
|
14525
|
+
const bezier = has(/\bBEZIER\b/);
|
|
14526
|
+
if (!this.binding.lookup2) {
|
|
14527
|
+
if (!this._warnedNoPos2) {
|
|
14528
|
+
this._warnedNoPos2 = true;
|
|
14529
|
+
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`);
|
|
14530
|
+
}
|
|
14531
|
+
return [];
|
|
14532
|
+
}
|
|
14533
|
+
const items = this._vectorItems();
|
|
14534
|
+
const oz = objectZoomFactor(liveZoom, this.mapOptions);
|
|
14535
|
+
const scale = styleNum(st.scale) || 1;
|
|
14536
|
+
const unit = oz * scale;
|
|
14537
|
+
const world = 512 * Math.pow(2, liveZoom);
|
|
14538
|
+
const toPx = ([lng, lat]) => {
|
|
14539
|
+
const s = Math.sin(Math.max(-85.0511, Math.min(85.0511, lat)) * Math.PI / 180);
|
|
14540
|
+
return [(lng + 180) / 360 * world, (0.5 - Math.log((1 + s) / (1 - s)) / (4 * Math.PI)) * world];
|
|
14541
|
+
};
|
|
14542
|
+
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];
|
|
14543
|
+
const inBox = c => c && c[0] >= bbox[0] && c[0] <= bbox[2] && c[1] >= bbox[1] && c[1] <= bbox[3];
|
|
14544
|
+
|
|
14545
|
+
const sizePow = resolveSizePow(st, this.flags);
|
|
14546
|
+
const nsv = styleNum(st.normalsizevalue);
|
|
14547
|
+
const maxSize = this._vectorMaxSize;
|
|
14548
|
+
// style.linewidth as flat holds it: the raw style value — flat's
|
|
14549
|
+
// `nLineWidth + 3` (the arrow length) joins a string ("0.6" + 3 =
|
|
14550
|
+
// "0.63") but adds a number
|
|
14551
|
+
const rawLw = this._definition && this._definition.style ? this._definition.style.linewidth : st.linewidth;
|
|
14552
|
+
const lwNum = styleNum(st.linewidth);
|
|
14553
|
+
const markerSize = styleNum(st.markersize) || 1;
|
|
14554
|
+
const gapSize = styleNum(st.gapsize) || 20;
|
|
14555
|
+
const fo = styleNum(st.fillopacity);
|
|
14556
|
+
const groupAlpha = styleNum(st.opacity) > 0 ? Math.min(1, styleNum(st.opacity)) : 1;
|
|
14557
|
+
const minValue = styleNum(st.minvalue);
|
|
14558
|
+
const rs = styleNum(st.rangescale);
|
|
14559
|
+
const dopMin = has(/DOPACITYMIN/), dop = !dopMin && has(/DOPACITY/);
|
|
14560
|
+
const dopPow = styleNum(st.dopacitypow) || 1, dopScale = styleNum(st.dopacityscale) || 1;
|
|
14561
|
+
const nMin = this._valueMin, nMax = this._valueMax;
|
|
14562
|
+
const lineColors = st.linecolor != null && st.linecolor !== '' ? flatToArray(st.linecolor) : null;
|
|
14563
|
+
const lineRgb = lineColors ? hexOrNamedToRgb(String(lineColors[lineColors.length - 1]).trim()) : null;
|
|
14564
|
+
const colors = this.categoryColorsRgb || [];
|
|
14565
|
+
const colorOf = it => {
|
|
14566
|
+
if (lineRgb) return lineRgb;
|
|
14567
|
+
if (this._vectorColorField) {
|
|
14568
|
+
const c = this._vectorColorOf.get(String(it.raw[this._vectorColorField]));
|
|
14569
|
+
if (c) return c;
|
|
14570
|
+
}
|
|
14571
|
+
if (it.cat != null) return it.cat >= 0 && colors[it.cat] ? colors[it.cat] : [0, 0, 0];
|
|
14572
|
+
return Number.isInteger(it.value) && colors[it.value - 1] ? colors[it.value - 1] : [0, 0, 0];
|
|
14573
|
+
};
|
|
14574
|
+
const fade = has(/\bFADEIN\b/) ? 0.2 : 2;
|
|
14575
|
+
const gradient = bezier && has(/\bGRADIENT\b|\bFADEIN\b/);
|
|
14576
|
+
const arrow = has(/\bPOINTER|ARROW\b/);
|
|
14577
|
+
const dash = has(/\bDASH\b/);
|
|
14578
|
+
const dopacity = it => {
|
|
14579
|
+
const range = Math.pow(nMax - nMin, 1 / dopPow);
|
|
14580
|
+
return Math.pow(dopMin ? nMax - it.nSize : it.nSize - nMin, 1 / dopPow) / range * (fo || 1) * dopScale;
|
|
14581
|
+
};
|
|
14582
|
+
|
|
14583
|
+
// flat's sort before drawing (maptheme.js 16774-16880)
|
|
14584
|
+
let order = items;
|
|
14585
|
+
const sized = !!this.binding.size;
|
|
14586
|
+
if (((nMin !== nMax) || sized) && !has(/NOSRT/) && (!has(/NOSIZE/) || has(/\bSORT\b/))
|
|
14587
|
+
&& (!this.flags.has('CATEGORICAL') || this.flags.has('AGGREGATE') || sized || has(/\bSORT\b/))) {
|
|
14588
|
+
const key = (sized || (this.flags.has('AGGREGATE') && has(/SUM/))) ? (it => it.nSize)
|
|
14589
|
+
: (this._vectorCategorical ? (it => (it.cat != null && it.cat >= 0 ? it.cat + 1 : 0)) : (it => it.value));
|
|
14590
|
+
const keyed = items.map(it => ({ it, y: key(it) })).filter(d => !isNaN(d.y));
|
|
14591
|
+
const down = has(/\bSORT\b/) && has(/\bUP\b/);
|
|
14592
|
+
keyed.sort((a, b) => (down ? b.y - a.y : a.y - b.y));
|
|
14593
|
+
order = keyed.map(d => d.it);
|
|
14594
|
+
}
|
|
14595
|
+
// the flows that would be drawn (as the loop below draws them), of
|
|
14596
|
+
// which flat keeps the biggest maxcharts
|
|
14597
|
+
if (Math.round(styleNum(st.maxcharts)) > 0) {
|
|
14598
|
+
order = applyFlatMaxCharts(order.filter(it => it.p2 && !(minValue && it.nSize < minValue) && (inBox(it.p1) || inBox(it.p2))), st);
|
|
14599
|
+
}
|
|
14600
|
+
|
|
14601
|
+
const paths = [], heads = [];
|
|
14602
|
+
const N = 32;
|
|
14603
|
+
const visible = c => !globeCenter || isOnVisibleHemisphere(c[0], c[1], globeCenter);
|
|
14604
|
+
for (const it of order) {
|
|
14605
|
+
let p1 = it.p1, p2 = it.p2;
|
|
14606
|
+
if (!p2) continue;
|
|
14607
|
+
if (minValue && it.nSize < minValue) continue;
|
|
14608
|
+
if (!inBox(p1) && !inBox(p2)) continue;
|
|
14609
|
+
const rgb = colorOf(it);
|
|
14610
|
+
let pts, end, tan, sw, headS, refX, refY, headAlpha, lineAlpha, stops = null, dashArr = null;
|
|
14611
|
+
if (bezier) {
|
|
14612
|
+
if (p1[0] === p2[0] && p1[1] === p2[1]) continue;
|
|
14613
|
+
const w = rawLw != null && rawLw !== '' && lwNum ? lwNum
|
|
14614
|
+
: 10 / Math.pow(nsv || maxSize, 1 / sizePow) * Math.pow(Math.abs(it.nSize), 1 / sizePow);
|
|
14615
|
+
sw = w * unit;
|
|
14616
|
+
if (!(sw > 0)) continue;
|
|
14617
|
+
if (has(/\bREVERSE\b/) || it.nSize < 0) [p1, p2] = [p2, p1];
|
|
14618
|
+
const a = toPx(p1), b = toPx(p2);
|
|
14619
|
+
const dx = b[0] - a[0], dy = b[1] - a[1];
|
|
14620
|
+
if (!(dx * dx + dy * dy > 0)) continue;
|
|
14621
|
+
let bow = rs;
|
|
14622
|
+
if (has(/\bRANDOM\b/)) bow -= rs * 0.66 * hashUnit(p1[0] + ',' + p1[1] + '&' + p2[0] + ',' + p2[1]);
|
|
14623
|
+
const gapAdd = has(/\bGAP\b/) ? gapSize : 3;
|
|
14624
|
+
const ll = typeof rawLw === 'string' && lwNum ? Number(rawLw + String(gapAdd)) : w + gapAdd;
|
|
14625
|
+
const lay = bezierVectorLayout(dx, dy, { w, ll, unit, oz, bow, t, marker: markerSize, gap: gapSize });
|
|
14626
|
+
pts = cubicBezierPoints(lay.start, lay.c1, lay.c2, lay.end, N);
|
|
14627
|
+
end = lay.end;
|
|
14628
|
+
tan = [lay.end[0] - lay.c2[0], lay.end[1] - lay.c2[1]];
|
|
14629
|
+
headS = Math.min(5, 2.5 + 5 / (w * scale)) * markerSize;
|
|
14630
|
+
refX = headS / 1.7; refY = headS / 2;
|
|
14631
|
+
lineAlpha = fo || 0.3;
|
|
14632
|
+
headAlpha = Math.min(1, (fo || 0.3) * 2);
|
|
14633
|
+
// (flat sets the computed opacity as is; one that is no number
|
|
14634
|
+
// — every size equal — SVG ignores: the opacity stays)
|
|
14635
|
+
if (dop || dopMin) { const d = dopacity(it); if (isFinite(d)) { lineAlpha = d; headAlpha = d; } }
|
|
14636
|
+
if (gradient) stops = fadeGradientStops(pts, lay.vx, lay.vy, fade);
|
|
14637
|
+
if (dash) dashArr = [50 * oz, 5 * oz + sw];
|
|
14638
|
+
pts = pts.map(p => [p[0] + a[0], p[1] + a[1]]);
|
|
14639
|
+
end = [end[0] + a[0], end[1] + a[1]];
|
|
14640
|
+
} else {
|
|
14641
|
+
const w = (rawLw != null && rawLw !== '' && lwNum) || (2 / (nsv || 1) / maxSize * it.nSize) || 0.1;
|
|
14642
|
+
sw = w * oz;
|
|
14643
|
+
const a = toPx(p1), b = toPx(p2);
|
|
14644
|
+
pts = [];
|
|
14645
|
+
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]);
|
|
14646
|
+
end = b;
|
|
14647
|
+
tan = [b[0] - a[0], b[1] - a[1]];
|
|
14648
|
+
headS = 4; refX = 4; refY = 2;
|
|
14649
|
+
lineAlpha = fo || 0.3;
|
|
14650
|
+
headAlpha = Math.min(1, (fo || 0.3) * 2);
|
|
14651
|
+
if (dash) dashArr = [50 * oz, 15 * oz];
|
|
14652
|
+
}
|
|
14653
|
+
// a line under a pixel: one pixel at its width as opacity
|
|
14654
|
+
const drawW = Math.max(1, sw);
|
|
14655
|
+
const thin = Math.min(1, sw);
|
|
14656
|
+
const color2 = lineColors && lineColors.length > 1 ? hexOrNamedToRgb(String(lineColors[0]).trim()) : rgb;
|
|
14657
|
+
const vertexColor = i => {
|
|
14658
|
+
const s = stops ? stops[i] : null;
|
|
14659
|
+
const k = s ? s.k : 1;
|
|
14660
|
+
const c = s ? [0, 1, 2].map(j => Math.round(color2[j] + (rgb[j] - color2[j]) * k)) : rgb.slice(0, 3);
|
|
14661
|
+
return [...c, Math.round(255 * Math.min(1, lineAlpha) * (s ? s.alpha : 1) * thin * groupAlpha)];
|
|
14662
|
+
};
|
|
14663
|
+
const properties = { value: it.nSize, cat: it.cat, raw: it.raw, count: it.count,
|
|
14664
|
+
vector: { itemValue: it.cat != null ? (it.cat >= 0 ? it.cat + 1 : 0) : it.value } };
|
|
14665
|
+
// the line in map coordinates; on the globe only its visible runs
|
|
14666
|
+
const ll = pts.map(toLngLat);
|
|
14667
|
+
let run = [], runColors = [];
|
|
14668
|
+
const flush = () => {
|
|
14669
|
+
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 });
|
|
14670
|
+
run = []; runColors = [];
|
|
14671
|
+
};
|
|
14672
|
+
ll.forEach((c, i) => { if (visible(c)) { run.push(c); runColors.push(vertexColor(i)); } else flush(); });
|
|
14673
|
+
flush();
|
|
14674
|
+
if (arrow && visible(toLngLat(end))) {
|
|
14675
|
+
const tri = arrowMarkerTriangle(end, tan[0], tan[1], sw, headS, refX, refY).map(toLngLat);
|
|
14676
|
+
heads.push({ type: 'Feature', properties, geometry: { type: 'Polygon', coordinates: [[...tri, tri[0]]] },
|
|
14677
|
+
fill: [...rgb.slice(0, 3), Math.round(255 * Math.min(1, headAlpha) * groupAlpha)] });
|
|
14678
|
+
}
|
|
14679
|
+
}
|
|
14680
|
+
const layers = [new PathLayer({
|
|
14681
|
+
id: `ix-vector-${this.name}`, data: paths, pickable: true,
|
|
14682
|
+
getPath: d => d.path, getColor: d => d.colors, getWidth: d => d.width,
|
|
14683
|
+
widthUnits: 'pixels', capRounded: dash && bezier, jointRounded: false, billboard: false,
|
|
14684
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS,
|
|
14685
|
+
...(dash ? { getDashArray: d => d.dash, extensions: [new PathStyleExtension({ dash: true })] } : {})
|
|
14686
|
+
})];
|
|
14687
|
+
if (heads.length) {
|
|
14688
|
+
// the head count in the id: deck kept a same-id polygon layer's
|
|
14689
|
+
// larger buffers when a filter cut the heads (the Zeit page's "to
|
|
14690
|
+
// Africa": bow-tie triangles from the unfiltered heads' vertices).
|
|
14691
|
+
// Every head has the same vertex count, so an equal count — a pan —
|
|
14692
|
+
// reuses the layer; a changed one gets fresh buffers
|
|
14693
|
+
layers.push(new GeoJsonLayer({
|
|
14694
|
+
id: `ix-vector-heads-${this.name}-n${heads.length}`, data: { type: 'FeatureCollection', features: heads }, pickable: true,
|
|
14695
|
+
filled: true, stroked: false, getFillColor: d => d.fill, parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14696
|
+
}));
|
|
14697
|
+
}
|
|
14698
|
+
return layers;
|
|
14699
|
+
}
|
|
14700
|
+
|
|
12614
14701
|
// flat's chart position for this build (flatChartAlignOffset): a
|
|
12615
14702
|
// function item → pixel offset, the same for every part of one chart
|
|
12616
14703
|
// (symbol, glow, shadow, box, title, value text). unit is flat's
|
|
@@ -12926,7 +15013,8 @@ in vec4 vPieLineColor;
|
|
|
12926
15013
|
}
|
|
12927
15014
|
|
|
12928
15015
|
// pure: oneHot → see the pure-function sections above the class
|
|
12929
|
-
|
|
15016
|
+
// CATEGORICAL without a value binding (count per aggregation cell) has no categoryLabels: one bucket
|
|
15017
|
+
_oneHot(cat, value) { return oneHot(cat, value, this.categoryLabels ? this.categoryLabels.length : 1); }
|
|
12930
15018
|
|
|
12931
15019
|
// pure: groupCoLocated → see the pure-function sections above the class
|
|
12932
15020
|
_groupCoLocated(clusterFeatures, zoom) { return groupCoLocated(clusterFeatures, zoom, this._clusterRadiusPx, this.categoryLabels ? this.categoryLabels.length : 1, this.flags); }
|
|
@@ -13152,7 +15240,7 @@ in vec4 vPieLineColor;
|
|
|
13152
15240
|
show() { mapHandle(this.szMap).setThemeVisible(this.szTheme, true); return this; },
|
|
13153
15241
|
hide() { mapHandle(this.szMap).setThemeVisible(this.szTheme, false); return this; },
|
|
13154
15242
|
toggle() {
|
|
13155
|
-
const rt =
|
|
15243
|
+
const rt = registryTheme(this.szTheme);
|
|
13156
15244
|
mapHandle(this.szMap).setThemeVisible(this.szTheme, rt ? !!rt._hidden : false);
|
|
13157
15245
|
return this;
|
|
13158
15246
|
},
|
|
@@ -13319,7 +15407,7 @@ in vec4 vPieLineColor;
|
|
|
13319
15407
|
// no id: flat's current theme — here the one a page's facets were
|
|
13320
15408
|
// built for (its own facet script sets ixmaps.filterThemeId), else the
|
|
13321
15409
|
// last chart / choropleth theme
|
|
13322
|
-
let rt = szId != null ?
|
|
15410
|
+
let rt = szId != null ? registryTheme(szId) : null;
|
|
13323
15411
|
if (szId == null) {
|
|
13324
15412
|
const pid = pageIxmaps() && pageIxmaps().filterThemeId;
|
|
13325
15413
|
rt = (pid != null && _globalThemeRegistry.get(pid))
|
|
@@ -13410,7 +15498,7 @@ in vec4 vPieLineColor;
|
|
|
13410
15498
|
// flat's ixmaps.getThemeDefinitionObj(szId): the theme's definition —
|
|
13411
15499
|
// type, style (sizefield/valuefield as flat names them), meta
|
|
13412
15500
|
function getThemeDefinitionObj(szId) {
|
|
13413
|
-
const rt =
|
|
15501
|
+
const rt = registryTheme(szId);
|
|
13414
15502
|
if (!rt) return null;
|
|
13415
15503
|
const style = Object.assign({}, rt.style);
|
|
13416
15504
|
if (rt.binding.size && !style.sizefield) style.sizefield = rt.binding.size;
|
|
@@ -13456,11 +15544,11 @@ in vec4 vPieLineColor;
|
|
|
13456
15544
|
// before that block ran (there is none, currently) would just no-op
|
|
13457
15545
|
// here instead of redrawing.
|
|
13458
15546
|
function markThemeClass(szId, index) {
|
|
13459
|
-
const rt =
|
|
15547
|
+
const rt = registryTheme(szId);
|
|
13460
15548
|
if (rt) markRuntimeClass(rt, index);
|
|
13461
15549
|
}
|
|
13462
15550
|
function unmarkThemeClass(szId, index) {
|
|
13463
|
-
const rt =
|
|
15551
|
+
const rt = registryTheme(szId);
|
|
13464
15552
|
if (rt) unmarkRuntimeClass(rt, index);
|
|
13465
15553
|
}
|
|
13466
15554
|
// the legend's own rows mark their theme directly: its .layer() name
|
|
@@ -13755,11 +15843,12 @@ in vec4 vPieLineColor;
|
|
|
13755
15843
|
const map = _lastMapApi.map;
|
|
13756
15844
|
const center = map.getCenter();
|
|
13757
15845
|
const proj = (typeof map.getProjection === 'function' && map.getProjection()) || { type: 'mercator' };
|
|
15846
|
+
const svgProjection = mapSvgProjection(map);
|
|
13758
15847
|
return JSON.stringify({
|
|
13759
15848
|
map: {
|
|
13760
|
-
map: proj.type === 'globe' ? 'maps/svg/maps/generic/orthographic.svg' : 'maps/svg/maps/generic/mercator.svg',
|
|
15849
|
+
map: proj.type === 'globe' ? 'maps/svg/maps/generic/orthographic.svg' : svgProjection ? svgProjection.svg : 'maps/svg/maps/generic/mercator.svg',
|
|
13761
15850
|
center: { lat: center.lat, lng: center.lng },
|
|
13762
|
-
zoom:
|
|
15851
|
+
zoom: flatViewZoomOf(map)
|
|
13763
15852
|
}
|
|
13764
15853
|
});
|
|
13765
15854
|
}
|
|
@@ -13830,6 +15919,13 @@ in vec4 vPieLineColor;
|
|
|
13830
15919
|
htmlgui_getAttributionString: () => (_lastMapApi && _lastMapApi.getAttribution ? _lastMapApi.getAttribution() : ''),
|
|
13831
15920
|
changeThemeStyle: compatChangeThemeStyle,
|
|
13832
15921
|
removeTheme: compatRemoveTheme,
|
|
15922
|
+
// flat's ixmaps.setLocalString(orig, local) (htmlgui.js 3814 →
|
|
15923
|
+
// Api.setLocalString → Dictionary.replace): a page-wide translation,
|
|
15924
|
+
// e.g. the migration page's ISO code → country name; usable before
|
|
15925
|
+
// the map exists. getLocalString: its translation (flat's never
|
|
15926
|
+
// returns it — a missing return there)
|
|
15927
|
+
setLocalString: (szOrig, szLocal) => { _localStrings[szOrig] = szLocal; },
|
|
15928
|
+
getLocalString: (szOrig) => makeLocalText([])(szOrig),
|
|
13833
15929
|
// flat's ixmaps.newTheme(title, theme, flag) — on the last built map
|
|
13834
15930
|
newTheme: (title, theme, flag) => _mapHandle.newTheme(title, theme, flag),
|
|
13835
15931
|
// flat's time slider: show the records of a theme (null = all themes) whose timefield lies in [min, max)
|
|
@@ -13858,16 +15954,16 @@ in vec4 vPieLineColor;
|
|
|
13858
15954
|
// test-only: lets test/unit/*.test.mjs call pure internals directly (the
|
|
13859
15955
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
13860
15956
|
global.__ixmapsGlInternals = {
|
|
13861
|
-
IXMAPS_GL_VERSION, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout,
|
|
15957
|
+
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
15958
|
normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
13863
15959
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
13864
15960
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|
|
13865
15961
|
computeAlphaStats, computeMultiFieldClasses, computeDominantStats, resolveDominantClass, computeComposeColorStats,
|
|
13866
15962
|
resolveComposedColor, computeRangeClasses, colorSchemeClassCount, resolveDopacityAlpha,
|
|
13867
15963
|
flatColorSweep, applyClassesToColorScheme, resolveClassColors, flatOutlierStats, parseCssColor, flatLegendLook,
|
|
13868
|
-
flatToMapLibreZoom, mapLibreToFlatZoom, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
|
|
15964
|
+
flatToMapLibreZoom, mapLibreToFlatZoom, flatViewToMapLibreZoom, mapLibreToFlatViewZoom, flatSvgProjectionOf, flatSvgProjectionByOptions, normalSizeScaleOf, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
|
|
13869
15965
|
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,
|
|
15966
|
+
applyWhereFilter, joinChartPositions, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatBubbleLook, bubbleIconLook, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
13871
15967
|
drawSymbolPath, normalizeSymbolShape,
|
|
13872
15968
|
flatFormatValue, themeStyleArgs, flatChangedStyleValue, explicitValueDecimals, legendDecimals,
|
|
13873
15969
|
dataCacheKey, dataCacheDisabled, cachedLayerData, featuresBounds, wantsZoomToExtent, legendIsOn,
|