ixmaps-gl 0.2.10 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/ixmaps-gl.js +824 -164
- package/package.json +1 -1
package/ixmaps-gl.js
CHANGED
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
// alpha100}) when set (alpha100:"$density$" divides by the polygon's
|
|
22
22
|
// own geodesic area — see _prepareAlphaField/_resolveDopacityAlpha).
|
|
23
23
|
// AGGREGATE CATEGORICAL on one field paints each polygon in its records'
|
|
24
|
-
// dominant category; a plain CATEGORICAL choropleth
|
|
25
|
-
// see _buildChoroplethLayers), and the CHART|SYMBOL|
|
|
24
|
+
// dominant category; a plain CATEGORICAL choropleth matches each polygon's
|
|
25
|
+
// raw value against the categories, see _buildChoroplethLayers), and the CHART|SYMBOL|
|
|
26
26
|
// GLOW|CATEGORICAL|AGGREGATE|COUNT|RELOCATE|VALUES pipeline (categorical
|
|
27
27
|
// clustering + sizing + glow + multi-bubble grouping + on-bubble value
|
|
28
28
|
// labels, generalized to however many distinct category values the DATA
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
// ---------------------------------------------------------------
|
|
83
83
|
// this engine's release (= package.json "version", checked by
|
|
84
84
|
// test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
|
|
85
|
-
const IXMAPS_GL_VERSION = '0.2.
|
|
85
|
+
const IXMAPS_GL_VERSION = '0.2.14';
|
|
86
86
|
|
|
87
87
|
const LIB_URLS = {
|
|
88
88
|
// Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
|
|
@@ -308,7 +308,7 @@
|
|
|
308
308
|
// crafted link must not be able to load arbitrary code into the page.
|
|
309
309
|
// ---------------------------------------------------------------
|
|
310
310
|
// v0.1.6: knows this engine's aliases, lookupfield rule, meta↔style, builder methods, project loading
|
|
311
|
-
const VALIDATOR_URL_DEFAULT = 'https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-grammar@v0.1.
|
|
311
|
+
const VALIDATOR_URL_DEFAULT = 'https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-grammar@v0.1.27/dist/validate.mjs';
|
|
312
312
|
// a page may set ixmaps.validate BEFORE loading this script — the
|
|
313
313
|
// `global.ixmaps = {...}` export below would otherwise overwrite it
|
|
314
314
|
const _preloadValidate = global.ixmaps && global.ixmaps.validate;
|
|
@@ -2125,7 +2125,7 @@
|
|
|
2125
2125
|
// edit it by hand; `npm run unit` fails when it drifts.
|
|
2126
2126
|
// ---------------------------------------------------------------
|
|
2127
2127
|
// <grammar:binding-aliases>
|
|
2128
|
-
// generated from ixmaps-grammar 0.1.
|
|
2128
|
+
// generated from ixmaps-grammar 0.1.27 (ixmaps-flat 1.0.42, 2b978d5 2026-10-02) — 52 aliases
|
|
2129
2129
|
const FLAT_BINDING_ALIASES = {
|
|
2130
2130
|
"aggregation": "style.aggregationfield",
|
|
2131
2131
|
"aggregationfield": "style.aggregationfield",
|
|
@@ -2187,7 +2187,7 @@
|
|
|
2187
2187
|
// both — so .style({tooltip}) is a tooltip and .meta({fillopacity}) a style
|
|
2188
2188
|
// property. GENERATED by test/sync-grammar.mjs — do not edit.
|
|
2189
2189
|
// <grammar:meta-keys>
|
|
2190
|
-
// generated from ixmaps-grammar 0.1.
|
|
2190
|
+
// generated from ixmaps-grammar 0.1.27 — 5 meta keys
|
|
2191
2191
|
const FLAT_META_KEYS = ["description","name","snippet","title","tooltip"];
|
|
2192
2192
|
// </grammar:meta-keys>
|
|
2193
2193
|
|
|
@@ -2206,6 +2206,7 @@
|
|
|
2206
2206
|
'style.alphafield': ['binding', 'alpha'],
|
|
2207
2207
|
'style.alphafield100': ['binding', 'alpha100'],
|
|
2208
2208
|
'style.valuefield': ['style', 'valuefield'],
|
|
2209
|
+
'style.timefield': ['binding', 'time'], // setThemeTimeFrame, see LayerRuntime.setTimeFrame
|
|
2209
2210
|
'style.titlefield': ['binding', 'title'], // {{theme.item.title}}, see _buildTooltipContext
|
|
2210
2211
|
// the lookup key's normalization (flatLookupKey): flat's newTheme moves
|
|
2211
2212
|
// binding.tonumber / digits / … into these style keys (htmlgui.js 1712)
|
|
@@ -2371,7 +2372,8 @@
|
|
|
2371
2372
|
const STYLE_NUMBER_KEYS = ['linewidth', 'fillopacity', 'scale', 'classes', 'valuedecimals', 'normalsizevalue',
|
|
2372
2373
|
'sizepow', 'rangescale', 'minvalue', 'maxvalue', 'markersize', 'boxopacity', 'outlierscale', 'valuescale',
|
|
2373
2374
|
'brightness', 'fractionscale', 'dopacityscale', 'dopacitypow', 'gridwidthpx', 'textscale', 'rangecentervalue',
|
|
2374
|
-
'shadowblur', 'shadowdx', 'shadowdy', 'maxshadow', 'offsetx', 'offsety'
|
|
2375
|
+
'shadowblur', 'shadowdx', 'shadowdy', 'maxshadow', 'offsetx', 'offsety', 'gridx', 'boxmargin', 'borderwidth',
|
|
2376
|
+
'borderradius', 'maxcharts'];
|
|
2375
2377
|
function toNumberIfNumeric(v) {
|
|
2376
2378
|
if (typeof v !== 'string' || !v.trim()) return v;
|
|
2377
2379
|
const n = Number(v);
|
|
@@ -2635,6 +2637,69 @@
|
|
|
2635
2637
|
return { dark: false, bg: 'rgba(255,255,255,0.9)' };
|
|
2636
2638
|
}
|
|
2637
2639
|
|
|
2640
|
+
// ---------------------------------------------------------------
|
|
2641
|
+
// Effective basemap darkness — what the map canvas actually shows,
|
|
2642
|
+
// not what its name suggests. flatLegendLook's name-plus-opacity
|
|
2643
|
+
// heuristic assumes the faded (basemapopacity <= 0.5) basemap blends
|
|
2644
|
+
// against a WHITE page, so a dark basemap faded to half goes light;
|
|
2645
|
+
// on a page with a dark background it reads dark all the same, and
|
|
2646
|
+
// tooltips/legends styled light stick out (the exact bug fixed here).
|
|
2647
|
+
// Rule: an explicit color (legendBackground, a color mapType) stays
|
|
2648
|
+
// authoritative as before; with a real basemap style, a DARK style
|
|
2649
|
+
// background means dark — except when it is faded to
|
|
2650
|
+
// basemapopacity <= 0.5, where the mix is mostly the page background
|
|
2651
|
+
// behind the semi-transparent canvas and THAT decides (dark page ->
|
|
2652
|
+
// still dark; white page -> flat's washed-out light look). A light
|
|
2653
|
+
// style background reads light however it is faded (flat's own rule).
|
|
2654
|
+
// Falls back to the name heuristic while no style exists.
|
|
2655
|
+
// ---------------------------------------------------------------
|
|
2656
|
+
function styleBackgroundColorOf(map) {
|
|
2657
|
+
try {
|
|
2658
|
+
const style = map && map.getStyle && map.getStyle();
|
|
2659
|
+
if (!style || !style.layers) return null;
|
|
2660
|
+
for (const layer of style.layers) {
|
|
2661
|
+
if (layer.type !== 'background') continue;
|
|
2662
|
+
const paint = layer.paint || {};
|
|
2663
|
+
const color = paint['background-color'] != null ? paint['background-color'] : (paint.background != null ? paint.background : null);
|
|
2664
|
+
// the blank background substituted for a color mapType IS the page's
|
|
2665
|
+
// requested background and never faded (see applyBasemapOpacity)
|
|
2666
|
+
return { color: color, faded: layer.id !== BLANK_BACKGROUND_LAYER_ID };
|
|
2667
|
+
}
|
|
2668
|
+
} catch (e) { /* no style (yet) */ }
|
|
2669
|
+
return null;
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
function pageBackgroundColorOf(el) {
|
|
2673
|
+
let node = el;
|
|
2674
|
+
while (node && node.nodeType === 1 && node !== document.documentElement) {
|
|
2675
|
+
const m = String(getComputedStyle(node).backgroundColor)
|
|
2676
|
+
.match(/^rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)(?:[,/\s]+([\d.]+))?\s*\)/i);
|
|
2677
|
+
if (m && (m[4] === undefined || +m[4] > 0)) return [+m[1], +m[2], +m[3]];
|
|
2678
|
+
node = node.parentElement;
|
|
2679
|
+
}
|
|
2680
|
+
return [255, 255, 255]; // the page default
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
function effectiveMapDark(map, el, basemapOpacity, mapType, legendBackground) {
|
|
2684
|
+
const id = legendBackground || mapType;
|
|
2685
|
+
if (flatIsCssColorValue(id)) return flatIsDarkColor(id);
|
|
2686
|
+
const base = styleBackgroundColorOf(map);
|
|
2687
|
+
const rgb = base && base.color != null ? parseCssColor(String(base.color)) : null;
|
|
2688
|
+
if (!rgb) {
|
|
2689
|
+
// no style (yet): the name heuristic, exactly as before
|
|
2690
|
+
return flatLegendLook(mapType, basemapOpacity, legendBackground).dark;
|
|
2691
|
+
}
|
|
2692
|
+
const o = isNaN(basemapOpacity) ? 1 : Math.max(0, Math.min(1, basemapOpacity));
|
|
2693
|
+
if (flatIsDarkColor('rgb(' + rgb.map(Math.round).join(',') + ')')) {
|
|
2694
|
+
// a dark basemap faded to half or less shows mostly the page background
|
|
2695
|
+
// behind the canvas - so at that point the PAGE decides (dark page ->
|
|
2696
|
+
// still dark; white page -> the washed-out flat look stays light)
|
|
2697
|
+
return (base.faded && o <= 0.5) ? flatIsDarkColor('rgb(' + pageBackgroundColorOf(el).map(Math.round).join(',') + ')') : true;
|
|
2698
|
+
}
|
|
2699
|
+
// a light basemap reads light however it is faded - flat's own rule
|
|
2700
|
+
return false;
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2638
2703
|
function buildBlankBackgroundStyle(color) {
|
|
2639
2704
|
return {
|
|
2640
2705
|
version: 8,
|
|
@@ -2644,6 +2709,73 @@
|
|
|
2644
2709
|
}
|
|
2645
2710
|
|
|
2646
2711
|
// ---------------------------------------------------------------
|
|
2712
|
+
// flat's map attribution (ui/html/mappage.html #attribution-div): the
|
|
2713
|
+
// page's text bottom left on the map; hidden while empty ("null" counts
|
|
2714
|
+
// as empty, htmlgui_flat.js 522-526). Here a MapLibre control in the
|
|
2715
|
+
// bottom-left corner, styled like MapLibre's own attribution (the
|
|
2716
|
+
// basemap credit, bottom right — same line, same pill: white, 12px
|
|
2717
|
+
// radius, 12px/20px Helvetica Neue, links rgba(0,0,0,0.75)).
|
|
2718
|
+
// ---------------------------------------------------------------
|
|
2719
|
+
let _attributionCss = false;
|
|
2720
|
+
function createAttribution(map) {
|
|
2721
|
+
if (!_attributionCss && typeof document !== 'undefined') {
|
|
2722
|
+
_attributionCss = true;
|
|
2723
|
+
const css = document.createElement('style');
|
|
2724
|
+
css.textContent = '.ixmaps-gl-attribution a{color:rgba(0,0,0,0.75);text-decoration:none}'
|
|
2725
|
+
+ '.ixmaps-gl-attribution a:hover{color:inherit;text-decoration:underline}';
|
|
2726
|
+
document.head.appendChild(css);
|
|
2727
|
+
}
|
|
2728
|
+
const box = document.createElement('div');
|
|
2729
|
+
box.className = 'maplibregl-ctrl ixmaps-gl-attribution';
|
|
2730
|
+
box.style.cssText = 'background:#fff;border-radius:12px;padding:2px 8px;min-height:20px;box-sizing:content-box;'
|
|
2731
|
+
+ 'font:12px/20px "Helvetica Neue",Arial,Helvetica,sans-serif;color:#000;display:none;';
|
|
2732
|
+
map.addControl({ onAdd: () => box, onRemove: () => box.remove() }, 'bottom-left');
|
|
2733
|
+
let text = '';
|
|
2734
|
+
return {
|
|
2735
|
+
set(t) {
|
|
2736
|
+
text = t == null || t === 'null' ? '' : String(t);
|
|
2737
|
+
box.innerHTML = text;
|
|
2738
|
+
box.style.display = text ? 'block' : 'none';
|
|
2739
|
+
},
|
|
2740
|
+
get() { return text; }
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
// ---------------------------------------------------------------
|
|
2745
|
+
// Embedded YouTube players in tooltips (data fields carrying <iframe
|
|
2746
|
+
// src=".../embed/ID">) are replaced by a thumbnail with a play button; the
|
|
2747
|
+
// real iframe is created on click. Keeps tooltips light, and a removed
|
|
2748
|
+
// video or a refused embed (e.g. error 153 on file:// pages) degrades to a
|
|
2749
|
+
// thumbnail that opens YouTube in a new tab instead of a broken player.
|
|
2750
|
+
const YT_IFRAME_RE = /<iframe\b[^>]*?\bsrc\s*=\s*["']?(?:https?:)?\/\/(?:www\.)?youtube(?:-nocookie)?\.com\/embed\/([\w-]{6,})[^"'\s>]*["']?[^>]*>\s*<\/iframe>/gi;
|
|
2751
|
+
function youtubeClickToPlay(html) {
|
|
2752
|
+
if (!html || html.indexOf('youtube') < 0) return html;
|
|
2753
|
+
return html.replace(YT_IFRAME_RE, (m, id) => {
|
|
2754
|
+
const w = (/\bwidth\s*=\s*["']?(\d+)/i.exec(m) || [])[1] || 240;
|
|
2755
|
+
const h = (/\bheight\s*=\s*["']?(\d+)/i.exec(m) || [])[1] || 180;
|
|
2756
|
+
return '<div class="ixgl-yt" data-yt="' + id + '" data-w="' + w + '" data-h="' + h + '" ' +
|
|
2757
|
+
'style="position:relative;width:' + w + 'px;height:' + h + 'px;cursor:pointer;background:#000 url(https://i.ytimg.com/vi/' + id + '/hqdefault.jpg) center/cover" ' +
|
|
2758
|
+
'title="play"><span style="position:absolute;left:50%;top:50%;width:48px;height:34px;margin:-17px 0 0 -24px;' +
|
|
2759
|
+
'border-radius:8px;background:rgba(220,0,0,0.9)"></span><span style="position:absolute;left:50%;top:50%;margin:-9px 0 0 -6px;' +
|
|
2760
|
+
'border-style:solid;border-width:9px 0 9px 15px;border-color:transparent transparent transparent #fff"></span></div>';
|
|
2761
|
+
});
|
|
2762
|
+
}
|
|
2763
|
+
if (typeof document !== 'undefined' && typeof document.addEventListener === 'function' && !global.__ixglYtClick) {
|
|
2764
|
+
global.__ixglYtClick = true;
|
|
2765
|
+
document.addEventListener('click', e => {
|
|
2766
|
+
const el = e.target.closest && e.target.closest('.ixgl-yt');
|
|
2767
|
+
if (!el) return;
|
|
2768
|
+
e.stopPropagation();
|
|
2769
|
+
const f = document.createElement('iframe');
|
|
2770
|
+
f.width = el.dataset.w; f.height = el.dataset.h; f.frameBorder = '0';
|
|
2771
|
+
f.allow = 'autoplay; encrypted-media; picture-in-picture; fullscreen';
|
|
2772
|
+
f.allowFullscreen = true;
|
|
2773
|
+
f.referrerPolicy = 'strict-origin-when-cross-origin';
|
|
2774
|
+
f.src = 'https://www.youtube.com/embed/' + el.dataset.yt + '?autoplay=1&rel=0';
|
|
2775
|
+
el.replaceWith(f);
|
|
2776
|
+
}, true);
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2647
2779
|
// Hover tooltips and the click-to-pin tooltip of a map, built once per map
|
|
2648
2780
|
// by MapBuilder.build(). ctx: the builder (its map options, for the
|
|
2649
2781
|
// tooltip look), the MapLibre map, its container element and
|
|
@@ -2653,6 +2785,12 @@
|
|
|
2653
2785
|
// ---------------------------------------------------------------
|
|
2654
2786
|
function createTooltips(ctx) {
|
|
2655
2787
|
const { builder, map, el, findRuntimeForLayerId } = ctx;
|
|
2788
|
+
// the effective (rendered) dark/light probe for tooltip and legend —
|
|
2789
|
+
// the builder carries it so the legend look (built later, without the
|
|
2790
|
+
// map element at hand) can ask the same question
|
|
2791
|
+
builder.__effectiveDark = function (basemapOpacity, mapType, legendBackground) {
|
|
2792
|
+
return effectiveMapDark(map, el, basemapOpacity, mapType, legendBackground);
|
|
2793
|
+
};
|
|
2656
2794
|
// Click-to-pin tooltip: matches the real ixmaps engine's own
|
|
2657
2795
|
// click-pins-the-tooltip convention in pan mode (a hover tooltip is
|
|
2658
2796
|
// transient; a click keeps one visible until dismissed). This is a
|
|
@@ -2662,28 +2800,101 @@
|
|
|
2662
2800
|
// at click time (not live-updating), but its screen POSITION is kept
|
|
2663
2801
|
// in sync with the map on every pan/zoom via map.project().
|
|
2664
2802
|
let pinned = null; // { runtime, object, lngLat }
|
|
2665
|
-
// Tooltip look, like the legend's
|
|
2666
|
-
//
|
|
2667
|
-
//
|
|
2668
|
-
//
|
|
2803
|
+
// Tooltip look, like the legend's: flat's light tooltip
|
|
2804
|
+
// (tooltip_mustache.js: white 0.95, #444 text, thin black border, 5px
|
|
2805
|
+
// radius) on light basemaps, this engine's dark tooltip on dark ones —
|
|
2806
|
+
// decided by the EFFECTIVE map darkness (style background vs page
|
|
2807
|
+
// background, see effectiveMapDark), not the basemap name alone. Read
|
|
2808
|
+
// when shown, so options set later (a
|
|
2669
2809
|
// myMap.then(...).options({basemapopacity})) count.
|
|
2670
|
-
const
|
|
2810
|
+
const tooltipIsDark = () => {
|
|
2671
2811
|
const o = parseFloat(builder._engineOptions.basemapopacity);
|
|
2672
|
-
const
|
|
2673
|
-
return
|
|
2674
|
-
?
|
|
2675
|
-
:
|
|
2676
|
-
|
|
2812
|
+
const legendBackground = builder.mapOptions.legendBackground || builder.mapOptions.legendbackground;
|
|
2813
|
+
return typeof builder.__effectiveDark === 'function'
|
|
2814
|
+
? builder.__effectiveDark(isNaN(o) ? 1 : o, builder.mapOptions.mapType, legendBackground)
|
|
2815
|
+
: flatLegendLook(builder.mapOptions.mapType, isNaN(o) ? 1 : o, legendBackground).dark;
|
|
2816
|
+
};
|
|
2817
|
+
// flat's tooltip look (tooltip_mustache.js 118/151-154): arial narrow,
|
|
2818
|
+
// the font size clamped to 11..14px; light: white 0.95, #444 text, thin
|
|
2819
|
+
// black border; dark: rgba(50,50,50,0.95), #555 border (text light for
|
|
2820
|
+
// readability — flat leaves its #444 on dark and only adapts the panel).
|
|
2821
|
+
// line-height is declared so hover (deck.gl's .deck-tooltip stylesheet
|
|
2822
|
+
// gives it 20px) and the pinned tooltip (a bare div that would inherit
|
|
2823
|
+
// the page's 'normal') render the same content identically
|
|
2824
|
+
const tooltipLook = () => {
|
|
2825
|
+
const fontsize = Math.min(14, Math.max(11, (22 / 1200 * (global.innerWidth || 1200))));
|
|
2826
|
+
const fonts = { fontFamily: 'arial narrow, system', fontSize: fontsize + 'px', lineHeight: '20px' };
|
|
2827
|
+
return tooltipIsDark()
|
|
2828
|
+
? Object.assign(fonts, { background: 'rgba(50,50,50,0.95)', color: 'rgb(200,205,214)', border: '0.5px solid #555', borderRadius: '5px',
|
|
2829
|
+
boxShadow: '0 2px 8px rgba(0,0,0,0.3)' })
|
|
2830
|
+
: Object.assign(fonts, { background: 'rgba(255,255,255,0.95)', color: '#444', border: '0.5px solid black', borderRadius: '5px',
|
|
2831
|
+
boxShadow: 'rgba(0,0,0,0.2) 0px 2px 4px 0px, rgba(0,0,0,0.19) 0px 3px 10px 0px' });
|
|
2677
2832
|
};
|
|
2833
|
+
// flat's content pane (tooltip_mustache.js 124): the whole tooltip
|
|
2834
|
+
// content scrolls inside its own padded pane — a close button outside
|
|
2835
|
+
// the pane stays visible while it scrolls, and the scrollbar clears
|
|
2836
|
+
// the content by a small right margin
|
|
2837
|
+
const TOOLTIP_PANE_STYLE = 'margin:0.5em;max-height:360px;overflow:auto';
|
|
2838
|
+
const tooltipPaneHtml = (html) => `<div style="${TOOLTIP_PANE_STYLE}">${html}</div>`;
|
|
2678
2839
|
const pinnedTooltipEl = document.createElement('div');
|
|
2679
2840
|
pinnedTooltipEl.style.cssText = 'position:absolute;top:0;left:0;z-index:6;display:none;' +
|
|
2680
|
-
'pointer-events:auto;max-width:
|
|
2681
|
-
|
|
2841
|
+
'pointer-events:auto;max-width:440px;padding:0.5em 0.7em 0.5em 0.5em;';
|
|
2842
|
+
|
|
2843
|
+
// flat's own tooltip placement (tooltip_mustache.js 145-148): flip it
|
|
2844
|
+
// to the left/above the anchor when there is no room right/below, then
|
|
2845
|
+
// clamp it into the window — a tooltip must always be completely
|
|
2846
|
+
// visible (an element larger than the viewport ends up at its edge)
|
|
2847
|
+
function keepTooltipVisible(tipEl, x, y) {
|
|
2848
|
+
const w = tipEl.offsetWidth, h = tipEl.offsetHeight;
|
|
2849
|
+
const vw = global.innerWidth || 1200, vh = global.innerHeight || 800;
|
|
2850
|
+
let tx = x > vw / 2 ? x - w - 30 : x + 30;
|
|
2851
|
+
let ty = y > vh / 2 ? y - h - 20 : y + 20;
|
|
2852
|
+
tx = Math.min(Math.max(8, tx), Math.max(8, vw - w - 8));
|
|
2853
|
+
ty = Math.min(Math.max(8, ty), Math.max(8, vh - h - 8));
|
|
2854
|
+
const transform = `translate(${Math.round(tx)}px, ${Math.round(ty)}px)`;
|
|
2855
|
+
tipEl.style.transform = transform;
|
|
2856
|
+
return transform;
|
|
2857
|
+
}
|
|
2682
2858
|
|
|
2683
2859
|
function updatePinnedTooltipPosition() {
|
|
2684
2860
|
if (!pinned) return;
|
|
2685
2861
|
const pt = map.project(pinned.lngLat);
|
|
2686
|
-
pinnedTooltipEl
|
|
2862
|
+
keepTooltipVisible(pinnedTooltipEl, pt.x, pt.y);
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
// hover tooltip: deck.gl pins it at the pointer without any viewport
|
|
2866
|
+
// logic (it overflows the right/bottom edge there) — watch the element's
|
|
2867
|
+
// style mutations and re-place it inside the window every time deck
|
|
2868
|
+
// moves it. Our own write is recognized and skipped (lastSetTransform).
|
|
2869
|
+
let __hoverTooltipWatch = null;
|
|
2870
|
+
function watchHoverTooltip() {
|
|
2871
|
+
if (__hoverTooltipWatch || !el.parentElement) return;
|
|
2872
|
+
const host = el.parentElement;
|
|
2873
|
+
const attach = (tipEl) => {
|
|
2874
|
+
let lastSetTransform = null;
|
|
2875
|
+
const mo = new MutationObserver(() => {
|
|
2876
|
+
if (tipEl.style.display === 'none' || !tipEl.textContent.trim()) return;
|
|
2877
|
+
const t = tipEl.style.transform;
|
|
2878
|
+
if (!t || t === lastSetTransform) return;
|
|
2879
|
+
const m = /translate\((-?[\d.]+)px,\s*(-?[\d.]+)px\)/.exec(t);
|
|
2880
|
+
if (!m) return;
|
|
2881
|
+
lastSetTransform = keepTooltipVisible(tipEl, +m[1], +m[2]);
|
|
2882
|
+
});
|
|
2883
|
+
mo.observe(tipEl, { attributes: true, attributeFilter: ['style'] });
|
|
2884
|
+
__hoverTooltipWatch = mo;
|
|
2885
|
+
};
|
|
2886
|
+
const existing = host.querySelector('.deck-tooltip');
|
|
2887
|
+
if (existing) { attach(existing); return; }
|
|
2888
|
+
const creation = new MutationObserver((muts, obs) => {
|
|
2889
|
+
for (const mu of muts) for (const n of mu.addedNodes) {
|
|
2890
|
+
if (n.nodeType === 1 && n.classList && n.classList.contains('deck-tooltip')) {
|
|
2891
|
+
attach(n);
|
|
2892
|
+
obs.disconnect();
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
});
|
|
2897
|
+
creation.observe(host, { childList: true, subtree: true });
|
|
2687
2898
|
}
|
|
2688
2899
|
|
|
2689
2900
|
// A stable per-geometry anchor coordinate for the identity check
|
|
@@ -2717,13 +2928,19 @@
|
|
|
2717
2928
|
}
|
|
2718
2929
|
|
|
2719
2930
|
// content lives in its own child so re-rendering (innerHTML) never
|
|
2720
|
-
//
|
|
2931
|
+
// content lives in its own child so re-rendering (innerHTML) never
|
|
2932
|
+
// wipes out the unpin button below (a sibling, not a descendant of it).
|
|
2933
|
+
// The content child sits inside a scroll PANE (flat's own tooltip
|
|
2934
|
+
// structure): the pane scrolls, the close button outside it does not
|
|
2935
|
+
const pinnedPaneEl = document.createElement('div');
|
|
2936
|
+
pinnedPaneEl.style.cssText = TOOLTIP_PANE_STYLE;
|
|
2721
2937
|
const pinnedContentEl = document.createElement('div');
|
|
2722
|
-
|
|
2938
|
+
pinnedPaneEl.appendChild(pinnedContentEl);
|
|
2939
|
+
pinnedTooltipEl.appendChild(pinnedPaneEl);
|
|
2723
2940
|
|
|
2724
2941
|
function renderPinnedTooltip() {
|
|
2725
2942
|
if (!pinned) { pinnedTooltipEl.style.display = 'none'; return; }
|
|
2726
|
-
const html = pinned.runtime.buildTooltipHtml(pinned.object);
|
|
2943
|
+
const html = pinned.runtime.buildTooltipHtml(pinned.object, tooltipIsDark());
|
|
2727
2944
|
if (!html) { pinned = null; pinnedTooltipEl.style.display = 'none'; return; }
|
|
2728
2945
|
pinnedContentEl.innerHTML = html;
|
|
2729
2946
|
Object.assign(pinnedTooltipEl.style, tooltipLook());
|
|
@@ -2764,7 +2981,15 @@
|
|
|
2764
2981
|
// a USER chart's picked item is {d, icon} (see _buildUserChartLayers):
|
|
2765
2982
|
// the feature is its d
|
|
2766
2983
|
function pickedFeature(o) { return o && o.icon && o.d ? o.d : o; }
|
|
2984
|
+
// flat's map tool 'pan' (Map option mode:"pan", Api.setMapTool): pure
|
|
2985
|
+
// navigation — no hover tooltip there (flat's own hover-tooltip paths
|
|
2986
|
+
// run only for the ""/info/clickinfo tool types, mapscript2.js 5232).
|
|
2987
|
+
// The click-to-pin tooltip stays: it is this engine's equivalent of
|
|
2988
|
+
// flat's info popup, so a click still shows the permanent tooltip.
|
|
2989
|
+
const isPanMode = () => /^\s*pan\s*$/i.test(String(builder.mapOptions.mode || ''));
|
|
2990
|
+
|
|
2767
2991
|
function tooltipFor({ object: picked, layer }) {
|
|
2992
|
+
if (isPanMode()) return null;
|
|
2768
2993
|
const object = pickedFeature(picked);
|
|
2769
2994
|
if (!object || !layer) return null;
|
|
2770
2995
|
// suppress the hover tooltip ONLY for the specific item that's
|
|
@@ -2774,8 +2999,10 @@
|
|
|
2774
2999
|
if (pinned && isSameAsPinned(layer.id, object)) return null;
|
|
2775
3000
|
const rt = findRuntimeForLayerId(layer.id);
|
|
2776
3001
|
if (!rt) return null;
|
|
2777
|
-
const html = rt.buildTooltipHtml(object);
|
|
2778
|
-
|
|
3002
|
+
const html = rt.buildTooltipHtml(object, tooltipIsDark());
|
|
3003
|
+
// same layout as the pinned tooltip: the content in flat's scroll
|
|
3004
|
+
// pane, the shared look (fonts included) on the container
|
|
3005
|
+
return html ? { html: tooltipPaneHtml(html), style: Object.assign({ padding: '0.5em 0.7em', maxWidth: '440px' }, tooltipLook()) } : null;
|
|
2779
3006
|
}
|
|
2780
3007
|
function clickFor(picked) {
|
|
2781
3008
|
const info = picked && Object.assign({}, picked, { object: pickedFeature(picked.object) });
|
|
@@ -2835,6 +3062,7 @@
|
|
|
2835
3062
|
}
|
|
2836
3063
|
function mount() {
|
|
2837
3064
|
map.on('move', updatePinnedTooltipPosition);
|
|
3065
|
+
watchHoverTooltip();
|
|
2838
3066
|
if (el.parentElement) {
|
|
2839
3067
|
el.parentElement.style.position = el.parentElement.style.position || 'relative';
|
|
2840
3068
|
el.parentElement.appendChild(pinnedTooltipEl);
|
|
@@ -3040,7 +3268,10 @@
|
|
|
3040
3268
|
});
|
|
3041
3269
|
legendStack.style.display = shown ? 'flex' : 'none';
|
|
3042
3270
|
};
|
|
3043
|
-
|
|
3271
|
+
// a plain FEATURE(S) base layer has one flat fill (no classes); FEATURE|CATEGORICAL
|
|
3272
|
+
// is coloured per category (see _buildFeaturesLayers), so it gets a legend
|
|
3273
|
+
const legendApplies = rt => rt.categoryLabels && rt.categoryLabels.length
|
|
3274
|
+
&& (!(rt.flags.has('FEATURE') || rt.flags.has('FEATURES')) || rt.flags.has('CATEGORICAL')) && !rt.flags.has('NOLEGEND');
|
|
3044
3275
|
// One theme's panel — at build time for every theme, and again for a
|
|
3045
3276
|
// theme defined later (map.layer(...) in a myMap.then(...) chain,
|
|
3046
3277
|
// defineLayer, loadProject); removeTheme removes it with the theme.
|
|
@@ -3076,8 +3307,16 @@
|
|
|
3076
3307
|
// flat's look (flatLegendLook) unless the page picks one with
|
|
3077
3308
|
// this engine's own legendtheme
|
|
3078
3309
|
const opacityOpt = parseFloat(builder._engineOptions.basemapopacity);
|
|
3079
|
-
const
|
|
3080
|
-
const
|
|
3310
|
+
const legendBackground = builder.mapOptions.legendBackground || builder.mapOptions.legendbackground;
|
|
3311
|
+
const flatLook = flatLegendLook(builder.mapOptions.mapType, isNaN(opacityOpt) ? 1 : opacityOpt, legendBackground);
|
|
3312
|
+
// effective darkness (style background vs page background, see
|
|
3313
|
+
// effectiveMapDark): a dark basemap faded to <= 0.5 over a DARK
|
|
3314
|
+
// page still reads dark — flatLegendLook's name heuristic would
|
|
3315
|
+
// wrongly switch the panel (and the tooltip) to light there
|
|
3316
|
+
const looksDark = typeof builder.__effectiveDark === 'function'
|
|
3317
|
+
? builder.__effectiveDark(isNaN(opacityOpt) ? 1 : opacityOpt, builder.mapOptions.mapType, legendBackground)
|
|
3318
|
+
: flatLook.dark;
|
|
3319
|
+
const isLightLegend = rt.style.legendtheme === 'light' || (rt.style.legendtheme !== 'dark' && !looksDark);
|
|
3081
3320
|
const legendColors = isLightLegend
|
|
3082
3321
|
? { bg: 'rgba(255,255,255,0.92)', fg: '#1a1a1a', shadow: '0 2px 10px rgba(0,0,0,0.18)',
|
|
3083
3322
|
rowMarked: 'rgba(0,0,0,0.08)', track: 'rgba(0,0,0,0.08)',
|
|
@@ -3085,7 +3324,10 @@
|
|
|
3085
3324
|
: { bg: 'rgba(28,30,34,0.92)', fg: '#eee', shadow: '0 2px 10px rgba(0,0,0,0.45)',
|
|
3086
3325
|
rowMarked: 'rgba(255,255,255,0.14)', track: 'rgba(255,255,255,0.08)',
|
|
3087
3326
|
selectBg: 'rgba(255,255,255,0.08)', selectBorder: 'rgba(255,255,255,0.25)' };
|
|
3088
|
-
|
|
3327
|
+
// the panel bg follows the same effective decision (flatLegendLook
|
|
3328
|
+
// would hand the dark look a light bg here: its dark branch never
|
|
3329
|
+
// ran for this map, its light bg would clash with dark rows/text)
|
|
3330
|
+
if (!rt.style.legendtheme) legendColors.bg = looksDark ? '#111' : flatLook.bg;
|
|
3089
3331
|
// max-height:66% resolves against el.parentElement's own
|
|
3090
3332
|
// height (the map container, which always has a definite
|
|
3091
3333
|
// height for the map itself to render into) since this panel
|
|
@@ -3175,7 +3417,11 @@
|
|
|
3175
3417
|
(rt._activeFeatures || rt.features).forEach(f => {
|
|
3176
3418
|
const idx = rt.categoryIndexByLabel ? rt.categoryIndexByLabel.get(f.properties[rt.binding.value]) : null;
|
|
3177
3419
|
if (idx == null) return;
|
|
3178
|
-
|
|
3420
|
+
// a polygon (FEATURE|CATEGORICAL) is counted at its first vertex
|
|
3421
|
+
let pt = f.geometry && f.geometry.coordinates;
|
|
3422
|
+
while (Array.isArray(pt) && Array.isArray(pt[0])) pt = pt[0];
|
|
3423
|
+
if (!Array.isArray(pt)) return;
|
|
3424
|
+
const [lng, lat] = pt;
|
|
3179
3425
|
if (lng < bbox[0] || lng > bbox[2] || lat < bbox[1] || lat > bbox[3]) return;
|
|
3180
3426
|
if (globeCenter && !isOnVisibleHemisphere(lng, lat, globeCenter)) return;
|
|
3181
3427
|
totals[idx] += useSum ? (parseFloat(f.properties[valueField]) || 0) : 1;
|
|
@@ -3587,7 +3833,13 @@
|
|
|
3587
3833
|
// needs (a chart library, a user chart) — page code, like a <script>
|
|
3588
3834
|
// tag; loaded in order before the layers (see loadRequiredScripts)
|
|
3589
3835
|
require(url) { this._required = (this._required || []).concat(url); return this; }
|
|
3590
|
-
attribution(
|
|
3836
|
+
// flat's .attribution(text): the page's attribution, bottom left on the
|
|
3837
|
+
// map (createAttribution) — also after the map is built
|
|
3838
|
+
attribution(a) {
|
|
3839
|
+
this._attributionText = a;
|
|
3840
|
+
if (this._setAttribution) this._setAttribution(a);
|
|
3841
|
+
return this;
|
|
3842
|
+
}
|
|
3591
3843
|
legend(html) { this._legendHtml = html; return this; }
|
|
3592
3844
|
// GL-PORT COMPAT: real ixmaps-flat's map.layer(name) returns a NEW
|
|
3593
3845
|
// per-layer builder for a .data()...define() chain on the LAYER
|
|
@@ -3606,6 +3858,8 @@
|
|
|
3606
3858
|
this._layerBuilders.push(nameOrBuilder);
|
|
3607
3859
|
return this;
|
|
3608
3860
|
}
|
|
3861
|
+
// .Layer() — alias of .layer()
|
|
3862
|
+
Layer(nameOrBuilder) { return this.layer(nameOrBuilder); }
|
|
3609
3863
|
// GL-PORT COMPAT: real ixmaps-flat's map.on("layerdraw", cb) — only
|
|
3610
3864
|
// "layerdraw" is actually wired up (see build()'s own note on how
|
|
3611
3865
|
// it's translated into onRedraw + a synthetic per-runtime event); any
|
|
@@ -3781,13 +4035,10 @@
|
|
|
3781
4035
|
: resolveBasemapStyleUrl(this.mapOptions.mapType),
|
|
3782
4036
|
center: [lon, lat],
|
|
3783
4037
|
zoom: this._viewZoom != null && this._viewZoom !== '' ? flatToMapLibreZoom(Number(this._viewZoom)) : 8,
|
|
3784
|
-
//
|
|
3785
|
-
//
|
|
3786
|
-
//
|
|
3787
|
-
//
|
|
3788
|
-
// accepts extra text via customAttribution, appended alongside the
|
|
3789
|
-
// basemap's own required CARTO/OpenStreetMap credit rather than
|
|
3790
|
-
// replacing it.
|
|
4038
|
+
// MapLibre's own AttributionControl (bottom right) keeps the
|
|
4039
|
+
// basemap's required CARTO/OpenStreetMap credit; the page's own
|
|
4040
|
+
// .attribution() goes bottom left, as flat shows it
|
|
4041
|
+
// (createAttribution).
|
|
3791
4042
|
//
|
|
3792
4043
|
// A color `mapType` (mapTypeColor truthy — see
|
|
3793
4044
|
// buildBlankBackgroundStyle above) has no real basemap at all, so
|
|
@@ -3798,7 +4049,6 @@
|
|
|
3798
4049
|
// customAttribution's own required-credit behavior for a REAL
|
|
3799
4050
|
// basemap is left untouched.
|
|
3800
4051
|
...(mapTypeColor ? { attributionControl: false } : {}),
|
|
3801
|
-
...(this._attributionText ? { customAttribution: this._attributionText } : {}),
|
|
3802
4052
|
// MapLibre's default (true) is kept: with false it also stops the
|
|
3803
4053
|
// zoom-out where the world gets narrower than the window, so a
|
|
3804
4054
|
// world map can't open fully zoomed out. The deck.gl overlay takes
|
|
@@ -3808,6 +4058,16 @@
|
|
|
3808
4058
|
// world once, at the cost of that zoom limit.
|
|
3809
4059
|
renderWorldCopies: this._engineOptions.worldcopies !== false
|
|
3810
4060
|
});
|
|
4061
|
+
// ixmaps.Map(id, {mapProjection}): flat's orthographic map is MapLibre's
|
|
4062
|
+
// native globe here (same mapping as loadProject, applyProjectMap);
|
|
4063
|
+
// "ortographic" is the spelling flat pages commonly carry. Any other
|
|
4064
|
+
// value stays mercator.
|
|
4065
|
+
if (/^(orth?ographic|globe)$/i.test(String(this.mapOptions.mapProjection || '').trim())) {
|
|
4066
|
+
map.once('style.load', () => {
|
|
4067
|
+
try { map.setProjection({ type: 'globe' }); }
|
|
4068
|
+
catch (e) { console.warn('[ixmaps-gl] mapProjection "globe": map.setProjection failed (needs maplibre-gl >=5.0.1)', e); }
|
|
4069
|
+
});
|
|
4070
|
+
}
|
|
3811
4071
|
_boundsSource = () => {
|
|
3812
4072
|
const b = map.getBounds();
|
|
3813
4073
|
return [{ lat: b.getSouth(), lng: b.getWest() }, { lat: b.getNorth(), lng: b.getEast() }];
|
|
@@ -3902,9 +4162,11 @@
|
|
|
3902
4162
|
const overlay = new MapLibreOverlay({
|
|
3903
4163
|
interleaved: true,
|
|
3904
4164
|
layers: [],
|
|
3905
|
-
// pointer becomes a hand over anything pickable (bubbles/points),
|
|
3906
|
-
//
|
|
3907
|
-
|
|
4165
|
+
// pointer becomes a hand over anything pickable (bubbles/points), so
|
|
4166
|
+
// hovering something clickable actually looks clickable — except in
|
|
4167
|
+
// the 'pan' input mode, where nothing responds to hovering
|
|
4168
|
+
getCursor: ({ isDragging, isHovering }) =>
|
|
4169
|
+
(isDragging ? 'grabbing' : ((isHovering && !/^\s*pan\s*$/i.test(String(this.mapOptions.mode || ''))) ? 'pointer' : 'grab')),
|
|
3908
4170
|
// (guarded: see createTooltips)
|
|
3909
4171
|
getTooltip: tooltips.getTooltip,
|
|
3910
4172
|
onClick: tooltips.onClick
|
|
@@ -3912,6 +4174,10 @@
|
|
|
3912
4174
|
map.addControl(overlay);
|
|
3913
4175
|
map.addControl(new maplibregl.NavigationControl(), 'top-left');
|
|
3914
4176
|
tooltips.mount();
|
|
4177
|
+
// the page's attribution (.attribution(), Map option attribution)
|
|
4178
|
+
const attributionBox = createAttribution(map);
|
|
4179
|
+
attributionBox.set(this._attributionText != null ? this._attributionText : this.mapOptions.attribution);
|
|
4180
|
+
this._setAttribution = text => attributionBox.set(text);
|
|
3915
4181
|
|
|
3916
4182
|
if (this._legendHtml && el.parentElement) {
|
|
3917
4183
|
const legendEl = document.createElement('div');
|
|
@@ -4083,6 +4349,11 @@
|
|
|
4083
4349
|
mapType: (id) => engineApi.setMapType(id),
|
|
4084
4350
|
// flat's getMapTypeId(): the basemap type name in use
|
|
4085
4351
|
getMapTypeId: () => String(builder.mapOptions.mapType || ''),
|
|
4352
|
+
// flat's attribution (htmlgui.js htmlgui_set/getAttributionString)
|
|
4353
|
+
setAttribution: (text) => { builder.attribution(text); return engineApi; },
|
|
4354
|
+
// flat's map handle ixmaps.map().attribution(text) (htmlgui_flat.js 1381)
|
|
4355
|
+
attribution: (text) => engineApi.setAttribution(text),
|
|
4356
|
+
getAttribution: () => attributionBox.get(),
|
|
4086
4357
|
setBasemapOpacity: (delta, mode) => {
|
|
4087
4358
|
_basemapOpacity = mode === 'relative'
|
|
4088
4359
|
? Math.max(0, Math.min(1, _basemapOpacity + (parseFloat(delta) || 0)))
|
|
@@ -4172,6 +4443,18 @@
|
|
|
4172
4443
|
refresh();
|
|
4173
4444
|
}
|
|
4174
4445
|
},
|
|
4446
|
+
// flat's ixmaps.setThemeTimeFrame(szId, min, max): show only the
|
|
4447
|
+
// records whose timefield lies in [min, max) (ms or Date-
|
|
4448
|
+
// parsable). null min+max clears. szId null/undefined = every theme
|
|
4449
|
+
// that has a timefield (flat: one call updates all themes at once).
|
|
4450
|
+
setThemeTimeFrame: (themeId, min, max) => {
|
|
4451
|
+
const targets = themeId == null ? runtimes.filter(r => r.binding && r.binding.time) : (() => {
|
|
4452
|
+
const rt = findRuntime(themeId);
|
|
4453
|
+
return rt ? siblingRuntimes(rt).filter(r => r.binding && r.binding.time) : [];
|
|
4454
|
+
})();
|
|
4455
|
+
targets.forEach(rt => rt.setTimeFrame(min, max));
|
|
4456
|
+
if (targets.length) refresh();
|
|
4457
|
+
},
|
|
4175
4458
|
// shows/hides a whole theme (all the deck.gl layers its
|
|
4176
4459
|
// buildDeckLayers would otherwise produce) without touching its
|
|
4177
4460
|
// data or style — for toggle controls like the real page's
|
|
@@ -4257,6 +4540,8 @@
|
|
|
4257
4540
|
};
|
|
4258
4541
|
return lb;
|
|
4259
4542
|
},
|
|
4543
|
+
// .Layer() — alias of .layer()
|
|
4544
|
+
Layer: (nameOrBuilder) => engineApi.layer(nameOrBuilder),
|
|
4260
4545
|
// Real ixmaps-flat's loadProject (htmlgui.js loadProject →
|
|
4261
4546
|
// setProjectJSON → continueSetProjectJSON): src is a project object,
|
|
4262
4547
|
// a JSON string, or a URL (fetched as DATA — nothing a project names
|
|
@@ -4589,7 +4874,15 @@
|
|
|
4589
4874
|
const svg = String(m.map || '');
|
|
4590
4875
|
if (svg && !/generic\/(mercator|orthographic)\.svg$/i.test(svg)) report.notes.push(`flat SVG map "${svg.split('/').pop()}" is not available in ixmaps-gl — mercator used`);
|
|
4591
4876
|
if (typeof map.setProjection === 'function') {
|
|
4592
|
-
try {
|
|
4877
|
+
try {
|
|
4878
|
+
map.setProjection({ type: /orthographic\.svg$/i.test(svg) ? 'globe' : 'mercator' });
|
|
4879
|
+
// the layers are projection-dependent (globe: far-hemisphere cull,
|
|
4880
|
+
// chart placement) and are otherwise rebuilt only by a pan: rebuild
|
|
4881
|
+
// now, and again once the new projection has rendered
|
|
4882
|
+
refreshLayers();
|
|
4883
|
+
map.triggerRepaint();
|
|
4884
|
+
map.once('idle', refreshLayers);
|
|
4885
|
+
}
|
|
4593
4886
|
catch (e) { console.warn('[ixmaps-gl] loadProject: map.setProjection failed (needs maplibre-gl >=5.0.1)', e); }
|
|
4594
4887
|
} else {
|
|
4595
4888
|
console.warn('[ixmaps-gl] loadProject: map.setProjection not available on the loaded maplibre-gl build (needs >=5.0.1) — projection unchanged');
|
|
@@ -5135,6 +5428,8 @@
|
|
|
5135
5428
|
// earlier hand-tuned 12.5 drew every symbol 1.2× (with the reference
|
|
5136
5429
|
// below, 1.3×) smaller than flat.
|
|
5137
5430
|
const NORMAL_RADIUS_PX = 15;
|
|
5431
|
+
// largest BOX side (px) still drawn as a canvas icon (at 2×), see _buildBoxIcon
|
|
5432
|
+
const BOX_ICON_MAX_PX = 2048;
|
|
5138
5433
|
// Flat's own map scale, which dynamic object scaling compares with
|
|
5139
5434
|
// normalSizeScale (mapscript2.js 2849: dx = nTrueMapScale · nZoomScale /
|
|
5140
5435
|
// nNormalSizeScale), read from a real flat page: the generic Mercator
|
|
@@ -5381,6 +5676,16 @@
|
|
|
5381
5676
|
|
|
5382
5677
|
function valueRadius(value, zoom, style, mapOptions, flags, maxSizeValue) {
|
|
5383
5678
|
const zoomFactor = objectZoomFactor(zoom, mapOptions);
|
|
5679
|
+
// FIXSIZE (a symbol chart; PLOT / GRIDSIZE size their markers
|
|
5680
|
+
// themselves): one radius for every item, whatever its value — the
|
|
5681
|
+
// normal radius ÷ normalsizevalue, half of it for BUBBLE (maptheme.js
|
|
5682
|
+
// 20711, 21847; measured on flat: PNRR "details", r = 1.5 units at
|
|
5683
|
+
// scale 0.1)
|
|
5684
|
+
if (flags && flags.has('FIXSIZE') && !flags.has('PLOT') && !flags.has('GRIDSIZE')) {
|
|
5685
|
+
const fixed = NORMAL_RADIUS_PX * (styleNum(style.scale) || 1) / (styleNum(style.normalsizevalue) || 1)
|
|
5686
|
+
* (flatChartBranch(flags) === 'bubble' ? 0.5 : 1);
|
|
5687
|
+
return Math.max(VALUE_RADIUS_MIN, Math.min(VALUE_RADIUS_MAX, fixed * zoomFactor));
|
|
5688
|
+
}
|
|
5384
5689
|
// Real default (maptheme.js ~line 5059) when the theme sets no
|
|
5385
5690
|
// normalsizevalue: the dataset's own max value on the bound size
|
|
5386
5691
|
// field (maxSizeValue, from LayerRuntime._prepare), not a fixed
|
|
@@ -5585,6 +5890,8 @@
|
|
|
5585
5890
|
const field = binding.size ? binding.size
|
|
5586
5891
|
: (symbols && !flags.has('CATEGORICAL') && !flags.has('AGGREGATE') && binding.value && !String(binding.value).includes('|') ? binding.value : null);
|
|
5587
5892
|
if (!field) return undefined;
|
|
5893
|
+
// "$item$": every record counts 1
|
|
5894
|
+
if (field === '$item$') return features.length ? 1 : 0;
|
|
5588
5895
|
return features.reduce((max, f) => {
|
|
5589
5896
|
const v = parseFloat(f.properties[field]);
|
|
5590
5897
|
return isNaN(v) ? max : Math.max(max, v);
|
|
@@ -6184,6 +6491,13 @@
|
|
|
6184
6491
|
// researched safe range (~1480-2980 distinct 48px icons before a real
|
|
6185
6492
|
// device's WebGL MAX_TEXTURE_SIZE is at risk).
|
|
6186
6493
|
const ICON_ATLAS_RESET_AFTER = 3000;
|
|
6494
|
+
// The same reset by icon AREA: a few large icons (BOX rectangles around
|
|
6495
|
+
// MULTIQUAD grids, hundreds of px each, new sizes at every zoom) fill
|
|
6496
|
+
// deck.gl's 1024 px wide atlas long before 3000 icons — on the PNRR
|
|
6497
|
+
// page a dozen boxes doubled it per zoom step, until it passed WebGL's
|
|
6498
|
+
// MAX_TEXTURE_SIZE (16384) and the boxes drew black. 2 Mpx leaves room
|
|
6499
|
+
// for the atlas's row-packing waste (measured ~2×) below that limit.
|
|
6500
|
+
const ICON_ATLAS_AREA_RESET_AFTER = 2 * 1024 * 1024;
|
|
6187
6501
|
|
|
6188
6502
|
// Shared packing layout for a multi-category group's sub-bubbles — used
|
|
6189
6503
|
// both to draw the packed icon raster (_buildBubbleIcon) and to position
|
|
@@ -6228,6 +6542,56 @@
|
|
|
6228
6542
|
};
|
|
6229
6543
|
}
|
|
6230
6544
|
|
|
6545
|
+
// MULTIQUAD / MULTISQUARE (flat maptheme.js 18648-18690): the items at
|
|
6546
|
+
// one position, in drawing order, fill quads around it — the k-th at
|
|
6547
|
+
// square = ⌊√k⌋, (k − square², square) while that is within the square,
|
|
6548
|
+
// else (square, square − (k − square² − square)); once square reaches
|
|
6549
|
+
// gridx, rows of gridx go on upward (k % gridx, ⌊k / gridx⌋). Steps of
|
|
6550
|
+
// 2 · normal radius · rangescale (flat's chart size, measured on the PNRR
|
|
6551
|
+
// page); y up. Returns per item its pixel offset and index, and per
|
|
6552
|
+
// position's first item the extent [minX, minY, maxX, maxY] of the
|
|
6553
|
+
// offsets there (flat sizes the BOX around all of them).
|
|
6554
|
+
function multiQuadOffsets(items, normalRadiusPx, rangeScale, gridX) {
|
|
6555
|
+
const step = 2 * normalRadiusPx * (rangeScale || 1);
|
|
6556
|
+
const count = new Map(), offset = new Map(), index = new Map(), first = new Map(), extent = new Map();
|
|
6557
|
+
for (const d of items) {
|
|
6558
|
+
const c = d.geometry && d.geometry.coordinates;
|
|
6559
|
+
const key = c ? c[0] + ',' + c[1] : '';
|
|
6560
|
+
const k = count.get(key) || 0;
|
|
6561
|
+
count.set(key, k + 1);
|
|
6562
|
+
index.set(d, k);
|
|
6563
|
+
if (k === 0) { first.set(key, d); extent.set(d, [0, 0, 0, 0]); }
|
|
6564
|
+
let x = 0, y = 0;
|
|
6565
|
+
if (k > 0) {
|
|
6566
|
+
const square = Math.floor(Math.sqrt(k));
|
|
6567
|
+
if (square < (gridX || Infinity)) {
|
|
6568
|
+
const line = k - square * square;
|
|
6569
|
+
x = line <= square ? line : square;
|
|
6570
|
+
y = line <= square ? square : square - (line - square);
|
|
6571
|
+
} else {
|
|
6572
|
+
x = k % gridX;
|
|
6573
|
+
y = Math.floor(k / gridX);
|
|
6574
|
+
}
|
|
6575
|
+
}
|
|
6576
|
+
offset.set(d, [x * step, -y * step]);
|
|
6577
|
+
const e = extent.get(first.get(key));
|
|
6578
|
+
e[0] = Math.min(e[0], x * step); e[1] = Math.min(e[1], -y * step);
|
|
6579
|
+
e[2] = Math.max(e[2], x * step); e[3] = Math.max(e[3], -y * step);
|
|
6580
|
+
}
|
|
6581
|
+
return { offset, index, extent };
|
|
6582
|
+
}
|
|
6583
|
+
|
|
6584
|
+
// A pixel offset [dx, dy] (y down) from lngLat at MapLibre zoom `zoom`,
|
|
6585
|
+
// as lng/lat (Web Mercator, 512 px world tiles) — for shapes too large
|
|
6586
|
+
// for an icon, drawn in map coordinates (rebuilt with every refresh).
|
|
6587
|
+
function pixelOffsetLngLat(lngLat, offset, zoom) {
|
|
6588
|
+
const world = 512 * Math.pow(2, zoom);
|
|
6589
|
+
const lat0 = lngLat[1] * Math.PI / 180;
|
|
6590
|
+
const y = (0.5 - Math.log(Math.tan(Math.PI / 4 + lat0 / 2)) / (2 * Math.PI)) * world + offset[1];
|
|
6591
|
+
const lat = (2 * Math.atan(Math.exp((0.5 - y / world) * 2 * Math.PI)) - Math.PI / 2) * 180 / Math.PI;
|
|
6592
|
+
return [lngLat[0] + offset[0] * 360 / world, lat];
|
|
6593
|
+
}
|
|
6594
|
+
|
|
6231
6595
|
// CHART|SYMBOL|SEQUENCE (flat maptheme.js 21184-21300, 21790-21925,
|
|
6232
6596
|
// 22296-22395): one symbol per category part of a chart, in category
|
|
6233
6597
|
// order or sorted (SORT: DOWN biggest first, UP smallest first), parts
|
|
@@ -7150,9 +7514,13 @@
|
|
|
7150
7514
|
// set (e.g. roma: summed Pericolosita, normalized to 0..1).
|
|
7151
7515
|
// (flat's per-item nSize holds a record count in that last case, but it
|
|
7152
7516
|
// is not what draws a value-based symbol.)
|
|
7517
|
+
// "$item$" (as size or value) is flat's record count: 1 per record — it
|
|
7518
|
+
// is no column (read as one, every record was 0 and a LABEL|TEXTONLY
|
|
7519
|
+
// theme's text 0.08 px high).
|
|
7153
7520
|
function resolveAggregateValue(binding, flags, props) {
|
|
7521
|
+
if (binding.size === '$item$') return 1;
|
|
7154
7522
|
if (binding.size) return parseFloat(props[binding.size]) || 0;
|
|
7155
|
-
if (flags.has('CATEGORICAL') || binding.value == null) return 1;
|
|
7523
|
+
if (flags.has('CATEGORICAL') || binding.value == null || binding.value === '$item$') return 1;
|
|
7156
7524
|
return parseFloat(props[binding.value]) || 0;
|
|
7157
7525
|
}
|
|
7158
7526
|
|
|
@@ -7250,7 +7618,12 @@
|
|
|
7250
7618
|
key = `${snapped.x}:${snapped.y}`;
|
|
7251
7619
|
}
|
|
7252
7620
|
let cell = cells.get(key);
|
|
7253
|
-
if (!cell) { cell = { key, snapped, sumX: 0, sumY: 0, n: 0, value: 0, classValue: 0, value100: 0, series: null, first: f }; cells.set(key, cell); }
|
|
7621
|
+
if (!cell) { cell = { key, snapped, sumX: 0, sumY: 0, n: 0, value: 0, classValue: 0, value100: 0, series: null, first: f, raws: null }; cells.set(key, cell); }
|
|
7622
|
+
// the members' own records, for the tooltip's {{theme.item.data}}
|
|
7623
|
+
// (flat's __add_data tables them, numbered, up to 50)
|
|
7624
|
+
if (f.properties.raw && (!cell.raws || cell.raws.length < 50)) {
|
|
7625
|
+
(cell.raws = cell.raws || []).push(f.properties.raw);
|
|
7626
|
+
}
|
|
7254
7627
|
cell.sumX += p.x; cell.sumY += p.y; cell.n++;
|
|
7255
7628
|
cell.value += f.properties.value;
|
|
7256
7629
|
if (f.properties.classValue !== undefined) cell.classValue += f.properties.classValue;
|
|
@@ -7269,7 +7642,11 @@
|
|
|
7269
7642
|
const geometry = { type: 'Point', coordinates: [ll.lng, ll.lat] };
|
|
7270
7643
|
const hasClass = cell.first.properties.classValue !== undefined;
|
|
7271
7644
|
let props = cell.n > 1
|
|
7272
|
-
|
|
7645
|
+
// titleRaw: the cell's first record — flat titles an aggregated
|
|
7646
|
+
// item by it (itemTitle)
|
|
7647
|
+
? { cluster: true, point_count: cell.n, value: cell.value, ...(hasClass ? { classValue: cell.classValue } : {}),
|
|
7648
|
+
titleRaw: cell.first.properties.raw || cell.first.properties,
|
|
7649
|
+
...(cell.raws && cell.raws.length ? { raws: cell.raws } : {}) }
|
|
7273
7650
|
: cell.first.properties;
|
|
7274
7651
|
if (cell.series && cell.n > 1) props = Object.assign({}, props, { series: cell.series });
|
|
7275
7652
|
// a field aggregation's cell key (the field value): the categories of
|
|
@@ -7335,6 +7712,19 @@
|
|
|
7335
7712
|
let cell = cells.get(key);
|
|
7336
7713
|
if (!cell) { cell = { sumX: 0, sumY: 0, n: 0, cx: snapped.x, cy: snapped.y, counts: new Array(n).fill(0), recordCounts: new Array(n).fill(0), classTotal: undefined }; cells.set(key, cell); }
|
|
7337
7714
|
cell.sumX += p.x; cell.sumY += p.y; cell.n++;
|
|
7715
|
+
if (!cell.titleRaw) cell.titleRaw = f.properties.titleRaw || f.properties.raw || null;
|
|
7716
|
+
// the members' own records, for the tooltip's {{theme.item.data}} —
|
|
7717
|
+
// flat's __add_data tables them (numbered, up to 50) for aggregated
|
|
7718
|
+
// items too; references only, so no copy is made. A member can itself
|
|
7719
|
+
// be a pre-aggregated cluster node, whose records it carries in raws
|
|
7720
|
+
if (!cell.raws) cell.raws = [];
|
|
7721
|
+
if (cell.raws.length < 50) {
|
|
7722
|
+
if (f.properties.raw) cell.raws.push(f.properties.raw);
|
|
7723
|
+
else if (f.properties.raws) for (const r of f.properties.raws) {
|
|
7724
|
+
if (cell.raws.length >= 50) break;
|
|
7725
|
+
cell.raws.push(r);
|
|
7726
|
+
}
|
|
7727
|
+
}
|
|
7338
7728
|
cell.counts[f.properties.cat] += f.properties.value;
|
|
7339
7729
|
if (f.properties.classValue !== undefined) cell.classTotal = (cell.classTotal || 0) + f.properties.classValue;
|
|
7340
7730
|
// point_count is the aggregated-record count of a multi-record
|
|
@@ -7348,7 +7738,9 @@
|
|
|
7348
7738
|
const ll = atCellCenter && cellPx ? worldPixelToLngLat(cell.cx, cell.cy, zoom) : worldPixelToLngLat(cell.sumX / cell.n, cell.sumY / cell.n, zoom);
|
|
7349
7739
|
return {
|
|
7350
7740
|
geometry: { type: 'Point', coordinates: [ll.lng, ll.lat] },
|
|
7351
|
-
properties: { counts: cell.counts, total: cell.counts.reduce((a, c) => a + c, 0), recordCounts: cell.recordCounts, ...(cell.classTotal !== undefined ? { classTotal: cell.classTotal } : {})
|
|
7741
|
+
properties: { counts: cell.counts, total: cell.counts.reduce((a, c) => a + c, 0), recordCounts: cell.recordCounts, ...(cell.classTotal !== undefined ? { classTotal: cell.classTotal } : {}),
|
|
7742
|
+
...(cell.raws && cell.raws.length ? { raws: cell.raws } : {}),
|
|
7743
|
+
...(cell.titleRaw ? { titleRaw: cell.titleRaw } : {}) }
|
|
7352
7744
|
};
|
|
7353
7745
|
});
|
|
7354
7746
|
}
|
|
@@ -7392,13 +7784,25 @@
|
|
|
7392
7784
|
return count;
|
|
7393
7785
|
}
|
|
7394
7786
|
|
|
7787
|
+
// flat matches type flags as substrings (szFlag.match(/SYMBOL/)), so a
|
|
7788
|
+
// "SYMBOLS" flag (PNRR pages) is a SYMBOL chart there. With only the symbol
|
|
7789
|
+
// "label" (maptheme.js 21833: the value as text, TEXTONLY: text alone) it is
|
|
7790
|
+
// the LABEL chart this engine draws as _buildLabelChartLayers.
|
|
7791
|
+
function symbolsFlagCompat(flags, style) {
|
|
7792
|
+
if (!flags.has('SYMBOLS') || flags.has('SYMBOL') || flags.has('LABEL')) return flags;
|
|
7793
|
+
const out = new Set(flags);
|
|
7794
|
+
const symbols = [].concat((style && style.symbols) || []);
|
|
7795
|
+
out.add(symbols.length && symbols.every(v => String(v) === 'label') ? 'LABEL' : 'SYMBOL');
|
|
7796
|
+
return out;
|
|
7797
|
+
}
|
|
7798
|
+
|
|
7395
7799
|
class LayerRuntime {
|
|
7396
7800
|
// spec: a normalizeTheme() result — never a raw LayerBuilder
|
|
7397
7801
|
constructor(spec, fc, mapOptions) {
|
|
7398
7802
|
this.name = spec.name;
|
|
7399
7803
|
this.binding = spec.binding;
|
|
7400
|
-
this.flags = spec.flags;
|
|
7401
7804
|
this.style = spec.style;
|
|
7805
|
+
this.flags = symbolsFlagCompat(spec.flags, spec.style);
|
|
7402
7806
|
this.meta = spec.meta;
|
|
7403
7807
|
// GL-PORT COMPAT: retained only for ixmaps.getThemeObj()'s szFilter
|
|
7404
7808
|
// (real ixmaps-flat global compat shim, see _globalThemeRegistry) —
|
|
@@ -7432,6 +7836,7 @@
|
|
|
7432
7836
|
// ICON_ATLAS_RESET_AFTER for why this is necessary at all).
|
|
7433
7837
|
this._iconGeneration = 0;
|
|
7434
7838
|
this._iconsSinceAtlasReset = 0;
|
|
7839
|
+
this._iconAreaSinceAtlasReset = 0;
|
|
7435
7840
|
this._prepare();
|
|
7436
7841
|
}
|
|
7437
7842
|
|
|
@@ -7454,9 +7859,11 @@
|
|
|
7454
7859
|
_cacheIcon(key, icon) {
|
|
7455
7860
|
if (!this._iconCache.has(key)) {
|
|
7456
7861
|
this._iconsSinceAtlasReset++;
|
|
7457
|
-
|
|
7862
|
+
this._iconAreaSinceAtlasReset += (icon.width || 0) * (icon.height || 0);
|
|
7863
|
+
if (this._iconsSinceAtlasReset > ICON_ATLAS_RESET_AFTER || this._iconAreaSinceAtlasReset > ICON_ATLAS_AREA_RESET_AFTER) {
|
|
7458
7864
|
this._iconGeneration++;
|
|
7459
7865
|
this._iconsSinceAtlasReset = 0;
|
|
7866
|
+
this._iconAreaSinceAtlasReset = 0;
|
|
7460
7867
|
}
|
|
7461
7868
|
}
|
|
7462
7869
|
if (this._iconCache.size >= ICON_CACHE_MAX) {
|
|
@@ -7597,6 +8004,10 @@
|
|
|
7597
8004
|
// a page could in principle use both at once, though no current
|
|
7598
8005
|
// ported page does.
|
|
7599
8006
|
this._runtimeFilterExpr = '';
|
|
8007
|
+
// flat's setThemeTimeFrame(id, min, max): records whose style.timefield
|
|
8008
|
+
// value lies in [min, max) — null = no time filter. AND'd with the
|
|
8009
|
+
// filters above in _rebuildActiveFeatures.
|
|
8010
|
+
this._timeFrame = null;
|
|
7600
8011
|
|
|
7601
8012
|
// GRIDSIZE layers (PLOT curves-chart, or its grid-mesh companion)
|
|
7602
8013
|
// also carry AGGREGATE in their type string, but they bin into a
|
|
@@ -7793,6 +8204,44 @@
|
|
|
7793
8204
|
this._rebuildActiveFeatures();
|
|
7794
8205
|
}
|
|
7795
8206
|
|
|
8207
|
+
// setThemeTimeFrame: min/max in ms or any Date-parsable value; a null /
|
|
8208
|
+
// NaN bound is open-ended, both null clears the frame.
|
|
8209
|
+
setTimeFrame(min, max) {
|
|
8210
|
+
const toMs = v => (v == null || v === '' ? NaN : (typeof v === 'number' ? v : new Date(v).getTime()));
|
|
8211
|
+
const lo = toMs(min), hi = toMs(max);
|
|
8212
|
+
// flat: with the slider at its start position (frame starts at or
|
|
8213
|
+
// before the earliest record) every item is shown, not just the first window
|
|
8214
|
+
const field = this.binding && this.binding.time;
|
|
8215
|
+
if (field && !isNaN(lo)) {
|
|
8216
|
+
if (this._timeMin === undefined) {
|
|
8217
|
+
let m = Infinity;
|
|
8218
|
+
for (const f of this.features) {
|
|
8219
|
+
const v = f.properties && f.properties[field];
|
|
8220
|
+
const t = typeof v === 'number' ? v : new Date(v).getTime();
|
|
8221
|
+
if (!isNaN(t) && t < m) m = t;
|
|
8222
|
+
}
|
|
8223
|
+
this._timeMin = m;
|
|
8224
|
+
}
|
|
8225
|
+
if (lo <= this._timeMin) { this._timeFrame = null; this._rebuildActiveFeatures(); return; }
|
|
8226
|
+
}
|
|
8227
|
+
this._timeFrame = isNaN(lo) && isNaN(hi) ? null : { min: lo, max: hi };
|
|
8228
|
+
this._rebuildActiveFeatures();
|
|
8229
|
+
}
|
|
8230
|
+
|
|
8231
|
+
// predicate over a record's properties for the active time frame
|
|
8232
|
+
_timePredicate() {
|
|
8233
|
+
const tf = this._timeFrame;
|
|
8234
|
+
if (!tf) return null;
|
|
8235
|
+
const field = this.binding && this.binding.time;
|
|
8236
|
+
if (!field) return null;
|
|
8237
|
+
return props => {
|
|
8238
|
+
const v = props && props[field];
|
|
8239
|
+
const t = typeof v === 'number' ? v : new Date(v).getTime();
|
|
8240
|
+
if (isNaN(t)) return false;
|
|
8241
|
+
return (isNaN(tf.min) || t >= tf.min) && (isNaN(tf.max) || t < tf.max);
|
|
8242
|
+
};
|
|
8243
|
+
}
|
|
8244
|
+
|
|
7796
8245
|
// `_featuresByCategory` (plain per-category bucketing — NOT yet
|
|
7797
8246
|
// grid-aggregated; that's a separate, later step gated by AGGREGATE
|
|
7798
8247
|
// alone, see _computeAggregatedItems) is needed whenever this runtime
|
|
@@ -7823,7 +8272,8 @@
|
|
|
7823
8272
|
replaceFeatures(fc) {
|
|
7824
8273
|
// new data → new deck.gl layers (see _dataLayerId)
|
|
7825
8274
|
this._dataGen = (this._dataGen || 0) + 1;
|
|
7826
|
-
|
|
8275
|
+
this._timeMin = undefined;
|
|
8276
|
+
const facetFilters = this.facetFilters, runtimeFilterExpr = this._runtimeFilterExpr, timeFrame = this._timeFrame;
|
|
7827
8277
|
this.binding = this._specBinding;
|
|
7828
8278
|
this.features = applyField100(filterFlatValues(this._withDensity(fc.features), this.binding, this.flags), this.binding, this.flags, this.style);
|
|
7829
8279
|
this.binding = field100Binding(this.binding, this.flags);
|
|
@@ -7833,6 +8283,7 @@
|
|
|
7833
8283
|
this._prepare();
|
|
7834
8284
|
this.facetFilters = facetFilters;
|
|
7835
8285
|
this._runtimeFilterExpr = runtimeFilterExpr;
|
|
8286
|
+
this._timeFrame = timeFrame;
|
|
7836
8287
|
this._rebuildActiveFeatures();
|
|
7837
8288
|
}
|
|
7838
8289
|
|
|
@@ -7845,6 +8296,27 @@
|
|
|
7845
8296
|
// the next redraw.
|
|
7846
8297
|
_dataLayerId(base) { return this._dataGen ? `${base}-d${this._dataGen}` : base; }
|
|
7847
8298
|
|
|
8299
|
+
// deck.gl compares a layer's data by reference: a new object makes it
|
|
8300
|
+
// re-process every polygon (on the PNRR page ~300 ms per refresh for
|
|
8301
|
+
// 8 000 comuni, at every pan tick). Polygon data that doesn't depend
|
|
8302
|
+
// on the view is therefore handed over as the same object until its
|
|
8303
|
+
// inputs change.
|
|
8304
|
+
_featureCollection() {
|
|
8305
|
+
if (!this._featuresFC || this._featuresFC.features !== this.features) {
|
|
8306
|
+
this._featuresFC = { type: 'FeatureCollection', features: this.features };
|
|
8307
|
+
}
|
|
8308
|
+
return this._featuresFC;
|
|
8309
|
+
}
|
|
8310
|
+
// the same for derived data: build() runs only when an entry of key
|
|
8311
|
+
// (compared by identity) differs from the last call's
|
|
8312
|
+
_cachedPolygonData(slot, key, build) {
|
|
8313
|
+
const last = this[slot];
|
|
8314
|
+
if (last && last.key.length === key.length && last.key.every((v, i) => v === key[i])) return last.data;
|
|
8315
|
+
const data = build();
|
|
8316
|
+
this[slot] = { key, data };
|
|
8317
|
+
return data;
|
|
8318
|
+
}
|
|
8319
|
+
|
|
7848
8320
|
_usesAggregationIndex() {
|
|
7849
8321
|
return (this.flags.has('AGGREGATE') || isSymbolChart(this.flags)) && !this.flags.has('GRIDSIZE');
|
|
7850
8322
|
}
|
|
@@ -7856,8 +8328,10 @@
|
|
|
7856
8328
|
if (isAggregatedCategoricalChoropleth(this)) {
|
|
7857
8329
|
const expr = this._runtimeFilterExpr;
|
|
7858
8330
|
const clauses = Array.from(this.facetFilters.entries());
|
|
7859
|
-
|
|
7860
|
-
|
|
8331
|
+
const timePred = this._timePredicate();
|
|
8332
|
+
if (!expr && !clauses.length && !timePred) { this._activeFeatures = null; return; }
|
|
8333
|
+
const wherePred = expr ? flatFilterPredicate(String(expr)) : null;
|
|
8334
|
+
const pred = wherePred && timePred ? (r => wherePred(r) && timePred(r)) : (wherePred || timePred);
|
|
7861
8335
|
this._activeFeatures = this.features.map(f => {
|
|
7862
8336
|
const rows = f.properties && f.properties[AGGREGATED_ROWS];
|
|
7863
8337
|
if (!rows) return f;
|
|
@@ -7875,11 +8349,13 @@
|
|
|
7875
8349
|
// comment) if one is active — reusing applyWhereFilter, the SAME
|
|
7876
8350
|
// parser the layer's load-time .filter(expr) already uses, just
|
|
7877
8351
|
// invoked again here instead of once at build time.
|
|
7878
|
-
|
|
8352
|
+
let base = this._runtimeFilterExpr
|
|
7879
8353
|
? applyWhereFilter({ type: 'FeatureCollection', features: this.features }, this._runtimeFilterExpr).features
|
|
7880
8354
|
: this.features;
|
|
8355
|
+
const timePred = this._timePredicate();
|
|
8356
|
+
if (timePred) base = base.filter(f => timePred(f.properties));
|
|
7881
8357
|
if (this.facetFilters.size === 0) {
|
|
7882
|
-
this._activeFeatures = this._runtimeFilterExpr ? base : null;
|
|
8358
|
+
this._activeFeatures = (this._runtimeFilterExpr || timePred) ? base : null;
|
|
7883
8359
|
} else {
|
|
7884
8360
|
const clauses = Array.from(this.facetFilters.entries());
|
|
7885
8361
|
this._activeFeatures = base.filter(f =>
|
|
@@ -8249,22 +8725,45 @@
|
|
|
8249
8725
|
// formatting) instead of an inline SVG chart — one line per present
|
|
8250
8726
|
// category, each showing ITS OWN value, never summed across
|
|
8251
8727
|
// categories (same rule as the VALUES bubble labels).
|
|
8252
|
-
// - theme.item.data (a table of the hovered record's raw fields)
|
|
8253
|
-
//
|
|
8254
|
-
//
|
|
8255
|
-
//
|
|
8256
|
-
|
|
8257
|
-
|
|
8728
|
+
// - theme.item.data (a table of the hovered record's raw fields)
|
|
8729
|
+
// tables an individual point's record, and an aggregated item's
|
|
8730
|
+
// member records (numbered, up to 50 — flat's __add_data does the
|
|
8731
|
+
// same); only a group without retained records (cluster-level
|
|
8732
|
+
// groups) resolves to an empty string there.
|
|
8733
|
+
// an item's title as flat gives it (maptheme.js 11838-11886): the title
|
|
8734
|
+
// field of its record — of an aggregated item its first record; an
|
|
8735
|
+
// aggregated grid cell (no aggregation field) of more than 3 records,
|
|
8736
|
+
// or without a title, is titled "(n)"
|
|
8737
|
+
_itemTitle(props) {
|
|
8738
|
+
const raw = props.raw || props.titleRaw;
|
|
8739
|
+
const title = this.binding.title && raw && raw[this.binding.title] != null ? String(raw[this.binding.title]) : '';
|
|
8740
|
+
const aggregated = !!(props.counts || props.cluster);
|
|
8741
|
+
if (aggregated && this._clusterRadiusPx && !this._clusterField) {
|
|
8742
|
+
const n = groupRecordCount(props);
|
|
8743
|
+
if (!title || n > 3) return '(' + n + ')';
|
|
8744
|
+
}
|
|
8745
|
+
return title;
|
|
8746
|
+
}
|
|
8747
|
+
|
|
8748
|
+
buildTooltipHtml(object, fDark) {
|
|
8749
|
+
// no .meta({tooltip}): flat's default template (tooltip_mustache.js 394)
|
|
8750
|
+
// — the theme title, the item's title and its chart
|
|
8751
|
+
const template = this.meta.tooltip || FLAT_DEFAULT_TOOLTIP;
|
|
8258
8752
|
if (!global.Mustache) {
|
|
8259
|
-
console.warn('[ixmaps-gl]
|
|
8753
|
+
console.warn('[ixmaps-gl] tooltips need Mustache.js, which is not loaded — ' +
|
|
8260
8754
|
'include https://unpkg.com/mustache@4.2.0/mustache.min.js before ixmaps-gl.js.');
|
|
8261
8755
|
return null;
|
|
8262
8756
|
}
|
|
8263
|
-
return global.Mustache.render(
|
|
8757
|
+
return youtubeClickToPlay(global.Mustache.render(template, this._buildTooltipContext(object, fDark)));
|
|
8264
8758
|
}
|
|
8265
8759
|
|
|
8266
|
-
_buildTooltipContext(object) {
|
|
8267
|
-
|
|
8760
|
+
_buildTooltipContext(object, fDark) {
|
|
8761
|
+
let props = object.properties;
|
|
8762
|
+
// a FEATURE|CATEGORICAL polygon is the raw data record itself, not the
|
|
8763
|
+
// {raw, cat, value} wrapper the other layer kinds build
|
|
8764
|
+
if (props && !props.raw && (this.flags.has('FEATURE') || this.flags.has('FEATURES')) && this.flags.has('CATEGORICAL')) {
|
|
8765
|
+
props = { raw: props, cat: this._resolveClassIndex(props[this.binding.value]) };
|
|
8766
|
+
}
|
|
8268
8767
|
const isGroup = !!props.counts;
|
|
8269
8768
|
const chartHtml = this._renderItemChartHtml(props);
|
|
8270
8769
|
|
|
@@ -8276,14 +8775,12 @@
|
|
|
8276
8775
|
chart: chartHtml,
|
|
8277
8776
|
item: {
|
|
8278
8777
|
chart: chartHtml,
|
|
8279
|
-
data: isGroup ?
|
|
8778
|
+
data: isGroup ? this._renderGroupDataTableHtml(props.raws, fDark) : this._renderItemDataTableHtml(props.raw, fDark),
|
|
8280
8779
|
value: this._hasValue(isGroup ? props.total : props.value) ? this._formatTooltipValue(isGroup ? props.total : props.value) : '',
|
|
8281
8780
|
label: isGroup ? '' : (this.categoryDisplayLabels ? (this.categoryDisplayLabels[props.cat] || '') : ''),
|
|
8282
8781
|
// flat: the item's title is its titlefield value (binding title/
|
|
8283
8782
|
// titlefield, e.g. the comune name); else the class label
|
|
8284
|
-
title: isGroup ? '' : (this.
|
|
8285
|
-
? String(props.raw[this.binding.title])
|
|
8286
|
-
: (this.categoryDisplayLabels ? (this.categoryDisplayLabels[props.cat] || '') : '')),
|
|
8783
|
+
title: this._itemTitle(props) || (isGroup ? '' : (this.categoryDisplayLabels ? (this.categoryDisplayLabels[props.cat] || '') : '')),
|
|
8287
8784
|
count: isGroup ? (props.recordCounts ? props.recordCounts.reduce((a, c) => a + c, 0) : '') : 1,
|
|
8288
8785
|
class: isGroup ? '' : props.cat
|
|
8289
8786
|
}
|
|
@@ -8316,11 +8813,16 @@
|
|
|
8316
8813
|
// breakdown, one line per present category, each with its own legend
|
|
8317
8814
|
// color swatch and its OWN value (never summed across categories).
|
|
8318
8815
|
_renderItemChartHtml(props) {
|
|
8816
|
+
const unitOf = this.style.units ? ' ' + this.style.units : '';
|
|
8817
|
+
// an AGGREGATE CATEGORICAL choropleth polygon: its sum per category
|
|
8818
|
+
if (props.parts) {
|
|
8819
|
+
const labels = this.categoryDisplayLabels || this.categoryLabels || [];
|
|
8820
|
+
return tooltipTable(props.parts.map((v, i) => (v > 0 ? { rgb: this.categoryColorsRgb[i], label: labels[i] || '(n/d)', value: this._formatTooltipValue(v) + unitOf } : null)).filter(Boolean));
|
|
8821
|
+
}
|
|
8319
8822
|
if (this.flags.has('CHOROPLETH') && (this.flags.has('DOMINANT') || this.flags.has('COMPOSECOLOR')) && props.raw) {
|
|
8320
8823
|
return this._renderMultiFieldBarsHtml(props.raw);
|
|
8321
8824
|
}
|
|
8322
|
-
const
|
|
8323
|
-
const unit = this.style.units ? ' ' + this.style.units : '';
|
|
8825
|
+
const unit = unitOf;
|
|
8324
8826
|
|
|
8325
8827
|
// PLOT|GRIDSIZE curve cells: properties.values is an array parallel
|
|
8326
8828
|
// to _plotCategories() (e.g. one FERITI sum per year), not a single
|
|
@@ -8330,10 +8832,10 @@
|
|
|
8330
8832
|
if (Array.isArray(props.values)) {
|
|
8331
8833
|
const categories = this._isItemPlot() ? this._itemPlotLabels() : this._plotCategories();
|
|
8332
8834
|
const labels = this._isItemPlot() ? categories : Array.isArray(this.style.label) ? this.style.label.map(String) : categories;
|
|
8333
|
-
return props.values.map((v, i) => {
|
|
8835
|
+
return tooltipTable(props.values.map((v, i) => {
|
|
8334
8836
|
if (v == null || isNaN(v)) return null;
|
|
8335
|
-
return
|
|
8336
|
-
}).filter(Boolean)
|
|
8837
|
+
return { rgb: null, label: labels[i] || categories[i] || i, value: this._formatTooltipValue(v) + unit };
|
|
8838
|
+
}).filter(Boolean));
|
|
8337
8839
|
}
|
|
8338
8840
|
|
|
8339
8841
|
if (props.counts) {
|
|
@@ -8352,19 +8854,17 @@
|
|
|
8352
8854
|
// zoom until something resets it. Root cause of the "trackpad
|
|
8353
8855
|
// zoom sometimes just stops updating" reports.
|
|
8354
8856
|
const labels = this.categoryDisplayLabels || this.categoryLabels || [];
|
|
8355
|
-
return labels.map((label, i) => {
|
|
8857
|
+
return tooltipTable(labels.map((label, i) => {
|
|
8356
8858
|
if (!(props.counts[i] > 0)) return null;
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
}).filter(Boolean).join('');
|
|
8859
|
+
return { rgb: this.categoryColorsRgb[i], label: label || '(n/d)', value: this._formatTooltipValue(props.counts[i]) + unit };
|
|
8860
|
+
}).filter(Boolean));
|
|
8360
8861
|
}
|
|
8361
8862
|
if (props.cat != null) {
|
|
8362
8863
|
const label = this.categoryDisplayLabels ? (this.categoryDisplayLabels[props.cat] || '') : '';
|
|
8363
8864
|
const rgb = this.categoryColorsRgb ? this.categoryColorsRgb[props.cat] : null;
|
|
8364
8865
|
// no bound size field (e.g. a plain DOT|CATEGORICAL layer) -> just
|
|
8365
8866
|
// the category label, no ": undefined" value suffix
|
|
8366
|
-
|
|
8367
|
-
return `<div>${rgb ? swatch(rgb) : ''}${label}${valueSuffix}</div>`;
|
|
8867
|
+
return tooltipTable([{ rgb, label, value: this._hasValue(props.value) ? this._formatTooltipValue(props.value) + unit : '' }]);
|
|
8368
8868
|
}
|
|
8369
8869
|
return '';
|
|
8370
8870
|
}
|
|
@@ -8418,14 +8918,49 @@
|
|
|
8418
8918
|
}
|
|
8419
8919
|
|
|
8420
8920
|
// theme.item.data approximation — a plain field:value table of the
|
|
8421
|
-
// hovered record's own raw properties (individual points only)
|
|
8422
|
-
|
|
8921
|
+
// hovered record's own raw properties (individual points only) — flat's
|
|
8922
|
+
// own row markup (tooltip_mustache.js 568-590, main.css .tooltip): the
|
|
8923
|
+
// field name right-aligned, the value left, a dotted rule above the
|
|
8924
|
+
// value, the table at 0.85em with flat's min-width. Light is flat
|
|
8925
|
+
// exactly (#cccccc labels, #000000 values, dotted black); dark keeps
|
|
8926
|
+
// the same visibility hierarchy inverted - DIM labels, BRIGHT values -
|
|
8927
|
+
// with its own colors, a dotted #555 rule and #888 record pills
|
|
8928
|
+
_renderItemDataTableHtml(raw, fDark) {
|
|
8423
8929
|
if (!raw) return '';
|
|
8930
|
+
const value = fDark ? '#e0e4ea' : '#000000';
|
|
8931
|
+
const label = fDark ? '#787d85' : '#cccccc';
|
|
8932
|
+
const rule = fDark ? '#555' : 'black';
|
|
8424
8933
|
const rows = Object.keys(raw)
|
|
8425
8934
|
.filter(k => k !== 'geometry')
|
|
8426
|
-
.map(k => `<tr><td style="
|
|
8935
|
+
.map(k => `<tr><td></td><td style="text-align:right;vertical-align:top;color:${label}">${k}</td>` +
|
|
8936
|
+
`<td style="text-align:left;vertical-align:top;color:${value};padding:0 0.2em 0 0.8em;border-top:dotted ${rule} 0.1px">` +
|
|
8937
|
+
`${this._isNumericValue(raw[k]) ? this._formatTooltipValue(raw[k]) : raw[k]}</td></tr>`)
|
|
8427
8938
|
.join('');
|
|
8428
|
-
return `<table style="font-size:0.85em;border-collapse:collapse">${rows}</table>`;
|
|
8939
|
+
return `<table style="font-size:0.85em;min-width:300px;border-collapse:collapse">${rows}</table>`;
|
|
8940
|
+
}
|
|
8941
|
+
|
|
8942
|
+
// an aggregated item's own records, numbered, up to 50 — like real
|
|
8943
|
+
// flat's __add_data (tooltip_mustache.js 490-570: one numbered block
|
|
8944
|
+
// per record when the item holds more than one). A single-record
|
|
8945
|
+
// group renders like an individual point.
|
|
8946
|
+
_renderGroupDataTableHtml(raws, fDark) {
|
|
8947
|
+
if (!raws || !raws.length) return '';
|
|
8948
|
+
if (raws.length === 1) return this._renderItemDataTableHtml(raws[0], fDark);
|
|
8949
|
+
const value = fDark ? '#e0e4ea' : '#000000';
|
|
8950
|
+
const label = fDark ? '#787d85' : '#cccccc';
|
|
8951
|
+
const rule = fDark ? '#555' : 'black';
|
|
8952
|
+
const pill = fDark ? '#888' : 'black';
|
|
8953
|
+
const rows = raws.slice(0, 50).map((raw, d) =>
|
|
8954
|
+
'<tr><td style="font-size:1em;"> </td></tr>' +
|
|
8955
|
+
`<tr><td style="font-size:1.5em;border:solid ${pill} 1px;border-radius:1em;padding:0.1em 0.4em 0em 0.3em">${d + 1}</td></tr>` +
|
|
8956
|
+
Object.keys(raw)
|
|
8957
|
+
.filter(k => k !== 'geometry')
|
|
8958
|
+
.map(k => `<tr><td></td><td style="text-align:right;vertical-align:top;color:${label}">${k}</td>` +
|
|
8959
|
+
`<td style="text-align:left;vertical-align:top;color:${value};padding:0 0.2em 0 0.8em;border-top:dotted ${rule} 0.1px">` +
|
|
8960
|
+
`${this._isNumericValue(raw[k]) ? this._formatTooltipValue(raw[k]) : raw[k]}</td></tr>`)
|
|
8961
|
+
.join('')
|
|
8962
|
+
).join('');
|
|
8963
|
+
return `<table style="font-size:0.85em;min-width:300px;border-collapse:collapse">${rows}</table>`;
|
|
8429
8964
|
}
|
|
8430
8965
|
|
|
8431
8966
|
_isNumericValue(v) {
|
|
@@ -8552,11 +9087,19 @@
|
|
|
8552
9087
|
const cs = this.style.colorscheme;
|
|
8553
9088
|
const raw = Array.isArray(cs) ? cs[0] : cs;
|
|
8554
9089
|
const filled = raw !== 'none';
|
|
9090
|
+
// FEATURE|CATEGORICAL: polygons coloured by their category (flat colours
|
|
9091
|
+
// each shape by its class), not one flat fill
|
|
9092
|
+
const byCategory = filled && this.flags.has('CATEGORICAL') && this.categoryIndexByLabel && this.binding.value
|
|
9093
|
+
&& this.categoryColorsRgb && this.categoryColorsRgb.length;
|
|
9094
|
+
const catAlpha = Math.round(255 * (styleNum(this.style.fillopacity) || 1));
|
|
9095
|
+
const nodataRgb = parseCssColor(this.style.nodatacolor) || [255, 255, 255];
|
|
8555
9096
|
const shadow = filled && flatShadowOn(this.style, this.features.length, zoom)
|
|
8556
9097
|
? this._buildShadowLayers(hexOrNamedToRgb(raw), zoom, bbox) : [];
|
|
8557
9098
|
return shadow.concat([new GeoJsonLayer({
|
|
8558
9099
|
id: this._dataLayerId(`ix-features-${this.name}`),
|
|
8559
|
-
data:
|
|
9100
|
+
data: this._featureCollection(),
|
|
9101
|
+
// FEATURE|CATEGORICAL polygons carry the data record: hover / click tooltips
|
|
9102
|
+
pickable: !!byCategory,
|
|
8560
9103
|
// linecolor "none": no outline (a FEATURES theme of points with
|
|
8561
9104
|
// colorscheme and linecolor "none" draws nothing, as flat)
|
|
8562
9105
|
stroked: styleLineColor(this.style.linecolor) !== 'none',
|
|
@@ -8567,7 +9110,13 @@
|
|
|
8567
9110
|
// flat puts fillopacity into the shape's fill-opacity only
|
|
8568
9111
|
// (maptheme.js 13407, 14071-14078): the outline stays opaque
|
|
8569
9112
|
// (a fill color's own alpha — rgba(…, 0.5) — times fillopacity)
|
|
8570
|
-
|
|
9113
|
+
...(byCategory ? { updateTriggers: { getFillColor: JSON.stringify(this.categoryColorsRgb) } } : {}),
|
|
9114
|
+
getFillColor: byCategory
|
|
9115
|
+
? f => {
|
|
9116
|
+
const cat = this._resolveClassIndex(f.properties[this.binding.value]);
|
|
9117
|
+
return [...(cat != null ? this.categoryColorsRgb[cat] : nodataRgb).slice(0, 3), catAlpha];
|
|
9118
|
+
}
|
|
9119
|
+
: filled ? [...hexOrNamedToRgb(raw).slice(0, 3), Math.round(255 * cssColorAlpha(raw) * (styleNum(this.style.fillopacity) || 1))] : [0, 0, 0, 0],
|
|
8571
9120
|
getLineColor: this.style.linecolor
|
|
8572
9121
|
? withAlpha(hexOrNamedToRgb(styleLineColor(this.style.linecolor)), cssColorAlpha(styleLineColor(this.style.linecolor)))
|
|
8573
9122
|
: [130, 130, 130],
|
|
@@ -8641,8 +9190,8 @@
|
|
|
8641
9190
|
// single winning class, so no `cat`/tooltip chart line, just the
|
|
8642
9191
|
// blended color directly); AGGREGATE CATEGORICAL on one field — the
|
|
8643
9192
|
// dominant category of each polygon's records (aggregatedCategoricalClass).
|
|
8644
|
-
// A plain CATEGORICAL choropleth
|
|
8645
|
-
//
|
|
9193
|
+
// A plain CATEGORICAL choropleth matches the raw value of the polygon's
|
|
9194
|
+
// row against the category labels. Geometry + properties are already the joined
|
|
8646
9195
|
// FeatureCollection from joinChoroplethFeatures. `zoom` is only used
|
|
8647
9196
|
// for style.fillopacity:"auto" (see resolveAutoFillOpacity) — every
|
|
8648
9197
|
// other branch here is zoom-independent.
|
|
@@ -8668,65 +9217,76 @@
|
|
|
8668
9217
|
// AGGREGATE CATEGORICAL: every polygon's parts and dominant class
|
|
8669
9218
|
// (aggregatedCategoricalClass), the biggest part per class over all
|
|
8670
9219
|
// polygons for the opacity (flat's nMaxA)
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
if (!rows) return;
|
|
8680
|
-
const agg = aggregatedCategoricalClass(rows, this.binding, this.flags, this.categoryIndexByLabel, nCats);
|
|
8681
|
-
if (!agg) return;
|
|
8682
|
-
aggOf.set(f, agg);
|
|
8683
|
-
agg.parts.forEach((v, i) => { if (v > aggMaxA[i]) aggMaxA[i] = v; });
|
|
8684
|
-
});
|
|
8685
|
-
}
|
|
8686
|
-
const data = source.map(f => {
|
|
9220
|
+
// the polygons' classes don't depend on the view or on marked
|
|
9221
|
+
// classes (those only change the colors, see updateTriggers below):
|
|
9222
|
+
// the data is rebuilt only when its inputs change
|
|
9223
|
+
const dataKey = [source, this._dataGen, this.categoryIndexByLabel, this.partsA, this._classSeparate,
|
|
9224
|
+
dopacityActive ? baseOpacity : null, JSON.stringify(this.style), JSON.stringify(this.binding)];
|
|
9225
|
+
const collection = this._cachedPolygonData('_choroplethData', dataKey, () => {
|
|
9226
|
+
const aggCat = isAggregatedCategoricalChoropleth(this) && this.categoryIndexByLabel;
|
|
9227
|
+
let aggOf = null, aggMaxA = null;
|
|
8687
9228
|
if (aggCat) {
|
|
8688
|
-
const
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
properties
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
if (isComposeColor) {
|
|
8700
|
-
return {
|
|
8701
|
-
type: 'Feature',
|
|
8702
|
-
geometry: f.geometry,
|
|
8703
|
-
properties: { raw: f.properties, composedColor: this._resolveComposedColor(f.properties) }
|
|
8704
|
-
};
|
|
9229
|
+
const nCats = this.categoryLabels.length;
|
|
9230
|
+
aggOf = new Map();
|
|
9231
|
+
aggMaxA = new Array(nCats).fill(0);
|
|
9232
|
+
source.forEach(f => {
|
|
9233
|
+
const rows = f.properties && f.properties[AGGREGATED_ROWS];
|
|
9234
|
+
if (!rows) return;
|
|
9235
|
+
const agg = aggregatedCategoricalClass(rows, this.binding, this.flags, this.categoryIndexByLabel, nCats);
|
|
9236
|
+
if (!agg) return;
|
|
9237
|
+
aggOf.set(f, agg);
|
|
9238
|
+
agg.parts.forEach((v, i) => { if (v > aggMaxA[i]) aggMaxA[i] = v; });
|
|
9239
|
+
});
|
|
8705
9240
|
}
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
9241
|
+
const data = source.map(f => {
|
|
9242
|
+
if (aggCat) {
|
|
9243
|
+
const agg = aggOf.get(f);
|
|
9244
|
+
return {
|
|
9245
|
+
type: 'Feature',
|
|
9246
|
+
geometry: f.geometry,
|
|
9247
|
+
properties: {
|
|
9248
|
+
// no part above 0: no item in flat, not painted
|
|
9249
|
+
value: agg ? agg.value : undefined, raw: agg ? f.properties : {}, cat: agg ? agg.index : null, parts: agg ? agg.parts : undefined,
|
|
9250
|
+
dopacityAlpha: agg && dopacityActive ? dominantDopacityAlpha(this.style, this.flags, agg.value, aggMaxA[agg.index]) : null
|
|
9251
|
+
}
|
|
9252
|
+
};
|
|
9253
|
+
}
|
|
9254
|
+
if (isComposeColor) {
|
|
9255
|
+
return {
|
|
9256
|
+
type: 'Feature',
|
|
9257
|
+
geometry: f.geometry,
|
|
9258
|
+
properties: { raw: f.properties, composedColor: this._resolveComposedColor(f.properties) }
|
|
9259
|
+
};
|
|
9260
|
+
}
|
|
9261
|
+
if (isDominant) {
|
|
9262
|
+
const dom = this._resolveDominantClass(f.properties);
|
|
9263
|
+
const value = dom ? dom.value : undefined;
|
|
9264
|
+
return {
|
|
9265
|
+
type: 'Feature',
|
|
9266
|
+
geometry: f.geometry,
|
|
9267
|
+
properties: {
|
|
9268
|
+
value, raw: f.properties, cat: dom ? dom.index : null,
|
|
9269
|
+
dopacityAlpha: dopacityActive ? this._resolveDopacityAlpha(f, value, baseOpacity) : null
|
|
9270
|
+
}
|
|
9271
|
+
};
|
|
9272
|
+
}
|
|
9273
|
+
// a polygon without a data row is no item in flat: no value
|
|
9274
|
+
const hasRow = f.properties && Object.keys(f.properties).length > 0;
|
|
9275
|
+
// CATEGORICAL matches the raw value against the category labels
|
|
9276
|
+
// (a text value has no numeric form: flatNumber would give NaN)
|
|
9277
|
+
const rawVal = hasRow ? f.properties[this.binding.value] : undefined;
|
|
9278
|
+
const value = this.categoryIndexByLabel ? rawVal : (hasRow ? flatNumber(rawVal, this.flags) : NaN);
|
|
9279
|
+
const cat = this._resolveClassIndex(value);
|
|
8709
9280
|
return {
|
|
8710
9281
|
type: 'Feature',
|
|
8711
9282
|
geometry: f.geometry,
|
|
8712
9283
|
properties: {
|
|
8713
|
-
value, raw: f.properties, cat
|
|
9284
|
+
value, raw: f.properties, cat,
|
|
8714
9285
|
dopacityAlpha: dopacityActive ? this._resolveDopacityAlpha(f, value, baseOpacity) : null
|
|
8715
9286
|
}
|
|
8716
9287
|
};
|
|
8717
|
-
}
|
|
8718
|
-
|
|
8719
|
-
const hasRow = f.properties && Object.keys(f.properties).length > 0;
|
|
8720
|
-
const value = hasRow ? flatNumber(f.properties[this.binding.value], this.flags) : NaN;
|
|
8721
|
-
const cat = this._resolveClassIndex(value);
|
|
8722
|
-
return {
|
|
8723
|
-
type: 'Feature',
|
|
8724
|
-
geometry: f.geometry,
|
|
8725
|
-
properties: {
|
|
8726
|
-
value, raw: f.properties, cat,
|
|
8727
|
-
dopacityAlpha: dopacityActive ? this._resolveDopacityAlpha(f, value, baseOpacity) : null
|
|
8728
|
-
}
|
|
8729
|
-
};
|
|
9288
|
+
});
|
|
9289
|
+
return { type: 'FeatureCollection', features: data };
|
|
8730
9290
|
});
|
|
8731
9291
|
// A marked class (legend row click / ixmaps.markThemeClass) as real
|
|
8732
9292
|
// ixmaps-flat shows it on a choropleth (MapTheme.markClass,
|
|
@@ -8748,9 +9308,13 @@
|
|
|
8748
9308
|
return evidenceMode === 'isolate' ? 'hide' : 'gray';
|
|
8749
9309
|
};
|
|
8750
9310
|
const grayOf = ([r, g, b]) => { const y = Math.round(0.299 * r + 0.587 * g + 0.114 * b); return [y, y, y]; };
|
|
9311
|
+
// deck.gl re-runs the color accessors only for new data or a changed trigger
|
|
9312
|
+
const colorKey = JSON.stringify([[...this._markedClasses], !!this._onMarksChanged, evidenceMode,
|
|
9313
|
+
this.categoryColorsRgb, fallbackRgb, classAlpha, lineRgb]);
|
|
8751
9314
|
return [new GeoJsonLayer({
|
|
8752
9315
|
id: this._dataLayerId(`ix-choropleth-${this.name}`),
|
|
8753
|
-
data:
|
|
9316
|
+
data: collection,
|
|
9317
|
+
updateTriggers: { getFillColor: colorKey, getLineColor: colorKey },
|
|
8754
9318
|
pickable: true,
|
|
8755
9319
|
stroked: true,
|
|
8756
9320
|
filled: true,
|
|
@@ -9720,7 +10284,8 @@
|
|
|
9720
10284
|
return {
|
|
9721
10285
|
geometry: f.geometry,
|
|
9722
10286
|
properties: { counts: this._oneHot(f.properties.cat, f.properties.point_count), total: f.properties.value,
|
|
9723
|
-
...(f.properties.classValue !== undefined ? { classTotal: f.properties.classValue } : {})
|
|
10287
|
+
...(f.properties.classValue !== undefined ? { classTotal: f.properties.classValue } : {}),
|
|
10288
|
+
...(f.properties.titleRaw ? { titleRaw: f.properties.titleRaw } : {}) }
|
|
9724
10289
|
};
|
|
9725
10290
|
});
|
|
9726
10291
|
return { individual, groups };
|
|
@@ -9822,7 +10387,11 @@
|
|
|
9822
10387
|
this._valueMedian = sorted[Math.floor((sorted.length - 1) / 2)];
|
|
9823
10388
|
}
|
|
9824
10389
|
|
|
9825
|
-
|
|
10390
|
+
// flat's default normalsizevalue is the largest aggregated size (maptheme.js 5021-5022)
|
|
10391
|
+
let sizeMax = 0;
|
|
10392
|
+
for (const v of rawSizeTotals) if (Math.abs(v) > sizeMax) sizeMax = Math.abs(v);
|
|
10393
|
+
|
|
10394
|
+
this._aggregateStatsCache = { zoom, outlier, normalize, breaks, sizeMax };
|
|
9826
10395
|
return this._aggregateStatsCache;
|
|
9827
10396
|
}
|
|
9828
10397
|
|
|
@@ -9846,6 +10415,12 @@
|
|
|
9846
10415
|
// _computeAggregatedItems below — a plain (non-AGGREGATE)
|
|
9847
10416
|
// BUBBLE/CHART theme never reads them.
|
|
9848
10417
|
if (this.flags.has('AGGREGATE')) this._ensureClusterIndices(zoom);
|
|
10418
|
+
// AGGREGATE sums without a normalsizevalue: flat's default is the
|
|
10419
|
+
// largest aggregated value (defaultNormalSizeValue leaves it unset, the
|
|
10420
|
+
// sizes depend on the aggregation)
|
|
10421
|
+
if (this.flags.has('AGGREGATE') && !this.flags.has('CATEGORICAL') && !styleNum(this.style.normalsizevalue) && !this.binding.size) {
|
|
10422
|
+
this._maxSizeValue = this._ensureAggregateStats(zoom).sizeMax || undefined;
|
|
10423
|
+
}
|
|
9849
10424
|
let { individual, groups } = this._computeAggregatedItems(bbox, zoom);
|
|
9850
10425
|
// Far-hemisphere cull under globe projection — see
|
|
9851
10426
|
// isOnVisibleHemisphere's own comment for why this is needed at
|
|
@@ -9862,7 +10437,7 @@
|
|
|
9862
10437
|
}
|
|
9863
10438
|
const layers = [];
|
|
9864
10439
|
|
|
9865
|
-
if (this.flags.has('NOOUTLIER')
|
|
10440
|
+
if (this.flags.has('NOOUTLIER')|| this.flags.has('NORMALIZE') || this._rangeClassed) {
|
|
9866
10441
|
const stats = this._ensureAggregateStats(zoom);
|
|
9867
10442
|
|
|
9868
10443
|
// the quantity NOOUTLIER/NORMALIZE/classes act on: the separate
|
|
@@ -9973,6 +10548,10 @@
|
|
|
9973
10548
|
// _buildTooltipContext's own `isGroup` check already uses).
|
|
9974
10549
|
const sizeValueOf = d => d.properties.counts ? d.properties.total : d.properties.value;
|
|
9975
10550
|
const combined = individual.concat(groups).sort((a, b) => sizeValueOf(a) - sizeValueOf(b));
|
|
10551
|
+
// maxcharts: like the flat engine (maptheme.js nMaxCharts), draw only the
|
|
10552
|
+
// biggest N items — combined is ascending, so drop from the front
|
|
10553
|
+
const maxCharts = Math.round(styleNum(this.style.maxcharts)) || 0;
|
|
10554
|
+
if (maxCharts > 0 && combined.length > maxCharts) combined.splice(0, combined.length - maxCharts);
|
|
9976
10555
|
if (this.flags.has('USER')) {
|
|
9977
10556
|
const userLayers = this._buildUserChartLayers(combined, liveZoom, sizeValueOf);
|
|
9978
10557
|
if (userLayers) return layers.concat(userLayers);
|
|
@@ -9995,7 +10574,16 @@
|
|
|
9995
10574
|
const itemRgb = d => this.categoryColorsRgb[d.properties.counts ? dominant(d.properties.counts) : d.properties.cat] || [128, 128, 128];
|
|
9996
10575
|
const radiusOf = d => valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue);
|
|
9997
10576
|
// align / offsetx / offsety: the whole chart moves (flatChartAlignOffset)
|
|
9998
|
-
|
|
10577
|
+
// MULTIQUAD / MULTISQUARE: the items at one position side by side
|
|
10578
|
+
// (multiQuadOffsets), added to the chart position
|
|
10579
|
+
const baseAlign = this._chartAlignFn(liveZoom, radiusOf);
|
|
10580
|
+
const multi = flatFlag(this.flags, 'MULTIQUAD') || flatFlag(this.flags, 'MULTISQUARE')
|
|
10581
|
+
? multiQuadOffsets(combined, NORMAL_RADIUS_PX * objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1),
|
|
10582
|
+
styleNum(this.style.rangescale) || 1, styleNum(this.style.gridx) || 0)
|
|
10583
|
+
: null;
|
|
10584
|
+
const alignOf = multi
|
|
10585
|
+
? Object.assign(d => { const a = baseAlign(d), m = multi.offset.get(d) || [0, 0]; return [a[0] + m[0], a[1] + m[1]]; }, { active: true })
|
|
10586
|
+
: baseAlign;
|
|
9999
10587
|
|
|
10000
10588
|
// SHADOW (style.shadow, flat's gate: maxshadow, shadowupper/lower — flatShadowOn): a soft
|
|
10001
10589
|
// drop shadow under each symbol, see _getShadowIcon
|
|
@@ -10077,30 +10665,42 @@
|
|
|
10077
10665
|
// (#888888) over a half-opaque white halo (stroke font/7); its first
|
|
10078
10666
|
// baseline one font below the box (BOTTOMTITLE) or 0.7 font above it.
|
|
10079
10667
|
// Box and title are removed outside boxupper/boxlower.
|
|
10668
|
+
// Flat draws box and title in the chart group, which is scaled by
|
|
10669
|
+
// style.scale: the units below are u · scale. Its size measure W is
|
|
10670
|
+
// the chart's width (2r; with MULTIQUAD the width of the whole grid
|
|
10671
|
+
// at the position, the box enclosing all of it); flat's chart size
|
|
10672
|
+
// is 30 units.
|
|
10080
10673
|
const boxShown = flatFlag(this.flags, 'BOX') && !boxHiddenByScale(this.style, liveZoom);
|
|
10081
10674
|
const circular = flatFlag(this.flags, 'CIRCULAR');
|
|
10082
|
-
const unitPx = objectZoomFactor(liveZoom, this.mapOptions);
|
|
10675
|
+
const unitPx = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1);
|
|
10083
10676
|
const boxMarginU = styleNum(this.style.boxmargin) || 2;
|
|
10084
10677
|
const titleShown = boxShown && flatFlag(this.flags, 'TITLE') && !!this.binding.title;
|
|
10085
10678
|
const titleFontPx = 5 * unitPx * (styleNum(this.style.textscale) || styleNum(this.style.valuescale) || 1);
|
|
10679
|
+
// flat: fill-opacity (boxopacity || 1) on the raw style value — the
|
|
10680
|
+
// usual string "0" is transparent; typed here, a given 0 is too
|
|
10681
|
+
const boxOpacity = isNaN(styleNum(this.style.boxopacity)) ? 1 : styleNum(this.style.boxopacity);
|
|
10086
10682
|
const bottomTitle = flatFlag(this.flags, 'BOTTOMTITLE');
|
|
10087
10683
|
const alignRight = /right/.test(String(this.style.align || ''));
|
|
10088
|
-
|
|
10684
|
+
// with MULTIQUAD only the first item of a position gets its box/title
|
|
10685
|
+
const boxLayout = boxShown ? combined.filter(d => !multi || multi.index.get(d) === 0).map(d => {
|
|
10089
10686
|
const r = valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue);
|
|
10090
|
-
const
|
|
10687
|
+
const ext = multi ? multi.extent.get(d) : [0, 0, 0, 0];
|
|
10688
|
+
const chart = [ext[0] - r, ext[1] - r, ext[2] + r, ext[3] + r];
|
|
10689
|
+
const w = chart[2] - chart[0];
|
|
10690
|
+
const margin = Math.min(2 * boxMarginU * unitPx, boxMarginU * w / 30);
|
|
10091
10691
|
const boxR = circular ? r * 1.1 + margin : r;
|
|
10092
10692
|
let title = null;
|
|
10093
10693
|
if (titleShown) {
|
|
10094
|
-
const
|
|
10095
|
-
const n = groupRecordCount(d.properties);
|
|
10096
|
-
const text = n > 3 ? '(' + n + ')' : (raw && raw[this.binding.title] != null ? String(raw[this.binding.title]) : '');
|
|
10694
|
+
const text = this._itemTitle(d.properties);
|
|
10097
10695
|
if (text) {
|
|
10098
10696
|
const f = titleFontPx;
|
|
10099
|
-
const lines = wrapTitleLines(text, f, boxR * 2).split('\n');
|
|
10697
|
+
const lines = wrapTitleLines(text, f, circular ? boxR * 2 : w).split('\n');
|
|
10100
10698
|
const width = Math.max(...lines.map(l => titleTextWidth(l, f)));
|
|
10101
10699
|
// first baseline; lines go down one font each
|
|
10102
|
-
const base =
|
|
10103
|
-
|
|
10700
|
+
const base = circular
|
|
10701
|
+
? (bottomTitle ? boxR + f : -boxR - 0.7 * f - (lines.length - 1) * f)
|
|
10702
|
+
: (bottomTitle ? chart[3] + f : chart[1] - 0.7 * f - (lines.length - 1) * f);
|
|
10703
|
+
const x = circular ? unitPx / 2 : (alignRight ? chart[2] : chart[0]);
|
|
10104
10704
|
const anchor = circular ? 'middle' : (alignRight ? 'end' : 'start');
|
|
10105
10705
|
const x0 = anchor === 'middle' ? x - width / 2 : anchor === 'end' ? x - width : x;
|
|
10106
10706
|
// arial: ascent 0.905, descent 0.212 of the font size
|
|
@@ -10110,16 +10710,16 @@
|
|
|
10110
10710
|
}
|
|
10111
10711
|
let rect = null;
|
|
10112
10712
|
if (!circular) {
|
|
10113
|
-
const e =
|
|
10713
|
+
const e = chart.slice();
|
|
10114
10714
|
// with a title flat trims 0.1 font off the top of chart + title
|
|
10115
10715
|
if (title) { e[0] = Math.min(e[0], title.extent[0]); e[1] = Math.min(e[1], title.extent[1]) + 0.1 * titleFontPx; e[2] = Math.max(e[2], title.extent[2]); e[3] = Math.max(e[3], title.extent[3]); }
|
|
10116
10716
|
rect = [e[0] - margin, e[1] - margin, e[2] + margin, e[3] + margin];
|
|
10117
10717
|
}
|
|
10118
|
-
return { d, r, boxR, rect, title };
|
|
10718
|
+
return { d, r, w, boxR, rect, title };
|
|
10119
10719
|
}) : null;
|
|
10120
10720
|
if (boxShown && circular) {
|
|
10121
10721
|
const boxRgb = hexOrNamedToRgb(this.style.boxcolor || '#eeeeee');
|
|
10122
|
-
const boxAlpha = Math.round(255 *
|
|
10722
|
+
const boxAlpha = Math.round(255 * boxOpacity);
|
|
10123
10723
|
const borderRgb = hexOrNamedToRgb(this.style.bordercolor || '#dddddd');
|
|
10124
10724
|
// ScatterplotLayer has no pixel offset: a moved chart (align,
|
|
10125
10725
|
// offsetx/offsety) gets its circle as an icon
|
|
@@ -10152,20 +10752,47 @@
|
|
|
10152
10752
|
const borderStyle = String(this.style.borderstyle || '');
|
|
10153
10753
|
const boxLook = {
|
|
10154
10754
|
fill: this.style.boxcolor || 'white',
|
|
10155
|
-
fillOpacity:
|
|
10755
|
+
fillOpacity: boxOpacity,
|
|
10156
10756
|
stroke: borderStyle === 'none' ? null : styleLineColor(this.style.bordercolor) || '#bbbbbb',
|
|
10157
10757
|
strokeOpacity: /^(solid|dotted|dashed)$/.test(borderStyle) ? 1 : 0.3,
|
|
10158
10758
|
};
|
|
10159
10759
|
const borderWidth = styleNum(this.style.borderwidth) || 1;
|
|
10160
10760
|
const borderRadius = styleNum(this.style.borderradius);
|
|
10161
|
-
|
|
10761
|
+
const strokeOf = b => borderWidth * 0.2 * b.w / 30;
|
|
10762
|
+
// a box larger than BOX_ICON_MAX_PX (a MULTIQUAD grid of thousands
|
|
10763
|
+
// of items) would need a huge canvas: it is drawn as a polygon in
|
|
10764
|
+
// map coordinates instead (square corners)
|
|
10765
|
+
const bigBox = b => Math.max(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1]) > BOX_ICON_MAX_PX;
|
|
10766
|
+
const iconBoxes = boxLayout.filter(b => !bigBox(b)), polygonBoxes = boxLayout.filter(bigBox);
|
|
10767
|
+
iconBoxes.forEach(b => {
|
|
10162
10768
|
b.icon = this._buildBoxIcon(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1],
|
|
10163
|
-
isNaN(borderRadius) ? 0 : borderRadius * Math.min(unitPx, b.
|
|
10164
|
-
|
|
10769
|
+
isNaN(borderRadius) ? 0 : borderRadius * Math.min(unitPx, b.w / 30),
|
|
10770
|
+
strokeOf(b), boxLook);
|
|
10165
10771
|
});
|
|
10166
|
-
|
|
10772
|
+
if (polygonBoxes.length) {
|
|
10773
|
+
const fillRgb = hexOrNamedToRgb(boxLook.fill);
|
|
10774
|
+
const strokeRgb = boxLook.stroke ? hexOrNamedToRgb(boxLook.stroke) : null;
|
|
10775
|
+
layers.push(new GeoJsonLayer({
|
|
10776
|
+
id: `ix-box-polygons-${this.name}`,
|
|
10777
|
+
data: polygonBoxes.map(b => {
|
|
10778
|
+
const o = alignOf(b.d), c = b.d.geometry.coordinates, r = b.rect;
|
|
10779
|
+
const at = (x, y) => pixelOffsetLngLat(c, [x + o[0], y + o[1]], liveZoom);
|
|
10780
|
+
return { type: 'Feature', properties: { stroke: strokeOf(b) },
|
|
10781
|
+
geometry: { type: 'Polygon', coordinates: [[at(r[0], r[1]), at(r[2], r[1]), at(r[2], r[3]), at(r[0], r[3]), at(r[0], r[1])]] } };
|
|
10782
|
+
}),
|
|
10783
|
+
pickable: false,
|
|
10784
|
+
filled: boxLook.fillOpacity > 0,
|
|
10785
|
+
stroked: !!strokeRgb,
|
|
10786
|
+
getFillColor: [...fillRgb.slice(0, 3), Math.round(255 * boxLook.fillOpacity)],
|
|
10787
|
+
getLineColor: strokeRgb ? [...strokeRgb.slice(0, 3), Math.round(255 * boxLook.strokeOpacity)] : [0, 0, 0, 0],
|
|
10788
|
+
getLineWidth: f => f.properties.stroke,
|
|
10789
|
+
lineWidthUnits: 'pixels',
|
|
10790
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
10791
|
+
}));
|
|
10792
|
+
}
|
|
10793
|
+
if (iconBoxes.length) layers.push(new IconLayer({
|
|
10167
10794
|
id: `ix-box-${this.name}-g${this._iconGeneration}`,
|
|
10168
|
-
data:
|
|
10795
|
+
data: iconBoxes, pickable: false,
|
|
10169
10796
|
getPosition: b => b.d.geometry.coordinates,
|
|
10170
10797
|
getIcon: b => b.icon,
|
|
10171
10798
|
// the icon is drawn at 2× with a stroke padding around the box
|
|
@@ -10185,7 +10812,9 @@
|
|
|
10185
10812
|
? this._buildBubbleIcon(d.properties.counts, this.categoryColorsRgb, this._groupIsolation(d))
|
|
10186
10813
|
: this._buildSingleIcon(this.categoryColorsRgb[d.properties.cat], fillOpacity, this._resolveSymbolShape(d.properties), singleBorderColorRgb, singleBorderWidthPx),
|
|
10187
10814
|
getSize: d => valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue) * 2,
|
|
10188
|
-
|
|
10815
|
+
// group icons bake a fixed 0.9 alpha; an explicit fillopacity scales it
|
|
10816
|
+
// so a near-transparent theme doesn't show opaque 1-px-cell clusters
|
|
10817
|
+
getColor: d => [255, 255, 255, Math.round(this._iconAlpha(d) * (d.properties.counts && this.style.fillopacity != null ? fillOpacity : 1))],
|
|
10189
10818
|
...(alignOf.active ? { getPixelOffset: alignOf } : {}),
|
|
10190
10819
|
sizeUnits: 'pixels',
|
|
10191
10820
|
billboard: true,
|
|
@@ -10750,6 +11379,7 @@
|
|
|
10750
11379
|
// - themes: getThemeObj (flat's theme object, with its data table and
|
|
10751
11380
|
// the items on the map), getThemeDefinitionObj, getThemes,
|
|
10752
11381
|
// changeThemeStyle / removeTheme / setBasemapOpacity (map name first),
|
|
11382
|
+
// setThemeTimeFrame (themeId, min, max),
|
|
10753
11383
|
// refreshTheme, markThemeClass / unmarkThemeClass
|
|
10754
11384
|
// - data: ixmaps.data (facets: getFacets, showFacets, …), the
|
|
10755
11385
|
// embeddedSVG.window tables (setExternalData is with the data loading)
|
|
@@ -10761,6 +11391,29 @@
|
|
|
10761
11391
|
|
|
10762
11392
|
// flat's page hook ixmaps.htmlgui_onNewTheme(szId), called when a theme
|
|
10763
11393
|
// is created (maptheme.js 927) — with the theme's id (its name)
|
|
11394
|
+
// flat's tooltip for a theme without its own template (ui/js/tools/
|
|
11395
|
+
// tooltip_mustache.js 394)
|
|
11396
|
+
// tooltip_mustache.js 394) — the same parts, structured: the theme title
|
|
11397
|
+
// as h2, the item title as h3, the chart (tooltipTable) below
|
|
11398
|
+
// (inline styles throughout: a page's own h2 / table / td rules must not
|
|
11399
|
+
// restyle the tooltip)
|
|
11400
|
+
const FLAT_DEFAULT_TOOLTIP = "<h2 style='margin:0 0 0.15em;padding:0;font-size:1.25em;font-weight:600;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis'>{{theme.title}}</h2>"
|
|
11401
|
+
+ "{{#theme.item.title}}<h3 style='margin:0 0 0.4em;padding:0;font-size:1.1em;font-weight:600;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis'>{{theme.item.title}}</h3>{{/theme.item.title}}"
|
|
11402
|
+
+ "{{theme.item.chart}}";
|
|
11403
|
+
// {{theme.item.chart}} as a table: per row a color swatch (or none), the
|
|
11404
|
+
// label (left, one line) and the value (right-aligned)
|
|
11405
|
+
function tooltipTable(rows) {
|
|
11406
|
+
if (!rows.length) return '';
|
|
11407
|
+
const swatch = rgb => (rgb ? `<span style="display:inline-block;width:0.75em;height:0.75em;border-radius:50%;background:rgb(${rgb.slice(0, 3).join(',')})"></span>` : '');
|
|
11408
|
+
const td = 'border:none;background:transparent;font:inherit;color:inherit;line-height:1.15;';
|
|
11409
|
+
return '<table style="border-collapse:collapse;border:none;background:transparent;margin:0.2em 0;font-size:1em;line-height:1.15">'
|
|
11410
|
+
+ rows.map(r => '<tr>'
|
|
11411
|
+
+ `<td style="${td}padding:0.05em 0.4em 0.05em 0;vertical-align:middle">${swatch(r.rgb)}</td>`
|
|
11412
|
+
// one line: a long label ends in an ellipsis (full text as its title)
|
|
11413
|
+
+ `<td style="${td}padding:0.05em 0.8em 0.05em 0;text-align:left"><span title="${escapeHtml(String(r.label).replace(/<[^>]*>/g, ''))}" style="display:inline-block;max-width:13em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;vertical-align:bottom">${r.label}</span></td>`
|
|
11414
|
+
+ `<td style="${td}padding:0.05em 0;text-align:right;white-space:nowrap">${r.value}</td></tr>`).join('')
|
|
11415
|
+
+ '</table>';
|
|
11416
|
+
}
|
|
10764
11417
|
function themeIdOf(rt) {
|
|
10765
11418
|
return (rt.style && rt.style.name) || (rt.meta && rt.meta.name) || rt.name;
|
|
10766
11419
|
}
|
|
@@ -10774,7 +11427,7 @@
|
|
|
10774
11427
|
// htmlgui_flat.js). Its methods act on the last built map; called while
|
|
10775
11428
|
// the map is still building, they run once it is ready.
|
|
10776
11429
|
const MAP_HANDLE_METHODS = ['replace', 'add', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
10777
|
-
'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible'];
|
|
11430
|
+
'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible', 'setThemeTimeFrame', 'attribution'];
|
|
10778
11431
|
const _mapHandle = {};
|
|
10779
11432
|
for (const m of MAP_HANDLE_METHODS) {
|
|
10780
11433
|
_mapHandle[m] = (...args) => {
|
|
@@ -11001,6 +11654,7 @@
|
|
|
11001
11654
|
const style = Object.assign({}, rt.style);
|
|
11002
11655
|
if (rt.binding.size && !style.sizefield) style.sizefield = rt.binding.size;
|
|
11003
11656
|
if (rt.binding.title && !style.titlefield) style.titlefield = rt.binding.title;
|
|
11657
|
+
if (rt.binding.time && !style.timefield) style.timefield = rt.binding.time;
|
|
11004
11658
|
style.type = rt.flags.typeString != null ? rt.flags.typeString : [...rt.flags].join('|');
|
|
11005
11659
|
const data = Object.assign({}, rt._definition && rt._definition.data);
|
|
11006
11660
|
if (!data.name) data.name = themeDataName(rt);
|
|
@@ -11360,7 +12014,7 @@
|
|
|
11360
12014
|
}
|
|
11361
12015
|
|
|
11362
12016
|
global.ixmaps = {
|
|
11363
|
-
layer, Map: createMap, setExternalData: setExternalDataBridge, getThemeObj, data: ixmapsData,
|
|
12017
|
+
layer, Layer: layer, Map: createMap, setExternalData: setExternalDataBridge, getThemeObj, data: ixmapsData,
|
|
11364
12018
|
szResourceBase: ixmapsSzResourceBase, getProjectString, setProjectJSON, loadProject,
|
|
11365
12019
|
markThemeClass, unmarkThemeClass, getBoundingBox, setTitle, setTitleBox, refreshTheme, formatValue: flatFormatValue,
|
|
11366
12020
|
map: mapHandle, getZoom, getThemeDefinitionObj, embeddedSVG,
|
|
@@ -11375,8 +12029,14 @@
|
|
|
11375
12029
|
// handle's methods on the last built map
|
|
11376
12030
|
getCenter: () => (_lastMapApi && _lastMapApi.map ? _lastMapApi.map.getCenter() : null),
|
|
11377
12031
|
getMapTypeId: () => (_lastMapApi && _lastMapApi.getMapTypeId ? _lastMapApi.getMapTypeId() : ''),
|
|
12032
|
+
// flat's attribution: ixmaps.setAttribution(text) and its htmlgui_ pair
|
|
12033
|
+
setAttribution: (text) => { if (_lastMapApi && _lastMapApi.setAttribution) _lastMapApi.setAttribution(text); },
|
|
12034
|
+
htmlgui_setAttributionString: (text) => { if (_lastMapApi && _lastMapApi.setAttribution) _lastMapApi.setAttribution(text); },
|
|
12035
|
+
htmlgui_getAttributionString: () => (_lastMapApi && _lastMapApi.getAttribution ? _lastMapApi.getAttribution() : ''),
|
|
11378
12036
|
changeThemeStyle: (szMap, szId, szStyle, szFlag) => _mapHandle.changeThemeStyle(szId, szStyle, szFlag),
|
|
11379
12037
|
removeTheme: (szMap, szId) => _mapHandle.remove(szId),
|
|
12038
|
+
// flat's time slider: show the records of a theme (null = all themes) whose timefield lies in [min, max)
|
|
12039
|
+
setThemeTimeFrame: (szId, nMin, nMax) => _mapHandle.setThemeTimeFrame(szId, nMin, nMax),
|
|
11380
12040
|
setBasemapOpacity: (szMap, nOpacity, szMode) => _mapHandle.setBasemapOpacity(nOpacity, szMode),
|
|
11381
12041
|
// this engine's version (flat's own ixmaps.version numbers flat)
|
|
11382
12042
|
glVersion: IXMAPS_GL_VERSION,
|
|
@@ -11401,7 +12061,7 @@
|
|
|
11401
12061
|
// test-only: lets test/unit/*.test.mjs call pure internals directly (the
|
|
11402
12062
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
11403
12063
|
global.__ixmapsGlInternals = {
|
|
11404
|
-
IXMAPS_GL_VERSION, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout,
|
|
12064
|
+
IXMAPS_GL_VERSION, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout, multiQuadOffsets, pixelOffsetLngLat,
|
|
11405
12065
|
normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, typeStyleNumbers, styleNum,
|
|
11406
12066
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
11407
12067
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|