ixmaps-gl 0.2.25 → 0.2.31
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 +21 -8
- package/ixmaps-gl.js +1036 -154
- 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.31';
|
|
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,7 @@ 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);
|
|
612
617
|
PieSliceLayer = makePieSliceLayer(ScatterplotLayer);
|
|
613
618
|
BarShapeLayer = makeBarShapeLayer(ScatterplotLayer);
|
|
614
619
|
FlowPathLayer = makeFlowPathLayer(PathLayer);
|
|
@@ -1201,7 +1206,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1201
1206
|
if (!table || !Array.isArray(table.records) || !Array.isArray(table.fields)) {
|
|
1202
1207
|
return table && typeof table.json === 'function' ? table.json() : [];
|
|
1203
1208
|
}
|
|
1204
|
-
|
|
1209
|
+
// a header cell that spans lines ("Sezione (*)\nSektion", bilingual
|
|
1210
|
+
// exports) reads as one line, as in flat: "Sezione (*) Sektion"
|
|
1211
|
+
const names = table.fields.map(f => String(f.id).replace(/[\r\n]+/g, ' '));
|
|
1205
1212
|
const rows = new Array(table.records.length);
|
|
1206
1213
|
for (let i = 0; i < table.records.length; i++) {
|
|
1207
1214
|
const rec = table.records[i], o = {};
|
|
@@ -1210,6 +1217,26 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1210
1217
|
}
|
|
1211
1218
|
return rows;
|
|
1212
1219
|
}
|
|
1220
|
+
// .data({process}) on topojson/geojson, as flat: its data.js hands the
|
|
1221
|
+
// process function the features' properties as a Table (column(...).map,
|
|
1222
|
+
// addColumn, ...); the result goes back onto the same features, by row
|
|
1223
|
+
function processFeatureCollection(fc, dataConfig) {
|
|
1224
|
+
if (!dataConfig.process || !fc || !Array.isArray(fc.features) || !fc.features.length) return fc;
|
|
1225
|
+
requireDataJs('process');
|
|
1226
|
+
const headers = [];
|
|
1227
|
+
const seen = new Set();
|
|
1228
|
+
for (const f of fc.features) {
|
|
1229
|
+
for (const k of Object.keys(f.properties || {})) if (!seen.has(k)) { seen.add(k); headers.push(k); }
|
|
1230
|
+
}
|
|
1231
|
+
const table = new global.Data.Table().setArray([headers, ...fc.features.map(f => headers.map(h => (f.properties || {})[h]))]);
|
|
1232
|
+
const rows = dataTableRows(runDataProcess(table, dataConfig));
|
|
1233
|
+
if (rows.length !== fc.features.length) {
|
|
1234
|
+
console.warn(`[ixmaps-gl] .data({process}) changed the row count of ${dataConfig.type} features (${fc.features.length} → ${rows.length}); process result ignored`);
|
|
1235
|
+
return fc;
|
|
1236
|
+
}
|
|
1237
|
+
fc.features.forEach((f, i) => { f.properties = rows[i]; });
|
|
1238
|
+
return fc;
|
|
1239
|
+
}
|
|
1213
1240
|
// parse CSV text with data.js (as flat) → a Table
|
|
1214
1241
|
function dataJsCsvTable(text) {
|
|
1215
1242
|
return new Promise((resolve, reject) => {
|
|
@@ -1531,8 +1558,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1531
1558
|
// genuinely CSV-shaped .obj with a missing binding.position would
|
|
1532
1559
|
// throw, even though this .obj was never row data in the first place.
|
|
1533
1560
|
if (dataConfig.obj) {
|
|
1534
|
-
if (dataConfig.type === 'topojson') return topojsonToFeatureCollection(dataConfig.obj, dataConfig.object);
|
|
1535
|
-
if (dataConfig.type === 'geojson') return sanitizeGeoJSON(dataConfig.obj);
|
|
1561
|
+
if (dataConfig.type === 'topojson') return processFeatureCollection(topojsonToFeatureCollection(dataConfig.obj, dataConfig.object), dataConfig);
|
|
1562
|
+
if (dataConfig.type === 'geojson') return processFeatureCollection(sanitizeGeoJSON(dataConfig.obj), dataConfig);
|
|
1536
1563
|
let obj = dataConfig.obj;
|
|
1537
1564
|
if (dataConfig.process) {
|
|
1538
1565
|
requireDataJs('process');
|
|
@@ -1624,8 +1651,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1624
1651
|
// them into a table with a geometry column, and back)
|
|
1625
1652
|
const resp = await fetch(dataConfig.url);
|
|
1626
1653
|
if (!resp.ok) throw new Error(`[ixmaps-gl] failed to fetch ${dataConfig.url}: ${resp.status}`);
|
|
1627
|
-
if (dataConfig.type === 'topojson') return topojsonToFeatureCollection(await resp.json(), dataConfig.object);
|
|
1628
|
-
return sanitizeGeoJSON(await resp.json());
|
|
1654
|
+
if (dataConfig.type === 'topojson') return processFeatureCollection(topojsonToFeatureCollection(await resp.json(), dataConfig.object), dataConfig);
|
|
1655
|
+
return processFeatureCollection(sanitizeGeoJSON(await resp.json()), dataConfig);
|
|
1629
1656
|
}
|
|
1630
1657
|
// every other format (json, jsonstat, ndjson, rss, kml, gml, geobuf,
|
|
1631
1658
|
// flatgeobuf, geopackage, parquet, ...) through data.js, as flat
|
|
@@ -1920,7 +1947,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1920
1947
|
function flatShapeAreaKm2(featureRt, f) {
|
|
1921
1948
|
const sizeField = featureRt.binding && featureRt.binding.size;
|
|
1922
1949
|
if (sizeField) {
|
|
1923
|
-
const v =
|
|
1950
|
+
const v = scanValue(f.properties && f.properties[sizeField]);
|
|
1924
1951
|
return isFinite(v) ? v / 1000000 : NaN;
|
|
1925
1952
|
}
|
|
1926
1953
|
return geodesicPolygonAreaKm2(f.geometry);
|
|
@@ -1979,6 +2006,415 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1979
2006
|
return isFinite(o) ? Math.max(0, Math.min(1, o)) : 0;
|
|
1980
2007
|
}
|
|
1981
2008
|
|
|
2009
|
+
// flat's CHART|SYMBOL DOPACITY (maptheme.js 22142-22145): (v-min)^(1/pow) /
|
|
2010
|
+
// (max-min)^(1/pow) · fillopacity(|| 1) · dopacityscale, clamped to 0..1 —
|
|
2011
|
+
// min/max are the theme-wide extremes of the cell values, one pair for all
|
|
2012
|
+
// cells (not the CHOROPLETH|DOPACITY median ramp, not a per-class max)
|
|
2013
|
+
function symbolDopacityAlpha(style, v, min, max, fillopacity) {
|
|
2014
|
+
const pow = 1 / (styleNum(style.dopacitypow) || 1);
|
|
2015
|
+
const scale = styleNum(style.dopacityscale) || 1;
|
|
2016
|
+
const fo = fillopacity || 1;
|
|
2017
|
+
const o = Math.pow(v - min, pow) / Math.pow(max - min, pow) * fo * scale;
|
|
2018
|
+
return isFinite(o) ? Math.max(0, Math.min(1, o)) : fo;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// .type("KDE") — kernel density estimate as stacked bands (the fire pages' KDE, done in the
|
|
2022
|
+
// engine). The pure parts: bandwidth by zoom, the points, the density grid, the bands.
|
|
2023
|
+
//
|
|
2024
|
+
// The value of a grid cell is what the pages computed by brute force — the sum over the points
|
|
2025
|
+
// of weight · exp(−d² / 2h²), d the distance in km, h the bandwidth — but here by BINNING each
|
|
2026
|
+
// weight into its four nearest cells (bilinear) and blurring the grid with a separable Gaussian
|
|
2027
|
+
// (σ = h in cells): O(points + cells · kernel) instead of O(cells · points). Thresholds are
|
|
2028
|
+
// fractions of the maximum, so only the shape of the density matters, not its absolute scale.
|
|
2029
|
+
|
|
2030
|
+
// flat zoom → bandwidth (km) and the cell budget of the grid, as the fire pages chose them
|
|
2031
|
+
const KDE_ZOOM_TABLE = [ // [from flat zoom, bandwidth km, max cells]
|
|
2032
|
+
[16, 0.04, 1000000], [15, 0.08, 800000], [14, 0.12, 600000], [13, 0.30, 300000],
|
|
2033
|
+
[12, 0.80, 100000], [11, 2.00, 40000], [10, 5.00, 25000], [9, 10.0, 20000]
|
|
2034
|
+
];
|
|
2035
|
+
const KDE_FAR_ZOOM = [24.0, 15000];
|
|
2036
|
+
const KDE_DEFAULT_LEVELS = [0.15, 0.30, 0.50, 0.70];
|
|
2037
|
+
const KDE_MIN_POINTS = 5; // fewer points have no density to speak of (the pages' rule)
|
|
2038
|
+
const KDE_MARGIN_SIGMAS = 3; // the grid reaches 3 bandwidths beyond the outermost points
|
|
2039
|
+
const KDE_KM_PER_DEG = 111.3; // the pages' constant
|
|
2040
|
+
|
|
2041
|
+
function kdeZoomRow(flatZoom) {
|
|
2042
|
+
const z = Number(flatZoom);
|
|
2043
|
+
const row = KDE_ZOOM_TABLE.find(r => z >= r[0]);
|
|
2044
|
+
return row ? [row[1], row[2]] : KDE_FAR_ZOOM;
|
|
2045
|
+
}
|
|
2046
|
+
function kdeBandwidthKm(flatZoom) { return kdeZoomRow(flatZoom)[0]; }
|
|
2047
|
+
function kdeMaxCells(flatZoom) { return kdeZoomRow(flatZoom)[1]; }
|
|
2048
|
+
|
|
2049
|
+
// style.kdelevels: the threshold fractions of the maximum, as an array or "0.2,0.5" — numbers in
|
|
2050
|
+
// (0, 1], ascending, each once; anything unusable gives the default
|
|
2051
|
+
function kdeLevels(style) {
|
|
2052
|
+
let v = style && style.kdelevels;
|
|
2053
|
+
if (typeof v === 'string') v = v.split(/[,\s|]+/).filter(Boolean);
|
|
2054
|
+
if (!Array.isArray(v)) return KDE_DEFAULT_LEVELS.slice();
|
|
2055
|
+
const out = [...new Set(v.map(Number).filter(x => isFinite(x) && x > 0 && x <= 1))].sort((a, b) => a - b);
|
|
2056
|
+
return out.length ? out : KDE_DEFAULT_LEVELS.slice();
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// the records' positions and weights as one Float64Array [lng, lat, weight, …] (cheap to hand to
|
|
2060
|
+
// a worker); `bbox` [west, south, east, north] keeps only what lies inside it
|
|
2061
|
+
function kdePoints(features, binding, bbox) {
|
|
2062
|
+
const out = [];
|
|
2063
|
+
for (const f of features) {
|
|
2064
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
2065
|
+
if (!c || typeof c[0] !== 'number' || !isFinite(c[0]) || !isFinite(c[1])) continue;
|
|
2066
|
+
if (bbox && (c[0] < bbox[0] || c[0] > bbox[2] || c[1] < bbox[1] || c[1] > bbox[3])) continue;
|
|
2067
|
+
const w = heatmapWeight(binding, f.properties || {});
|
|
2068
|
+
if (w > 0) out.push(c[0], c[1], w);
|
|
2069
|
+
}
|
|
2070
|
+
return Float64Array.from(out);
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
// The density grid of `points` ([lng, lat, w, …]) at bandwidth `bwKm`, on about `maxCells` cells
|
|
2074
|
+
// that are square in km. Returns { values (row-major, row j at latitude s + (j + 0.5)·stepLat),
|
|
2075
|
+
// nx, ny, w0, s, stepLng, stepLat, max }, or null for fewer than KDE_MIN_POINTS points. Cells
|
|
2076
|
+
// coarser than the bandwidth still conserve the weights (binning, not sampling).
|
|
2077
|
+
// SELF-CONTAINED on purpose: its source is handed to a Web Worker (Function.prototype.toString),
|
|
2078
|
+
// so it uses nothing from this file.
|
|
2079
|
+
function kdeGrid(points, bwKm, maxCells) {
|
|
2080
|
+
const n = points.length / 3;
|
|
2081
|
+
if (n < 5 || !(bwKm > 0)) return null;
|
|
2082
|
+
const KM = 111.3;
|
|
2083
|
+
let w0 = Infinity, e = -Infinity, s = Infinity, nn = -Infinity;
|
|
2084
|
+
for (let i = 0; i < points.length; i += 3) {
|
|
2085
|
+
const lng = points[i], lat = points[i + 1];
|
|
2086
|
+
if (lng < w0) w0 = lng; if (lng > e) e = lng;
|
|
2087
|
+
if (lat < s) s = lat; if (lat > nn) nn = lat;
|
|
2088
|
+
}
|
|
2089
|
+
const kmLng = Math.max(KM * Math.cos(((s + nn) / 2) * Math.PI / 180), 0.001);
|
|
2090
|
+
const margin = 3 * bwKm;
|
|
2091
|
+
s -= margin / KM; nn += margin / KM; w0 -= margin / kmLng; e += margin / kmLng;
|
|
2092
|
+
const wKm = (e - w0) * kmLng, hKm = (nn - s) * KM;
|
|
2093
|
+
const cell = Math.sqrt(wKm * hKm / Math.max(maxCells, 25));
|
|
2094
|
+
const nx = Math.max(5, Math.ceil(wKm / cell)), ny = Math.max(5, Math.ceil(hKm / cell));
|
|
2095
|
+
const stepLng = (e - w0) / nx, stepLat = (nn - s) / ny;
|
|
2096
|
+
|
|
2097
|
+
// bin: each weight into its four nearest cell centers (cell i is centered at (i + 0.5) · step)
|
|
2098
|
+
let a = new Float32Array(nx * ny);
|
|
2099
|
+
const put = (ix, iy, v) => {
|
|
2100
|
+
if (ix < 0) ix = 0; else if (ix > nx - 1) ix = nx - 1;
|
|
2101
|
+
if (iy < 0) iy = 0; else if (iy > ny - 1) iy = ny - 1;
|
|
2102
|
+
a[iy * nx + ix] += v;
|
|
2103
|
+
};
|
|
2104
|
+
for (let i = 0; i < points.length; i += 3) {
|
|
2105
|
+
const cx = (points[i] - w0) / stepLng - 0.5, cy = (points[i + 1] - s) / stepLat - 0.5;
|
|
2106
|
+
const ix = Math.floor(cx), iy = Math.floor(cy), fx = cx - ix, fy = cy - iy, w = points[i + 2];
|
|
2107
|
+
put(ix, iy, w * (1 - fx) * (1 - fy)); put(ix + 1, iy, w * fx * (1 - fy));
|
|
2108
|
+
put(ix, iy + 1, w * (1 - fx) * fy); put(ix + 1, iy + 1, w * fx * fy);
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// separable Gaussian, peak 1 (σ in cells per axis, kernel to 3σ; no blur when σ is below a third of a cell)
|
|
2112
|
+
const kernel = sigma => {
|
|
2113
|
+
const r = sigma < 0.33 ? 0 : Math.ceil(3 * sigma);
|
|
2114
|
+
const k = new Float32Array(2 * r + 1);
|
|
2115
|
+
for (let d = -r; d <= r; d++) k[d + r] = Math.exp(-(d * d) / (2 * sigma * sigma));
|
|
2116
|
+
return { k, r };
|
|
2117
|
+
};
|
|
2118
|
+
const kx = kernel(bwKm / (stepLng * kmLng)), ky = kernel(bwKm / (stepLat * KM));
|
|
2119
|
+
let b = new Float32Array(nx * ny);
|
|
2120
|
+
for (let j = 0; j < ny; j++) {
|
|
2121
|
+
const row = j * nx;
|
|
2122
|
+
for (let i = 0; i < nx; i++) {
|
|
2123
|
+
let v = 0;
|
|
2124
|
+
const lo = Math.max(0, i - kx.r), hi = Math.min(nx - 1, i + kx.r);
|
|
2125
|
+
for (let t = lo; t <= hi; t++) v += a[row + t] * kx.k[t - i + kx.r];
|
|
2126
|
+
b[row + i] = v;
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
a = new Float32Array(nx * ny);
|
|
2130
|
+
let max = 0;
|
|
2131
|
+
for (let j = 0; j < ny; j++) {
|
|
2132
|
+
const lo = Math.max(0, j - ky.r), hi = Math.min(ny - 1, j + ky.r);
|
|
2133
|
+
for (let i = 0; i < nx; i++) {
|
|
2134
|
+
let v = 0;
|
|
2135
|
+
for (let t = lo; t <= hi; t++) v += b[t * nx + i] * ky.k[t - j + ky.r];
|
|
2136
|
+
a[j * nx + i] = v;
|
|
2137
|
+
if (v > max) max = v;
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
return { values: a, nx, ny, w0, s, stepLng, stepLat, max };
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
// The bands of a kdeGrid: one MultiPolygon feature per threshold (fraction · max), each the area
|
|
2144
|
+
// at or above it — so the bands stack, the highest on top — in lng/lat. `makeContours` is
|
|
2145
|
+
// d3.contours (d3-contour), handed in so this stays pure. d3 puts the value of cell (i, j) at
|
|
2146
|
+
// (i + 0.5, j + 0.5), the same convention as the grid, hence the plain scaling by the steps.
|
|
2147
|
+
function kdeBands(grid, fractions, makeContours) {
|
|
2148
|
+
const empty = { type: 'FeatureCollection', features: [] };
|
|
2149
|
+
if (!grid || !(grid.max > 0) || !fractions.length) return empty;
|
|
2150
|
+
const thresholds = fractions.map(f => f * grid.max);
|
|
2151
|
+
const bands = makeContours().size([grid.nx, grid.ny]).thresholds(thresholds)(grid.values);
|
|
2152
|
+
const features = [];
|
|
2153
|
+
bands.forEach((mp, k) => {
|
|
2154
|
+
if (!mp.coordinates || !mp.coordinates.length) return;
|
|
2155
|
+
features.push({
|
|
2156
|
+
type: 'Feature',
|
|
2157
|
+
properties: { level: k, fraction: fractions[k], value: mp.value },
|
|
2158
|
+
geometry: {
|
|
2159
|
+
type: 'MultiPolygon',
|
|
2160
|
+
coordinates: mp.coordinates.map(poly => poly.map(ring => ring.map(p => [grid.w0 + p[0] * grid.stepLng, grid.s + p[1] * grid.stepLat])))
|
|
2161
|
+
}
|
|
2162
|
+
});
|
|
2163
|
+
});
|
|
2164
|
+
return { type: 'FeatureCollection', features };
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
// The worker's whole source: kdeGrid alone (it uses nothing else of this file), answering
|
|
2168
|
+
// {id, points, bwKm, maxCells} with {id, grid}, the grid's values handed back by transfer.
|
|
2169
|
+
function kdeWorkerSource() {
|
|
2170
|
+
return `const kdeGrid = ${kdeGrid.toString()};
|
|
2171
|
+
self.onmessage = e => {
|
|
2172
|
+
const { id, points, bwKm, maxCells } = e.data;
|
|
2173
|
+
const grid = kdeGrid(points, bwKm, maxCells);
|
|
2174
|
+
self.postMessage({ id, grid }, grid ? [grid.values.buffer] : []);
|
|
2175
|
+
};`;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// kdeGrid off the main thread: one shared Web Worker, built from kdeWorkerSource. Without a Worker
|
|
2179
|
+
// (a page that forbids blob workers, a test) or when it fails, the same function runs inline.
|
|
2180
|
+
let _kdeWorker = null, _kdeSeq = 0;
|
|
2181
|
+
const _kdePending = new Map();
|
|
2182
|
+
// tests: forget the worker and the loaded d3-contour
|
|
2183
|
+
function kdeReset() { if (_kdeWorker && _kdeWorker.terminate) _kdeWorker.terminate(); _kdeWorker = null; _kdePending.clear(); _d3ContourPromise = null; }
|
|
2184
|
+
// tests: the deck.gl layer classes a runtime builds with, without loading deck.gl
|
|
2185
|
+
function setDeckLayerClassesForTests(classes) { if (classes.GeoJsonLayer) GeoJsonLayer = classes.GeoJsonLayer; }
|
|
2186
|
+
function kdeCompute(points, bwKm, maxCells) {
|
|
2187
|
+
return new Promise(resolve => {
|
|
2188
|
+
try {
|
|
2189
|
+
if (_kdeWorker === null && typeof global.Worker === 'function' && typeof global.Blob === 'function' && global.URL && global.URL.createObjectURL) {
|
|
2190
|
+
const w = new global.Worker(global.URL.createObjectURL(new global.Blob([kdeWorkerSource()], { type: 'text/javascript' })));
|
|
2191
|
+
w.onmessage = e => { const p = _kdePending.get(e.data.id); if (p) { _kdePending.delete(e.data.id); p.resolve(e.data.grid); } };
|
|
2192
|
+
w.onerror = () => {
|
|
2193
|
+
_kdeWorker = false; // no worker from now on: whatever is waiting is computed here
|
|
2194
|
+
const waiting = [..._kdePending.values()]; _kdePending.clear();
|
|
2195
|
+
waiting.forEach(p => p.resolve(kdeGrid(...p.args)));
|
|
2196
|
+
};
|
|
2197
|
+
_kdeWorker = w;
|
|
2198
|
+
}
|
|
2199
|
+
} catch (err) { _kdeWorker = false; }
|
|
2200
|
+
if (!_kdeWorker) { resolve(kdeGrid(points, bwKm, maxCells)); return; }
|
|
2201
|
+
const id = ++_kdeSeq;
|
|
2202
|
+
_kdePending.set(id, { resolve, args: [points, bwKm, maxCells] });
|
|
2203
|
+
_kdeWorker.postMessage({ id, points, bwKm, maxCells });
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
// d3.contours of d3-contour 4 (+ d3-array 3), fetched once and evaluated PRIVATELY with a
|
|
2208
|
+
// CommonJS `require`: nothing is added to a window.d3 the page may have (d3-array would overwrite
|
|
2209
|
+
// the same-named functions of a d3 v3). Resolves to the `contours` function.
|
|
2210
|
+
let _d3ContourPromise = null;
|
|
2211
|
+
function ensureD3Contour() {
|
|
2212
|
+
if (!_d3ContourPromise) {
|
|
2213
|
+
const get = url => global.fetch(url).then(r => { if (!r.ok) throw new Error(`[ixmaps-gl] failed to load ${url}`); return r.text(); });
|
|
2214
|
+
const run = (src, req) => { const module = { exports: {} }; new Function('module', 'exports', 'require', src)(module, module.exports, req); return module.exports; };
|
|
2215
|
+
_d3ContourPromise = Promise.all([get(LIB_URLS.d3Array), get(LIB_URLS.d3Contour)]).then(([arraySrc, contourSrc]) => {
|
|
2216
|
+
const d3Array = run(arraySrc, () => { throw new Error('[ixmaps-gl] d3-array has no dependencies'); });
|
|
2217
|
+
const d3Contour = run(contourSrc, name => { if (name === 'd3-array') return d3Array; throw new Error(`[ixmaps-gl] d3-contour requires ${name}`); });
|
|
2218
|
+
if (typeof d3Contour.contours !== 'function') throw new Error('[ixmaps-gl] d3-contour: no contours()');
|
|
2219
|
+
return d3Contour.contours;
|
|
2220
|
+
}).catch(err => { _d3ContourPromise = null; throw err; });
|
|
2221
|
+
}
|
|
2222
|
+
return _d3ContourPromise;
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
// A color ramp of `steps` RGB colors from a colorscheme, low → high: a generated scheme
|
|
2226
|
+
// (["5","white","blue"]) is flat's own sweep, a list of colors is taken as given (interpolated),
|
|
2227
|
+
// ONE color is a ramp of itself; `fallback` when nothing in it can be read. Shared by HEATMAP
|
|
2228
|
+
// (colorRange) and KDE (one color per band).
|
|
2229
|
+
function colorRampRgb(colorscheme, steps, fallback) {
|
|
2230
|
+
let rgb;
|
|
2231
|
+
if (isGeneratedColorScheme(colorscheme)) {
|
|
2232
|
+
rgb = flatColorSweep(colorscheme[1], colorscheme[2], Number(colorscheme[0]) || steps, colorscheme[3], colorscheme[4]) || [];
|
|
2233
|
+
} else {
|
|
2234
|
+
const list = Array.isArray(colorscheme) ? colorscheme : colorscheme ? [colorscheme] : [];
|
|
2235
|
+
rgb = list.map(c => parseCssColor(String(c))).filter(Boolean);
|
|
2236
|
+
}
|
|
2237
|
+
if (!rgb.length) rgb = [fallback];
|
|
2238
|
+
return Array.from({ length: steps }, (_, i) => {
|
|
2239
|
+
const t = steps > 1 ? i / (steps - 1) : 1;
|
|
2240
|
+
const x = t * (rgb.length - 1), k = Math.min(rgb.length - 2, Math.floor(x));
|
|
2241
|
+
const c = rgb.length === 1 ? rgb[0] : rgb[k].map((v, j) => Math.round(v + (rgb[k + 1][j] - v) * (x - k)));
|
|
2242
|
+
return [c[0], c[1], c[2]];
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
// .type("HEATMAP") — deck.gl HeatmapLayer's colorRange (low → high density, RGBA): the ramp of
|
|
2247
|
+
// `steps` colors with an alpha that rises from 0 over the first part of it, so the tails of a
|
|
2248
|
+
// kernel fade out instead of ending in a hard edge.
|
|
2249
|
+
function heatmapColorRange(colorscheme, steps = 6) {
|
|
2250
|
+
return colorRampRgb(colorscheme, steps, [229, 57, 53]).map((c, i) => {
|
|
2251
|
+
const t = steps > 1 ? i / (steps - 1) : 1;
|
|
2252
|
+
return [c[0], c[1], c[2], Math.round(255 * Math.min(1, t * 2.5))];
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// .type("KDE") — the color of each band, lowest threshold first (the fire pages' #dd0088 when the
|
|
2257
|
+
// colorscheme says nothing usable)
|
|
2258
|
+
function kdeBandColors(colorscheme, count) { return colorRampRgb(colorscheme, count, [221, 0, 136]); }
|
|
2259
|
+
|
|
2260
|
+
// .type("HEATMAP") — the weight of one record: its size field if bound, else its value field
|
|
2261
|
+
// (a text value field just counts the record), 1 for "$item$"; below 0 or not a number → 0
|
|
2262
|
+
function heatmapWeight(binding, props) {
|
|
2263
|
+
const b = binding || {};
|
|
2264
|
+
const fromSize = !!b.size && b.size !== '$item$';
|
|
2265
|
+
const field = fromSize ? b.size
|
|
2266
|
+
: (b.size === '$item$' || !b.value || b.value === '$item$' || String(b.value).includes('|')) ? null : b.value;
|
|
2267
|
+
if (!field) return 1;
|
|
2268
|
+
const w = scanValue(props[field]);
|
|
2269
|
+
if (isFinite(w)) return w > 0 ? w : 0;
|
|
2270
|
+
return fromSize ? 0 : 1;
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
// ── .type("MLHEATMAP") — MapLibre's NATIVE heatmap layer (type "heatmap"). The pure parts: the
|
|
2274
|
+
// points, the weight that counts 1, the radius by zoom, the paint. Unlike HEATMAP (deck.gl's
|
|
2275
|
+
// HeatmapLayer, which aggregates the whole dataset into one 2048² texture whenever the view
|
|
2276
|
+
// changes and scales the density to the maximum in view), MapLibre keeps the points in tile
|
|
2277
|
+
// buffers, evaluates radius and intensity from the zoom on the GPU at draw time and reads the
|
|
2278
|
+
// density ABSOLUTELY through the color ramp (clamped at 1): zooming costs nothing and a cell means
|
|
2279
|
+
// the same at every zoom. The weight is `w`, the density 1 is reached by a point of weight
|
|
2280
|
+
// weightmax (and more by overlapping points).
|
|
2281
|
+
const MLHEATMAP_MIN_RADIUS_PX = 0.5;
|
|
2282
|
+
const MLHEATMAP_MAX_RADIUS_PX = 400;
|
|
2283
|
+
|
|
2284
|
+
// the records as GeoJSON points {w: weight}, for the native layer's source (weights ≤ 0 and
|
|
2285
|
+
// records without a position are left out)
|
|
2286
|
+
function mlHeatmapPoints(features, binding) {
|
|
2287
|
+
const out = [];
|
|
2288
|
+
for (const f of features) {
|
|
2289
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
2290
|
+
if (!c || typeof c[0] !== 'number' || !isFinite(c[0]) || !isFinite(c[1])) continue;
|
|
2291
|
+
const w = heatmapWeight(binding, f.properties || {});
|
|
2292
|
+
if (w > 0) out.push({ type: 'Feature', geometry: { type: 'Point', coordinates: [c[0], c[1]] }, properties: { w } });
|
|
2293
|
+
}
|
|
2294
|
+
return { type: 'FeatureCollection', features: out };
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
// the weight that counts 1: style.weightmax, else the largest weight of the records (1 for none)
|
|
2298
|
+
function mlHeatmapWeightMax(features, binding, style) {
|
|
2299
|
+
const given = styleNum(style && style.weightmax);
|
|
2300
|
+
if (given > 0) return given;
|
|
2301
|
+
let max = 0;
|
|
2302
|
+
for (const f of features) {
|
|
2303
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
2304
|
+
if (!c || typeof c[0] !== 'number' || !isFinite(c[0]) || !isFinite(c[1])) continue;
|
|
2305
|
+
const w = heatmapWeight(binding, f.properties || {});
|
|
2306
|
+
if (w > max) max = w;
|
|
2307
|
+
}
|
|
2308
|
+
return max > 0 ? max : 1;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
// heatmap-radius: `radiusPx` at the reference zoom `zoomRef`, growing with the map like every object
|
|
2312
|
+
// (objectZoomFactor: radiusPx · 2^((zoom − zoomRef) / pow)), kept between 0.5 and 400 px. An
|
|
2313
|
+
// exponential interpolation of base 2^(1/pow) between the two zooms where the limits are reached is
|
|
2314
|
+
// exactly that curve, so MapLibre evaluates it from the zoom without any rebuild. A number when the
|
|
2315
|
+
// radius does not follow the zoom (objectscaling not dynamic) or is already at a limit everywhere.
|
|
2316
|
+
function mlHeatmapRadius(radiusPx, zoomRef, pow, dynamic) {
|
|
2317
|
+
const r0 = radiusPx > 0 ? radiusPx : 30;
|
|
2318
|
+
const clamp = r => Math.max(MLHEATMAP_MIN_RADIUS_PX, Math.min(MLHEATMAP_MAX_RADIUS_PX, r));
|
|
2319
|
+
if (!dynamic) return clamp(r0);
|
|
2320
|
+
const p = pow > 0 ? pow : 1;
|
|
2321
|
+
const zoomOf = r => zoomRef + p * Math.log2(r / r0); // the zoom at which the radius is r
|
|
2322
|
+
const radiusAt = z => r0 * Math.pow(2, (z - zoomRef) / p);
|
|
2323
|
+
const zLow = Math.max(0, zoomOf(MLHEATMAP_MIN_RADIUS_PX));
|
|
2324
|
+
const zCap = Math.min(24, zoomOf(MLHEATMAP_MAX_RADIUS_PX));
|
|
2325
|
+
if (!(zCap > zLow + 1e-6)) return clamp(radiusAt(zLow));
|
|
2326
|
+
return ['interpolate', ['exponential', Math.pow(2, 1 / p)], ['zoom'], zLow, clamp(radiusAt(zLow)), zCap, clamp(radiusAt(zCap))];
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
// the paint of the native heatmap layer. Colors: the colorscheme as a ramp over the densities
|
|
2330
|
+
// 0.2 … 1 (transparent at 0, like MapLibre's own example, so the tails fade out); a scheme of one
|
|
2331
|
+
// color fades in by alpha instead. style: intensity (default 1), fillopacity (default 1).
|
|
2332
|
+
function mlHeatmapPaint(style, weightMax, radius) {
|
|
2333
|
+
const st = style || {};
|
|
2334
|
+
const ramp = colorRampRgb(st.colorscheme, 5, [229, 57, 53]);
|
|
2335
|
+
const single = ramp.every(c => c.join() === ramp[0].join());
|
|
2336
|
+
const color = ['interpolate', ['linear'], ['heatmap-density'], 0, `rgba(${ramp[0].join(',')},0)`];
|
|
2337
|
+
ramp.forEach((c, i) => {
|
|
2338
|
+
const d = (i + 1) / 5;
|
|
2339
|
+
color.push(d, single ? `rgba(${c.join(',')},${d})` : `rgb(${c.join(',')})`);
|
|
2340
|
+
});
|
|
2341
|
+
const intensity = styleNum(st.intensity), opacity = styleNum(st.fillopacity);
|
|
2342
|
+
return {
|
|
2343
|
+
'heatmap-weight': ['interpolate', ['linear'], ['get', 'w'], 0, 0, weightMax > 0 ? weightMax : 1, 1],
|
|
2344
|
+
'heatmap-intensity': intensity > 0 ? intensity : 1,
|
|
2345
|
+
'heatmap-color': color,
|
|
2346
|
+
'heatmap-radius': radius,
|
|
2347
|
+
'heatmap-opacity': opacity > 0 ? Math.min(opacity, 1) : 1
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
// The native heatmap layers of the MLHEATMAP themes, kept in step with the runtimes (as the WMS
|
|
2352
|
+
// raster layers are): per theme one GeoJSON source of weighted points and one `heatmap` layer, put
|
|
2353
|
+
// BEFORE the first deck.gl layer (those are MapLibre custom layers) so the symbols stay on top.
|
|
2354
|
+
// Recomputed only when something changed — the records (filter, reload), the binding, the weight
|
|
2355
|
+
// max — and the paint only when its values changed; a new style (setMapType) drops sources and
|
|
2356
|
+
// layers, so a missing one is simply added again from the cached points. `owned` holds the layer ids
|
|
2357
|
+
// this function added, so those of removed themes can be taken away.
|
|
2358
|
+
// (MapLibre's basemap-opacity pass, applyBasemapOpacity, only knows fill/line/raster/circle/symbol
|
|
2359
|
+
// types: a heatmap layer is not faded with the basemap.)
|
|
2360
|
+
const _mlSourceIds = new WeakMap();
|
|
2361
|
+
let _mlSourceSeq = 0;
|
|
2362
|
+
const mlHeatLayerId = name => 'ixmaps-mlheat:' + String(name).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
2363
|
+
function mlFirstCustomLayerId(map) {
|
|
2364
|
+
const order = typeof map.getLayersOrder === 'function' ? map.getLayersOrder() : (map.getStyle().layers || []).map(l => l.id);
|
|
2365
|
+
return order.find(id => { const l = map.getLayer(id); return l && l.type === 'custom'; });
|
|
2366
|
+
}
|
|
2367
|
+
function syncNativeHeatmaps(map, runtimes, owned) {
|
|
2368
|
+
if (!map || typeof map.getLayer !== 'function') return;
|
|
2369
|
+
const desired = runtimes.filter(rt => rt.flags && rt.flags.has('MLHEATMAP'));
|
|
2370
|
+
if (!desired.length && !owned.size) return;
|
|
2371
|
+
const wanted = new Set(desired.map(rt => mlHeatLayerId(rt.name)));
|
|
2372
|
+
[...owned].forEach(id => {
|
|
2373
|
+
if (wanted.has(id)) return;
|
|
2374
|
+
owned.delete(id);
|
|
2375
|
+
try { if (map.getLayer(id)) map.removeLayer(id); } catch (e) { /* already gone */ }
|
|
2376
|
+
try { if (map.getSource(id)) map.removeSource(id); } catch (e) { /* already gone */ }
|
|
2377
|
+
});
|
|
2378
|
+
desired.forEach(rt => {
|
|
2379
|
+
const id = mlHeatLayerId(rt.name);
|
|
2380
|
+
const st = rt._mlHeat || (rt._mlHeat = {});
|
|
2381
|
+
try {
|
|
2382
|
+
const records = rt._activeFeatures || rt.features || [];
|
|
2383
|
+
if (!_mlSourceIds.has(records)) _mlSourceIds.set(records, ++_mlSourceSeq);
|
|
2384
|
+
const style = rt.style || {};
|
|
2385
|
+
const dataKey = JSON.stringify([rt._dataGen, _mlSourceIds.get(records), rt.binding]);
|
|
2386
|
+
const weightKey = JSON.stringify([dataKey, style.weightmax]);
|
|
2387
|
+
if (st.weightKey !== weightKey) { st.weightMax = mlHeatmapWeightMax(records, rt.binding, style); st.weightKey = weightKey; }
|
|
2388
|
+
if (st.dataKey !== dataKey || !st.fc) {
|
|
2389
|
+
st.fc = mlHeatmapPoints(records, rt.binding);
|
|
2390
|
+
st.dataKey = dataKey;
|
|
2391
|
+
const src = map.getSource(id);
|
|
2392
|
+
if (src && typeof src.setData === 'function') src.setData(st.fc);
|
|
2393
|
+
}
|
|
2394
|
+
if (!map.getSource(id)) map.addSource(id, { type: 'geojson', data: st.fc });
|
|
2395
|
+
owned.add(id);
|
|
2396
|
+
// the radius follows the zoom like every object (objectscaling), from `radiuspx` at the reference zoom
|
|
2397
|
+
const mo = rt.mapOptions || {};
|
|
2398
|
+
const radius = mlHeatmapRadius((styleNum(style.radiuspx) || 30) * (styleNum(style.scale) || 1),
|
|
2399
|
+
resolveZoomReference(mo), resolveDynamicScalePow(mo), String(mo.objectscaling || 'dynamic') === 'dynamic');
|
|
2400
|
+
const paint = mlHeatmapPaint(style, st.weightMax, radius);
|
|
2401
|
+
const paintKey = JSON.stringify(paint);
|
|
2402
|
+
if (!map.getLayer(id)) {
|
|
2403
|
+
map.addLayer({ id, type: 'heatmap', source: id, paint }, mlFirstCustomLayerId(map));
|
|
2404
|
+
st.paintKey = paintKey;
|
|
2405
|
+
} else if (st.paintKey !== paintKey) {
|
|
2406
|
+
Object.keys(paint).forEach(k => map.setPaintProperty(id, k, paint[k]));
|
|
2407
|
+
st.paintKey = paintKey;
|
|
2408
|
+
}
|
|
2409
|
+
const visibility = rt._hidden ? 'none' : 'visible';
|
|
2410
|
+
if ((map.getLayoutProperty(id, 'visibility') || 'visible') !== visibility) map.setLayoutProperty(id, 'visibility', visibility);
|
|
2411
|
+
st.failed = false;
|
|
2412
|
+
} catch (err) {
|
|
2413
|
+
if (!st.failed) { st.failed = true; console.warn(`[ixmaps-gl] layer "${rt.name}": MLHEATMAP not drawn:`, err && err.message ? err.message : err); }
|
|
2414
|
+
}
|
|
2415
|
+
});
|
|
2416
|
+
}
|
|
2417
|
+
|
|
1982
2418
|
// a choropleth shape's outline color: a LINE shape strokes with its value
|
|
1983
2419
|
// color; any other shape keeps the theme's outline, hidden while a class
|
|
1984
2420
|
// is isolated and the shape is not in it
|
|
@@ -2011,6 +2447,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2011
2447
|
// specific FEATURE runtime as a donor, not just sharing its name
|
|
2012
2448
|
// coincidentally.
|
|
2013
2449
|
geomRt._isChoroplethGeometryDonor = true;
|
|
2450
|
+
// the donor's own style (outline) — the layer registry is keyed by name,
|
|
2451
|
+
// so the same-named CHOROPLETH replaces the donor there and can't find it
|
|
2452
|
+
Object.defineProperty(spec, '_donorStyle', { value: geomRt.style, configurable: true });
|
|
2014
2453
|
const idField = geomRt.binding.id;
|
|
2015
2454
|
const lookupField = spec.binding.lookup;
|
|
2016
2455
|
if (!idField || !lookupField) {
|
|
@@ -2798,7 +3237,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2798
3237
|
// edit it by hand; `npm run unit` fails when it drifts.
|
|
2799
3238
|
// ---------------------------------------------------------------
|
|
2800
3239
|
// <grammar:binding-aliases>
|
|
2801
|
-
// generated from ixmaps-grammar 0.1.
|
|
3240
|
+
// generated from ixmaps-grammar 0.1.29 (ixmaps-flat 1.0.42, 2b978d5 2026-10-02) — 52 aliases
|
|
2802
3241
|
const FLAT_BINDING_ALIASES = {
|
|
2803
3242
|
"aggregation": "style.aggregationfield",
|
|
2804
3243
|
"aggregationfield": "style.aggregationfield",
|
|
@@ -2860,7 +3299,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2860
3299
|
// both — so .style({tooltip}) is a tooltip and .meta({fillopacity}) a style
|
|
2861
3300
|
// property. GENERATED by test/sync-grammar.mjs — do not edit.
|
|
2862
3301
|
// <grammar:meta-keys>
|
|
2863
|
-
// generated from ixmaps-grammar 0.1.
|
|
3302
|
+
// generated from ixmaps-grammar 0.1.29 — 5 meta keys
|
|
2864
3303
|
const FLAT_META_KEYS = ["description","name","snippet","title","tooltip"];
|
|
2865
3304
|
// </grammar:meta-keys>
|
|
2866
3305
|
|
|
@@ -3100,7 +3539,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3100
3539
|
'sizepow', 'rangescale', 'minvalue', 'maxvalue', 'markersize', 'boxopacity', 'outlierscale', 'valuescale',
|
|
3101
3540
|
'brightness', 'fractionscale', 'dopacityscale', 'dopacitypow', 'gridwidthpx', 'textscale', 'rangecentervalue',
|
|
3102
3541
|
'shadowblur', 'shadowdx', 'shadowdy', 'maxshadow', 'offsetx', 'offsety', 'gridx', 'boxmargin', 'borderwidth',
|
|
3103
|
-
'borderradius', 'maxcharts', 'archeight'
|
|
3542
|
+
'borderradius', 'maxcharts', 'archeight', 'alignmargin',
|
|
3543
|
+
// HEATMAP / MLHEATMAP / KDE (a project JSON writes them as strings)
|
|
3544
|
+
'radiuspx', 'intensity', 'threshold', 'weightmax', 'bandwidth'];
|
|
3104
3545
|
function toNumberIfNumeric(v) {
|
|
3105
3546
|
if (typeof v !== 'string' || !v.trim()) return v;
|
|
3106
3547
|
const n = Number(v);
|
|
@@ -3175,6 +3616,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3175
3616
|
delete style.title;
|
|
3176
3617
|
|
|
3177
3618
|
const flags = new Set(typeStr ? typeStr.split('|') : []);
|
|
3619
|
+
// flat's theme configurator appends "CENTER|DOMINANT" to a categorical type without a
|
|
3620
|
+
// separator ("...|CATEGORICALCENTER|DOMINANT"); flat's substring tests read that as
|
|
3621
|
+
// CATEGORICAL + CENTER, so split a glued CATEGORICAL prefix back into its flags
|
|
3622
|
+
[...flags].forEach(f => {
|
|
3623
|
+
if (f.length > 11 && f.startsWith('CATEGORICAL')) { flags.add('CATEGORICAL'); flags.add(f.slice(11)); }
|
|
3624
|
+
});
|
|
3178
3625
|
if (flags.has('BUBBLE')) flags.add('SYMBOL');
|
|
3179
3626
|
// flat tests its base type as a substring (szFlag.match(/CHOROPLETH/)),
|
|
3180
3627
|
// so "CHOROPLETHE" is a choropleth there too
|
|
@@ -3592,6 +4039,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3592
4039
|
// The ocean of flat's world / orthographic maps is a {type:"Sphere"} feature: the whole map disc,
|
|
3593
4040
|
// filled with the layer's color. Here it is the surface of the map (the globe, or the flat
|
|
3594
4041
|
// map): a background layer under everything, in that color and opacity.
|
|
4042
|
+
// the themes are drawn once, as in flat, even where the map repeats its world (the themecopies map option:
|
|
4043
|
+
// true repeats them on every world copy)
|
|
4044
|
+
function applyThemeCopies(map, value) {
|
|
4045
|
+
if (value !== true && value !== 'true') {
|
|
4046
|
+
if (!map.__ixOrigGetRenderWorldCopies) map.__ixOrigGetRenderWorldCopies = map.getRenderWorldCopies;
|
|
4047
|
+
map.getRenderWorldCopies = () => false;
|
|
4048
|
+
} else if (map.__ixOrigGetRenderWorldCopies) {
|
|
4049
|
+
map.getRenderWorldCopies = map.__ixOrigGetRenderWorldCopies;
|
|
4050
|
+
delete map.__ixOrigGetRenderWorldCopies;
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
3595
4053
|
const SPHERE_BACKDROP_LAYER_ID = '__ixmaps_gl_sphere_backdrop';
|
|
3596
4054
|
function applySphereBackdrop(map, style) {
|
|
3597
4055
|
const cs = style && style.colorscheme;
|
|
@@ -3631,6 +4089,23 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3631
4089
|
// basemap credit, bottom right — same line, same pill: white, 12px
|
|
3632
4090
|
// radius, 12px/20px Helvetica Neue, links rgba(0,0,0,0.75)).
|
|
3633
4091
|
// ---------------------------------------------------------------
|
|
4092
|
+
// On a dark map (the effective darkness, as the legend and the tooltip use it) the MapLibre control buttons
|
|
4093
|
+
// (zoom, compass) and the page's attribution are dark too: the class ixgl-dark on the map element switches them
|
|
4094
|
+
let _controlLookCss = false;
|
|
4095
|
+
function applyControlLook(el, dark) {
|
|
4096
|
+
if (!_controlLookCss && typeof document !== 'undefined') {
|
|
4097
|
+
_controlLookCss = true;
|
|
4098
|
+
const css = document.createElement('style');
|
|
4099
|
+
css.textContent = '.ixgl-dark .maplibregl-ctrl-group{background:#222!important;box-shadow:0 0 0 1px rgba(255,255,255,.15)!important}'
|
|
4100
|
+
+ '.ixgl-dark .maplibregl-ctrl-group button+button{border-top:1px solid #444!important}'
|
|
4101
|
+
+ '.ixgl-dark .maplibregl-ctrl-group button:not(:disabled):hover{background-color:#3a3a3a!important}'
|
|
4102
|
+
+ '.ixgl-dark .maplibregl-ctrl-group .maplibregl-ctrl-icon{filter:invert(1)}'
|
|
4103
|
+
+ '.ixgl-dark .ixmaps-gl-attribution{background:#222!important;color:#aaa!important}'
|
|
4104
|
+
+ '.ixgl-dark .ixmaps-gl-attribution a{color:#ddd!important}';
|
|
4105
|
+
document.head.appendChild(css);
|
|
4106
|
+
}
|
|
4107
|
+
if (el && el.classList) el.classList.toggle('ixgl-dark', !!dark);
|
|
4108
|
+
}
|
|
3634
4109
|
let _attributionCss = false;
|
|
3635
4110
|
function createAttribution(map) {
|
|
3636
4111
|
if (!_attributionCss && typeof document !== 'undefined') {
|
|
@@ -3937,11 +4412,29 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3937
4412
|
// pane, the shared look (fonts included) on the container
|
|
3938
4413
|
return html ? { html: tooltipPaneHtml(html), style: Object.assign({ padding: '0.5em 0.7em', maxWidth: '440px' }, tooltipLook()) } : null;
|
|
3939
4414
|
}
|
|
4415
|
+
// flat's click-to-action hook (ui/js/tools/tooltip_to_action.js): ixmaps.onItemAction(szId, item, themeObj) on a
|
|
4416
|
+
// click on an item — item.szSelectionId is the value of the theme's selection field — and onItemAction(null, null)
|
|
4417
|
+
// on a click on the map background
|
|
4418
|
+
function callItemAction(rt, object) {
|
|
4419
|
+
const hook = pageIxmaps() && pageIxmaps().onItemAction;
|
|
4420
|
+
if (typeof hook !== 'function') return;
|
|
4421
|
+
try {
|
|
4422
|
+
if (!rt) { hook.call(global.ixmaps, null, null); return; }
|
|
4423
|
+
const field = selectionFieldOf(rt);
|
|
4424
|
+
const props = object.properties || {};
|
|
4425
|
+
// a single record, or an aggregated item's first one
|
|
4426
|
+
const raw = props.raw || props.firstRaw || props.titleRaw || (props.raws && props.raws[0]) || props;
|
|
4427
|
+
const value = field && raw ? raw[field] : null;
|
|
4428
|
+
if (value == null) return;
|
|
4429
|
+
hook.call(global.ixmaps, themeIdOf(rt) + '::' + value, { szSelectionId: String(value) }, getThemeObj(themeIdOf(rt)));
|
|
4430
|
+
} catch (err) { console.error('[ixmaps-gl] onItemAction failed:', err); }
|
|
4431
|
+
}
|
|
3940
4432
|
function clickFor(picked) {
|
|
3941
4433
|
const info = picked && Object.assign({}, picked, { object: pickedFeature(picked.object) });
|
|
3942
4434
|
if (info && info.object && info.layer) {
|
|
3943
4435
|
const rt = findRuntimeForLayerId(info.layer.id);
|
|
3944
4436
|
if (rt) {
|
|
4437
|
+
callItemAction(rt, info.object);
|
|
3945
4438
|
// Only a Point geometry's own coordinates ARE an
|
|
3946
4439
|
// [lng,lat] pair (every point/bubble/dot/chart-cluster
|
|
3947
4440
|
// layer) — a CHOROPLETH polygon's geometry.coordinates is
|
|
@@ -3972,6 +4465,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3972
4465
|
}
|
|
3973
4466
|
}
|
|
3974
4467
|
// clicked empty map space (or a non-tooltippable layer) -> unpin
|
|
4468
|
+
callItemAction(null, null);
|
|
3975
4469
|
pinned = null;
|
|
3976
4470
|
pinnedTooltipEl.style.display = 'none';
|
|
3977
4471
|
return false;
|
|
@@ -4249,7 +4743,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4249
4743
|
// category rows: its panel is the text legend (title / snippet / description)
|
|
4250
4744
|
const countOnlyLegend = rt => !rt.categoryLabels && rt.flags.has('CATEGORICAL') && !rt.binding.value;
|
|
4251
4745
|
const legendApplies = rt => (countOnlyLegend(rt) || (rt.categoryLabels && rt.categoryLabels.length))
|
|
4252
|
-
&& (!(rt.flags.has('FEATURE') || rt.flags.has('FEATURES')) || rt.flags.has('CATEGORICAL')) && !rt.flags.has('NOLEGEND')
|
|
4746
|
+
&& (!(rt.flags.has('FEATURE') || rt.flags.has('FEATURES')) || rt.flags.has('CATEGORICAL')) && !rt.flags.has('NOLEGEND')
|
|
4747
|
+
// KDE and HEATMAP draw a density, not classes of the value: the range legend that a numeric
|
|
4748
|
+
// value would get is meaningless for them (no legend for these, as the fire pages)
|
|
4749
|
+
&& !rt.flags.has('KDE') && !rt.flags.has('HEATMAP') && !rt.flags.has('MLHEATMAP');
|
|
4253
4750
|
// One theme's panel — at build time for every theme, and again for a
|
|
4254
4751
|
// theme defined later (map.layer(...) in a myMap.then(...) chain,
|
|
4255
4752
|
// defineLayer, loadProject); removeTheme removes it with the theme.
|
|
@@ -4350,7 +4847,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4350
4847
|
// no SUM) falls back to a per-category record COUNT.
|
|
4351
4848
|
// a VECTOR / BEZIER flow legend sums its size field (flat's rows
|
|
4352
4849
|
// show the flows' summed sizes, measured on the twin page)
|
|
4353
|
-
|
|
4850
|
+
// a categorical symbol/bubble sized by a column: SUM sums that column per category (flat:
|
|
4851
|
+
// partsA[i].nSum accumulates each item's nSize, maptheme.js 16649; legend.js 677); a size
|
|
4852
|
+
// of "$item$" is the count anyway
|
|
4853
|
+
const useSum = flatFlag(rt.flags, 'SUM') && (rt.style.valuefield
|
|
4854
|
+
|| ((rt._isVectorChart() || rt._isAggregatedPie()) && rt.binding.size)
|
|
4855
|
+
|| (rt.flags.has('CATEGORICAL') && rt.binding.size && rt.binding.size !== '$item$'));
|
|
4354
4856
|
const valueField = rt.style.valuefield || rt.binding.size;
|
|
4355
4857
|
// legendunits wins over the theme's general-purpose units
|
|
4356
4858
|
// (used elsewhere for tooltips, e.g. _renderItemChartHtml) —
|
|
@@ -4433,7 +4935,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4433
4935
|
if (!c || typeof c[0] !== 'number') return;
|
|
4434
4936
|
const [lng, lat] = c;
|
|
4435
4937
|
if (!inViewport(lng, lat, bbox, globeCenter)) return;
|
|
4436
|
-
rt._multiFields.forEach((k, i) => { totals[i] +=
|
|
4938
|
+
rt._multiFields.forEach((k, i) => { totals[i] += scanValue(f.properties[k]) || 0; });
|
|
4437
4939
|
});
|
|
4438
4940
|
maxTotal = Math.max(0, ...totals);
|
|
4439
4941
|
order = totals.map((v, i) => i).sort((a, b) => totals[b] - totals[a]);
|
|
@@ -4446,7 +4948,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4446
4948
|
// with whatever the country filter is currently narrowing
|
|
4447
4949
|
// the map down to, not just with the viewport/hemisphere.
|
|
4448
4950
|
(rt._activeFeatures || rt.features).forEach(f => {
|
|
4449
|
-
const idx = rt.categoryIndexByLabel ? rt.
|
|
4951
|
+
const idx = rt.categoryIndexByLabel ? rt._resolveClassIndex(f.properties[rt.binding.value]) : null;
|
|
4450
4952
|
if (idx == null) return;
|
|
4451
4953
|
// a polygon (FEATURE|CATEGORICAL) is counted at its first vertex
|
|
4452
4954
|
let pt = f.geometry && f.geometry.coordinates;
|
|
@@ -4454,7 +4956,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4454
4956
|
if (!Array.isArray(pt)) return;
|
|
4455
4957
|
const [lng, lat] = pt;
|
|
4456
4958
|
if (!inViewport(lng, lat, bbox, globeCenter)) return;
|
|
4457
|
-
totals[idx] += useSum ? (
|
|
4959
|
+
totals[idx] += useSum ? (scanValue(f.properties[valueField]) || 0) : 1;
|
|
4458
4960
|
});
|
|
4459
4961
|
maxTotal = Math.max(0, ...totals);
|
|
4460
4962
|
order = totals.map((v, i) => i).sort((a, b) => totals[b] - totals[a]);
|
|
@@ -5313,11 +5815,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5313
5815
|
// world map can't open fully zoomed out. The deck.gl overlay takes
|
|
5314
5816
|
// its `repeat` from map.getRenderWorldCopies(), so the themes are
|
|
5315
5817
|
// drawn on every world copy too (flat draws them once; only its
|
|
5316
|
-
// basemap tiles wrap)
|
|
5818
|
+
// basemap tiles wrap) — unless themecopies is off, which is the default
|
|
5819
|
+
// (see applyThemeCopies). .options({worldcopies: false}) draws the
|
|
5317
5820
|
// world once, at the cost of that zoom limit.
|
|
5318
5821
|
renderWorldCopies: this._engineOptions.worldcopies !== false
|
|
5319
5822
|
});
|
|
5320
5823
|
engineOptionsByMap.set(map, this._engineOptions);
|
|
5824
|
+
// themecopies (default: off): the map keeps its world copies (free panning past the world's edge, empty
|
|
5825
|
+
// space beside it) but the deck.gl overlay, which asks map.getRenderWorldCopies() for its `repeat`, draws the
|
|
5826
|
+
// themes once, as flat does. .options({themecopies: true}) repeats them. MapLibre's own transform does not
|
|
5827
|
+
// use this method.
|
|
5828
|
+
applyThemeCopies(map, this._engineOptions.themecopies);
|
|
5321
5829
|
sphereBackdrops.forEach(style => applySphereBackdrop(map, style));
|
|
5322
5830
|
// the live-SVG pane USER charts render into (see chartHostFor) —
|
|
5323
5831
|
// registered here, keyed by this map's options object, so every
|
|
@@ -5446,6 +5954,16 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5446
5954
|
// the page's attribution (.attribution(), Map option attribution)
|
|
5447
5955
|
const attributionBox = createAttribution(map);
|
|
5448
5956
|
attributionBox.set(this._attributionText != null ? this._attributionText : this.mapOptions.attribution);
|
|
5957
|
+
// dark controls on a dark map; re-read when a style (basemap) loads and when basemapopacity is set
|
|
5958
|
+
const refreshControlLook = () => {
|
|
5959
|
+
const o = parseFloat(this._engineOptions.basemapopacity);
|
|
5960
|
+
const legendBackground = this.mapOptions.legendBackground || this.mapOptions.legendbackground;
|
|
5961
|
+
applyControlLook(el, typeof this.__effectiveDark === 'function'
|
|
5962
|
+
? this.__effectiveDark(isNaN(o) ? 1 : o, this.mapOptions.mapType, legendBackground) : false);
|
|
5963
|
+
};
|
|
5964
|
+
this._refreshControlLook = refreshControlLook;
|
|
5965
|
+
refreshControlLook();
|
|
5966
|
+
map.on('style.load', refreshControlLook);
|
|
5449
5967
|
this._setAttribution = text => attributionBox.set(text);
|
|
5450
5968
|
|
|
5451
5969
|
if (this._legendHtml) {
|
|
@@ -5586,18 +6104,19 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5586
6104
|
add: (layerBuilder, flag) => {
|
|
5587
6105
|
layerBuilder = unwrapLayer(layerBuilder);
|
|
5588
6106
|
_chainedLayers = _chainedLayers.then(() => {
|
|
6107
|
+
let slot = null;
|
|
5589
6108
|
if (/replace/i.test(flag || '')) {
|
|
5590
6109
|
const def = layerBuilder.definition();
|
|
5591
6110
|
const id = (def.style && def.style.name) || (def.meta && def.meta.name);
|
|
5592
|
-
if (id) removeThemeById(id);
|
|
6111
|
+
if (id) { slot = captureSlot(id); removeThemeById(id); }
|
|
5593
6112
|
}
|
|
5594
|
-
return engineApi.defineLayer(layerBuilder);
|
|
6113
|
+
return engineApi.defineLayer(layerBuilder, slot);
|
|
5595
6114
|
}).catch(err => console.error('[ixmaps-gl] map.add():', err));
|
|
5596
6115
|
return engineApi;
|
|
5597
6116
|
},
|
|
5598
6117
|
replace: (id, layerBuilder, flag) => {
|
|
5599
6118
|
layerBuilder = unwrapLayer(layerBuilder);
|
|
5600
|
-
_chainedLayers = _chainedLayers.then(() => { removeThemeById(id); return engineApi.defineLayer(layerBuilder); })
|
|
6119
|
+
_chainedLayers = _chainedLayers.then(() => { const slot = captureSlot(id); removeThemeById(id); return engineApi.defineLayer(layerBuilder, slot); })
|
|
5601
6120
|
.catch(err => console.error('[ixmaps-gl] map.replace():', err));
|
|
5602
6121
|
return engineApi;
|
|
5603
6122
|
},
|
|
@@ -5612,11 +6131,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5612
6131
|
layerBuilder = unwrapLayer(layerBuilder);
|
|
5613
6132
|
const f = String(flag || '');
|
|
5614
6133
|
_chainedLayers = _chainedLayers.then(() => {
|
|
6134
|
+
let slot = null;
|
|
5615
6135
|
if (!/force/i.test(f)) {
|
|
5616
6136
|
if (/replace/i.test(f)) {
|
|
5617
6137
|
const def = layerBuilder.definition();
|
|
5618
6138
|
const id = (def.style && def.style.name) || (def.meta && def.meta.name);
|
|
5619
|
-
if (id) removeThemeById(id);
|
|
6139
|
+
if (id) { slot = captureSlot(id); removeThemeById(id); }
|
|
5620
6140
|
} else if (/clearcharts|clear|clearall/i.test(f)) {
|
|
5621
6141
|
const charts = /clearcharts/i.test(f);
|
|
5622
6142
|
const doomed = runtimes.filter(rt => !flatFlag(rt.flags, 'LOCKED') && (!charts || rt.flags.has('CHART')));
|
|
@@ -5625,7 +6145,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5625
6145
|
byName.forEach((list, name) => removeRuntimes(name, list));
|
|
5626
6146
|
}
|
|
5627
6147
|
}
|
|
5628
|
-
return engineApi.defineLayer(layerBuilder);
|
|
6148
|
+
return engineApi.defineLayer(layerBuilder, slot);
|
|
5629
6149
|
}).catch(err => console.error('[ixmaps-gl] map.newTheme():', err));
|
|
5630
6150
|
return engineApi;
|
|
5631
6151
|
},
|
|
@@ -5857,7 +6377,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5857
6377
|
// exactly how this engine's own facet-filter/style setters below
|
|
5858
6378
|
// already push responsibility for "what should happen" to the
|
|
5859
6379
|
// caller rather than guessing.
|
|
5860
|
-
defineLayer: (layerBuilder) => defineFromDefinition(unwrapLayer(layerBuilder).definition()),
|
|
6380
|
+
defineLayer: (layerBuilder, slot) => defineFromDefinition(unwrapLayer(layerBuilder).definition(), slot),
|
|
5861
6381
|
// GL-PORT COMPAT: real ixmaps-flat's map handle, as a page gets it
|
|
5862
6382
|
// from `myMap.then(map => map.view(...).options(...).layer(a)
|
|
5863
6383
|
// .layer(b))` — the common multi-layer idiom — is chainable on the
|
|
@@ -5879,6 +6399,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5879
6399
|
if (o && typeof o === 'object') {
|
|
5880
6400
|
Object.assign(builder._engineOptions, o);
|
|
5881
6401
|
// read only when the MapLibre map is created — switched here at runtime
|
|
6402
|
+
if ('basemapopacity' in o && builder._refreshControlLook) builder._refreshControlLook();
|
|
6403
|
+
if ('themecopies' in o) { applyThemeCopies(map, o.themecopies); map.triggerRepaint(); }
|
|
5882
6404
|
if ('worldcopies' in o) map.setRenderWorldCopies(o.worldcopies !== false && o.worldcopies !== 'false');
|
|
5883
6405
|
refresh();
|
|
5884
6406
|
}
|
|
@@ -6017,6 +6539,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6017
6539
|
if (removed) { refresh(); notifyRedraw(); }
|
|
6018
6540
|
return removed;
|
|
6019
6541
|
}
|
|
6542
|
+
// where the theme(s) removeThemeById(id) would remove stand in the stack: the first remaining
|
|
6543
|
+
// theme after them (null = they were on top). Taken BEFORE the removal, handed to defineLayer.
|
|
6544
|
+
function captureSlot(id) {
|
|
6545
|
+
const named = runtimes.filter(r => (r.style && r.style.name === id) || (r.meta && r.meta.name === id));
|
|
6546
|
+
const doomed = named.length ? named : runtimes.filter(r => r.name === id);
|
|
6547
|
+
if (!doomed.length) return null;
|
|
6548
|
+
const last = Math.max(...doomed.map(r => runtimes.indexOf(r)));
|
|
6549
|
+
const anchor = runtimes.slice(last + 1).find(r => !doomed.includes(r) && !doomed.some(d => d._subTheme === r)) || null;
|
|
6550
|
+
return { anchor };
|
|
6551
|
+
}
|
|
6552
|
+
|
|
6020
6553
|
// flat addresses a theme by its id — style.name / meta.name (several
|
|
6021
6554
|
// themes may share one layer), else the layer name
|
|
6022
6555
|
function removeThemeById(id) {
|
|
@@ -6187,11 +6720,16 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6187
6720
|
// flat itself does ("defining a theme with the same name replaces it
|
|
6188
6721
|
// in place"; a page's live-refresh loop relies on it, e.g. the NYC
|
|
6189
6722
|
// status page re-defining its two themes every 10s).
|
|
6190
|
-
|
|
6723
|
+
// `slot` (from captureSlot) is where a replaced theme stood in the stack: the replace paths
|
|
6724
|
+
// remove the old theme first and pass it on, so the new one is not appended on top
|
|
6725
|
+
async function defineFromDefinition(def, slot) {
|
|
6191
6726
|
if (validation) validation.definition(def);
|
|
6192
6727
|
const pdef = projectThemeToDefinition(def);
|
|
6193
6728
|
const replacedId = (pdef.meta && pdef.meta.name) || (pdef.style && pdef.style.name);
|
|
6194
|
-
if (replacedId)
|
|
6729
|
+
if (replacedId) {
|
|
6730
|
+
if (!slot) slot = captureSlot(replacedId);
|
|
6731
|
+
removeThemeById(replacedId);
|
|
6732
|
+
}
|
|
6195
6733
|
let spec = normalizeTheme(pdef);
|
|
6196
6734
|
let raw;
|
|
6197
6735
|
const deferred = deferredFeatureLoad(spec, map.getZoom());
|
|
@@ -6218,7 +6756,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6218
6756
|
rt._zoomToPending = wantsZoomToExtent(spec.flags);
|
|
6219
6757
|
rt._dataSourceKey = named ? 'name:' + spec.data.name
|
|
6220
6758
|
: sourceKey || JSON.stringify({ url: spec.data && spec.data.url, urls: spec.data && spec.data.urls, type: spec.data && spec.data.type, query: spec.data && spec.data.query, obj: !!(spec.data && spec.data.obj) });
|
|
6221
|
-
|
|
6759
|
+
// a replacement keeps the stacking position (draw order, legend order) of the theme it replaces:
|
|
6760
|
+
// it goes in front of the theme that followed the old one, still on the map after the await above
|
|
6761
|
+
const slotAt = slot && slot.anchor ? runtimes.indexOf(slot.anchor) : -1;
|
|
6762
|
+
if (slotAt >= 0) runtimes.splice(slotAt, 0, rt); else runtimes.push(rt);
|
|
6222
6763
|
if (fc._sphere) applySphereBackdrop(map, rt.style);
|
|
6223
6764
|
_globalThemeRegistry.set(rt.name, rt);
|
|
6224
6765
|
if (rt.meta && rt.meta.name && rt.meta.name !== rt.name) _globalThemeRegistry.set(rt.meta.name, rt);
|
|
@@ -6268,10 +6809,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6268
6809
|
for (let i = 0; i < themes.length; i++) {
|
|
6269
6810
|
const t = themes[i] || {};
|
|
6270
6811
|
const name = (t.style && t.style.name) || (t.meta && t.meta.name);
|
|
6271
|
-
|
|
6812
|
+
let slot = null;
|
|
6813
|
+
if (/replace/i.test(f) && name) { slot = captureSlot(name); removeThemesNamed(name); }
|
|
6272
6814
|
else if (i === 0 && !/add/i.test(f)) [...new Set(runtimes.map(r => r.name))].forEach(n => engineApi.removeTheme(n));
|
|
6273
6815
|
try {
|
|
6274
|
-
report.themes.push(await defineFromDefinition(withoutProjectCode(t, report)));
|
|
6816
|
+
report.themes.push(await defineFromDefinition(withoutProjectCode(t, report), slot));
|
|
6275
6817
|
} catch (e) {
|
|
6276
6818
|
report.skipped.push({ layer: t.layer, reason: e.message });
|
|
6277
6819
|
}
|
|
@@ -6435,7 +6977,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6435
6977
|
// charts in their own group above them: whatever the theme order
|
|
6436
6978
|
// (a choropleth swapped in with add(..., 'replace') comes last), the
|
|
6437
6979
|
// shapes lie under the charts — in theme order among themselves
|
|
6438
|
-
const isShape = l => /^ix-(features|choropleth|shadow)-/.test(l.id);
|
|
6980
|
+
const isShape = l => /^ix-(features|choropleth|shadow|kde)-/.test(l.id);
|
|
6439
6981
|
layers = layers.filter(isShape).concat(layers.filter(l => !isShape(l)));
|
|
6440
6982
|
// every deck.gl layer id once: two themes of one layer name and type
|
|
6441
6983
|
// (the page's comune points and comune polygons, both FEATURES on
|
|
@@ -6479,6 +7021,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6479
7021
|
// flat's WMS themes render as native raster layers, not deck.gl —
|
|
6480
7022
|
// reconcile them with the current runtimes on every rebuild
|
|
6481
7023
|
syncWmsLayers();
|
|
7024
|
+
syncNativeHeatmaps(map, runtimes, mlHeatLayerIds);
|
|
6482
7025
|
}
|
|
6483
7026
|
|
|
6484
7027
|
// flat's WMS theme type (szFlag /\bWMS|IMAGE\b/, maptheme.js 8207-8247):
|
|
@@ -6495,6 +7038,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6495
7038
|
// map.getStyle() (a deep copy of the whole style) on every rebuild
|
|
6496
7039
|
// of a page that has no WMS theme
|
|
6497
7040
|
const wmsLayerIds = new Set();
|
|
7041
|
+
const mlHeatLayerIds = new Set(); // the MLHEATMAP layers added to the map (syncNativeHeatmaps)
|
|
6498
7042
|
function syncWmsLayers() {
|
|
6499
7043
|
if (!map || !map.getLayer) return;
|
|
6500
7044
|
const idOf = name => 'ixmaps-wms:' + String(name).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
@@ -6540,7 +7084,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6540
7084
|
}
|
|
6541
7085
|
// a style swap (setMapType) drops every native layer — rebuild the
|
|
6542
7086
|
// WMS ones once the new style stands
|
|
6543
|
-
map.on('style.load', () => syncWmsLayers());
|
|
7087
|
+
map.on('style.load', () => { syncWmsLayers(); syncNativeHeatmaps(map, runtimes, mlHeatLayerIds); });
|
|
6544
7088
|
|
|
6545
7089
|
// isZooming flips true/false on every 'zoomstart'/'zoomend' pair —
|
|
6546
7090
|
// and a scroll-wheel or trackpad zoom fires MANY such pairs per
|
|
@@ -7409,7 +7953,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7409
7953
|
: 1;
|
|
7410
7954
|
}
|
|
7411
7955
|
|
|
7412
|
-
function valueRadius(value, zoom, style, mapOptions, flags, maxSizeValue) {
|
|
7956
|
+
function valueRadius(value, zoom, style, mapOptions, flags, maxSizeValue, sizeBound) {
|
|
7413
7957
|
const zoomFactor = objectZoomFactor(zoom, mapOptions);
|
|
7414
7958
|
// FIXSIZE (a symbol chart; PLOT / GRIDSIZE size their markers
|
|
7415
7959
|
// themselves): one radius for every item, whatever its value — the
|
|
@@ -7452,7 +7996,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7452
7996
|
if (flags && flags.has('SYMBOL') && !flags.has('BUBBLE') && !flags.has('USER')
|
|
7453
7997
|
&& !flags.has('FIXSIZE') && !flags.has('PLOT') && !flags.has('GRIDSIZE')
|
|
7454
7998
|
&& flags.has('CATEGORICAL') && !flags.has('AGGREGATE') && !flatFlag(flags, 'GROUP')
|
|
7455
|
-
&& !style.sizefield) {
|
|
7999
|
+
&& !style.sizefield && !sizeBound) {
|
|
7456
8000
|
const fixed = NORMAL_RADIUS_PX * (styleNum(style.scale) || 1);
|
|
7457
8001
|
return Math.max(VALUE_RADIUS_MIN, Math.min(VALUE_RADIUS_MAX, fixed * zoomFactor));
|
|
7458
8002
|
}
|
|
@@ -7663,7 +8207,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7663
8207
|
// "$item$": every record counts 1
|
|
7664
8208
|
if (field === '$item$') return features.length ? 1 : 0;
|
|
7665
8209
|
return features.reduce((max, f) => {
|
|
7666
|
-
const v =
|
|
8210
|
+
const v = scanValue(f.properties[field]);
|
|
7667
8211
|
return isNaN(v) ? max : Math.max(max, v);
|
|
7668
8212
|
}, 0);
|
|
7669
8213
|
}
|
|
@@ -7825,6 +8369,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7825
8369
|
return out;
|
|
7826
8370
|
}
|
|
7827
8371
|
|
|
8372
|
+
// the outline colour of a flat bubble icon: none for NOLINES / linecolor "none", else linecolor, else the
|
|
8373
|
+
// class colour's darker derivate — and none when there is neither (an unfilled "none" class has no colour
|
|
8374
|
+
// to derive from; with a linecolor its outline must still draw)
|
|
8375
|
+
function bubbleStrokeRgb(look, rgb) {
|
|
8376
|
+
return look.noStroke ? null : (look.stroke || (rgb ? flatDerivateRgb(rgb, 0.7) : null));
|
|
8377
|
+
}
|
|
8378
|
+
|
|
7828
8379
|
// ChartColors.textColor (colorscheme.js 104-116): darker on light colors
|
|
7829
8380
|
function flatChartTextRgb(rgb) {
|
|
7830
8381
|
return flatDerivateRgb(rgb, rgb[0] + rgb[1] + rgb[2] > 450 ? 0.6 : 3);
|
|
@@ -7930,6 +8481,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7930
8481
|
const scale = scaleDenominatorAt(zoom);
|
|
7931
8482
|
return !!((upper && scale > upper) || (lower && scale < lower));
|
|
7932
8483
|
}
|
|
8484
|
+
// GLOW follows the theme's overall `opacity` (a number or numeric string; unset or 0 = 1, flat treats a falsy opacity as unset): the
|
|
8485
|
+
// halo is a fixed soft gradient texture (_getGlowIcon), so its strength is this alpha factor, as a bubble's is fillopacity
|
|
8486
|
+
function glowOpacityOf(style) {
|
|
8487
|
+
const v = Array.isArray(style.opacity) ? style.opacity[0] : style.opacity;
|
|
8488
|
+
const o = v == null || String(v).trim() === '' ? NaN : Number(v);
|
|
8489
|
+
return o > 0 && o < 1 ? o : 1;
|
|
8490
|
+
}
|
|
7933
8491
|
function themeHiddenByScale(flags, style, zoom) {
|
|
7934
8492
|
return (flags.has('FEATURE') || flags.has('FEATURES')) ? featuresHiddenByScale(style, zoom) : chartHiddenByScale(style, zoom);
|
|
7935
8493
|
}
|
|
@@ -9260,7 +9818,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9260
9818
|
}
|
|
9261
9819
|
const parts = [], zeroNot = [];
|
|
9262
9820
|
for (let k = 0; k < o.fields.length; k++) {
|
|
9263
|
-
let v =
|
|
9821
|
+
let v = scanValue(raw[o.fields[k]]);
|
|
9264
9822
|
// flat's weights multiply every record's per-field value as it is
|
|
9265
9823
|
// accumulated (maptheme.js 10506-10508) — the SIZE (nValueSize)
|
|
9266
9824
|
// stays raw, exactly as there
|
|
@@ -9923,14 +10481,14 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9923
10481
|
let maxAlpha = -Infinity, minAlpha = Infinity;
|
|
9924
10482
|
features.forEach(f => {
|
|
9925
10483
|
// flat: an alpha that is no number is 0 (maptheme.js 9853-9857)
|
|
9926
|
-
let v =
|
|
10484
|
+
let v = scanValue(f.properties[binding.alpha]);
|
|
9927
10485
|
if (isNaN(v)) v = 0;
|
|
9928
10486
|
if (isDensity) {
|
|
9929
10487
|
const areaKm2 = featureAreaKm2(f);
|
|
9930
10488
|
if (!areaKm2) return;
|
|
9931
10489
|
v = v / areaKm2;
|
|
9932
10490
|
} else if (alpha100) {
|
|
9933
|
-
const v100 =
|
|
10491
|
+
const v100 = scanValue(f.properties[alpha100]);
|
|
9934
10492
|
if (!isNaN(v100) && v100) v = 100 / v100 * v;
|
|
9935
10493
|
}
|
|
9936
10494
|
out._alphaByFeature.set(f, v);
|
|
@@ -10007,10 +10565,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10007
10565
|
features.forEach(f => {
|
|
10008
10566
|
if (!f.properties || !Object.keys(f.properties).length) return; // unjoined polygon
|
|
10009
10567
|
nItems++;
|
|
10010
|
-
const v100 = f100 ?
|
|
10568
|
+
const v100 = f100 ? scanValue(f.properties[f100]) : NaN;
|
|
10011
10569
|
if (f100 && isFinite(v100)) sum100 += v100;
|
|
10012
10570
|
fields.forEach((field, i) => {
|
|
10013
|
-
const v =
|
|
10571
|
+
const v = scanValue(f.properties[field]);
|
|
10014
10572
|
if (!isFinite(v)) return;
|
|
10015
10573
|
sums[i] += v;
|
|
10016
10574
|
if (f100 && isFinite(v100)) pooled[i] += v * v100;
|
|
@@ -10071,7 +10629,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10071
10629
|
const useDeviation = ctx.flags.has('DEVIATION');
|
|
10072
10630
|
let bestIndex = -1, bestRelevance = 0, bestValue = null;
|
|
10073
10631
|
for (let i = 0; i < fields.length; i++) {
|
|
10074
|
-
const v =
|
|
10632
|
+
const v = scanValue(props[fields[i]]);
|
|
10075
10633
|
if (!(v > (ctx._dominantMins[i] || 0))) continue;
|
|
10076
10634
|
const relevance = useDeviation ? (v - ctx._dominantMeans[i]) / ctx._dominantStdDevs[i]
|
|
10077
10635
|
: usePercentOfMean ? 100 * v / ctx._dominantMeans[i]
|
|
@@ -10099,7 +10657,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10099
10657
|
const fields = out._multiFields;
|
|
10100
10658
|
let nMax = 0;
|
|
10101
10659
|
features.forEach(f => fields.forEach(field => {
|
|
10102
|
-
const v =
|
|
10660
|
+
const v = scanValue(f.properties[field]);
|
|
10103
10661
|
if (v > nMax) nMax = v;
|
|
10104
10662
|
}));
|
|
10105
10663
|
out._composeColorMax = nMax;
|
|
@@ -10139,7 +10697,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10139
10697
|
const subtractive = ctx.flags.has('SUBTRACTIVE');
|
|
10140
10698
|
let rr = 0, gg = 0, bb = 0;
|
|
10141
10699
|
for (let i = 0; i < fields.length; i++) {
|
|
10142
|
-
const v =
|
|
10700
|
+
const v = scanValue(props[fields[i]]) || 0;
|
|
10143
10701
|
const weight = v / nMax;
|
|
10144
10702
|
const [r, g, b] = rgbs[i];
|
|
10145
10703
|
if (subtractive) { rr += (255 - r) * weight; gg += (255 - g) * weight; bb += (255 - b) * weight; }
|
|
@@ -10212,14 +10770,20 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10212
10770
|
// flat's reading of a value field (maptheme.js 9776-9800, defaults
|
|
10213
10771
|
// 7740-7801): a value that is no number counts as 0 — unless
|
|
10214
10772
|
// UNDEFINEDISNOTVALUE —, and 0 is a value — unless ZEROISNOTVALUE
|
|
10773
|
+
// flat's __scanValue (maptheme.js 9266): a value with a comma is EUROPEAN format (dots are thousands separators, the comma
|
|
10774
|
+
// the decimal: "1.234,5" -> 1234.5, "0,0637" -> 0.0637); a value without one is parsed plain, blanks stripped. Used wherever a
|
|
10775
|
+
// theme reads a value / size / alpha / value100 field from a record — the same strings plain parseFloat would cut at the comma
|
|
10776
|
+
function scanValue(v) {
|
|
10777
|
+
const s = String(v);
|
|
10778
|
+
return s.includes(',') ? parseFloat(s.replace(/\./g, '').replace(/,/g, '.')) : parseFloat(s.replace(/ /g, ''));
|
|
10779
|
+
}
|
|
10215
10780
|
function flatNumber(v, flags) {
|
|
10216
10781
|
// flat's own value scan (maptheme.js __scanValue 9265-9272): a
|
|
10217
10782
|
// value with a comma is EUROPEAN format — dots stripped as
|
|
10218
10783
|
// thousands separators, the comma is the decimal ("78,8" -> 78.8);
|
|
10219
10784
|
// a value without one is parsed plain, blanks stripped. Applied
|
|
10220
10785
|
// to every theme value in flat, so also here
|
|
10221
|
-
|
|
10222
|
-
let n = s.includes(',') ? parseFloat(s.replace(/\./g, '').replace(/,/g, '.')) : parseFloat(s.replace(/ /g, ''));
|
|
10786
|
+
let n = scanValue(v);
|
|
10223
10787
|
if (isNaN(n)) n = flags && flags.has('UNDEFINEDISNOTVALUE') ? NaN : 0;
|
|
10224
10788
|
if (n === 0 && flags && flags.has('ZEROISNOTVALUE')) return NaN;
|
|
10225
10789
|
return n;
|
|
@@ -10348,14 +10912,14 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10348
10912
|
if (!p) { out.push(f); continue; }
|
|
10349
10913
|
let clear = [];
|
|
10350
10914
|
if (size) {
|
|
10351
|
-
const v =
|
|
10915
|
+
const v = scanValue(p[size]);
|
|
10352
10916
|
if (!(isFinite(v) && v > 0)) {
|
|
10353
10917
|
if (!keepShapes) continue;
|
|
10354
10918
|
clear.push(size);
|
|
10355
10919
|
}
|
|
10356
10920
|
}
|
|
10357
10921
|
if (negativeIsNot) {
|
|
10358
|
-
const negative = checked.filter(k =>
|
|
10922
|
+
const negative = checked.filter(k => scanValue(p[k]) < 0);
|
|
10359
10923
|
if (negative.length && aggregate && !keepShapes) continue;
|
|
10360
10924
|
clear = clear.concat(negative);
|
|
10361
10925
|
}
|
|
@@ -10502,7 +11066,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10502
11066
|
// without AGGREGATE applyField100 already differenced every record
|
|
10503
11067
|
// (flat's nValuesA, popped): the first field IS the value
|
|
10504
11068
|
? (!flags.has('AGGREGATE') ? p => flatNumber(p[fields[0]], flags)
|
|
10505
|
-
: p => cellAggregatedValues({ sums: fields.map(k =>
|
|
11069
|
+
: p => cellAggregatedValues({ sums: fields.map(k => scanValue(p[k]) || 0), counts: fields.map(() => 1) }, flags, style)[0])
|
|
10506
11070
|
// "$item$" is flat's record count: 1 per record
|
|
10507
11071
|
: binding.value === '$item$' ? () => 1
|
|
10508
11072
|
: p => (p && Object.keys(p).length ? flatNumber(p[binding.value], flags) : NaN);
|
|
@@ -10653,9 +11217,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10653
11217
|
// theme's text 0.08 px high).
|
|
10654
11218
|
function resolveAggregateValue(binding, flags, props) {
|
|
10655
11219
|
if (binding.size === '$item$') return 1;
|
|
10656
|
-
if (binding.size) return
|
|
11220
|
+
if (binding.size) return scanValue(props[binding.size]) || 0;
|
|
10657
11221
|
if (flags.has('CATEGORICAL') || binding.value == null || binding.value === '$item$') return 1;
|
|
10658
|
-
return
|
|
11222
|
+
return scanValue(props[binding.value]) || 0;
|
|
10659
11223
|
}
|
|
10660
11224
|
|
|
10661
11225
|
// Whether a range-classed AGGREGATE cell's class value (flat: the sum
|
|
@@ -10959,7 +11523,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10959
11523
|
const add = (cat, v) => { if (cat == null || cat < 0 || cat >= n) return; count[cat]++; sum[cat] += Number.isFinite(v) ? v : 0; };
|
|
10960
11524
|
if (rt.flags.has('CHOROPLETH')) {
|
|
10961
11525
|
(rt._activeFeatures || rt.features).forEach(f => {
|
|
10962
|
-
const v =
|
|
11526
|
+
const v = scanValue(f.properties[rt.binding.value]);
|
|
10963
11527
|
add(rt._resolvePartsClass(v), v);
|
|
10964
11528
|
});
|
|
10965
11529
|
} else if (rt._legendItems) {
|
|
@@ -10970,7 +11534,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10970
11534
|
if (!c || typeof c[0] !== 'number') return;
|
|
10971
11535
|
const [lng, lat] = c;
|
|
10972
11536
|
if (!inViewport(lng, lat, bbox, globeCenter)) return;
|
|
10973
|
-
const v =
|
|
11537
|
+
const v = scanValue(f.properties[rt.binding.value]);
|
|
10974
11538
|
add(rt._resolvePartsClass(v), v);
|
|
10975
11539
|
});
|
|
10976
11540
|
}
|
|
@@ -11012,6 +11576,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11012
11576
|
this.name = spec.name;
|
|
11013
11577
|
this.binding = spec.binding;
|
|
11014
11578
|
this.style = spec.style;
|
|
11579
|
+
this._donorStyle = spec._donorStyle;
|
|
11015
11580
|
this.flags = symbolsFlagCompat(spec.flags, spec.style);
|
|
11016
11581
|
this.meta = spec.meta;
|
|
11017
11582
|
// see LAYER_DATA_GENERATION above — unique per runtime, see _dataLayerId
|
|
@@ -11117,7 +11682,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11117
11682
|
// a bar chart's parts are a pie's (its value fields or categories)
|
|
11118
11683
|
if ((this._isPieChart() || this._isBarChart()) && this.binding.value) {
|
|
11119
11684
|
this._preparePie();
|
|
11120
|
-
} else if (this.flags.has('DOMINANT') && this.binding.value && !isAggregatedCategoricalChoropleth(this)) {
|
|
11685
|
+
} else if (this.flags.has('DOMINANT') && this.binding.value && !isAggregatedCategoricalChoropleth(this) && !this._isDominantGrid()) {
|
|
11121
11686
|
// .type("CHOROPLETH|DOMINANT") — a MULTI-field bound value
|
|
11122
11687
|
// (binding.value is a pipe-joined field list, e.g. one CSV column
|
|
11123
11688
|
// per age band for the same year/sex — same pipe convention
|
|
@@ -11154,7 +11719,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11154
11719
|
} else {
|
|
11155
11720
|
const seen = new Map();
|
|
11156
11721
|
this.features.forEach(f => {
|
|
11157
|
-
|
|
11722
|
+
let v = f.properties[this.binding.value];
|
|
11723
|
+
if (typeof v === 'string') v = v.trim(); // flat: getStringValueIndex strips trailing blanks
|
|
11158
11724
|
if (v != null && !seen.has(v)) seen.set(v, seen.size);
|
|
11159
11725
|
});
|
|
11160
11726
|
this.categoryLabels = Array.from(seen.keys());
|
|
@@ -11197,7 +11763,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11197
11763
|
const scheme = Array.isArray(this.style.colorscheme) ? this.style.colorscheme : [];
|
|
11198
11764
|
this._partNoFill = new Set(this._multiFields.map((f, i) => i).filter(i => String(scheme[i]).trim().toLowerCase() === 'none'));
|
|
11199
11765
|
let max = 0;
|
|
11200
|
-
for (const f of this.features) for (const field of this._multiFields) { const v = Math.abs(
|
|
11766
|
+
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; }
|
|
11201
11767
|
this._multiFieldPartMax = max || 1;
|
|
11202
11768
|
// AUTO100's scale, measured on these features by _buildSequenceChartLayers
|
|
11203
11769
|
this._seqAuto100Max = null;
|
|
@@ -11354,12 +11920,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11354
11920
|
: this.flags.has('PERMILLE') ? 1000 : 100;
|
|
11355
11921
|
let sum = 0;
|
|
11356
11922
|
fields.forEach((field, i) => {
|
|
11357
|
-
const part =
|
|
11923
|
+
const part = scanValue(props[field]);
|
|
11358
11924
|
if (isNaN(part)) return;
|
|
11359
|
-
const v100 = f100A ?
|
|
11925
|
+
const v100 = f100A ? scanValue(props[f100A[Math.min(i, f100A.length - 1)]]) : NaN;
|
|
11360
11926
|
sum += isNaN(v100) ? part : (part * v100) / multiplier;
|
|
11361
11927
|
});
|
|
11362
|
-
const v100_0 = f100A ?
|
|
11928
|
+
const v100_0 = f100A ? scanValue(props[f100A[0]]) : NaN;
|
|
11363
11929
|
const nMyValue = sum / ((v100_0 / 100) || 1);
|
|
11364
11930
|
return composedDopacityAlpha(this.style, this.flags, nMyValue, this._composeColorMax || 1);
|
|
11365
11931
|
}
|
|
@@ -11378,8 +11944,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11378
11944
|
// (_buildPartsA, numeric bucket), or neither (a single implicit
|
|
11379
11945
|
// bucket, today's behavior for any other theme).
|
|
11380
11946
|
_resolveClassIndex(rawValue) {
|
|
11381
|
-
if (this.categoryIndexByLabel)
|
|
11382
|
-
|
|
11947
|
+
if (this.categoryIndexByLabel) {
|
|
11948
|
+
const i = this.categoryIndexByLabel.get(rawValue);
|
|
11949
|
+
return i === undefined && typeof rawValue === 'string' ? this.categoryIndexByLabel.get(rawValue.trim()) : i;
|
|
11950
|
+
}
|
|
11951
|
+
if (this.partsA) return this._resolvePartsClass(scanValue(rawValue));
|
|
11383
11952
|
return 0;
|
|
11384
11953
|
}
|
|
11385
11954
|
|
|
@@ -11449,8 +12018,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11449
12018
|
// as flat's chart node value (NYC status: value="63" = e-bikes)
|
|
11450
12019
|
const sizeField = this.binding.size && this.binding.size !== '$item$' ? this.binding.size : null;
|
|
11451
12020
|
this._featuresByCategory = [sourceFeatures.map(f => {
|
|
11452
|
-
const parts = this._multiFields.map(k =>
|
|
11453
|
-
const size = sizeField ? Math.abs(
|
|
12021
|
+
const parts = this._multiFields.map(k => scanValue(f.properties[k]) || 0);
|
|
12022
|
+
const size = sizeField ? Math.abs(scanValue(f.properties[sizeField]) || 0) : undefined;
|
|
11454
12023
|
return { type: 'Feature', geometry: f.geometry,
|
|
11455
12024
|
properties: { value: parts[0], parts, series: parts, ...(size !== undefined ? { size } : {}), raw: f.properties } };
|
|
11456
12025
|
})];
|
|
@@ -11476,9 +12045,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11476
12045
|
// values, the difference is taken on the cell sums (see post)
|
|
11477
12046
|
const seriesFields = String(this.binding.value || '').split('|');
|
|
11478
12047
|
this._seriesDifference = this.flags.has('DIFFERENCE') && seriesFields.length > 1 && !this._aggregateField100;
|
|
11479
|
-
const seriesOf = props => seriesFields.map(k =>
|
|
12048
|
+
const seriesOf = props => seriesFields.map(k => scanValue(props[k]) || 0);
|
|
11480
12049
|
// without AGGREGATE the records are already differenced (applyField100)
|
|
11481
|
-
const seriesValue = !this.flags.has('AGGREGATE') ? props =>
|
|
12050
|
+
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];
|
|
11482
12051
|
this._featuresByCategory = [sourceFeatures.map(f => ({
|
|
11483
12052
|
type: 'Feature',
|
|
11484
12053
|
geometry: f.geometry,
|
|
@@ -11486,8 +12055,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11486
12055
|
value: this._seriesDifference ? seriesValue(f.properties) : this._resolveAggregateValue(f.properties),
|
|
11487
12056
|
...(this._seriesDifference ? { series: seriesOf(f.properties) } : {}),
|
|
11488
12057
|
// "$item$": flat's record count, 1 per record
|
|
11489
|
-
...(this._classSeparate ? { classValue: this.binding.value === '$item$' ? 1 :
|
|
11490
|
-
...(this._aggregateField100 ? { value100:
|
|
12058
|
+
...(this._classSeparate ? { classValue: this.binding.value === '$item$' ? 1 : scanValue(f.properties[this.binding.value]) || 0 } : {}),
|
|
12059
|
+
...(this._aggregateField100 ? { value100: scanValue(f.properties[f100]) || 0 } : {}),
|
|
11491
12060
|
raw: f.properties
|
|
11492
12061
|
}
|
|
11493
12062
|
}))];
|
|
@@ -11776,7 +12345,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11776
12345
|
const countsByField = new Map(fields.map(f => [f, new Map()]));
|
|
11777
12346
|
|
|
11778
12347
|
scoped.forEach(f => {
|
|
11779
|
-
const weight = sizeField ? (
|
|
12348
|
+
const weight = sizeField ? (scanValue(f.properties[sizeField]) || 0) : 1;
|
|
11780
12349
|
for (const field of fields) {
|
|
11781
12350
|
const v = f.properties[field];
|
|
11782
12351
|
if (v == null || v === '') continue;
|
|
@@ -12103,6 +12672,91 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12103
12672
|
return Array.isArray(cs) && cs.length === 1 && String(cs[0]).trim().toLowerCase() === 'none';
|
|
12104
12673
|
}
|
|
12105
12674
|
|
|
12675
|
+
// an external symbol file (.svg/.png/.jpeg) named by `symbols` — flat draws it as an
|
|
12676
|
+
// <image> (maptheme.js 21743, 22168), not as the chart's own shape. null for a shape name
|
|
12677
|
+
_urlSymbolOf(sym) {
|
|
12678
|
+
if (typeof sym !== 'string') return null;
|
|
12679
|
+
const s = sym.trim();
|
|
12680
|
+
// a data: URI is a symbol file too, and the only kind fetch() can read on a file:// page
|
|
12681
|
+
return /\.(svg|png|jpe?g)([?#]|$)/i.test(s) || /^data:image\/(svg\+xml|png|jpe?g)[;,]/i.test(s) ? s : null;
|
|
12682
|
+
}
|
|
12683
|
+
|
|
12684
|
+
// the symbol file as an IconLayer icon descriptor, NOT tinted (flat doesn't recolor an
|
|
12685
|
+
// <image> either); fetched and rasterized once per URL, shared by SEQUENCE and plain symbols
|
|
12686
|
+
_urlSymbolIcon(u) {
|
|
12687
|
+
if (!this._urlIconCache) this._urlIconCache = new Map();
|
|
12688
|
+
if (!this._urlIconCache.has(u)) {
|
|
12689
|
+
// the placeholder is a transparent 1x1 PNG, NOT the raw symbol
|
|
12690
|
+
// URL: deck.gl would immediately try to load that itself, and an
|
|
12691
|
+
// SVG without natural dimensions (a viewBox only, the bus
|
|
12692
|
+
// pictogram of the City-moving page) makes Chrome's
|
|
12693
|
+
// createImageBitmap throw — a console error and a part that
|
|
12694
|
+
// never renders. The real raster replaces it below
|
|
12695
|
+
const entry = { url: TRANSPARENT_1PX_PNG, width: 256, height: 256, anchorX: 128, anchorY: 128, mask: false, id: 'seq-sym-pending:' + u };
|
|
12696
|
+
this._urlIconCache.set(u, entry);
|
|
12697
|
+
// fetch the symbol ONCE and rasterize it into the descriptor's
|
|
12698
|
+
// own 256px cell here: deck then always sees exactly the declared
|
|
12699
|
+
// dimensions, and dimension-less SVGs work like any other. The
|
|
12700
|
+
// redraw lets the next getIcon see the patched descriptor
|
|
12701
|
+
fetch(u).then(r => r.ok ? r.text() : null).then(text => new Promise((resolve, reject) => {
|
|
12702
|
+
if (typeof text !== 'string' || !/<svg[\s>]/i.test(text)) { reject(new Error('not svg')); return; }
|
|
12703
|
+
if (!/\swidth\s*=/i.test(text) || !/\sheight\s*=/i.test(text)) {
|
|
12704
|
+
const vb = text.match(/viewBox\s*=\s*["']([^"']*)["']/i);
|
|
12705
|
+
const p = vb && vb[1].trim().split(/[\s,]+/).map(Number);
|
|
12706
|
+
if (p && p.length === 4 && isFinite(p[2]) && p[2] > 0 && isFinite(p[3]) && p[3] > 0) {
|
|
12707
|
+
text = text.replace(/<svg/i, '<svg width="' + p[2] + '" height="' + p[3] + '"');
|
|
12708
|
+
}
|
|
12709
|
+
}
|
|
12710
|
+
const img = new Image();
|
|
12711
|
+
img.onload = () => resolve(img);
|
|
12712
|
+
img.onerror = () => reject(new Error('svg decode failed'));
|
|
12713
|
+
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(text);
|
|
12714
|
+
})).then(img => {
|
|
12715
|
+
const size = entry.width;
|
|
12716
|
+
const canvas = document.createElement('canvas');
|
|
12717
|
+
canvas.width = size; canvas.height = size;
|
|
12718
|
+
const ctx = canvas.getContext('2d');
|
|
12719
|
+
const s = Math.min(size / (img.naturalWidth || size), size / (img.naturalHeight || size));
|
|
12720
|
+
const dw = (img.naturalWidth || size) * s, dh = (img.naturalHeight || size) * s;
|
|
12721
|
+
ctx.drawImage(img, (size - dw) / 2, (size - dh) / 2, dw, dh);
|
|
12722
|
+
entry.url = canvas.toDataURL('image/png');
|
|
12723
|
+
entry.id = 'seq-sym-raster:' + u;
|
|
12724
|
+
if (this._triggerRedraw) this._triggerRedraw();
|
|
12725
|
+
}).catch(() => {
|
|
12726
|
+
// a symbol FILE of a file:// page can't be fetched (nor loaded by deck): say so once
|
|
12727
|
+
let onDisk = false;
|
|
12728
|
+
try { onDisk = new URL(u, location.href).protocol === 'file:'; } catch (e) { /* not a URL */ }
|
|
12729
|
+
if (onDisk && !this._warnedFileSymbol) {
|
|
12730
|
+
this._warnedFileSymbol = true;
|
|
12731
|
+
console.warn('[ixmaps-gl] symbol file "' + u + '" cannot be loaded from a file:// page — serve the page over http(s) or give the symbol as a data: URI');
|
|
12732
|
+
}
|
|
12733
|
+
// not an SVG (a PNG/JPEG symbol): deck's own URL loading takes
|
|
12734
|
+
// it from here — and a pre-sized SVG that failed to rasterize
|
|
12735
|
+
// gets that same second chance
|
|
12736
|
+
entry.url = u;
|
|
12737
|
+
entry.id = 'seq-sym-natural:' + u;
|
|
12738
|
+
if (this._triggerRedraw) this._triggerRedraw();
|
|
12739
|
+
});
|
|
12740
|
+
}
|
|
12741
|
+
return this._urlIconCache.get(u);
|
|
12742
|
+
}
|
|
12743
|
+
|
|
12744
|
+
// a size field is bound (flat's szSizeField): .binding({size}) or style.sizefield, live —
|
|
12745
|
+
// setSizeField() rebinds at runtime. '$item$' is not a field
|
|
12746
|
+
_sizeBound() { return !!(this.binding.size && this.binding.size !== '$item$'); }
|
|
12747
|
+
|
|
12748
|
+
// the symbol file of a plain (non-SEQUENCE) item, or null: the same class→symbol /
|
|
12749
|
+
// symbolfield mapping as _resolveSymbolShape, before it folds unknown names to 'circle'
|
|
12750
|
+
_resolveSymbolUrl(props) {
|
|
12751
|
+
const s = this.style.symbols;
|
|
12752
|
+
if (!Array.isArray(s)) return null;
|
|
12753
|
+
if (!this.style.symbolfield) {
|
|
12754
|
+
const cat = props && props.cat != null ? props.cat : 0;
|
|
12755
|
+
return this._urlSymbolOf(s[Math.min(cat, s.length - 1)]);
|
|
12756
|
+
}
|
|
12757
|
+
return this._urlSymbolOf(this._resolveSymbolShape(props));
|
|
12758
|
+
}
|
|
12759
|
+
|
|
12106
12760
|
_buildSingleIcon(colorRgb, opacity, shape, borderColorRgb, borderWidth, borderOpacity = 1) {
|
|
12107
12761
|
if (!colorRgb) { colorRgb = [128, 128, 128]; opacity = 0; } // no class colour (["none"]): unfilled
|
|
12108
12762
|
shape = shape || 'circle';
|
|
@@ -12340,13 +12994,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12340
12994
|
const fields = String(this.binding.value || '').split('|');
|
|
12341
12995
|
if (this._multiFieldMax === undefined) {
|
|
12342
12996
|
let max = 0;
|
|
12343
|
-
for (const f of this.features) for (const field of fields) { const v = Math.abs(
|
|
12997
|
+
for (const f of this.features) for (const field of fields) { const v = Math.abs(scanValue(f.properties[field])); if (v > max) max = v; }
|
|
12344
12998
|
this._multiFieldMax = max;
|
|
12345
12999
|
}
|
|
12346
13000
|
const labels = Array.isArray(this.style.label) && this.style.label.length === fields.length ? this.style.label.map(String)
|
|
12347
13001
|
: (this.categoryDisplayLabels || this.categoryLabels || fields);
|
|
12348
13002
|
const unit = this.style.units ? ' ' + this.style.units : '';
|
|
12349
|
-
let rows = fields.map((field, i) => ({ i, v:
|
|
13003
|
+
let rows = fields.map((field, i) => ({ i, v: scanValue(raw[field]) })).filter(r => Number.isFinite(r.v));
|
|
12350
13004
|
if (!rows.length) return '';
|
|
12351
13005
|
if (this.flags.has('SORT')) rows.sort((a, b) => (this.flags.has('UP') ? a.v - b.v : b.v - a.v));
|
|
12352
13006
|
const color = i => { const c = this.classRgb(i); return `rgb(${c.slice(0, 3).join(',')})`; };
|
|
@@ -12501,6 +13155,23 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12501
13155
|
// drawChart/modifier machinery entirely — a layer flagged DOT never
|
|
12502
13156
|
// reaches CHART|SYMBOL's clustering/GLOW/VALUES pipeline, even if
|
|
12503
13157
|
// those flags are also present in the type string).
|
|
13158
|
+
// MLHEATMAP: MapLibre's native heatmap layer (syncNativeHeatmaps) — nothing for deck.gl to draw
|
|
13159
|
+
if (this.flags.has('MLHEATMAP')) return [];
|
|
13160
|
+
// KDE: the density bands of the visible records (_buildKdeLayers)
|
|
13161
|
+
if (this.flags.has('KDE')) return this._buildKdeLayers(zoom, bbox, globeCenter);
|
|
13162
|
+
// HEATMAP: one GPU density surface over the visible records (_buildHeatmapLayers)
|
|
13163
|
+
if (this.flags.has('HEATMAP')) {
|
|
13164
|
+
// deck.gl's HeatmapLayer aggregates in screen space on a Web-Mercator viewport: under the
|
|
13165
|
+
// globe it draws nothing (measured, deck.gl 9.4.0 / MapLibre globe) — say so once
|
|
13166
|
+
if (globeCenter) {
|
|
13167
|
+
if (!this._warnedHeatmapGlobe) {
|
|
13168
|
+
this._warnedHeatmapGlobe = true;
|
|
13169
|
+
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`);
|
|
13170
|
+
}
|
|
13171
|
+
return [];
|
|
13172
|
+
}
|
|
13173
|
+
return this._buildHeatmapLayers(liveZoom);
|
|
13174
|
+
}
|
|
12504
13175
|
if (this.flags.has('DOT')) return this._buildDotLayers();
|
|
12505
13176
|
// GRIDSIZE (spatial grid binning) is checked before the generic
|
|
12506
13177
|
// CHART|SYMBOL bubble pipeline, same "more specific base type wins"
|
|
@@ -12600,7 +13271,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12600
13271
|
// layer keeps its one fill color
|
|
12601
13272
|
&& this.binding.value && this.binding.value !== '$item$' && !String(this.binding.value).includes('|');
|
|
12602
13273
|
const classColorOf = f => {
|
|
12603
|
-
const cls = this._resolvePartsClass(
|
|
13274
|
+
const cls = this._resolvePartsClass(scanValue(f.properties && f.properties[this.binding.value]));
|
|
12604
13275
|
return this.classRgb(cls);
|
|
12605
13276
|
};
|
|
12606
13277
|
const catAlpha = Math.round(255 * (styleNum(this.style.fillopacity) || 1));
|
|
@@ -12823,7 +13494,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12823
13494
|
// linecolor "none": no outline; a class color "none" (an explicit
|
|
12824
13495
|
// colorscheme list) leaves its polygons unpainted, an rgba() class
|
|
12825
13496
|
// color keeps its alpha — as flat's SVG fill does
|
|
12826
|
-
|
|
13497
|
+
// flat re-styles the SAME shape nodes the FEATURE base drew, so an
|
|
13498
|
+
// outline the choropleth doesn't define itself stays the base's own
|
|
13499
|
+
const donorStyle = this._donorStyle;
|
|
13500
|
+
const lineColorSrc = this.style.linecolor || (donorStyle && donorStyle.linecolor);
|
|
13501
|
+
const lineColorRaw = lineColorSrc ? styleLineColor(lineColorSrc) : null;
|
|
12827
13502
|
const lineRgb = lineColorRaw ? withAlpha(hexOrNamedToRgb(lineColorRaw), cssColorAlpha(lineColorRaw)) : [255, 255, 255];
|
|
12828
13503
|
const cs = this.style.colorscheme;
|
|
12829
13504
|
const classAlpha = Array.isArray(cs) && cs.length && !/^\d+$/.test(String(cs[0])) ? cs.map(cssColorAlpha) : null;
|
|
@@ -12855,11 +13530,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12855
13530
|
// flat re-styles the SAME shape nodes the FEATURE base drew, so a line
|
|
12856
13531
|
// choropleth keeps the base's own line width — the donor's linewidth
|
|
12857
13532
|
// is the fallback when the choropleth has none of its own
|
|
12858
|
-
const donorRt = [..._globalThemeRegistry.values()].find(r => r.name === this.name && r._isChoroplethGeometryDonor);
|
|
12859
13533
|
const anyLine = collection.features.some(isLineGeometry);
|
|
12860
13534
|
const isolating = !!(this._markedClasses.size && !this._onMarksChanged && evidenceMode === 'isolate');
|
|
12861
13535
|
const lineW = styleNum(this.style.linewidth)
|
|
12862
|
-
|| (
|
|
13536
|
+
|| (donorStyle && styleNum(donorStyle.linewidth)) || 1;
|
|
12863
13537
|
return [new GeoJsonLayer({
|
|
12864
13538
|
id: this._dataLayerId(`ix-choropleth-${this.name}`),
|
|
12865
13539
|
data: collection,
|
|
@@ -12941,8 +13615,15 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12941
13615
|
// GRIDSIZE without PLOT drawn as one uniform square per cell: only a
|
|
12942
13616
|
// plain grid — a single color, no VALUES, no GLOW
|
|
12943
13617
|
_isPlainGridMesh() {
|
|
12944
|
-
|
|
12945
|
-
|
|
13618
|
+
// categorical CENTER|DOMINANT cells are drawn by the same mesh path, per-cell colored
|
|
13619
|
+
return this._isDominantGrid() || (!this.flags.has('PLOT') && (this.categoryColorsRgb || []).length <= 1
|
|
13620
|
+
&& !this.flags.has('VALUES') && !this.flags.has('GLOW'));
|
|
13621
|
+
}
|
|
13622
|
+
// flat's categorical HexBin (GRIDSIZE|AGGREGATE|CATEGORICAL|DOMINANT): each grid cell takes
|
|
13623
|
+
// the color of its dominant category, with dynamic opacity from that category's part
|
|
13624
|
+
_isDominantGrid() {
|
|
13625
|
+
return this.flags.has('GRIDSIZE') && !this.flags.has('PLOT') && this.flags.has('CATEGORICAL')
|
|
13626
|
+
&& this.flags.has('DOMINANT');
|
|
12946
13627
|
}
|
|
12947
13628
|
// GRIDSIZE (no PLOT) symbol chart radius, px: flat's nGridSize / 2
|
|
12948
13629
|
// (maptheme.js 21843), nGridSize the aggregation cell pitch / nAutoSize
|
|
@@ -12959,6 +13640,19 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12959
13640
|
const autoSize = rect ? (gap ? 1.15 : 1.0) : (gap ? 0.8 : 0.75);
|
|
12960
13641
|
return cellPx / autoSize * (styleNum(this.style.scale) || 1) / 2;
|
|
12961
13642
|
}
|
|
13643
|
+
|
|
13644
|
+
// flat's AUTOSIZE (maptheme.js 21831-21843): the biggest aggregated item is the size of its
|
|
13645
|
+
// grid cell — the radius GRIDSIZE gives every item (hexagon cell/1.5, RECT cell/2; GAP 1.6 /
|
|
13646
|
+
// 2.3) — and the others follow (value/max)^(1/sizepow). This is the biggest item's radius;
|
|
13647
|
+
// null without AUTOSIZE, without a grid, or on a GRIDSIZE/PLOT theme (those size themselves)
|
|
13648
|
+
_autoSizeMaxRadiusPx(zoom, liveZoom) {
|
|
13649
|
+
if (!this.flags.has('AUTOSIZE') || this.flags.has('GRIDSIZE') || this.flags.has('PLOT') || !this._clusterRadiusPx) return null;
|
|
13650
|
+
const fromZoom = this._clusterUsesFixedZoom ? GRIDWIDTH_METERS_REFERENCE_ZOOM : zoom;
|
|
13651
|
+
const cellPx = this._clusterRadiusPx * Math.pow(2, liveZoom - fromZoom);
|
|
13652
|
+
const rect = flatFlag(this.flags, 'RECT');
|
|
13653
|
+
const gap = flatFlag(this.flags, 'GAP');
|
|
13654
|
+
return cellPx / (rect ? (gap ? 2.3 : 2.0) : (gap ? 1.6 : 1.5)) * (styleNum(this.style.scale) || 1);
|
|
13655
|
+
}
|
|
12962
13656
|
_isItemPlot() {
|
|
12963
13657
|
return this.flags.has('PLOT') && !this.flags.has('GRIDSIZE') && String(this.binding.value || '').includes('|');
|
|
12964
13658
|
}
|
|
@@ -13109,7 +13803,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13109
13803
|
for (const f of this.features) {
|
|
13110
13804
|
const pos = itemAnchor(f.geometry);
|
|
13111
13805
|
if (!pos) continue;
|
|
13112
|
-
const values = fields.map(k =>
|
|
13806
|
+
const values = fields.map(k => scanValue(f.properties[k]));
|
|
13113
13807
|
for (const v of values) if (!isNaN(v)) { dMin = Math.min(dMin, v); dMax = Math.max(dMax, v); }
|
|
13114
13808
|
if (!active || active.has(f)) items.push({ geometry: { type: 'Point', coordinates: pos }, properties: { values, raw: f.properties } });
|
|
13115
13809
|
}
|
|
@@ -13226,9 +13920,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13226
13920
|
: Array.isArray(this.style.xaxis) ? this.style.xaxis : null;
|
|
13227
13921
|
if (explicit) {
|
|
13228
13922
|
if (!this._explicitCatIndex) this._explicitCatIndex = new Map(explicit.map((v, i) => [String(v), i]));
|
|
13229
|
-
|
|
13923
|
+
const i = this._explicitCatIndex.get(String(rawValue));
|
|
13924
|
+
return i === undefined ? this._explicitCatIndex.get(String(rawValue).trim()) : i;
|
|
13230
13925
|
}
|
|
13231
|
-
|
|
13926
|
+
// "$item$" is no field: every record counts 1 (resolveAggregateValue), so its class is the 1
|
|
13927
|
+
return this._resolveClassIndex(this.binding.value === '$item$' ? 1 : rawValue);
|
|
13232
13928
|
}
|
|
13233
13929
|
|
|
13234
13930
|
// pure: cellAggregatedValues → see the pure-function sections above the class
|
|
@@ -13569,15 +14265,93 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13569
14265
|
// style.colorscheme fill.
|
|
13570
14266
|
_buildGridMeshLayers(zoom) {
|
|
13571
14267
|
this._ensureGridIndex(zoom);
|
|
13572
|
-
const data = this._gridIndex.map(cell => {
|
|
13573
|
-
const center = worldPixelToLngLat(cell.px, cell.py, zoom);
|
|
13574
|
-
return { geometry: { type: 'Point', coordinates: [center.lng, center.lat] } };
|
|
13575
|
-
});
|
|
13576
|
-
|
|
13577
14268
|
const fillColor = (resolveColorScheme(this.style.colorscheme, ['']) || ['rgba(255,255,255,0.3)'])[0] || 'rgba(255,255,255,0.3)';
|
|
13578
14269
|
const borderColor = styleLineColor(this.style.linecolor);
|
|
13579
14270
|
const borderWidth = styleNum(this.style.linewidth);
|
|
13580
14271
|
const cellPx = this._gridCellPx;
|
|
14272
|
+
|
|
14273
|
+
// flat's "Hexbin - dynamic opacity" (CHART|SYMBOL|GRIDSIZE|AGGREGATE|SUM|DOPACITY,
|
|
14274
|
+
// symbols:["hexagon"]): hexagon cells, each cell's fill OPACITY ramped from its
|
|
14275
|
+
// aggregated value (resolveDopacityAlpha, the CHOROPLETH|DOPACITY formulas). The plain
|
|
14276
|
+
// one-color mesh (neither flag) stays the uniform square below.
|
|
14277
|
+
const hexCells = Array.isArray(this.style.symbols) && this.style.symbols[0] === 'hexagon';
|
|
14278
|
+
const dopacity = this.flags.has('DOPACITY') || this.flags.has('DOPACITYMIN')
|
|
14279
|
+
|| this.flags.has('DOPACITYMAX') || this.flags.has('DOPACITYMINMAX');
|
|
14280
|
+
if (hexCells || dopacity) {
|
|
14281
|
+
const cells = this._gridIndex;
|
|
14282
|
+
const baseOpacity = styleNum(this.style.fillopacity);
|
|
14283
|
+
const base = isNaN(baseOpacity) ? 0.9 : baseOpacity;
|
|
14284
|
+
// CATEGORICAL|DOMINANT: dominant category per cell (the biggest part above 0, the
|
|
14285
|
+
// first of equal ones — aggregatedCategoricalClass), opacity from that part against
|
|
14286
|
+
// the biggest dominant part of the same class over all cells (dominantDopacityAlpha)
|
|
14287
|
+
if (this._isDominantGrid()) {
|
|
14288
|
+
const labels = this._plotCategories();
|
|
14289
|
+
const rgb = resolveClassColors(this.style.colorscheme, labels, undefined) || [];
|
|
14290
|
+
const dom = cells.map(c => {
|
|
14291
|
+
let i = -1;
|
|
14292
|
+
c.sums.forEach((v, k) => { if (v > 0 && (i < 0 || v > c.sums[i])) i = k; });
|
|
14293
|
+
return i < 0 ? null : { i, v: c.sums[i] };
|
|
14294
|
+
});
|
|
14295
|
+
// flat's nMin/nMax: extremes of the cell TOTAL over all categories
|
|
14296
|
+
const totals = cells.map(c => c.sums.reduce((s, x) => s + x, 0));
|
|
14297
|
+
const tMin = Math.min(...totals), tMax = Math.max(...totals);
|
|
14298
|
+
const data = [];
|
|
14299
|
+
// legend click (ixmaps.markThemeClass): flat's evidence "isolate" shows only the cells
|
|
14300
|
+
// whose dominant category is marked; "isolate_gray" keeps the others, dimmed
|
|
14301
|
+
const marking = this._markedClasses.size && !this._onMarksChanged;
|
|
14302
|
+
const gray = String(this.style.evidence || 'isolate') === 'isolate_gray';
|
|
14303
|
+
cells.forEach((cell, n) => {
|
|
14304
|
+
const d = dom[n];
|
|
14305
|
+
if (!d) return;
|
|
14306
|
+
if (marking && !this._markedClasses.has(d.i) && !gray) return;
|
|
14307
|
+
const center = worldPixelToLngLat(cell.px, cell.py, zoom);
|
|
14308
|
+
let a = dopacity ? symbolDopacityAlpha(this.style, d.v, tMin, tMax, baseOpacity) : base;
|
|
14309
|
+
if (marking && !this._markedClasses.has(d.i)) a = DIMMED_ICON_ALPHA / 255;
|
|
14310
|
+
const c = rgb[d.i] || [130, 130, 130];
|
|
14311
|
+
data.push({ geometry: { type: 'Point', coordinates: [center.lng, center.lat] }, rgb: c, alpha: a });
|
|
14312
|
+
});
|
|
14313
|
+
// white hexagon, tinted per cell by getColor (rgb + alpha)
|
|
14314
|
+
const r = cellPx / 1.5;
|
|
14315
|
+
// mask icon: deck tints only mask icons with getColor's rgb (non-mask: alpha only)
|
|
14316
|
+
const icon = Object.assign({}, this._buildGridHexIcon('#ffffff', 'none', 0, r), { mask: true });
|
|
14317
|
+
return [new IconLayer({
|
|
14318
|
+
id: `ix-grid-${this.name}-g${this._iconGeneration}`,
|
|
14319
|
+
data, pickable: false,
|
|
14320
|
+
getPosition: d => d.geometry.coordinates,
|
|
14321
|
+
getIcon: () => icon,
|
|
14322
|
+
getColor: d => [d.rgb[0], d.rgb[1], d.rgb[2], Math.round(255 * d.alpha)],
|
|
14323
|
+
getSize: 2 * r,
|
|
14324
|
+
sizeUnits: 'pixels'
|
|
14325
|
+
})];
|
|
14326
|
+
}
|
|
14327
|
+
// cell-level value stats (the grid IS the dataset for DOPACITY here)
|
|
14328
|
+
const vals = cells.map(c => c.sums[0]).filter(v => v != null && !isNaN(v)).sort((a, b) => a - b);
|
|
14329
|
+
const vMin = vals[0], vMax = vals[vals.length - 1];
|
|
14330
|
+
const data = cells.map(cell => {
|
|
14331
|
+
const center = worldPixelToLngLat(cell.px, cell.py, zoom);
|
|
14332
|
+
const a = dopacity ? symbolDopacityAlpha(this.style, cell.sums[0], vMin, vMax, baseOpacity) : base;
|
|
14333
|
+
return { geometry: { type: 'Point', coordinates: [center.lng, center.lat] }, alpha: a == null ? base : a };
|
|
14334
|
+
});
|
|
14335
|
+
// hexagon tiling (snapToAggregationGrid): column pitch cellPx = 1.5 r, so r = cellPx / 1.5
|
|
14336
|
+
const r = cellPx / 1.5;
|
|
14337
|
+
const icon = hexCells
|
|
14338
|
+
? this._buildGridHexIcon(fillColor, borderColor, isNaN(borderWidth) ? 0 : borderWidth, r)
|
|
14339
|
+
: this._buildGridSquareIcon(fillColor, borderColor, isNaN(borderWidth) ? 0 : borderWidth, cellPx);
|
|
14340
|
+
return [new IconLayer({
|
|
14341
|
+
id: `ix-grid-${this.name}-g${this._iconGeneration}`,
|
|
14342
|
+
data, pickable: false,
|
|
14343
|
+
getPosition: d => d.geometry.coordinates,
|
|
14344
|
+
getIcon: () => icon,
|
|
14345
|
+
getColor: d => [255, 255, 255, Math.round(255 * d.alpha)],
|
|
14346
|
+
getSize: hexCells ? 2 * r : cellPx,
|
|
14347
|
+
sizeUnits: 'pixels'
|
|
14348
|
+
})];
|
|
14349
|
+
}
|
|
14350
|
+
|
|
14351
|
+
const data = this._gridIndex.map(cell => {
|
|
14352
|
+
const center = worldPixelToLngLat(cell.px, cell.py, zoom);
|
|
14353
|
+
return { geometry: { type: 'Point', coordinates: [center.lng, center.lat] } };
|
|
14354
|
+
});
|
|
13581
14355
|
const icon = this._buildGridSquareIcon(fillColor, borderColor, isNaN(borderWidth) ? 0 : borderWidth, cellPx);
|
|
13582
14356
|
|
|
13583
14357
|
return [new IconLayer({
|
|
@@ -13681,7 +14455,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13681
14455
|
// the item's class (range classes set it for groups too — a negative
|
|
13682
14456
|
// group total would make dominant() pick the wrong class)
|
|
13683
14457
|
const cat = d.properties.cat != null ? d.properties.cat : (d.properties.counts ? dominant(d.properties.counts) : 0);
|
|
13684
|
-
const radiusPx = valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue);
|
|
14458
|
+
const radiusPx = valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue, this._sizeBound());
|
|
13685
14459
|
const size = radiusPx / pxPerUnit;
|
|
13686
14460
|
const chart = this._userChartSvg(draw, name, theme, flag, d, value, cat, size, sizeValueOf(d));
|
|
13687
14461
|
if (!chart) continue;
|
|
@@ -14021,6 +14795,30 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14021
14795
|
return this._cacheIcon(key, icon);
|
|
14022
14796
|
}
|
|
14023
14797
|
|
|
14798
|
+
// one flat-top hexagon of circumradius r (px) filling a square raster — the GRIDSIZE
|
|
14799
|
+
// "hexagon" cell (see _buildGridMeshLayers); drawSymbolPath's own hexagon outline
|
|
14800
|
+
_buildGridHexIcon(fillColor, borderColor, borderWidth, r) {
|
|
14801
|
+
const key = `gridhex|${fillColor}|${borderColor}|${borderWidth}|${r}`;
|
|
14802
|
+
if (this._iconCache.has(key)) return this._iconCache.get(key);
|
|
14803
|
+
const size = Math.max(32, Math.min(256, Math.round(2 * r) || 32));
|
|
14804
|
+
const canvas = document.createElement('canvas');
|
|
14805
|
+
canvas.width = size; canvas.height = size;
|
|
14806
|
+
const ctx = canvas.getContext('2d');
|
|
14807
|
+
const rr = size / 2 - 1; // 1px margin so the stroke isn't clipped
|
|
14808
|
+
ctx.beginPath();
|
|
14809
|
+
drawSymbolPath(ctx, 'hexagon', size / 2, size / 2, rr);
|
|
14810
|
+
ctx.closePath();
|
|
14811
|
+
ctx.fillStyle = fillColor;
|
|
14812
|
+
ctx.fill();
|
|
14813
|
+
if (borderColor && borderColor !== 'none' && borderWidth > 0) {
|
|
14814
|
+
ctx.strokeStyle = borderColor;
|
|
14815
|
+
ctx.lineWidth = borderWidth * (size / (2 * r || size));
|
|
14816
|
+
ctx.stroke();
|
|
14817
|
+
}
|
|
14818
|
+
const icon = { url: canvas.toDataURL(), width: size, height: size, anchorX: size / 2, anchorY: size / 2, id: key };
|
|
14819
|
+
return this._cacheIcon(key, icon);
|
|
14820
|
+
}
|
|
14821
|
+
|
|
14024
14822
|
_buildGridSquareIcon(fillColor, borderColor, borderWidth, cellPx) {
|
|
14025
14823
|
const key = `grid|${fillColor}|${borderColor}|${borderWidth}|${cellPx}`;
|
|
14026
14824
|
if (this._iconCache.has(key)) return this._iconCache.get(key);
|
|
@@ -14049,6 +14847,114 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14049
14847
|
return this._cacheIcon(key, icon);
|
|
14050
14848
|
}
|
|
14051
14849
|
|
|
14850
|
+
// .type("KDE"): the kernel density of the VISIBLE records as stacked bands (the fire pages' KDE).
|
|
14851
|
+
// The records inside the view (plus 3 bandwidths around it, which still add density to it) go
|
|
14852
|
+
// to kdeGrid in a worker, d3-contour cuts the bands at fractions of the maximum (style.kdelevels,
|
|
14853
|
+
// default 0.15 / 0.30 / 0.50 / 0.70), each band the area at or above its threshold, so they
|
|
14854
|
+
// stack, colored from style.colorscheme, drawn with style.fillopacity. The bandwidth is
|
|
14855
|
+
// style.bandwidth (km) or, without it, follows the zoom (kdeBandwidthKm).
|
|
14856
|
+
// The result belongs to a view: while the map moves the last bands stay, and once it stands still
|
|
14857
|
+
// (and the view, the records, the binding or the bandwidth changed) the new ones are computed
|
|
14858
|
+
// after a short pause and drawn by a redraw — never more than one computation wanted at a time.
|
|
14859
|
+
// Weights as HEATMAP: the size field, else the numeric value field, else 1 each ($item$).
|
|
14860
|
+
_buildKdeLayers(zoom, bbox, globeCenter) {
|
|
14861
|
+
// the bands are lng/lat polygons of a flat view: under the globe there is no such view — say so once
|
|
14862
|
+
if (globeCenter) {
|
|
14863
|
+
if (!this._warnedKdeGlobe) {
|
|
14864
|
+
this._warnedKdeGlobe = true;
|
|
14865
|
+
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`);
|
|
14866
|
+
}
|
|
14867
|
+
return [];
|
|
14868
|
+
}
|
|
14869
|
+
const source = this._activeFeatures || this.features;
|
|
14870
|
+
if (this._kdeSource !== source) { this._kdeSource = source; this._kdeSourceId = (this._kdeSourceId || 0) + 1; }
|
|
14871
|
+
const flatZoom = mapLibreToFlatZoom(zoom);
|
|
14872
|
+
const bwKm = styleNum(this.style.bandwidth) > 0 ? styleNum(this.style.bandwidth) : kdeBandwidthKm(flatZoom);
|
|
14873
|
+
const maxCells = kdeMaxCells(flatZoom);
|
|
14874
|
+
const levels = kdeLevels(this.style);
|
|
14875
|
+
const key = JSON.stringify([this._dataGen, this._kdeSourceId, this.binding, bwKm, maxCells, levels, bbox.map(v => Math.round(v * 1e4) / 1e4)]);
|
|
14876
|
+
if (key !== this._kdeKeyDone && key !== this._kdeKeyWanted && !this._viewMoving) {
|
|
14877
|
+
this._kdeKeyWanted = key;
|
|
14878
|
+
clearTimeout(this._kdeTimer);
|
|
14879
|
+
this._kdeTimer = setTimeout(() => {
|
|
14880
|
+
const margin = 3 * bwKm;
|
|
14881
|
+
const mLat = margin / 111.3;
|
|
14882
|
+
const mLng = margin / (111.3 * Math.max(0.05, Math.cos((bbox[1] + bbox[3]) / 2 * Math.PI / 180)));
|
|
14883
|
+
const points = kdePoints(source, this.binding, [bbox[0] - mLng, bbox[1] - mLat, bbox[2] + mLng, bbox[3] + mLat]);
|
|
14884
|
+
Promise.all([ensureD3Contour(), kdeCompute(points, bwKm, maxCells)]).then(([contours, grid]) => {
|
|
14885
|
+
if (this._kdeKeyWanted !== key) return; // the view moved on: a newer computation is wanted
|
|
14886
|
+
this._kdeBands = kdeBands(grid, levels, () => contours());
|
|
14887
|
+
this._kdeBandsGen = (this._kdeBandsGen || 0) + 1; // every set of bands is a new layer for deck.gl
|
|
14888
|
+
this._kdeLevelCount = levels.length;
|
|
14889
|
+
this._kdeKeyDone = key;
|
|
14890
|
+
if (this._triggerRedraw) this._triggerRedraw();
|
|
14891
|
+
}).catch(err => {
|
|
14892
|
+
console.error(`[ixmaps-gl] layer "${this.name}": KDE not computed:`, err);
|
|
14893
|
+
if (this._kdeKeyWanted === key) this._kdeKeyDone = key; // not retried until the view changes
|
|
14894
|
+
});
|
|
14895
|
+
}, 150);
|
|
14896
|
+
}
|
|
14897
|
+
const bands = this._kdeBands;
|
|
14898
|
+
if (!bands || !bands.features.length) return [];
|
|
14899
|
+
const colors = kdeBandColors(this.style.colorscheme, this._kdeLevelCount || levels.length);
|
|
14900
|
+
return [new GeoJsonLayer({
|
|
14901
|
+
// a new id for each set of bands: deck.gl keeps a layer's buffers by id, and bands whose polygon
|
|
14902
|
+
// structure changed (holes, counts) under the SAME id drew long triangles between polygons
|
|
14903
|
+
id: this._dataLayerId(`ix-kde-${this.name}-b${this._kdeBandsGen || 0}`),
|
|
14904
|
+
data: bands, pickable: false, stroked: false, filled: true,
|
|
14905
|
+
getFillColor: f => colors[f.properties.level] || colors[colors.length - 1],
|
|
14906
|
+
updateTriggers: { getFillColor: [JSON.stringify(this.style.colorscheme), colors.length] },
|
|
14907
|
+
opacity: styleNum(this.style.fillopacity) || 0.25
|
|
14908
|
+
})];
|
|
14909
|
+
}
|
|
14910
|
+
|
|
14911
|
+
// .type("HEATMAP"): a kernel-density surface (deck.gl HeatmapLayer) of the records' positions,
|
|
14912
|
+
// weighted by heatmapWeight. style.radiuspx (default 30) is the kernel radius in screen pixels
|
|
14913
|
+
// at the reference zoom; it follows the map's object scaling like every symbol
|
|
14914
|
+
// (objectZoomFactor), i.e. it grows and shrinks with the zoom like a real-world distance.
|
|
14915
|
+
// style: scale (radius multiplier), intensity, threshold, colorscheme, fillopacity.
|
|
14916
|
+
//
|
|
14917
|
+
// PERFORMANCE: the layer is rebuilt on every frame of a zoom gesture. A new data array, a new
|
|
14918
|
+
// colorRange or a new radius makes deck redo its GPU work (weight map, color texture) — measured
|
|
14919
|
+
// 90 ms a frame with them rebuilt each time, 15 ms with the same array/colors and a radius that
|
|
14920
|
+
// moves in steps of 2^(1/6) (~12%). So the data and the colors are kept until the records, the
|
|
14921
|
+
// binding or the colorscheme change, and the radius is quantized.
|
|
14922
|
+
_buildHeatmapLayers(liveZoom) {
|
|
14923
|
+
const source = this._activeFeatures || this.features;
|
|
14924
|
+
const bindingKey = JSON.stringify(this.binding);
|
|
14925
|
+
if (!this._heatmapData || this._heatmapSource !== source || this._heatmapBinding !== bindingKey) {
|
|
14926
|
+
const data = [];
|
|
14927
|
+
for (const f of source) {
|
|
14928
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
14929
|
+
if (!c || typeof c[0] !== 'number' || !isFinite(c[0]) || !isFinite(c[1])) continue;
|
|
14930
|
+
const w = heatmapWeight(this.binding, f.properties || {});
|
|
14931
|
+
if (w > 0) data.push({ position: [c[0], c[1]], weight: w });
|
|
14932
|
+
}
|
|
14933
|
+
this._heatmapData = data;
|
|
14934
|
+
this._heatmapSource = source;
|
|
14935
|
+
this._heatmapBinding = bindingKey;
|
|
14936
|
+
}
|
|
14937
|
+
const colorKey = JSON.stringify(this.style.colorscheme);
|
|
14938
|
+
if (!this._heatmapColors || this._heatmapColorKey !== colorKey) {
|
|
14939
|
+
this._heatmapColors = heatmapColorRange(this.style.colorscheme);
|
|
14940
|
+
this._heatmapColorKey = colorKey;
|
|
14941
|
+
}
|
|
14942
|
+
const radiusPx = (styleNum(this.style.radiuspx) || 30) * (styleNum(this.style.scale) || 1)
|
|
14943
|
+
* objectZoomFactor(liveZoom, this.mapOptions);
|
|
14944
|
+
const stepped = Math.pow(2, Math.round(Math.log2(Math.max(1, radiusPx)) * 6) / 6);
|
|
14945
|
+
return [new HeatmapLayer({
|
|
14946
|
+
id: `ix-heatmap-${this.name}`,
|
|
14947
|
+
data: this._heatmapData, pickable: false,
|
|
14948
|
+
getPosition: d => d.position,
|
|
14949
|
+
getWeight: d => d.weight,
|
|
14950
|
+
radiusPixels: Math.max(1, Math.min(400, stepped)),
|
|
14951
|
+
intensity: styleNum(this.style.intensity) || 1,
|
|
14952
|
+
threshold: styleNum(this.style.threshold) || 0.05,
|
|
14953
|
+
colorRange: this._heatmapColors,
|
|
14954
|
+
opacity: styleNum(this.style.fillopacity) || 1
|
|
14955
|
+
})];
|
|
14956
|
+
}
|
|
14957
|
+
|
|
14052
14958
|
// .type("DOT") — one point per record, always (no clustering, matching
|
|
14053
14959
|
// the real engine bypassing AGGREGATE for this type). Color comes from
|
|
14054
14960
|
// a bound value's class — CATEGORICAL (exact match) or numeric
|
|
@@ -14064,7 +14970,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14064
14970
|
const data = source.map(f => ({
|
|
14065
14971
|
geometry: f.geometry,
|
|
14066
14972
|
properties: {
|
|
14067
|
-
value: this.binding.size ?
|
|
14973
|
+
value: this.binding.size ? scanValue(f.properties[this.binding.size]) : undefined,
|
|
14068
14974
|
raw: f.properties,
|
|
14069
14975
|
cat: hasClasses ? this._resolveClassIndex(f.properties[this.binding.value]) : null
|
|
14070
14976
|
}
|
|
@@ -14321,7 +15227,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14321
15227
|
// GRIDSIZE: every chart the size of its grid cell, whatever its value
|
|
14322
15228
|
// (_gridChartRadiusPx); otherwise valueRadius
|
|
14323
15229
|
const gridR = this._gridChartRadiusPx(zoom, liveZoom);
|
|
14324
|
-
|
|
15230
|
+
// AUTOSIZE: the biggest aggregated item is the size of the cell, the others by value
|
|
15231
|
+
const autoR = this._autoSizeMaxRadiusPx(zoom, liveZoom);
|
|
15232
|
+
const autoMax = autoR != null ? (this._ensureAggregateStats(zoom).sizeMax || 1) : 1;
|
|
15233
|
+
const autoPow = autoR != null ? resolveSizePow(this.style, this.flags) : 1;
|
|
15234
|
+
const vRadius = v => (gridR != null ? gridR
|
|
15235
|
+
: autoR != null ? Math.max(VALUE_RADIUS_MIN, autoR * Math.pow(Math.abs(v || 0) / autoMax, 1 / autoPow))
|
|
15236
|
+
: valueRadius(v, liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue, this._sizeBound()));
|
|
14325
15237
|
// AGGREGATE sums without a normalsizevalue: flat's default is the
|
|
14326
15238
|
// largest aggregated value (defaultNormalSizeValue leaves it unset, the
|
|
14327
15239
|
// sizes depend on the aggregation)
|
|
@@ -14537,7 +15449,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14537
15449
|
getPosition: d => d.geometry.coordinates,
|
|
14538
15450
|
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]),
|
|
14539
15451
|
getSize: d => vRadius(sizeValueOf(d)) * (d.properties.counts ? 9 : 11),
|
|
14540
|
-
getColor: d => [255, 255, 255, this._iconAlpha(d)],
|
|
15452
|
+
getColor: d => [255, 255, 255, Math.round(this._iconAlpha(d) * glowOpacityOf(this.style))],
|
|
14541
15453
|
...(alignOf.active ? { getPixelOffset: alignOf } : {}),
|
|
14542
15454
|
sizeUnits: 'pixels',
|
|
14543
15455
|
billboard: true,
|
|
@@ -14641,7 +15553,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14641
15553
|
if (!flatLook) return this._buildSingleIcon(rgb, fillOpacity, shapeOf(d.properties), singleBorderColorRgb, singleBorderWidthPx);
|
|
14642
15554
|
const look = flatLook(d);
|
|
14643
15555
|
return this._buildSingleIcon(rgb, look.fillOpacity, shapeOf(d.properties),
|
|
14644
|
-
|
|
15556
|
+
bubbleStrokeRgb(look, rgb), look.width, look.strokeOpacity);
|
|
14645
15557
|
};
|
|
14646
15558
|
// flat's symbol outline (maptheme.js 21595-21610 and 21950-21997,
|
|
14647
15559
|
// drawn 21625-21634 / 21995-22035): every SYMBOL shape is stroked,
|
|
@@ -14676,7 +15588,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14676
15588
|
// routing goes by the final id (layerRuntimeById). A symbol's outline
|
|
14677
15589
|
// is centered on its radius (flat); BUBBLE's border keeps the
|
|
14678
15590
|
// icon's look, inside the radius
|
|
14679
|
-
if (combined.length && !alignOf.active && combined.every(d => singleSymbol(d) && shapeOf(d.properties) === 'circle')) {
|
|
15591
|
+
if (combined.length && !alignOf.active && combined.every(d => singleSymbol(d) && shapeOf(d.properties) === 'circle' && !this._resolveSymbolUrl(d.properties))) {
|
|
14680
15592
|
const radiusPx = d => vRadius(sizeValueOf(d));
|
|
14681
15593
|
// a CATEGORICAL BUBBLE: flat's look (flatBubbleLook) in screen px,
|
|
14682
15594
|
// its stroke centered on the radius as flat's SVG circle
|
|
@@ -14705,6 +15617,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14705
15617
|
getPosition: d => d.geometry.coordinates,
|
|
14706
15618
|
getIcon: d => {
|
|
14707
15619
|
if (!singleSymbol(d)) return this._buildBubbleIcon(d.properties.counts, this.categoryColorsRgb, this._groupIsolation(d), Array.isArray(this.style.symbols) ? this.style.symbols : null, flatLook && flatLook(d));
|
|
15620
|
+
const symbolUrl = this._resolveSymbolUrl(d.properties);
|
|
15621
|
+
if (symbolUrl) return this._urlSymbolIcon(symbolUrl);
|
|
14708
15622
|
if (flatLook) return singleIcon(d);
|
|
14709
15623
|
const shape = shapeOf(d.properties), rgb = outlineRgb(d);
|
|
14710
15624
|
if (!symbolOutline || !rgb) return this._buildSingleIcon((this.categoryColorsRgb || [])[d.properties.cat], fillOpacity, shape, rgb, rgb ? singleBorderWidthPx : 0);
|
|
@@ -14947,7 +15861,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14947
15861
|
if (multi && auto100 && !this._seqAuto100Max) {
|
|
14948
15862
|
let m = 0;
|
|
14949
15863
|
for (const f of this.features) {
|
|
14950
|
-
const parts = this._multiFields.map(k =>
|
|
15864
|
+
const parts = this._multiFields.map(k => scanValue(f.properties && f.properties[k]) || 0);
|
|
14951
15865
|
const sum = parts.reduce((a, v) => a + v, 0);
|
|
14952
15866
|
if (sum > 0) for (const v of parts) { const nv = v / (sum / 100); if (nv > m) m = nv; }
|
|
14953
15867
|
}
|
|
@@ -14968,12 +15882,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14968
15882
|
const sizeField = multi && this.binding.size && this.binding.size !== '$item$' ? this.binding.size : null;
|
|
14969
15883
|
let sizeMax = 0;
|
|
14970
15884
|
if (sizeField) {
|
|
14971
|
-
for (const f of this.features) { const v = Math.abs(
|
|
15885
|
+
for (const f of this.features) { const v = Math.abs(scanValue(f.properties && f.properties[sizeField]) || 0); if (v > sizeMax) sizeMax = v; }
|
|
14972
15886
|
}
|
|
14973
15887
|
const itemSizeFactor = d => {
|
|
14974
15888
|
if (!sizeField) return 1;
|
|
14975
15889
|
const sz = typeof d.properties.size === 'number' ? d.properties.size
|
|
14976
|
-
: Math.abs(
|
|
15890
|
+
: Math.abs(scanValue((d.properties.raw || {})[sizeField]) || 0);
|
|
14977
15891
|
return sizeMax > 0 ? Math.pow(sz / sizeMax, 1 / sizePow) : 0;
|
|
14978
15892
|
};
|
|
14979
15893
|
const rings = flatFlag(this.flags, 'RINGS');
|
|
@@ -14994,60 +15908,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14994
15908
|
// it itself (its ImageBitmap loader falls back to <img>, which
|
|
14995
15909
|
// rasterizes SVG) — NOT tinted: these files carry their own colors,
|
|
14996
15910
|
// and flat doesn't recolor an <image> either
|
|
14997
|
-
const urlSymbolOf = i =>
|
|
14998
|
-
|
|
14999
|
-
return typeof s === 'string' && /\.(svg|png|jpe?g)([?#]|$)/i.test(s.trim()) ? s.trim() : null;
|
|
15000
|
-
};
|
|
15001
|
-
if (!this._urlIconCache) this._urlIconCache = new Map();
|
|
15002
|
-
const urlIcon = u => {
|
|
15003
|
-
if (!this._urlIconCache.has(u)) {
|
|
15004
|
-
// the placeholder is a transparent 1x1 PNG, NOT the raw symbol
|
|
15005
|
-
// URL: deck.gl would immediately try to load that itself, and an
|
|
15006
|
-
// SVG without natural dimensions (a viewBox only, the bus
|
|
15007
|
-
// pictogram of the City-moving page) makes Chrome's
|
|
15008
|
-
// createImageBitmap throw — a console error and a part that
|
|
15009
|
-
// never renders. The real raster replaces it below
|
|
15010
|
-
const entry = { url: TRANSPARENT_1PX_PNG, width: 256, height: 256, anchorX: 128, anchorY: 128, mask: false, id: 'seq-sym-pending:' + u };
|
|
15011
|
-
this._urlIconCache.set(u, entry);
|
|
15012
|
-
// fetch the symbol ONCE and rasterize it into the descriptor's
|
|
15013
|
-
// own 256px cell here: deck then always sees exactly the declared
|
|
15014
|
-
// dimensions, and dimension-less SVGs work like any other. The
|
|
15015
|
-
// redraw lets the next getIcon see the patched descriptor
|
|
15016
|
-
fetch(u).then(r => r.ok ? r.text() : null).then(text => new Promise((resolve, reject) => {
|
|
15017
|
-
if (typeof text !== 'string' || !/<svg[\s>]/i.test(text)) { reject(new Error('not svg')); return; }
|
|
15018
|
-
if (!/\swidth\s*=/i.test(text) || !/\sheight\s*=/i.test(text)) {
|
|
15019
|
-
const vb = text.match(/viewBox\s*=\s*["']([^"']*)["']/i);
|
|
15020
|
-
const p = vb && vb[1].trim().split(/[\s,]+/).map(Number);
|
|
15021
|
-
if (p && p.length === 4 && isFinite(p[2]) && p[2] > 0 && isFinite(p[3]) && p[3] > 0) {
|
|
15022
|
-
text = text.replace(/<svg/i, '<svg width="' + p[2] + '" height="' + p[3] + '"');
|
|
15023
|
-
}
|
|
15024
|
-
}
|
|
15025
|
-
const img = new Image();
|
|
15026
|
-
img.onload = () => resolve(img);
|
|
15027
|
-
img.onerror = () => reject(new Error('svg decode failed'));
|
|
15028
|
-
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(text);
|
|
15029
|
-
})).then(img => {
|
|
15030
|
-
const size = entry.width;
|
|
15031
|
-
const canvas = document.createElement('canvas');
|
|
15032
|
-
canvas.width = size; canvas.height = size;
|
|
15033
|
-
const ctx = canvas.getContext('2d');
|
|
15034
|
-
const s = Math.min(size / (img.naturalWidth || size), size / (img.naturalHeight || size));
|
|
15035
|
-
const dw = (img.naturalWidth || size) * s, dh = (img.naturalHeight || size) * s;
|
|
15036
|
-
ctx.drawImage(img, (size - dw) / 2, (size - dh) / 2, dw, dh);
|
|
15037
|
-
entry.url = canvas.toDataURL('image/png');
|
|
15038
|
-
entry.id = 'seq-sym-raster:' + u;
|
|
15039
|
-
if (this._triggerRedraw) this._triggerRedraw();
|
|
15040
|
-
}).catch(() => {
|
|
15041
|
-
// not an SVG (a PNG/JPEG symbol): deck's own URL loading takes
|
|
15042
|
-
// it from here — and a pre-sized SVG that failed to rasterize
|
|
15043
|
-
// gets that same second chance
|
|
15044
|
-
entry.url = u;
|
|
15045
|
-
entry.id = 'seq-sym-natural:' + u;
|
|
15046
|
-
if (this._triggerRedraw) this._triggerRedraw();
|
|
15047
|
-
});
|
|
15048
|
-
}
|
|
15049
|
-
return this._urlIconCache.get(u);
|
|
15050
|
-
};
|
|
15911
|
+
const urlSymbolOf = i => this._urlSymbolOf(symbols && (symbols[i] || symbols[0]));
|
|
15912
|
+
const urlIcon = u => this._urlSymbolIcon(u);
|
|
15051
15913
|
const shapeOf = i => { const sh = symbols && (symbols[i] || symbols[0]); return normalizeSymbolShape(sh); };
|
|
15052
15914
|
const colorOf = i => this.classRgb(i);
|
|
15053
15915
|
const allParts = [], allGlows = [];
|
|
@@ -15077,7 +15939,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15077
15939
|
const sizeFactor = itemSizeFactor(d);
|
|
15078
15940
|
const partRadius = multi
|
|
15079
15941
|
? v => Math.max(VALUE_RADIUS_MIN, normalR * Math.pow(Math.abs(v) / seqMax, 1 / sizePow) * sizeFactor)
|
|
15080
|
-
: v => valueRadius(v, liveZoom, st, this.mapOptions, this.flags, seqMax);
|
|
15942
|
+
: v => valueRadius(v, liveZoom, st, this.mapOptions, this.flags, seqMax, this._sizeBound());
|
|
15081
15943
|
let layout = rings ? ringsLayout(counts, partRadius)
|
|
15082
15944
|
: sequenceLayout(counts, this.flags, partRadius, { maxRadius: normalR, maxValue: seqMax, nParts: nCats, rangeScale: styleNum(st.rangescale) || 0 });
|
|
15083
15945
|
if (!layout.length) continue;
|
|
@@ -15208,7 +16070,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15208
16070
|
getIcon: d => this._getGlowIcon(colorOf(d.part.i)),
|
|
15209
16071
|
getSize: d => d.part.r * 11,
|
|
15210
16072
|
getPixelOffset: offsetOf,
|
|
15211
|
-
getColor: d => [255, 255, 255, alphaOf(d)],
|
|
16073
|
+
getColor: d => [255, 255, 255, Math.round(alphaOf(d) * glowOpacityOf(st))],
|
|
15212
16074
|
sizeUnits: 'pixels', billboard: true,
|
|
15213
16075
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
15214
16076
|
}));
|
|
@@ -15563,7 +16425,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15563
16425
|
if (!layout) continue;
|
|
15564
16426
|
const sum = values.reduce((a, v) => a + v, 0);
|
|
15565
16427
|
if (star) for (const v of values) if (v > starMaxPart) starMaxPart = v;
|
|
15566
|
-
const size = this.binding.size ?
|
|
16428
|
+
const size = this.binding.size ? scanValue(raw[this.binding.size]) : undefined;
|
|
15567
16429
|
const r = nSizeOf(sum, size) * unit;
|
|
15568
16430
|
if (!(r > 0) || !isFinite(r)) continue;
|
|
15569
16431
|
// flat's draw-order key (maptheme.js 16812-16860): the size value,
|
|
@@ -16011,7 +16873,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16011
16873
|
return { values: parts, size };
|
|
16012
16874
|
}
|
|
16013
16875
|
const raw = f.properties || {};
|
|
16014
|
-
return { values: fields.map(k => flatNumber(raw[k], this.flags)), size: sizeField ?
|
|
16876
|
+
return { values: fields.map(k => flatNumber(raw[k], this.flags)), size: sizeField ? scanValue(raw[this.binding.size]) : undefined };
|
|
16015
16877
|
};
|
|
16016
16878
|
const all = agg ? this._pieAggregatedItems(zoom, [-180, -90, 180, 90]) : (this._activeFeatures || this.features);
|
|
16017
16879
|
// flat's per-part mean, median (getMeanMedianQuantile) and standard
|
|
@@ -16631,7 +17493,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16631
17493
|
// flat's chart position for this build (flatChartAlignOffset): a
|
|
16632
17494
|
// function item → pixel offset, the same for every part of one chart
|
|
16633
17495
|
// (symbol, glow, shadow, box, title, value text). unit is flat's
|
|
16634
|
-
// normalX(1) on screen, the object zoom × style.scale.
|
|
17496
|
+
// normalX(1) on screen, the object zoom × style.scale. flat keeps a margin of 5 units between the
|
|
17497
|
+
// position and the chart's facing edge for left/right/top/bottom; the gl-only style.alignmargin
|
|
17498
|
+
// changes it (0: the edge touches the position, so a left and a right chart touch each other)
|
|
16635
17499
|
_chartAlignFn(liveZoom, radiusOf, textOnly = false) {
|
|
16636
17500
|
const st = this.style;
|
|
16637
17501
|
const none = () => [0, 0];
|
|
@@ -16640,12 +17504,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16640
17504
|
const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(st.scale) || 1);
|
|
16641
17505
|
const symbol = flatChartBranch(this.flags) === 'symbol';
|
|
16642
17506
|
const normalR = NORMAL_RADIUS_PX * unit; // flat's normalX(chart size / 2)
|
|
17507
|
+
const margin = Number.isFinite(styleNum(st.alignmargin)) ? styleNum(st.alignmargin) : 5;
|
|
16643
17508
|
const cache = new Map();
|
|
16644
17509
|
const fn = d => {
|
|
16645
17510
|
let v = cache.get(d);
|
|
16646
17511
|
if (v) return v;
|
|
16647
17512
|
const r = radiusOf(d);
|
|
16648
|
-
v = flatChartAlignOffset(st, { unit, symbolScale: symbol ? r / (2 * normalR) : 1, half: symbol ? normalR : r, orig: r + (textOnly ? 0 :
|
|
17513
|
+
v = flatChartAlignOffset(st, { unit, symbolScale: symbol ? r / (2 * normalR) : 1, half: symbol ? normalR : r, orig: r + (textOnly ? 0 : margin * unit) });
|
|
16649
17514
|
cache.set(d, v);
|
|
16650
17515
|
return v;
|
|
16651
17516
|
};
|
|
@@ -16733,7 +17598,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16733
17598
|
let lo = Infinity, hi = -Infinity;
|
|
16734
17599
|
const field = this.binding.value;
|
|
16735
17600
|
for (const f of this.features || []) {
|
|
16736
|
-
const v = field === '$item$' ? 1 :
|
|
17601
|
+
const v = field === '$item$' ? 1 : scanValue(f.properties && f.properties[field]);
|
|
16737
17602
|
if (isNaN(v)) continue;
|
|
16738
17603
|
if (v < lo) lo = v;
|
|
16739
17604
|
if (v > hi) hi = v;
|
|
@@ -16824,7 +17689,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16824
17689
|
const textOnly = this.flags.has('TEXTONLY');
|
|
16825
17690
|
const showValues = this.flags.has('VALUES') && !valuesHiddenByScale(this.style, zoom);
|
|
16826
17691
|
if (textOnly && !showValues) return [];
|
|
16827
|
-
const radiusOf = d => valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue);
|
|
17692
|
+
const radiusOf = d => valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue, this._sizeBound());
|
|
16828
17693
|
const alignOf = this._chartAlignFn(liveZoom, radiusOf, textOnly);
|
|
16829
17694
|
const valueScale = styleNum(this.style.valuescale) || 1;
|
|
16830
17695
|
const textOverride = this.style.valuecolor || this.style.textcolor;
|
|
@@ -17385,6 +18250,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17385
18250
|
// several themes of a page
|
|
17386
18251
|
szId: themeIdOf(rt),
|
|
17387
18252
|
szName: rt.name,
|
|
18253
|
+
// flat's szSelectionField: the field a click on an item selects by (the id binding, else the field the
|
|
18254
|
+
// item is positioned by)
|
|
18255
|
+
szSelectionField: selectionFieldOf(rt),
|
|
17388
18256
|
// the filter in force: a runtime one (changeThemeStyle "filter")
|
|
17389
18257
|
// replaces the definition's
|
|
17390
18258
|
szFilter: rt._runtimeFilterExpr || rt._filterExpr || '',
|
|
@@ -17421,6 +18289,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17421
18289
|
get itemA() { return onMap().itemA; }
|
|
17422
18290
|
};
|
|
17423
18291
|
}
|
|
18292
|
+
// the field whose value identifies an item on a click (flat's szSelectionField): binding.id, else the position field
|
|
18293
|
+
// (which a join to a geometry layer turns into binding.lookup)
|
|
18294
|
+
function selectionFieldOf(rt) {
|
|
18295
|
+
const b = rt.binding || {};
|
|
18296
|
+
const f = b.id || b.position || b.lookup;
|
|
18297
|
+
return typeof f === 'string' && f && f !== 'geometry' && !/^geometry$/i.test(f) ? f : null;
|
|
18298
|
+
}
|
|
17424
18299
|
const _warnedOnce = new Set();
|
|
17425
18300
|
function warnOnce(key, msg) { if (!_warnedOnce.has(key)) { _warnedOnce.add(key); console.info(msg); } }
|
|
17426
18301
|
// the theme's records as flat's dbFields/dbRecords (cached per data)
|
|
@@ -17852,6 +18727,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17852
18727
|
function compatRemoveTheme(...a) {
|
|
17853
18728
|
return _mapHandle.remove(a.length >= 2 ? a[1] : a[0]);
|
|
17854
18729
|
}
|
|
18730
|
+
// flat's ixmaps.toggleTheme(szMap, szTheme) (htmlgui.js): show / hide a theme by its id, e.g. a page's on / off buttons
|
|
18731
|
+
function compatToggleTheme(...a) {
|
|
18732
|
+
new themeApi(null, a.length >= 2 ? a[1] : a[0]).toggle();
|
|
18733
|
+
}
|
|
17855
18734
|
function compatSetBasemapOpacity(...a) {
|
|
17856
18735
|
if (a.length >= 3) {
|
|
17857
18736
|
const api = (a[0] && _mapApis[a[0]]) || _lastMapApi;
|
|
@@ -17887,6 +18766,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17887
18766
|
htmlgui_getAttributionString: () => (_lastMapApi && _lastMapApi.getAttribution ? _lastMapApi.getAttribution() : ''),
|
|
17888
18767
|
changeThemeStyle: compatChangeThemeStyle,
|
|
17889
18768
|
removeTheme: compatRemoveTheme,
|
|
18769
|
+
toggleTheme: compatToggleTheme,
|
|
17890
18770
|
// flat's ixmaps.setLocalString(orig, local) (htmlgui.js 3814 →
|
|
17891
18771
|
// Api.setLocalString → Dictionary.replace): a page-wide translation,
|
|
17892
18772
|
// e.g. the migration page's ISO code → country name; usable before
|
|
@@ -17935,7 +18815,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17935
18815
|
flatToMapLibreZoom, mapLibreToFlatZoom, flatViewToMapLibreZoom, mapLibreToFlatViewZoom, flatSvgProjectionOf, flatSvgProjectionByOptions, normalSizeScaleOf, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
|
|
17936
18816
|
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,
|
|
17937
18817
|
applyWhereFilter, joinChartPositions, joinChoroplethFeatures, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatBubbleLook, bubbleIconLook, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
17938
|
-
drawSymbolPath, normalizeSymbolShape,
|
|
18818
|
+
drawSymbolPath, normalizeSymbolShape, bubbleStrokeRgb, heatmapColorRange, heatmapWeight,
|
|
18819
|
+
mlHeatmapPoints, mlHeatmapWeightMax, mlHeatmapRadius, mlHeatmapPaint, syncNativeHeatmaps, mlHeatLayerId,
|
|
18820
|
+
kdeBandwidthKm, kdeMaxCells, kdeLevels, kdePoints, kdeGrid, kdeBands, kdeBandColors, kdeWorkerSource, kdeCompute, kdeReset, setDeckLayerClassesForTests, ensureD3Contour, colorRampRgb,
|
|
17939
18821
|
flatFormatValue, themeStyleArgs, flatChangedStyleValue, explicitValueDecimals, legendDecimals,
|
|
17940
18822
|
dataCacheKey, dataCacheDisabled, cachedLayerData, featuresBounds, wantsZoomToExtent, legendIsOn, applySphereBackdrop,
|
|
17941
18823
|
};
|