ixmaps-gl 0.2.11 → 0.2.15
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 +28 -3
- package/ixmaps-gl.js +883 -181
- 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.15';
|
|
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;
|
|
@@ -1951,6 +1951,10 @@
|
|
|
1951
1951
|
// per-map handle — most real pages only ever build one map anyway. Set
|
|
1952
1952
|
// once per successful build(), just before it returns engineApi below.
|
|
1953
1953
|
let _lastMapApi = null;
|
|
1954
|
+
// flat's map instance registry (htmlgui_flat.js 2416): the built maps by
|
|
1955
|
+
// their Map() options name — ixmaps.map("name") is the handle of THAT
|
|
1956
|
+
// map, ixmaps.map() stays the last built one
|
|
1957
|
+
const _mapApis = Object.create(null);
|
|
1954
1958
|
// resolves with the first built map's API: ixmaps.map() calls a page
|
|
1955
1959
|
// makes while the map is still building wait for it
|
|
1956
1960
|
let _resolveMapReady;
|
|
@@ -2125,7 +2129,7 @@
|
|
|
2125
2129
|
// edit it by hand; `npm run unit` fails when it drifts.
|
|
2126
2130
|
// ---------------------------------------------------------------
|
|
2127
2131
|
// <grammar:binding-aliases>
|
|
2128
|
-
// generated from ixmaps-grammar 0.1.
|
|
2132
|
+
// generated from ixmaps-grammar 0.1.27 (ixmaps-flat 1.0.42, 2b978d5 2026-10-02) — 52 aliases
|
|
2129
2133
|
const FLAT_BINDING_ALIASES = {
|
|
2130
2134
|
"aggregation": "style.aggregationfield",
|
|
2131
2135
|
"aggregationfield": "style.aggregationfield",
|
|
@@ -2187,7 +2191,7 @@
|
|
|
2187
2191
|
// both — so .style({tooltip}) is a tooltip and .meta({fillopacity}) a style
|
|
2188
2192
|
// property. GENERATED by test/sync-grammar.mjs — do not edit.
|
|
2189
2193
|
// <grammar:meta-keys>
|
|
2190
|
-
// generated from ixmaps-grammar 0.1.
|
|
2194
|
+
// generated from ixmaps-grammar 0.1.27 — 5 meta keys
|
|
2191
2195
|
const FLAT_META_KEYS = ["description","name","snippet","title","tooltip"];
|
|
2192
2196
|
// </grammar:meta-keys>
|
|
2193
2197
|
|
|
@@ -2206,6 +2210,7 @@
|
|
|
2206
2210
|
'style.alphafield': ['binding', 'alpha'],
|
|
2207
2211
|
'style.alphafield100': ['binding', 'alpha100'],
|
|
2208
2212
|
'style.valuefield': ['style', 'valuefield'],
|
|
2213
|
+
'style.timefield': ['binding', 'time'], // setThemeTimeFrame, see LayerRuntime.setTimeFrame
|
|
2209
2214
|
'style.titlefield': ['binding', 'title'], // {{theme.item.title}}, see _buildTooltipContext
|
|
2210
2215
|
// the lookup key's normalization (flatLookupKey): flat's newTheme moves
|
|
2211
2216
|
// binding.tonumber / digits / … into these style keys (htmlgui.js 1712)
|
|
@@ -2371,7 +2376,8 @@
|
|
|
2371
2376
|
const STYLE_NUMBER_KEYS = ['linewidth', 'fillopacity', 'scale', 'classes', 'valuedecimals', 'normalsizevalue',
|
|
2372
2377
|
'sizepow', 'rangescale', 'minvalue', 'maxvalue', 'markersize', 'boxopacity', 'outlierscale', 'valuescale',
|
|
2373
2378
|
'brightness', 'fractionscale', 'dopacityscale', 'dopacitypow', 'gridwidthpx', 'textscale', 'rangecentervalue',
|
|
2374
|
-
'shadowblur', 'shadowdx', 'shadowdy', 'maxshadow', 'offsetx', 'offsety'
|
|
2379
|
+
'shadowblur', 'shadowdx', 'shadowdy', 'maxshadow', 'offsetx', 'offsety', 'gridx', 'boxmargin', 'borderwidth',
|
|
2380
|
+
'borderradius', 'maxcharts'];
|
|
2375
2381
|
function toNumberIfNumeric(v) {
|
|
2376
2382
|
if (typeof v !== 'string' || !v.trim()) return v;
|
|
2377
2383
|
const n = Number(v);
|
|
@@ -2635,6 +2641,69 @@
|
|
|
2635
2641
|
return { dark: false, bg: 'rgba(255,255,255,0.9)' };
|
|
2636
2642
|
}
|
|
2637
2643
|
|
|
2644
|
+
// ---------------------------------------------------------------
|
|
2645
|
+
// Effective basemap darkness — what the map canvas actually shows,
|
|
2646
|
+
// not what its name suggests. flatLegendLook's name-plus-opacity
|
|
2647
|
+
// heuristic assumes the faded (basemapopacity <= 0.5) basemap blends
|
|
2648
|
+
// against a WHITE page, so a dark basemap faded to half goes light;
|
|
2649
|
+
// on a page with a dark background it reads dark all the same, and
|
|
2650
|
+
// tooltips/legends styled light stick out (the exact bug fixed here).
|
|
2651
|
+
// Rule: an explicit color (legendBackground, a color mapType) stays
|
|
2652
|
+
// authoritative as before; with a real basemap style, a DARK style
|
|
2653
|
+
// background means dark — except when it is faded to
|
|
2654
|
+
// basemapopacity <= 0.5, where the mix is mostly the page background
|
|
2655
|
+
// behind the semi-transparent canvas and THAT decides (dark page ->
|
|
2656
|
+
// still dark; white page -> flat's washed-out light look). A light
|
|
2657
|
+
// style background reads light however it is faded (flat's own rule).
|
|
2658
|
+
// Falls back to the name heuristic while no style exists.
|
|
2659
|
+
// ---------------------------------------------------------------
|
|
2660
|
+
function styleBackgroundColorOf(map) {
|
|
2661
|
+
try {
|
|
2662
|
+
const style = map && map.getStyle && map.getStyle();
|
|
2663
|
+
if (!style || !style.layers) return null;
|
|
2664
|
+
for (const layer of style.layers) {
|
|
2665
|
+
if (layer.type !== 'background') continue;
|
|
2666
|
+
const paint = layer.paint || {};
|
|
2667
|
+
const color = paint['background-color'] != null ? paint['background-color'] : (paint.background != null ? paint.background : null);
|
|
2668
|
+
// the blank background substituted for a color mapType IS the page's
|
|
2669
|
+
// requested background and never faded (see applyBasemapOpacity)
|
|
2670
|
+
return { color: color, faded: layer.id !== BLANK_BACKGROUND_LAYER_ID };
|
|
2671
|
+
}
|
|
2672
|
+
} catch (e) { /* no style (yet) */ }
|
|
2673
|
+
return null;
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
function pageBackgroundColorOf(el) {
|
|
2677
|
+
let node = el;
|
|
2678
|
+
while (node && node.nodeType === 1 && node !== document.documentElement) {
|
|
2679
|
+
const m = String(getComputedStyle(node).backgroundColor)
|
|
2680
|
+
.match(/^rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)(?:[,/\s]+([\d.]+))?\s*\)/i);
|
|
2681
|
+
if (m && (m[4] === undefined || +m[4] > 0)) return [+m[1], +m[2], +m[3]];
|
|
2682
|
+
node = node.parentElement;
|
|
2683
|
+
}
|
|
2684
|
+
return [255, 255, 255]; // the page default
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
function effectiveMapDark(map, el, basemapOpacity, mapType, legendBackground) {
|
|
2688
|
+
const id = legendBackground || mapType;
|
|
2689
|
+
if (flatIsCssColorValue(id)) return flatIsDarkColor(id);
|
|
2690
|
+
const base = styleBackgroundColorOf(map);
|
|
2691
|
+
const rgb = base && base.color != null ? parseCssColor(String(base.color)) : null;
|
|
2692
|
+
if (!rgb) {
|
|
2693
|
+
// no style (yet): the name heuristic, exactly as before
|
|
2694
|
+
return flatLegendLook(mapType, basemapOpacity, legendBackground).dark;
|
|
2695
|
+
}
|
|
2696
|
+
const o = isNaN(basemapOpacity) ? 1 : Math.max(0, Math.min(1, basemapOpacity));
|
|
2697
|
+
if (flatIsDarkColor('rgb(' + rgb.map(Math.round).join(',') + ')')) {
|
|
2698
|
+
// a dark basemap faded to half or less shows mostly the page background
|
|
2699
|
+
// behind the canvas - so at that point the PAGE decides (dark page ->
|
|
2700
|
+
// still dark; white page -> the washed-out flat look stays light)
|
|
2701
|
+
return (base.faded && o <= 0.5) ? flatIsDarkColor('rgb(' + pageBackgroundColorOf(el).map(Math.round).join(',') + ')') : true;
|
|
2702
|
+
}
|
|
2703
|
+
// a light basemap reads light however it is faded - flat's own rule
|
|
2704
|
+
return false;
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2638
2707
|
function buildBlankBackgroundStyle(color) {
|
|
2639
2708
|
return {
|
|
2640
2709
|
version: 8,
|
|
@@ -2677,6 +2746,40 @@
|
|
|
2677
2746
|
}
|
|
2678
2747
|
|
|
2679
2748
|
// ---------------------------------------------------------------
|
|
2749
|
+
// Embedded YouTube players in tooltips (data fields carrying <iframe
|
|
2750
|
+
// src=".../embed/ID">) are replaced by a thumbnail with a play button; the
|
|
2751
|
+
// real iframe is created on click. Keeps tooltips light, and a removed
|
|
2752
|
+
// video or a refused embed (e.g. error 153 on file:// pages) degrades to a
|
|
2753
|
+
// thumbnail that opens YouTube in a new tab instead of a broken player.
|
|
2754
|
+
const YT_IFRAME_RE = /<iframe\b[^>]*?\bsrc\s*=\s*["']?(?:https?:)?\/\/(?:www\.)?youtube(?:-nocookie)?\.com\/embed\/([\w-]{6,})[^"'\s>]*["']?[^>]*>\s*<\/iframe>/gi;
|
|
2755
|
+
function youtubeClickToPlay(html) {
|
|
2756
|
+
if (!html || html.indexOf('youtube') < 0) return html;
|
|
2757
|
+
return html.replace(YT_IFRAME_RE, (m, id) => {
|
|
2758
|
+
const w = (/\bwidth\s*=\s*["']?(\d+)/i.exec(m) || [])[1] || 240;
|
|
2759
|
+
const h = (/\bheight\s*=\s*["']?(\d+)/i.exec(m) || [])[1] || 180;
|
|
2760
|
+
return '<div class="ixgl-yt" data-yt="' + id + '" data-w="' + w + '" data-h="' + h + '" ' +
|
|
2761
|
+
'style="position:relative;width:' + w + 'px;height:' + h + 'px;cursor:pointer;background:#000 url(https://i.ytimg.com/vi/' + id + '/hqdefault.jpg) center/cover" ' +
|
|
2762
|
+
'title="play"><span style="position:absolute;left:50%;top:50%;width:48px;height:34px;margin:-17px 0 0 -24px;' +
|
|
2763
|
+
'border-radius:8px;background:rgba(220,0,0,0.9)"></span><span style="position:absolute;left:50%;top:50%;margin:-9px 0 0 -6px;' +
|
|
2764
|
+
'border-style:solid;border-width:9px 0 9px 15px;border-color:transparent transparent transparent #fff"></span></div>';
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
if (typeof document !== 'undefined' && typeof document.addEventListener === 'function' && !global.__ixglYtClick) {
|
|
2768
|
+
global.__ixglYtClick = true;
|
|
2769
|
+
document.addEventListener('click', e => {
|
|
2770
|
+
const el = e.target.closest && e.target.closest('.ixgl-yt');
|
|
2771
|
+
if (!el) return;
|
|
2772
|
+
e.stopPropagation();
|
|
2773
|
+
const f = document.createElement('iframe');
|
|
2774
|
+
f.width = el.dataset.w; f.height = el.dataset.h; f.frameBorder = '0';
|
|
2775
|
+
f.allow = 'autoplay; encrypted-media; picture-in-picture; fullscreen';
|
|
2776
|
+
f.allowFullscreen = true;
|
|
2777
|
+
f.referrerPolicy = 'strict-origin-when-cross-origin';
|
|
2778
|
+
f.src = 'https://www.youtube.com/embed/' + el.dataset.yt + '?autoplay=1&rel=0';
|
|
2779
|
+
el.replaceWith(f);
|
|
2780
|
+
}, true);
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2680
2783
|
// Hover tooltips and the click-to-pin tooltip of a map, built once per map
|
|
2681
2784
|
// by MapBuilder.build(). ctx: the builder (its map options, for the
|
|
2682
2785
|
// tooltip look), the MapLibre map, its container element and
|
|
@@ -2686,6 +2789,12 @@
|
|
|
2686
2789
|
// ---------------------------------------------------------------
|
|
2687
2790
|
function createTooltips(ctx) {
|
|
2688
2791
|
const { builder, map, el, findRuntimeForLayerId } = ctx;
|
|
2792
|
+
// the effective (rendered) dark/light probe for tooltip and legend —
|
|
2793
|
+
// the builder carries it so the legend look (built later, without the
|
|
2794
|
+
// map element at hand) can ask the same question
|
|
2795
|
+
builder.__effectiveDark = function (basemapOpacity, mapType, legendBackground) {
|
|
2796
|
+
return effectiveMapDark(map, el, basemapOpacity, mapType, legendBackground);
|
|
2797
|
+
};
|
|
2689
2798
|
// Click-to-pin tooltip: matches the real ixmaps engine's own
|
|
2690
2799
|
// click-pins-the-tooltip convention in pan mode (a hover tooltip is
|
|
2691
2800
|
// transient; a click keeps one visible until dismissed). This is a
|
|
@@ -2695,28 +2804,101 @@
|
|
|
2695
2804
|
// at click time (not live-updating), but its screen POSITION is kept
|
|
2696
2805
|
// in sync with the map on every pan/zoom via map.project().
|
|
2697
2806
|
let pinned = null; // { runtime, object, lngLat }
|
|
2698
|
-
// Tooltip look, like the legend's
|
|
2699
|
-
//
|
|
2700
|
-
//
|
|
2701
|
-
//
|
|
2807
|
+
// Tooltip look, like the legend's: flat's light tooltip
|
|
2808
|
+
// (tooltip_mustache.js: white 0.95, #444 text, thin black border, 5px
|
|
2809
|
+
// radius) on light basemaps, this engine's dark tooltip on dark ones —
|
|
2810
|
+
// decided by the EFFECTIVE map darkness (style background vs page
|
|
2811
|
+
// background, see effectiveMapDark), not the basemap name alone. Read
|
|
2812
|
+
// when shown, so options set later (a
|
|
2702
2813
|
// myMap.then(...).options({basemapopacity})) count.
|
|
2703
|
-
const
|
|
2814
|
+
const tooltipIsDark = () => {
|
|
2704
2815
|
const o = parseFloat(builder._engineOptions.basemapopacity);
|
|
2705
|
-
const
|
|
2706
|
-
return
|
|
2707
|
-
?
|
|
2708
|
-
:
|
|
2709
|
-
|
|
2816
|
+
const legendBackground = builder.mapOptions.legendBackground || builder.mapOptions.legendbackground;
|
|
2817
|
+
return typeof builder.__effectiveDark === 'function'
|
|
2818
|
+
? builder.__effectiveDark(isNaN(o) ? 1 : o, builder.mapOptions.mapType, legendBackground)
|
|
2819
|
+
: flatLegendLook(builder.mapOptions.mapType, isNaN(o) ? 1 : o, legendBackground).dark;
|
|
2820
|
+
};
|
|
2821
|
+
// flat's tooltip look (tooltip_mustache.js 118/151-154): arial narrow,
|
|
2822
|
+
// the font size clamped to 11..14px; light: white 0.95, #444 text, thin
|
|
2823
|
+
// black border; dark: rgba(50,50,50,0.95), #555 border (text light for
|
|
2824
|
+
// readability — flat leaves its #444 on dark and only adapts the panel).
|
|
2825
|
+
// line-height is declared so hover (deck.gl's .deck-tooltip stylesheet
|
|
2826
|
+
// gives it 20px) and the pinned tooltip (a bare div that would inherit
|
|
2827
|
+
// the page's 'normal') render the same content identically
|
|
2828
|
+
const tooltipLook = () => {
|
|
2829
|
+
const fontsize = Math.min(14, Math.max(11, (22 / 1200 * (global.innerWidth || 1200))));
|
|
2830
|
+
const fonts = { fontFamily: 'arial narrow, system', fontSize: fontsize + 'px', lineHeight: '20px' };
|
|
2831
|
+
return tooltipIsDark()
|
|
2832
|
+
? Object.assign(fonts, { background: 'rgba(50,50,50,0.95)', color: 'rgb(200,205,214)', border: '0.5px solid #555', borderRadius: '5px',
|
|
2833
|
+
boxShadow: '0 2px 8px rgba(0,0,0,0.3)' })
|
|
2834
|
+
: Object.assign(fonts, { background: 'rgba(255,255,255,0.95)', color: '#444', border: '0.5px solid black', borderRadius: '5px',
|
|
2835
|
+
boxShadow: 'rgba(0,0,0,0.2) 0px 2px 4px 0px, rgba(0,0,0,0.19) 0px 3px 10px 0px' });
|
|
2710
2836
|
};
|
|
2837
|
+
// flat's content pane (tooltip_mustache.js 124): the whole tooltip
|
|
2838
|
+
// content scrolls inside its own padded pane — a close button outside
|
|
2839
|
+
// the pane stays visible while it scrolls, and the scrollbar clears
|
|
2840
|
+
// the content by a small right margin
|
|
2841
|
+
const TOOLTIP_PANE_STYLE = 'margin:0.5em;max-height:360px;overflow:auto';
|
|
2842
|
+
const tooltipPaneHtml = (html) => `<div style="${TOOLTIP_PANE_STYLE}">${html}</div>`;
|
|
2711
2843
|
const pinnedTooltipEl = document.createElement('div');
|
|
2712
2844
|
pinnedTooltipEl.style.cssText = 'position:absolute;top:0;left:0;z-index:6;display:none;' +
|
|
2713
|
-
'pointer-events:auto;max-width:440px;
|
|
2714
|
-
|
|
2845
|
+
'pointer-events:auto;max-width:440px;padding:0.5em 0.7em 0.5em 0.5em;';
|
|
2846
|
+
|
|
2847
|
+
// flat's own tooltip placement (tooltip_mustache.js 145-148): flip it
|
|
2848
|
+
// to the left/above the anchor when there is no room right/below, then
|
|
2849
|
+
// clamp it into the window — a tooltip must always be completely
|
|
2850
|
+
// visible (an element larger than the viewport ends up at its edge)
|
|
2851
|
+
function keepTooltipVisible(tipEl, x, y) {
|
|
2852
|
+
const w = tipEl.offsetWidth, h = tipEl.offsetHeight;
|
|
2853
|
+
const vw = global.innerWidth || 1200, vh = global.innerHeight || 800;
|
|
2854
|
+
let tx = x > vw / 2 ? x - w - 30 : x + 30;
|
|
2855
|
+
let ty = y > vh / 2 ? y - h - 20 : y + 20;
|
|
2856
|
+
tx = Math.min(Math.max(8, tx), Math.max(8, vw - w - 8));
|
|
2857
|
+
ty = Math.min(Math.max(8, ty), Math.max(8, vh - h - 8));
|
|
2858
|
+
const transform = `translate(${Math.round(tx)}px, ${Math.round(ty)}px)`;
|
|
2859
|
+
tipEl.style.transform = transform;
|
|
2860
|
+
return transform;
|
|
2861
|
+
}
|
|
2715
2862
|
|
|
2716
2863
|
function updatePinnedTooltipPosition() {
|
|
2717
2864
|
if (!pinned) return;
|
|
2718
2865
|
const pt = map.project(pinned.lngLat);
|
|
2719
|
-
pinnedTooltipEl
|
|
2866
|
+
keepTooltipVisible(pinnedTooltipEl, pt.x, pt.y);
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
// hover tooltip: deck.gl pins it at the pointer without any viewport
|
|
2870
|
+
// logic (it overflows the right/bottom edge there) — watch the element's
|
|
2871
|
+
// style mutations and re-place it inside the window every time deck
|
|
2872
|
+
// moves it. Our own write is recognized and skipped (lastSetTransform).
|
|
2873
|
+
let __hoverTooltipWatch = null;
|
|
2874
|
+
function watchHoverTooltip() {
|
|
2875
|
+
if (__hoverTooltipWatch || !el.parentElement) return;
|
|
2876
|
+
const host = el.parentElement;
|
|
2877
|
+
const attach = (tipEl) => {
|
|
2878
|
+
let lastSetTransform = null;
|
|
2879
|
+
const mo = new MutationObserver(() => {
|
|
2880
|
+
if (tipEl.style.display === 'none' || !tipEl.textContent.trim()) return;
|
|
2881
|
+
const t = tipEl.style.transform;
|
|
2882
|
+
if (!t || t === lastSetTransform) return;
|
|
2883
|
+
const m = /translate\((-?[\d.]+)px,\s*(-?[\d.]+)px\)/.exec(t);
|
|
2884
|
+
if (!m) return;
|
|
2885
|
+
lastSetTransform = keepTooltipVisible(tipEl, +m[1], +m[2]);
|
|
2886
|
+
});
|
|
2887
|
+
mo.observe(tipEl, { attributes: true, attributeFilter: ['style'] });
|
|
2888
|
+
__hoverTooltipWatch = mo;
|
|
2889
|
+
};
|
|
2890
|
+
const existing = host.querySelector('.deck-tooltip');
|
|
2891
|
+
if (existing) { attach(existing); return; }
|
|
2892
|
+
const creation = new MutationObserver((muts, obs) => {
|
|
2893
|
+
for (const mu of muts) for (const n of mu.addedNodes) {
|
|
2894
|
+
if (n.nodeType === 1 && n.classList && n.classList.contains('deck-tooltip')) {
|
|
2895
|
+
attach(n);
|
|
2896
|
+
obs.disconnect();
|
|
2897
|
+
return;
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
});
|
|
2901
|
+
creation.observe(host, { childList: true, subtree: true });
|
|
2720
2902
|
}
|
|
2721
2903
|
|
|
2722
2904
|
// A stable per-geometry anchor coordinate for the identity check
|
|
@@ -2750,13 +2932,19 @@
|
|
|
2750
2932
|
}
|
|
2751
2933
|
|
|
2752
2934
|
// content lives in its own child so re-rendering (innerHTML) never
|
|
2753
|
-
//
|
|
2935
|
+
// content lives in its own child so re-rendering (innerHTML) never
|
|
2936
|
+
// wipes out the unpin button below (a sibling, not a descendant of it).
|
|
2937
|
+
// The content child sits inside a scroll PANE (flat's own tooltip
|
|
2938
|
+
// structure): the pane scrolls, the close button outside it does not
|
|
2939
|
+
const pinnedPaneEl = document.createElement('div');
|
|
2940
|
+
pinnedPaneEl.style.cssText = TOOLTIP_PANE_STYLE;
|
|
2754
2941
|
const pinnedContentEl = document.createElement('div');
|
|
2755
|
-
|
|
2942
|
+
pinnedPaneEl.appendChild(pinnedContentEl);
|
|
2943
|
+
pinnedTooltipEl.appendChild(pinnedPaneEl);
|
|
2756
2944
|
|
|
2757
2945
|
function renderPinnedTooltip() {
|
|
2758
2946
|
if (!pinned) { pinnedTooltipEl.style.display = 'none'; return; }
|
|
2759
|
-
const html = pinned.runtime.buildTooltipHtml(pinned.object);
|
|
2947
|
+
const html = pinned.runtime.buildTooltipHtml(pinned.object, tooltipIsDark());
|
|
2760
2948
|
if (!html) { pinned = null; pinnedTooltipEl.style.display = 'none'; return; }
|
|
2761
2949
|
pinnedContentEl.innerHTML = html;
|
|
2762
2950
|
Object.assign(pinnedTooltipEl.style, tooltipLook());
|
|
@@ -2797,7 +2985,15 @@
|
|
|
2797
2985
|
// a USER chart's picked item is {d, icon} (see _buildUserChartLayers):
|
|
2798
2986
|
// the feature is its d
|
|
2799
2987
|
function pickedFeature(o) { return o && o.icon && o.d ? o.d : o; }
|
|
2988
|
+
// flat's map tool 'pan' (Map option mode:"pan", Api.setMapTool): pure
|
|
2989
|
+
// navigation — no hover tooltip there (flat's own hover-tooltip paths
|
|
2990
|
+
// run only for the ""/info/clickinfo tool types, mapscript2.js 5232).
|
|
2991
|
+
// The click-to-pin tooltip stays: it is this engine's equivalent of
|
|
2992
|
+
// flat's info popup, so a click still shows the permanent tooltip.
|
|
2993
|
+
const isPanMode = () => /^\s*pan\s*$/i.test(String(builder.mapOptions.mode || ''));
|
|
2994
|
+
|
|
2800
2995
|
function tooltipFor({ object: picked, layer }) {
|
|
2996
|
+
if (isPanMode()) return null;
|
|
2801
2997
|
const object = pickedFeature(picked);
|
|
2802
2998
|
if (!object || !layer) return null;
|
|
2803
2999
|
// suppress the hover tooltip ONLY for the specific item that's
|
|
@@ -2807,8 +3003,10 @@
|
|
|
2807
3003
|
if (pinned && isSameAsPinned(layer.id, object)) return null;
|
|
2808
3004
|
const rt = findRuntimeForLayerId(layer.id);
|
|
2809
3005
|
if (!rt) return null;
|
|
2810
|
-
const html = rt.buildTooltipHtml(object);
|
|
2811
|
-
|
|
3006
|
+
const html = rt.buildTooltipHtml(object, tooltipIsDark());
|
|
3007
|
+
// same layout as the pinned tooltip: the content in flat's scroll
|
|
3008
|
+
// pane, the shared look (fonts included) on the container
|
|
3009
|
+
return html ? { html: tooltipPaneHtml(html), style: Object.assign({ padding: '0.5em 0.7em', maxWidth: '440px' }, tooltipLook()) } : null;
|
|
2812
3010
|
}
|
|
2813
3011
|
function clickFor(picked) {
|
|
2814
3012
|
const info = picked && Object.assign({}, picked, { object: pickedFeature(picked.object) });
|
|
@@ -2868,10 +3066,13 @@
|
|
|
2868
3066
|
}
|
|
2869
3067
|
function mount() {
|
|
2870
3068
|
map.on('move', updatePinnedTooltipPosition);
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
3069
|
+
watchHoverTooltip();
|
|
3070
|
+
// anchor inside the map container: map.project() gives container-
|
|
3071
|
+
// relative pixels, and styling the page's own body (relative) breaks
|
|
3072
|
+
// any page element absolutely positioned against it (AirBnB's
|
|
3073
|
+
// #onmap-buttons on a zero-height all-float body flew off-screen)
|
|
3074
|
+
el.style.position = el.style.position || 'relative';
|
|
3075
|
+
el.appendChild(pinnedTooltipEl);
|
|
2875
3076
|
}
|
|
2876
3077
|
return { getTooltip, onClick, mount };
|
|
2877
3078
|
}
|
|
@@ -2885,7 +3086,7 @@
|
|
|
2885
3086
|
// layoutLegends().
|
|
2886
3087
|
// ---------------------------------------------------------------
|
|
2887
3088
|
function createLegend(ctx) {
|
|
2888
|
-
const { builder, map, el, overlay, runtimes, engineApi, built, refresh, scheduleRefresh, notifyRedraw } = ctx;
|
|
3089
|
+
const { builder, map, el, overlay, runtimes, engineApi, built, refresh, scheduleRefresh, notifyRedraw, quietRefresh } = ctx;
|
|
2889
3090
|
let addLegendPanel = () => {};
|
|
2890
3091
|
let setLegendOption = () => {};
|
|
2891
3092
|
let updateSubTheme = () => {};
|
|
@@ -3033,15 +3234,28 @@
|
|
|
3033
3234
|
legendStack = document.createElement('div');
|
|
3034
3235
|
legendStack.className = 'ix-legend-stack';
|
|
3035
3236
|
legendStack.style.cssText = 'position:absolute;' + legendAlignCss + 'z-index:6;width:280px;'
|
|
3036
|
-
+ 'display:flex;flex-direction:column;max-height:
|
|
3237
|
+
+ 'display:flex;flex-direction:column;max-height:66.6%;overflow-y:auto;'
|
|
3037
3238
|
+ 'border-radius:6px;box-shadow:' + shadow + ';pointer-events:auto;';
|
|
3038
3239
|
if (!document.getElementById('ix-legend-stack-css')) {
|
|
3039
3240
|
const css = document.createElement('style');
|
|
3040
3241
|
css.id = 'ix-legend-stack-css';
|
|
3041
|
-
|
|
3242
|
+
// the slider rules are flat's own legend.css .slider block
|
|
3243
|
+
// (355-411) — its two duplicate blocks resolved to what the
|
|
3244
|
+
// cascade actually shows: a 5px rounded grey track at 0.7
|
|
3245
|
+
// opacity (1 on hover) and a green thumb
|
|
3246
|
+
css.textContent = '.ix-legend-stack>.ix-native-legend:not([data-ixoff])~.ix-native-legend:not([data-ixoff]){border-top:1px solid rgba(128,128,128,.35)}'
|
|
3247
|
+
+ '.ix-legend-stack input[type=range]{-webkit-appearance:none;appearance:none;height:5px;border-radius:10px;background:rgba(155,155,155,0.3);border:solid #888 0px;outline:none;opacity:0.7;transition:opacity .2s;}'
|
|
3248
|
+
+ '.ix-legend-stack input[type=range]:hover{opacity:1;}'
|
|
3249
|
+
+ '.ix-legend-stack input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;border-radius:0.5em;background:#4CAF50;cursor:pointer;}'
|
|
3250
|
+
+ '.ix-legend-stack input[type=range]::-moz-range-thumb{width:25px;height:25px;border-radius:50%;background:#4CAF50;cursor:pointer;}';
|
|
3042
3251
|
document.head.appendChild(css);
|
|
3043
3252
|
}
|
|
3044
|
-
|
|
3253
|
+
// flat anchors its legend inside the map div (#map-legend), so the
|
|
3254
|
+
// panel's max-height resolves against the MAP's height — the stack's
|
|
3255
|
+
// former parent anchor collapsed it to 0 on pages whose body has
|
|
3256
|
+
// only floated children (height 0)
|
|
3257
|
+
el.style.position = el.style.position || 'relative';
|
|
3258
|
+
el.appendChild(legendStack);
|
|
3045
3259
|
return legendStack;
|
|
3046
3260
|
};
|
|
3047
3261
|
// map.setLegend(value): the `legend` map option at runtime, same values as
|
|
@@ -3073,7 +3287,10 @@
|
|
|
3073
3287
|
});
|
|
3074
3288
|
legendStack.style.display = shown ? 'flex' : 'none';
|
|
3075
3289
|
};
|
|
3076
|
-
|
|
3290
|
+
// a plain FEATURE(S) base layer has one flat fill (no classes); FEATURE|CATEGORICAL
|
|
3291
|
+
// is coloured per category (see _buildFeaturesLayers), so it gets a legend
|
|
3292
|
+
const legendApplies = rt => rt.categoryLabels && rt.categoryLabels.length
|
|
3293
|
+
&& (!(rt.flags.has('FEATURE') || rt.flags.has('FEATURES')) || rt.flags.has('CATEGORICAL')) && !rt.flags.has('NOLEGEND');
|
|
3077
3294
|
// One theme's panel — at build time for every theme, and again for a
|
|
3078
3295
|
// theme defined later (map.layer(...) in a myMap.then(...) chain,
|
|
3079
3296
|
// defineLayer, loadProject); removeTheme removes it with the theme.
|
|
@@ -3083,7 +3300,7 @@
|
|
|
3083
3300
|
rt._triggerRedraw = () => { refresh(); };
|
|
3084
3301
|
if (subThemeCapable(rt)) rt._onMarksChanged = () => updateSubTheme(rt);
|
|
3085
3302
|
if (!legendOn || !el.parentElement || !legendApplies(rt)) return;
|
|
3086
|
-
el.
|
|
3303
|
+
el.style.position = el.style.position || 'relative';
|
|
3087
3304
|
{
|
|
3088
3305
|
// .type("...|NOLEGEND") — the fourth real legend-related type()
|
|
3089
3306
|
// token: opts a theme OUT of the legend entirely (real engine's
|
|
@@ -3109,8 +3326,16 @@
|
|
|
3109
3326
|
// flat's look (flatLegendLook) unless the page picks one with
|
|
3110
3327
|
// this engine's own legendtheme
|
|
3111
3328
|
const opacityOpt = parseFloat(builder._engineOptions.basemapopacity);
|
|
3112
|
-
const
|
|
3113
|
-
const
|
|
3329
|
+
const legendBackground = builder.mapOptions.legendBackground || builder.mapOptions.legendbackground;
|
|
3330
|
+
const flatLook = flatLegendLook(builder.mapOptions.mapType, isNaN(opacityOpt) ? 1 : opacityOpt, legendBackground);
|
|
3331
|
+
// effective darkness (style background vs page background, see
|
|
3332
|
+
// effectiveMapDark): a dark basemap faded to <= 0.5 over a DARK
|
|
3333
|
+
// page still reads dark — flatLegendLook's name heuristic would
|
|
3334
|
+
// wrongly switch the panel (and the tooltip) to light there
|
|
3335
|
+
const looksDark = typeof builder.__effectiveDark === 'function'
|
|
3336
|
+
? builder.__effectiveDark(isNaN(opacityOpt) ? 1 : opacityOpt, builder.mapOptions.mapType, legendBackground)
|
|
3337
|
+
: flatLook.dark;
|
|
3338
|
+
const isLightLegend = rt.style.legendtheme === 'light' || (rt.style.legendtheme !== 'dark' && !looksDark);
|
|
3114
3339
|
const legendColors = isLightLegend
|
|
3115
3340
|
? { bg: 'rgba(255,255,255,0.92)', fg: '#1a1a1a', shadow: '0 2px 10px rgba(0,0,0,0.18)',
|
|
3116
3341
|
rowMarked: 'rgba(0,0,0,0.08)', track: 'rgba(0,0,0,0.08)',
|
|
@@ -3118,24 +3343,26 @@
|
|
|
3118
3343
|
: { bg: 'rgba(28,30,34,0.92)', fg: '#eee', shadow: '0 2px 10px rgba(0,0,0,0.45)',
|
|
3119
3344
|
rowMarked: 'rgba(255,255,255,0.14)', track: 'rgba(255,255,255,0.08)',
|
|
3120
3345
|
selectBg: 'rgba(255,255,255,0.08)', selectBorder: 'rgba(255,255,255,0.25)' };
|
|
3121
|
-
|
|
3122
|
-
//
|
|
3123
|
-
//
|
|
3124
|
-
|
|
3125
|
-
//
|
|
3126
|
-
//
|
|
3127
|
-
//
|
|
3128
|
-
//
|
|
3129
|
-
//
|
|
3130
|
-
//
|
|
3131
|
-
// slider stay fixed
|
|
3132
|
-
//
|
|
3133
|
-
//
|
|
3346
|
+
// the panel bg follows the same effective decision (flatLegendLook
|
|
3347
|
+
// would hand the dark look a light bg here: its dark branch never
|
|
3348
|
+
// ran for this map, its light bg would clash with dark rows/text)
|
|
3349
|
+
if (!rt.style.legendtheme) legendColors.bg = looksDark ? '#111' : flatLook.bg;
|
|
3350
|
+
// max-height:66.6% resolves against el's own height (the map
|
|
3351
|
+
// container, which always has a definite height for the map
|
|
3352
|
+
// itself to render into) since this panel sits in the stack,
|
|
3353
|
+
// absolutely positioned inside el. flex:0 1 auto + min-height:0
|
|
3354
|
+
// lets the panel SHRINK to the stack's 2/3 cap so the ROWS
|
|
3355
|
+
// wrapper (its own flex:1 1 auto + min-height:0 below) scrolls
|
|
3356
|
+
// internally while the header/description/slider stay fixed —
|
|
3357
|
+
// with flex:0 0 auto the panel grew past the cap and the whole
|
|
3358
|
+
// stack scrolled instead. text-align:left: the page body may
|
|
3359
|
+
// centre everything (the AirBnB port does), the legend is left
|
|
3360
|
+
// aligned chrome.
|
|
3134
3361
|
// one box for every theme, as flat's #map-legend (legend.js
|
|
3135
3362
|
// 2200-2460): the panels stack in the legend stack in theme
|
|
3136
3363
|
// order, a line between them (see layoutLegends)
|
|
3137
|
-
panel.style.cssText = 'position:relative;flex:0
|
|
3138
|
-
+ 'display:flex;flex-direction:column;'
|
|
3364
|
+
panel.style.cssText = 'position:relative;flex:0 1 auto;min-height:0;overflow:hidden;'
|
|
3365
|
+
+ 'display:flex;flex-direction:column;text-align:left;'
|
|
3139
3366
|
+ 'background:' + legendColors.bg + ';color:' + legendColors.fg + ';font:12px/1.4 -apple-system,Arial,sans-serif;'
|
|
3140
3367
|
+ 'padding:10px 12px 12px;pointer-events:auto;';
|
|
3141
3368
|
const stack = legendStackEl(legendColors.shadow);
|
|
@@ -3208,7 +3435,11 @@
|
|
|
3208
3435
|
(rt._activeFeatures || rt.features).forEach(f => {
|
|
3209
3436
|
const idx = rt.categoryIndexByLabel ? rt.categoryIndexByLabel.get(f.properties[rt.binding.value]) : null;
|
|
3210
3437
|
if (idx == null) return;
|
|
3211
|
-
|
|
3438
|
+
// a polygon (FEATURE|CATEGORICAL) is counted at its first vertex
|
|
3439
|
+
let pt = f.geometry && f.geometry.coordinates;
|
|
3440
|
+
while (Array.isArray(pt) && Array.isArray(pt[0])) pt = pt[0];
|
|
3441
|
+
if (!Array.isArray(pt)) return;
|
|
3442
|
+
const [lng, lat] = pt;
|
|
3212
3443
|
if (lng < bbox[0] || lng > bbox[2] || lat < bbox[1] || lat > bbox[3]) return;
|
|
3213
3444
|
if (globeCenter && !isOnVisibleHemisphere(lng, lat, globeCenter)) return;
|
|
3214
3445
|
totals[idx] += useSum ? (parseFloat(f.properties[valueField]) || 0) : 1;
|
|
@@ -3570,12 +3801,12 @@
|
|
|
3570
3801
|
slider.min = isOpacitySlider ? '0' : '25';
|
|
3571
3802
|
slider.max = isOpacitySlider ? '100' : '200';
|
|
3572
3803
|
slider.value = String(initialPct);
|
|
3573
|
-
slider.style.cssText = 'width:
|
|
3804
|
+
slider.style.cssText = 'width:50%;margin-top:0.4em;'; // flat's own inline slider style (legend.js 2392)
|
|
3574
3805
|
slider.addEventListener('input', () => {
|
|
3575
3806
|
const pct = parseInt(slider.value, 10);
|
|
3576
3807
|
sliderRow.querySelector('.ix-legend-scale-val').textContent = pct;
|
|
3577
3808
|
rt.setStyle(isOpacitySlider ? { fillopacity: pct / 100 } : { scale: pct / 100 });
|
|
3578
|
-
|
|
3809
|
+
quietRefresh();
|
|
3579
3810
|
});
|
|
3580
3811
|
bodyEl.appendChild(slider);
|
|
3581
3812
|
}
|
|
@@ -3645,6 +3876,8 @@
|
|
|
3645
3876
|
this._layerBuilders.push(nameOrBuilder);
|
|
3646
3877
|
return this;
|
|
3647
3878
|
}
|
|
3879
|
+
// .Layer() — alias of .layer()
|
|
3880
|
+
Layer(nameOrBuilder) { return this.layer(nameOrBuilder); }
|
|
3648
3881
|
// GL-PORT COMPAT: real ixmaps-flat's map.on("layerdraw", cb) — only
|
|
3649
3882
|
// "layerdraw" is actually wired up (see build()'s own note on how
|
|
3650
3883
|
// it's translated into onRedraw + a synthetic per-runtime event); any
|
|
@@ -3843,6 +4076,16 @@
|
|
|
3843
4076
|
// world once, at the cost of that zoom limit.
|
|
3844
4077
|
renderWorldCopies: this._engineOptions.worldcopies !== false
|
|
3845
4078
|
});
|
|
4079
|
+
// ixmaps.Map(id, {mapProjection}): flat's orthographic map is MapLibre's
|
|
4080
|
+
// native globe here (same mapping as loadProject, applyProjectMap);
|
|
4081
|
+
// "ortographic" is the spelling flat pages commonly carry. Any other
|
|
4082
|
+
// value stays mercator.
|
|
4083
|
+
if (/^(orth?ographic|globe)$/i.test(String(this.mapOptions.mapProjection || '').trim())) {
|
|
4084
|
+
map.once('style.load', () => {
|
|
4085
|
+
try { map.setProjection({ type: 'globe' }); }
|
|
4086
|
+
catch (e) { console.warn('[ixmaps-gl] mapProjection "globe": map.setProjection failed (needs maplibre-gl >=5.0.1)', e); }
|
|
4087
|
+
});
|
|
4088
|
+
}
|
|
3846
4089
|
_boundsSource = () => {
|
|
3847
4090
|
const b = map.getBounds();
|
|
3848
4091
|
return [{ lat: b.getSouth(), lng: b.getWest() }, { lat: b.getNorth(), lng: b.getEast() }];
|
|
@@ -3937,9 +4180,11 @@
|
|
|
3937
4180
|
const overlay = new MapLibreOverlay({
|
|
3938
4181
|
interleaved: true,
|
|
3939
4182
|
layers: [],
|
|
3940
|
-
// pointer becomes a hand over anything pickable (bubbles/points),
|
|
3941
|
-
//
|
|
3942
|
-
|
|
4183
|
+
// pointer becomes a hand over anything pickable (bubbles/points), so
|
|
4184
|
+
// hovering something clickable actually looks clickable — except in
|
|
4185
|
+
// the 'pan' input mode, where nothing responds to hovering
|
|
4186
|
+
getCursor: ({ isDragging, isHovering }) =>
|
|
4187
|
+
(isDragging ? 'grabbing' : ((isHovering && !/^\s*pan\s*$/i.test(String(this.mapOptions.mode || ''))) ? 'pointer' : 'grab')),
|
|
3943
4188
|
// (guarded: see createTooltips)
|
|
3944
4189
|
getTooltip: tooltips.getTooltip,
|
|
3945
4190
|
onClick: tooltips.onClick
|
|
@@ -3952,12 +4197,14 @@
|
|
|
3952
4197
|
attributionBox.set(this._attributionText != null ? this._attributionText : this.mapOptions.attribution);
|
|
3953
4198
|
this._setAttribution = text => attributionBox.set(text);
|
|
3954
4199
|
|
|
3955
|
-
if (this._legendHtml
|
|
4200
|
+
if (this._legendHtml) {
|
|
3956
4201
|
const legendEl = document.createElement('div');
|
|
3957
4202
|
legendEl.style.cssText = 'position:absolute;top:0;left:0;z-index:5;pointer-events:none';
|
|
3958
4203
|
legendEl.innerHTML = this._legendHtml;
|
|
3959
|
-
|
|
3960
|
-
|
|
4204
|
+
// inside the map container, never the page's body (see the pinned
|
|
4205
|
+
// tooltip mount) — styling body breaks page-positioned elements
|
|
4206
|
+
el.style.position = el.style.position || 'relative';
|
|
4207
|
+
el.appendChild(legendEl);
|
|
3961
4208
|
}
|
|
3962
4209
|
|
|
3963
4210
|
// themes are addressed by either the layer()'s own name (e.g. "AREU")
|
|
@@ -4216,6 +4463,18 @@
|
|
|
4216
4463
|
refresh();
|
|
4217
4464
|
}
|
|
4218
4465
|
},
|
|
4466
|
+
// flat's ixmaps.setThemeTimeFrame(szId, min, max): show only the
|
|
4467
|
+
// records whose timefield lies in [min, max) (ms or Date-
|
|
4468
|
+
// parsable). null min+max clears. szId null/undefined = every theme
|
|
4469
|
+
// that has a timefield (flat: one call updates all themes at once).
|
|
4470
|
+
setThemeTimeFrame: (themeId, min, max) => {
|
|
4471
|
+
const targets = themeId == null ? runtimes.filter(r => r.binding && r.binding.time) : (() => {
|
|
4472
|
+
const rt = findRuntime(themeId);
|
|
4473
|
+
return rt ? siblingRuntimes(rt).filter(r => r.binding && r.binding.time) : [];
|
|
4474
|
+
})();
|
|
4475
|
+
targets.forEach(rt => rt.setTimeFrame(min, max));
|
|
4476
|
+
if (targets.length) refresh();
|
|
4477
|
+
},
|
|
4219
4478
|
// shows/hides a whole theme (all the deck.gl layers its
|
|
4220
4479
|
// buildDeckLayers would otherwise produce) without touching its
|
|
4221
4480
|
// data or style — for toggle controls like the real page's
|
|
@@ -4301,6 +4560,8 @@
|
|
|
4301
4560
|
};
|
|
4302
4561
|
return lb;
|
|
4303
4562
|
},
|
|
4563
|
+
// .Layer() — alias of .layer()
|
|
4564
|
+
Layer: (nameOrBuilder) => engineApi.layer(nameOrBuilder),
|
|
4304
4565
|
// Real ixmaps-flat's loadProject (htmlgui.js loadProject →
|
|
4305
4566
|
// setProjectJSON → continueSetProjectJSON): src is a project object,
|
|
4306
4567
|
// a JSON string, or a URL (fetched as DATA — nothing a project names
|
|
@@ -4633,7 +4894,15 @@
|
|
|
4633
4894
|
const svg = String(m.map || '');
|
|
4634
4895
|
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
4896
|
if (typeof map.setProjection === 'function') {
|
|
4636
|
-
try {
|
|
4897
|
+
try {
|
|
4898
|
+
map.setProjection({ type: /orthographic\.svg$/i.test(svg) ? 'globe' : 'mercator' });
|
|
4899
|
+
// the layers are projection-dependent (globe: far-hemisphere cull,
|
|
4900
|
+
// chart placement) and are otherwise rebuilt only by a pan: rebuild
|
|
4901
|
+
// now, and again once the new projection has rendered
|
|
4902
|
+
refreshLayers();
|
|
4903
|
+
map.triggerRepaint();
|
|
4904
|
+
map.once('idle', refreshLayers);
|
|
4905
|
+
}
|
|
4637
4906
|
catch (e) { console.warn('[ixmaps-gl] loadProject: map.setProjection failed (needs maplibre-gl >=5.0.1)', e); }
|
|
4638
4907
|
} else {
|
|
4639
4908
|
console.warn('[ixmaps-gl] loadProject: map.setProjection not available on the loaded maplibre-gl build (needs >=5.0.1) — projection unchanged');
|
|
@@ -4979,9 +5248,18 @@
|
|
|
4979
5248
|
// the native legend: createLegend (its four hooks are what the rest
|
|
4980
5249
|
// of the map calls — panels, the legend option, sub-themes, layout)
|
|
4981
5250
|
({ addLegendPanel, setLegendOption, updateSubTheme, layoutLegends } =
|
|
4982
|
-
createLegend({ builder, map, el, overlay, runtimes, engineApi, built, refresh, scheduleRefresh, notifyRedraw
|
|
5251
|
+
createLegend({ builder, map, el, overlay, runtimes, engineApi, built, refresh, scheduleRefresh, notifyRedraw,
|
|
5252
|
+
// flat's legend slider (legend.js 2839-2854) patches the style and
|
|
5253
|
+
// calls redrawTheme — a per-theme redraw that does NOT fire
|
|
5254
|
+
// htmlgui_onDrawTheme, so a page's own onDrawTheme hook (this
|
|
5255
|
+
// port's hookStatistics re-applying its own slider's scale)
|
|
5256
|
+
// only reasserts on the next full redraw (pan/zoom). refresh()
|
|
5257
|
+
// here would notify synchronously and clobber the new scale in
|
|
5258
|
+
// the same tick — this quiet variant is that flat behavior.
|
|
5259
|
+
quietRefresh: () => { refreshLayers(); } }));
|
|
4983
5260
|
|
|
4984
5261
|
_lastMapApi = engineApi;
|
|
5262
|
+
if (this.mapOptions && this.mapOptions.name) _mapApis[this.mapOptions.name] = engineApi;
|
|
4985
5263
|
// ?ixgl-debug: ixmaps.__glDebug.check() reports, for the moment it is
|
|
4986
5264
|
// called, every theme's geometry problems (invalid or mixed-dimension
|
|
4987
5265
|
// coordinates, a shape far larger than the theme's others) and what
|
|
@@ -5047,6 +5325,27 @@
|
|
|
5047
5325
|
}
|
|
5048
5326
|
}
|
|
5049
5327
|
|
|
5328
|
+
// GL-PORT COMPAT: flat's ixmaps.Map(div, opts, cb) hands the callback a
|
|
5329
|
+
// mapApi HANDLE (htmlgui_flat.js 904-935) — a real page keeps it and
|
|
5330
|
+
// calls its methods on it (__map.setView(...), __map.replace(...) —
|
|
5331
|
+
// AirBnB's city selector). This engine hands the callback the BUILDER
|
|
5332
|
+
// for its own .options().view().layer() chain; the builder here also
|
|
5333
|
+
// carries flat's handle methods, delegated to the map handle of THIS
|
|
5334
|
+
// map's options name (calls before the map exists wait for it, exactly
|
|
5335
|
+
// as ixmaps.map() does). The builder's own methods with different
|
|
5336
|
+
// semantics — layer, view, options, require, attribution, on — keep
|
|
5337
|
+
// theirs and are not overridden.
|
|
5338
|
+
for (const m of ['replace', 'add', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
5339
|
+
'refreshTheme', 'redrawTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'resize',
|
|
5340
|
+
'loadProject', 'project', 'setThemeVisible', 'setThemeTimeFrame', 'show', 'hide', 'getZoom', 'getThemeObj',
|
|
5341
|
+
'getThemes', 'theme', 'setView']) {
|
|
5342
|
+
if (!MapBuilder.prototype[m]) {
|
|
5343
|
+
MapBuilder.prototype[m] = function (...args) {
|
|
5344
|
+
return mapHandle(this.mapOptions && this.mapOptions.name)[m](...args);
|
|
5345
|
+
};
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
5348
|
+
|
|
5050
5349
|
// named createMap (not Map) so it doesn't shadow the built-in ES6 Map
|
|
5051
5350
|
// class used throughout this file — a real bug caught during testing
|
|
5052
5351
|
// (every `new Map()` below silently resolved to *this* function instead)
|
|
@@ -5179,6 +5478,8 @@
|
|
|
5179
5478
|
// earlier hand-tuned 12.5 drew every symbol 1.2× (with the reference
|
|
5180
5479
|
// below, 1.3×) smaller than flat.
|
|
5181
5480
|
const NORMAL_RADIUS_PX = 15;
|
|
5481
|
+
// largest BOX side (px) still drawn as a canvas icon (at 2×), see _buildBoxIcon
|
|
5482
|
+
const BOX_ICON_MAX_PX = 2048;
|
|
5182
5483
|
// Flat's own map scale, which dynamic object scaling compares with
|
|
5183
5484
|
// normalSizeScale (mapscript2.js 2849: dx = nTrueMapScale · nZoomScale /
|
|
5184
5485
|
// nNormalSizeScale), read from a real flat page: the generic Mercator
|
|
@@ -5425,6 +5726,16 @@
|
|
|
5425
5726
|
|
|
5426
5727
|
function valueRadius(value, zoom, style, mapOptions, flags, maxSizeValue) {
|
|
5427
5728
|
const zoomFactor = objectZoomFactor(zoom, mapOptions);
|
|
5729
|
+
// FIXSIZE (a symbol chart; PLOT / GRIDSIZE size their markers
|
|
5730
|
+
// themselves): one radius for every item, whatever its value — the
|
|
5731
|
+
// normal radius ÷ normalsizevalue, half of it for BUBBLE (maptheme.js
|
|
5732
|
+
// 20711, 21847; measured on flat: PNRR "details", r = 1.5 units at
|
|
5733
|
+
// scale 0.1)
|
|
5734
|
+
if (flags && flags.has('FIXSIZE') && !flags.has('PLOT') && !flags.has('GRIDSIZE')) {
|
|
5735
|
+
const fixed = NORMAL_RADIUS_PX * (styleNum(style.scale) || 1) / (styleNum(style.normalsizevalue) || 1)
|
|
5736
|
+
* (flatChartBranch(flags) === 'bubble' ? 0.5 : 1);
|
|
5737
|
+
return Math.max(VALUE_RADIUS_MIN, Math.min(VALUE_RADIUS_MAX, fixed * zoomFactor));
|
|
5738
|
+
}
|
|
5428
5739
|
// Real default (maptheme.js ~line 5059) when the theme sets no
|
|
5429
5740
|
// normalsizevalue: the dataset's own max value on the bound size
|
|
5430
5741
|
// field (maxSizeValue, from LayerRuntime._prepare), not a fixed
|
|
@@ -5629,6 +5940,8 @@
|
|
|
5629
5940
|
const field = binding.size ? binding.size
|
|
5630
5941
|
: (symbols && !flags.has('CATEGORICAL') && !flags.has('AGGREGATE') && binding.value && !String(binding.value).includes('|') ? binding.value : null);
|
|
5631
5942
|
if (!field) return undefined;
|
|
5943
|
+
// "$item$": every record counts 1
|
|
5944
|
+
if (field === '$item$') return features.length ? 1 : 0;
|
|
5632
5945
|
return features.reduce((max, f) => {
|
|
5633
5946
|
const v = parseFloat(f.properties[field]);
|
|
5634
5947
|
return isNaN(v) ? max : Math.max(max, v);
|
|
@@ -6228,6 +6541,13 @@
|
|
|
6228
6541
|
// researched safe range (~1480-2980 distinct 48px icons before a real
|
|
6229
6542
|
// device's WebGL MAX_TEXTURE_SIZE is at risk).
|
|
6230
6543
|
const ICON_ATLAS_RESET_AFTER = 3000;
|
|
6544
|
+
// The same reset by icon AREA: a few large icons (BOX rectangles around
|
|
6545
|
+
// MULTIQUAD grids, hundreds of px each, new sizes at every zoom) fill
|
|
6546
|
+
// deck.gl's 1024 px wide atlas long before 3000 icons — on the PNRR
|
|
6547
|
+
// page a dozen boxes doubled it per zoom step, until it passed WebGL's
|
|
6548
|
+
// MAX_TEXTURE_SIZE (16384) and the boxes drew black. 2 Mpx leaves room
|
|
6549
|
+
// for the atlas's row-packing waste (measured ~2×) below that limit.
|
|
6550
|
+
const ICON_ATLAS_AREA_RESET_AFTER = 2 * 1024 * 1024;
|
|
6231
6551
|
|
|
6232
6552
|
// Shared packing layout for a multi-category group's sub-bubbles — used
|
|
6233
6553
|
// both to draw the packed icon raster (_buildBubbleIcon) and to position
|
|
@@ -6272,6 +6592,56 @@
|
|
|
6272
6592
|
};
|
|
6273
6593
|
}
|
|
6274
6594
|
|
|
6595
|
+
// MULTIQUAD / MULTISQUARE (flat maptheme.js 18648-18690): the items at
|
|
6596
|
+
// one position, in drawing order, fill quads around it — the k-th at
|
|
6597
|
+
// square = ⌊√k⌋, (k − square², square) while that is within the square,
|
|
6598
|
+
// else (square, square − (k − square² − square)); once square reaches
|
|
6599
|
+
// gridx, rows of gridx go on upward (k % gridx, ⌊k / gridx⌋). Steps of
|
|
6600
|
+
// 2 · normal radius · rangescale (flat's chart size, measured on the PNRR
|
|
6601
|
+
// page); y up. Returns per item its pixel offset and index, and per
|
|
6602
|
+
// position's first item the extent [minX, minY, maxX, maxY] of the
|
|
6603
|
+
// offsets there (flat sizes the BOX around all of them).
|
|
6604
|
+
function multiQuadOffsets(items, normalRadiusPx, rangeScale, gridX) {
|
|
6605
|
+
const step = 2 * normalRadiusPx * (rangeScale || 1);
|
|
6606
|
+
const count = new Map(), offset = new Map(), index = new Map(), first = new Map(), extent = new Map();
|
|
6607
|
+
for (const d of items) {
|
|
6608
|
+
const c = d.geometry && d.geometry.coordinates;
|
|
6609
|
+
const key = c ? c[0] + ',' + c[1] : '';
|
|
6610
|
+
const k = count.get(key) || 0;
|
|
6611
|
+
count.set(key, k + 1);
|
|
6612
|
+
index.set(d, k);
|
|
6613
|
+
if (k === 0) { first.set(key, d); extent.set(d, [0, 0, 0, 0]); }
|
|
6614
|
+
let x = 0, y = 0;
|
|
6615
|
+
if (k > 0) {
|
|
6616
|
+
const square = Math.floor(Math.sqrt(k));
|
|
6617
|
+
if (square < (gridX || Infinity)) {
|
|
6618
|
+
const line = k - square * square;
|
|
6619
|
+
x = line <= square ? line : square;
|
|
6620
|
+
y = line <= square ? square : square - (line - square);
|
|
6621
|
+
} else {
|
|
6622
|
+
x = k % gridX;
|
|
6623
|
+
y = Math.floor(k / gridX);
|
|
6624
|
+
}
|
|
6625
|
+
}
|
|
6626
|
+
offset.set(d, [x * step, -y * step]);
|
|
6627
|
+
const e = extent.get(first.get(key));
|
|
6628
|
+
e[0] = Math.min(e[0], x * step); e[1] = Math.min(e[1], -y * step);
|
|
6629
|
+
e[2] = Math.max(e[2], x * step); e[3] = Math.max(e[3], -y * step);
|
|
6630
|
+
}
|
|
6631
|
+
return { offset, index, extent };
|
|
6632
|
+
}
|
|
6633
|
+
|
|
6634
|
+
// A pixel offset [dx, dy] (y down) from lngLat at MapLibre zoom `zoom`,
|
|
6635
|
+
// as lng/lat (Web Mercator, 512 px world tiles) — for shapes too large
|
|
6636
|
+
// for an icon, drawn in map coordinates (rebuilt with every refresh).
|
|
6637
|
+
function pixelOffsetLngLat(lngLat, offset, zoom) {
|
|
6638
|
+
const world = 512 * Math.pow(2, zoom);
|
|
6639
|
+
const lat0 = lngLat[1] * Math.PI / 180;
|
|
6640
|
+
const y = (0.5 - Math.log(Math.tan(Math.PI / 4 + lat0 / 2)) / (2 * Math.PI)) * world + offset[1];
|
|
6641
|
+
const lat = (2 * Math.atan(Math.exp((0.5 - y / world) * 2 * Math.PI)) - Math.PI / 2) * 180 / Math.PI;
|
|
6642
|
+
return [lngLat[0] + offset[0] * 360 / world, lat];
|
|
6643
|
+
}
|
|
6644
|
+
|
|
6275
6645
|
// CHART|SYMBOL|SEQUENCE (flat maptheme.js 21184-21300, 21790-21925,
|
|
6276
6646
|
// 22296-22395): one symbol per category part of a chart, in category
|
|
6277
6647
|
// order or sorted (SORT: DOWN biggest first, UP smallest first), parts
|
|
@@ -7194,9 +7564,13 @@
|
|
|
7194
7564
|
// set (e.g. roma: summed Pericolosita, normalized to 0..1).
|
|
7195
7565
|
// (flat's per-item nSize holds a record count in that last case, but it
|
|
7196
7566
|
// is not what draws a value-based symbol.)
|
|
7567
|
+
// "$item$" (as size or value) is flat's record count: 1 per record — it
|
|
7568
|
+
// is no column (read as one, every record was 0 and a LABEL|TEXTONLY
|
|
7569
|
+
// theme's text 0.08 px high).
|
|
7197
7570
|
function resolveAggregateValue(binding, flags, props) {
|
|
7571
|
+
if (binding.size === '$item$') return 1;
|
|
7198
7572
|
if (binding.size) return parseFloat(props[binding.size]) || 0;
|
|
7199
|
-
if (flags.has('CATEGORICAL') || binding.value == null) return 1;
|
|
7573
|
+
if (flags.has('CATEGORICAL') || binding.value == null || binding.value === '$item$') return 1;
|
|
7200
7574
|
return parseFloat(props[binding.value]) || 0;
|
|
7201
7575
|
}
|
|
7202
7576
|
|
|
@@ -7294,7 +7668,12 @@
|
|
|
7294
7668
|
key = `${snapped.x}:${snapped.y}`;
|
|
7295
7669
|
}
|
|
7296
7670
|
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); }
|
|
7671
|
+
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); }
|
|
7672
|
+
// the members' own records, for the tooltip's {{theme.item.data}}
|
|
7673
|
+
// (flat's __add_data tables them, numbered, up to 50)
|
|
7674
|
+
if (f.properties.raw && (!cell.raws || cell.raws.length < 50)) {
|
|
7675
|
+
(cell.raws = cell.raws || []).push(f.properties.raw);
|
|
7676
|
+
}
|
|
7298
7677
|
cell.sumX += p.x; cell.sumY += p.y; cell.n++;
|
|
7299
7678
|
cell.value += f.properties.value;
|
|
7300
7679
|
if (f.properties.classValue !== undefined) cell.classValue += f.properties.classValue;
|
|
@@ -7316,7 +7695,8 @@
|
|
|
7316
7695
|
// titleRaw: the cell's first record — flat titles an aggregated
|
|
7317
7696
|
// item by it (itemTitle)
|
|
7318
7697
|
? { cluster: true, point_count: cell.n, value: cell.value, ...(hasClass ? { classValue: cell.classValue } : {}),
|
|
7319
|
-
titleRaw: cell.first.properties.raw || cell.first.properties
|
|
7698
|
+
titleRaw: cell.first.properties.raw || cell.first.properties,
|
|
7699
|
+
...(cell.raws && cell.raws.length ? { raws: cell.raws } : {}) }
|
|
7320
7700
|
: cell.first.properties;
|
|
7321
7701
|
if (cell.series && cell.n > 1) props = Object.assign({}, props, { series: cell.series });
|
|
7322
7702
|
// a field aggregation's cell key (the field value): the categories of
|
|
@@ -7383,6 +7763,18 @@
|
|
|
7383
7763
|
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
7764
|
cell.sumX += p.x; cell.sumY += p.y; cell.n++;
|
|
7385
7765
|
if (!cell.titleRaw) cell.titleRaw = f.properties.titleRaw || f.properties.raw || null;
|
|
7766
|
+
// the members' own records, for the tooltip's {{theme.item.data}} —
|
|
7767
|
+
// flat's __add_data tables them (numbered, up to 50) for aggregated
|
|
7768
|
+
// items too; references only, so no copy is made. A member can itself
|
|
7769
|
+
// be a pre-aggregated cluster node, whose records it carries in raws
|
|
7770
|
+
if (!cell.raws) cell.raws = [];
|
|
7771
|
+
if (cell.raws.length < 50) {
|
|
7772
|
+
if (f.properties.raw) cell.raws.push(f.properties.raw);
|
|
7773
|
+
else if (f.properties.raws) for (const r of f.properties.raws) {
|
|
7774
|
+
if (cell.raws.length >= 50) break;
|
|
7775
|
+
cell.raws.push(r);
|
|
7776
|
+
}
|
|
7777
|
+
}
|
|
7386
7778
|
cell.counts[f.properties.cat] += f.properties.value;
|
|
7387
7779
|
if (f.properties.classValue !== undefined) cell.classTotal = (cell.classTotal || 0) + f.properties.classValue;
|
|
7388
7780
|
// point_count is the aggregated-record count of a multi-record
|
|
@@ -7397,6 +7789,7 @@
|
|
|
7397
7789
|
return {
|
|
7398
7790
|
geometry: { type: 'Point', coordinates: [ll.lng, ll.lat] },
|
|
7399
7791
|
properties: { counts: cell.counts, total: cell.counts.reduce((a, c) => a + c, 0), recordCounts: cell.recordCounts, ...(cell.classTotal !== undefined ? { classTotal: cell.classTotal } : {}),
|
|
7792
|
+
...(cell.raws && cell.raws.length ? { raws: cell.raws } : {}),
|
|
7400
7793
|
...(cell.titleRaw ? { titleRaw: cell.titleRaw } : {}) }
|
|
7401
7794
|
};
|
|
7402
7795
|
});
|
|
@@ -7441,13 +7834,25 @@
|
|
|
7441
7834
|
return count;
|
|
7442
7835
|
}
|
|
7443
7836
|
|
|
7837
|
+
// flat matches type flags as substrings (szFlag.match(/SYMBOL/)), so a
|
|
7838
|
+
// "SYMBOLS" flag (PNRR pages) is a SYMBOL chart there. With only the symbol
|
|
7839
|
+
// "label" (maptheme.js 21833: the value as text, TEXTONLY: text alone) it is
|
|
7840
|
+
// the LABEL chart this engine draws as _buildLabelChartLayers.
|
|
7841
|
+
function symbolsFlagCompat(flags, style) {
|
|
7842
|
+
if (!flags.has('SYMBOLS') || flags.has('SYMBOL') || flags.has('LABEL')) return flags;
|
|
7843
|
+
const out = new Set(flags);
|
|
7844
|
+
const symbols = [].concat((style && style.symbols) || []);
|
|
7845
|
+
out.add(symbols.length && symbols.every(v => String(v) === 'label') ? 'LABEL' : 'SYMBOL');
|
|
7846
|
+
return out;
|
|
7847
|
+
}
|
|
7848
|
+
|
|
7444
7849
|
class LayerRuntime {
|
|
7445
7850
|
// spec: a normalizeTheme() result — never a raw LayerBuilder
|
|
7446
7851
|
constructor(spec, fc, mapOptions) {
|
|
7447
7852
|
this.name = spec.name;
|
|
7448
7853
|
this.binding = spec.binding;
|
|
7449
|
-
this.flags = spec.flags;
|
|
7450
7854
|
this.style = spec.style;
|
|
7855
|
+
this.flags = symbolsFlagCompat(spec.flags, spec.style);
|
|
7451
7856
|
this.meta = spec.meta;
|
|
7452
7857
|
// GL-PORT COMPAT: retained only for ixmaps.getThemeObj()'s szFilter
|
|
7453
7858
|
// (real ixmaps-flat global compat shim, see _globalThemeRegistry) —
|
|
@@ -7481,6 +7886,7 @@
|
|
|
7481
7886
|
// ICON_ATLAS_RESET_AFTER for why this is necessary at all).
|
|
7482
7887
|
this._iconGeneration = 0;
|
|
7483
7888
|
this._iconsSinceAtlasReset = 0;
|
|
7889
|
+
this._iconAreaSinceAtlasReset = 0;
|
|
7484
7890
|
this._prepare();
|
|
7485
7891
|
}
|
|
7486
7892
|
|
|
@@ -7503,9 +7909,11 @@
|
|
|
7503
7909
|
_cacheIcon(key, icon) {
|
|
7504
7910
|
if (!this._iconCache.has(key)) {
|
|
7505
7911
|
this._iconsSinceAtlasReset++;
|
|
7506
|
-
|
|
7912
|
+
this._iconAreaSinceAtlasReset += (icon.width || 0) * (icon.height || 0);
|
|
7913
|
+
if (this._iconsSinceAtlasReset > ICON_ATLAS_RESET_AFTER || this._iconAreaSinceAtlasReset > ICON_ATLAS_AREA_RESET_AFTER) {
|
|
7507
7914
|
this._iconGeneration++;
|
|
7508
7915
|
this._iconsSinceAtlasReset = 0;
|
|
7916
|
+
this._iconAreaSinceAtlasReset = 0;
|
|
7509
7917
|
}
|
|
7510
7918
|
}
|
|
7511
7919
|
if (this._iconCache.size >= ICON_CACHE_MAX) {
|
|
@@ -7646,6 +8054,10 @@
|
|
|
7646
8054
|
// a page could in principle use both at once, though no current
|
|
7647
8055
|
// ported page does.
|
|
7648
8056
|
this._runtimeFilterExpr = '';
|
|
8057
|
+
// flat's setThemeTimeFrame(id, min, max): records whose style.timefield
|
|
8058
|
+
// value lies in [min, max) — null = no time filter. AND'd with the
|
|
8059
|
+
// filters above in _rebuildActiveFeatures.
|
|
8060
|
+
this._timeFrame = null;
|
|
7649
8061
|
|
|
7650
8062
|
// GRIDSIZE layers (PLOT curves-chart, or its grid-mesh companion)
|
|
7651
8063
|
// also carry AGGREGATE in their type string, but they bin into a
|
|
@@ -7842,6 +8254,44 @@
|
|
|
7842
8254
|
this._rebuildActiveFeatures();
|
|
7843
8255
|
}
|
|
7844
8256
|
|
|
8257
|
+
// setThemeTimeFrame: min/max in ms or any Date-parsable value; a null /
|
|
8258
|
+
// NaN bound is open-ended, both null clears the frame.
|
|
8259
|
+
setTimeFrame(min, max) {
|
|
8260
|
+
const toMs = v => (v == null || v === '' ? NaN : (typeof v === 'number' ? v : new Date(v).getTime()));
|
|
8261
|
+
const lo = toMs(min), hi = toMs(max);
|
|
8262
|
+
// flat: with the slider at its start position (frame starts at or
|
|
8263
|
+
// before the earliest record) every item is shown, not just the first window
|
|
8264
|
+
const field = this.binding && this.binding.time;
|
|
8265
|
+
if (field && !isNaN(lo)) {
|
|
8266
|
+
if (this._timeMin === undefined) {
|
|
8267
|
+
let m = Infinity;
|
|
8268
|
+
for (const f of this.features) {
|
|
8269
|
+
const v = f.properties && f.properties[field];
|
|
8270
|
+
const t = typeof v === 'number' ? v : new Date(v).getTime();
|
|
8271
|
+
if (!isNaN(t) && t < m) m = t;
|
|
8272
|
+
}
|
|
8273
|
+
this._timeMin = m;
|
|
8274
|
+
}
|
|
8275
|
+
if (lo <= this._timeMin) { this._timeFrame = null; this._rebuildActiveFeatures(); return; }
|
|
8276
|
+
}
|
|
8277
|
+
this._timeFrame = isNaN(lo) && isNaN(hi) ? null : { min: lo, max: hi };
|
|
8278
|
+
this._rebuildActiveFeatures();
|
|
8279
|
+
}
|
|
8280
|
+
|
|
8281
|
+
// predicate over a record's properties for the active time frame
|
|
8282
|
+
_timePredicate() {
|
|
8283
|
+
const tf = this._timeFrame;
|
|
8284
|
+
if (!tf) return null;
|
|
8285
|
+
const field = this.binding && this.binding.time;
|
|
8286
|
+
if (!field) return null;
|
|
8287
|
+
return props => {
|
|
8288
|
+
const v = props && props[field];
|
|
8289
|
+
const t = typeof v === 'number' ? v : new Date(v).getTime();
|
|
8290
|
+
if (isNaN(t)) return false;
|
|
8291
|
+
return (isNaN(tf.min) || t >= tf.min) && (isNaN(tf.max) || t < tf.max);
|
|
8292
|
+
};
|
|
8293
|
+
}
|
|
8294
|
+
|
|
7845
8295
|
// `_featuresByCategory` (plain per-category bucketing — NOT yet
|
|
7846
8296
|
// grid-aggregated; that's a separate, later step gated by AGGREGATE
|
|
7847
8297
|
// alone, see _computeAggregatedItems) is needed whenever this runtime
|
|
@@ -7872,7 +8322,8 @@
|
|
|
7872
8322
|
replaceFeatures(fc) {
|
|
7873
8323
|
// new data → new deck.gl layers (see _dataLayerId)
|
|
7874
8324
|
this._dataGen = (this._dataGen || 0) + 1;
|
|
7875
|
-
|
|
8325
|
+
this._timeMin = undefined;
|
|
8326
|
+
const facetFilters = this.facetFilters, runtimeFilterExpr = this._runtimeFilterExpr, timeFrame = this._timeFrame;
|
|
7876
8327
|
this.binding = this._specBinding;
|
|
7877
8328
|
this.features = applyField100(filterFlatValues(this._withDensity(fc.features), this.binding, this.flags), this.binding, this.flags, this.style);
|
|
7878
8329
|
this.binding = field100Binding(this.binding, this.flags);
|
|
@@ -7882,6 +8333,7 @@
|
|
|
7882
8333
|
this._prepare();
|
|
7883
8334
|
this.facetFilters = facetFilters;
|
|
7884
8335
|
this._runtimeFilterExpr = runtimeFilterExpr;
|
|
8336
|
+
this._timeFrame = timeFrame;
|
|
7885
8337
|
this._rebuildActiveFeatures();
|
|
7886
8338
|
}
|
|
7887
8339
|
|
|
@@ -7894,6 +8346,27 @@
|
|
|
7894
8346
|
// the next redraw.
|
|
7895
8347
|
_dataLayerId(base) { return this._dataGen ? `${base}-d${this._dataGen}` : base; }
|
|
7896
8348
|
|
|
8349
|
+
// deck.gl compares a layer's data by reference: a new object makes it
|
|
8350
|
+
// re-process every polygon (on the PNRR page ~300 ms per refresh for
|
|
8351
|
+
// 8 000 comuni, at every pan tick). Polygon data that doesn't depend
|
|
8352
|
+
// on the view is therefore handed over as the same object until its
|
|
8353
|
+
// inputs change.
|
|
8354
|
+
_featureCollection() {
|
|
8355
|
+
if (!this._featuresFC || this._featuresFC.features !== this.features) {
|
|
8356
|
+
this._featuresFC = { type: 'FeatureCollection', features: this.features };
|
|
8357
|
+
}
|
|
8358
|
+
return this._featuresFC;
|
|
8359
|
+
}
|
|
8360
|
+
// the same for derived data: build() runs only when an entry of key
|
|
8361
|
+
// (compared by identity) differs from the last call's
|
|
8362
|
+
_cachedPolygonData(slot, key, build) {
|
|
8363
|
+
const last = this[slot];
|
|
8364
|
+
if (last && last.key.length === key.length && last.key.every((v, i) => v === key[i])) return last.data;
|
|
8365
|
+
const data = build();
|
|
8366
|
+
this[slot] = { key, data };
|
|
8367
|
+
return data;
|
|
8368
|
+
}
|
|
8369
|
+
|
|
7897
8370
|
_usesAggregationIndex() {
|
|
7898
8371
|
return (this.flags.has('AGGREGATE') || isSymbolChart(this.flags)) && !this.flags.has('GRIDSIZE');
|
|
7899
8372
|
}
|
|
@@ -7905,8 +8378,10 @@
|
|
|
7905
8378
|
if (isAggregatedCategoricalChoropleth(this)) {
|
|
7906
8379
|
const expr = this._runtimeFilterExpr;
|
|
7907
8380
|
const clauses = Array.from(this.facetFilters.entries());
|
|
7908
|
-
|
|
7909
|
-
|
|
8381
|
+
const timePred = this._timePredicate();
|
|
8382
|
+
if (!expr && !clauses.length && !timePred) { this._activeFeatures = null; return; }
|
|
8383
|
+
const wherePred = expr ? flatFilterPredicate(String(expr)) : null;
|
|
8384
|
+
const pred = wherePred && timePred ? (r => wherePred(r) && timePred(r)) : (wherePred || timePred);
|
|
7910
8385
|
this._activeFeatures = this.features.map(f => {
|
|
7911
8386
|
const rows = f.properties && f.properties[AGGREGATED_ROWS];
|
|
7912
8387
|
if (!rows) return f;
|
|
@@ -7924,11 +8399,13 @@
|
|
|
7924
8399
|
// comment) if one is active — reusing applyWhereFilter, the SAME
|
|
7925
8400
|
// parser the layer's load-time .filter(expr) already uses, just
|
|
7926
8401
|
// invoked again here instead of once at build time.
|
|
7927
|
-
|
|
8402
|
+
let base = this._runtimeFilterExpr
|
|
7928
8403
|
? applyWhereFilter({ type: 'FeatureCollection', features: this.features }, this._runtimeFilterExpr).features
|
|
7929
8404
|
: this.features;
|
|
8405
|
+
const timePred = this._timePredicate();
|
|
8406
|
+
if (timePred) base = base.filter(f => timePred(f.properties));
|
|
7930
8407
|
if (this.facetFilters.size === 0) {
|
|
7931
|
-
this._activeFeatures = this._runtimeFilterExpr ? base : null;
|
|
8408
|
+
this._activeFeatures = (this._runtimeFilterExpr || timePred) ? base : null;
|
|
7932
8409
|
} else {
|
|
7933
8410
|
const clauses = Array.from(this.facetFilters.entries());
|
|
7934
8411
|
this._activeFeatures = base.filter(f =>
|
|
@@ -8298,10 +8775,11 @@
|
|
|
8298
8775
|
// formatting) instead of an inline SVG chart — one line per present
|
|
8299
8776
|
// category, each showing ITS OWN value, never summed across
|
|
8300
8777
|
// categories (same rule as the VALUES bubble labels).
|
|
8301
|
-
// - theme.item.data (a table of the hovered record's raw fields)
|
|
8302
|
-
//
|
|
8303
|
-
//
|
|
8304
|
-
//
|
|
8778
|
+
// - theme.item.data (a table of the hovered record's raw fields)
|
|
8779
|
+
// tables an individual point's record, and an aggregated item's
|
|
8780
|
+
// member records (numbered, up to 50 — flat's __add_data does the
|
|
8781
|
+
// same); only a group without retained records (cluster-level
|
|
8782
|
+
// groups) resolves to an empty string there.
|
|
8305
8783
|
// an item's title as flat gives it (maptheme.js 11838-11886): the title
|
|
8306
8784
|
// field of its record — of an aggregated item its first record; an
|
|
8307
8785
|
// aggregated grid cell (no aggregation field) of more than 3 records,
|
|
@@ -8317,7 +8795,7 @@
|
|
|
8317
8795
|
return title;
|
|
8318
8796
|
}
|
|
8319
8797
|
|
|
8320
|
-
buildTooltipHtml(object) {
|
|
8798
|
+
buildTooltipHtml(object, fDark) {
|
|
8321
8799
|
// no .meta({tooltip}): flat's default template (tooltip_mustache.js 394)
|
|
8322
8800
|
// — the theme title, the item's title and its chart
|
|
8323
8801
|
const template = this.meta.tooltip || FLAT_DEFAULT_TOOLTIP;
|
|
@@ -8326,11 +8804,16 @@
|
|
|
8326
8804
|
'include https://unpkg.com/mustache@4.2.0/mustache.min.js before ixmaps-gl.js.');
|
|
8327
8805
|
return null;
|
|
8328
8806
|
}
|
|
8329
|
-
return global.Mustache.render(template, this._buildTooltipContext(object));
|
|
8807
|
+
return youtubeClickToPlay(global.Mustache.render(template, this._buildTooltipContext(object, fDark)));
|
|
8330
8808
|
}
|
|
8331
8809
|
|
|
8332
|
-
_buildTooltipContext(object) {
|
|
8333
|
-
|
|
8810
|
+
_buildTooltipContext(object, fDark) {
|
|
8811
|
+
let props = object.properties;
|
|
8812
|
+
// a FEATURE|CATEGORICAL polygon is the raw data record itself, not the
|
|
8813
|
+
// {raw, cat, value} wrapper the other layer kinds build
|
|
8814
|
+
if (props && !props.raw && (this.flags.has('FEATURE') || this.flags.has('FEATURES')) && this.flags.has('CATEGORICAL')) {
|
|
8815
|
+
props = { raw: props, cat: this._resolveClassIndex(props[this.binding.value]) };
|
|
8816
|
+
}
|
|
8334
8817
|
const isGroup = !!props.counts;
|
|
8335
8818
|
const chartHtml = this._renderItemChartHtml(props);
|
|
8336
8819
|
|
|
@@ -8342,7 +8825,7 @@
|
|
|
8342
8825
|
chart: chartHtml,
|
|
8343
8826
|
item: {
|
|
8344
8827
|
chart: chartHtml,
|
|
8345
|
-
data: isGroup ?
|
|
8828
|
+
data: isGroup ? this._renderGroupDataTableHtml(props.raws, fDark) : this._renderItemDataTableHtml(props.raw, fDark),
|
|
8346
8829
|
value: this._hasValue(isGroup ? props.total : props.value) ? this._formatTooltipValue(isGroup ? props.total : props.value) : '',
|
|
8347
8830
|
label: isGroup ? '' : (this.categoryDisplayLabels ? (this.categoryDisplayLabels[props.cat] || '') : ''),
|
|
8348
8831
|
// flat: the item's title is its titlefield value (binding title/
|
|
@@ -8485,14 +8968,49 @@
|
|
|
8485
8968
|
}
|
|
8486
8969
|
|
|
8487
8970
|
// theme.item.data approximation — a plain field:value table of the
|
|
8488
|
-
// hovered record's own raw properties (individual points only)
|
|
8489
|
-
|
|
8971
|
+
// hovered record's own raw properties (individual points only) — flat's
|
|
8972
|
+
// own row markup (tooltip_mustache.js 568-590, main.css .tooltip): the
|
|
8973
|
+
// field name right-aligned, the value left, a dotted rule above the
|
|
8974
|
+
// value, the table at 0.85em with flat's min-width. Light is flat
|
|
8975
|
+
// exactly (#cccccc labels, #000000 values, dotted black); dark keeps
|
|
8976
|
+
// the same visibility hierarchy inverted - DIM labels, BRIGHT values -
|
|
8977
|
+
// with its own colors, a dotted #555 rule and #888 record pills
|
|
8978
|
+
_renderItemDataTableHtml(raw, fDark) {
|
|
8490
8979
|
if (!raw) return '';
|
|
8980
|
+
const value = fDark ? '#e0e4ea' : '#000000';
|
|
8981
|
+
const label = fDark ? '#787d85' : '#cccccc';
|
|
8982
|
+
const rule = fDark ? '#555' : 'black';
|
|
8491
8983
|
const rows = Object.keys(raw)
|
|
8492
8984
|
.filter(k => k !== 'geometry')
|
|
8493
|
-
.map(k => `<tr><td style="
|
|
8985
|
+
.map(k => `<tr><td></td><td style="text-align:right;vertical-align:top;color:${label}">${k}</td>` +
|
|
8986
|
+
`<td style="text-align:left;vertical-align:top;color:${value};padding:0 0.2em 0 0.8em;border-top:dotted ${rule} 0.1px">` +
|
|
8987
|
+
`${this._isNumericValue(raw[k]) ? this._formatTooltipValue(raw[k]) : raw[k]}</td></tr>`)
|
|
8494
8988
|
.join('');
|
|
8495
|
-
return `<table style="font-size:0.
|
|
8989
|
+
return `<table style="font-size:0.85em;min-width:300px;border-collapse:collapse">${rows}</table>`;
|
|
8990
|
+
}
|
|
8991
|
+
|
|
8992
|
+
// an aggregated item's own records, numbered, up to 50 — like real
|
|
8993
|
+
// flat's __add_data (tooltip_mustache.js 490-570: one numbered block
|
|
8994
|
+
// per record when the item holds more than one). A single-record
|
|
8995
|
+
// group renders like an individual point.
|
|
8996
|
+
_renderGroupDataTableHtml(raws, fDark) {
|
|
8997
|
+
if (!raws || !raws.length) return '';
|
|
8998
|
+
if (raws.length === 1) return this._renderItemDataTableHtml(raws[0], fDark);
|
|
8999
|
+
const value = fDark ? '#e0e4ea' : '#000000';
|
|
9000
|
+
const label = fDark ? '#787d85' : '#cccccc';
|
|
9001
|
+
const rule = fDark ? '#555' : 'black';
|
|
9002
|
+
const pill = fDark ? '#888' : 'black';
|
|
9003
|
+
const rows = raws.slice(0, 50).map((raw, d) =>
|
|
9004
|
+
'<tr><td style="font-size:1em;"> </td></tr>' +
|
|
9005
|
+
`<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>` +
|
|
9006
|
+
Object.keys(raw)
|
|
9007
|
+
.filter(k => k !== 'geometry')
|
|
9008
|
+
.map(k => `<tr><td></td><td style="text-align:right;vertical-align:top;color:${label}">${k}</td>` +
|
|
9009
|
+
`<td style="text-align:left;vertical-align:top;color:${value};padding:0 0.2em 0 0.8em;border-top:dotted ${rule} 0.1px">` +
|
|
9010
|
+
`${this._isNumericValue(raw[k]) ? this._formatTooltipValue(raw[k]) : raw[k]}</td></tr>`)
|
|
9011
|
+
.join('')
|
|
9012
|
+
).join('');
|
|
9013
|
+
return `<table style="font-size:0.85em;min-width:300px;border-collapse:collapse">${rows}</table>`;
|
|
8496
9014
|
}
|
|
8497
9015
|
|
|
8498
9016
|
_isNumericValue(v) {
|
|
@@ -8619,11 +9137,19 @@
|
|
|
8619
9137
|
const cs = this.style.colorscheme;
|
|
8620
9138
|
const raw = Array.isArray(cs) ? cs[0] : cs;
|
|
8621
9139
|
const filled = raw !== 'none';
|
|
9140
|
+
// FEATURE|CATEGORICAL: polygons coloured by their category (flat colours
|
|
9141
|
+
// each shape by its class), not one flat fill
|
|
9142
|
+
const byCategory = filled && this.flags.has('CATEGORICAL') && this.categoryIndexByLabel && this.binding.value
|
|
9143
|
+
&& this.categoryColorsRgb && this.categoryColorsRgb.length;
|
|
9144
|
+
const catAlpha = Math.round(255 * (styleNum(this.style.fillopacity) || 1));
|
|
9145
|
+
const nodataRgb = parseCssColor(this.style.nodatacolor) || [255, 255, 255];
|
|
8622
9146
|
const shadow = filled && flatShadowOn(this.style, this.features.length, zoom)
|
|
8623
9147
|
? this._buildShadowLayers(hexOrNamedToRgb(raw), zoom, bbox) : [];
|
|
8624
9148
|
return shadow.concat([new GeoJsonLayer({
|
|
8625
9149
|
id: this._dataLayerId(`ix-features-${this.name}`),
|
|
8626
|
-
data:
|
|
9150
|
+
data: this._featureCollection(),
|
|
9151
|
+
// FEATURE|CATEGORICAL polygons carry the data record: hover / click tooltips
|
|
9152
|
+
pickable: !!byCategory,
|
|
8627
9153
|
// linecolor "none": no outline (a FEATURES theme of points with
|
|
8628
9154
|
// colorscheme and linecolor "none" draws nothing, as flat)
|
|
8629
9155
|
stroked: styleLineColor(this.style.linecolor) !== 'none',
|
|
@@ -8634,7 +9160,13 @@
|
|
|
8634
9160
|
// flat puts fillopacity into the shape's fill-opacity only
|
|
8635
9161
|
// (maptheme.js 13407, 14071-14078): the outline stays opaque
|
|
8636
9162
|
// (a fill color's own alpha — rgba(…, 0.5) — times fillopacity)
|
|
8637
|
-
|
|
9163
|
+
...(byCategory ? { updateTriggers: { getFillColor: JSON.stringify(this.categoryColorsRgb) } } : {}),
|
|
9164
|
+
getFillColor: byCategory
|
|
9165
|
+
? f => {
|
|
9166
|
+
const cat = this._resolveClassIndex(f.properties[this.binding.value]);
|
|
9167
|
+
return [...(cat != null ? this.categoryColorsRgb[cat] : nodataRgb).slice(0, 3), catAlpha];
|
|
9168
|
+
}
|
|
9169
|
+
: filled ? [...hexOrNamedToRgb(raw).slice(0, 3), Math.round(255 * cssColorAlpha(raw) * (styleNum(this.style.fillopacity) || 1))] : [0, 0, 0, 0],
|
|
8638
9170
|
getLineColor: this.style.linecolor
|
|
8639
9171
|
? withAlpha(hexOrNamedToRgb(styleLineColor(this.style.linecolor)), cssColorAlpha(styleLineColor(this.style.linecolor)))
|
|
8640
9172
|
: [130, 130, 130],
|
|
@@ -8708,8 +9240,8 @@
|
|
|
8708
9240
|
// single winning class, so no `cat`/tooltip chart line, just the
|
|
8709
9241
|
// blended color directly); AGGREGATE CATEGORICAL on one field — the
|
|
8710
9242
|
// dominant category of each polygon's records (aggregatedCategoricalClass).
|
|
8711
|
-
// A plain CATEGORICAL choropleth
|
|
8712
|
-
//
|
|
9243
|
+
// A plain CATEGORICAL choropleth matches the raw value of the polygon's
|
|
9244
|
+
// row against the category labels. Geometry + properties are already the joined
|
|
8713
9245
|
// FeatureCollection from joinChoroplethFeatures. `zoom` is only used
|
|
8714
9246
|
// for style.fillopacity:"auto" (see resolveAutoFillOpacity) — every
|
|
8715
9247
|
// other branch here is zoom-independent.
|
|
@@ -8735,65 +9267,76 @@
|
|
|
8735
9267
|
// AGGREGATE CATEGORICAL: every polygon's parts and dominant class
|
|
8736
9268
|
// (aggregatedCategoricalClass), the biggest part per class over all
|
|
8737
9269
|
// 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 => {
|
|
9270
|
+
// the polygons' classes don't depend on the view or on marked
|
|
9271
|
+
// classes (those only change the colors, see updateTriggers below):
|
|
9272
|
+
// the data is rebuilt only when its inputs change
|
|
9273
|
+
const dataKey = [source, this._dataGen, this.categoryIndexByLabel, this.partsA, this._classSeparate,
|
|
9274
|
+
dopacityActive ? baseOpacity : null, JSON.stringify(this.style), JSON.stringify(this.binding)];
|
|
9275
|
+
const collection = this._cachedPolygonData('_choroplethData', dataKey, () => {
|
|
9276
|
+
const aggCat = isAggregatedCategoricalChoropleth(this) && this.categoryIndexByLabel;
|
|
9277
|
+
let aggOf = null, aggMaxA = null;
|
|
8754
9278
|
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
|
-
};
|
|
9279
|
+
const nCats = this.categoryLabels.length;
|
|
9280
|
+
aggOf = new Map();
|
|
9281
|
+
aggMaxA = new Array(nCats).fill(0);
|
|
9282
|
+
source.forEach(f => {
|
|
9283
|
+
const rows = f.properties && f.properties[AGGREGATED_ROWS];
|
|
9284
|
+
if (!rows) return;
|
|
9285
|
+
const agg = aggregatedCategoricalClass(rows, this.binding, this.flags, this.categoryIndexByLabel, nCats);
|
|
9286
|
+
if (!agg) return;
|
|
9287
|
+
aggOf.set(f, agg);
|
|
9288
|
+
agg.parts.forEach((v, i) => { if (v > aggMaxA[i]) aggMaxA[i] = v; });
|
|
9289
|
+
});
|
|
8772
9290
|
}
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
9291
|
+
const data = source.map(f => {
|
|
9292
|
+
if (aggCat) {
|
|
9293
|
+
const agg = aggOf.get(f);
|
|
9294
|
+
return {
|
|
9295
|
+
type: 'Feature',
|
|
9296
|
+
geometry: f.geometry,
|
|
9297
|
+
properties: {
|
|
9298
|
+
// no part above 0: no item in flat, not painted
|
|
9299
|
+
value: agg ? agg.value : undefined, raw: agg ? f.properties : {}, cat: agg ? agg.index : null, parts: agg ? agg.parts : undefined,
|
|
9300
|
+
dopacityAlpha: agg && dopacityActive ? dominantDopacityAlpha(this.style, this.flags, agg.value, aggMaxA[agg.index]) : null
|
|
9301
|
+
}
|
|
9302
|
+
};
|
|
9303
|
+
}
|
|
9304
|
+
if (isComposeColor) {
|
|
9305
|
+
return {
|
|
9306
|
+
type: 'Feature',
|
|
9307
|
+
geometry: f.geometry,
|
|
9308
|
+
properties: { raw: f.properties, composedColor: this._resolveComposedColor(f.properties) }
|
|
9309
|
+
};
|
|
9310
|
+
}
|
|
9311
|
+
if (isDominant) {
|
|
9312
|
+
const dom = this._resolveDominantClass(f.properties);
|
|
9313
|
+
const value = dom ? dom.value : undefined;
|
|
9314
|
+
return {
|
|
9315
|
+
type: 'Feature',
|
|
9316
|
+
geometry: f.geometry,
|
|
9317
|
+
properties: {
|
|
9318
|
+
value, raw: f.properties, cat: dom ? dom.index : null,
|
|
9319
|
+
dopacityAlpha: dopacityActive ? this._resolveDopacityAlpha(f, value, baseOpacity) : null
|
|
9320
|
+
}
|
|
9321
|
+
};
|
|
9322
|
+
}
|
|
9323
|
+
// a polygon without a data row is no item in flat: no value
|
|
9324
|
+
const hasRow = f.properties && Object.keys(f.properties).length > 0;
|
|
9325
|
+
// CATEGORICAL matches the raw value against the category labels
|
|
9326
|
+
// (a text value has no numeric form: flatNumber would give NaN)
|
|
9327
|
+
const rawVal = hasRow ? f.properties[this.binding.value] : undefined;
|
|
9328
|
+
const value = this.categoryIndexByLabel ? rawVal : (hasRow ? flatNumber(rawVal, this.flags) : NaN);
|
|
9329
|
+
const cat = this._resolveClassIndex(value);
|
|
8776
9330
|
return {
|
|
8777
9331
|
type: 'Feature',
|
|
8778
9332
|
geometry: f.geometry,
|
|
8779
9333
|
properties: {
|
|
8780
|
-
value, raw: f.properties, cat
|
|
9334
|
+
value, raw: f.properties, cat,
|
|
8781
9335
|
dopacityAlpha: dopacityActive ? this._resolveDopacityAlpha(f, value, baseOpacity) : null
|
|
8782
9336
|
}
|
|
8783
9337
|
};
|
|
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
|
-
};
|
|
9338
|
+
});
|
|
9339
|
+
return { type: 'FeatureCollection', features: data };
|
|
8797
9340
|
});
|
|
8798
9341
|
// A marked class (legend row click / ixmaps.markThemeClass) as real
|
|
8799
9342
|
// ixmaps-flat shows it on a choropleth (MapTheme.markClass,
|
|
@@ -8815,9 +9358,13 @@
|
|
|
8815
9358
|
return evidenceMode === 'isolate' ? 'hide' : 'gray';
|
|
8816
9359
|
};
|
|
8817
9360
|
const grayOf = ([r, g, b]) => { const y = Math.round(0.299 * r + 0.587 * g + 0.114 * b); return [y, y, y]; };
|
|
9361
|
+
// deck.gl re-runs the color accessors only for new data or a changed trigger
|
|
9362
|
+
const colorKey = JSON.stringify([[...this._markedClasses], !!this._onMarksChanged, evidenceMode,
|
|
9363
|
+
this.categoryColorsRgb, fallbackRgb, classAlpha, lineRgb]);
|
|
8818
9364
|
return [new GeoJsonLayer({
|
|
8819
9365
|
id: this._dataLayerId(`ix-choropleth-${this.name}`),
|
|
8820
|
-
data:
|
|
9366
|
+
data: collection,
|
|
9367
|
+
updateTriggers: { getFillColor: colorKey, getLineColor: colorKey },
|
|
8821
9368
|
pickable: true,
|
|
8822
9369
|
stroked: true,
|
|
8823
9370
|
filled: true,
|
|
@@ -9890,7 +10437,11 @@
|
|
|
9890
10437
|
this._valueMedian = sorted[Math.floor((sorted.length - 1) / 2)];
|
|
9891
10438
|
}
|
|
9892
10439
|
|
|
9893
|
-
|
|
10440
|
+
// flat's default normalsizevalue is the largest aggregated size (maptheme.js 5021-5022)
|
|
10441
|
+
let sizeMax = 0;
|
|
10442
|
+
for (const v of rawSizeTotals) if (Math.abs(v) > sizeMax) sizeMax = Math.abs(v);
|
|
10443
|
+
|
|
10444
|
+
this._aggregateStatsCache = { zoom, outlier, normalize, breaks, sizeMax };
|
|
9894
10445
|
return this._aggregateStatsCache;
|
|
9895
10446
|
}
|
|
9896
10447
|
|
|
@@ -9914,6 +10465,12 @@
|
|
|
9914
10465
|
// _computeAggregatedItems below — a plain (non-AGGREGATE)
|
|
9915
10466
|
// BUBBLE/CHART theme never reads them.
|
|
9916
10467
|
if (this.flags.has('AGGREGATE')) this._ensureClusterIndices(zoom);
|
|
10468
|
+
// AGGREGATE sums without a normalsizevalue: flat's default is the
|
|
10469
|
+
// largest aggregated value (defaultNormalSizeValue leaves it unset, the
|
|
10470
|
+
// sizes depend on the aggregation)
|
|
10471
|
+
if (this.flags.has('AGGREGATE') && !this.flags.has('CATEGORICAL') && !styleNum(this.style.normalsizevalue) && !this.binding.size) {
|
|
10472
|
+
this._maxSizeValue = this._ensureAggregateStats(zoom).sizeMax || undefined;
|
|
10473
|
+
}
|
|
9917
10474
|
let { individual, groups } = this._computeAggregatedItems(bbox, zoom);
|
|
9918
10475
|
// Far-hemisphere cull under globe projection — see
|
|
9919
10476
|
// isOnVisibleHemisphere's own comment for why this is needed at
|
|
@@ -9930,7 +10487,7 @@
|
|
|
9930
10487
|
}
|
|
9931
10488
|
const layers = [];
|
|
9932
10489
|
|
|
9933
|
-
if (this.flags.has('NOOUTLIER')
|
|
10490
|
+
if (this.flags.has('NOOUTLIER')|| this.flags.has('NORMALIZE') || this._rangeClassed) {
|
|
9934
10491
|
const stats = this._ensureAggregateStats(zoom);
|
|
9935
10492
|
|
|
9936
10493
|
// the quantity NOOUTLIER/NORMALIZE/classes act on: the separate
|
|
@@ -10041,6 +10598,10 @@
|
|
|
10041
10598
|
// _buildTooltipContext's own `isGroup` check already uses).
|
|
10042
10599
|
const sizeValueOf = d => d.properties.counts ? d.properties.total : d.properties.value;
|
|
10043
10600
|
const combined = individual.concat(groups).sort((a, b) => sizeValueOf(a) - sizeValueOf(b));
|
|
10601
|
+
// maxcharts: like the flat engine (maptheme.js nMaxCharts), draw only the
|
|
10602
|
+
// biggest N items — combined is ascending, so drop from the front
|
|
10603
|
+
const maxCharts = Math.round(styleNum(this.style.maxcharts)) || 0;
|
|
10604
|
+
if (maxCharts > 0 && combined.length > maxCharts) combined.splice(0, combined.length - maxCharts);
|
|
10044
10605
|
if (this.flags.has('USER')) {
|
|
10045
10606
|
const userLayers = this._buildUserChartLayers(combined, liveZoom, sizeValueOf);
|
|
10046
10607
|
if (userLayers) return layers.concat(userLayers);
|
|
@@ -10063,7 +10624,16 @@
|
|
|
10063
10624
|
const itemRgb = d => this.categoryColorsRgb[d.properties.counts ? dominant(d.properties.counts) : d.properties.cat] || [128, 128, 128];
|
|
10064
10625
|
const radiusOf = d => valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue);
|
|
10065
10626
|
// align / offsetx / offsety: the whole chart moves (flatChartAlignOffset)
|
|
10066
|
-
|
|
10627
|
+
// MULTIQUAD / MULTISQUARE: the items at one position side by side
|
|
10628
|
+
// (multiQuadOffsets), added to the chart position
|
|
10629
|
+
const baseAlign = this._chartAlignFn(liveZoom, radiusOf);
|
|
10630
|
+
const multi = flatFlag(this.flags, 'MULTIQUAD') || flatFlag(this.flags, 'MULTISQUARE')
|
|
10631
|
+
? multiQuadOffsets(combined, NORMAL_RADIUS_PX * objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1),
|
|
10632
|
+
styleNum(this.style.rangescale) || 1, styleNum(this.style.gridx) || 0)
|
|
10633
|
+
: null;
|
|
10634
|
+
const alignOf = multi
|
|
10635
|
+
? 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 })
|
|
10636
|
+
: baseAlign;
|
|
10067
10637
|
|
|
10068
10638
|
// SHADOW (style.shadow, flat's gate: maxshadow, shadowupper/lower — flatShadowOn): a soft
|
|
10069
10639
|
// drop shadow under each symbol, see _getShadowIcon
|
|
@@ -10145,28 +10715,42 @@
|
|
|
10145
10715
|
// (#888888) over a half-opaque white halo (stroke font/7); its first
|
|
10146
10716
|
// baseline one font below the box (BOTTOMTITLE) or 0.7 font above it.
|
|
10147
10717
|
// Box and title are removed outside boxupper/boxlower.
|
|
10718
|
+
// Flat draws box and title in the chart group, which is scaled by
|
|
10719
|
+
// style.scale: the units below are u · scale. Its size measure W is
|
|
10720
|
+
// the chart's width (2r; with MULTIQUAD the width of the whole grid
|
|
10721
|
+
// at the position, the box enclosing all of it); flat's chart size
|
|
10722
|
+
// is 30 units.
|
|
10148
10723
|
const boxShown = flatFlag(this.flags, 'BOX') && !boxHiddenByScale(this.style, liveZoom);
|
|
10149
10724
|
const circular = flatFlag(this.flags, 'CIRCULAR');
|
|
10150
|
-
const unitPx = objectZoomFactor(liveZoom, this.mapOptions);
|
|
10725
|
+
const unitPx = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1);
|
|
10151
10726
|
const boxMarginU = styleNum(this.style.boxmargin) || 2;
|
|
10152
10727
|
const titleShown = boxShown && flatFlag(this.flags, 'TITLE') && !!this.binding.title;
|
|
10153
10728
|
const titleFontPx = 5 * unitPx * (styleNum(this.style.textscale) || styleNum(this.style.valuescale) || 1);
|
|
10729
|
+
// flat: fill-opacity (boxopacity || 1) on the raw style value — the
|
|
10730
|
+
// usual string "0" is transparent; typed here, a given 0 is too
|
|
10731
|
+
const boxOpacity = isNaN(styleNum(this.style.boxopacity)) ? 1 : styleNum(this.style.boxopacity);
|
|
10154
10732
|
const bottomTitle = flatFlag(this.flags, 'BOTTOMTITLE');
|
|
10155
10733
|
const alignRight = /right/.test(String(this.style.align || ''));
|
|
10156
|
-
|
|
10734
|
+
// with MULTIQUAD only the first item of a position gets its box/title
|
|
10735
|
+
const boxLayout = boxShown ? combined.filter(d => !multi || multi.index.get(d) === 0).map(d => {
|
|
10157
10736
|
const r = valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue);
|
|
10158
|
-
const
|
|
10737
|
+
const ext = multi ? multi.extent.get(d) : [0, 0, 0, 0];
|
|
10738
|
+
const chart = [ext[0] - r, ext[1] - r, ext[2] + r, ext[3] + r];
|
|
10739
|
+
const w = chart[2] - chart[0];
|
|
10740
|
+
const margin = Math.min(2 * boxMarginU * unitPx, boxMarginU * w / 30);
|
|
10159
10741
|
const boxR = circular ? r * 1.1 + margin : r;
|
|
10160
10742
|
let title = null;
|
|
10161
10743
|
if (titleShown) {
|
|
10162
10744
|
const text = this._itemTitle(d.properties);
|
|
10163
10745
|
if (text) {
|
|
10164
10746
|
const f = titleFontPx;
|
|
10165
|
-
const lines = wrapTitleLines(text, f, boxR * 2).split('\n');
|
|
10747
|
+
const lines = wrapTitleLines(text, f, circular ? boxR * 2 : w).split('\n');
|
|
10166
10748
|
const width = Math.max(...lines.map(l => titleTextWidth(l, f)));
|
|
10167
10749
|
// first baseline; lines go down one font each
|
|
10168
|
-
const base =
|
|
10169
|
-
|
|
10750
|
+
const base = circular
|
|
10751
|
+
? (bottomTitle ? boxR + f : -boxR - 0.7 * f - (lines.length - 1) * f)
|
|
10752
|
+
: (bottomTitle ? chart[3] + f : chart[1] - 0.7 * f - (lines.length - 1) * f);
|
|
10753
|
+
const x = circular ? unitPx / 2 : (alignRight ? chart[2] : chart[0]);
|
|
10170
10754
|
const anchor = circular ? 'middle' : (alignRight ? 'end' : 'start');
|
|
10171
10755
|
const x0 = anchor === 'middle' ? x - width / 2 : anchor === 'end' ? x - width : x;
|
|
10172
10756
|
// arial: ascent 0.905, descent 0.212 of the font size
|
|
@@ -10176,16 +10760,16 @@
|
|
|
10176
10760
|
}
|
|
10177
10761
|
let rect = null;
|
|
10178
10762
|
if (!circular) {
|
|
10179
|
-
const e =
|
|
10763
|
+
const e = chart.slice();
|
|
10180
10764
|
// with a title flat trims 0.1 font off the top of chart + title
|
|
10181
10765
|
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
10766
|
rect = [e[0] - margin, e[1] - margin, e[2] + margin, e[3] + margin];
|
|
10183
10767
|
}
|
|
10184
|
-
return { d, r, boxR, rect, title };
|
|
10768
|
+
return { d, r, w, boxR, rect, title };
|
|
10185
10769
|
}) : null;
|
|
10186
10770
|
if (boxShown && circular) {
|
|
10187
10771
|
const boxRgb = hexOrNamedToRgb(this.style.boxcolor || '#eeeeee');
|
|
10188
|
-
const boxAlpha = Math.round(255 *
|
|
10772
|
+
const boxAlpha = Math.round(255 * boxOpacity);
|
|
10189
10773
|
const borderRgb = hexOrNamedToRgb(this.style.bordercolor || '#dddddd');
|
|
10190
10774
|
// ScatterplotLayer has no pixel offset: a moved chart (align,
|
|
10191
10775
|
// offsetx/offsety) gets its circle as an icon
|
|
@@ -10218,20 +10802,47 @@
|
|
|
10218
10802
|
const borderStyle = String(this.style.borderstyle || '');
|
|
10219
10803
|
const boxLook = {
|
|
10220
10804
|
fill: this.style.boxcolor || 'white',
|
|
10221
|
-
fillOpacity:
|
|
10805
|
+
fillOpacity: boxOpacity,
|
|
10222
10806
|
stroke: borderStyle === 'none' ? null : styleLineColor(this.style.bordercolor) || '#bbbbbb',
|
|
10223
10807
|
strokeOpacity: /^(solid|dotted|dashed)$/.test(borderStyle) ? 1 : 0.3,
|
|
10224
10808
|
};
|
|
10225
10809
|
const borderWidth = styleNum(this.style.borderwidth) || 1;
|
|
10226
10810
|
const borderRadius = styleNum(this.style.borderradius);
|
|
10227
|
-
|
|
10811
|
+
const strokeOf = b => borderWidth * 0.2 * b.w / 30;
|
|
10812
|
+
// a box larger than BOX_ICON_MAX_PX (a MULTIQUAD grid of thousands
|
|
10813
|
+
// of items) would need a huge canvas: it is drawn as a polygon in
|
|
10814
|
+
// map coordinates instead (square corners)
|
|
10815
|
+
const bigBox = b => Math.max(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1]) > BOX_ICON_MAX_PX;
|
|
10816
|
+
const iconBoxes = boxLayout.filter(b => !bigBox(b)), polygonBoxes = boxLayout.filter(bigBox);
|
|
10817
|
+
iconBoxes.forEach(b => {
|
|
10228
10818
|
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
|
-
|
|
10819
|
+
isNaN(borderRadius) ? 0 : borderRadius * Math.min(unitPx, b.w / 30),
|
|
10820
|
+
strokeOf(b), boxLook);
|
|
10231
10821
|
});
|
|
10232
|
-
|
|
10822
|
+
if (polygonBoxes.length) {
|
|
10823
|
+
const fillRgb = hexOrNamedToRgb(boxLook.fill);
|
|
10824
|
+
const strokeRgb = boxLook.stroke ? hexOrNamedToRgb(boxLook.stroke) : null;
|
|
10825
|
+
layers.push(new GeoJsonLayer({
|
|
10826
|
+
id: `ix-box-polygons-${this.name}`,
|
|
10827
|
+
data: polygonBoxes.map(b => {
|
|
10828
|
+
const o = alignOf(b.d), c = b.d.geometry.coordinates, r = b.rect;
|
|
10829
|
+
const at = (x, y) => pixelOffsetLngLat(c, [x + o[0], y + o[1]], liveZoom);
|
|
10830
|
+
return { type: 'Feature', properties: { stroke: strokeOf(b) },
|
|
10831
|
+
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])]] } };
|
|
10832
|
+
}),
|
|
10833
|
+
pickable: false,
|
|
10834
|
+
filled: boxLook.fillOpacity > 0,
|
|
10835
|
+
stroked: !!strokeRgb,
|
|
10836
|
+
getFillColor: [...fillRgb.slice(0, 3), Math.round(255 * boxLook.fillOpacity)],
|
|
10837
|
+
getLineColor: strokeRgb ? [...strokeRgb.slice(0, 3), Math.round(255 * boxLook.strokeOpacity)] : [0, 0, 0, 0],
|
|
10838
|
+
getLineWidth: f => f.properties.stroke,
|
|
10839
|
+
lineWidthUnits: 'pixels',
|
|
10840
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
10841
|
+
}));
|
|
10842
|
+
}
|
|
10843
|
+
if (iconBoxes.length) layers.push(new IconLayer({
|
|
10233
10844
|
id: `ix-box-${this.name}-g${this._iconGeneration}`,
|
|
10234
|
-
data:
|
|
10845
|
+
data: iconBoxes, pickable: false,
|
|
10235
10846
|
getPosition: b => b.d.geometry.coordinates,
|
|
10236
10847
|
getIcon: b => b.icon,
|
|
10237
10848
|
// the icon is drawn at 2× with a stroke padding around the box
|
|
@@ -10251,7 +10862,9 @@
|
|
|
10251
10862
|
? this._buildBubbleIcon(d.properties.counts, this.categoryColorsRgb, this._groupIsolation(d))
|
|
10252
10863
|
: this._buildSingleIcon(this.categoryColorsRgb[d.properties.cat], fillOpacity, this._resolveSymbolShape(d.properties), singleBorderColorRgb, singleBorderWidthPx),
|
|
10253
10864
|
getSize: d => valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue) * 2,
|
|
10254
|
-
|
|
10865
|
+
// group icons bake a fixed 0.9 alpha; an explicit fillopacity scales it
|
|
10866
|
+
// so a near-transparent theme doesn't show opaque 1-px-cell clusters
|
|
10867
|
+
getColor: d => [255, 255, 255, Math.round(this._iconAlpha(d) * (d.properties.counts && this.style.fillopacity != null ? fillOpacity : 1))],
|
|
10255
10868
|
...(alignOf.active ? { getPixelOffset: alignOf } : {}),
|
|
10256
10869
|
sizeUnits: 'pixels',
|
|
10257
10870
|
billboard: true,
|
|
@@ -10816,6 +11429,7 @@
|
|
|
10816
11429
|
// - themes: getThemeObj (flat's theme object, with its data table and
|
|
10817
11430
|
// the items on the map), getThemeDefinitionObj, getThemes,
|
|
10818
11431
|
// changeThemeStyle / removeTheme / setBasemapOpacity (map name first),
|
|
11432
|
+
// setThemeTimeFrame (themeId, min, max),
|
|
10819
11433
|
// refreshTheme, markThemeClass / unmarkThemeClass
|
|
10820
11434
|
// - data: ixmaps.data (facets: getFacets, showFacets, …), the
|
|
10821
11435
|
// embeddedSVG.window tables (setExternalData is with the data loading)
|
|
@@ -10859,22 +11473,73 @@
|
|
|
10859
11473
|
try { hook.call(global.ixmaps, themeIdOf(rt)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onNewTheme:', e); }
|
|
10860
11474
|
}
|
|
10861
11475
|
|
|
10862
|
-
// flat's ixmaps.
|
|
10863
|
-
//
|
|
10864
|
-
//
|
|
11476
|
+
// flat's ixmaps.themeApi (htmlgui_flat.js 1112-1196): the theme handle a
|
|
11477
|
+
// page gets from map().theme(name) — changeStyle is flat's canonical
|
|
11478
|
+
// global changeThemeStyle(szThemeName, szStyle, szFlag) (htmlgui.js 2050)
|
|
11479
|
+
// in handle form; show / hide / toggle are setThemeVisible, remove the map
|
|
11480
|
+
// handle's. flat's own markClass delegates 3-arg — a stale call against
|
|
11481
|
+
// htmlgui.js's 2-arg markThemeClass (2196) — the canonical form is used
|
|
11482
|
+
// here. Theme ids are global (the registry), so the map binding only
|
|
11483
|
+
// matters for replace.
|
|
11484
|
+
function themeApi(szMap, szTheme) {
|
|
11485
|
+
this.szMap = szMap || null;
|
|
11486
|
+
this.szTheme = szTheme || null;
|
|
11487
|
+
}
|
|
11488
|
+
themeApi.prototype = {
|
|
11489
|
+
changeStyle(szStyle, szFlag) { compatChangeThemeStyle(this.szTheme, szStyle, szFlag); return this; },
|
|
11490
|
+
markClass(nClass) { markThemeClass(this.szTheme, nClass); return this; },
|
|
11491
|
+
unmarkClass(nClass) { unmarkThemeClass(this.szTheme, nClass); return this; },
|
|
11492
|
+
show() { mapHandle(this.szMap).setThemeVisible(this.szTheme, true); return this; },
|
|
11493
|
+
hide() { mapHandle(this.szMap).setThemeVisible(this.szTheme, false); return this; },
|
|
11494
|
+
toggle() {
|
|
11495
|
+
const rt = _globalThemeRegistry.get(this.szTheme);
|
|
11496
|
+
mapHandle(this.szMap).setThemeVisible(this.szTheme, rt ? !!rt._hidden : false);
|
|
11497
|
+
return this;
|
|
11498
|
+
},
|
|
11499
|
+
remove() { mapHandle(this.szMap).remove(this.szTheme); return this; },
|
|
11500
|
+
replace(theme, flag) { mapHandle(this.szMap).replaceTheme(this.szTheme, theme, flag); return this; }
|
|
11501
|
+
};
|
|
11502
|
+
|
|
11503
|
+
// flat's ixmaps.map() / ixmaps.api(szMap) — the running map's handle
|
|
11504
|
+
// (htmlgui.js 173ff, htmlgui_flat.js mapApi 1283). Its methods act on the
|
|
11505
|
+
// map named by szMap (its Map() options name) or, without a name, the
|
|
11506
|
+
// last built map; called while the map is still building, they run once
|
|
11507
|
+
// it is ready.
|
|
10865
11508
|
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'];
|
|
10867
|
-
const
|
|
10868
|
-
|
|
10869
|
-
|
|
10870
|
-
|
|
10871
|
-
|
|
10872
|
-
|
|
10873
|
-
|
|
11509
|
+
'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible', 'setThemeTimeFrame', 'attribution'];
|
|
11510
|
+
const _mapHandles = Object.create(null);
|
|
11511
|
+
function makeMapHandle(szMap) {
|
|
11512
|
+
const h = {};
|
|
11513
|
+
const api = () => (szMap && _mapApis[szMap]) || _lastMapApi;
|
|
11514
|
+
for (const m of MAP_HANDLE_METHODS) {
|
|
11515
|
+
h[m] = (...args) => {
|
|
11516
|
+
const a = api();
|
|
11517
|
+
if (a) { const r = a[m](...args); return r === a ? h : r; }
|
|
11518
|
+
_mapReady.then(ready => ready[m](...args));
|
|
11519
|
+
return h;
|
|
11520
|
+
};
|
|
11521
|
+
}
|
|
11522
|
+
h.getZoom = () => { const a = api(); return a ? a.getZoom() : undefined; };
|
|
11523
|
+
h.getThemeObj = id => getThemeObj(id);
|
|
11524
|
+
// flat's mapApi.theme(szTheme) (htmlgui_flat.js 1571): the theme handle
|
|
11525
|
+
h.theme = szTheme => new themeApi(szMap, szTheme);
|
|
11526
|
+
// flat's mapApi aliases (htmlgui_flat.js 1286+): show/hide a theme,
|
|
11527
|
+
// redrawTheme (refreshTheme here), project (loadProject), getThemes(),
|
|
11528
|
+
// setView (view, both flat's object form and (array, zoom))
|
|
11529
|
+
h.show = id => h.setThemeVisible(id, true);
|
|
11530
|
+
h.hide = id => h.setThemeVisible(id, false);
|
|
11531
|
+
h.redrawTheme = id => h.refreshTheme(id);
|
|
11532
|
+
h.project = (...a) => h.loadProject(...a);
|
|
11533
|
+
h.setView = (...a) => h.view(...a);
|
|
11534
|
+
h.getThemes = () => [...new Set(_globalThemeRegistry.values())].map(rt => getThemeObj(themeIdOf(rt))).filter(Boolean);
|
|
11535
|
+
return h;
|
|
11536
|
+
}
|
|
11537
|
+
const _mapHandle = makeMapHandle(null);
|
|
11538
|
+
function mapHandle(szMap) {
|
|
11539
|
+
if (!szMap) return _mapHandle;
|
|
11540
|
+
if (!_mapHandles[szMap]) _mapHandles[szMap] = makeMapHandle(szMap);
|
|
11541
|
+
return _mapHandles[szMap];
|
|
10874
11542
|
}
|
|
10875
|
-
_mapHandle.getZoom = () => (_lastMapApi ? _lastMapApi.getZoom() : undefined);
|
|
10876
|
-
_mapHandle.getThemeObj = id => getThemeObj(id);
|
|
10877
|
-
function mapHandle() { return _mapHandle; }
|
|
10878
11543
|
|
|
10879
11544
|
// flat's ixmaps.formatValue(value, precision, flag) (ui/js/tools/format.js),
|
|
10880
11545
|
// which user chart scripts call: thousands separated by "." (BLANK: a
|
|
@@ -11090,6 +11755,7 @@
|
|
|
11090
11755
|
const style = Object.assign({}, rt.style);
|
|
11091
11756
|
if (rt.binding.size && !style.sizefield) style.sizefield = rt.binding.size;
|
|
11092
11757
|
if (rt.binding.title && !style.titlefield) style.titlefield = rt.binding.title;
|
|
11758
|
+
if (rt.binding.time && !style.timefield) style.timefield = rt.binding.time;
|
|
11093
11759
|
style.type = rt.flags.typeString != null ? rt.flags.typeString : [...rt.flags].join('|');
|
|
11094
11760
|
const data = Object.assign({}, rt._definition && rt._definition.data);
|
|
11095
11761
|
if (!data.name) data.name = themeDataName(rt);
|
|
@@ -11448,10 +12114,44 @@
|
|
|
11448
12114
|
return _lastMapApi.loadProject(src, flags);
|
|
11449
12115
|
}
|
|
11450
12116
|
|
|
12117
|
+
// flat's global compat forms come in two arities: the canonical
|
|
12118
|
+
// theme-first signatures — changeThemeStyle(szThemeName, szStyle, szFlag)
|
|
12119
|
+
// (htmlgui.js 2050, with its duplicate-leading shift, 2052),
|
|
12120
|
+
// removeTheme(szThemeId) (1884), setBasemapOpacity(nOpacity, szMode)
|
|
12121
|
+
// (1409) — and the older map-first pages calling
|
|
12122
|
+
// (null|szMap, szId, ...) — PNRR passes an explicit null. Arity tells
|
|
12123
|
+
// them apart; theme ids are global (the registry), so the leading map
|
|
12124
|
+
// argument only matters for setBasemapOpacity, where a registered map
|
|
12125
|
+
// name targets that map and null or an unknown name the last built one.
|
|
12126
|
+
function themeStyleArgs(a) {
|
|
12127
|
+
if (a.length >= 4) a = a.slice(1);
|
|
12128
|
+
else if (a[0] === a[1]) a = a.slice(1);
|
|
12129
|
+
return a;
|
|
12130
|
+
}
|
|
12131
|
+
function compatChangeThemeStyle(...a) {
|
|
12132
|
+
const [szId, szStyle, szFlag] = themeStyleArgs(a);
|
|
12133
|
+
return _mapHandle.changeThemeStyle(szId, szStyle, szFlag);
|
|
12134
|
+
}
|
|
12135
|
+
function compatRemoveTheme(...a) {
|
|
12136
|
+
return _mapHandle.remove(a.length >= 2 ? a[1] : a[0]);
|
|
12137
|
+
}
|
|
12138
|
+
function compatSetBasemapOpacity(...a) {
|
|
12139
|
+
if (a.length >= 3) {
|
|
12140
|
+
const api = (a[0] && _mapApis[a[0]]) || _lastMapApi;
|
|
12141
|
+
if (api) api.setBasemapOpacity(a[1], a[2]);
|
|
12142
|
+
return;
|
|
12143
|
+
}
|
|
12144
|
+
return _mapHandle.setBasemapOpacity(a[0], a[1]);
|
|
12145
|
+
}
|
|
12146
|
+
|
|
11451
12147
|
global.ixmaps = {
|
|
11452
|
-
layer, Map: createMap, setExternalData: setExternalDataBridge, getThemeObj, data: ixmapsData,
|
|
12148
|
+
layer, Layer: layer, Map: createMap, setExternalData: setExternalDataBridge, getThemeObj, data: ixmapsData,
|
|
11453
12149
|
szResourceBase: ixmapsSzResourceBase, getProjectString, setProjectJSON, loadProject,
|
|
11454
12150
|
markThemeClass, unmarkThemeClass, getBoundingBox, setTitle, setTitleBox, refreshTheme, formatValue: flatFormatValue,
|
|
12151
|
+
// flat's ixmaps.__formatValue (ui/js/tools/legend.js 451-553) — pages call
|
|
12152
|
+
// it for statistics/legend values; legend's own variant differs from
|
|
12153
|
+
// format.js's only in the SPACE flag, which flatFormatValue covers too
|
|
12154
|
+
__formatValue: flatFormatValue,
|
|
11455
12155
|
map: mapHandle, getZoom, getThemeDefinitionObj, embeddedSVG,
|
|
11456
12156
|
// flat's theme list: the theme objects of every theme
|
|
11457
12157
|
getThemes: () => [...new Set(_globalThemeRegistry.values())].map(rt => getThemeObj(themeIdOf(rt))).filter(Boolean),
|
|
@@ -11468,9 +12168,11 @@
|
|
|
11468
12168
|
setAttribution: (text) => { if (_lastMapApi && _lastMapApi.setAttribution) _lastMapApi.setAttribution(text); },
|
|
11469
12169
|
htmlgui_setAttributionString: (text) => { if (_lastMapApi && _lastMapApi.setAttribution) _lastMapApi.setAttribution(text); },
|
|
11470
12170
|
htmlgui_getAttributionString: () => (_lastMapApi && _lastMapApi.getAttribution ? _lastMapApi.getAttribution() : ''),
|
|
11471
|
-
changeThemeStyle:
|
|
11472
|
-
removeTheme:
|
|
11473
|
-
|
|
12171
|
+
changeThemeStyle: compatChangeThemeStyle,
|
|
12172
|
+
removeTheme: compatRemoveTheme,
|
|
12173
|
+
// flat's time slider: show the records of a theme (null = all themes) whose timefield lies in [min, max)
|
|
12174
|
+
setThemeTimeFrame: (szId, nMin, nMax) => _mapHandle.setThemeTimeFrame(szId, nMin, nMax),
|
|
12175
|
+
setBasemapOpacity: compatSetBasemapOpacity,
|
|
11474
12176
|
// this engine's version (flat's own ixmaps.version numbers flat)
|
|
11475
12177
|
glVersion: IXMAPS_GL_VERSION,
|
|
11476
12178
|
// flat always has this page hook (its layer legend tool defines it,
|
|
@@ -11494,8 +12196,8 @@
|
|
|
11494
12196
|
// test-only: lets test/unit/*.test.mjs call pure internals directly (the
|
|
11495
12197
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
11496
12198
|
global.__ixmapsGlInternals = {
|
|
11497
|
-
IXMAPS_GL_VERSION, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout,
|
|
11498
|
-
normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, typeStyleNumbers, styleNum,
|
|
12199
|
+
IXMAPS_GL_VERSION, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout, multiQuadOffsets, pixelOffsetLngLat,
|
|
12200
|
+
normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
11499
12201
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
11500
12202
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|
|
11501
12203
|
computeAlphaStats, computeMultiFieldClasses, computeDominantStats, resolveDominantClass, computeComposeColorStats,
|
|
@@ -11504,7 +12206,7 @@
|
|
|
11504
12206
|
flatToMapLibreZoom, mapLibreToFlatZoom, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
|
|
11505
12207
|
fetchLayerData, parseCsvText, dataTableRows, geometryRowsToFeatureCollection, filterFlatValues, applyField100, field100Binding, rangeClassLegendTotals, resolveAggregateValue, classValueSeparate, cellAggregatedValues, oneHot, groupCoLocated, aggregateOnGrid, GridAggregateIndex, flatRangeParts, aggregateField100, valueRadius, itemPlotGeometry, itemAnchor, objectZoomFactor, resolveZoomReference, defaultNormalSizeValue,
|
|
11506
12208
|
applyWhereFilter, joinChartPositions, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
11507
|
-
flatFormatValue,
|
|
12209
|
+
flatFormatValue, themeStyleArgs,
|
|
11508
12210
|
dataCacheKey, dataCacheDisabled, cachedLayerData, featuresBounds, wantsZoomToExtent, legendIsOn,
|
|
11509
12211
|
};
|
|
11510
12212
|
global.__setFilter = __setFilter;
|