ixmaps-gl 0.2.30 → 0.2.33
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 +39 -7
- package/ixmaps-gl.js +1129 -128
- package/package.json +1 -1
package/ixmaps-gl.js
CHANGED
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
// ---------------------------------------------------------------
|
|
101
101
|
// this engine's release (= package.json "version", checked by
|
|
102
102
|
// test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
|
|
103
|
-
const IXMAPS_GL_VERSION = '0.2.
|
|
103
|
+
const IXMAPS_GL_VERSION = '0.2.33';
|
|
104
104
|
|
|
105
105
|
const LIB_URLS = {
|
|
106
106
|
// Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
|
|
@@ -135,6 +135,11 @@
|
|
|
135
135
|
// supplied by MapLibre GL JS itself, nothing this engine manages).
|
|
136
136
|
deck: 'https://unpkg.com/deck.gl@9.4.0/dist.min.js',
|
|
137
137
|
mustache: 'https://unpkg.com/mustache@4.2.0/mustache.min.js',
|
|
138
|
+
// .type("KDE") draws its bands with d3-contour; loaded on the first KDE theme only, and evaluated
|
|
139
|
+
// privately (ensureD3Contour) — never as a script tag: d3-array would overwrite the functions of
|
|
140
|
+
// a d3 v3 the page may already have (flat's user charts load d3.v3)
|
|
141
|
+
d3Array: 'https://cdn.jsdelivr.net/npm/d3-array@3.2.4/dist/d3-array.min.js',
|
|
142
|
+
d3Contour: 'https://cdn.jsdelivr.net/npm/d3-contour@4.0.2/dist/d3-contour.min.js',
|
|
138
143
|
// loaded by default, as flat does (its resource list loads
|
|
139
144
|
// ../data.js/data.js — from its CDN build, this URL); every data format
|
|
140
145
|
// except GeoJSON/TopoJSON is read by it, see fetchLayerData
|
|
@@ -217,7 +222,7 @@
|
|
|
217
222
|
// byte-grep of the shipped file (deck.gl's own @deck.gl/maplibre
|
|
218
223
|
// overview page claims ES-modules-only, which the actual UMD bundle
|
|
219
224
|
// contradicts).
|
|
220
|
-
let IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer, PathLayer, PathStyleExtension;
|
|
225
|
+
let IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer, PathLayer, PathStyleExtension, HeatmapLayer;
|
|
221
226
|
// PieSliceLayer (see makePieSliceLayer) and BarShapeLayer
|
|
222
227
|
// (makeBarShapeLayer) are derived from ScatterplotLayer once deck.gl is there
|
|
223
228
|
let PieSliceLayer, BarShapeLayer, FlowPathLayer;
|
|
@@ -608,7 +613,20 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
608
613
|
global.Mustache ? Promise.resolve() : loadScript(LIB_URLS.mustache),
|
|
609
614
|
[...document.styleSheets].some(s => s.href === LIB_URLS.maplibreCss) ? Promise.resolve() : loadStylesheet(LIB_URLS.maplibreCss)
|
|
610
615
|
]).then(() => {
|
|
611
|
-
({ IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer, PathLayer, PathStyleExtension } = global.deck);
|
|
616
|
+
({ IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer, PathLayer, PathStyleExtension, HeatmapLayer } = global.deck);
|
|
617
|
+
// deck.gl 9 gamma-encodes the layer-level `opacity` prop in its `layer` shader module
|
|
618
|
+
// (Math.pow(opacity, 1/2.2); deck 8 and flat's SVG fill-opacity are linear), so a declared
|
|
619
|
+
// fillopacity 0.2 would draw as 0.48. Hand deck opacity ** 2.2: it draws exactly the declared alpha.
|
|
620
|
+
// Alpha inside a color array (getFillColor [r,g,b,a]) is linear and stays as it is.
|
|
621
|
+
const linearOpacity = Cls => new Proxy(Cls, {
|
|
622
|
+
construct(target, args, newTarget) {
|
|
623
|
+
const fixed = args.map(p => (p && typeof p === 'object' && typeof p.opacity === 'number' && p.opacity >= 0 && p.opacity < 1)
|
|
624
|
+
? Object.assign({}, p, { opacity: Math.pow(p.opacity, 2.2) }) : p);
|
|
625
|
+
return Reflect.construct(target, fixed, newTarget);
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
[IconLayer, ScatterplotLayer, GeoJsonLayer, TextLayer, PathLayer, HeatmapLayer] =
|
|
629
|
+
[IconLayer, ScatterplotLayer, GeoJsonLayer, TextLayer, PathLayer, HeatmapLayer].map(linearOpacity);
|
|
612
630
|
PieSliceLayer = makePieSliceLayer(ScatterplotLayer);
|
|
613
631
|
BarShapeLayer = makeBarShapeLayer(ScatterplotLayer);
|
|
614
632
|
FlowPathLayer = makeFlowPathLayer(PathLayer);
|
|
@@ -1856,12 +1874,31 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1856
1874
|
})(g && g.coordinates);
|
|
1857
1875
|
return minX <= maxX ? [(minX + maxX) / 2, (minY + maxY) / 2] : null;
|
|
1858
1876
|
}
|
|
1877
|
+
// flat finds a geometry id field whatever its letter case (the comuni topojson has "pro_com_t", the page
|
|
1878
|
+
// binds id "pro_com_T"): the property name of a FEATURE runtime's shapes that matches the binding, else
|
|
1879
|
+
// the binding as written. Cached per runtime and name
|
|
1880
|
+
function geometryIdField(rt, name) {
|
|
1881
|
+
if (!name || !rt) return name;
|
|
1882
|
+
const cache = rt._idFieldCache || (rt._idFieldCache = {});
|
|
1883
|
+
if (cache[name] !== undefined) return cache[name];
|
|
1884
|
+
const feats = rt.features || [];
|
|
1885
|
+
let found = name;
|
|
1886
|
+
const has = f => f.properties && Object.prototype.hasOwnProperty.call(f.properties, name);
|
|
1887
|
+
if (!feats.slice(0, 50).some(has)) {
|
|
1888
|
+
const low = String(name).toLowerCase();
|
|
1889
|
+
for (const f of feats.slice(0, 50)) {
|
|
1890
|
+
const k = f.properties && Object.keys(f.properties).find(key => key.toLowerCase() === low);
|
|
1891
|
+
if (k) { found = k; break; }
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
return (cache[name] = found);
|
|
1895
|
+
}
|
|
1859
1896
|
// id → position of one FEATURE runtime's shapes (flat's getNodePosition,
|
|
1860
1897
|
// maptheme.js 24702-24784): its shape (flatShapeWins) at flat's shape
|
|
1861
1898
|
// center (flatShapeCenter)
|
|
1862
1899
|
function featurePositionIndex(rt) {
|
|
1863
1900
|
if (rt._positionIndex && rt._positionIndexOf === rt.features) return rt._positionIndex;
|
|
1864
|
-
const idField = rt.binding && rt.binding.id;
|
|
1901
|
+
const idField = geometryIdField(rt, rt.binding && rt.binding.id);
|
|
1865
1902
|
const best = new Map();
|
|
1866
1903
|
if (idField) {
|
|
1867
1904
|
for (const f of rt.features) {
|
|
@@ -1942,7 +1979,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1942
1979
|
function flatShapeAreaKm2(featureRt, f) {
|
|
1943
1980
|
const sizeField = featureRt.binding && featureRt.binding.size;
|
|
1944
1981
|
if (sizeField) {
|
|
1945
|
-
const v =
|
|
1982
|
+
const v = scanValue(f.properties && f.properties[sizeField]);
|
|
1946
1983
|
return isFinite(v) ? v / 1000000 : NaN;
|
|
1947
1984
|
}
|
|
1948
1985
|
return geodesicPolygonAreaKm2(f.geometry);
|
|
@@ -2001,6 +2038,415 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2001
2038
|
return isFinite(o) ? Math.max(0, Math.min(1, o)) : 0;
|
|
2002
2039
|
}
|
|
2003
2040
|
|
|
2041
|
+
// flat's CHART|SYMBOL DOPACITY (maptheme.js 22142-22145): (v-min)^(1/pow) /
|
|
2042
|
+
// (max-min)^(1/pow) · fillopacity(|| 1) · dopacityscale, clamped to 0..1 —
|
|
2043
|
+
// min/max are the theme-wide extremes of the cell values, one pair for all
|
|
2044
|
+
// cells (not the CHOROPLETH|DOPACITY median ramp, not a per-class max)
|
|
2045
|
+
function symbolDopacityAlpha(style, v, min, max, fillopacity) {
|
|
2046
|
+
const pow = 1 / (styleNum(style.dopacitypow) || 1);
|
|
2047
|
+
const scale = styleNum(style.dopacityscale) || 1;
|
|
2048
|
+
const fo = fillopacity || 1;
|
|
2049
|
+
const o = Math.pow(v - min, pow) / Math.pow(max - min, pow) * fo * scale;
|
|
2050
|
+
return isFinite(o) ? Math.max(0, Math.min(1, o)) : fo;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
// .type("KDE") — kernel density estimate as stacked bands (the fire pages' KDE, done in the
|
|
2054
|
+
// engine). The pure parts: bandwidth by zoom, the points, the density grid, the bands.
|
|
2055
|
+
//
|
|
2056
|
+
// The value of a grid cell is what the pages computed by brute force — the sum over the points
|
|
2057
|
+
// of weight · exp(−d² / 2h²), d the distance in km, h the bandwidth — but here by BINNING each
|
|
2058
|
+
// weight into its four nearest cells (bilinear) and blurring the grid with a separable Gaussian
|
|
2059
|
+
// (σ = h in cells): O(points + cells · kernel) instead of O(cells · points). Thresholds are
|
|
2060
|
+
// fractions of the maximum, so only the shape of the density matters, not its absolute scale.
|
|
2061
|
+
|
|
2062
|
+
// flat zoom → bandwidth (km) and the cell budget of the grid, as the fire pages chose them
|
|
2063
|
+
const KDE_ZOOM_TABLE = [ // [from flat zoom, bandwidth km, max cells]
|
|
2064
|
+
[16, 0.04, 1000000], [15, 0.08, 800000], [14, 0.12, 600000], [13, 0.30, 300000],
|
|
2065
|
+
[12, 0.80, 100000], [11, 2.00, 40000], [10, 5.00, 25000], [9, 10.0, 20000]
|
|
2066
|
+
];
|
|
2067
|
+
const KDE_FAR_ZOOM = [24.0, 15000];
|
|
2068
|
+
const KDE_DEFAULT_LEVELS = [0.15, 0.30, 0.50, 0.70];
|
|
2069
|
+
const KDE_MIN_POINTS = 5; // fewer points have no density to speak of (the pages' rule)
|
|
2070
|
+
const KDE_MARGIN_SIGMAS = 3; // the grid reaches 3 bandwidths beyond the outermost points
|
|
2071
|
+
const KDE_KM_PER_DEG = 111.3; // the pages' constant
|
|
2072
|
+
|
|
2073
|
+
function kdeZoomRow(flatZoom) {
|
|
2074
|
+
const z = Number(flatZoom);
|
|
2075
|
+
const row = KDE_ZOOM_TABLE.find(r => z >= r[0]);
|
|
2076
|
+
return row ? [row[1], row[2]] : KDE_FAR_ZOOM;
|
|
2077
|
+
}
|
|
2078
|
+
function kdeBandwidthKm(flatZoom) { return kdeZoomRow(flatZoom)[0]; }
|
|
2079
|
+
function kdeMaxCells(flatZoom) { return kdeZoomRow(flatZoom)[1]; }
|
|
2080
|
+
|
|
2081
|
+
// style.kdelevels: the threshold fractions of the maximum, as an array or "0.2,0.5" — numbers in
|
|
2082
|
+
// (0, 1], ascending, each once; anything unusable gives the default
|
|
2083
|
+
function kdeLevels(style) {
|
|
2084
|
+
let v = style && style.kdelevels;
|
|
2085
|
+
if (typeof v === 'string') v = v.split(/[,\s|]+/).filter(Boolean);
|
|
2086
|
+
if (!Array.isArray(v)) return KDE_DEFAULT_LEVELS.slice();
|
|
2087
|
+
const out = [...new Set(v.map(Number).filter(x => isFinite(x) && x > 0 && x <= 1))].sort((a, b) => a - b);
|
|
2088
|
+
return out.length ? out : KDE_DEFAULT_LEVELS.slice();
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
// the records' positions and weights as one Float64Array [lng, lat, weight, …] (cheap to hand to
|
|
2092
|
+
// a worker); `bbox` [west, south, east, north] keeps only what lies inside it
|
|
2093
|
+
function kdePoints(features, binding, bbox) {
|
|
2094
|
+
const out = [];
|
|
2095
|
+
for (const f of features) {
|
|
2096
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
2097
|
+
if (!c || typeof c[0] !== 'number' || !isFinite(c[0]) || !isFinite(c[1])) continue;
|
|
2098
|
+
if (bbox && (c[0] < bbox[0] || c[0] > bbox[2] || c[1] < bbox[1] || c[1] > bbox[3])) continue;
|
|
2099
|
+
const w = heatmapWeight(binding, f.properties || {});
|
|
2100
|
+
if (w > 0) out.push(c[0], c[1], w);
|
|
2101
|
+
}
|
|
2102
|
+
return Float64Array.from(out);
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
// The density grid of `points` ([lng, lat, w, …]) at bandwidth `bwKm`, on about `maxCells` cells
|
|
2106
|
+
// that are square in km. Returns { values (row-major, row j at latitude s + (j + 0.5)·stepLat),
|
|
2107
|
+
// nx, ny, w0, s, stepLng, stepLat, max }, or null for fewer than KDE_MIN_POINTS points. Cells
|
|
2108
|
+
// coarser than the bandwidth still conserve the weights (binning, not sampling).
|
|
2109
|
+
// SELF-CONTAINED on purpose: its source is handed to a Web Worker (Function.prototype.toString),
|
|
2110
|
+
// so it uses nothing from this file.
|
|
2111
|
+
function kdeGrid(points, bwKm, maxCells) {
|
|
2112
|
+
const n = points.length / 3;
|
|
2113
|
+
if (n < 5 || !(bwKm > 0)) return null;
|
|
2114
|
+
const KM = 111.3;
|
|
2115
|
+
let w0 = Infinity, e = -Infinity, s = Infinity, nn = -Infinity;
|
|
2116
|
+
for (let i = 0; i < points.length; i += 3) {
|
|
2117
|
+
const lng = points[i], lat = points[i + 1];
|
|
2118
|
+
if (lng < w0) w0 = lng; if (lng > e) e = lng;
|
|
2119
|
+
if (lat < s) s = lat; if (lat > nn) nn = lat;
|
|
2120
|
+
}
|
|
2121
|
+
const kmLng = Math.max(KM * Math.cos(((s + nn) / 2) * Math.PI / 180), 0.001);
|
|
2122
|
+
const margin = 3 * bwKm;
|
|
2123
|
+
s -= margin / KM; nn += margin / KM; w0 -= margin / kmLng; e += margin / kmLng;
|
|
2124
|
+
const wKm = (e - w0) * kmLng, hKm = (nn - s) * KM;
|
|
2125
|
+
const cell = Math.sqrt(wKm * hKm / Math.max(maxCells, 25));
|
|
2126
|
+
const nx = Math.max(5, Math.ceil(wKm / cell)), ny = Math.max(5, Math.ceil(hKm / cell));
|
|
2127
|
+
const stepLng = (e - w0) / nx, stepLat = (nn - s) / ny;
|
|
2128
|
+
|
|
2129
|
+
// bin: each weight into its four nearest cell centers (cell i is centered at (i + 0.5) · step)
|
|
2130
|
+
let a = new Float32Array(nx * ny);
|
|
2131
|
+
const put = (ix, iy, v) => {
|
|
2132
|
+
if (ix < 0) ix = 0; else if (ix > nx - 1) ix = nx - 1;
|
|
2133
|
+
if (iy < 0) iy = 0; else if (iy > ny - 1) iy = ny - 1;
|
|
2134
|
+
a[iy * nx + ix] += v;
|
|
2135
|
+
};
|
|
2136
|
+
for (let i = 0; i < points.length; i += 3) {
|
|
2137
|
+
const cx = (points[i] - w0) / stepLng - 0.5, cy = (points[i + 1] - s) / stepLat - 0.5;
|
|
2138
|
+
const ix = Math.floor(cx), iy = Math.floor(cy), fx = cx - ix, fy = cy - iy, w = points[i + 2];
|
|
2139
|
+
put(ix, iy, w * (1 - fx) * (1 - fy)); put(ix + 1, iy, w * fx * (1 - fy));
|
|
2140
|
+
put(ix, iy + 1, w * (1 - fx) * fy); put(ix + 1, iy + 1, w * fx * fy);
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
// separable Gaussian, peak 1 (σ in cells per axis, kernel to 3σ; no blur when σ is below a third of a cell)
|
|
2144
|
+
const kernel = sigma => {
|
|
2145
|
+
const r = sigma < 0.33 ? 0 : Math.ceil(3 * sigma);
|
|
2146
|
+
const k = new Float32Array(2 * r + 1);
|
|
2147
|
+
for (let d = -r; d <= r; d++) k[d + r] = Math.exp(-(d * d) / (2 * sigma * sigma));
|
|
2148
|
+
return { k, r };
|
|
2149
|
+
};
|
|
2150
|
+
const kx = kernel(bwKm / (stepLng * kmLng)), ky = kernel(bwKm / (stepLat * KM));
|
|
2151
|
+
let b = new Float32Array(nx * ny);
|
|
2152
|
+
for (let j = 0; j < ny; j++) {
|
|
2153
|
+
const row = j * nx;
|
|
2154
|
+
for (let i = 0; i < nx; i++) {
|
|
2155
|
+
let v = 0;
|
|
2156
|
+
const lo = Math.max(0, i - kx.r), hi = Math.min(nx - 1, i + kx.r);
|
|
2157
|
+
for (let t = lo; t <= hi; t++) v += a[row + t] * kx.k[t - i + kx.r];
|
|
2158
|
+
b[row + i] = v;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
a = new Float32Array(nx * ny);
|
|
2162
|
+
let max = 0;
|
|
2163
|
+
for (let j = 0; j < ny; j++) {
|
|
2164
|
+
const lo = Math.max(0, j - ky.r), hi = Math.min(ny - 1, j + ky.r);
|
|
2165
|
+
for (let i = 0; i < nx; i++) {
|
|
2166
|
+
let v = 0;
|
|
2167
|
+
for (let t = lo; t <= hi; t++) v += b[t * nx + i] * ky.k[t - j + ky.r];
|
|
2168
|
+
a[j * nx + i] = v;
|
|
2169
|
+
if (v > max) max = v;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
return { values: a, nx, ny, w0, s, stepLng, stepLat, max };
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
// The bands of a kdeGrid: one MultiPolygon feature per threshold (fraction · max), each the area
|
|
2176
|
+
// at or above it — so the bands stack, the highest on top — in lng/lat. `makeContours` is
|
|
2177
|
+
// d3.contours (d3-contour), handed in so this stays pure. d3 puts the value of cell (i, j) at
|
|
2178
|
+
// (i + 0.5, j + 0.5), the same convention as the grid, hence the plain scaling by the steps.
|
|
2179
|
+
function kdeBands(grid, fractions, makeContours) {
|
|
2180
|
+
const empty = { type: 'FeatureCollection', features: [] };
|
|
2181
|
+
if (!grid || !(grid.max > 0) || !fractions.length) return empty;
|
|
2182
|
+
const thresholds = fractions.map(f => f * grid.max);
|
|
2183
|
+
const bands = makeContours().size([grid.nx, grid.ny]).thresholds(thresholds)(grid.values);
|
|
2184
|
+
const features = [];
|
|
2185
|
+
bands.forEach((mp, k) => {
|
|
2186
|
+
if (!mp.coordinates || !mp.coordinates.length) return;
|
|
2187
|
+
features.push({
|
|
2188
|
+
type: 'Feature',
|
|
2189
|
+
properties: { level: k, fraction: fractions[k], value: mp.value },
|
|
2190
|
+
geometry: {
|
|
2191
|
+
type: 'MultiPolygon',
|
|
2192
|
+
coordinates: mp.coordinates.map(poly => poly.map(ring => ring.map(p => [grid.w0 + p[0] * grid.stepLng, grid.s + p[1] * grid.stepLat])))
|
|
2193
|
+
}
|
|
2194
|
+
});
|
|
2195
|
+
});
|
|
2196
|
+
return { type: 'FeatureCollection', features };
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// The worker's whole source: kdeGrid alone (it uses nothing else of this file), answering
|
|
2200
|
+
// {id, points, bwKm, maxCells} with {id, grid}, the grid's values handed back by transfer.
|
|
2201
|
+
function kdeWorkerSource() {
|
|
2202
|
+
return `const kdeGrid = ${kdeGrid.toString()};
|
|
2203
|
+
self.onmessage = e => {
|
|
2204
|
+
const { id, points, bwKm, maxCells } = e.data;
|
|
2205
|
+
const grid = kdeGrid(points, bwKm, maxCells);
|
|
2206
|
+
self.postMessage({ id, grid }, grid ? [grid.values.buffer] : []);
|
|
2207
|
+
};`;
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
// kdeGrid off the main thread: one shared Web Worker, built from kdeWorkerSource. Without a Worker
|
|
2211
|
+
// (a page that forbids blob workers, a test) or when it fails, the same function runs inline.
|
|
2212
|
+
let _kdeWorker = null, _kdeSeq = 0;
|
|
2213
|
+
const _kdePending = new Map();
|
|
2214
|
+
// tests: forget the worker and the loaded d3-contour
|
|
2215
|
+
function kdeReset() { if (_kdeWorker && _kdeWorker.terminate) _kdeWorker.terminate(); _kdeWorker = null; _kdePending.clear(); _d3ContourPromise = null; }
|
|
2216
|
+
// tests: the deck.gl layer classes a runtime builds with, without loading deck.gl
|
|
2217
|
+
function setDeckLayerClassesForTests(classes) { if (classes.GeoJsonLayer) GeoJsonLayer = classes.GeoJsonLayer; }
|
|
2218
|
+
function kdeCompute(points, bwKm, maxCells) {
|
|
2219
|
+
return new Promise(resolve => {
|
|
2220
|
+
try {
|
|
2221
|
+
if (_kdeWorker === null && typeof global.Worker === 'function' && typeof global.Blob === 'function' && global.URL && global.URL.createObjectURL) {
|
|
2222
|
+
const w = new global.Worker(global.URL.createObjectURL(new global.Blob([kdeWorkerSource()], { type: 'text/javascript' })));
|
|
2223
|
+
w.onmessage = e => { const p = _kdePending.get(e.data.id); if (p) { _kdePending.delete(e.data.id); p.resolve(e.data.grid); } };
|
|
2224
|
+
w.onerror = () => {
|
|
2225
|
+
_kdeWorker = false; // no worker from now on: whatever is waiting is computed here
|
|
2226
|
+
const waiting = [..._kdePending.values()]; _kdePending.clear();
|
|
2227
|
+
waiting.forEach(p => p.resolve(kdeGrid(...p.args)));
|
|
2228
|
+
};
|
|
2229
|
+
_kdeWorker = w;
|
|
2230
|
+
}
|
|
2231
|
+
} catch (err) { _kdeWorker = false; }
|
|
2232
|
+
if (!_kdeWorker) { resolve(kdeGrid(points, bwKm, maxCells)); return; }
|
|
2233
|
+
const id = ++_kdeSeq;
|
|
2234
|
+
_kdePending.set(id, { resolve, args: [points, bwKm, maxCells] });
|
|
2235
|
+
_kdeWorker.postMessage({ id, points, bwKm, maxCells });
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
// d3.contours of d3-contour 4 (+ d3-array 3), fetched once and evaluated PRIVATELY with a
|
|
2240
|
+
// CommonJS `require`: nothing is added to a window.d3 the page may have (d3-array would overwrite
|
|
2241
|
+
// the same-named functions of a d3 v3). Resolves to the `contours` function.
|
|
2242
|
+
let _d3ContourPromise = null;
|
|
2243
|
+
function ensureD3Contour() {
|
|
2244
|
+
if (!_d3ContourPromise) {
|
|
2245
|
+
const get = url => global.fetch(url).then(r => { if (!r.ok) throw new Error(`[ixmaps-gl] failed to load ${url}`); return r.text(); });
|
|
2246
|
+
const run = (src, req) => { const module = { exports: {} }; new Function('module', 'exports', 'require', src)(module, module.exports, req); return module.exports; };
|
|
2247
|
+
_d3ContourPromise = Promise.all([get(LIB_URLS.d3Array), get(LIB_URLS.d3Contour)]).then(([arraySrc, contourSrc]) => {
|
|
2248
|
+
const d3Array = run(arraySrc, () => { throw new Error('[ixmaps-gl] d3-array has no dependencies'); });
|
|
2249
|
+
const d3Contour = run(contourSrc, name => { if (name === 'd3-array') return d3Array; throw new Error(`[ixmaps-gl] d3-contour requires ${name}`); });
|
|
2250
|
+
if (typeof d3Contour.contours !== 'function') throw new Error('[ixmaps-gl] d3-contour: no contours()');
|
|
2251
|
+
return d3Contour.contours;
|
|
2252
|
+
}).catch(err => { _d3ContourPromise = null; throw err; });
|
|
2253
|
+
}
|
|
2254
|
+
return _d3ContourPromise;
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
// A color ramp of `steps` RGB colors from a colorscheme, low → high: a generated scheme
|
|
2258
|
+
// (["5","white","blue"]) is flat's own sweep, a list of colors is taken as given (interpolated),
|
|
2259
|
+
// ONE color is a ramp of itself; `fallback` when nothing in it can be read. Shared by HEATMAP
|
|
2260
|
+
// (colorRange) and KDE (one color per band).
|
|
2261
|
+
function colorRampRgb(colorscheme, steps, fallback) {
|
|
2262
|
+
let rgb;
|
|
2263
|
+
if (isGeneratedColorScheme(colorscheme)) {
|
|
2264
|
+
rgb = flatColorSweep(colorscheme[1], colorscheme[2], Number(colorscheme[0]) || steps, colorscheme[3], colorscheme[4]) || [];
|
|
2265
|
+
} else {
|
|
2266
|
+
const list = Array.isArray(colorscheme) ? colorscheme : colorscheme ? [colorscheme] : [];
|
|
2267
|
+
rgb = list.map(c => parseCssColor(String(c))).filter(Boolean);
|
|
2268
|
+
}
|
|
2269
|
+
if (!rgb.length) rgb = [fallback];
|
|
2270
|
+
return Array.from({ length: steps }, (_, i) => {
|
|
2271
|
+
const t = steps > 1 ? i / (steps - 1) : 1;
|
|
2272
|
+
const x = t * (rgb.length - 1), k = Math.min(rgb.length - 2, Math.floor(x));
|
|
2273
|
+
const c = rgb.length === 1 ? rgb[0] : rgb[k].map((v, j) => Math.round(v + (rgb[k + 1][j] - v) * (x - k)));
|
|
2274
|
+
return [c[0], c[1], c[2]];
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
// .type("HEATMAP") — deck.gl HeatmapLayer's colorRange (low → high density, RGBA): the ramp of
|
|
2279
|
+
// `steps` colors with an alpha that rises from 0 over the first part of it, so the tails of a
|
|
2280
|
+
// kernel fade out instead of ending in a hard edge.
|
|
2281
|
+
function heatmapColorRange(colorscheme, steps = 6) {
|
|
2282
|
+
return colorRampRgb(colorscheme, steps, [229, 57, 53]).map((c, i) => {
|
|
2283
|
+
const t = steps > 1 ? i / (steps - 1) : 1;
|
|
2284
|
+
return [c[0], c[1], c[2], Math.round(255 * Math.min(1, t * 2.5))];
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
// .type("KDE") — the color of each band, lowest threshold first (the fire pages' #dd0088 when the
|
|
2289
|
+
// colorscheme says nothing usable)
|
|
2290
|
+
function kdeBandColors(colorscheme, count) { return colorRampRgb(colorscheme, count, [221, 0, 136]); }
|
|
2291
|
+
|
|
2292
|
+
// .type("HEATMAP") — the weight of one record: its size field if bound, else its value field
|
|
2293
|
+
// (a text value field just counts the record), 1 for "$item$"; below 0 or not a number → 0
|
|
2294
|
+
function heatmapWeight(binding, props) {
|
|
2295
|
+
const b = binding || {};
|
|
2296
|
+
const fromSize = !!b.size && b.size !== '$item$';
|
|
2297
|
+
const field = fromSize ? b.size
|
|
2298
|
+
: (b.size === '$item$' || !b.value || b.value === '$item$' || String(b.value).includes('|')) ? null : b.value;
|
|
2299
|
+
if (!field) return 1;
|
|
2300
|
+
const w = scanValue(props[field]);
|
|
2301
|
+
if (isFinite(w)) return w > 0 ? w : 0;
|
|
2302
|
+
return fromSize ? 0 : 1;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
// ── .type("MLHEATMAP") — MapLibre's NATIVE heatmap layer (type "heatmap"). The pure parts: the
|
|
2306
|
+
// points, the weight that counts 1, the radius by zoom, the paint. Unlike HEATMAP (deck.gl's
|
|
2307
|
+
// HeatmapLayer, which aggregates the whole dataset into one 2048² texture whenever the view
|
|
2308
|
+
// changes and scales the density to the maximum in view), MapLibre keeps the points in tile
|
|
2309
|
+
// buffers, evaluates radius and intensity from the zoom on the GPU at draw time and reads the
|
|
2310
|
+
// density ABSOLUTELY through the color ramp (clamped at 1): zooming costs nothing and a cell means
|
|
2311
|
+
// the same at every zoom. The weight is `w`, the density 1 is reached by a point of weight
|
|
2312
|
+
// weightmax (and more by overlapping points).
|
|
2313
|
+
const MLHEATMAP_MIN_RADIUS_PX = 0.5;
|
|
2314
|
+
const MLHEATMAP_MAX_RADIUS_PX = 400;
|
|
2315
|
+
|
|
2316
|
+
// the records as GeoJSON points {w: weight}, for the native layer's source (weights ≤ 0 and
|
|
2317
|
+
// records without a position are left out)
|
|
2318
|
+
function mlHeatmapPoints(features, binding) {
|
|
2319
|
+
const out = [];
|
|
2320
|
+
for (const f of features) {
|
|
2321
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
2322
|
+
if (!c || typeof c[0] !== 'number' || !isFinite(c[0]) || !isFinite(c[1])) continue;
|
|
2323
|
+
const w = heatmapWeight(binding, f.properties || {});
|
|
2324
|
+
if (w > 0) out.push({ type: 'Feature', geometry: { type: 'Point', coordinates: [c[0], c[1]] }, properties: { w } });
|
|
2325
|
+
}
|
|
2326
|
+
return { type: 'FeatureCollection', features: out };
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
// the weight that counts 1: style.weightmax, else the largest weight of the records (1 for none)
|
|
2330
|
+
function mlHeatmapWeightMax(features, binding, style) {
|
|
2331
|
+
const given = styleNum(style && style.weightmax);
|
|
2332
|
+
if (given > 0) return given;
|
|
2333
|
+
let max = 0;
|
|
2334
|
+
for (const f of features) {
|
|
2335
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
2336
|
+
if (!c || typeof c[0] !== 'number' || !isFinite(c[0]) || !isFinite(c[1])) continue;
|
|
2337
|
+
const w = heatmapWeight(binding, f.properties || {});
|
|
2338
|
+
if (w > max) max = w;
|
|
2339
|
+
}
|
|
2340
|
+
return max > 0 ? max : 1;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
// heatmap-radius: `radiusPx` at the reference zoom `zoomRef`, growing with the map like every object
|
|
2344
|
+
// (objectZoomFactor: radiusPx · 2^((zoom − zoomRef) / pow)), kept between 0.5 and 400 px. An
|
|
2345
|
+
// exponential interpolation of base 2^(1/pow) between the two zooms where the limits are reached is
|
|
2346
|
+
// exactly that curve, so MapLibre evaluates it from the zoom without any rebuild. A number when the
|
|
2347
|
+
// radius does not follow the zoom (objectscaling not dynamic) or is already at a limit everywhere.
|
|
2348
|
+
function mlHeatmapRadius(radiusPx, zoomRef, pow, dynamic) {
|
|
2349
|
+
const r0 = radiusPx > 0 ? radiusPx : 30;
|
|
2350
|
+
const clamp = r => Math.max(MLHEATMAP_MIN_RADIUS_PX, Math.min(MLHEATMAP_MAX_RADIUS_PX, r));
|
|
2351
|
+
if (!dynamic) return clamp(r0);
|
|
2352
|
+
const p = pow > 0 ? pow : 1;
|
|
2353
|
+
const zoomOf = r => zoomRef + p * Math.log2(r / r0); // the zoom at which the radius is r
|
|
2354
|
+
const radiusAt = z => r0 * Math.pow(2, (z - zoomRef) / p);
|
|
2355
|
+
const zLow = Math.max(0, zoomOf(MLHEATMAP_MIN_RADIUS_PX));
|
|
2356
|
+
const zCap = Math.min(24, zoomOf(MLHEATMAP_MAX_RADIUS_PX));
|
|
2357
|
+
if (!(zCap > zLow + 1e-6)) return clamp(radiusAt(zLow));
|
|
2358
|
+
return ['interpolate', ['exponential', Math.pow(2, 1 / p)], ['zoom'], zLow, clamp(radiusAt(zLow)), zCap, clamp(radiusAt(zCap))];
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
// the paint of the native heatmap layer. Colors: the colorscheme as a ramp over the densities
|
|
2362
|
+
// 0.2 … 1 (transparent at 0, like MapLibre's own example, so the tails fade out); a scheme of one
|
|
2363
|
+
// color fades in by alpha instead. style: intensity (default 1), fillopacity (default 1).
|
|
2364
|
+
function mlHeatmapPaint(style, weightMax, radius) {
|
|
2365
|
+
const st = style || {};
|
|
2366
|
+
const ramp = colorRampRgb(st.colorscheme, 5, [229, 57, 53]);
|
|
2367
|
+
const single = ramp.every(c => c.join() === ramp[0].join());
|
|
2368
|
+
const color = ['interpolate', ['linear'], ['heatmap-density'], 0, `rgba(${ramp[0].join(',')},0)`];
|
|
2369
|
+
ramp.forEach((c, i) => {
|
|
2370
|
+
const d = (i + 1) / 5;
|
|
2371
|
+
color.push(d, single ? `rgba(${c.join(',')},${d})` : `rgb(${c.join(',')})`);
|
|
2372
|
+
});
|
|
2373
|
+
const intensity = styleNum(st.intensity), opacity = styleNum(st.fillopacity);
|
|
2374
|
+
return {
|
|
2375
|
+
'heatmap-weight': ['interpolate', ['linear'], ['get', 'w'], 0, 0, weightMax > 0 ? weightMax : 1, 1],
|
|
2376
|
+
'heatmap-intensity': intensity > 0 ? intensity : 1,
|
|
2377
|
+
'heatmap-color': color,
|
|
2378
|
+
'heatmap-radius': radius,
|
|
2379
|
+
'heatmap-opacity': opacity > 0 ? Math.min(opacity, 1) : 1
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
// The native heatmap layers of the MLHEATMAP themes, kept in step with the runtimes (as the WMS
|
|
2384
|
+
// raster layers are): per theme one GeoJSON source of weighted points and one `heatmap` layer, put
|
|
2385
|
+
// BEFORE the first deck.gl layer (those are MapLibre custom layers) so the symbols stay on top.
|
|
2386
|
+
// Recomputed only when something changed — the records (filter, reload), the binding, the weight
|
|
2387
|
+
// max — and the paint only when its values changed; a new style (setMapType) drops sources and
|
|
2388
|
+
// layers, so a missing one is simply added again from the cached points. `owned` holds the layer ids
|
|
2389
|
+
// this function added, so those of removed themes can be taken away.
|
|
2390
|
+
// (MapLibre's basemap-opacity pass, applyBasemapOpacity, only knows fill/line/raster/circle/symbol
|
|
2391
|
+
// types: a heatmap layer is not faded with the basemap.)
|
|
2392
|
+
const _mlSourceIds = new WeakMap();
|
|
2393
|
+
let _mlSourceSeq = 0;
|
|
2394
|
+
const mlHeatLayerId = name => 'ixmaps-mlheat:' + String(name).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
2395
|
+
function mlFirstCustomLayerId(map) {
|
|
2396
|
+
const order = typeof map.getLayersOrder === 'function' ? map.getLayersOrder() : (map.getStyle().layers || []).map(l => l.id);
|
|
2397
|
+
return order.find(id => { const l = map.getLayer(id); return l && l.type === 'custom'; });
|
|
2398
|
+
}
|
|
2399
|
+
function syncNativeHeatmaps(map, runtimes, owned) {
|
|
2400
|
+
if (!map || typeof map.getLayer !== 'function') return;
|
|
2401
|
+
const desired = runtimes.filter(rt => rt.flags && rt.flags.has('MLHEATMAP'));
|
|
2402
|
+
if (!desired.length && !owned.size) return;
|
|
2403
|
+
const wanted = new Set(desired.map(rt => mlHeatLayerId(rt.name)));
|
|
2404
|
+
[...owned].forEach(id => {
|
|
2405
|
+
if (wanted.has(id)) return;
|
|
2406
|
+
owned.delete(id);
|
|
2407
|
+
try { if (map.getLayer(id)) map.removeLayer(id); } catch (e) { /* already gone */ }
|
|
2408
|
+
try { if (map.getSource(id)) map.removeSource(id); } catch (e) { /* already gone */ }
|
|
2409
|
+
});
|
|
2410
|
+
desired.forEach(rt => {
|
|
2411
|
+
const id = mlHeatLayerId(rt.name);
|
|
2412
|
+
const st = rt._mlHeat || (rt._mlHeat = {});
|
|
2413
|
+
try {
|
|
2414
|
+
const records = rt._activeFeatures || rt.features || [];
|
|
2415
|
+
if (!_mlSourceIds.has(records)) _mlSourceIds.set(records, ++_mlSourceSeq);
|
|
2416
|
+
const style = rt.style || {};
|
|
2417
|
+
const dataKey = JSON.stringify([rt._dataGen, _mlSourceIds.get(records), rt.binding]);
|
|
2418
|
+
const weightKey = JSON.stringify([dataKey, style.weightmax]);
|
|
2419
|
+
if (st.weightKey !== weightKey) { st.weightMax = mlHeatmapWeightMax(records, rt.binding, style); st.weightKey = weightKey; }
|
|
2420
|
+
if (st.dataKey !== dataKey || !st.fc) {
|
|
2421
|
+
st.fc = mlHeatmapPoints(records, rt.binding);
|
|
2422
|
+
st.dataKey = dataKey;
|
|
2423
|
+
const src = map.getSource(id);
|
|
2424
|
+
if (src && typeof src.setData === 'function') src.setData(st.fc);
|
|
2425
|
+
}
|
|
2426
|
+
if (!map.getSource(id)) map.addSource(id, { type: 'geojson', data: st.fc });
|
|
2427
|
+
owned.add(id);
|
|
2428
|
+
// the radius follows the zoom like every object (objectscaling), from `radiuspx` at the reference zoom
|
|
2429
|
+
const mo = rt.mapOptions || {};
|
|
2430
|
+
const radius = mlHeatmapRadius((styleNum(style.radiuspx) || 30) * (styleNum(style.scale) || 1),
|
|
2431
|
+
resolveZoomReference(mo), resolveDynamicScalePow(mo), String(mo.objectscaling || 'dynamic') === 'dynamic');
|
|
2432
|
+
const paint = mlHeatmapPaint(style, st.weightMax, radius);
|
|
2433
|
+
const paintKey = JSON.stringify(paint);
|
|
2434
|
+
if (!map.getLayer(id)) {
|
|
2435
|
+
map.addLayer({ id, type: 'heatmap', source: id, paint }, mlFirstCustomLayerId(map));
|
|
2436
|
+
st.paintKey = paintKey;
|
|
2437
|
+
} else if (st.paintKey !== paintKey) {
|
|
2438
|
+
Object.keys(paint).forEach(k => map.setPaintProperty(id, k, paint[k]));
|
|
2439
|
+
st.paintKey = paintKey;
|
|
2440
|
+
}
|
|
2441
|
+
const visibility = rt._hidden ? 'none' : 'visible';
|
|
2442
|
+
if ((map.getLayoutProperty(id, 'visibility') || 'visible') !== visibility) map.setLayoutProperty(id, 'visibility', visibility);
|
|
2443
|
+
st.failed = false;
|
|
2444
|
+
} catch (err) {
|
|
2445
|
+
if (!st.failed) { st.failed = true; console.warn(`[ixmaps-gl] layer "${rt.name}": MLHEATMAP not drawn:`, err && err.message ? err.message : err); }
|
|
2446
|
+
}
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2004
2450
|
// a choropleth shape's outline color: a LINE shape strokes with its value
|
|
2005
2451
|
// color; any other shape keeps the theme's outline, hidden while a class
|
|
2006
2452
|
// is isolated and the shape is not in it
|
|
@@ -2033,7 +2479,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2033
2479
|
// specific FEATURE runtime as a donor, not just sharing its name
|
|
2034
2480
|
// coincidentally.
|
|
2035
2481
|
geomRt._isChoroplethGeometryDonor = true;
|
|
2036
|
-
|
|
2482
|
+
// the donor's own style (outline) — the layer registry is keyed by name,
|
|
2483
|
+
// so the same-named CHOROPLETH replaces the donor there and can't find it
|
|
2484
|
+
Object.defineProperty(spec, '_donorStyle', { value: geomRt.style, configurable: true });
|
|
2485
|
+
const idField = geometryIdField(geomRt, geomRt.binding.id);
|
|
2037
2486
|
const lookupField = spec.binding.lookup;
|
|
2038
2487
|
if (!idField || !lookupField) {
|
|
2039
2488
|
throw new Error(`[ixmaps-gl] CHOROPLETH layer "${spec.name}" needs .binding({lookup}), and its FEATURE base needs .binding({id})`);
|
|
@@ -2757,6 +3206,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2757
3206
|
if (this._filterExpr != null) style.filter = this._filterExpr;
|
|
2758
3207
|
if (this._titleField != null) style.title = this._titleField;
|
|
2759
3208
|
if (this._lookupfield != null) style.lookupfield = this._lookupfield;
|
|
3209
|
+
// flat's .data() (htmlgui_flat.js ~1827): a geojson/topojson layer with no type of its own is
|
|
3210
|
+
// a FEATURES base layer ("FEATURES|NOLEGEND", lookup on its geometry) — the base a CHOROPLETH
|
|
3211
|
+
// of the same name needs. Only when neither .type() nor style.type says otherwise
|
|
3212
|
+
if (!style.type && this._data && /geojson|topojson/i.test(String(this._data.type || ''))) {
|
|
3213
|
+
style.type = 'FEATURES|NOLEGEND';
|
|
3214
|
+
if (style.lookupfield == null) style.lookupfield = 'geometry';
|
|
3215
|
+
}
|
|
2760
3216
|
let data = this._data;
|
|
2761
3217
|
if (this._query != null || this._process != null) {
|
|
2762
3218
|
data = Object.assign({}, this._data);
|
|
@@ -2820,7 +3276,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2820
3276
|
// edit it by hand; `npm run unit` fails when it drifts.
|
|
2821
3277
|
// ---------------------------------------------------------------
|
|
2822
3278
|
// <grammar:binding-aliases>
|
|
2823
|
-
// generated from ixmaps-grammar 0.1.
|
|
3279
|
+
// generated from ixmaps-grammar 0.1.30 (ixmaps-flat 1.0.42, 2b978d5 2026-10-02) — 52 aliases
|
|
2824
3280
|
const FLAT_BINDING_ALIASES = {
|
|
2825
3281
|
"aggregation": "style.aggregationfield",
|
|
2826
3282
|
"aggregationfield": "style.aggregationfield",
|
|
@@ -2882,7 +3338,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2882
3338
|
// both — so .style({tooltip}) is a tooltip and .meta({fillopacity}) a style
|
|
2883
3339
|
// property. GENERATED by test/sync-grammar.mjs — do not edit.
|
|
2884
3340
|
// <grammar:meta-keys>
|
|
2885
|
-
// generated from ixmaps-grammar 0.1.
|
|
3341
|
+
// generated from ixmaps-grammar 0.1.30 — 5 meta keys
|
|
2886
3342
|
const FLAT_META_KEYS = ["description","name","snippet","title","tooltip"];
|
|
2887
3343
|
// </grammar:meta-keys>
|
|
2888
3344
|
|
|
@@ -3122,7 +3578,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3122
3578
|
'sizepow', 'rangescale', 'minvalue', 'maxvalue', 'markersize', 'boxopacity', 'outlierscale', 'valuescale',
|
|
3123
3579
|
'brightness', 'fractionscale', 'dopacityscale', 'dopacitypow', 'gridwidthpx', 'textscale', 'rangecentervalue',
|
|
3124
3580
|
'shadowblur', 'shadowdx', 'shadowdy', 'maxshadow', 'offsetx', 'offsety', 'gridx', 'boxmargin', 'borderwidth',
|
|
3125
|
-
'borderradius', 'maxcharts', 'archeight', 'alignmargin'
|
|
3581
|
+
'borderradius', 'maxcharts', 'archeight', 'alignmargin',
|
|
3582
|
+
// HEATMAP / MLHEATMAP / KDE (a project JSON writes them as strings)
|
|
3583
|
+
'radiuspx', 'intensity', 'threshold', 'weightmax', 'bandwidth', 'bandwidthscale', 'bandwidthzoompow', 'bandwidthzoomref'];
|
|
3126
3584
|
function toNumberIfNumeric(v) {
|
|
3127
3585
|
if (typeof v !== 'string' || !v.trim()) return v;
|
|
3128
3586
|
const n = Number(v);
|
|
@@ -3197,6 +3655,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3197
3655
|
delete style.title;
|
|
3198
3656
|
|
|
3199
3657
|
const flags = new Set(typeStr ? typeStr.split('|') : []);
|
|
3658
|
+
// flat's theme configurator appends "CENTER|DOMINANT" to a categorical type without a
|
|
3659
|
+
// separator ("...|CATEGORICALCENTER|DOMINANT"); flat's substring tests read that as
|
|
3660
|
+
// CATEGORICAL + CENTER, so split a glued CATEGORICAL prefix back into its flags
|
|
3661
|
+
[...flags].forEach(f => {
|
|
3662
|
+
if (f.length > 11 && f.startsWith('CATEGORICAL')) { flags.add('CATEGORICAL'); flags.add(f.slice(11)); }
|
|
3663
|
+
});
|
|
3200
3664
|
if (flags.has('BUBBLE')) flags.add('SYMBOL');
|
|
3201
3665
|
// flat tests its base type as a substring (szFlag.match(/CHOROPLETH/)),
|
|
3202
3666
|
// so "CHOROPLETHE" is a choropleth there too
|
|
@@ -3500,9 +3964,23 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3500
3964
|
if (t === entry.name || (entry.aliases || []).includes(t)) return entry;
|
|
3501
3965
|
}
|
|
3502
3966
|
}
|
|
3967
|
+
// keyless vector styles by NAME (exact match, before the dark/light rule): flat's own VT_VOYAGER names
|
|
3968
|
+
// resolve to the CARTO Voyager style they are made from; the rest are further keyless MapLibre styles
|
|
3969
|
+
const KEYLESS_VECTOR_BASEMAPS = [
|
|
3970
|
+
{ name: 'CARTO - Voyager', aliases: ['VT_VOYAGER'], url: 'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json' },
|
|
3971
|
+
{ name: 'CARTO - Voyager (no labels)', aliases: ['VT_VOYAGER_LIGHT'], url: 'https://basemaps.cartocdn.com/gl/voyager-nolabels-gl-style/style.json' },
|
|
3972
|
+
{ name: 'CARTO - Positron (no labels)', url: 'https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json' },
|
|
3973
|
+
{ name: 'CARTO - Dark Matter (no labels)', url: 'https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json' },
|
|
3974
|
+
{ name: 'OpenFreeMap - Liberty', url: 'https://tiles.openfreemap.org/styles/liberty' },
|
|
3975
|
+
{ name: 'OpenFreeMap - Bright', url: 'https://tiles.openfreemap.org/styles/bright' },
|
|
3976
|
+
{ name: 'OpenFreeMap - Positron', url: 'https://tiles.openfreemap.org/styles/positron' },
|
|
3977
|
+
];
|
|
3503
3978
|
function resolveBasemapStyleUrl(mapType) {
|
|
3504
3979
|
const raster = flatRasterBasemap(mapType);
|
|
3505
3980
|
if (raster) return buildRasterBasemapStyle(raster);
|
|
3981
|
+
const typeName = typeof mapType === 'string' ? mapType.trim() : '';
|
|
3982
|
+
const vector = KEYLESS_VECTOR_BASEMAPS.find(e => e.name === typeName || (e.aliases || []).includes(typeName));
|
|
3983
|
+
if (vector) return vector.url;
|
|
3506
3984
|
return typeof mapType === 'string' && /dark|black|night|matter/i.test(mapType) ? BASEMAP_STYLE_URLS.dark : BASEMAP_STYLE_URLS.light;
|
|
3507
3985
|
}
|
|
3508
3986
|
// Real ixmaps-flat's legend look (tools/legend.js 3773-3845, via
|
|
@@ -3989,18 +4467,34 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3989
4467
|
}
|
|
3990
4468
|
// flat's click-to-action hook (ui/js/tools/tooltip_to_action.js): ixmaps.onItemAction(szId, item, themeObj) on a
|
|
3991
4469
|
// click on an item — item.szSelectionId is the value of the theme's selection field — and onItemAction(null, null)
|
|
3992
|
-
// on a click on the map background
|
|
3993
|
-
|
|
4470
|
+
// on a click on the map background — gl adds the click position as third argument there:
|
|
4471
|
+
// onItemAction(null, null, { lngLat: {lng, lat}, point: {x, y} }) (flat passes none)
|
|
4472
|
+
function clickPosition(info) {
|
|
4473
|
+
const c = info && info.coordinate;
|
|
4474
|
+
if (!c || !isFinite(c[0]) || !isFinite(c[1])) return undefined;
|
|
4475
|
+
return { lngLat: { lng: c[0], lat: c[1] }, point: { x: info.x, y: info.y } };
|
|
4476
|
+
}
|
|
4477
|
+
function callItemAction(rt, object, info) {
|
|
3994
4478
|
const hook = pageIxmaps() && pageIxmaps().onItemAction;
|
|
3995
|
-
if (typeof hook !== 'function') return;
|
|
3996
4479
|
try {
|
|
3997
|
-
|
|
4480
|
+
const pos = clickPosition(info);
|
|
4481
|
+
if (!rt) { if (typeof hook === 'function') hook.call(global.ixmaps, null, null, pos); return; }
|
|
3998
4482
|
const field = selectionFieldOf(rt);
|
|
3999
4483
|
const props = object.properties || {};
|
|
4000
4484
|
// a single record, or an aggregated item's first one
|
|
4001
4485
|
const raw = props.raw || props.firstRaw || props.titleRaw || (props.raws && props.raws[0]) || props;
|
|
4002
4486
|
const value = field && raw ? raw[field] : null;
|
|
4003
|
-
|
|
4487
|
+
// the map's "click" event, for EVERY item as in flat (szId = theme::itemKey): the key is the selection
|
|
4488
|
+
// field's value, else the feature's id, else its position "lng,lat" — a theme bound by geometry only
|
|
4489
|
+
// (no id/lookup field) has no selection value. gl adds the record and the click position.
|
|
4490
|
+
if (builder.__emitItemClick) {
|
|
4491
|
+
const g = object.geometry, a = anchorCoordOf(g) || (g && Array.isArray(g.coordinates) && g.coordinates[0]);
|
|
4492
|
+
const key = value != null ? String(value)
|
|
4493
|
+
: (object.id != null ? String(object.id) : (a && typeof a[0] === 'number' ? a[0] + ',' + a[1] : ''));
|
|
4494
|
+
builder.__emitItemClick(Object.assign({ szId: themeIdOf(rt) + '::' + key, id: key, theme: themeIdOf(rt), record: raw }, pos));
|
|
4495
|
+
}
|
|
4496
|
+
// flat's onItemAction needs the theme's selection value
|
|
4497
|
+
if (value == null || typeof hook !== 'function') return;
|
|
4004
4498
|
hook.call(global.ixmaps, themeIdOf(rt) + '::' + value, { szSelectionId: String(value) }, getThemeObj(themeIdOf(rt)));
|
|
4005
4499
|
} catch (err) { console.error('[ixmaps-gl] onItemAction failed:', err); }
|
|
4006
4500
|
}
|
|
@@ -4009,7 +4503,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4009
4503
|
if (info && info.object && info.layer) {
|
|
4010
4504
|
const rt = findRuntimeForLayerId(info.layer.id);
|
|
4011
4505
|
if (rt) {
|
|
4012
|
-
callItemAction(rt, info.object);
|
|
4506
|
+
callItemAction(rt, info.object, info);
|
|
4013
4507
|
// Only a Point geometry's own coordinates ARE an
|
|
4014
4508
|
// [lng,lat] pair (every point/bubble/dot/chart-cluster
|
|
4015
4509
|
// layer) — a CHOROPLETH polygon's geometry.coordinates is
|
|
@@ -4040,7 +4534,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4040
4534
|
}
|
|
4041
4535
|
}
|
|
4042
4536
|
// clicked empty map space (or a non-tooltippable layer) -> unpin
|
|
4043
|
-
callItemAction(null, null);
|
|
4537
|
+
callItemAction(null, null, picked);
|
|
4044
4538
|
pinned = null;
|
|
4045
4539
|
pinnedTooltipEl.style.display = 'none';
|
|
4046
4540
|
return false;
|
|
@@ -4318,7 +4812,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4318
4812
|
// category rows: its panel is the text legend (title / snippet / description)
|
|
4319
4813
|
const countOnlyLegend = rt => !rt.categoryLabels && rt.flags.has('CATEGORICAL') && !rt.binding.value;
|
|
4320
4814
|
const legendApplies = rt => (countOnlyLegend(rt) || (rt.categoryLabels && rt.categoryLabels.length))
|
|
4321
|
-
&& (!(rt.flags.has('FEATURE') || rt.flags.has('FEATURES')) || rt.flags.has('CATEGORICAL')) && !rt.flags.has('NOLEGEND')
|
|
4815
|
+
&& (!(rt.flags.has('FEATURE') || rt.flags.has('FEATURES')) || rt.flags.has('CATEGORICAL')) && !rt.flags.has('NOLEGEND')
|
|
4816
|
+
// KDE and HEATMAP draw a density, not classes of the value: the range legend that a numeric
|
|
4817
|
+
// value would get is meaningless for them (no legend for these, as the fire pages)
|
|
4818
|
+
&& !rt.flags.has('KDE') && !rt.flags.has('HEATMAP') && !rt.flags.has('MLHEATMAP');
|
|
4322
4819
|
// One theme's panel — at build time for every theme, and again for a
|
|
4323
4820
|
// theme defined later (map.layer(...) in a myMap.then(...) chain,
|
|
4324
4821
|
// defineLayer, loadProject); removeTheme removes it with the theme.
|
|
@@ -4419,7 +4916,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4419
4916
|
// no SUM) falls back to a per-category record COUNT.
|
|
4420
4917
|
// a VECTOR / BEZIER flow legend sums its size field (flat's rows
|
|
4421
4918
|
// show the flows' summed sizes, measured on the twin page)
|
|
4422
|
-
|
|
4919
|
+
// a categorical symbol/bubble sized by a column: SUM sums that column per category (flat:
|
|
4920
|
+
// partsA[i].nSum accumulates each item's nSize, maptheme.js 16649; legend.js 677); a size
|
|
4921
|
+
// of "$item$" is the count anyway
|
|
4922
|
+
const useSum = flatFlag(rt.flags, 'SUM') && (rt.style.valuefield
|
|
4923
|
+
|| ((rt._isVectorChart() || rt._isAggregatedPie()) && rt.binding.size)
|
|
4924
|
+
|| (rt.flags.has('CATEGORICAL') && rt.binding.size && rt.binding.size !== '$item$'));
|
|
4423
4925
|
const valueField = rt.style.valuefield || rt.binding.size;
|
|
4424
4926
|
// legendunits wins over the theme's general-purpose units
|
|
4425
4927
|
// (used elsewhere for tooltips, e.g. _renderItemChartHtml) —
|
|
@@ -4502,7 +5004,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4502
5004
|
if (!c || typeof c[0] !== 'number') return;
|
|
4503
5005
|
const [lng, lat] = c;
|
|
4504
5006
|
if (!inViewport(lng, lat, bbox, globeCenter)) return;
|
|
4505
|
-
rt._multiFields.forEach((k, i) => { totals[i] +=
|
|
5007
|
+
rt._multiFields.forEach((k, i) => { totals[i] += scanValue(f.properties[k]) || 0; });
|
|
4506
5008
|
});
|
|
4507
5009
|
maxTotal = Math.max(0, ...totals);
|
|
4508
5010
|
order = totals.map((v, i) => i).sort((a, b) => totals[b] - totals[a]);
|
|
@@ -4515,7 +5017,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4515
5017
|
// with whatever the country filter is currently narrowing
|
|
4516
5018
|
// the map down to, not just with the viewport/hemisphere.
|
|
4517
5019
|
(rt._activeFeatures || rt.features).forEach(f => {
|
|
4518
|
-
const idx = rt.categoryIndexByLabel ? rt.
|
|
5020
|
+
const idx = rt.categoryIndexByLabel ? rt._resolveClassIndex(f.properties[rt.binding.value]) : null;
|
|
4519
5021
|
if (idx == null) return;
|
|
4520
5022
|
// a polygon (FEATURE|CATEGORICAL) is counted at its first vertex
|
|
4521
5023
|
let pt = f.geometry && f.geometry.coordinates;
|
|
@@ -4523,7 +5025,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4523
5025
|
if (!Array.isArray(pt)) return;
|
|
4524
5026
|
const [lng, lat] = pt;
|
|
4525
5027
|
if (!inViewport(lng, lat, bbox, globeCenter)) return;
|
|
4526
|
-
totals[idx] += useSum ? (
|
|
5028
|
+
totals[idx] += useSum ? (scanValue(f.properties[valueField]) || 0) : 1;
|
|
4527
5029
|
});
|
|
4528
5030
|
maxTotal = Math.max(0, ...totals);
|
|
4529
5031
|
order = totals.map((v, i) => i).sort((a, b) => totals[b] - totals[a]);
|
|
@@ -5039,6 +5541,23 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5039
5541
|
return { addLegendPanel, setLegendOption, updateSubTheme, layoutLegends };
|
|
5040
5542
|
}
|
|
5041
5543
|
|
|
5544
|
+
// flat's map event names (htmlgui_flat.js mapApi.on, with its aliases) and the ones this engine fires:
|
|
5545
|
+
// viewchange (every view change end), zoomend / moveend ({ nZoom, zoomChanged, panChanged }, as flat),
|
|
5546
|
+
// ready (the map has loaded), click (an item: { szId, id, theme } + lngLat/point), layerdraw ({ id }) and,
|
|
5547
|
+
// as a gl addition, mapclick (every click on the map, background included: { lngLat, point })
|
|
5548
|
+
const MAP_EVENT_ALIAS = { zoompan: 'viewchange', itemover: 'mouseover', itemclick: 'click', itemout: 'mouseout', mapready: 'ready',
|
|
5549
|
+
drawtheme: 'layerdraw', newtheme: 'layeradd', removetheme: 'layerremove' };
|
|
5550
|
+
const MAP_EVENTS_WIRED = new Set(['viewchange', 'zoomend', 'moveend', 'ready', 'click', 'mapclick', 'layerdraw']);
|
|
5551
|
+
function splitMapEvents(events) {
|
|
5552
|
+
return String(events == null ? '' : events).trim().toLowerCase().split(/\s+/).filter(Boolean).map(e => MAP_EVENT_ALIAS[e] || e);
|
|
5553
|
+
}
|
|
5554
|
+
const _warnedMapEvents = new Set();
|
|
5555
|
+
function warnUnwiredMapEvent(ev) {
|
|
5556
|
+
if (MAP_EVENTS_WIRED.has(ev) || _warnedMapEvents.has(ev)) return;
|
|
5557
|
+
_warnedMapEvents.add(ev);
|
|
5558
|
+
console.warn(`[ixmaps-gl] map event "${ev}" is not implemented — the handler is accepted but never fires (supported: ${[...MAP_EVENTS_WIRED].join(', ')})`);
|
|
5559
|
+
}
|
|
5560
|
+
|
|
5042
5561
|
class MapBuilder {
|
|
5043
5562
|
constructor(containerId, options) {
|
|
5044
5563
|
this.containerId = containerId;
|
|
@@ -5103,15 +5622,21 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5103
5622
|
}
|
|
5104
5623
|
// .Layer() — alias of .layer()
|
|
5105
5624
|
Layer(nameOrBuilder) { return this.layer(nameOrBuilder); }
|
|
5106
|
-
//
|
|
5107
|
-
//
|
|
5108
|
-
//
|
|
5109
|
-
// OTHER event name is accepted and stored but never fired —
|
|
5110
|
-
// recognized rather than throwing, the same "known but inert" pattern
|
|
5111
|
-
// KNOWN_INERT_FLAGS already uses for unimplemented type flags.
|
|
5625
|
+
// flat's mapApi.on(events, cb) (htmlgui_flat.js 1631), "a b" for several names; see MAP_EVENTS for the
|
|
5626
|
+
// ones wired here. Any other name is accepted (never throws) but never fires, and says so once on the console.
|
|
5627
|
+
// Registered before the map is built, they are attached by build().
|
|
5112
5628
|
on(event, cb) {
|
|
5113
5629
|
this._pendingEvents = this._pendingEvents || [];
|
|
5114
|
-
|
|
5630
|
+
for (const ev of splitMapEvents(event)) {
|
|
5631
|
+
warnUnwiredMapEvent(ev);
|
|
5632
|
+
this._pendingEvents.push([ev, cb]);
|
|
5633
|
+
}
|
|
5634
|
+
return this;
|
|
5635
|
+
}
|
|
5636
|
+
// flat's mapApi.off(events, cb), before the map is built (after it: the handle's off)
|
|
5637
|
+
off(event, cb) {
|
|
5638
|
+
const names = new Set(splitMapEvents(event));
|
|
5639
|
+
this._pendingEvents = (this._pendingEvents || []).filter(([ev, f]) => !(names.has(ev) && f === cb));
|
|
5115
5640
|
return this;
|
|
5116
5641
|
}
|
|
5117
5642
|
|
|
@@ -5533,20 +6058,29 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5533
6058
|
map.on('style.load', refreshControlLook);
|
|
5534
6059
|
this._setAttribution = text => attributionBox.set(text);
|
|
5535
6060
|
|
|
5536
|
-
|
|
6061
|
+
// the page legend (map.legend(html)), or the legend a loaded project
|
|
6062
|
+
// carries in its map part (applyProjectMap) — flat shows the project's
|
|
6063
|
+
// legend in the same pane, so the project's replaces the page's
|
|
6064
|
+
let pageLegendEl = null;
|
|
6065
|
+
const showPageLegend = html => {
|
|
6066
|
+
if (pageLegendEl) { pageLegendEl.remove(); pageLegendEl = null; }
|
|
6067
|
+
this._legendHtml = html;
|
|
6068
|
+
if (!html) return;
|
|
5537
6069
|
const legendEl = document.createElement('div');
|
|
5538
6070
|
// a page legend (map.legend(html)) is positioned by the map's
|
|
5539
6071
|
// own align option — the same rule that places the native stack
|
|
5540
6072
|
legendEl.style.cssText = 'position:absolute;' + legendAlignCssFor(this.mapOptions) + 'z-index:5;pointer-events:none';
|
|
5541
6073
|
// flat's legend takes the pane's html OR a URL to it (htmlgui):
|
|
5542
6074
|
// a value without a tag is fetched, its html shown
|
|
5543
|
-
if (/<[a-z!]/i.test(
|
|
5544
|
-
else fetch(
|
|
6075
|
+
if (/<[a-z!]/i.test(html)) legendEl.innerHTML = html;
|
|
6076
|
+
else fetch(html).then(r => r.ok ? r.text() : '').then(t => { legendEl.innerHTML = t; }).catch(() => {});
|
|
5545
6077
|
// inside the map container, never the page's body (see the pinned
|
|
5546
6078
|
// tooltip mount) — styling body breaks page-positioned elements
|
|
5547
6079
|
anchorInside(el);
|
|
5548
6080
|
el.appendChild(legendEl);
|
|
5549
|
-
|
|
6081
|
+
pageLegendEl = legendEl;
|
|
6082
|
+
};
|
|
6083
|
+
if (this._legendHtml) showPageLegend(this._legendHtml);
|
|
5550
6084
|
|
|
5551
6085
|
// themes are addressed by either the layer()'s own name (e.g. "AREU")
|
|
5552
6086
|
// or style.name (e.g. "chart") — the real engine's changeThemeStyle
|
|
@@ -6432,6 +6966,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6432
6966
|
Object.assign(builder._engineOptions, o);
|
|
6433
6967
|
}
|
|
6434
6968
|
if (m.scaleParam && m.scaleParam.normalSizeScale != null) builder._engineOptions.normalSizeScale = m.scaleParam.normalSizeScale;
|
|
6969
|
+
// the project's own legend (map.legend: html or a URL to it) replaces the page's, as in flat;
|
|
6970
|
+
// like a page legend it takes the place of the theme-generated legend panels
|
|
6971
|
+
if (typeof m.legend === 'string' && m.legend.trim()) {
|
|
6972
|
+
showPageLegend(m.legend);
|
|
6973
|
+
setLegendOption(builder.mapOptions.legend);
|
|
6974
|
+
}
|
|
6435
6975
|
if (m.basemap) report.notes.push(`basemap "${m.basemap}" not switched (ixmaps-gl keeps the map's own basemap)`);
|
|
6436
6976
|
}
|
|
6437
6977
|
|
|
@@ -6544,7 +7084,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6544
7084
|
// charts in their own group above them: whatever the theme order
|
|
6545
7085
|
// (a choropleth swapped in with add(..., 'replace') comes last), the
|
|
6546
7086
|
// shapes lie under the charts — in theme order among themselves
|
|
6547
|
-
const isShape = l => /^ix-(features|choropleth|shadow)-/.test(l.id);
|
|
7087
|
+
const isShape = l => /^ix-(features|choropleth|shadow|kde)-/.test(l.id);
|
|
6548
7088
|
layers = layers.filter(isShape).concat(layers.filter(l => !isShape(l)));
|
|
6549
7089
|
// every deck.gl layer id once: two themes of one layer name and type
|
|
6550
7090
|
// (the page's comune points and comune polygons, both FEATURES on
|
|
@@ -6588,6 +7128,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6588
7128
|
// flat's WMS themes render as native raster layers, not deck.gl —
|
|
6589
7129
|
// reconcile them with the current runtimes on every rebuild
|
|
6590
7130
|
syncWmsLayers();
|
|
7131
|
+
syncNativeHeatmaps(map, runtimes, mlHeatLayerIds);
|
|
6591
7132
|
}
|
|
6592
7133
|
|
|
6593
7134
|
// flat's WMS theme type (szFlag /\bWMS|IMAGE\b/, maptheme.js 8207-8247):
|
|
@@ -6604,6 +7145,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6604
7145
|
// map.getStyle() (a deep copy of the whole style) on every rebuild
|
|
6605
7146
|
// of a page that has no WMS theme
|
|
6606
7147
|
const wmsLayerIds = new Set();
|
|
7148
|
+
const mlHeatLayerIds = new Set(); // the MLHEATMAP layers added to the map (syncNativeHeatmaps)
|
|
6607
7149
|
function syncWmsLayers() {
|
|
6608
7150
|
if (!map || !map.getLayer) return;
|
|
6609
7151
|
const idOf = name => 'ixmaps-wms:' + String(name).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
@@ -6649,7 +7191,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6649
7191
|
}
|
|
6650
7192
|
// a style swap (setMapType) drops every native layer — rebuild the
|
|
6651
7193
|
// WMS ones once the new style stands
|
|
6652
|
-
map.on('style.load', () => syncWmsLayers());
|
|
7194
|
+
map.on('style.load', () => { syncWmsLayers(); syncNativeHeatmaps(map, runtimes, mlHeatLayerIds); });
|
|
6653
7195
|
|
|
6654
7196
|
// isZooming flips true/false on every 'zoomstart'/'zoomend' pair —
|
|
6655
7197
|
// and a scroll-wheel or trackpad zoom fires MANY such pairs per
|
|
@@ -6773,8 +7315,61 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6773
7315
|
}
|
|
6774
7316
|
map.on('moveend', loadDeferredThemes);
|
|
6775
7317
|
|
|
7318
|
+
// map events (flat's mapApi.on): see MAP_EVENTS_WIRED. "layerdraw" is approximated below.
|
|
7319
|
+
const mapListeners = Object.create(null);
|
|
7320
|
+
const drawHooks = new Map(); // layerdraw handler -> its onRedraw unsubscribe functions
|
|
7321
|
+
const mapName = (this.mapOptions && this.mapOptions.name) || null; // flat's szMap
|
|
7322
|
+
let mapReady = false;
|
|
7323
|
+
const emitMapEvent = (type, detail) => (mapListeners[type] || []).slice().forEach(cb => {
|
|
7324
|
+
try { cb(Object.assign({ type, szMap: mapName }, detail)); } catch (err) { console.error(`[ixmaps-gl] "${type}" callback failed:`, err); }
|
|
7325
|
+
});
|
|
7326
|
+
engineApi.on = (events, cb) => {
|
|
7327
|
+
if (typeof cb !== 'function') return engineApi;
|
|
7328
|
+
for (const ev of splitMapEvents(events)) {
|
|
7329
|
+
warnUnwiredMapEvent(ev);
|
|
7330
|
+
if (ev === 'layerdraw') {
|
|
7331
|
+
// flat fires "layerdraw" once per theme as it finishes drawing; here onRedraw fires once per refresh of
|
|
7332
|
+
// the whole map, so the callback is called once per current theme every time — a page that re-reads
|
|
7333
|
+
// that theme's state (getThemeObj/getFacets) gets fresh numbers after every redraw
|
|
7334
|
+
const off = engineApi.onRedraw(() => runtimes.forEach(rt => {
|
|
7335
|
+
const id = themeIdOf(rt);
|
|
7336
|
+
try { cb({ type: 'layerdraw', szId: id, id, szMap: mapName }); } catch (err) { console.error('[ixmaps-gl] "layerdraw" callback failed:', err); }
|
|
7337
|
+
}));
|
|
7338
|
+
drawHooks.set(cb, (drawHooks.get(cb) || []).concat(off));
|
|
7339
|
+
} else {
|
|
7340
|
+
(mapListeners[ev] = mapListeners[ev] || []).push(cb);
|
|
7341
|
+
if (ev === 'ready' && mapReady) setTimeout(() => { try { cb({ type: 'ready', szMap: mapName }); } catch (err) { console.error('[ixmaps-gl] "ready" callback failed:', err); } }, 0);
|
|
7342
|
+
}
|
|
7343
|
+
}
|
|
7344
|
+
return engineApi;
|
|
7345
|
+
};
|
|
7346
|
+
// flat's mapApi.off(events, cb): removes the handler(s) given to on()
|
|
7347
|
+
engineApi.off = (events, cb) => {
|
|
7348
|
+
if (typeof cb !== 'function') return engineApi;
|
|
7349
|
+
for (const ev of splitMapEvents(events)) {
|
|
7350
|
+
if (ev === 'layerdraw') { (drawHooks.get(cb) || []).forEach(off => off()); drawHooks.delete(cb); continue; }
|
|
7351
|
+
const list = mapListeners[ev];
|
|
7352
|
+
if (list) mapListeners[ev] = list.filter(f => f !== cb);
|
|
7353
|
+
}
|
|
7354
|
+
return engineApi;
|
|
7355
|
+
};
|
|
7356
|
+
let lastViewZoom = map.getZoom(), lastViewCenter = map.getCenter();
|
|
7357
|
+
map.on('moveend', () => {
|
|
7358
|
+
const z = map.getZoom(), c = map.getCenter();
|
|
7359
|
+
const zoomChanged = z !== lastViewZoom, panChanged = c.lng !== lastViewCenter.lng || c.lat !== lastViewCenter.lat;
|
|
7360
|
+
lastViewZoom = z; lastViewCenter = c;
|
|
7361
|
+
const detail = { nZoom: flatViewZoomOf(map), zoomChanged, panChanged };
|
|
7362
|
+
emitMapEvent('viewchange', detail);
|
|
7363
|
+
if (zoomChanged) emitMapEvent('zoomend', detail);
|
|
7364
|
+
if (panChanged && !zoomChanged) emitMapEvent('moveend', detail);
|
|
7365
|
+
});
|
|
7366
|
+
map.on('click', e => emitMapEvent('mapclick', { lngLat: { lng: e.lngLat.lng, lat: e.lngLat.lat }, point: { x: e.point.x, y: e.point.y } }));
|
|
7367
|
+
this.__emitItemClick = detail => emitMapEvent('click', detail);
|
|
7368
|
+
|
|
6776
7369
|
map.on('load', () => {
|
|
6777
7370
|
mapHasLoaded = true;
|
|
7371
|
+
mapReady = true;
|
|
7372
|
+
emitMapEvent('ready');
|
|
6778
7373
|
loadDeferredThemes();
|
|
6779
7374
|
refresh();
|
|
6780
7375
|
runtimes.forEach(maybeZoomToTheme);
|
|
@@ -6840,18 +7435,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6840
7435
|
// gets the same practical effect: fresh numbers after every redraw,
|
|
6841
7436
|
// for every theme, without needing the page rewritten around a
|
|
6842
7437
|
// finer-grained event this engine doesn't produce.
|
|
6843
|
-
|
|
6844
|
-
.filter(([event]) => event === 'layerdraw')
|
|
6845
|
-
.map(([, cb]) => cb);
|
|
6846
|
-
if (layerdrawCbs.length) {
|
|
6847
|
-
engineApi.onRedraw(() => {
|
|
6848
|
-
runtimes.forEach(rt => {
|
|
6849
|
-
layerdrawCbs.forEach(cb => {
|
|
6850
|
-
try { cb({ id: rt.name }); } catch (err) { console.error('[ixmaps-gl] "layerdraw" callback failed:', err); }
|
|
6851
|
-
});
|
|
6852
|
-
});
|
|
6853
|
-
});
|
|
6854
|
-
}
|
|
7438
|
+
(this._pendingEvents || []).forEach(([event, cb]) => engineApi.on(event, cb));
|
|
6855
7439
|
|
|
6856
7440
|
// the native legend: createLegend (its four hooks are what the rest
|
|
6857
7441
|
// of the map calls — panels, the legend option, sub-themes, layout)
|
|
@@ -7035,7 +7619,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7035
7619
|
promise.legend = (...args) => { builder.legend(...args); return promise; };
|
|
7036
7620
|
promise.local = (...args) => { builder.local(...args); return promise; };
|
|
7037
7621
|
promise.require = (url) => { if (resolvedApi) resolvedApi.require(url); else builder.require(url); return promise; };
|
|
7038
|
-
promise.on = (...args) => { builder.on(...args); return promise; };
|
|
7622
|
+
promise.on = (...args) => { if (resolvedApi) resolvedApi.on(...args); else builder.on(...args); return promise; };
|
|
7623
|
+
promise.off = (...args) => { if (resolvedApi) resolvedApi.off(...args); else builder.off(...args); return promise; };
|
|
7624
|
+
promise.getBounds = () => getBounds();
|
|
7625
|
+
promise.getMapLibre = () => (resolvedApi ? resolvedApi.map : undefined);
|
|
7039
7626
|
promise.layer = (nameOrBuilder) => {
|
|
7040
7627
|
if (typeof nameOrBuilder !== 'string') {
|
|
7041
7628
|
// object form (a pre-built LayerBuilder, e.g. from the global
|
|
@@ -7772,7 +8359,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7772
8359
|
// "$item$": every record counts 1
|
|
7773
8360
|
if (field === '$item$') return features.length ? 1 : 0;
|
|
7774
8361
|
return features.reduce((max, f) => {
|
|
7775
|
-
const v =
|
|
8362
|
+
const v = scanValue(f.properties[field]);
|
|
7776
8363
|
return isNaN(v) ? max : Math.max(max, v);
|
|
7777
8364
|
}, 0);
|
|
7778
8365
|
}
|
|
@@ -8046,6 +8633,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8046
8633
|
const scale = scaleDenominatorAt(zoom);
|
|
8047
8634
|
return !!((upper && scale > upper) || (lower && scale < lower));
|
|
8048
8635
|
}
|
|
8636
|
+
// GLOW follows the theme's overall `opacity` (a number or numeric string; unset or 0 = 1, flat treats a falsy opacity as unset): the
|
|
8637
|
+
// halo is a fixed soft gradient texture (_getGlowIcon), so its strength is this alpha factor, as a bubble's is fillopacity
|
|
8638
|
+
function glowOpacityOf(style) {
|
|
8639
|
+
const v = Array.isArray(style.opacity) ? style.opacity[0] : style.opacity;
|
|
8640
|
+
const o = v == null || String(v).trim() === '' ? NaN : Number(v);
|
|
8641
|
+
return o > 0 && o < 1 ? o : 1;
|
|
8642
|
+
}
|
|
8049
8643
|
function themeHiddenByScale(flags, style, zoom) {
|
|
8050
8644
|
return (flags.has('FEATURE') || flags.has('FEATURES')) ? featuresHiddenByScale(style, zoom) : chartHiddenByScale(style, zoom);
|
|
8051
8645
|
}
|
|
@@ -9376,7 +9970,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9376
9970
|
}
|
|
9377
9971
|
const parts = [], zeroNot = [];
|
|
9378
9972
|
for (let k = 0; k < o.fields.length; k++) {
|
|
9379
|
-
let v =
|
|
9973
|
+
let v = scanValue(raw[o.fields[k]]);
|
|
9380
9974
|
// flat's weights multiply every record's per-field value as it is
|
|
9381
9975
|
// accumulated (maptheme.js 10506-10508) — the SIZE (nValueSize)
|
|
9382
9976
|
// stays raw, exactly as there
|
|
@@ -10039,14 +10633,14 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10039
10633
|
let maxAlpha = -Infinity, minAlpha = Infinity;
|
|
10040
10634
|
features.forEach(f => {
|
|
10041
10635
|
// flat: an alpha that is no number is 0 (maptheme.js 9853-9857)
|
|
10042
|
-
let v =
|
|
10636
|
+
let v = scanValue(f.properties[binding.alpha]);
|
|
10043
10637
|
if (isNaN(v)) v = 0;
|
|
10044
10638
|
if (isDensity) {
|
|
10045
10639
|
const areaKm2 = featureAreaKm2(f);
|
|
10046
10640
|
if (!areaKm2) return;
|
|
10047
10641
|
v = v / areaKm2;
|
|
10048
10642
|
} else if (alpha100) {
|
|
10049
|
-
const v100 =
|
|
10643
|
+
const v100 = scanValue(f.properties[alpha100]);
|
|
10050
10644
|
if (!isNaN(v100) && v100) v = 100 / v100 * v;
|
|
10051
10645
|
}
|
|
10052
10646
|
out._alphaByFeature.set(f, v);
|
|
@@ -10123,10 +10717,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10123
10717
|
features.forEach(f => {
|
|
10124
10718
|
if (!f.properties || !Object.keys(f.properties).length) return; // unjoined polygon
|
|
10125
10719
|
nItems++;
|
|
10126
|
-
const v100 = f100 ?
|
|
10720
|
+
const v100 = f100 ? scanValue(f.properties[f100]) : NaN;
|
|
10127
10721
|
if (f100 && isFinite(v100)) sum100 += v100;
|
|
10128
10722
|
fields.forEach((field, i) => {
|
|
10129
|
-
const v =
|
|
10723
|
+
const v = scanValue(f.properties[field]);
|
|
10130
10724
|
if (!isFinite(v)) return;
|
|
10131
10725
|
sums[i] += v;
|
|
10132
10726
|
if (f100 && isFinite(v100)) pooled[i] += v * v100;
|
|
@@ -10187,7 +10781,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10187
10781
|
const useDeviation = ctx.flags.has('DEVIATION');
|
|
10188
10782
|
let bestIndex = -1, bestRelevance = 0, bestValue = null;
|
|
10189
10783
|
for (let i = 0; i < fields.length; i++) {
|
|
10190
|
-
const v =
|
|
10784
|
+
const v = scanValue(props[fields[i]]);
|
|
10191
10785
|
if (!(v > (ctx._dominantMins[i] || 0))) continue;
|
|
10192
10786
|
const relevance = useDeviation ? (v - ctx._dominantMeans[i]) / ctx._dominantStdDevs[i]
|
|
10193
10787
|
: usePercentOfMean ? 100 * v / ctx._dominantMeans[i]
|
|
@@ -10215,7 +10809,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10215
10809
|
const fields = out._multiFields;
|
|
10216
10810
|
let nMax = 0;
|
|
10217
10811
|
features.forEach(f => fields.forEach(field => {
|
|
10218
|
-
const v =
|
|
10812
|
+
const v = scanValue(f.properties[field]);
|
|
10219
10813
|
if (v > nMax) nMax = v;
|
|
10220
10814
|
}));
|
|
10221
10815
|
out._composeColorMax = nMax;
|
|
@@ -10255,7 +10849,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10255
10849
|
const subtractive = ctx.flags.has('SUBTRACTIVE');
|
|
10256
10850
|
let rr = 0, gg = 0, bb = 0;
|
|
10257
10851
|
for (let i = 0; i < fields.length; i++) {
|
|
10258
|
-
const v =
|
|
10852
|
+
const v = scanValue(props[fields[i]]) || 0;
|
|
10259
10853
|
const weight = v / nMax;
|
|
10260
10854
|
const [r, g, b] = rgbs[i];
|
|
10261
10855
|
if (subtractive) { rr += (255 - r) * weight; gg += (255 - g) * weight; bb += (255 - b) * weight; }
|
|
@@ -10328,14 +10922,20 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10328
10922
|
// flat's reading of a value field (maptheme.js 9776-9800, defaults
|
|
10329
10923
|
// 7740-7801): a value that is no number counts as 0 — unless
|
|
10330
10924
|
// UNDEFINEDISNOTVALUE —, and 0 is a value — unless ZEROISNOTVALUE
|
|
10925
|
+
// flat's __scanValue (maptheme.js 9266): a value with a comma is EUROPEAN format (dots are thousands separators, the comma
|
|
10926
|
+
// the decimal: "1.234,5" -> 1234.5, "0,0637" -> 0.0637); a value without one is parsed plain, blanks stripped. Used wherever a
|
|
10927
|
+
// theme reads a value / size / alpha / value100 field from a record — the same strings plain parseFloat would cut at the comma
|
|
10928
|
+
function scanValue(v) {
|
|
10929
|
+
const s = String(v);
|
|
10930
|
+
return s.includes(',') ? parseFloat(s.replace(/\./g, '').replace(/,/g, '.')) : parseFloat(s.replace(/ /g, ''));
|
|
10931
|
+
}
|
|
10331
10932
|
function flatNumber(v, flags) {
|
|
10332
10933
|
// flat's own value scan (maptheme.js __scanValue 9265-9272): a
|
|
10333
10934
|
// value with a comma is EUROPEAN format — dots stripped as
|
|
10334
10935
|
// thousands separators, the comma is the decimal ("78,8" -> 78.8);
|
|
10335
10936
|
// a value without one is parsed plain, blanks stripped. Applied
|
|
10336
10937
|
// to every theme value in flat, so also here
|
|
10337
|
-
|
|
10338
|
-
let n = s.includes(',') ? parseFloat(s.replace(/\./g, '').replace(/,/g, '.')) : parseFloat(s.replace(/ /g, ''));
|
|
10938
|
+
let n = scanValue(v);
|
|
10339
10939
|
if (isNaN(n)) n = flags && flags.has('UNDEFINEDISNOTVALUE') ? NaN : 0;
|
|
10340
10940
|
if (n === 0 && flags && flags.has('ZEROISNOTVALUE')) return NaN;
|
|
10341
10941
|
return n;
|
|
@@ -10464,14 +11064,14 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10464
11064
|
if (!p) { out.push(f); continue; }
|
|
10465
11065
|
let clear = [];
|
|
10466
11066
|
if (size) {
|
|
10467
|
-
const v =
|
|
11067
|
+
const v = scanValue(p[size]);
|
|
10468
11068
|
if (!(isFinite(v) && v > 0)) {
|
|
10469
11069
|
if (!keepShapes) continue;
|
|
10470
11070
|
clear.push(size);
|
|
10471
11071
|
}
|
|
10472
11072
|
}
|
|
10473
11073
|
if (negativeIsNot) {
|
|
10474
|
-
const negative = checked.filter(k =>
|
|
11074
|
+
const negative = checked.filter(k => scanValue(p[k]) < 0);
|
|
10475
11075
|
if (negative.length && aggregate && !keepShapes) continue;
|
|
10476
11076
|
clear = clear.concat(negative);
|
|
10477
11077
|
}
|
|
@@ -10618,7 +11218,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10618
11218
|
// without AGGREGATE applyField100 already differenced every record
|
|
10619
11219
|
// (flat's nValuesA, popped): the first field IS the value
|
|
10620
11220
|
? (!flags.has('AGGREGATE') ? p => flatNumber(p[fields[0]], flags)
|
|
10621
|
-
: p => cellAggregatedValues({ sums: fields.map(k =>
|
|
11221
|
+
: p => cellAggregatedValues({ sums: fields.map(k => scanValue(p[k]) || 0), counts: fields.map(() => 1) }, flags, style)[0])
|
|
10622
11222
|
// "$item$" is flat's record count: 1 per record
|
|
10623
11223
|
: binding.value === '$item$' ? () => 1
|
|
10624
11224
|
: p => (p && Object.keys(p).length ? flatNumber(p[binding.value], flags) : NaN);
|
|
@@ -10769,9 +11369,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10769
11369
|
// theme's text 0.08 px high).
|
|
10770
11370
|
function resolveAggregateValue(binding, flags, props) {
|
|
10771
11371
|
if (binding.size === '$item$') return 1;
|
|
10772
|
-
if (binding.size) return
|
|
11372
|
+
if (binding.size) return scanValue(props[binding.size]) || 0;
|
|
10773
11373
|
if (flags.has('CATEGORICAL') || binding.value == null || binding.value === '$item$') return 1;
|
|
10774
|
-
return
|
|
11374
|
+
return scanValue(props[binding.value]) || 0;
|
|
10775
11375
|
}
|
|
10776
11376
|
|
|
10777
11377
|
// Whether a range-classed AGGREGATE cell's class value (flat: the sum
|
|
@@ -11075,7 +11675,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11075
11675
|
const add = (cat, v) => { if (cat == null || cat < 0 || cat >= n) return; count[cat]++; sum[cat] += Number.isFinite(v) ? v : 0; };
|
|
11076
11676
|
if (rt.flags.has('CHOROPLETH')) {
|
|
11077
11677
|
(rt._activeFeatures || rt.features).forEach(f => {
|
|
11078
|
-
const v =
|
|
11678
|
+
const v = scanValue(f.properties[rt.binding.value]);
|
|
11079
11679
|
add(rt._resolvePartsClass(v), v);
|
|
11080
11680
|
});
|
|
11081
11681
|
} else if (rt._legendItems) {
|
|
@@ -11086,7 +11686,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11086
11686
|
if (!c || typeof c[0] !== 'number') return;
|
|
11087
11687
|
const [lng, lat] = c;
|
|
11088
11688
|
if (!inViewport(lng, lat, bbox, globeCenter)) return;
|
|
11089
|
-
const v =
|
|
11689
|
+
const v = scanValue(f.properties[rt.binding.value]);
|
|
11090
11690
|
add(rt._resolvePartsClass(v), v);
|
|
11091
11691
|
});
|
|
11092
11692
|
}
|
|
@@ -11128,6 +11728,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11128
11728
|
this.name = spec.name;
|
|
11129
11729
|
this.binding = spec.binding;
|
|
11130
11730
|
this.style = spec.style;
|
|
11731
|
+
this._donorStyle = spec._donorStyle;
|
|
11131
11732
|
this.flags = symbolsFlagCompat(spec.flags, spec.style);
|
|
11132
11733
|
this.meta = spec.meta;
|
|
11133
11734
|
// see LAYER_DATA_GENERATION above — unique per runtime, see _dataLayerId
|
|
@@ -11233,7 +11834,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11233
11834
|
// a bar chart's parts are a pie's (its value fields or categories)
|
|
11234
11835
|
if ((this._isPieChart() || this._isBarChart()) && this.binding.value) {
|
|
11235
11836
|
this._preparePie();
|
|
11236
|
-
} else if (this.flags.has('DOMINANT') && this.binding.value && !isAggregatedCategoricalChoropleth(this)) {
|
|
11837
|
+
} else if (this.flags.has('DOMINANT') && this.binding.value && !isAggregatedCategoricalChoropleth(this) && !this._isDominantGrid()) {
|
|
11237
11838
|
// .type("CHOROPLETH|DOMINANT") — a MULTI-field bound value
|
|
11238
11839
|
// (binding.value is a pipe-joined field list, e.g. one CSV column
|
|
11239
11840
|
// per age band for the same year/sex — same pipe convention
|
|
@@ -11270,7 +11871,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11270
11871
|
} else {
|
|
11271
11872
|
const seen = new Map();
|
|
11272
11873
|
this.features.forEach(f => {
|
|
11273
|
-
|
|
11874
|
+
let v = f.properties[this.binding.value];
|
|
11875
|
+
if (typeof v === 'string') v = v.trim(); // flat: getStringValueIndex strips trailing blanks
|
|
11274
11876
|
if (v != null && !seen.has(v)) seen.set(v, seen.size);
|
|
11275
11877
|
});
|
|
11276
11878
|
this.categoryLabels = Array.from(seen.keys());
|
|
@@ -11313,7 +11915,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11313
11915
|
const scheme = Array.isArray(this.style.colorscheme) ? this.style.colorscheme : [];
|
|
11314
11916
|
this._partNoFill = new Set(this._multiFields.map((f, i) => i).filter(i => String(scheme[i]).trim().toLowerCase() === 'none'));
|
|
11315
11917
|
let max = 0;
|
|
11316
|
-
for (const f of this.features) for (const field of this._multiFields) { const v = Math.abs(
|
|
11918
|
+
for (const f of this.features) for (const field of this._multiFields) { const v = Math.abs(scanValue(f.properties[field])); if (v > max) max = v; }
|
|
11317
11919
|
this._multiFieldPartMax = max || 1;
|
|
11318
11920
|
// AUTO100's scale, measured on these features by _buildSequenceChartLayers
|
|
11319
11921
|
this._seqAuto100Max = null;
|
|
@@ -11463,19 +12065,31 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11463
12065
|
// flat's nValueSum is the RAW weighted sum, whatever the flags.
|
|
11464
12066
|
// (RELATIVE/INVERT subtract after the multiply — not reconstructable
|
|
11465
12067
|
// from the parts; flat's own composed branch never pairs them.)
|
|
11466
|
-
|
|
12068
|
+
// With binding.alpha the opacity follows the alpha field instead of the value sum — flat's
|
|
12069
|
+
// `if (this.szAlphaField)` branch of the same DOPACITY block (maptheme.js 13476-13482):
|
|
12070
|
+
// dopacityscale / nAlphaMax^(1/pow) · nAlpha^(1/pow), nAlpha as _prepareAlphaField made it
|
|
12071
|
+
// (alpha100 "$density$": alpha / the shape's area, the FEATURE base's size field when it binds one);
|
|
12072
|
+
// a shape with no alpha is 0. Not capped at 0.9 and not times fillopacity, as flat
|
|
12073
|
+
_composedDopacityAlpha(props, feature) {
|
|
12074
|
+
if (this.binding && this.binding.alpha) {
|
|
12075
|
+
const nAlpha = this._alphaByFeature && feature ? this._alphaByFeature.get(feature) : undefined;
|
|
12076
|
+
if (nAlpha == null) return 0;
|
|
12077
|
+
const pow = 1 / (styleNum(this.style.dopacitypow) || 1);
|
|
12078
|
+
const o = (styleNum(this.style.dopacityscale) || 1) / Math.pow(this._alphaMax || 1, pow) * Math.pow(nAlpha, pow);
|
|
12079
|
+
return isFinite(o) ? Math.max(0, Math.min(1, o)) : 0;
|
|
12080
|
+
}
|
|
11467
12081
|
const fields = this._multiFields || [];
|
|
11468
12082
|
const f100A = this.binding && this.binding.field100 ? String(this.binding.field100).split('|') : null;
|
|
11469
12083
|
const multiplier = this.flags.has('FRACTION') ? (styleNum(this.style.fractionscale) || 1)
|
|
11470
12084
|
: this.flags.has('PERMILLE') ? 1000 : 100;
|
|
11471
12085
|
let sum = 0;
|
|
11472
12086
|
fields.forEach((field, i) => {
|
|
11473
|
-
const part =
|
|
12087
|
+
const part = scanValue(props[field]);
|
|
11474
12088
|
if (isNaN(part)) return;
|
|
11475
|
-
const v100 = f100A ?
|
|
12089
|
+
const v100 = f100A ? scanValue(props[f100A[Math.min(i, f100A.length - 1)]]) : NaN;
|
|
11476
12090
|
sum += isNaN(v100) ? part : (part * v100) / multiplier;
|
|
11477
12091
|
});
|
|
11478
|
-
const v100_0 = f100A ?
|
|
12092
|
+
const v100_0 = f100A ? scanValue(props[f100A[0]]) : NaN;
|
|
11479
12093
|
const nMyValue = sum / ((v100_0 / 100) || 1);
|
|
11480
12094
|
return composedDopacityAlpha(this.style, this.flags, nMyValue, this._composeColorMax || 1);
|
|
11481
12095
|
}
|
|
@@ -11494,8 +12108,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11494
12108
|
// (_buildPartsA, numeric bucket), or neither (a single implicit
|
|
11495
12109
|
// bucket, today's behavior for any other theme).
|
|
11496
12110
|
_resolveClassIndex(rawValue) {
|
|
11497
|
-
if (this.categoryIndexByLabel)
|
|
11498
|
-
|
|
12111
|
+
if (this.categoryIndexByLabel) {
|
|
12112
|
+
const i = this.categoryIndexByLabel.get(rawValue);
|
|
12113
|
+
return i === undefined && typeof rawValue === 'string' ? this.categoryIndexByLabel.get(rawValue.trim()) : i;
|
|
12114
|
+
}
|
|
12115
|
+
if (this.partsA) return this._resolvePartsClass(scanValue(rawValue));
|
|
11499
12116
|
return 0;
|
|
11500
12117
|
}
|
|
11501
12118
|
|
|
@@ -11565,8 +12182,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11565
12182
|
// as flat's chart node value (NYC status: value="63" = e-bikes)
|
|
11566
12183
|
const sizeField = this.binding.size && this.binding.size !== '$item$' ? this.binding.size : null;
|
|
11567
12184
|
this._featuresByCategory = [sourceFeatures.map(f => {
|
|
11568
|
-
const parts = this._multiFields.map(k =>
|
|
11569
|
-
const size = sizeField ? Math.abs(
|
|
12185
|
+
const parts = this._multiFields.map(k => scanValue(f.properties[k]) || 0);
|
|
12186
|
+
const size = sizeField ? Math.abs(scanValue(f.properties[sizeField]) || 0) : undefined;
|
|
11570
12187
|
return { type: 'Feature', geometry: f.geometry,
|
|
11571
12188
|
properties: { value: parts[0], parts, series: parts, ...(size !== undefined ? { size } : {}), raw: f.properties } };
|
|
11572
12189
|
})];
|
|
@@ -11592,9 +12209,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11592
12209
|
// values, the difference is taken on the cell sums (see post)
|
|
11593
12210
|
const seriesFields = String(this.binding.value || '').split('|');
|
|
11594
12211
|
this._seriesDifference = this.flags.has('DIFFERENCE') && seriesFields.length > 1 && !this._aggregateField100;
|
|
11595
|
-
const seriesOf = props => seriesFields.map(k =>
|
|
12212
|
+
const seriesOf = props => seriesFields.map(k => scanValue(props[k]) || 0);
|
|
11596
12213
|
// without AGGREGATE the records are already differenced (applyField100)
|
|
11597
|
-
const seriesValue = !this.flags.has('AGGREGATE') ? props =>
|
|
12214
|
+
const seriesValue = !this.flags.has('AGGREGATE') ? props => scanValue(props[seriesFields[0]]) || 0 : props => cellAggregatedValues({ sums: seriesOf(props), counts: seriesFields.map(() => 1) }, this.flags, this.style)[0];
|
|
11598
12215
|
this._featuresByCategory = [sourceFeatures.map(f => ({
|
|
11599
12216
|
type: 'Feature',
|
|
11600
12217
|
geometry: f.geometry,
|
|
@@ -11602,8 +12219,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11602
12219
|
value: this._seriesDifference ? seriesValue(f.properties) : this._resolveAggregateValue(f.properties),
|
|
11603
12220
|
...(this._seriesDifference ? { series: seriesOf(f.properties) } : {}),
|
|
11604
12221
|
// "$item$": flat's record count, 1 per record
|
|
11605
|
-
...(this._classSeparate ? { classValue: this.binding.value === '$item$' ? 1 :
|
|
11606
|
-
...(this._aggregateField100 ? { value100:
|
|
12222
|
+
...(this._classSeparate ? { classValue: this.binding.value === '$item$' ? 1 : scanValue(f.properties[this.binding.value]) || 0 } : {}),
|
|
12223
|
+
...(this._aggregateField100 ? { value100: scanValue(f.properties[f100]) || 0 } : {}),
|
|
11607
12224
|
raw: f.properties
|
|
11608
12225
|
}
|
|
11609
12226
|
}))];
|
|
@@ -11892,7 +12509,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11892
12509
|
const countsByField = new Map(fields.map(f => [f, new Map()]));
|
|
11893
12510
|
|
|
11894
12511
|
scoped.forEach(f => {
|
|
11895
|
-
const weight = sizeField ? (
|
|
12512
|
+
const weight = sizeField ? (scanValue(f.properties[sizeField]) || 0) : 1;
|
|
11896
12513
|
for (const field of fields) {
|
|
11897
12514
|
const v = f.properties[field];
|
|
11898
12515
|
if (v == null || v === '') continue;
|
|
@@ -12479,7 +13096,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12479
13096
|
const labels = this.categoryDisplayLabels || this.categoryLabels || [];
|
|
12480
13097
|
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));
|
|
12481
13098
|
}
|
|
12482
|
-
|
|
13099
|
+
// (a USER chart over several value fields — the pinnacle peaks of a COMPOSECOLOR theme — lists its fields
|
|
13100
|
+
// the same way: its single item row would show the first class with the size total)
|
|
13101
|
+
const multiFieldUser = this.flags.has('USER') && String(this.binding.value || '').includes('|');
|
|
13102
|
+
if ((this.flags.has('CHOROPLETH') || multiFieldUser) && (this.flags.has('DOMINANT') || this.flags.has('COMPOSECOLOR')) && props.raw) {
|
|
12483
13103
|
return this._renderMultiFieldBarsHtml(props.raw);
|
|
12484
13104
|
}
|
|
12485
13105
|
const unit = unitOf;
|
|
@@ -12541,13 +13161,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12541
13161
|
const fields = String(this.binding.value || '').split('|');
|
|
12542
13162
|
if (this._multiFieldMax === undefined) {
|
|
12543
13163
|
let max = 0;
|
|
12544
|
-
for (const f of this.features) for (const field of fields) { const v = Math.abs(
|
|
13164
|
+
for (const f of this.features) for (const field of fields) { const v = Math.abs(scanValue(f.properties[field])); if (v > max) max = v; }
|
|
12545
13165
|
this._multiFieldMax = max;
|
|
12546
13166
|
}
|
|
12547
13167
|
const labels = Array.isArray(this.style.label) && this.style.label.length === fields.length ? this.style.label.map(String)
|
|
12548
13168
|
: (this.categoryDisplayLabels || this.categoryLabels || fields);
|
|
12549
13169
|
const unit = this.style.units ? ' ' + this.style.units : '';
|
|
12550
|
-
let rows = fields.map((field, i) => ({ i, v:
|
|
13170
|
+
let rows = fields.map((field, i) => ({ i, v: scanValue(raw[field]) })).filter(r => Number.isFinite(r.v));
|
|
12551
13171
|
if (!rows.length) return '';
|
|
12552
13172
|
if (this.flags.has('SORT')) rows.sort((a, b) => (this.flags.has('UP') ? a.v - b.v : b.v - a.v));
|
|
12553
13173
|
const color = i => { const c = this.classRgb(i); return `rgb(${c.slice(0, 3).join(',')})`; };
|
|
@@ -12695,13 +13315,30 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12695
13315
|
// early return here must also unmount its groups, or the pane keeps
|
|
12696
13316
|
// showing the last drawing after the theme went out of scale (or was
|
|
12697
13317
|
// hidden via setThemeVisible)
|
|
12698
|
-
if (this._hidden) { this._clearUserChartPane(); return []; }
|
|
13318
|
+
if (this._hidden) { this._kdeShowGate = true; this._clearUserChartPane(); return []; }
|
|
12699
13319
|
if (!this.flags.has('FEATURE') && !this.flags.has('FEATURES') && chartHiddenByScale(this.style, liveZoom)) { this._clearUserChartPane(); return []; }
|
|
12700
13320
|
// DOT is checked FIRST, matching the real engine's own precedence
|
|
12701
13321
|
// (its DOT branch is a loop-level fast-path that bypasses the whole
|
|
12702
13322
|
// drawChart/modifier machinery entirely — a layer flagged DOT never
|
|
12703
13323
|
// reaches CHART|SYMBOL's clustering/GLOW/VALUES pipeline, even if
|
|
12704
13324
|
// those flags are also present in the type string).
|
|
13325
|
+
// MLHEATMAP: MapLibre's native heatmap layer (syncNativeHeatmaps) — nothing for deck.gl to draw
|
|
13326
|
+
if (this.flags.has('MLHEATMAP')) return [];
|
|
13327
|
+
// KDE: the density bands of the visible records (_buildKdeLayers)
|
|
13328
|
+
if (this.flags.has('KDE')) return this._buildKdeLayers(zoom, bbox, globeCenter);
|
|
13329
|
+
// HEATMAP: one GPU density surface over the visible records (_buildHeatmapLayers)
|
|
13330
|
+
if (this.flags.has('HEATMAP')) {
|
|
13331
|
+
// deck.gl's HeatmapLayer aggregates in screen space on a Web-Mercator viewport: under the
|
|
13332
|
+
// globe it draws nothing (measured, deck.gl 9.4.0 / MapLibre globe) — say so once
|
|
13333
|
+
if (globeCenter) {
|
|
13334
|
+
if (!this._warnedHeatmapGlobe) {
|
|
13335
|
+
this._warnedHeatmapGlobe = true;
|
|
13336
|
+
console.warn(`[ixmaps-gl] layer "${this.name}": HEATMAP is not drawn under the globe projection (deck.gl HeatmapLayer needs a Mercator view) — use mapProjection mercator, or another type`);
|
|
13337
|
+
}
|
|
13338
|
+
return [];
|
|
13339
|
+
}
|
|
13340
|
+
return this._buildHeatmapLayers(liveZoom);
|
|
13341
|
+
}
|
|
12705
13342
|
if (this.flags.has('DOT')) return this._buildDotLayers();
|
|
12706
13343
|
// GRIDSIZE (spatial grid binning) is checked before the generic
|
|
12707
13344
|
// CHART|SYMBOL bubble pipeline, same "more specific base type wins"
|
|
@@ -12801,7 +13438,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12801
13438
|
// layer keeps its one fill color
|
|
12802
13439
|
&& this.binding.value && this.binding.value !== '$item$' && !String(this.binding.value).includes('|');
|
|
12803
13440
|
const classColorOf = f => {
|
|
12804
|
-
const cls = this._resolvePartsClass(
|
|
13441
|
+
const cls = this._resolvePartsClass(scanValue(f.properties && f.properties[this.binding.value]));
|
|
12805
13442
|
return this.classRgb(cls);
|
|
12806
13443
|
};
|
|
12807
13444
|
const catAlpha = Math.round(255 * (styleNum(this.style.fillopacity) || 1));
|
|
@@ -12981,7 +13618,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12981
13618
|
// sum over (field100/100), against the same nMax the color
|
|
12982
13619
|
// blend normalizes by. The raw weighted sum is reconstructed
|
|
12983
13620
|
// from the fractioned part values (raw = part × its field100)
|
|
12984
|
-
dopacityAlpha: dopacityActive ? this._composedDopacityAlpha(f.properties) : null
|
|
13621
|
+
dopacityAlpha: dopacityActive ? this._composedDopacityAlpha(f.properties, f) : null
|
|
12985
13622
|
}
|
|
12986
13623
|
};
|
|
12987
13624
|
}
|
|
@@ -13024,7 +13661,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13024
13661
|
// linecolor "none": no outline; a class color "none" (an explicit
|
|
13025
13662
|
// colorscheme list) leaves its polygons unpainted, an rgba() class
|
|
13026
13663
|
// color keeps its alpha — as flat's SVG fill does
|
|
13027
|
-
|
|
13664
|
+
// flat re-styles the SAME shape nodes the FEATURE base drew, so an
|
|
13665
|
+
// outline the choropleth doesn't define itself stays the base's own
|
|
13666
|
+
const donorStyle = this._donorStyle;
|
|
13667
|
+
const lineColorSrc = this.style.linecolor || (donorStyle && donorStyle.linecolor);
|
|
13668
|
+
const lineColorRaw = lineColorSrc ? styleLineColor(lineColorSrc) : null;
|
|
13028
13669
|
const lineRgb = lineColorRaw ? withAlpha(hexOrNamedToRgb(lineColorRaw), cssColorAlpha(lineColorRaw)) : [255, 255, 255];
|
|
13029
13670
|
const cs = this.style.colorscheme;
|
|
13030
13671
|
const classAlpha = Array.isArray(cs) && cs.length && !/^\d+$/.test(String(cs[0])) ? cs.map(cssColorAlpha) : null;
|
|
@@ -13056,11 +13697,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13056
13697
|
// flat re-styles the SAME shape nodes the FEATURE base drew, so a line
|
|
13057
13698
|
// choropleth keeps the base's own line width — the donor's linewidth
|
|
13058
13699
|
// is the fallback when the choropleth has none of its own
|
|
13059
|
-
const donorRt = [..._globalThemeRegistry.values()].find(r => r.name === this.name && r._isChoroplethGeometryDonor);
|
|
13060
13700
|
const anyLine = collection.features.some(isLineGeometry);
|
|
13061
13701
|
const isolating = !!(this._markedClasses.size && !this._onMarksChanged && evidenceMode === 'isolate');
|
|
13062
13702
|
const lineW = styleNum(this.style.linewidth)
|
|
13063
|
-
|| (
|
|
13703
|
+
|| (donorStyle && styleNum(donorStyle.linewidth)) || 1;
|
|
13064
13704
|
return [new GeoJsonLayer({
|
|
13065
13705
|
id: this._dataLayerId(`ix-choropleth-${this.name}`),
|
|
13066
13706
|
data: collection,
|
|
@@ -13142,8 +13782,15 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13142
13782
|
// GRIDSIZE without PLOT drawn as one uniform square per cell: only a
|
|
13143
13783
|
// plain grid — a single color, no VALUES, no GLOW
|
|
13144
13784
|
_isPlainGridMesh() {
|
|
13145
|
-
|
|
13146
|
-
|
|
13785
|
+
// categorical CENTER|DOMINANT cells are drawn by the same mesh path, per-cell colored
|
|
13786
|
+
return this._isDominantGrid() || (!this.flags.has('PLOT') && (this.categoryColorsRgb || []).length <= 1
|
|
13787
|
+
&& !this.flags.has('VALUES') && !this.flags.has('GLOW'));
|
|
13788
|
+
}
|
|
13789
|
+
// flat's categorical HexBin (GRIDSIZE|AGGREGATE|CATEGORICAL|DOMINANT): each grid cell takes
|
|
13790
|
+
// the color of its dominant category, with dynamic opacity from that category's part
|
|
13791
|
+
_isDominantGrid() {
|
|
13792
|
+
return this.flags.has('GRIDSIZE') && !this.flags.has('PLOT') && this.flags.has('CATEGORICAL')
|
|
13793
|
+
&& this.flags.has('DOMINANT');
|
|
13147
13794
|
}
|
|
13148
13795
|
// GRIDSIZE (no PLOT) symbol chart radius, px: flat's nGridSize / 2
|
|
13149
13796
|
// (maptheme.js 21843), nGridSize the aggregation cell pitch / nAutoSize
|
|
@@ -13160,6 +13807,19 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13160
13807
|
const autoSize = rect ? (gap ? 1.15 : 1.0) : (gap ? 0.8 : 0.75);
|
|
13161
13808
|
return cellPx / autoSize * (styleNum(this.style.scale) || 1) / 2;
|
|
13162
13809
|
}
|
|
13810
|
+
|
|
13811
|
+
// flat's AUTOSIZE (maptheme.js 21831-21843): the biggest aggregated item is the size of its
|
|
13812
|
+
// grid cell — the radius GRIDSIZE gives every item (hexagon cell/1.5, RECT cell/2; GAP 1.6 /
|
|
13813
|
+
// 2.3) — and the others follow (value/max)^(1/sizepow). This is the biggest item's radius;
|
|
13814
|
+
// null without AUTOSIZE, without a grid, or on a GRIDSIZE/PLOT theme (those size themselves)
|
|
13815
|
+
_autoSizeMaxRadiusPx(zoom, liveZoom) {
|
|
13816
|
+
if (!this.flags.has('AUTOSIZE') || this.flags.has('GRIDSIZE') || this.flags.has('PLOT') || !this._clusterRadiusPx) return null;
|
|
13817
|
+
const fromZoom = this._clusterUsesFixedZoom ? GRIDWIDTH_METERS_REFERENCE_ZOOM : zoom;
|
|
13818
|
+
const cellPx = this._clusterRadiusPx * Math.pow(2, liveZoom - fromZoom);
|
|
13819
|
+
const rect = flatFlag(this.flags, 'RECT');
|
|
13820
|
+
const gap = flatFlag(this.flags, 'GAP');
|
|
13821
|
+
return cellPx / (rect ? (gap ? 2.3 : 2.0) : (gap ? 1.6 : 1.5)) * (styleNum(this.style.scale) || 1);
|
|
13822
|
+
}
|
|
13163
13823
|
_isItemPlot() {
|
|
13164
13824
|
return this.flags.has('PLOT') && !this.flags.has('GRIDSIZE') && String(this.binding.value || '').includes('|');
|
|
13165
13825
|
}
|
|
@@ -13310,7 +13970,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13310
13970
|
for (const f of this.features) {
|
|
13311
13971
|
const pos = itemAnchor(f.geometry);
|
|
13312
13972
|
if (!pos) continue;
|
|
13313
|
-
const values = fields.map(k =>
|
|
13973
|
+
const values = fields.map(k => scanValue(f.properties[k]));
|
|
13314
13974
|
for (const v of values) if (!isNaN(v)) { dMin = Math.min(dMin, v); dMax = Math.max(dMax, v); }
|
|
13315
13975
|
if (!active || active.has(f)) items.push({ geometry: { type: 'Point', coordinates: pos }, properties: { values, raw: f.properties } });
|
|
13316
13976
|
}
|
|
@@ -13427,9 +14087,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13427
14087
|
: Array.isArray(this.style.xaxis) ? this.style.xaxis : null;
|
|
13428
14088
|
if (explicit) {
|
|
13429
14089
|
if (!this._explicitCatIndex) this._explicitCatIndex = new Map(explicit.map((v, i) => [String(v), i]));
|
|
13430
|
-
|
|
14090
|
+
const i = this._explicitCatIndex.get(String(rawValue));
|
|
14091
|
+
return i === undefined ? this._explicitCatIndex.get(String(rawValue).trim()) : i;
|
|
13431
14092
|
}
|
|
13432
|
-
|
|
14093
|
+
// "$item$" is no field: every record counts 1 (resolveAggregateValue), so its class is the 1
|
|
14094
|
+
return this._resolveClassIndex(this.binding.value === '$item$' ? 1 : rawValue);
|
|
13433
14095
|
}
|
|
13434
14096
|
|
|
13435
14097
|
// pure: cellAggregatedValues → see the pure-function sections above the class
|
|
@@ -13770,15 +14432,93 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13770
14432
|
// style.colorscheme fill.
|
|
13771
14433
|
_buildGridMeshLayers(zoom) {
|
|
13772
14434
|
this._ensureGridIndex(zoom);
|
|
13773
|
-
const data = this._gridIndex.map(cell => {
|
|
13774
|
-
const center = worldPixelToLngLat(cell.px, cell.py, zoom);
|
|
13775
|
-
return { geometry: { type: 'Point', coordinates: [center.lng, center.lat] } };
|
|
13776
|
-
});
|
|
13777
|
-
|
|
13778
14435
|
const fillColor = (resolveColorScheme(this.style.colorscheme, ['']) || ['rgba(255,255,255,0.3)'])[0] || 'rgba(255,255,255,0.3)';
|
|
13779
14436
|
const borderColor = styleLineColor(this.style.linecolor);
|
|
13780
14437
|
const borderWidth = styleNum(this.style.linewidth);
|
|
13781
14438
|
const cellPx = this._gridCellPx;
|
|
14439
|
+
|
|
14440
|
+
// flat's "Hexbin - dynamic opacity" (CHART|SYMBOL|GRIDSIZE|AGGREGATE|SUM|DOPACITY,
|
|
14441
|
+
// symbols:["hexagon"]): hexagon cells, each cell's fill OPACITY ramped from its
|
|
14442
|
+
// aggregated value (resolveDopacityAlpha, the CHOROPLETH|DOPACITY formulas). The plain
|
|
14443
|
+
// one-color mesh (neither flag) stays the uniform square below.
|
|
14444
|
+
const hexCells = Array.isArray(this.style.symbols) && this.style.symbols[0] === 'hexagon';
|
|
14445
|
+
const dopacity = this.flags.has('DOPACITY') || this.flags.has('DOPACITYMIN')
|
|
14446
|
+
|| this.flags.has('DOPACITYMAX') || this.flags.has('DOPACITYMINMAX');
|
|
14447
|
+
if (hexCells || dopacity) {
|
|
14448
|
+
const cells = this._gridIndex;
|
|
14449
|
+
const baseOpacity = styleNum(this.style.fillopacity);
|
|
14450
|
+
const base = isNaN(baseOpacity) ? 0.9 : baseOpacity;
|
|
14451
|
+
// CATEGORICAL|DOMINANT: dominant category per cell (the biggest part above 0, the
|
|
14452
|
+
// first of equal ones — aggregatedCategoricalClass), opacity from that part against
|
|
14453
|
+
// the biggest dominant part of the same class over all cells (dominantDopacityAlpha)
|
|
14454
|
+
if (this._isDominantGrid()) {
|
|
14455
|
+
const labels = this._plotCategories();
|
|
14456
|
+
const rgb = resolveClassColors(this.style.colorscheme, labels, undefined) || [];
|
|
14457
|
+
const dom = cells.map(c => {
|
|
14458
|
+
let i = -1;
|
|
14459
|
+
c.sums.forEach((v, k) => { if (v > 0 && (i < 0 || v > c.sums[i])) i = k; });
|
|
14460
|
+
return i < 0 ? null : { i, v: c.sums[i] };
|
|
14461
|
+
});
|
|
14462
|
+
// flat's nMin/nMax: extremes of the cell TOTAL over all categories
|
|
14463
|
+
const totals = cells.map(c => c.sums.reduce((s, x) => s + x, 0));
|
|
14464
|
+
const tMin = Math.min(...totals), tMax = Math.max(...totals);
|
|
14465
|
+
const data = [];
|
|
14466
|
+
// legend click (ixmaps.markThemeClass): flat's evidence "isolate" shows only the cells
|
|
14467
|
+
// whose dominant category is marked; "isolate_gray" keeps the others, dimmed
|
|
14468
|
+
const marking = this._markedClasses.size && !this._onMarksChanged;
|
|
14469
|
+
const gray = String(this.style.evidence || 'isolate') === 'isolate_gray';
|
|
14470
|
+
cells.forEach((cell, n) => {
|
|
14471
|
+
const d = dom[n];
|
|
14472
|
+
if (!d) return;
|
|
14473
|
+
if (marking && !this._markedClasses.has(d.i) && !gray) return;
|
|
14474
|
+
const center = worldPixelToLngLat(cell.px, cell.py, zoom);
|
|
14475
|
+
let a = dopacity ? symbolDopacityAlpha(this.style, d.v, tMin, tMax, baseOpacity) : base;
|
|
14476
|
+
if (marking && !this._markedClasses.has(d.i)) a = DIMMED_ICON_ALPHA / 255;
|
|
14477
|
+
const c = rgb[d.i] || [130, 130, 130];
|
|
14478
|
+
data.push({ geometry: { type: 'Point', coordinates: [center.lng, center.lat] }, rgb: c, alpha: a });
|
|
14479
|
+
});
|
|
14480
|
+
// white hexagon, tinted per cell by getColor (rgb + alpha)
|
|
14481
|
+
const r = cellPx / 1.5;
|
|
14482
|
+
// mask icon: deck tints only mask icons with getColor's rgb (non-mask: alpha only)
|
|
14483
|
+
const icon = Object.assign({}, this._buildGridHexIcon('#ffffff', 'none', 0, r), { mask: true });
|
|
14484
|
+
return [new IconLayer({
|
|
14485
|
+
id: `ix-grid-${this.name}-g${this._iconGeneration}`,
|
|
14486
|
+
data, pickable: false,
|
|
14487
|
+
getPosition: d => d.geometry.coordinates,
|
|
14488
|
+
getIcon: () => icon,
|
|
14489
|
+
getColor: d => [d.rgb[0], d.rgb[1], d.rgb[2], Math.round(255 * d.alpha)],
|
|
14490
|
+
getSize: 2 * r,
|
|
14491
|
+
sizeUnits: 'pixels'
|
|
14492
|
+
})];
|
|
14493
|
+
}
|
|
14494
|
+
// cell-level value stats (the grid IS the dataset for DOPACITY here)
|
|
14495
|
+
const vals = cells.map(c => c.sums[0]).filter(v => v != null && !isNaN(v)).sort((a, b) => a - b);
|
|
14496
|
+
const vMin = vals[0], vMax = vals[vals.length - 1];
|
|
14497
|
+
const data = cells.map(cell => {
|
|
14498
|
+
const center = worldPixelToLngLat(cell.px, cell.py, zoom);
|
|
14499
|
+
const a = dopacity ? symbolDopacityAlpha(this.style, cell.sums[0], vMin, vMax, baseOpacity) : base;
|
|
14500
|
+
return { geometry: { type: 'Point', coordinates: [center.lng, center.lat] }, alpha: a == null ? base : a };
|
|
14501
|
+
});
|
|
14502
|
+
// hexagon tiling (snapToAggregationGrid): column pitch cellPx = 1.5 r, so r = cellPx / 1.5
|
|
14503
|
+
const r = cellPx / 1.5;
|
|
14504
|
+
const icon = hexCells
|
|
14505
|
+
? this._buildGridHexIcon(fillColor, borderColor, isNaN(borderWidth) ? 0 : borderWidth, r)
|
|
14506
|
+
: this._buildGridSquareIcon(fillColor, borderColor, isNaN(borderWidth) ? 0 : borderWidth, cellPx);
|
|
14507
|
+
return [new IconLayer({
|
|
14508
|
+
id: `ix-grid-${this.name}-g${this._iconGeneration}`,
|
|
14509
|
+
data, pickable: false,
|
|
14510
|
+
getPosition: d => d.geometry.coordinates,
|
|
14511
|
+
getIcon: () => icon,
|
|
14512
|
+
getColor: d => [255, 255, 255, Math.round(255 * d.alpha)],
|
|
14513
|
+
getSize: hexCells ? 2 * r : cellPx,
|
|
14514
|
+
sizeUnits: 'pixels'
|
|
14515
|
+
})];
|
|
14516
|
+
}
|
|
14517
|
+
|
|
14518
|
+
const data = this._gridIndex.map(cell => {
|
|
14519
|
+
const center = worldPixelToLngLat(cell.px, cell.py, zoom);
|
|
14520
|
+
return { geometry: { type: 'Point', coordinates: [center.lng, center.lat] } };
|
|
14521
|
+
});
|
|
13782
14522
|
const icon = this._buildGridSquareIcon(fillColor, borderColor, isNaN(borderWidth) ? 0 : borderWidth, cellPx);
|
|
13783
14523
|
|
|
13784
14524
|
return [new IconLayer({
|
|
@@ -13895,8 +14635,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13895
14635
|
// drawn now (fresh), else a cached chart's: the previous drawing's copy
|
|
13896
14636
|
const def = this._userChartGroup.querySelector('#' + CSS.escape(id)) || this._userChartGroup.ownerDocument.getElementById(id);
|
|
13897
14637
|
if (def) paneDefs.appendChild(def.cloneNode(true));
|
|
14638
|
+
else if (chart.defsById && chart.defsById[id]) paneDefs.insertAdjacentHTML('beforeend', chart.defsById[id]);
|
|
13898
14639
|
}
|
|
13899
|
-
items.push({ d, coords: d.geometry.coordinates, ox: chart.ox, oy: chart.oy, s: pxPerUnit,
|
|
14640
|
+
items.push({ d, coords: d.geometry.coordinates, ox: chart.ox, oy: chart.oy, s: pxPerUnit, pick: chart.pick,
|
|
13900
14641
|
r: Math.max(chart.vw, chart.vh) * pxPerUnit / 2,
|
|
13901
14642
|
// the chart's measured box (flat's getBox of the chart group) in px
|
|
13902
14643
|
// around the map position: the pane draws a chart point at
|
|
@@ -13939,14 +14680,31 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13939
14680
|
positionOf: b => b.d.geometry.coordinates, offsetOf: () => [0, 0],
|
|
13940
14681
|
ids: { circle: `ix-user-box-${this.name}`, icon: `ix-user-box-${this.name}`, polygons: `ix-user-box-polygons-${this.name}`, titles: `ix-user-titles-${this.name}` }
|
|
13941
14682
|
}) : { box: [], titles: [] };
|
|
13942
|
-
|
|
14683
|
+
// each chart answers to its own outline: the polygons sampled from its drawing (else its box), in px
|
|
14684
|
+
// around the map position, here as lng/lat at this zoom (a px is 360/(512·2^z) degrees across,
|
|
14685
|
+
// times cos(lat) up); a few px of transparent stroke keep a thin peak easy to hit
|
|
14686
|
+
const degPerPx = 360 / (512 * Math.pow(2, liveZoom));
|
|
14687
|
+
const pickFeatures = items.map(it => {
|
|
14688
|
+
const [lng, lat] = it.coords;
|
|
14689
|
+
const kx = it.s * degPerPx, ky = it.s * degPerPx * Math.cos(lat * Math.PI / 180);
|
|
14690
|
+
const rings = it.pick && it.pick.length ? it.pick
|
|
14691
|
+
: [[[it.ext[0] / it.s, it.ext[1] / it.s], [it.ext[2] / it.s, it.ext[1] / it.s], [it.ext[2] / it.s, it.ext[3] / it.s], [it.ext[0] / it.s, it.ext[3] / it.s]]];
|
|
14692
|
+
const polys = rings.map(r => {
|
|
14693
|
+
const ring = r.map(([x, y]) => [lng + x * kx, lat - y * ky]);
|
|
14694
|
+
ring.push(ring[0]);
|
|
14695
|
+
return [ring];
|
|
14696
|
+
});
|
|
14697
|
+
// icon + d: what pickedFeature unwraps to the item
|
|
14698
|
+
return { type: 'Feature', geometry: { type: 'MultiPolygon', coordinates: polys }, properties: {}, icon: true, d: it.d };
|
|
14699
|
+
});
|
|
14700
|
+
this._userChartLastLayers = [...boxLayers.box, new GeoJsonLayer({
|
|
13943
14701
|
id: `ix-bubbles-${this.name}`,
|
|
13944
|
-
data:
|
|
14702
|
+
data: { type: 'FeatureCollection', features: pickFeatures },
|
|
13945
14703
|
pickable: true,
|
|
13946
|
-
|
|
13947
|
-
|
|
13948
|
-
|
|
13949
|
-
|
|
14704
|
+
filled: true, stroked: true,
|
|
14705
|
+
// alpha 1/255: invisible, but never a fully transparent fragment a renderer might skip
|
|
14706
|
+
getFillColor: [0, 0, 0, 1], getLineColor: [0, 0, 0, 1],
|
|
14707
|
+
lineWidthUnits: 'pixels', getLineWidth: 4,
|
|
13950
14708
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
13951
14709
|
}), ...boxLayers.titles];
|
|
13952
14710
|
return this._userChartLastLayers;
|
|
@@ -13959,11 +14717,40 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13959
14717
|
const raw = d.properties.raw || d.properties;
|
|
13960
14718
|
const titleField = this.style.titlefield || this.binding.title;
|
|
13961
14719
|
const title = titleField && raw && raw[titleField] != null ? String(raw[titleField]) : undefined;
|
|
13962
|
-
|
|
14720
|
+
// flat hands the chart the item's value fields: values = every field of binding.value (after the
|
|
14721
|
+
// field100 arithmetic — the record already carries it) and value = the first of them
|
|
14722
|
+
// (maptheme.js 19667, nValuesA[0]); a theme with one value field keeps the item's value
|
|
14723
|
+
let values = [value];
|
|
14724
|
+
const fieldNames = this.binding.value && !d.properties.counts ? String(this.binding.value).split('|') : [];
|
|
14725
|
+
if (fieldNames.length > 1) {
|
|
14726
|
+
const parts = fieldNames.map(f => scanValue(raw && raw[f[0] === '!' ? f.slice(1) : f]));
|
|
14727
|
+
if (parts.some(v => !isNaN(v))) {
|
|
14728
|
+
values = parts.map(v => (isNaN(v) ? 0 : v));
|
|
14729
|
+
// COMPOSECOLOR blends all the fields into one colour, so the chart's value is the one that wins it —
|
|
14730
|
+
// the highest field (flat itself would hand over the first field here: a deliberate departure,
|
|
14731
|
+
// so a "party with the highest result" map prints that party's share)
|
|
14732
|
+
value = this.flags.has('COMPOSECOLOR') ? Math.max(...values) : values[0];
|
|
14733
|
+
}
|
|
14734
|
+
}
|
|
14735
|
+
// DOMINANT: the chart is the dominant field's — its class colour and its value (flat's USER branch,
|
|
14736
|
+
// maptheme.js ~19700-19770: nClass = the dominant index, nValue = nValuesA[nIndex]); the relevance
|
|
14737
|
+
// (plain value, PERCENTOFMEAN, DEVIATION) is the one the DOMINANT choropleth uses
|
|
14738
|
+
if (this.flags.has('DOMINANT') && !this.flags.has('COMPOSECOLOR') && this._multiFields && this._dominantMins && raw && fieldNames.length > 1) {
|
|
14739
|
+
const dominant = this._resolveDominantClass(raw);
|
|
14740
|
+
if (dominant) { value = dominant.value; cat = dominant.index; }
|
|
14741
|
+
}
|
|
14742
|
+
// COMPOSECOLOR: the chart takes the item's blended colour (flat's itemA[a].szColor, the same
|
|
14743
|
+
// blend the choropleth uses) instead of one class colour
|
|
14744
|
+
let composed;
|
|
14745
|
+
if (this.flags.has('COMPOSECOLOR') && this._multiFields && raw) {
|
|
14746
|
+
const c = this._resolveComposedColor(raw);
|
|
14747
|
+
composed = `rgb(${c[0]},${c[1]},${c[2]})`;
|
|
14748
|
+
}
|
|
14749
|
+
const base = `user|${name}|${cat}|${Math.round(value * 100) / 100}|${values.length > 1 ? values.join(',') : ''}|${composed || ''}|${Math.round(size * 2) / 2}|${flag}|${title || ''}`;
|
|
13963
14750
|
if (!this._userChartSvgs) this._userChartSvgs = new Map();
|
|
13964
14751
|
let chart = this._userChartSvgs.get(base);
|
|
13965
14752
|
if (chart === undefined) {
|
|
13966
|
-
chart = this._drawUserChartSvg(draw, name, theme, flag, d, value, cat, size, nSize);
|
|
14753
|
+
chart = this._drawUserChartSvg(draw, name, theme, flag, d, value, cat, size, nSize, values, composed);
|
|
13967
14754
|
if (chart === undefined) return null;
|
|
13968
14755
|
if (this._userChartSvgs.size >= ICON_CACHE_MAX) this._userChartSvgs.delete(this._userChartSvgs.keys().next().value);
|
|
13969
14756
|
this._userChartSvgs.set(base, chart);
|
|
@@ -13973,10 +14760,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13973
14760
|
// the page's chart function draws one chart into the hidden SVG: its
|
|
13974
14761
|
// markup and box (chart units), null when it draws nothing, undefined
|
|
13975
14762
|
// when it fails
|
|
13976
|
-
_drawUserChartSvg(draw, name, theme, flag, d, value, cat, size, nSize) {
|
|
14763
|
+
_drawUserChartSvg(draw, name, theme, flag, d, value, cat, size, nSize, values, composedColor) {
|
|
13977
14764
|
const g = document.createElementNS(SVG_NS, 'g');
|
|
13978
14765
|
this._userChartGroup.appendChild(g);
|
|
13979
|
-
const color = this._userChartColor(cat);
|
|
14766
|
+
const color = composedColor || this._userChartColor(cat);
|
|
13980
14767
|
const raw = d.properties.raw || d.properties;
|
|
13981
14768
|
const label = this.binding.title && raw && raw[this.binding.title] != null ? String(raw[this.binding.title]) : undefined;
|
|
13982
14769
|
// flat's chart title (style.titlefield, maptheme's item.szTitle) —
|
|
@@ -13986,7 +14773,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13986
14773
|
let chart = null;
|
|
13987
14774
|
try {
|
|
13988
14775
|
const ret = draw.call(global.ixmaps, document, {
|
|
13989
|
-
target: g, theme, value, values: [value], size, maxSize: USER_CHART_MAX_SIZE, color, class: cat, flag,
|
|
14776
|
+
target: g, theme, value, values: values || [value], size, maxSize: USER_CHART_MAX_SIZE, color, class: cat, flag,
|
|
13990
14777
|
// flat hands the chart the item's color CLASS object (maptheme's
|
|
13991
14778
|
// ColorScheme entry) — pinnacleChart reads ccolor.lowColor for its
|
|
13992
14779
|
// VALUES label fill (usercharts/d3/chart.js 199)
|
|
@@ -13997,13 +14784,51 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13997
14784
|
if (bb && bb.width > 0 && bb.height > 0) {
|
|
13998
14785
|
const markup = new XMLSerializer().serializeToString(g);
|
|
13999
14786
|
const ids = new Set([...markup.matchAll(/url\(#([^)"']+)\)/g)].map(m => m[1]));
|
|
14000
|
-
|
|
14787
|
+
// the gradients the chart uses, kept WITH the chart: a cached chart that was not on the screen in the
|
|
14788
|
+
// last drawing finds no copy of them in the document any more (they are one per colour, and only the
|
|
14789
|
+
// theme's own colours are defined again by _init), and would lose its fill
|
|
14790
|
+
const defsById = {};
|
|
14791
|
+
for (const id of ids) {
|
|
14792
|
+
const el = this._userChartGroup.ownerDocument.getElementById(id);
|
|
14793
|
+
if (el) defsById[id] = new XMLSerializer().serializeToString(el);
|
|
14794
|
+
}
|
|
14795
|
+
const defs = Object.values(defsById).join('');
|
|
14001
14796
|
// strokes and text halos reach past the geometric box
|
|
14002
14797
|
const pad = Math.max(bb.width, bb.height) * 0.06 + 10;
|
|
14003
14798
|
const vx = bb.x - pad, vy = bb.y - pad, vw = bb.width + 2 * pad, vh = bb.height + 2 * pad;
|
|
14004
14799
|
// the chart's origin (0,0 — or the point flat moves it to, ret) sits at the map position
|
|
14005
14800
|
const ox = (ret && ret.x) || 0, oy = (ret && ret.y) || 0;
|
|
14006
|
-
|
|
14801
|
+
// the chart's own outline for hover and click: its shapes (the peak's triangle, a pie's circle ...)
|
|
14802
|
+
// sampled into polygons, in chart units around the origin — not the box around them (a tall peak
|
|
14803
|
+
// would otherwise answer to a rectangle, or a circle, far wider than the peak itself)
|
|
14804
|
+
const pick = [];
|
|
14805
|
+
try {
|
|
14806
|
+
const inv = g.getCTM().inverse();
|
|
14807
|
+
const at = (el, x, y) => { const p = new DOMPoint(x, y).matrixTransform(inv.multiply(el.getCTM())); return [p.x - ox, p.y - oy]; };
|
|
14808
|
+
g.querySelectorAll('path,rect,circle,ellipse,polygon').forEach(el => {
|
|
14809
|
+
if (el.closest('defs,clipPath,mask')) return;
|
|
14810
|
+
let ring = null;
|
|
14811
|
+
if (el.tagName === 'path') {
|
|
14812
|
+
const len = el.getTotalLength();
|
|
14813
|
+
if (!(len > 0)) return;
|
|
14814
|
+
const n = Math.max(8, Math.min(40, Math.ceil(len / 50)));
|
|
14815
|
+
ring = [];
|
|
14816
|
+
for (let i = 0; i <= n; i++) { const q = el.getPointAtLength(len * i / n); ring.push(at(el, q.x, q.y)); }
|
|
14817
|
+
} else if (el.tagName === 'rect') {
|
|
14818
|
+
const x = el.x.baseVal.value, y = el.y.baseVal.value, w = el.width.baseVal.value, h = el.height.baseVal.value;
|
|
14819
|
+
ring = [at(el, x, y), at(el, x + w, y), at(el, x + w, y + h), at(el, x, y + h)];
|
|
14820
|
+
} else if (el.tagName === 'polygon') {
|
|
14821
|
+
ring = [...el.points].map(q => at(el, q.x, q.y));
|
|
14822
|
+
} else {
|
|
14823
|
+
const cx = el.cx.baseVal.value, cy = el.cy.baseVal.value;
|
|
14824
|
+
const rx = el.tagName === 'circle' ? el.r.baseVal.value : el.rx.baseVal.value, ry = el.tagName === 'circle' ? rx : el.ry.baseVal.value;
|
|
14825
|
+
ring = [];
|
|
14826
|
+
for (let i = 0; i < 12; i++) ring.push(at(el, cx + rx * Math.cos(i * Math.PI / 6), cy + ry * Math.sin(i * Math.PI / 6)));
|
|
14827
|
+
}
|
|
14828
|
+
if (ring && ring.length >= 3 && ring.every(q => isFinite(q[0]) && isFinite(q[1]))) pick.push(ring);
|
|
14829
|
+
});
|
|
14830
|
+
} catch (e) { /* the box below stands in */ }
|
|
14831
|
+
chart = { vx, vy, vw, vh, ox, oy, markup, ids: [...ids], defsById, pick, bb: { x: bb.x, y: bb.y, width: bb.width, height: bb.height },
|
|
14007
14832
|
svg: `<svg xmlns="${SVG_NS}" width="__W__" height="__H__" viewBox="${vx} ${vy} ${vw} ${vh}"><defs>${defs}</defs>${markup}</svg>` };
|
|
14008
14833
|
}
|
|
14009
14834
|
} catch (e) {
|
|
@@ -14222,6 +15047,30 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14222
15047
|
return this._cacheIcon(key, icon);
|
|
14223
15048
|
}
|
|
14224
15049
|
|
|
15050
|
+
// one flat-top hexagon of circumradius r (px) filling a square raster — the GRIDSIZE
|
|
15051
|
+
// "hexagon" cell (see _buildGridMeshLayers); drawSymbolPath's own hexagon outline
|
|
15052
|
+
_buildGridHexIcon(fillColor, borderColor, borderWidth, r) {
|
|
15053
|
+
const key = `gridhex|${fillColor}|${borderColor}|${borderWidth}|${r}`;
|
|
15054
|
+
if (this._iconCache.has(key)) return this._iconCache.get(key);
|
|
15055
|
+
const size = Math.max(32, Math.min(256, Math.round(2 * r) || 32));
|
|
15056
|
+
const canvas = document.createElement('canvas');
|
|
15057
|
+
canvas.width = size; canvas.height = size;
|
|
15058
|
+
const ctx = canvas.getContext('2d');
|
|
15059
|
+
const rr = size / 2 - 1; // 1px margin so the stroke isn't clipped
|
|
15060
|
+
ctx.beginPath();
|
|
15061
|
+
drawSymbolPath(ctx, 'hexagon', size / 2, size / 2, rr);
|
|
15062
|
+
ctx.closePath();
|
|
15063
|
+
ctx.fillStyle = fillColor;
|
|
15064
|
+
ctx.fill();
|
|
15065
|
+
if (borderColor && borderColor !== 'none' && borderWidth > 0) {
|
|
15066
|
+
ctx.strokeStyle = borderColor;
|
|
15067
|
+
ctx.lineWidth = borderWidth * (size / (2 * r || size));
|
|
15068
|
+
ctx.stroke();
|
|
15069
|
+
}
|
|
15070
|
+
const icon = { url: canvas.toDataURL(), width: size, height: size, anchorX: size / 2, anchorY: size / 2, id: key };
|
|
15071
|
+
return this._cacheIcon(key, icon);
|
|
15072
|
+
}
|
|
15073
|
+
|
|
14225
15074
|
_buildGridSquareIcon(fillColor, borderColor, borderWidth, cellPx) {
|
|
14226
15075
|
const key = `grid|${fillColor}|${borderColor}|${borderWidth}|${cellPx}`;
|
|
14227
15076
|
if (this._iconCache.has(key)) return this._iconCache.get(key);
|
|
@@ -14250,6 +15099,135 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14250
15099
|
return this._cacheIcon(key, icon);
|
|
14251
15100
|
}
|
|
14252
15101
|
|
|
15102
|
+
// .type("KDE"): the kernel density of the VISIBLE records as stacked bands (the fire pages' KDE).
|
|
15103
|
+
// The records inside the view (plus 3 bandwidths around it, which still add density to it) go
|
|
15104
|
+
// to kdeGrid in a worker, d3-contour cuts the bands at fractions of the maximum (style.kdelevels,
|
|
15105
|
+
// default 0.15 / 0.30 / 0.50 / 0.70), each band the area at or above its threshold, so they
|
|
15106
|
+
// stack, colored from style.colorscheme, drawn with style.fillopacity. The bandwidth is
|
|
15107
|
+
// style.bandwidth (km) or, without it, follows the zoom (kdeBandwidthKm); style.bandwidthscale
|
|
15108
|
+
// (default 1) multiplies either, style.bandwidthzoompow (default 1) flattens the zoom dependence, so a radius control can stay zoom-aware without lagging a redraw.
|
|
15109
|
+
// The result belongs to a view: while the map moves the last bands stay, and once it stands still
|
|
15110
|
+
// (and the view, the records, the binding or the bandwidth changed) the new ones are computed
|
|
15111
|
+
// after a short pause and drawn by a redraw — never more than one computation wanted at a time.
|
|
15112
|
+
// Weights as HEATMAP: the size field, else the numeric value field, else 1 each ($item$).
|
|
15113
|
+
_buildKdeLayers(zoom, bbox, globeCenter) {
|
|
15114
|
+
// the bands are lng/lat polygons of a flat view: under the globe there is no such view — say so once
|
|
15115
|
+
if (globeCenter) {
|
|
15116
|
+
if (!this._warnedKdeGlobe) {
|
|
15117
|
+
this._warnedKdeGlobe = true;
|
|
15118
|
+
console.warn(`[ixmaps-gl] layer "${this.name}": KDE is not drawn under the globe projection (its bands are computed for a flat view) — use mapProjection mercator`);
|
|
15119
|
+
}
|
|
15120
|
+
return [];
|
|
15121
|
+
}
|
|
15122
|
+
const source = this._activeFeatures || this.features;
|
|
15123
|
+
if (this._kdeSource !== source) { this._kdeSource = source; this._kdeSourceId = (this._kdeSourceId || 0) + 1; }
|
|
15124
|
+
const flatZoom = mapLibreToFlatZoom(zoom);
|
|
15125
|
+
const bwScale = styleNum(this.style.bandwidthscale) > 0 ? styleNum(this.style.bandwidthscale) : 1;
|
|
15126
|
+
// style.bandwidthzoompow (0..1) flattens the zoom dependence: the radius is the table's ratio to its
|
|
15127
|
+
// value at the reference flat zoom (style.bandwidthzoomref, default 12), raised to this power
|
|
15128
|
+
// (0 = constant). Without style.bandwidth the radius is the table's, default power 1 (as the fire
|
|
15129
|
+
// pages); WITH style.bandwidth (km) that is the radius AT the reference zoom, default power 0 (fixed).
|
|
15130
|
+
const explicitKm = styleNum(this.style.bandwidth) > 0 ? styleNum(this.style.bandwidth) : 0;
|
|
15131
|
+
const powStyle = styleNum(this.style.bandwidthzoompow);
|
|
15132
|
+
const zoomPow = powStyle >= 0 ? Math.min(1, powStyle) : (explicitKm ? 0 : 1);
|
|
15133
|
+
const refZoom = styleNum(this.style.bandwidthzoomref) > 0 ? styleNum(this.style.bandwidthzoomref) : 12;
|
|
15134
|
+
const tableKm = kdeBandwidthKm(flatZoom), refKm = kdeBandwidthKm(refZoom);
|
|
15135
|
+
const zoomRatio = zoomPow === 1 ? tableKm / refKm : Math.pow(tableKm / refKm, zoomPow);
|
|
15136
|
+
const bwKm = (explicitKm ? explicitKm * zoomRatio : (zoomPow === 1 ? tableKm : refKm * zoomRatio)) * bwScale;
|
|
15137
|
+
const maxCells = kdeMaxCells(flatZoom);
|
|
15138
|
+
const levels = kdeLevels(this.style);
|
|
15139
|
+
const key = JSON.stringify([this._dataGen, this._kdeSourceId, this.binding, bwKm, maxCells, levels, bbox.map(v => Math.round(v * 1e4) / 1e4)]);
|
|
15140
|
+
if (key !== this._kdeKeyDone && key !== this._kdeKeyWanted && !this._viewMoving) {
|
|
15141
|
+
this._kdeKeyWanted = key;
|
|
15142
|
+
clearTimeout(this._kdeTimer);
|
|
15143
|
+
this._kdeTimer = setTimeout(() => {
|
|
15144
|
+
const margin = 3 * bwKm;
|
|
15145
|
+
const mLat = margin / 111.3;
|
|
15146
|
+
const mLng = margin / (111.3 * Math.max(0.05, Math.cos((bbox[1] + bbox[3]) / 2 * Math.PI / 180)));
|
|
15147
|
+
const points = kdePoints(source, this.binding, [bbox[0] - mLng, bbox[1] - mLat, bbox[2] + mLng, bbox[3] + mLat]);
|
|
15148
|
+
Promise.all([ensureD3Contour(), kdeCompute(points, bwKm, maxCells)]).then(([contours, grid]) => {
|
|
15149
|
+
if (this._kdeKeyWanted !== key) return; // the view moved on: a newer computation is wanted
|
|
15150
|
+
this._kdeBands = kdeBands(grid, levels, () => contours());
|
|
15151
|
+
this._kdeBandsGen = (this._kdeBandsGen || 0) + 1; // every set of bands is a new layer for deck.gl
|
|
15152
|
+
this._kdeLevelCount = levels.length;
|
|
15153
|
+
this._kdeBandsBw = bwKm; // the radius these bands were computed for
|
|
15154
|
+
this._kdeKeyDone = key;
|
|
15155
|
+
if (this._triggerRedraw) this._triggerRedraw();
|
|
15156
|
+
}).catch(err => {
|
|
15157
|
+
console.error(`[ixmaps-gl] layer "${this.name}": KDE not computed:`, err);
|
|
15158
|
+
if (this._kdeKeyWanted === key) this._kdeKeyDone = key; // not retried until the view changes
|
|
15159
|
+
});
|
|
15160
|
+
}, 150);
|
|
15161
|
+
}
|
|
15162
|
+
const bands = this._kdeBands;
|
|
15163
|
+
if (!bands || !bands.features.length) return [];
|
|
15164
|
+
// bands kept from before the layer was switched off belong to another view and radius: they are not
|
|
15165
|
+
// shown on switching on until the right ones are computed (a wrong-radius density flashing first
|
|
15166
|
+
// reads as flicker). Once the right ones are there the gate opens, and while the map zooms the
|
|
15167
|
+
// last bands stay as before — a gap on every zoom step would be worse.
|
|
15168
|
+
if (this._kdeShowGate) {
|
|
15169
|
+
if (this._kdeBandsBw !== bwKm) return [];
|
|
15170
|
+
this._kdeShowGate = false;
|
|
15171
|
+
}
|
|
15172
|
+
const colors = kdeBandColors(this.style.colorscheme, this._kdeLevelCount || levels.length);
|
|
15173
|
+
return [new GeoJsonLayer({
|
|
15174
|
+
// a new id for each set of bands: deck.gl keeps a layer's buffers by id, and bands whose polygon
|
|
15175
|
+
// structure changed (holes, counts) under the SAME id drew long triangles between polygons
|
|
15176
|
+
id: this._dataLayerId(`ix-kde-${this.name}-b${this._kdeBandsGen || 0}`),
|
|
15177
|
+
data: bands, pickable: false, stroked: false, filled: true,
|
|
15178
|
+
getFillColor: f => colors[f.properties.level] || colors[colors.length - 1],
|
|
15179
|
+
updateTriggers: { getFillColor: [JSON.stringify(this.style.colorscheme), colors.length] },
|
|
15180
|
+
opacity: styleNum(this.style.fillopacity) || 0.25
|
|
15181
|
+
})];
|
|
15182
|
+
}
|
|
15183
|
+
|
|
15184
|
+
// .type("HEATMAP"): a kernel-density surface (deck.gl HeatmapLayer) of the records' positions,
|
|
15185
|
+
// weighted by heatmapWeight. style.radiuspx (default 30) is the kernel radius in screen pixels
|
|
15186
|
+
// at the reference zoom; it follows the map's object scaling like every symbol
|
|
15187
|
+
// (objectZoomFactor), i.e. it grows and shrinks with the zoom like a real-world distance.
|
|
15188
|
+
// style: scale (radius multiplier), intensity, threshold, colorscheme, fillopacity.
|
|
15189
|
+
//
|
|
15190
|
+
// PERFORMANCE: the layer is rebuilt on every frame of a zoom gesture. A new data array, a new
|
|
15191
|
+
// colorRange or a new radius makes deck redo its GPU work (weight map, color texture) — measured
|
|
15192
|
+
// 90 ms a frame with them rebuilt each time, 15 ms with the same array/colors and a radius that
|
|
15193
|
+
// moves in steps of 2^(1/6) (~12%). So the data and the colors are kept until the records, the
|
|
15194
|
+
// binding or the colorscheme change, and the radius is quantized.
|
|
15195
|
+
_buildHeatmapLayers(liveZoom) {
|
|
15196
|
+
const source = this._activeFeatures || this.features;
|
|
15197
|
+
const bindingKey = JSON.stringify(this.binding);
|
|
15198
|
+
if (!this._heatmapData || this._heatmapSource !== source || this._heatmapBinding !== bindingKey) {
|
|
15199
|
+
const data = [];
|
|
15200
|
+
for (const f of source) {
|
|
15201
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
15202
|
+
if (!c || typeof c[0] !== 'number' || !isFinite(c[0]) || !isFinite(c[1])) continue;
|
|
15203
|
+
const w = heatmapWeight(this.binding, f.properties || {});
|
|
15204
|
+
if (w > 0) data.push({ position: [c[0], c[1]], weight: w });
|
|
15205
|
+
}
|
|
15206
|
+
this._heatmapData = data;
|
|
15207
|
+
this._heatmapSource = source;
|
|
15208
|
+
this._heatmapBinding = bindingKey;
|
|
15209
|
+
}
|
|
15210
|
+
const colorKey = JSON.stringify(this.style.colorscheme);
|
|
15211
|
+
if (!this._heatmapColors || this._heatmapColorKey !== colorKey) {
|
|
15212
|
+
this._heatmapColors = heatmapColorRange(this.style.colorscheme);
|
|
15213
|
+
this._heatmapColorKey = colorKey;
|
|
15214
|
+
}
|
|
15215
|
+
const radiusPx = (styleNum(this.style.radiuspx) || 30) * (styleNum(this.style.scale) || 1)
|
|
15216
|
+
* objectZoomFactor(liveZoom, this.mapOptions);
|
|
15217
|
+
const stepped = Math.pow(2, Math.round(Math.log2(Math.max(1, radiusPx)) * 6) / 6);
|
|
15218
|
+
return [new HeatmapLayer({
|
|
15219
|
+
id: `ix-heatmap-${this.name}`,
|
|
15220
|
+
data: this._heatmapData, pickable: false,
|
|
15221
|
+
getPosition: d => d.position,
|
|
15222
|
+
getWeight: d => d.weight,
|
|
15223
|
+
radiusPixels: Math.max(1, Math.min(400, stepped)),
|
|
15224
|
+
intensity: styleNum(this.style.intensity) || 1,
|
|
15225
|
+
threshold: styleNum(this.style.threshold) || 0.05,
|
|
15226
|
+
colorRange: this._heatmapColors,
|
|
15227
|
+
opacity: styleNum(this.style.fillopacity) || 1
|
|
15228
|
+
})];
|
|
15229
|
+
}
|
|
15230
|
+
|
|
14253
15231
|
// .type("DOT") — one point per record, always (no clustering, matching
|
|
14254
15232
|
// the real engine bypassing AGGREGATE for this type). Color comes from
|
|
14255
15233
|
// a bound value's class — CATEGORICAL (exact match) or numeric
|
|
@@ -14265,7 +15243,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14265
15243
|
const data = source.map(f => ({
|
|
14266
15244
|
geometry: f.geometry,
|
|
14267
15245
|
properties: {
|
|
14268
|
-
value: this.binding.size ?
|
|
15246
|
+
value: this.binding.size ? scanValue(f.properties[this.binding.size]) : undefined,
|
|
14269
15247
|
raw: f.properties,
|
|
14270
15248
|
cat: hasClasses ? this._resolveClassIndex(f.properties[this.binding.value]) : null
|
|
14271
15249
|
}
|
|
@@ -14522,7 +15500,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14522
15500
|
// GRIDSIZE: every chart the size of its grid cell, whatever its value
|
|
14523
15501
|
// (_gridChartRadiusPx); otherwise valueRadius
|
|
14524
15502
|
const gridR = this._gridChartRadiusPx(zoom, liveZoom);
|
|
14525
|
-
|
|
15503
|
+
// AUTOSIZE: the biggest aggregated item is the size of the cell, the others by value
|
|
15504
|
+
const autoR = this._autoSizeMaxRadiusPx(zoom, liveZoom);
|
|
15505
|
+
const autoMax = autoR != null ? (this._ensureAggregateStats(zoom).sizeMax || 1) : 1;
|
|
15506
|
+
const autoPow = autoR != null ? resolveSizePow(this.style, this.flags) : 1;
|
|
15507
|
+
const vRadius = v => (gridR != null ? gridR
|
|
15508
|
+
: autoR != null ? Math.max(VALUE_RADIUS_MIN, autoR * Math.pow(Math.abs(v || 0) / autoMax, 1 / autoPow))
|
|
15509
|
+
: valueRadius(v, liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue, this._sizeBound()));
|
|
14526
15510
|
// AGGREGATE sums without a normalsizevalue: flat's default is the
|
|
14527
15511
|
// largest aggregated value (defaultNormalSizeValue leaves it unset, the
|
|
14528
15512
|
// sizes depend on the aggregation)
|
|
@@ -14738,7 +15722,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14738
15722
|
getPosition: d => d.geometry.coordinates,
|
|
14739
15723
|
getIcon: d => this._getGlowIcon((this.categoryColorsRgb || [])[d.properties.counts ? dominant(this._groupIsolation(d) ? d.properties.counts.map((c, i) => (this._markedClasses.has(i) ? c : 0)) : d.properties.counts) : d.properties.cat]),
|
|
14740
15724
|
getSize: d => vRadius(sizeValueOf(d)) * (d.properties.counts ? 9 : 11),
|
|
14741
|
-
getColor: d => [255, 255, 255, this._iconAlpha(d)],
|
|
15725
|
+
getColor: d => [255, 255, 255, Math.round(this._iconAlpha(d) * glowOpacityOf(this.style))],
|
|
14742
15726
|
...(alignOf.active ? { getPixelOffset: alignOf } : {}),
|
|
14743
15727
|
sizeUnits: 'pixels',
|
|
14744
15728
|
billboard: true,
|
|
@@ -15150,7 +16134,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15150
16134
|
if (multi && auto100 && !this._seqAuto100Max) {
|
|
15151
16135
|
let m = 0;
|
|
15152
16136
|
for (const f of this.features) {
|
|
15153
|
-
const parts = this._multiFields.map(k =>
|
|
16137
|
+
const parts = this._multiFields.map(k => scanValue(f.properties && f.properties[k]) || 0);
|
|
15154
16138
|
const sum = parts.reduce((a, v) => a + v, 0);
|
|
15155
16139
|
if (sum > 0) for (const v of parts) { const nv = v / (sum / 100); if (nv > m) m = nv; }
|
|
15156
16140
|
}
|
|
@@ -15171,12 +16155,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15171
16155
|
const sizeField = multi && this.binding.size && this.binding.size !== '$item$' ? this.binding.size : null;
|
|
15172
16156
|
let sizeMax = 0;
|
|
15173
16157
|
if (sizeField) {
|
|
15174
|
-
for (const f of this.features) { const v = Math.abs(
|
|
16158
|
+
for (const f of this.features) { const v = Math.abs(scanValue(f.properties && f.properties[sizeField]) || 0); if (v > sizeMax) sizeMax = v; }
|
|
15175
16159
|
}
|
|
15176
16160
|
const itemSizeFactor = d => {
|
|
15177
16161
|
if (!sizeField) return 1;
|
|
15178
16162
|
const sz = typeof d.properties.size === 'number' ? d.properties.size
|
|
15179
|
-
: Math.abs(
|
|
16163
|
+
: Math.abs(scanValue((d.properties.raw || {})[sizeField]) || 0);
|
|
15180
16164
|
return sizeMax > 0 ? Math.pow(sz / sizeMax, 1 / sizePow) : 0;
|
|
15181
16165
|
};
|
|
15182
16166
|
const rings = flatFlag(this.flags, 'RINGS');
|
|
@@ -15359,7 +16343,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15359
16343
|
getIcon: d => this._getGlowIcon(colorOf(d.part.i)),
|
|
15360
16344
|
getSize: d => d.part.r * 11,
|
|
15361
16345
|
getPixelOffset: offsetOf,
|
|
15362
|
-
getColor: d => [255, 255, 255, alphaOf(d)],
|
|
16346
|
+
getColor: d => [255, 255, 255, Math.round(alphaOf(d) * glowOpacityOf(st))],
|
|
15363
16347
|
sizeUnits: 'pixels', billboard: true,
|
|
15364
16348
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
15365
16349
|
}));
|
|
@@ -15714,7 +16698,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15714
16698
|
if (!layout) continue;
|
|
15715
16699
|
const sum = values.reduce((a, v) => a + v, 0);
|
|
15716
16700
|
if (star) for (const v of values) if (v > starMaxPart) starMaxPart = v;
|
|
15717
|
-
const size = this.binding.size ?
|
|
16701
|
+
const size = this.binding.size ? scanValue(raw[this.binding.size]) : undefined;
|
|
15718
16702
|
const r = nSizeOf(sum, size) * unit;
|
|
15719
16703
|
if (!(r > 0) || !isFinite(r)) continue;
|
|
15720
16704
|
// flat's draw-order key (maptheme.js 16812-16860): the size value,
|
|
@@ -16162,7 +17146,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16162
17146
|
return { values: parts, size };
|
|
16163
17147
|
}
|
|
16164
17148
|
const raw = f.properties || {};
|
|
16165
|
-
return { values: fields.map(k => flatNumber(raw[k], this.flags)), size: sizeField ?
|
|
17149
|
+
return { values: fields.map(k => flatNumber(raw[k], this.flags)), size: sizeField ? scanValue(raw[this.binding.size]) : undefined };
|
|
16166
17150
|
};
|
|
16167
17151
|
const all = agg ? this._pieAggregatedItems(zoom, [-180, -90, 180, 90]) : (this._activeFeatures || this.features);
|
|
16168
17152
|
// flat's per-part mean, median (getMeanMedianQuantile) and standard
|
|
@@ -16887,7 +17871,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16887
17871
|
let lo = Infinity, hi = -Infinity;
|
|
16888
17872
|
const field = this.binding.value;
|
|
16889
17873
|
for (const f of this.features || []) {
|
|
16890
|
-
const v = field === '$item$' ? 1 :
|
|
17874
|
+
const v = field === '$item$' ? 1 : scanValue(f.properties && f.properties[field]);
|
|
16891
17875
|
if (isNaN(v)) continue;
|
|
16892
17876
|
if (v < lo) lo = v;
|
|
16893
17877
|
if (v > hi) hi = v;
|
|
@@ -16997,6 +17981,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16997
17981
|
if (!(fontSize > 0)) continue;
|
|
16998
17982
|
labels.push({ ...d, text: textOf(d.properties), fontSize, halo: itemRgb(d.properties), pixelOffset: alignOf(d) });
|
|
16999
17983
|
}
|
|
17984
|
+
// a text field read as the value/size scans to 0 (flat's __scanValue), so every label has size 0 and nothing is drawn
|
|
17985
|
+
if (!labels.length && combined.length && !this._warnedNoLabels) {
|
|
17986
|
+
this._warnedNoLabels = true;
|
|
17987
|
+
console.warn(`[ixmaps-gl] theme "${this.name}": LABEL|TEXTONLY drew no labels — the size of all ${combined.length} items is 0. ` +
|
|
17988
|
+
'A non-numeric (text) field bound as value/size reads as 0; bind a numeric size field.');
|
|
17989
|
+
}
|
|
17000
17990
|
// TextLayer's outlineColor is one per layer: a layer per halo color
|
|
17001
17991
|
const byHalo = new Map();
|
|
17002
17992
|
for (const l of labels) {
|
|
@@ -17358,7 +18348,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17358
18348
|
// last built map; called while the map is still building, they run once
|
|
17359
18349
|
// it is ready.
|
|
17360
18350
|
const MAP_HANDLE_METHODS = ['replace', 'add', 'newTheme', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
17361
|
-
'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'setProjection', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible', 'switchLayer', 'setThemeTimeFrame', 'attribution'];
|
|
18351
|
+
'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'setProjection', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible', 'switchLayer', 'setThemeTimeFrame', 'attribution', 'on', 'off'];
|
|
17362
18352
|
const _mapHandles = Object.create(null);
|
|
17363
18353
|
function makeMapHandle(szMap) {
|
|
17364
18354
|
const h = {};
|
|
@@ -17376,6 +18366,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17376
18366
|
};
|
|
17377
18367
|
}
|
|
17378
18368
|
h.getZoom = () => { const a = api(); return a ? a.getZoom() : undefined; };
|
|
18369
|
+
h.getBounds = () => getBounds();
|
|
18370
|
+
// the MapLibre map itself (undefined until the map is built)
|
|
18371
|
+
h.getMapLibre = () => { const a = api(); return a ? a.map : undefined; };
|
|
17379
18372
|
h.getThemeObj = id => getThemeObj(id);
|
|
17380
18373
|
// flat's mapApi.message(szText) (htmlgui_flat.js 1381): a short status
|
|
17381
18374
|
// line over the map — the same accepted no-op as the global
|
|
@@ -17487,6 +18480,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17487
18480
|
return [{ lat: Math.max(b[0].lat, -85.05), lng: b[0].lng }, { lat: Math.min(b[1].lat, 85.05), lng: b[1].lng }];
|
|
17488
18481
|
}
|
|
17489
18482
|
|
|
18483
|
+
// flat's mapApi.getBounds() (htmlgui_flat.js): [swLat, swLng, neLat, neLng] — the same box as getBoundingBox
|
|
18484
|
+
function getBounds() {
|
|
18485
|
+
const b = getBoundingBox();
|
|
18486
|
+
return b ? [b[0].lat, b[0].lng, b[1].lat, b[1].lng] : null;
|
|
18487
|
+
}
|
|
18488
|
+
|
|
17490
18489
|
// flat's ixmaps.setTitle(html) / setTitleBox(text, color) (htmlgui.js
|
|
17491
18490
|
// 1311-1331): a message line over the last built map, same markup as
|
|
17492
18491
|
// flat's default (legend not aligned left); '' clears it.
|
|
@@ -18032,7 +19031,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
18032
19031
|
global.ixmaps = {
|
|
18033
19032
|
layer, Layer: layer, Map: createMap, setExternalData: setExternalDataBridge, getThemeObj, data: ixmapsData,
|
|
18034
19033
|
szResourceBase: ixmapsSzResourceBase, getProjectString, setProjectJSON, loadProject,
|
|
18035
|
-
markThemeClass, unmarkThemeClass, getBoundingBox, setTitle, setTitleBox, refreshTheme, formatValue: flatFormatValue,
|
|
19034
|
+
markThemeClass, unmarkThemeClass, getBoundingBox, getBounds, setTitle, setTitleBox, refreshTheme, formatValue: flatFormatValue,
|
|
18036
19035
|
// flat's ixmaps.__formatValue (ui/js/tools/legend.js 451-553) — pages call
|
|
18037
19036
|
// it for statistics/legend values; legend's own variant differs from
|
|
18038
19037
|
// format.js's only in the SPACE flag, which flatFormatValue covers too
|
|
@@ -18095,7 +19094,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
18095
19094
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
18096
19095
|
global.__ixmapsGlInternals = {
|
|
18097
19096
|
IXMAPS_GL_VERSION, applyFlatMaxCharts, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout, ringsLayout, pieSliceLayout, pieValueLabelLayout, isBarChart, flatBarLayout, flatValueRules, flatPieRecord, flatPieAccumulate, flatPieItemValues, flatVectorItems, orthographicPlane, vectorArcHeight, pathBackIndex, greatCircleMeters, cameraOptions, bezierVectorLayout, cubicBezierPoints, arrowMarkerTriangle, fadeGradientStops, flatToArray, hashUnit, flowDashPhase, multiQuadOffsets, pixelOffsetLngLat, chartTextTransmittance, insideSymbolShape,
|
|
18098
|
-
chartBoxLayout, choroplethOutlineColor, normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
19097
|
+
splitMapEvents, getBounds, chartBoxLayout, choroplethOutlineColor, normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
18099
19098
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
18100
19099
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|
|
18101
19100
|
computeAlphaStats, computeMultiFieldClasses, computeDominantStats, resolveDominantClass, computeComposeColorStats,
|
|
@@ -18104,7 +19103,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
18104
19103
|
flatToMapLibreZoom, mapLibreToFlatZoom, flatViewToMapLibreZoom, mapLibreToFlatViewZoom, flatSvgProjectionOf, flatSvgProjectionByOptions, normalSizeScaleOf, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
|
|
18105
19104
|
fetchLayerData, QUERY_HANDOFF, parseCsvText, dataTableRows, geometryRowsToFeatureCollection, filterFlatValues, applyField100, field100Binding, rangeClassLegendTotals, resolveAggregateValue, classValueSeparate, cellAggregatedValues, oneHot, groupCoLocated, aggregateOnGrid, GridAggregateIndex, flatRangeParts, explicitRangeBreaks, explicitRangeParts, resolvePartsClass, isNoneColor, aggregateField100, valueRadius, itemPlotGeometry, itemAnchor, objectZoomFactor, resolveZoomReference, defaultNormalSizeValue,
|
|
18106
19105
|
applyWhereFilter, joinChartPositions, joinChoroplethFeatures, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatBubbleLook, bubbleIconLook, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
18107
|
-
drawSymbolPath, normalizeSymbolShape, bubbleStrokeRgb,
|
|
19106
|
+
drawSymbolPath, normalizeSymbolShape, bubbleStrokeRgb, heatmapColorRange, heatmapWeight,
|
|
19107
|
+
mlHeatmapPoints, mlHeatmapWeightMax, mlHeatmapRadius, mlHeatmapPaint, syncNativeHeatmaps, mlHeatLayerId,
|
|
19108
|
+
kdeBandwidthKm, kdeMaxCells, kdeLevels, kdePoints, kdeGrid, kdeBands, kdeBandColors, kdeWorkerSource, kdeCompute, kdeReset, setDeckLayerClassesForTests, ensureD3Contour, colorRampRgb,
|
|
18108
19109
|
flatFormatValue, themeStyleArgs, flatChangedStyleValue, explicitValueDecimals, legendDecimals,
|
|
18109
19110
|
dataCacheKey, dataCacheDisabled, cachedLayerData, featuresBounds, wantsZoomToExtent, legendIsOn, applySphereBackdrop,
|
|
18110
19111
|
};
|