ixmaps-gl 0.2.11 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/ixmaps-gl.js +701 -134
- package/package.json +1 -1
package/ixmaps-gl.js
CHANGED
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
// alpha100}) when set (alpha100:"$density$" divides by the polygon's
|
|
22
22
|
// own geodesic area — see _prepareAlphaField/_resolveDopacityAlpha).
|
|
23
23
|
// AGGREGATE CATEGORICAL on one field paints each polygon in its records'
|
|
24
|
-
// dominant category; a plain CATEGORICAL choropleth
|
|
25
|
-
// see _buildChoroplethLayers), and the CHART|SYMBOL|
|
|
24
|
+
// dominant category; a plain CATEGORICAL choropleth matches each polygon's
|
|
25
|
+
// raw value against the categories, see _buildChoroplethLayers), and the CHART|SYMBOL|
|
|
26
26
|
// GLOW|CATEGORICAL|AGGREGATE|COUNT|RELOCATE|VALUES pipeline (categorical
|
|
27
27
|
// clustering + sizing + glow + multi-bubble grouping + on-bubble value
|
|
28
28
|
// labels, generalized to however many distinct category values the DATA
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
// ---------------------------------------------------------------
|
|
83
83
|
// this engine's release (= package.json "version", checked by
|
|
84
84
|
// test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
|
|
85
|
-
const IXMAPS_GL_VERSION = '0.2.
|
|
85
|
+
const IXMAPS_GL_VERSION = '0.2.14';
|
|
86
86
|
|
|
87
87
|
const LIB_URLS = {
|
|
88
88
|
// Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
|
|
@@ -308,7 +308,7 @@
|
|
|
308
308
|
// crafted link must not be able to load arbitrary code into the page.
|
|
309
309
|
// ---------------------------------------------------------------
|
|
310
310
|
// v0.1.6: knows this engine's aliases, lookupfield rule, meta↔style, builder methods, project loading
|
|
311
|
-
const VALIDATOR_URL_DEFAULT = 'https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-grammar@v0.1.
|
|
311
|
+
const VALIDATOR_URL_DEFAULT = 'https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-grammar@v0.1.27/dist/validate.mjs';
|
|
312
312
|
// a page may set ixmaps.validate BEFORE loading this script — the
|
|
313
313
|
// `global.ixmaps = {...}` export below would otherwise overwrite it
|
|
314
314
|
const _preloadValidate = global.ixmaps && global.ixmaps.validate;
|
|
@@ -2125,7 +2125,7 @@
|
|
|
2125
2125
|
// edit it by hand; `npm run unit` fails when it drifts.
|
|
2126
2126
|
// ---------------------------------------------------------------
|
|
2127
2127
|
// <grammar:binding-aliases>
|
|
2128
|
-
// generated from ixmaps-grammar 0.1.
|
|
2128
|
+
// generated from ixmaps-grammar 0.1.27 (ixmaps-flat 1.0.42, 2b978d5 2026-10-02) — 52 aliases
|
|
2129
2129
|
const FLAT_BINDING_ALIASES = {
|
|
2130
2130
|
"aggregation": "style.aggregationfield",
|
|
2131
2131
|
"aggregationfield": "style.aggregationfield",
|
|
@@ -2187,7 +2187,7 @@
|
|
|
2187
2187
|
// both — so .style({tooltip}) is a tooltip and .meta({fillopacity}) a style
|
|
2188
2188
|
// property. GENERATED by test/sync-grammar.mjs — do not edit.
|
|
2189
2189
|
// <grammar:meta-keys>
|
|
2190
|
-
// generated from ixmaps-grammar 0.1.
|
|
2190
|
+
// generated from ixmaps-grammar 0.1.27 — 5 meta keys
|
|
2191
2191
|
const FLAT_META_KEYS = ["description","name","snippet","title","tooltip"];
|
|
2192
2192
|
// </grammar:meta-keys>
|
|
2193
2193
|
|
|
@@ -2206,6 +2206,7 @@
|
|
|
2206
2206
|
'style.alphafield': ['binding', 'alpha'],
|
|
2207
2207
|
'style.alphafield100': ['binding', 'alpha100'],
|
|
2208
2208
|
'style.valuefield': ['style', 'valuefield'],
|
|
2209
|
+
'style.timefield': ['binding', 'time'], // setThemeTimeFrame, see LayerRuntime.setTimeFrame
|
|
2209
2210
|
'style.titlefield': ['binding', 'title'], // {{theme.item.title}}, see _buildTooltipContext
|
|
2210
2211
|
// the lookup key's normalization (flatLookupKey): flat's newTheme moves
|
|
2211
2212
|
// binding.tonumber / digits / … into these style keys (htmlgui.js 1712)
|
|
@@ -2371,7 +2372,8 @@
|
|
|
2371
2372
|
const STYLE_NUMBER_KEYS = ['linewidth', 'fillopacity', 'scale', 'classes', 'valuedecimals', 'normalsizevalue',
|
|
2372
2373
|
'sizepow', 'rangescale', 'minvalue', 'maxvalue', 'markersize', 'boxopacity', 'outlierscale', 'valuescale',
|
|
2373
2374
|
'brightness', 'fractionscale', 'dopacityscale', 'dopacitypow', 'gridwidthpx', 'textscale', 'rangecentervalue',
|
|
2374
|
-
'shadowblur', 'shadowdx', 'shadowdy', 'maxshadow', 'offsetx', 'offsety'
|
|
2375
|
+
'shadowblur', 'shadowdx', 'shadowdy', 'maxshadow', 'offsetx', 'offsety', 'gridx', 'boxmargin', 'borderwidth',
|
|
2376
|
+
'borderradius', 'maxcharts'];
|
|
2375
2377
|
function toNumberIfNumeric(v) {
|
|
2376
2378
|
if (typeof v !== 'string' || !v.trim()) return v;
|
|
2377
2379
|
const n = Number(v);
|
|
@@ -2635,6 +2637,69 @@
|
|
|
2635
2637
|
return { dark: false, bg: 'rgba(255,255,255,0.9)' };
|
|
2636
2638
|
}
|
|
2637
2639
|
|
|
2640
|
+
// ---------------------------------------------------------------
|
|
2641
|
+
// Effective basemap darkness — what the map canvas actually shows,
|
|
2642
|
+
// not what its name suggests. flatLegendLook's name-plus-opacity
|
|
2643
|
+
// heuristic assumes the faded (basemapopacity <= 0.5) basemap blends
|
|
2644
|
+
// against a WHITE page, so a dark basemap faded to half goes light;
|
|
2645
|
+
// on a page with a dark background it reads dark all the same, and
|
|
2646
|
+
// tooltips/legends styled light stick out (the exact bug fixed here).
|
|
2647
|
+
// Rule: an explicit color (legendBackground, a color mapType) stays
|
|
2648
|
+
// authoritative as before; with a real basemap style, a DARK style
|
|
2649
|
+
// background means dark — except when it is faded to
|
|
2650
|
+
// basemapopacity <= 0.5, where the mix is mostly the page background
|
|
2651
|
+
// behind the semi-transparent canvas and THAT decides (dark page ->
|
|
2652
|
+
// still dark; white page -> flat's washed-out light look). A light
|
|
2653
|
+
// style background reads light however it is faded (flat's own rule).
|
|
2654
|
+
// Falls back to the name heuristic while no style exists.
|
|
2655
|
+
// ---------------------------------------------------------------
|
|
2656
|
+
function styleBackgroundColorOf(map) {
|
|
2657
|
+
try {
|
|
2658
|
+
const style = map && map.getStyle && map.getStyle();
|
|
2659
|
+
if (!style || !style.layers) return null;
|
|
2660
|
+
for (const layer of style.layers) {
|
|
2661
|
+
if (layer.type !== 'background') continue;
|
|
2662
|
+
const paint = layer.paint || {};
|
|
2663
|
+
const color = paint['background-color'] != null ? paint['background-color'] : (paint.background != null ? paint.background : null);
|
|
2664
|
+
// the blank background substituted for a color mapType IS the page's
|
|
2665
|
+
// requested background and never faded (see applyBasemapOpacity)
|
|
2666
|
+
return { color: color, faded: layer.id !== BLANK_BACKGROUND_LAYER_ID };
|
|
2667
|
+
}
|
|
2668
|
+
} catch (e) { /* no style (yet) */ }
|
|
2669
|
+
return null;
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
function pageBackgroundColorOf(el) {
|
|
2673
|
+
let node = el;
|
|
2674
|
+
while (node && node.nodeType === 1 && node !== document.documentElement) {
|
|
2675
|
+
const m = String(getComputedStyle(node).backgroundColor)
|
|
2676
|
+
.match(/^rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)(?:[,/\s]+([\d.]+))?\s*\)/i);
|
|
2677
|
+
if (m && (m[4] === undefined || +m[4] > 0)) return [+m[1], +m[2], +m[3]];
|
|
2678
|
+
node = node.parentElement;
|
|
2679
|
+
}
|
|
2680
|
+
return [255, 255, 255]; // the page default
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
function effectiveMapDark(map, el, basemapOpacity, mapType, legendBackground) {
|
|
2684
|
+
const id = legendBackground || mapType;
|
|
2685
|
+
if (flatIsCssColorValue(id)) return flatIsDarkColor(id);
|
|
2686
|
+
const base = styleBackgroundColorOf(map);
|
|
2687
|
+
const rgb = base && base.color != null ? parseCssColor(String(base.color)) : null;
|
|
2688
|
+
if (!rgb) {
|
|
2689
|
+
// no style (yet): the name heuristic, exactly as before
|
|
2690
|
+
return flatLegendLook(mapType, basemapOpacity, legendBackground).dark;
|
|
2691
|
+
}
|
|
2692
|
+
const o = isNaN(basemapOpacity) ? 1 : Math.max(0, Math.min(1, basemapOpacity));
|
|
2693
|
+
if (flatIsDarkColor('rgb(' + rgb.map(Math.round).join(',') + ')')) {
|
|
2694
|
+
// a dark basemap faded to half or less shows mostly the page background
|
|
2695
|
+
// behind the canvas - so at that point the PAGE decides (dark page ->
|
|
2696
|
+
// still dark; white page -> the washed-out flat look stays light)
|
|
2697
|
+
return (base.faded && o <= 0.5) ? flatIsDarkColor('rgb(' + pageBackgroundColorOf(el).map(Math.round).join(',') + ')') : true;
|
|
2698
|
+
}
|
|
2699
|
+
// a light basemap reads light however it is faded - flat's own rule
|
|
2700
|
+
return false;
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2638
2703
|
function buildBlankBackgroundStyle(color) {
|
|
2639
2704
|
return {
|
|
2640
2705
|
version: 8,
|
|
@@ -2677,6 +2742,40 @@
|
|
|
2677
2742
|
}
|
|
2678
2743
|
|
|
2679
2744
|
// ---------------------------------------------------------------
|
|
2745
|
+
// Embedded YouTube players in tooltips (data fields carrying <iframe
|
|
2746
|
+
// src=".../embed/ID">) are replaced by a thumbnail with a play button; the
|
|
2747
|
+
// real iframe is created on click. Keeps tooltips light, and a removed
|
|
2748
|
+
// video or a refused embed (e.g. error 153 on file:// pages) degrades to a
|
|
2749
|
+
// thumbnail that opens YouTube in a new tab instead of a broken player.
|
|
2750
|
+
const YT_IFRAME_RE = /<iframe\b[^>]*?\bsrc\s*=\s*["']?(?:https?:)?\/\/(?:www\.)?youtube(?:-nocookie)?\.com\/embed\/([\w-]{6,})[^"'\s>]*["']?[^>]*>\s*<\/iframe>/gi;
|
|
2751
|
+
function youtubeClickToPlay(html) {
|
|
2752
|
+
if (!html || html.indexOf('youtube') < 0) return html;
|
|
2753
|
+
return html.replace(YT_IFRAME_RE, (m, id) => {
|
|
2754
|
+
const w = (/\bwidth\s*=\s*["']?(\d+)/i.exec(m) || [])[1] || 240;
|
|
2755
|
+
const h = (/\bheight\s*=\s*["']?(\d+)/i.exec(m) || [])[1] || 180;
|
|
2756
|
+
return '<div class="ixgl-yt" data-yt="' + id + '" data-w="' + w + '" data-h="' + h + '" ' +
|
|
2757
|
+
'style="position:relative;width:' + w + 'px;height:' + h + 'px;cursor:pointer;background:#000 url(https://i.ytimg.com/vi/' + id + '/hqdefault.jpg) center/cover" ' +
|
|
2758
|
+
'title="play"><span style="position:absolute;left:50%;top:50%;width:48px;height:34px;margin:-17px 0 0 -24px;' +
|
|
2759
|
+
'border-radius:8px;background:rgba(220,0,0,0.9)"></span><span style="position:absolute;left:50%;top:50%;margin:-9px 0 0 -6px;' +
|
|
2760
|
+
'border-style:solid;border-width:9px 0 9px 15px;border-color:transparent transparent transparent #fff"></span></div>';
|
|
2761
|
+
});
|
|
2762
|
+
}
|
|
2763
|
+
if (typeof document !== 'undefined' && typeof document.addEventListener === 'function' && !global.__ixglYtClick) {
|
|
2764
|
+
global.__ixglYtClick = true;
|
|
2765
|
+
document.addEventListener('click', e => {
|
|
2766
|
+
const el = e.target.closest && e.target.closest('.ixgl-yt');
|
|
2767
|
+
if (!el) return;
|
|
2768
|
+
e.stopPropagation();
|
|
2769
|
+
const f = document.createElement('iframe');
|
|
2770
|
+
f.width = el.dataset.w; f.height = el.dataset.h; f.frameBorder = '0';
|
|
2771
|
+
f.allow = 'autoplay; encrypted-media; picture-in-picture; fullscreen';
|
|
2772
|
+
f.allowFullscreen = true;
|
|
2773
|
+
f.referrerPolicy = 'strict-origin-when-cross-origin';
|
|
2774
|
+
f.src = 'https://www.youtube.com/embed/' + el.dataset.yt + '?autoplay=1&rel=0';
|
|
2775
|
+
el.replaceWith(f);
|
|
2776
|
+
}, true);
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2680
2779
|
// Hover tooltips and the click-to-pin tooltip of a map, built once per map
|
|
2681
2780
|
// by MapBuilder.build(). ctx: the builder (its map options, for the
|
|
2682
2781
|
// tooltip look), the MapLibre map, its container element and
|
|
@@ -2686,6 +2785,12 @@
|
|
|
2686
2785
|
// ---------------------------------------------------------------
|
|
2687
2786
|
function createTooltips(ctx) {
|
|
2688
2787
|
const { builder, map, el, findRuntimeForLayerId } = ctx;
|
|
2788
|
+
// the effective (rendered) dark/light probe for tooltip and legend —
|
|
2789
|
+
// the builder carries it so the legend look (built later, without the
|
|
2790
|
+
// map element at hand) can ask the same question
|
|
2791
|
+
builder.__effectiveDark = function (basemapOpacity, mapType, legendBackground) {
|
|
2792
|
+
return effectiveMapDark(map, el, basemapOpacity, mapType, legendBackground);
|
|
2793
|
+
};
|
|
2689
2794
|
// Click-to-pin tooltip: matches the real ixmaps engine's own
|
|
2690
2795
|
// click-pins-the-tooltip convention in pan mode (a hover tooltip is
|
|
2691
2796
|
// transient; a click keeps one visible until dismissed). This is a
|
|
@@ -2695,28 +2800,101 @@
|
|
|
2695
2800
|
// at click time (not live-updating), but its screen POSITION is kept
|
|
2696
2801
|
// in sync with the map on every pan/zoom via map.project().
|
|
2697
2802
|
let pinned = null; // { runtime, object, lngLat }
|
|
2698
|
-
// Tooltip look, like the legend's
|
|
2699
|
-
//
|
|
2700
|
-
//
|
|
2701
|
-
//
|
|
2803
|
+
// Tooltip look, like the legend's: flat's light tooltip
|
|
2804
|
+
// (tooltip_mustache.js: white 0.95, #444 text, thin black border, 5px
|
|
2805
|
+
// radius) on light basemaps, this engine's dark tooltip on dark ones —
|
|
2806
|
+
// decided by the EFFECTIVE map darkness (style background vs page
|
|
2807
|
+
// background, see effectiveMapDark), not the basemap name alone. Read
|
|
2808
|
+
// when shown, so options set later (a
|
|
2702
2809
|
// myMap.then(...).options({basemapopacity})) count.
|
|
2703
|
-
const
|
|
2810
|
+
const tooltipIsDark = () => {
|
|
2704
2811
|
const o = parseFloat(builder._engineOptions.basemapopacity);
|
|
2705
|
-
const
|
|
2706
|
-
return
|
|
2707
|
-
?
|
|
2708
|
-
:
|
|
2709
|
-
boxShadow: 'rgba(0,0,0,0.2) 0px 2px 4px 0px, rgba(0,0,0,0.19) 0px 3px 10px 0px' };
|
|
2812
|
+
const legendBackground = builder.mapOptions.legendBackground || builder.mapOptions.legendbackground;
|
|
2813
|
+
return typeof builder.__effectiveDark === 'function'
|
|
2814
|
+
? builder.__effectiveDark(isNaN(o) ? 1 : o, builder.mapOptions.mapType, legendBackground)
|
|
2815
|
+
: flatLegendLook(builder.mapOptions.mapType, isNaN(o) ? 1 : o, legendBackground).dark;
|
|
2710
2816
|
};
|
|
2817
|
+
// flat's tooltip look (tooltip_mustache.js 118/151-154): arial narrow,
|
|
2818
|
+
// the font size clamped to 11..14px; light: white 0.95, #444 text, thin
|
|
2819
|
+
// black border; dark: rgba(50,50,50,0.95), #555 border (text light for
|
|
2820
|
+
// readability — flat leaves its #444 on dark and only adapts the panel).
|
|
2821
|
+
// line-height is declared so hover (deck.gl's .deck-tooltip stylesheet
|
|
2822
|
+
// gives it 20px) and the pinned tooltip (a bare div that would inherit
|
|
2823
|
+
// the page's 'normal') render the same content identically
|
|
2824
|
+
const tooltipLook = () => {
|
|
2825
|
+
const fontsize = Math.min(14, Math.max(11, (22 / 1200 * (global.innerWidth || 1200))));
|
|
2826
|
+
const fonts = { fontFamily: 'arial narrow, system', fontSize: fontsize + 'px', lineHeight: '20px' };
|
|
2827
|
+
return tooltipIsDark()
|
|
2828
|
+
? Object.assign(fonts, { background: 'rgba(50,50,50,0.95)', color: 'rgb(200,205,214)', border: '0.5px solid #555', borderRadius: '5px',
|
|
2829
|
+
boxShadow: '0 2px 8px rgba(0,0,0,0.3)' })
|
|
2830
|
+
: Object.assign(fonts, { background: 'rgba(255,255,255,0.95)', color: '#444', border: '0.5px solid black', borderRadius: '5px',
|
|
2831
|
+
boxShadow: 'rgba(0,0,0,0.2) 0px 2px 4px 0px, rgba(0,0,0,0.19) 0px 3px 10px 0px' });
|
|
2832
|
+
};
|
|
2833
|
+
// flat's content pane (tooltip_mustache.js 124): the whole tooltip
|
|
2834
|
+
// content scrolls inside its own padded pane — a close button outside
|
|
2835
|
+
// the pane stays visible while it scrolls, and the scrollbar clears
|
|
2836
|
+
// the content by a small right margin
|
|
2837
|
+
const TOOLTIP_PANE_STYLE = 'margin:0.5em;max-height:360px;overflow:auto';
|
|
2838
|
+
const tooltipPaneHtml = (html) => `<div style="${TOOLTIP_PANE_STYLE}">${html}</div>`;
|
|
2711
2839
|
const pinnedTooltipEl = document.createElement('div');
|
|
2712
2840
|
pinnedTooltipEl.style.cssText = 'position:absolute;top:0;left:0;z-index:6;display:none;' +
|
|
2713
|
-
'pointer-events:auto;max-width:440px;
|
|
2714
|
-
|
|
2841
|
+
'pointer-events:auto;max-width:440px;padding:0.5em 0.7em 0.5em 0.5em;';
|
|
2842
|
+
|
|
2843
|
+
// flat's own tooltip placement (tooltip_mustache.js 145-148): flip it
|
|
2844
|
+
// to the left/above the anchor when there is no room right/below, then
|
|
2845
|
+
// clamp it into the window — a tooltip must always be completely
|
|
2846
|
+
// visible (an element larger than the viewport ends up at its edge)
|
|
2847
|
+
function keepTooltipVisible(tipEl, x, y) {
|
|
2848
|
+
const w = tipEl.offsetWidth, h = tipEl.offsetHeight;
|
|
2849
|
+
const vw = global.innerWidth || 1200, vh = global.innerHeight || 800;
|
|
2850
|
+
let tx = x > vw / 2 ? x - w - 30 : x + 30;
|
|
2851
|
+
let ty = y > vh / 2 ? y - h - 20 : y + 20;
|
|
2852
|
+
tx = Math.min(Math.max(8, tx), Math.max(8, vw - w - 8));
|
|
2853
|
+
ty = Math.min(Math.max(8, ty), Math.max(8, vh - h - 8));
|
|
2854
|
+
const transform = `translate(${Math.round(tx)}px, ${Math.round(ty)}px)`;
|
|
2855
|
+
tipEl.style.transform = transform;
|
|
2856
|
+
return transform;
|
|
2857
|
+
}
|
|
2715
2858
|
|
|
2716
2859
|
function updatePinnedTooltipPosition() {
|
|
2717
2860
|
if (!pinned) return;
|
|
2718
2861
|
const pt = map.project(pinned.lngLat);
|
|
2719
|
-
pinnedTooltipEl
|
|
2862
|
+
keepTooltipVisible(pinnedTooltipEl, pt.x, pt.y);
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
// hover tooltip: deck.gl pins it at the pointer without any viewport
|
|
2866
|
+
// logic (it overflows the right/bottom edge there) — watch the element's
|
|
2867
|
+
// style mutations and re-place it inside the window every time deck
|
|
2868
|
+
// moves it. Our own write is recognized and skipped (lastSetTransform).
|
|
2869
|
+
let __hoverTooltipWatch = null;
|
|
2870
|
+
function watchHoverTooltip() {
|
|
2871
|
+
if (__hoverTooltipWatch || !el.parentElement) return;
|
|
2872
|
+
const host = el.parentElement;
|
|
2873
|
+
const attach = (tipEl) => {
|
|
2874
|
+
let lastSetTransform = null;
|
|
2875
|
+
const mo = new MutationObserver(() => {
|
|
2876
|
+
if (tipEl.style.display === 'none' || !tipEl.textContent.trim()) return;
|
|
2877
|
+
const t = tipEl.style.transform;
|
|
2878
|
+
if (!t || t === lastSetTransform) return;
|
|
2879
|
+
const m = /translate\((-?[\d.]+)px,\s*(-?[\d.]+)px\)/.exec(t);
|
|
2880
|
+
if (!m) return;
|
|
2881
|
+
lastSetTransform = keepTooltipVisible(tipEl, +m[1], +m[2]);
|
|
2882
|
+
});
|
|
2883
|
+
mo.observe(tipEl, { attributes: true, attributeFilter: ['style'] });
|
|
2884
|
+
__hoverTooltipWatch = mo;
|
|
2885
|
+
};
|
|
2886
|
+
const existing = host.querySelector('.deck-tooltip');
|
|
2887
|
+
if (existing) { attach(existing); return; }
|
|
2888
|
+
const creation = new MutationObserver((muts, obs) => {
|
|
2889
|
+
for (const mu of muts) for (const n of mu.addedNodes) {
|
|
2890
|
+
if (n.nodeType === 1 && n.classList && n.classList.contains('deck-tooltip')) {
|
|
2891
|
+
attach(n);
|
|
2892
|
+
obs.disconnect();
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
});
|
|
2897
|
+
creation.observe(host, { childList: true, subtree: true });
|
|
2720
2898
|
}
|
|
2721
2899
|
|
|
2722
2900
|
// A stable per-geometry anchor coordinate for the identity check
|
|
@@ -2750,13 +2928,19 @@
|
|
|
2750
2928
|
}
|
|
2751
2929
|
|
|
2752
2930
|
// content lives in its own child so re-rendering (innerHTML) never
|
|
2753
|
-
//
|
|
2931
|
+
// content lives in its own child so re-rendering (innerHTML) never
|
|
2932
|
+
// wipes out the unpin button below (a sibling, not a descendant of it).
|
|
2933
|
+
// The content child sits inside a scroll PANE (flat's own tooltip
|
|
2934
|
+
// structure): the pane scrolls, the close button outside it does not
|
|
2935
|
+
const pinnedPaneEl = document.createElement('div');
|
|
2936
|
+
pinnedPaneEl.style.cssText = TOOLTIP_PANE_STYLE;
|
|
2754
2937
|
const pinnedContentEl = document.createElement('div');
|
|
2755
|
-
|
|
2938
|
+
pinnedPaneEl.appendChild(pinnedContentEl);
|
|
2939
|
+
pinnedTooltipEl.appendChild(pinnedPaneEl);
|
|
2756
2940
|
|
|
2757
2941
|
function renderPinnedTooltip() {
|
|
2758
2942
|
if (!pinned) { pinnedTooltipEl.style.display = 'none'; return; }
|
|
2759
|
-
const html = pinned.runtime.buildTooltipHtml(pinned.object);
|
|
2943
|
+
const html = pinned.runtime.buildTooltipHtml(pinned.object, tooltipIsDark());
|
|
2760
2944
|
if (!html) { pinned = null; pinnedTooltipEl.style.display = 'none'; return; }
|
|
2761
2945
|
pinnedContentEl.innerHTML = html;
|
|
2762
2946
|
Object.assign(pinnedTooltipEl.style, tooltipLook());
|
|
@@ -2797,7 +2981,15 @@
|
|
|
2797
2981
|
// a USER chart's picked item is {d, icon} (see _buildUserChartLayers):
|
|
2798
2982
|
// the feature is its d
|
|
2799
2983
|
function pickedFeature(o) { return o && o.icon && o.d ? o.d : o; }
|
|
2984
|
+
// flat's map tool 'pan' (Map option mode:"pan", Api.setMapTool): pure
|
|
2985
|
+
// navigation — no hover tooltip there (flat's own hover-tooltip paths
|
|
2986
|
+
// run only for the ""/info/clickinfo tool types, mapscript2.js 5232).
|
|
2987
|
+
// The click-to-pin tooltip stays: it is this engine's equivalent of
|
|
2988
|
+
// flat's info popup, so a click still shows the permanent tooltip.
|
|
2989
|
+
const isPanMode = () => /^\s*pan\s*$/i.test(String(builder.mapOptions.mode || ''));
|
|
2990
|
+
|
|
2800
2991
|
function tooltipFor({ object: picked, layer }) {
|
|
2992
|
+
if (isPanMode()) return null;
|
|
2801
2993
|
const object = pickedFeature(picked);
|
|
2802
2994
|
if (!object || !layer) return null;
|
|
2803
2995
|
// suppress the hover tooltip ONLY for the specific item that's
|
|
@@ -2807,8 +2999,10 @@
|
|
|
2807
2999
|
if (pinned && isSameAsPinned(layer.id, object)) return null;
|
|
2808
3000
|
const rt = findRuntimeForLayerId(layer.id);
|
|
2809
3001
|
if (!rt) return null;
|
|
2810
|
-
const html = rt.buildTooltipHtml(object);
|
|
2811
|
-
|
|
3002
|
+
const html = rt.buildTooltipHtml(object, tooltipIsDark());
|
|
3003
|
+
// same layout as the pinned tooltip: the content in flat's scroll
|
|
3004
|
+
// pane, the shared look (fonts included) on the container
|
|
3005
|
+
return html ? { html: tooltipPaneHtml(html), style: Object.assign({ padding: '0.5em 0.7em', maxWidth: '440px' }, tooltipLook()) } : null;
|
|
2812
3006
|
}
|
|
2813
3007
|
function clickFor(picked) {
|
|
2814
3008
|
const info = picked && Object.assign({}, picked, { object: pickedFeature(picked.object) });
|
|
@@ -2868,6 +3062,7 @@
|
|
|
2868
3062
|
}
|
|
2869
3063
|
function mount() {
|
|
2870
3064
|
map.on('move', updatePinnedTooltipPosition);
|
|
3065
|
+
watchHoverTooltip();
|
|
2871
3066
|
if (el.parentElement) {
|
|
2872
3067
|
el.parentElement.style.position = el.parentElement.style.position || 'relative';
|
|
2873
3068
|
el.parentElement.appendChild(pinnedTooltipEl);
|
|
@@ -3073,7 +3268,10 @@
|
|
|
3073
3268
|
});
|
|
3074
3269
|
legendStack.style.display = shown ? 'flex' : 'none';
|
|
3075
3270
|
};
|
|
3076
|
-
|
|
3271
|
+
// a plain FEATURE(S) base layer has one flat fill (no classes); FEATURE|CATEGORICAL
|
|
3272
|
+
// is coloured per category (see _buildFeaturesLayers), so it gets a legend
|
|
3273
|
+
const legendApplies = rt => rt.categoryLabels && rt.categoryLabels.length
|
|
3274
|
+
&& (!(rt.flags.has('FEATURE') || rt.flags.has('FEATURES')) || rt.flags.has('CATEGORICAL')) && !rt.flags.has('NOLEGEND');
|
|
3077
3275
|
// One theme's panel — at build time for every theme, and again for a
|
|
3078
3276
|
// theme defined later (map.layer(...) in a myMap.then(...) chain,
|
|
3079
3277
|
// defineLayer, loadProject); removeTheme removes it with the theme.
|
|
@@ -3109,8 +3307,16 @@
|
|
|
3109
3307
|
// flat's look (flatLegendLook) unless the page picks one with
|
|
3110
3308
|
// this engine's own legendtheme
|
|
3111
3309
|
const opacityOpt = parseFloat(builder._engineOptions.basemapopacity);
|
|
3112
|
-
const
|
|
3113
|
-
const
|
|
3310
|
+
const legendBackground = builder.mapOptions.legendBackground || builder.mapOptions.legendbackground;
|
|
3311
|
+
const flatLook = flatLegendLook(builder.mapOptions.mapType, isNaN(opacityOpt) ? 1 : opacityOpt, legendBackground);
|
|
3312
|
+
// effective darkness (style background vs page background, see
|
|
3313
|
+
// effectiveMapDark): a dark basemap faded to <= 0.5 over a DARK
|
|
3314
|
+
// page still reads dark — flatLegendLook's name heuristic would
|
|
3315
|
+
// wrongly switch the panel (and the tooltip) to light there
|
|
3316
|
+
const looksDark = typeof builder.__effectiveDark === 'function'
|
|
3317
|
+
? builder.__effectiveDark(isNaN(opacityOpt) ? 1 : opacityOpt, builder.mapOptions.mapType, legendBackground)
|
|
3318
|
+
: flatLook.dark;
|
|
3319
|
+
const isLightLegend = rt.style.legendtheme === 'light' || (rt.style.legendtheme !== 'dark' && !looksDark);
|
|
3114
3320
|
const legendColors = isLightLegend
|
|
3115
3321
|
? { bg: 'rgba(255,255,255,0.92)', fg: '#1a1a1a', shadow: '0 2px 10px rgba(0,0,0,0.18)',
|
|
3116
3322
|
rowMarked: 'rgba(0,0,0,0.08)', track: 'rgba(0,0,0,0.08)',
|
|
@@ -3118,7 +3324,10 @@
|
|
|
3118
3324
|
: { bg: 'rgba(28,30,34,0.92)', fg: '#eee', shadow: '0 2px 10px rgba(0,0,0,0.45)',
|
|
3119
3325
|
rowMarked: 'rgba(255,255,255,0.14)', track: 'rgba(255,255,255,0.08)',
|
|
3120
3326
|
selectBg: 'rgba(255,255,255,0.08)', selectBorder: 'rgba(255,255,255,0.25)' };
|
|
3121
|
-
|
|
3327
|
+
// the panel bg follows the same effective decision (flatLegendLook
|
|
3328
|
+
// would hand the dark look a light bg here: its dark branch never
|
|
3329
|
+
// ran for this map, its light bg would clash with dark rows/text)
|
|
3330
|
+
if (!rt.style.legendtheme) legendColors.bg = looksDark ? '#111' : flatLook.bg;
|
|
3122
3331
|
// max-height:66% resolves against el.parentElement's own
|
|
3123
3332
|
// height (the map container, which always has a definite
|
|
3124
3333
|
// height for the map itself to render into) since this panel
|
|
@@ -3208,7 +3417,11 @@
|
|
|
3208
3417
|
(rt._activeFeatures || rt.features).forEach(f => {
|
|
3209
3418
|
const idx = rt.categoryIndexByLabel ? rt.categoryIndexByLabel.get(f.properties[rt.binding.value]) : null;
|
|
3210
3419
|
if (idx == null) return;
|
|
3211
|
-
|
|
3420
|
+
// a polygon (FEATURE|CATEGORICAL) is counted at its first vertex
|
|
3421
|
+
let pt = f.geometry && f.geometry.coordinates;
|
|
3422
|
+
while (Array.isArray(pt) && Array.isArray(pt[0])) pt = pt[0];
|
|
3423
|
+
if (!Array.isArray(pt)) return;
|
|
3424
|
+
const [lng, lat] = pt;
|
|
3212
3425
|
if (lng < bbox[0] || lng > bbox[2] || lat < bbox[1] || lat > bbox[3]) return;
|
|
3213
3426
|
if (globeCenter && !isOnVisibleHemisphere(lng, lat, globeCenter)) return;
|
|
3214
3427
|
totals[idx] += useSum ? (parseFloat(f.properties[valueField]) || 0) : 1;
|
|
@@ -3645,6 +3858,8 @@
|
|
|
3645
3858
|
this._layerBuilders.push(nameOrBuilder);
|
|
3646
3859
|
return this;
|
|
3647
3860
|
}
|
|
3861
|
+
// .Layer() — alias of .layer()
|
|
3862
|
+
Layer(nameOrBuilder) { return this.layer(nameOrBuilder); }
|
|
3648
3863
|
// GL-PORT COMPAT: real ixmaps-flat's map.on("layerdraw", cb) — only
|
|
3649
3864
|
// "layerdraw" is actually wired up (see build()'s own note on how
|
|
3650
3865
|
// it's translated into onRedraw + a synthetic per-runtime event); any
|
|
@@ -3843,6 +4058,16 @@
|
|
|
3843
4058
|
// world once, at the cost of that zoom limit.
|
|
3844
4059
|
renderWorldCopies: this._engineOptions.worldcopies !== false
|
|
3845
4060
|
});
|
|
4061
|
+
// ixmaps.Map(id, {mapProjection}): flat's orthographic map is MapLibre's
|
|
4062
|
+
// native globe here (same mapping as loadProject, applyProjectMap);
|
|
4063
|
+
// "ortographic" is the spelling flat pages commonly carry. Any other
|
|
4064
|
+
// value stays mercator.
|
|
4065
|
+
if (/^(orth?ographic|globe)$/i.test(String(this.mapOptions.mapProjection || '').trim())) {
|
|
4066
|
+
map.once('style.load', () => {
|
|
4067
|
+
try { map.setProjection({ type: 'globe' }); }
|
|
4068
|
+
catch (e) { console.warn('[ixmaps-gl] mapProjection "globe": map.setProjection failed (needs maplibre-gl >=5.0.1)', e); }
|
|
4069
|
+
});
|
|
4070
|
+
}
|
|
3846
4071
|
_boundsSource = () => {
|
|
3847
4072
|
const b = map.getBounds();
|
|
3848
4073
|
return [{ lat: b.getSouth(), lng: b.getWest() }, { lat: b.getNorth(), lng: b.getEast() }];
|
|
@@ -3937,9 +4162,11 @@
|
|
|
3937
4162
|
const overlay = new MapLibreOverlay({
|
|
3938
4163
|
interleaved: true,
|
|
3939
4164
|
layers: [],
|
|
3940
|
-
// pointer becomes a hand over anything pickable (bubbles/points),
|
|
3941
|
-
//
|
|
3942
|
-
|
|
4165
|
+
// pointer becomes a hand over anything pickable (bubbles/points), so
|
|
4166
|
+
// hovering something clickable actually looks clickable — except in
|
|
4167
|
+
// the 'pan' input mode, where nothing responds to hovering
|
|
4168
|
+
getCursor: ({ isDragging, isHovering }) =>
|
|
4169
|
+
(isDragging ? 'grabbing' : ((isHovering && !/^\s*pan\s*$/i.test(String(this.mapOptions.mode || ''))) ? 'pointer' : 'grab')),
|
|
3943
4170
|
// (guarded: see createTooltips)
|
|
3944
4171
|
getTooltip: tooltips.getTooltip,
|
|
3945
4172
|
onClick: tooltips.onClick
|
|
@@ -4216,6 +4443,18 @@
|
|
|
4216
4443
|
refresh();
|
|
4217
4444
|
}
|
|
4218
4445
|
},
|
|
4446
|
+
// flat's ixmaps.setThemeTimeFrame(szId, min, max): show only the
|
|
4447
|
+
// records whose timefield lies in [min, max) (ms or Date-
|
|
4448
|
+
// parsable). null min+max clears. szId null/undefined = every theme
|
|
4449
|
+
// that has a timefield (flat: one call updates all themes at once).
|
|
4450
|
+
setThemeTimeFrame: (themeId, min, max) => {
|
|
4451
|
+
const targets = themeId == null ? runtimes.filter(r => r.binding && r.binding.time) : (() => {
|
|
4452
|
+
const rt = findRuntime(themeId);
|
|
4453
|
+
return rt ? siblingRuntimes(rt).filter(r => r.binding && r.binding.time) : [];
|
|
4454
|
+
})();
|
|
4455
|
+
targets.forEach(rt => rt.setTimeFrame(min, max));
|
|
4456
|
+
if (targets.length) refresh();
|
|
4457
|
+
},
|
|
4219
4458
|
// shows/hides a whole theme (all the deck.gl layers its
|
|
4220
4459
|
// buildDeckLayers would otherwise produce) without touching its
|
|
4221
4460
|
// data or style — for toggle controls like the real page's
|
|
@@ -4301,6 +4540,8 @@
|
|
|
4301
4540
|
};
|
|
4302
4541
|
return lb;
|
|
4303
4542
|
},
|
|
4543
|
+
// .Layer() — alias of .layer()
|
|
4544
|
+
Layer: (nameOrBuilder) => engineApi.layer(nameOrBuilder),
|
|
4304
4545
|
// Real ixmaps-flat's loadProject (htmlgui.js loadProject →
|
|
4305
4546
|
// setProjectJSON → continueSetProjectJSON): src is a project object,
|
|
4306
4547
|
// a JSON string, or a URL (fetched as DATA — nothing a project names
|
|
@@ -4633,7 +4874,15 @@
|
|
|
4633
4874
|
const svg = String(m.map || '');
|
|
4634
4875
|
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`);
|
|
4635
4876
|
if (typeof map.setProjection === 'function') {
|
|
4636
|
-
try {
|
|
4877
|
+
try {
|
|
4878
|
+
map.setProjection({ type: /orthographic\.svg$/i.test(svg) ? 'globe' : 'mercator' });
|
|
4879
|
+
// the layers are projection-dependent (globe: far-hemisphere cull,
|
|
4880
|
+
// chart placement) and are otherwise rebuilt only by a pan: rebuild
|
|
4881
|
+
// now, and again once the new projection has rendered
|
|
4882
|
+
refreshLayers();
|
|
4883
|
+
map.triggerRepaint();
|
|
4884
|
+
map.once('idle', refreshLayers);
|
|
4885
|
+
}
|
|
4637
4886
|
catch (e) { console.warn('[ixmaps-gl] loadProject: map.setProjection failed (needs maplibre-gl >=5.0.1)', e); }
|
|
4638
4887
|
} else {
|
|
4639
4888
|
console.warn('[ixmaps-gl] loadProject: map.setProjection not available on the loaded maplibre-gl build (needs >=5.0.1) — projection unchanged');
|
|
@@ -5179,6 +5428,8 @@
|
|
|
5179
5428
|
// earlier hand-tuned 12.5 drew every symbol 1.2× (with the reference
|
|
5180
5429
|
// below, 1.3×) smaller than flat.
|
|
5181
5430
|
const NORMAL_RADIUS_PX = 15;
|
|
5431
|
+
// largest BOX side (px) still drawn as a canvas icon (at 2×), see _buildBoxIcon
|
|
5432
|
+
const BOX_ICON_MAX_PX = 2048;
|
|
5182
5433
|
// Flat's own map scale, which dynamic object scaling compares with
|
|
5183
5434
|
// normalSizeScale (mapscript2.js 2849: dx = nTrueMapScale · nZoomScale /
|
|
5184
5435
|
// nNormalSizeScale), read from a real flat page: the generic Mercator
|
|
@@ -5425,6 +5676,16 @@
|
|
|
5425
5676
|
|
|
5426
5677
|
function valueRadius(value, zoom, style, mapOptions, flags, maxSizeValue) {
|
|
5427
5678
|
const zoomFactor = objectZoomFactor(zoom, mapOptions);
|
|
5679
|
+
// FIXSIZE (a symbol chart; PLOT / GRIDSIZE size their markers
|
|
5680
|
+
// themselves): one radius for every item, whatever its value — the
|
|
5681
|
+
// normal radius ÷ normalsizevalue, half of it for BUBBLE (maptheme.js
|
|
5682
|
+
// 20711, 21847; measured on flat: PNRR "details", r = 1.5 units at
|
|
5683
|
+
// scale 0.1)
|
|
5684
|
+
if (flags && flags.has('FIXSIZE') && !flags.has('PLOT') && !flags.has('GRIDSIZE')) {
|
|
5685
|
+
const fixed = NORMAL_RADIUS_PX * (styleNum(style.scale) || 1) / (styleNum(style.normalsizevalue) || 1)
|
|
5686
|
+
* (flatChartBranch(flags) === 'bubble' ? 0.5 : 1);
|
|
5687
|
+
return Math.max(VALUE_RADIUS_MIN, Math.min(VALUE_RADIUS_MAX, fixed * zoomFactor));
|
|
5688
|
+
}
|
|
5428
5689
|
// Real default (maptheme.js ~line 5059) when the theme sets no
|
|
5429
5690
|
// normalsizevalue: the dataset's own max value on the bound size
|
|
5430
5691
|
// field (maxSizeValue, from LayerRuntime._prepare), not a fixed
|
|
@@ -5629,6 +5890,8 @@
|
|
|
5629
5890
|
const field = binding.size ? binding.size
|
|
5630
5891
|
: (symbols && !flags.has('CATEGORICAL') && !flags.has('AGGREGATE') && binding.value && !String(binding.value).includes('|') ? binding.value : null);
|
|
5631
5892
|
if (!field) return undefined;
|
|
5893
|
+
// "$item$": every record counts 1
|
|
5894
|
+
if (field === '$item$') return features.length ? 1 : 0;
|
|
5632
5895
|
return features.reduce((max, f) => {
|
|
5633
5896
|
const v = parseFloat(f.properties[field]);
|
|
5634
5897
|
return isNaN(v) ? max : Math.max(max, v);
|
|
@@ -6228,6 +6491,13 @@
|
|
|
6228
6491
|
// researched safe range (~1480-2980 distinct 48px icons before a real
|
|
6229
6492
|
// device's WebGL MAX_TEXTURE_SIZE is at risk).
|
|
6230
6493
|
const ICON_ATLAS_RESET_AFTER = 3000;
|
|
6494
|
+
// The same reset by icon AREA: a few large icons (BOX rectangles around
|
|
6495
|
+
// MULTIQUAD grids, hundreds of px each, new sizes at every zoom) fill
|
|
6496
|
+
// deck.gl's 1024 px wide atlas long before 3000 icons — on the PNRR
|
|
6497
|
+
// page a dozen boxes doubled it per zoom step, until it passed WebGL's
|
|
6498
|
+
// MAX_TEXTURE_SIZE (16384) and the boxes drew black. 2 Mpx leaves room
|
|
6499
|
+
// for the atlas's row-packing waste (measured ~2×) below that limit.
|
|
6500
|
+
const ICON_ATLAS_AREA_RESET_AFTER = 2 * 1024 * 1024;
|
|
6231
6501
|
|
|
6232
6502
|
// Shared packing layout for a multi-category group's sub-bubbles — used
|
|
6233
6503
|
// both to draw the packed icon raster (_buildBubbleIcon) and to position
|
|
@@ -6272,6 +6542,56 @@
|
|
|
6272
6542
|
};
|
|
6273
6543
|
}
|
|
6274
6544
|
|
|
6545
|
+
// MULTIQUAD / MULTISQUARE (flat maptheme.js 18648-18690): the items at
|
|
6546
|
+
// one position, in drawing order, fill quads around it — the k-th at
|
|
6547
|
+
// square = ⌊√k⌋, (k − square², square) while that is within the square,
|
|
6548
|
+
// else (square, square − (k − square² − square)); once square reaches
|
|
6549
|
+
// gridx, rows of gridx go on upward (k % gridx, ⌊k / gridx⌋). Steps of
|
|
6550
|
+
// 2 · normal radius · rangescale (flat's chart size, measured on the PNRR
|
|
6551
|
+
// page); y up. Returns per item its pixel offset and index, and per
|
|
6552
|
+
// position's first item the extent [minX, minY, maxX, maxY] of the
|
|
6553
|
+
// offsets there (flat sizes the BOX around all of them).
|
|
6554
|
+
function multiQuadOffsets(items, normalRadiusPx, rangeScale, gridX) {
|
|
6555
|
+
const step = 2 * normalRadiusPx * (rangeScale || 1);
|
|
6556
|
+
const count = new Map(), offset = new Map(), index = new Map(), first = new Map(), extent = new Map();
|
|
6557
|
+
for (const d of items) {
|
|
6558
|
+
const c = d.geometry && d.geometry.coordinates;
|
|
6559
|
+
const key = c ? c[0] + ',' + c[1] : '';
|
|
6560
|
+
const k = count.get(key) || 0;
|
|
6561
|
+
count.set(key, k + 1);
|
|
6562
|
+
index.set(d, k);
|
|
6563
|
+
if (k === 0) { first.set(key, d); extent.set(d, [0, 0, 0, 0]); }
|
|
6564
|
+
let x = 0, y = 0;
|
|
6565
|
+
if (k > 0) {
|
|
6566
|
+
const square = Math.floor(Math.sqrt(k));
|
|
6567
|
+
if (square < (gridX || Infinity)) {
|
|
6568
|
+
const line = k - square * square;
|
|
6569
|
+
x = line <= square ? line : square;
|
|
6570
|
+
y = line <= square ? square : square - (line - square);
|
|
6571
|
+
} else {
|
|
6572
|
+
x = k % gridX;
|
|
6573
|
+
y = Math.floor(k / gridX);
|
|
6574
|
+
}
|
|
6575
|
+
}
|
|
6576
|
+
offset.set(d, [x * step, -y * step]);
|
|
6577
|
+
const e = extent.get(first.get(key));
|
|
6578
|
+
e[0] = Math.min(e[0], x * step); e[1] = Math.min(e[1], -y * step);
|
|
6579
|
+
e[2] = Math.max(e[2], x * step); e[3] = Math.max(e[3], -y * step);
|
|
6580
|
+
}
|
|
6581
|
+
return { offset, index, extent };
|
|
6582
|
+
}
|
|
6583
|
+
|
|
6584
|
+
// A pixel offset [dx, dy] (y down) from lngLat at MapLibre zoom `zoom`,
|
|
6585
|
+
// as lng/lat (Web Mercator, 512 px world tiles) — for shapes too large
|
|
6586
|
+
// for an icon, drawn in map coordinates (rebuilt with every refresh).
|
|
6587
|
+
function pixelOffsetLngLat(lngLat, offset, zoom) {
|
|
6588
|
+
const world = 512 * Math.pow(2, zoom);
|
|
6589
|
+
const lat0 = lngLat[1] * Math.PI / 180;
|
|
6590
|
+
const y = (0.5 - Math.log(Math.tan(Math.PI / 4 + lat0 / 2)) / (2 * Math.PI)) * world + offset[1];
|
|
6591
|
+
const lat = (2 * Math.atan(Math.exp((0.5 - y / world) * 2 * Math.PI)) - Math.PI / 2) * 180 / Math.PI;
|
|
6592
|
+
return [lngLat[0] + offset[0] * 360 / world, lat];
|
|
6593
|
+
}
|
|
6594
|
+
|
|
6275
6595
|
// CHART|SYMBOL|SEQUENCE (flat maptheme.js 21184-21300, 21790-21925,
|
|
6276
6596
|
// 22296-22395): one symbol per category part of a chart, in category
|
|
6277
6597
|
// order or sorted (SORT: DOWN biggest first, UP smallest first), parts
|
|
@@ -7194,9 +7514,13 @@
|
|
|
7194
7514
|
// set (e.g. roma: summed Pericolosita, normalized to 0..1).
|
|
7195
7515
|
// (flat's per-item nSize holds a record count in that last case, but it
|
|
7196
7516
|
// is not what draws a value-based symbol.)
|
|
7517
|
+
// "$item$" (as size or value) is flat's record count: 1 per record — it
|
|
7518
|
+
// is no column (read as one, every record was 0 and a LABEL|TEXTONLY
|
|
7519
|
+
// theme's text 0.08 px high).
|
|
7197
7520
|
function resolveAggregateValue(binding, flags, props) {
|
|
7521
|
+
if (binding.size === '$item$') return 1;
|
|
7198
7522
|
if (binding.size) return parseFloat(props[binding.size]) || 0;
|
|
7199
|
-
if (flags.has('CATEGORICAL') || binding.value == null) return 1;
|
|
7523
|
+
if (flags.has('CATEGORICAL') || binding.value == null || binding.value === '$item$') return 1;
|
|
7200
7524
|
return parseFloat(props[binding.value]) || 0;
|
|
7201
7525
|
}
|
|
7202
7526
|
|
|
@@ -7294,7 +7618,12 @@
|
|
|
7294
7618
|
key = `${snapped.x}:${snapped.y}`;
|
|
7295
7619
|
}
|
|
7296
7620
|
let cell = cells.get(key);
|
|
7297
|
-
if (!cell) { cell = { key, snapped, sumX: 0, sumY: 0, n: 0, value: 0, classValue: 0, value100: 0, series: null, first: f }; cells.set(key, cell); }
|
|
7621
|
+
if (!cell) { cell = { key, snapped, sumX: 0, sumY: 0, n: 0, value: 0, classValue: 0, value100: 0, series: null, first: f, raws: null }; cells.set(key, cell); }
|
|
7622
|
+
// the members' own records, for the tooltip's {{theme.item.data}}
|
|
7623
|
+
// (flat's __add_data tables them, numbered, up to 50)
|
|
7624
|
+
if (f.properties.raw && (!cell.raws || cell.raws.length < 50)) {
|
|
7625
|
+
(cell.raws = cell.raws || []).push(f.properties.raw);
|
|
7626
|
+
}
|
|
7298
7627
|
cell.sumX += p.x; cell.sumY += p.y; cell.n++;
|
|
7299
7628
|
cell.value += f.properties.value;
|
|
7300
7629
|
if (f.properties.classValue !== undefined) cell.classValue += f.properties.classValue;
|
|
@@ -7316,7 +7645,8 @@
|
|
|
7316
7645
|
// titleRaw: the cell's first record — flat titles an aggregated
|
|
7317
7646
|
// item by it (itemTitle)
|
|
7318
7647
|
? { cluster: true, point_count: cell.n, value: cell.value, ...(hasClass ? { classValue: cell.classValue } : {}),
|
|
7319
|
-
titleRaw: cell.first.properties.raw || cell.first.properties
|
|
7648
|
+
titleRaw: cell.first.properties.raw || cell.first.properties,
|
|
7649
|
+
...(cell.raws && cell.raws.length ? { raws: cell.raws } : {}) }
|
|
7320
7650
|
: cell.first.properties;
|
|
7321
7651
|
if (cell.series && cell.n > 1) props = Object.assign({}, props, { series: cell.series });
|
|
7322
7652
|
// a field aggregation's cell key (the field value): the categories of
|
|
@@ -7383,6 +7713,18 @@
|
|
|
7383
7713
|
if (!cell) { cell = { sumX: 0, sumY: 0, n: 0, cx: snapped.x, cy: snapped.y, counts: new Array(n).fill(0), recordCounts: new Array(n).fill(0), classTotal: undefined }; cells.set(key, cell); }
|
|
7384
7714
|
cell.sumX += p.x; cell.sumY += p.y; cell.n++;
|
|
7385
7715
|
if (!cell.titleRaw) cell.titleRaw = f.properties.titleRaw || f.properties.raw || null;
|
|
7716
|
+
// the members' own records, for the tooltip's {{theme.item.data}} —
|
|
7717
|
+
// flat's __add_data tables them (numbered, up to 50) for aggregated
|
|
7718
|
+
// items too; references only, so no copy is made. A member can itself
|
|
7719
|
+
// be a pre-aggregated cluster node, whose records it carries in raws
|
|
7720
|
+
if (!cell.raws) cell.raws = [];
|
|
7721
|
+
if (cell.raws.length < 50) {
|
|
7722
|
+
if (f.properties.raw) cell.raws.push(f.properties.raw);
|
|
7723
|
+
else if (f.properties.raws) for (const r of f.properties.raws) {
|
|
7724
|
+
if (cell.raws.length >= 50) break;
|
|
7725
|
+
cell.raws.push(r);
|
|
7726
|
+
}
|
|
7727
|
+
}
|
|
7386
7728
|
cell.counts[f.properties.cat] += f.properties.value;
|
|
7387
7729
|
if (f.properties.classValue !== undefined) cell.classTotal = (cell.classTotal || 0) + f.properties.classValue;
|
|
7388
7730
|
// point_count is the aggregated-record count of a multi-record
|
|
@@ -7397,6 +7739,7 @@
|
|
|
7397
7739
|
return {
|
|
7398
7740
|
geometry: { type: 'Point', coordinates: [ll.lng, ll.lat] },
|
|
7399
7741
|
properties: { counts: cell.counts, total: cell.counts.reduce((a, c) => a + c, 0), recordCounts: cell.recordCounts, ...(cell.classTotal !== undefined ? { classTotal: cell.classTotal } : {}),
|
|
7742
|
+
...(cell.raws && cell.raws.length ? { raws: cell.raws } : {}),
|
|
7400
7743
|
...(cell.titleRaw ? { titleRaw: cell.titleRaw } : {}) }
|
|
7401
7744
|
};
|
|
7402
7745
|
});
|
|
@@ -7441,13 +7784,25 @@
|
|
|
7441
7784
|
return count;
|
|
7442
7785
|
}
|
|
7443
7786
|
|
|
7787
|
+
// flat matches type flags as substrings (szFlag.match(/SYMBOL/)), so a
|
|
7788
|
+
// "SYMBOLS" flag (PNRR pages) is a SYMBOL chart there. With only the symbol
|
|
7789
|
+
// "label" (maptheme.js 21833: the value as text, TEXTONLY: text alone) it is
|
|
7790
|
+
// the LABEL chart this engine draws as _buildLabelChartLayers.
|
|
7791
|
+
function symbolsFlagCompat(flags, style) {
|
|
7792
|
+
if (!flags.has('SYMBOLS') || flags.has('SYMBOL') || flags.has('LABEL')) return flags;
|
|
7793
|
+
const out = new Set(flags);
|
|
7794
|
+
const symbols = [].concat((style && style.symbols) || []);
|
|
7795
|
+
out.add(symbols.length && symbols.every(v => String(v) === 'label') ? 'LABEL' : 'SYMBOL');
|
|
7796
|
+
return out;
|
|
7797
|
+
}
|
|
7798
|
+
|
|
7444
7799
|
class LayerRuntime {
|
|
7445
7800
|
// spec: a normalizeTheme() result — never a raw LayerBuilder
|
|
7446
7801
|
constructor(spec, fc, mapOptions) {
|
|
7447
7802
|
this.name = spec.name;
|
|
7448
7803
|
this.binding = spec.binding;
|
|
7449
|
-
this.flags = spec.flags;
|
|
7450
7804
|
this.style = spec.style;
|
|
7805
|
+
this.flags = symbolsFlagCompat(spec.flags, spec.style);
|
|
7451
7806
|
this.meta = spec.meta;
|
|
7452
7807
|
// GL-PORT COMPAT: retained only for ixmaps.getThemeObj()'s szFilter
|
|
7453
7808
|
// (real ixmaps-flat global compat shim, see _globalThemeRegistry) —
|
|
@@ -7481,6 +7836,7 @@
|
|
|
7481
7836
|
// ICON_ATLAS_RESET_AFTER for why this is necessary at all).
|
|
7482
7837
|
this._iconGeneration = 0;
|
|
7483
7838
|
this._iconsSinceAtlasReset = 0;
|
|
7839
|
+
this._iconAreaSinceAtlasReset = 0;
|
|
7484
7840
|
this._prepare();
|
|
7485
7841
|
}
|
|
7486
7842
|
|
|
@@ -7503,9 +7859,11 @@
|
|
|
7503
7859
|
_cacheIcon(key, icon) {
|
|
7504
7860
|
if (!this._iconCache.has(key)) {
|
|
7505
7861
|
this._iconsSinceAtlasReset++;
|
|
7506
|
-
|
|
7862
|
+
this._iconAreaSinceAtlasReset += (icon.width || 0) * (icon.height || 0);
|
|
7863
|
+
if (this._iconsSinceAtlasReset > ICON_ATLAS_RESET_AFTER || this._iconAreaSinceAtlasReset > ICON_ATLAS_AREA_RESET_AFTER) {
|
|
7507
7864
|
this._iconGeneration++;
|
|
7508
7865
|
this._iconsSinceAtlasReset = 0;
|
|
7866
|
+
this._iconAreaSinceAtlasReset = 0;
|
|
7509
7867
|
}
|
|
7510
7868
|
}
|
|
7511
7869
|
if (this._iconCache.size >= ICON_CACHE_MAX) {
|
|
@@ -7646,6 +8004,10 @@
|
|
|
7646
8004
|
// a page could in principle use both at once, though no current
|
|
7647
8005
|
// ported page does.
|
|
7648
8006
|
this._runtimeFilterExpr = '';
|
|
8007
|
+
// flat's setThemeTimeFrame(id, min, max): records whose style.timefield
|
|
8008
|
+
// value lies in [min, max) — null = no time filter. AND'd with the
|
|
8009
|
+
// filters above in _rebuildActiveFeatures.
|
|
8010
|
+
this._timeFrame = null;
|
|
7649
8011
|
|
|
7650
8012
|
// GRIDSIZE layers (PLOT curves-chart, or its grid-mesh companion)
|
|
7651
8013
|
// also carry AGGREGATE in their type string, but they bin into a
|
|
@@ -7842,6 +8204,44 @@
|
|
|
7842
8204
|
this._rebuildActiveFeatures();
|
|
7843
8205
|
}
|
|
7844
8206
|
|
|
8207
|
+
// setThemeTimeFrame: min/max in ms or any Date-parsable value; a null /
|
|
8208
|
+
// NaN bound is open-ended, both null clears the frame.
|
|
8209
|
+
setTimeFrame(min, max) {
|
|
8210
|
+
const toMs = v => (v == null || v === '' ? NaN : (typeof v === 'number' ? v : new Date(v).getTime()));
|
|
8211
|
+
const lo = toMs(min), hi = toMs(max);
|
|
8212
|
+
// flat: with the slider at its start position (frame starts at or
|
|
8213
|
+
// before the earliest record) every item is shown, not just the first window
|
|
8214
|
+
const field = this.binding && this.binding.time;
|
|
8215
|
+
if (field && !isNaN(lo)) {
|
|
8216
|
+
if (this._timeMin === undefined) {
|
|
8217
|
+
let m = Infinity;
|
|
8218
|
+
for (const f of this.features) {
|
|
8219
|
+
const v = f.properties && f.properties[field];
|
|
8220
|
+
const t = typeof v === 'number' ? v : new Date(v).getTime();
|
|
8221
|
+
if (!isNaN(t) && t < m) m = t;
|
|
8222
|
+
}
|
|
8223
|
+
this._timeMin = m;
|
|
8224
|
+
}
|
|
8225
|
+
if (lo <= this._timeMin) { this._timeFrame = null; this._rebuildActiveFeatures(); return; }
|
|
8226
|
+
}
|
|
8227
|
+
this._timeFrame = isNaN(lo) && isNaN(hi) ? null : { min: lo, max: hi };
|
|
8228
|
+
this._rebuildActiveFeatures();
|
|
8229
|
+
}
|
|
8230
|
+
|
|
8231
|
+
// predicate over a record's properties for the active time frame
|
|
8232
|
+
_timePredicate() {
|
|
8233
|
+
const tf = this._timeFrame;
|
|
8234
|
+
if (!tf) return null;
|
|
8235
|
+
const field = this.binding && this.binding.time;
|
|
8236
|
+
if (!field) return null;
|
|
8237
|
+
return props => {
|
|
8238
|
+
const v = props && props[field];
|
|
8239
|
+
const t = typeof v === 'number' ? v : new Date(v).getTime();
|
|
8240
|
+
if (isNaN(t)) return false;
|
|
8241
|
+
return (isNaN(tf.min) || t >= tf.min) && (isNaN(tf.max) || t < tf.max);
|
|
8242
|
+
};
|
|
8243
|
+
}
|
|
8244
|
+
|
|
7845
8245
|
// `_featuresByCategory` (plain per-category bucketing — NOT yet
|
|
7846
8246
|
// grid-aggregated; that's a separate, later step gated by AGGREGATE
|
|
7847
8247
|
// alone, see _computeAggregatedItems) is needed whenever this runtime
|
|
@@ -7872,7 +8272,8 @@
|
|
|
7872
8272
|
replaceFeatures(fc) {
|
|
7873
8273
|
// new data → new deck.gl layers (see _dataLayerId)
|
|
7874
8274
|
this._dataGen = (this._dataGen || 0) + 1;
|
|
7875
|
-
|
|
8275
|
+
this._timeMin = undefined;
|
|
8276
|
+
const facetFilters = this.facetFilters, runtimeFilterExpr = this._runtimeFilterExpr, timeFrame = this._timeFrame;
|
|
7876
8277
|
this.binding = this._specBinding;
|
|
7877
8278
|
this.features = applyField100(filterFlatValues(this._withDensity(fc.features), this.binding, this.flags), this.binding, this.flags, this.style);
|
|
7878
8279
|
this.binding = field100Binding(this.binding, this.flags);
|
|
@@ -7882,6 +8283,7 @@
|
|
|
7882
8283
|
this._prepare();
|
|
7883
8284
|
this.facetFilters = facetFilters;
|
|
7884
8285
|
this._runtimeFilterExpr = runtimeFilterExpr;
|
|
8286
|
+
this._timeFrame = timeFrame;
|
|
7885
8287
|
this._rebuildActiveFeatures();
|
|
7886
8288
|
}
|
|
7887
8289
|
|
|
@@ -7894,6 +8296,27 @@
|
|
|
7894
8296
|
// the next redraw.
|
|
7895
8297
|
_dataLayerId(base) { return this._dataGen ? `${base}-d${this._dataGen}` : base; }
|
|
7896
8298
|
|
|
8299
|
+
// deck.gl compares a layer's data by reference: a new object makes it
|
|
8300
|
+
// re-process every polygon (on the PNRR page ~300 ms per refresh for
|
|
8301
|
+
// 8 000 comuni, at every pan tick). Polygon data that doesn't depend
|
|
8302
|
+
// on the view is therefore handed over as the same object until its
|
|
8303
|
+
// inputs change.
|
|
8304
|
+
_featureCollection() {
|
|
8305
|
+
if (!this._featuresFC || this._featuresFC.features !== this.features) {
|
|
8306
|
+
this._featuresFC = { type: 'FeatureCollection', features: this.features };
|
|
8307
|
+
}
|
|
8308
|
+
return this._featuresFC;
|
|
8309
|
+
}
|
|
8310
|
+
// the same for derived data: build() runs only when an entry of key
|
|
8311
|
+
// (compared by identity) differs from the last call's
|
|
8312
|
+
_cachedPolygonData(slot, key, build) {
|
|
8313
|
+
const last = this[slot];
|
|
8314
|
+
if (last && last.key.length === key.length && last.key.every((v, i) => v === key[i])) return last.data;
|
|
8315
|
+
const data = build();
|
|
8316
|
+
this[slot] = { key, data };
|
|
8317
|
+
return data;
|
|
8318
|
+
}
|
|
8319
|
+
|
|
7897
8320
|
_usesAggregationIndex() {
|
|
7898
8321
|
return (this.flags.has('AGGREGATE') || isSymbolChart(this.flags)) && !this.flags.has('GRIDSIZE');
|
|
7899
8322
|
}
|
|
@@ -7905,8 +8328,10 @@
|
|
|
7905
8328
|
if (isAggregatedCategoricalChoropleth(this)) {
|
|
7906
8329
|
const expr = this._runtimeFilterExpr;
|
|
7907
8330
|
const clauses = Array.from(this.facetFilters.entries());
|
|
7908
|
-
|
|
7909
|
-
|
|
8331
|
+
const timePred = this._timePredicate();
|
|
8332
|
+
if (!expr && !clauses.length && !timePred) { this._activeFeatures = null; return; }
|
|
8333
|
+
const wherePred = expr ? flatFilterPredicate(String(expr)) : null;
|
|
8334
|
+
const pred = wherePred && timePred ? (r => wherePred(r) && timePred(r)) : (wherePred || timePred);
|
|
7910
8335
|
this._activeFeatures = this.features.map(f => {
|
|
7911
8336
|
const rows = f.properties && f.properties[AGGREGATED_ROWS];
|
|
7912
8337
|
if (!rows) return f;
|
|
@@ -7924,11 +8349,13 @@
|
|
|
7924
8349
|
// comment) if one is active — reusing applyWhereFilter, the SAME
|
|
7925
8350
|
// parser the layer's load-time .filter(expr) already uses, just
|
|
7926
8351
|
// invoked again here instead of once at build time.
|
|
7927
|
-
|
|
8352
|
+
let base = this._runtimeFilterExpr
|
|
7928
8353
|
? applyWhereFilter({ type: 'FeatureCollection', features: this.features }, this._runtimeFilterExpr).features
|
|
7929
8354
|
: this.features;
|
|
8355
|
+
const timePred = this._timePredicate();
|
|
8356
|
+
if (timePred) base = base.filter(f => timePred(f.properties));
|
|
7930
8357
|
if (this.facetFilters.size === 0) {
|
|
7931
|
-
this._activeFeatures = this._runtimeFilterExpr ? base : null;
|
|
8358
|
+
this._activeFeatures = (this._runtimeFilterExpr || timePred) ? base : null;
|
|
7932
8359
|
} else {
|
|
7933
8360
|
const clauses = Array.from(this.facetFilters.entries());
|
|
7934
8361
|
this._activeFeatures = base.filter(f =>
|
|
@@ -8298,10 +8725,11 @@
|
|
|
8298
8725
|
// formatting) instead of an inline SVG chart — one line per present
|
|
8299
8726
|
// category, each showing ITS OWN value, never summed across
|
|
8300
8727
|
// categories (same rule as the VALUES bubble labels).
|
|
8301
|
-
// - theme.item.data (a table of the hovered record's raw fields)
|
|
8302
|
-
//
|
|
8303
|
-
//
|
|
8304
|
-
//
|
|
8728
|
+
// - theme.item.data (a table of the hovered record's raw fields)
|
|
8729
|
+
// tables an individual point's record, and an aggregated item's
|
|
8730
|
+
// member records (numbered, up to 50 — flat's __add_data does the
|
|
8731
|
+
// same); only a group without retained records (cluster-level
|
|
8732
|
+
// groups) resolves to an empty string there.
|
|
8305
8733
|
// an item's title as flat gives it (maptheme.js 11838-11886): the title
|
|
8306
8734
|
// field of its record — of an aggregated item its first record; an
|
|
8307
8735
|
// aggregated grid cell (no aggregation field) of more than 3 records,
|
|
@@ -8317,7 +8745,7 @@
|
|
|
8317
8745
|
return title;
|
|
8318
8746
|
}
|
|
8319
8747
|
|
|
8320
|
-
buildTooltipHtml(object) {
|
|
8748
|
+
buildTooltipHtml(object, fDark) {
|
|
8321
8749
|
// no .meta({tooltip}): flat's default template (tooltip_mustache.js 394)
|
|
8322
8750
|
// — the theme title, the item's title and its chart
|
|
8323
8751
|
const template = this.meta.tooltip || FLAT_DEFAULT_TOOLTIP;
|
|
@@ -8326,11 +8754,16 @@
|
|
|
8326
8754
|
'include https://unpkg.com/mustache@4.2.0/mustache.min.js before ixmaps-gl.js.');
|
|
8327
8755
|
return null;
|
|
8328
8756
|
}
|
|
8329
|
-
return global.Mustache.render(template, this._buildTooltipContext(object));
|
|
8757
|
+
return youtubeClickToPlay(global.Mustache.render(template, this._buildTooltipContext(object, fDark)));
|
|
8330
8758
|
}
|
|
8331
8759
|
|
|
8332
|
-
_buildTooltipContext(object) {
|
|
8333
|
-
|
|
8760
|
+
_buildTooltipContext(object, fDark) {
|
|
8761
|
+
let props = object.properties;
|
|
8762
|
+
// a FEATURE|CATEGORICAL polygon is the raw data record itself, not the
|
|
8763
|
+
// {raw, cat, value} wrapper the other layer kinds build
|
|
8764
|
+
if (props && !props.raw && (this.flags.has('FEATURE') || this.flags.has('FEATURES')) && this.flags.has('CATEGORICAL')) {
|
|
8765
|
+
props = { raw: props, cat: this._resolveClassIndex(props[this.binding.value]) };
|
|
8766
|
+
}
|
|
8334
8767
|
const isGroup = !!props.counts;
|
|
8335
8768
|
const chartHtml = this._renderItemChartHtml(props);
|
|
8336
8769
|
|
|
@@ -8342,7 +8775,7 @@
|
|
|
8342
8775
|
chart: chartHtml,
|
|
8343
8776
|
item: {
|
|
8344
8777
|
chart: chartHtml,
|
|
8345
|
-
data: isGroup ?
|
|
8778
|
+
data: isGroup ? this._renderGroupDataTableHtml(props.raws, fDark) : this._renderItemDataTableHtml(props.raw, fDark),
|
|
8346
8779
|
value: this._hasValue(isGroup ? props.total : props.value) ? this._formatTooltipValue(isGroup ? props.total : props.value) : '',
|
|
8347
8780
|
label: isGroup ? '' : (this.categoryDisplayLabels ? (this.categoryDisplayLabels[props.cat] || '') : ''),
|
|
8348
8781
|
// flat: the item's title is its titlefield value (binding title/
|
|
@@ -8485,14 +8918,49 @@
|
|
|
8485
8918
|
}
|
|
8486
8919
|
|
|
8487
8920
|
// theme.item.data approximation — a plain field:value table of the
|
|
8488
|
-
// hovered record's own raw properties (individual points only)
|
|
8489
|
-
|
|
8921
|
+
// hovered record's own raw properties (individual points only) — flat's
|
|
8922
|
+
// own row markup (tooltip_mustache.js 568-590, main.css .tooltip): the
|
|
8923
|
+
// field name right-aligned, the value left, a dotted rule above the
|
|
8924
|
+
// value, the table at 0.85em with flat's min-width. Light is flat
|
|
8925
|
+
// exactly (#cccccc labels, #000000 values, dotted black); dark keeps
|
|
8926
|
+
// the same visibility hierarchy inverted - DIM labels, BRIGHT values -
|
|
8927
|
+
// with its own colors, a dotted #555 rule and #888 record pills
|
|
8928
|
+
_renderItemDataTableHtml(raw, fDark) {
|
|
8490
8929
|
if (!raw) return '';
|
|
8930
|
+
const value = fDark ? '#e0e4ea' : '#000000';
|
|
8931
|
+
const label = fDark ? '#787d85' : '#cccccc';
|
|
8932
|
+
const rule = fDark ? '#555' : 'black';
|
|
8491
8933
|
const rows = Object.keys(raw)
|
|
8492
8934
|
.filter(k => k !== 'geometry')
|
|
8493
|
-
.map(k => `<tr><td style="
|
|
8935
|
+
.map(k => `<tr><td></td><td style="text-align:right;vertical-align:top;color:${label}">${k}</td>` +
|
|
8936
|
+
`<td style="text-align:left;vertical-align:top;color:${value};padding:0 0.2em 0 0.8em;border-top:dotted ${rule} 0.1px">` +
|
|
8937
|
+
`${this._isNumericValue(raw[k]) ? this._formatTooltipValue(raw[k]) : raw[k]}</td></tr>`)
|
|
8494
8938
|
.join('');
|
|
8495
|
-
return `<table style="font-size:0.
|
|
8939
|
+
return `<table style="font-size:0.85em;min-width:300px;border-collapse:collapse">${rows}</table>`;
|
|
8940
|
+
}
|
|
8941
|
+
|
|
8942
|
+
// an aggregated item's own records, numbered, up to 50 — like real
|
|
8943
|
+
// flat's __add_data (tooltip_mustache.js 490-570: one numbered block
|
|
8944
|
+
// per record when the item holds more than one). A single-record
|
|
8945
|
+
// group renders like an individual point.
|
|
8946
|
+
_renderGroupDataTableHtml(raws, fDark) {
|
|
8947
|
+
if (!raws || !raws.length) return '';
|
|
8948
|
+
if (raws.length === 1) return this._renderItemDataTableHtml(raws[0], fDark);
|
|
8949
|
+
const value = fDark ? '#e0e4ea' : '#000000';
|
|
8950
|
+
const label = fDark ? '#787d85' : '#cccccc';
|
|
8951
|
+
const rule = fDark ? '#555' : 'black';
|
|
8952
|
+
const pill = fDark ? '#888' : 'black';
|
|
8953
|
+
const rows = raws.slice(0, 50).map((raw, d) =>
|
|
8954
|
+
'<tr><td style="font-size:1em;"> </td></tr>' +
|
|
8955
|
+
`<tr><td style="font-size:1.5em;border:solid ${pill} 1px;border-radius:1em;padding:0.1em 0.4em 0em 0.3em">${d + 1}</td></tr>` +
|
|
8956
|
+
Object.keys(raw)
|
|
8957
|
+
.filter(k => k !== 'geometry')
|
|
8958
|
+
.map(k => `<tr><td></td><td style="text-align:right;vertical-align:top;color:${label}">${k}</td>` +
|
|
8959
|
+
`<td style="text-align:left;vertical-align:top;color:${value};padding:0 0.2em 0 0.8em;border-top:dotted ${rule} 0.1px">` +
|
|
8960
|
+
`${this._isNumericValue(raw[k]) ? this._formatTooltipValue(raw[k]) : raw[k]}</td></tr>`)
|
|
8961
|
+
.join('')
|
|
8962
|
+
).join('');
|
|
8963
|
+
return `<table style="font-size:0.85em;min-width:300px;border-collapse:collapse">${rows}</table>`;
|
|
8496
8964
|
}
|
|
8497
8965
|
|
|
8498
8966
|
_isNumericValue(v) {
|
|
@@ -8619,11 +9087,19 @@
|
|
|
8619
9087
|
const cs = this.style.colorscheme;
|
|
8620
9088
|
const raw = Array.isArray(cs) ? cs[0] : cs;
|
|
8621
9089
|
const filled = raw !== 'none';
|
|
9090
|
+
// FEATURE|CATEGORICAL: polygons coloured by their category (flat colours
|
|
9091
|
+
// each shape by its class), not one flat fill
|
|
9092
|
+
const byCategory = filled && this.flags.has('CATEGORICAL') && this.categoryIndexByLabel && this.binding.value
|
|
9093
|
+
&& this.categoryColorsRgb && this.categoryColorsRgb.length;
|
|
9094
|
+
const catAlpha = Math.round(255 * (styleNum(this.style.fillopacity) || 1));
|
|
9095
|
+
const nodataRgb = parseCssColor(this.style.nodatacolor) || [255, 255, 255];
|
|
8622
9096
|
const shadow = filled && flatShadowOn(this.style, this.features.length, zoom)
|
|
8623
9097
|
? this._buildShadowLayers(hexOrNamedToRgb(raw), zoom, bbox) : [];
|
|
8624
9098
|
return shadow.concat([new GeoJsonLayer({
|
|
8625
9099
|
id: this._dataLayerId(`ix-features-${this.name}`),
|
|
8626
|
-
data:
|
|
9100
|
+
data: this._featureCollection(),
|
|
9101
|
+
// FEATURE|CATEGORICAL polygons carry the data record: hover / click tooltips
|
|
9102
|
+
pickable: !!byCategory,
|
|
8627
9103
|
// linecolor "none": no outline (a FEATURES theme of points with
|
|
8628
9104
|
// colorscheme and linecolor "none" draws nothing, as flat)
|
|
8629
9105
|
stroked: styleLineColor(this.style.linecolor) !== 'none',
|
|
@@ -8634,7 +9110,13 @@
|
|
|
8634
9110
|
// flat puts fillopacity into the shape's fill-opacity only
|
|
8635
9111
|
// (maptheme.js 13407, 14071-14078): the outline stays opaque
|
|
8636
9112
|
// (a fill color's own alpha — rgba(…, 0.5) — times fillopacity)
|
|
8637
|
-
|
|
9113
|
+
...(byCategory ? { updateTriggers: { getFillColor: JSON.stringify(this.categoryColorsRgb) } } : {}),
|
|
9114
|
+
getFillColor: byCategory
|
|
9115
|
+
? f => {
|
|
9116
|
+
const cat = this._resolveClassIndex(f.properties[this.binding.value]);
|
|
9117
|
+
return [...(cat != null ? this.categoryColorsRgb[cat] : nodataRgb).slice(0, 3), catAlpha];
|
|
9118
|
+
}
|
|
9119
|
+
: filled ? [...hexOrNamedToRgb(raw).slice(0, 3), Math.round(255 * cssColorAlpha(raw) * (styleNum(this.style.fillopacity) || 1))] : [0, 0, 0, 0],
|
|
8638
9120
|
getLineColor: this.style.linecolor
|
|
8639
9121
|
? withAlpha(hexOrNamedToRgb(styleLineColor(this.style.linecolor)), cssColorAlpha(styleLineColor(this.style.linecolor)))
|
|
8640
9122
|
: [130, 130, 130],
|
|
@@ -8708,8 +9190,8 @@
|
|
|
8708
9190
|
// single winning class, so no `cat`/tooltip chart line, just the
|
|
8709
9191
|
// blended color directly); AGGREGATE CATEGORICAL on one field — the
|
|
8710
9192
|
// dominant category of each polygon's records (aggregatedCategoricalClass).
|
|
8711
|
-
// A plain CATEGORICAL choropleth
|
|
8712
|
-
//
|
|
9193
|
+
// A plain CATEGORICAL choropleth matches the raw value of the polygon's
|
|
9194
|
+
// row against the category labels. Geometry + properties are already the joined
|
|
8713
9195
|
// FeatureCollection from joinChoroplethFeatures. `zoom` is only used
|
|
8714
9196
|
// for style.fillopacity:"auto" (see resolveAutoFillOpacity) — every
|
|
8715
9197
|
// other branch here is zoom-independent.
|
|
@@ -8735,65 +9217,76 @@
|
|
|
8735
9217
|
// AGGREGATE CATEGORICAL: every polygon's parts and dominant class
|
|
8736
9218
|
// (aggregatedCategoricalClass), the biggest part per class over all
|
|
8737
9219
|
// polygons for the opacity (flat's nMaxA)
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
if (!rows) return;
|
|
8747
|
-
const agg = aggregatedCategoricalClass(rows, this.binding, this.flags, this.categoryIndexByLabel, nCats);
|
|
8748
|
-
if (!agg) return;
|
|
8749
|
-
aggOf.set(f, agg);
|
|
8750
|
-
agg.parts.forEach((v, i) => { if (v > aggMaxA[i]) aggMaxA[i] = v; });
|
|
8751
|
-
});
|
|
8752
|
-
}
|
|
8753
|
-
const data = source.map(f => {
|
|
9220
|
+
// the polygons' classes don't depend on the view or on marked
|
|
9221
|
+
// classes (those only change the colors, see updateTriggers below):
|
|
9222
|
+
// the data is rebuilt only when its inputs change
|
|
9223
|
+
const dataKey = [source, this._dataGen, this.categoryIndexByLabel, this.partsA, this._classSeparate,
|
|
9224
|
+
dopacityActive ? baseOpacity : null, JSON.stringify(this.style), JSON.stringify(this.binding)];
|
|
9225
|
+
const collection = this._cachedPolygonData('_choroplethData', dataKey, () => {
|
|
9226
|
+
const aggCat = isAggregatedCategoricalChoropleth(this) && this.categoryIndexByLabel;
|
|
9227
|
+
let aggOf = null, aggMaxA = null;
|
|
8754
9228
|
if (aggCat) {
|
|
8755
|
-
const
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
properties
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
if (isComposeColor) {
|
|
8767
|
-
return {
|
|
8768
|
-
type: 'Feature',
|
|
8769
|
-
geometry: f.geometry,
|
|
8770
|
-
properties: { raw: f.properties, composedColor: this._resolveComposedColor(f.properties) }
|
|
8771
|
-
};
|
|
9229
|
+
const nCats = this.categoryLabels.length;
|
|
9230
|
+
aggOf = new Map();
|
|
9231
|
+
aggMaxA = new Array(nCats).fill(0);
|
|
9232
|
+
source.forEach(f => {
|
|
9233
|
+
const rows = f.properties && f.properties[AGGREGATED_ROWS];
|
|
9234
|
+
if (!rows) return;
|
|
9235
|
+
const agg = aggregatedCategoricalClass(rows, this.binding, this.flags, this.categoryIndexByLabel, nCats);
|
|
9236
|
+
if (!agg) return;
|
|
9237
|
+
aggOf.set(f, agg);
|
|
9238
|
+
agg.parts.forEach((v, i) => { if (v > aggMaxA[i]) aggMaxA[i] = v; });
|
|
9239
|
+
});
|
|
8772
9240
|
}
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
9241
|
+
const data = source.map(f => {
|
|
9242
|
+
if (aggCat) {
|
|
9243
|
+
const agg = aggOf.get(f);
|
|
9244
|
+
return {
|
|
9245
|
+
type: 'Feature',
|
|
9246
|
+
geometry: f.geometry,
|
|
9247
|
+
properties: {
|
|
9248
|
+
// no part above 0: no item in flat, not painted
|
|
9249
|
+
value: agg ? agg.value : undefined, raw: agg ? f.properties : {}, cat: agg ? agg.index : null, parts: agg ? agg.parts : undefined,
|
|
9250
|
+
dopacityAlpha: agg && dopacityActive ? dominantDopacityAlpha(this.style, this.flags, agg.value, aggMaxA[agg.index]) : null
|
|
9251
|
+
}
|
|
9252
|
+
};
|
|
9253
|
+
}
|
|
9254
|
+
if (isComposeColor) {
|
|
9255
|
+
return {
|
|
9256
|
+
type: 'Feature',
|
|
9257
|
+
geometry: f.geometry,
|
|
9258
|
+
properties: { raw: f.properties, composedColor: this._resolveComposedColor(f.properties) }
|
|
9259
|
+
};
|
|
9260
|
+
}
|
|
9261
|
+
if (isDominant) {
|
|
9262
|
+
const dom = this._resolveDominantClass(f.properties);
|
|
9263
|
+
const value = dom ? dom.value : undefined;
|
|
9264
|
+
return {
|
|
9265
|
+
type: 'Feature',
|
|
9266
|
+
geometry: f.geometry,
|
|
9267
|
+
properties: {
|
|
9268
|
+
value, raw: f.properties, cat: dom ? dom.index : null,
|
|
9269
|
+
dopacityAlpha: dopacityActive ? this._resolveDopacityAlpha(f, value, baseOpacity) : null
|
|
9270
|
+
}
|
|
9271
|
+
};
|
|
9272
|
+
}
|
|
9273
|
+
// a polygon without a data row is no item in flat: no value
|
|
9274
|
+
const hasRow = f.properties && Object.keys(f.properties).length > 0;
|
|
9275
|
+
// CATEGORICAL matches the raw value against the category labels
|
|
9276
|
+
// (a text value has no numeric form: flatNumber would give NaN)
|
|
9277
|
+
const rawVal = hasRow ? f.properties[this.binding.value] : undefined;
|
|
9278
|
+
const value = this.categoryIndexByLabel ? rawVal : (hasRow ? flatNumber(rawVal, this.flags) : NaN);
|
|
9279
|
+
const cat = this._resolveClassIndex(value);
|
|
8776
9280
|
return {
|
|
8777
9281
|
type: 'Feature',
|
|
8778
9282
|
geometry: f.geometry,
|
|
8779
9283
|
properties: {
|
|
8780
|
-
value, raw: f.properties, cat
|
|
9284
|
+
value, raw: f.properties, cat,
|
|
8781
9285
|
dopacityAlpha: dopacityActive ? this._resolveDopacityAlpha(f, value, baseOpacity) : null
|
|
8782
9286
|
}
|
|
8783
9287
|
};
|
|
8784
|
-
}
|
|
8785
|
-
|
|
8786
|
-
const hasRow = f.properties && Object.keys(f.properties).length > 0;
|
|
8787
|
-
const value = hasRow ? flatNumber(f.properties[this.binding.value], this.flags) : NaN;
|
|
8788
|
-
const cat = this._resolveClassIndex(value);
|
|
8789
|
-
return {
|
|
8790
|
-
type: 'Feature',
|
|
8791
|
-
geometry: f.geometry,
|
|
8792
|
-
properties: {
|
|
8793
|
-
value, raw: f.properties, cat,
|
|
8794
|
-
dopacityAlpha: dopacityActive ? this._resolveDopacityAlpha(f, value, baseOpacity) : null
|
|
8795
|
-
}
|
|
8796
|
-
};
|
|
9288
|
+
});
|
|
9289
|
+
return { type: 'FeatureCollection', features: data };
|
|
8797
9290
|
});
|
|
8798
9291
|
// A marked class (legend row click / ixmaps.markThemeClass) as real
|
|
8799
9292
|
// ixmaps-flat shows it on a choropleth (MapTheme.markClass,
|
|
@@ -8815,9 +9308,13 @@
|
|
|
8815
9308
|
return evidenceMode === 'isolate' ? 'hide' : 'gray';
|
|
8816
9309
|
};
|
|
8817
9310
|
const grayOf = ([r, g, b]) => { const y = Math.round(0.299 * r + 0.587 * g + 0.114 * b); return [y, y, y]; };
|
|
9311
|
+
// deck.gl re-runs the color accessors only for new data or a changed trigger
|
|
9312
|
+
const colorKey = JSON.stringify([[...this._markedClasses], !!this._onMarksChanged, evidenceMode,
|
|
9313
|
+
this.categoryColorsRgb, fallbackRgb, classAlpha, lineRgb]);
|
|
8818
9314
|
return [new GeoJsonLayer({
|
|
8819
9315
|
id: this._dataLayerId(`ix-choropleth-${this.name}`),
|
|
8820
|
-
data:
|
|
9316
|
+
data: collection,
|
|
9317
|
+
updateTriggers: { getFillColor: colorKey, getLineColor: colorKey },
|
|
8821
9318
|
pickable: true,
|
|
8822
9319
|
stroked: true,
|
|
8823
9320
|
filled: true,
|
|
@@ -9890,7 +10387,11 @@
|
|
|
9890
10387
|
this._valueMedian = sorted[Math.floor((sorted.length - 1) / 2)];
|
|
9891
10388
|
}
|
|
9892
10389
|
|
|
9893
|
-
|
|
10390
|
+
// flat's default normalsizevalue is the largest aggregated size (maptheme.js 5021-5022)
|
|
10391
|
+
let sizeMax = 0;
|
|
10392
|
+
for (const v of rawSizeTotals) if (Math.abs(v) > sizeMax) sizeMax = Math.abs(v);
|
|
10393
|
+
|
|
10394
|
+
this._aggregateStatsCache = { zoom, outlier, normalize, breaks, sizeMax };
|
|
9894
10395
|
return this._aggregateStatsCache;
|
|
9895
10396
|
}
|
|
9896
10397
|
|
|
@@ -9914,6 +10415,12 @@
|
|
|
9914
10415
|
// _computeAggregatedItems below — a plain (non-AGGREGATE)
|
|
9915
10416
|
// BUBBLE/CHART theme never reads them.
|
|
9916
10417
|
if (this.flags.has('AGGREGATE')) this._ensureClusterIndices(zoom);
|
|
10418
|
+
// AGGREGATE sums without a normalsizevalue: flat's default is the
|
|
10419
|
+
// largest aggregated value (defaultNormalSizeValue leaves it unset, the
|
|
10420
|
+
// sizes depend on the aggregation)
|
|
10421
|
+
if (this.flags.has('AGGREGATE') && !this.flags.has('CATEGORICAL') && !styleNum(this.style.normalsizevalue) && !this.binding.size) {
|
|
10422
|
+
this._maxSizeValue = this._ensureAggregateStats(zoom).sizeMax || undefined;
|
|
10423
|
+
}
|
|
9917
10424
|
let { individual, groups } = this._computeAggregatedItems(bbox, zoom);
|
|
9918
10425
|
// Far-hemisphere cull under globe projection — see
|
|
9919
10426
|
// isOnVisibleHemisphere's own comment for why this is needed at
|
|
@@ -9930,7 +10437,7 @@
|
|
|
9930
10437
|
}
|
|
9931
10438
|
const layers = [];
|
|
9932
10439
|
|
|
9933
|
-
if (this.flags.has('NOOUTLIER')
|
|
10440
|
+
if (this.flags.has('NOOUTLIER')|| this.flags.has('NORMALIZE') || this._rangeClassed) {
|
|
9934
10441
|
const stats = this._ensureAggregateStats(zoom);
|
|
9935
10442
|
|
|
9936
10443
|
// the quantity NOOUTLIER/NORMALIZE/classes act on: the separate
|
|
@@ -10041,6 +10548,10 @@
|
|
|
10041
10548
|
// _buildTooltipContext's own `isGroup` check already uses).
|
|
10042
10549
|
const sizeValueOf = d => d.properties.counts ? d.properties.total : d.properties.value;
|
|
10043
10550
|
const combined = individual.concat(groups).sort((a, b) => sizeValueOf(a) - sizeValueOf(b));
|
|
10551
|
+
// maxcharts: like the flat engine (maptheme.js nMaxCharts), draw only the
|
|
10552
|
+
// biggest N items — combined is ascending, so drop from the front
|
|
10553
|
+
const maxCharts = Math.round(styleNum(this.style.maxcharts)) || 0;
|
|
10554
|
+
if (maxCharts > 0 && combined.length > maxCharts) combined.splice(0, combined.length - maxCharts);
|
|
10044
10555
|
if (this.flags.has('USER')) {
|
|
10045
10556
|
const userLayers = this._buildUserChartLayers(combined, liveZoom, sizeValueOf);
|
|
10046
10557
|
if (userLayers) return layers.concat(userLayers);
|
|
@@ -10063,7 +10574,16 @@
|
|
|
10063
10574
|
const itemRgb = d => this.categoryColorsRgb[d.properties.counts ? dominant(d.properties.counts) : d.properties.cat] || [128, 128, 128];
|
|
10064
10575
|
const radiusOf = d => valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue);
|
|
10065
10576
|
// align / offsetx / offsety: the whole chart moves (flatChartAlignOffset)
|
|
10066
|
-
|
|
10577
|
+
// MULTIQUAD / MULTISQUARE: the items at one position side by side
|
|
10578
|
+
// (multiQuadOffsets), added to the chart position
|
|
10579
|
+
const baseAlign = this._chartAlignFn(liveZoom, radiusOf);
|
|
10580
|
+
const multi = flatFlag(this.flags, 'MULTIQUAD') || flatFlag(this.flags, 'MULTISQUARE')
|
|
10581
|
+
? multiQuadOffsets(combined, NORMAL_RADIUS_PX * objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1),
|
|
10582
|
+
styleNum(this.style.rangescale) || 1, styleNum(this.style.gridx) || 0)
|
|
10583
|
+
: null;
|
|
10584
|
+
const alignOf = multi
|
|
10585
|
+
? Object.assign(d => { const a = baseAlign(d), m = multi.offset.get(d) || [0, 0]; return [a[0] + m[0], a[1] + m[1]]; }, { active: true })
|
|
10586
|
+
: baseAlign;
|
|
10067
10587
|
|
|
10068
10588
|
// SHADOW (style.shadow, flat's gate: maxshadow, shadowupper/lower — flatShadowOn): a soft
|
|
10069
10589
|
// drop shadow under each symbol, see _getShadowIcon
|
|
@@ -10145,28 +10665,42 @@
|
|
|
10145
10665
|
// (#888888) over a half-opaque white halo (stroke font/7); its first
|
|
10146
10666
|
// baseline one font below the box (BOTTOMTITLE) or 0.7 font above it.
|
|
10147
10667
|
// Box and title are removed outside boxupper/boxlower.
|
|
10668
|
+
// Flat draws box and title in the chart group, which is scaled by
|
|
10669
|
+
// style.scale: the units below are u · scale. Its size measure W is
|
|
10670
|
+
// the chart's width (2r; with MULTIQUAD the width of the whole grid
|
|
10671
|
+
// at the position, the box enclosing all of it); flat's chart size
|
|
10672
|
+
// is 30 units.
|
|
10148
10673
|
const boxShown = flatFlag(this.flags, 'BOX') && !boxHiddenByScale(this.style, liveZoom);
|
|
10149
10674
|
const circular = flatFlag(this.flags, 'CIRCULAR');
|
|
10150
|
-
const unitPx = objectZoomFactor(liveZoom, this.mapOptions);
|
|
10675
|
+
const unitPx = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1);
|
|
10151
10676
|
const boxMarginU = styleNum(this.style.boxmargin) || 2;
|
|
10152
10677
|
const titleShown = boxShown && flatFlag(this.flags, 'TITLE') && !!this.binding.title;
|
|
10153
10678
|
const titleFontPx = 5 * unitPx * (styleNum(this.style.textscale) || styleNum(this.style.valuescale) || 1);
|
|
10679
|
+
// flat: fill-opacity (boxopacity || 1) on the raw style value — the
|
|
10680
|
+
// usual string "0" is transparent; typed here, a given 0 is too
|
|
10681
|
+
const boxOpacity = isNaN(styleNum(this.style.boxopacity)) ? 1 : styleNum(this.style.boxopacity);
|
|
10154
10682
|
const bottomTitle = flatFlag(this.flags, 'BOTTOMTITLE');
|
|
10155
10683
|
const alignRight = /right/.test(String(this.style.align || ''));
|
|
10156
|
-
|
|
10684
|
+
// with MULTIQUAD only the first item of a position gets its box/title
|
|
10685
|
+
const boxLayout = boxShown ? combined.filter(d => !multi || multi.index.get(d) === 0).map(d => {
|
|
10157
10686
|
const r = valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue);
|
|
10158
|
-
const
|
|
10687
|
+
const ext = multi ? multi.extent.get(d) : [0, 0, 0, 0];
|
|
10688
|
+
const chart = [ext[0] - r, ext[1] - r, ext[2] + r, ext[3] + r];
|
|
10689
|
+
const w = chart[2] - chart[0];
|
|
10690
|
+
const margin = Math.min(2 * boxMarginU * unitPx, boxMarginU * w / 30);
|
|
10159
10691
|
const boxR = circular ? r * 1.1 + margin : r;
|
|
10160
10692
|
let title = null;
|
|
10161
10693
|
if (titleShown) {
|
|
10162
10694
|
const text = this._itemTitle(d.properties);
|
|
10163
10695
|
if (text) {
|
|
10164
10696
|
const f = titleFontPx;
|
|
10165
|
-
const lines = wrapTitleLines(text, f, boxR * 2).split('\n');
|
|
10697
|
+
const lines = wrapTitleLines(text, f, circular ? boxR * 2 : w).split('\n');
|
|
10166
10698
|
const width = Math.max(...lines.map(l => titleTextWidth(l, f)));
|
|
10167
10699
|
// first baseline; lines go down one font each
|
|
10168
|
-
const base =
|
|
10169
|
-
|
|
10700
|
+
const base = circular
|
|
10701
|
+
? (bottomTitle ? boxR + f : -boxR - 0.7 * f - (lines.length - 1) * f)
|
|
10702
|
+
: (bottomTitle ? chart[3] + f : chart[1] - 0.7 * f - (lines.length - 1) * f);
|
|
10703
|
+
const x = circular ? unitPx / 2 : (alignRight ? chart[2] : chart[0]);
|
|
10170
10704
|
const anchor = circular ? 'middle' : (alignRight ? 'end' : 'start');
|
|
10171
10705
|
const x0 = anchor === 'middle' ? x - width / 2 : anchor === 'end' ? x - width : x;
|
|
10172
10706
|
// arial: ascent 0.905, descent 0.212 of the font size
|
|
@@ -10176,16 +10710,16 @@
|
|
|
10176
10710
|
}
|
|
10177
10711
|
let rect = null;
|
|
10178
10712
|
if (!circular) {
|
|
10179
|
-
const e =
|
|
10713
|
+
const e = chart.slice();
|
|
10180
10714
|
// with a title flat trims 0.1 font off the top of chart + title
|
|
10181
10715
|
if (title) { e[0] = Math.min(e[0], title.extent[0]); e[1] = Math.min(e[1], title.extent[1]) + 0.1 * titleFontPx; e[2] = Math.max(e[2], title.extent[2]); e[3] = Math.max(e[3], title.extent[3]); }
|
|
10182
10716
|
rect = [e[0] - margin, e[1] - margin, e[2] + margin, e[3] + margin];
|
|
10183
10717
|
}
|
|
10184
|
-
return { d, r, boxR, rect, title };
|
|
10718
|
+
return { d, r, w, boxR, rect, title };
|
|
10185
10719
|
}) : null;
|
|
10186
10720
|
if (boxShown && circular) {
|
|
10187
10721
|
const boxRgb = hexOrNamedToRgb(this.style.boxcolor || '#eeeeee');
|
|
10188
|
-
const boxAlpha = Math.round(255 *
|
|
10722
|
+
const boxAlpha = Math.round(255 * boxOpacity);
|
|
10189
10723
|
const borderRgb = hexOrNamedToRgb(this.style.bordercolor || '#dddddd');
|
|
10190
10724
|
// ScatterplotLayer has no pixel offset: a moved chart (align,
|
|
10191
10725
|
// offsetx/offsety) gets its circle as an icon
|
|
@@ -10218,20 +10752,47 @@
|
|
|
10218
10752
|
const borderStyle = String(this.style.borderstyle || '');
|
|
10219
10753
|
const boxLook = {
|
|
10220
10754
|
fill: this.style.boxcolor || 'white',
|
|
10221
|
-
fillOpacity:
|
|
10755
|
+
fillOpacity: boxOpacity,
|
|
10222
10756
|
stroke: borderStyle === 'none' ? null : styleLineColor(this.style.bordercolor) || '#bbbbbb',
|
|
10223
10757
|
strokeOpacity: /^(solid|dotted|dashed)$/.test(borderStyle) ? 1 : 0.3,
|
|
10224
10758
|
};
|
|
10225
10759
|
const borderWidth = styleNum(this.style.borderwidth) || 1;
|
|
10226
10760
|
const borderRadius = styleNum(this.style.borderradius);
|
|
10227
|
-
|
|
10761
|
+
const strokeOf = b => borderWidth * 0.2 * b.w / 30;
|
|
10762
|
+
// a box larger than BOX_ICON_MAX_PX (a MULTIQUAD grid of thousands
|
|
10763
|
+
// of items) would need a huge canvas: it is drawn as a polygon in
|
|
10764
|
+
// map coordinates instead (square corners)
|
|
10765
|
+
const bigBox = b => Math.max(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1]) > BOX_ICON_MAX_PX;
|
|
10766
|
+
const iconBoxes = boxLayout.filter(b => !bigBox(b)), polygonBoxes = boxLayout.filter(bigBox);
|
|
10767
|
+
iconBoxes.forEach(b => {
|
|
10228
10768
|
b.icon = this._buildBoxIcon(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1],
|
|
10229
|
-
isNaN(borderRadius) ? 0 : borderRadius * Math.min(unitPx, b.
|
|
10230
|
-
|
|
10769
|
+
isNaN(borderRadius) ? 0 : borderRadius * Math.min(unitPx, b.w / 30),
|
|
10770
|
+
strokeOf(b), boxLook);
|
|
10231
10771
|
});
|
|
10232
|
-
|
|
10772
|
+
if (polygonBoxes.length) {
|
|
10773
|
+
const fillRgb = hexOrNamedToRgb(boxLook.fill);
|
|
10774
|
+
const strokeRgb = boxLook.stroke ? hexOrNamedToRgb(boxLook.stroke) : null;
|
|
10775
|
+
layers.push(new GeoJsonLayer({
|
|
10776
|
+
id: `ix-box-polygons-${this.name}`,
|
|
10777
|
+
data: polygonBoxes.map(b => {
|
|
10778
|
+
const o = alignOf(b.d), c = b.d.geometry.coordinates, r = b.rect;
|
|
10779
|
+
const at = (x, y) => pixelOffsetLngLat(c, [x + o[0], y + o[1]], liveZoom);
|
|
10780
|
+
return { type: 'Feature', properties: { stroke: strokeOf(b) },
|
|
10781
|
+
geometry: { type: 'Polygon', coordinates: [[at(r[0], r[1]), at(r[2], r[1]), at(r[2], r[3]), at(r[0], r[3]), at(r[0], r[1])]] } };
|
|
10782
|
+
}),
|
|
10783
|
+
pickable: false,
|
|
10784
|
+
filled: boxLook.fillOpacity > 0,
|
|
10785
|
+
stroked: !!strokeRgb,
|
|
10786
|
+
getFillColor: [...fillRgb.slice(0, 3), Math.round(255 * boxLook.fillOpacity)],
|
|
10787
|
+
getLineColor: strokeRgb ? [...strokeRgb.slice(0, 3), Math.round(255 * boxLook.strokeOpacity)] : [0, 0, 0, 0],
|
|
10788
|
+
getLineWidth: f => f.properties.stroke,
|
|
10789
|
+
lineWidthUnits: 'pixels',
|
|
10790
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
10791
|
+
}));
|
|
10792
|
+
}
|
|
10793
|
+
if (iconBoxes.length) layers.push(new IconLayer({
|
|
10233
10794
|
id: `ix-box-${this.name}-g${this._iconGeneration}`,
|
|
10234
|
-
data:
|
|
10795
|
+
data: iconBoxes, pickable: false,
|
|
10235
10796
|
getPosition: b => b.d.geometry.coordinates,
|
|
10236
10797
|
getIcon: b => b.icon,
|
|
10237
10798
|
// the icon is drawn at 2× with a stroke padding around the box
|
|
@@ -10251,7 +10812,9 @@
|
|
|
10251
10812
|
? this._buildBubbleIcon(d.properties.counts, this.categoryColorsRgb, this._groupIsolation(d))
|
|
10252
10813
|
: this._buildSingleIcon(this.categoryColorsRgb[d.properties.cat], fillOpacity, this._resolveSymbolShape(d.properties), singleBorderColorRgb, singleBorderWidthPx),
|
|
10253
10814
|
getSize: d => valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue) * 2,
|
|
10254
|
-
|
|
10815
|
+
// group icons bake a fixed 0.9 alpha; an explicit fillopacity scales it
|
|
10816
|
+
// so a near-transparent theme doesn't show opaque 1-px-cell clusters
|
|
10817
|
+
getColor: d => [255, 255, 255, Math.round(this._iconAlpha(d) * (d.properties.counts && this.style.fillopacity != null ? fillOpacity : 1))],
|
|
10255
10818
|
...(alignOf.active ? { getPixelOffset: alignOf } : {}),
|
|
10256
10819
|
sizeUnits: 'pixels',
|
|
10257
10820
|
billboard: true,
|
|
@@ -10816,6 +11379,7 @@
|
|
|
10816
11379
|
// - themes: getThemeObj (flat's theme object, with its data table and
|
|
10817
11380
|
// the items on the map), getThemeDefinitionObj, getThemes,
|
|
10818
11381
|
// changeThemeStyle / removeTheme / setBasemapOpacity (map name first),
|
|
11382
|
+
// setThemeTimeFrame (themeId, min, max),
|
|
10819
11383
|
// refreshTheme, markThemeClass / unmarkThemeClass
|
|
10820
11384
|
// - data: ixmaps.data (facets: getFacets, showFacets, …), the
|
|
10821
11385
|
// embeddedSVG.window tables (setExternalData is with the data loading)
|
|
@@ -10863,7 +11427,7 @@
|
|
|
10863
11427
|
// htmlgui_flat.js). Its methods act on the last built map; called while
|
|
10864
11428
|
// the map is still building, they run once it is ready.
|
|
10865
11429
|
const MAP_HANDLE_METHODS = ['replace', 'add', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
10866
|
-
'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible', 'attribution'];
|
|
11430
|
+
'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible', 'setThemeTimeFrame', 'attribution'];
|
|
10867
11431
|
const _mapHandle = {};
|
|
10868
11432
|
for (const m of MAP_HANDLE_METHODS) {
|
|
10869
11433
|
_mapHandle[m] = (...args) => {
|
|
@@ -11090,6 +11654,7 @@
|
|
|
11090
11654
|
const style = Object.assign({}, rt.style);
|
|
11091
11655
|
if (rt.binding.size && !style.sizefield) style.sizefield = rt.binding.size;
|
|
11092
11656
|
if (rt.binding.title && !style.titlefield) style.titlefield = rt.binding.title;
|
|
11657
|
+
if (rt.binding.time && !style.timefield) style.timefield = rt.binding.time;
|
|
11093
11658
|
style.type = rt.flags.typeString != null ? rt.flags.typeString : [...rt.flags].join('|');
|
|
11094
11659
|
const data = Object.assign({}, rt._definition && rt._definition.data);
|
|
11095
11660
|
if (!data.name) data.name = themeDataName(rt);
|
|
@@ -11449,7 +12014,7 @@
|
|
|
11449
12014
|
}
|
|
11450
12015
|
|
|
11451
12016
|
global.ixmaps = {
|
|
11452
|
-
layer, Map: createMap, setExternalData: setExternalDataBridge, getThemeObj, data: ixmapsData,
|
|
12017
|
+
layer, Layer: layer, Map: createMap, setExternalData: setExternalDataBridge, getThemeObj, data: ixmapsData,
|
|
11453
12018
|
szResourceBase: ixmapsSzResourceBase, getProjectString, setProjectJSON, loadProject,
|
|
11454
12019
|
markThemeClass, unmarkThemeClass, getBoundingBox, setTitle, setTitleBox, refreshTheme, formatValue: flatFormatValue,
|
|
11455
12020
|
map: mapHandle, getZoom, getThemeDefinitionObj, embeddedSVG,
|
|
@@ -11470,6 +12035,8 @@
|
|
|
11470
12035
|
htmlgui_getAttributionString: () => (_lastMapApi && _lastMapApi.getAttribution ? _lastMapApi.getAttribution() : ''),
|
|
11471
12036
|
changeThemeStyle: (szMap, szId, szStyle, szFlag) => _mapHandle.changeThemeStyle(szId, szStyle, szFlag),
|
|
11472
12037
|
removeTheme: (szMap, szId) => _mapHandle.remove(szId),
|
|
12038
|
+
// flat's time slider: show the records of a theme (null = all themes) whose timefield lies in [min, max)
|
|
12039
|
+
setThemeTimeFrame: (szId, nMin, nMax) => _mapHandle.setThemeTimeFrame(szId, nMin, nMax),
|
|
11473
12040
|
setBasemapOpacity: (szMap, nOpacity, szMode) => _mapHandle.setBasemapOpacity(nOpacity, szMode),
|
|
11474
12041
|
// this engine's version (flat's own ixmaps.version numbers flat)
|
|
11475
12042
|
glVersion: IXMAPS_GL_VERSION,
|
|
@@ -11494,7 +12061,7 @@
|
|
|
11494
12061
|
// test-only: lets test/unit/*.test.mjs call pure internals directly (the
|
|
11495
12062
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
11496
12063
|
global.__ixmapsGlInternals = {
|
|
11497
|
-
IXMAPS_GL_VERSION, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout,
|
|
12064
|
+
IXMAPS_GL_VERSION, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout, multiQuadOffsets, pixelOffsetLngLat,
|
|
11498
12065
|
normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, typeStyleNumbers, styleNum,
|
|
11499
12066
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
11500
12067
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|