ixmaps-gl 0.2.18 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/ixmaps-gl.js +11 -165
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -243,7 +243,7 @@ of your own project files gl can render.
|
|
|
243
243
|
Load the engine from jsDelivr, pinned to a release tag (or `@main` for the latest commit):
|
|
244
244
|
|
|
245
245
|
```html
|
|
246
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
246
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.19/ixmaps-gl.js"></script>
|
|
247
247
|
```
|
|
248
248
|
|
|
249
249
|
or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmaps-gl)
|
|
@@ -251,13 +251,13 @@ or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmap
|
|
|
251
251
|
browser script, which defines the global `ixmaps`):
|
|
252
252
|
|
|
253
253
|
```html
|
|
254
|
-
<script src="https://unpkg.com/ixmaps-gl@0.2.
|
|
254
|
+
<script src="https://unpkg.com/ixmaps-gl@0.2.19/ixmaps-gl.js"></script>
|
|
255
255
|
```
|
|
256
256
|
|
|
257
257
|
or use a local copy (`<script src="ixmaps-gl.js"></script>`). A minimal page:
|
|
258
258
|
|
|
259
259
|
```html
|
|
260
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
260
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.19/ixmaps-gl.js"></script>
|
|
261
261
|
<div id="map_div" style="position:absolute;inset:0;"></div>
|
|
262
262
|
|
|
263
263
|
<script>
|
package/ixmaps-gl.js
CHANGED
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
// ---------------------------------------------------------------
|
|
83
83
|
// this engine's release (= package.json "version", checked by
|
|
84
84
|
// test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
|
|
85
|
-
const IXMAPS_GL_VERSION = '0.2.
|
|
85
|
+
const IXMAPS_GL_VERSION = '0.2.19';
|
|
86
86
|
|
|
87
87
|
const LIB_URLS = {
|
|
88
88
|
// Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
|
|
@@ -7045,83 +7045,6 @@
|
|
|
7045
7045
|
return { offset, index, extent };
|
|
7046
7046
|
}
|
|
7047
7047
|
|
|
7048
|
-
// Chart texts covered by charts drawn after them. Flat draws each chart
|
|
7049
|
-
// as one SVG group (glow, symbol, value text, in that order) in the
|
|
7050
|
-
// sorted draw order, so a later chart's symbol and glow paint over the
|
|
7051
|
-
// texts of every chart beneath it; here the texts are separate TextLayers
|
|
7052
|
-
// above every icon. This composites that occlusion on the CPU: for each
|
|
7053
|
-
// text, the transmittance Π(1 − α) of every LATER chart's symbol parts
|
|
7054
|
-
// and glow at a few sample points of the text box, averaged — the
|
|
7055
|
-
// fraction of the text still showing through, its alpha. Partly covered
|
|
7056
|
-
// texts fade instead of being clipped (no per-pixel mask in deck.gl).
|
|
7057
|
-
// charts: in draw order, { x, y, parts: [{ dx, dy, r, shape, alpha }],
|
|
7058
|
-
// glowR, glowAlpha } in screen pixels; a part's alpha its opacity,
|
|
7059
|
-
// glowR 0 for none (glowAlpha scales glowProfileAlpha)
|
|
7060
|
-
// texts: { order (index into charts), x, y, w, h }
|
|
7061
|
-
// Returns each text's transmittance, 0..1.
|
|
7062
|
-
const OCCLUSION_CELL_PX = 64;
|
|
7063
|
-
// below this a text is as good as hidden: left out of its TextLayer
|
|
7064
|
-
const CHART_TEXT_MIN_TRANSMITTANCE = 0.08;
|
|
7065
|
-
function chartTextTransmittance(charts, texts) {
|
|
7066
|
-
const grid = new Map();
|
|
7067
|
-
charts.forEach((c, k) => {
|
|
7068
|
-
let ext = c.glowR || 0;
|
|
7069
|
-
for (const p of c.parts) ext = Math.max(ext, Math.hypot(p.dx, p.dy) + p.r * Math.SQRT2);
|
|
7070
|
-
const x0 = Math.floor((c.x - ext) / OCCLUSION_CELL_PX), x1 = Math.floor((c.x + ext) / OCCLUSION_CELL_PX);
|
|
7071
|
-
const y0 = Math.floor((c.y - ext) / OCCLUSION_CELL_PX), y1 = Math.floor((c.y + ext) / OCCLUSION_CELL_PX);
|
|
7072
|
-
for (let gx = x0; gx <= x1; gx++) {
|
|
7073
|
-
for (let gy = y0; gy <= y1; gy++) {
|
|
7074
|
-
const key = gx + ',' + gy;
|
|
7075
|
-
let cell = grid.get(key);
|
|
7076
|
-
if (!cell) grid.set(key, cell = []);
|
|
7077
|
-
cell.push(k);
|
|
7078
|
-
}
|
|
7079
|
-
}
|
|
7080
|
-
});
|
|
7081
|
-
const alphaAt = (c, x, y) => {
|
|
7082
|
-
let keep = 1;
|
|
7083
|
-
if (c.glowR) {
|
|
7084
|
-
const d = Math.hypot(x - c.x, y - c.y) / c.glowR;
|
|
7085
|
-
if (d < 1) keep *= 1 - c.glowAlpha * glowProfileAlpha(d);
|
|
7086
|
-
}
|
|
7087
|
-
for (const p of c.parts) {
|
|
7088
|
-
if (insideSymbolShape(p.shape, x - c.x - p.dx, y - c.y - p.dy, p.r)) keep *= 1 - p.alpha;
|
|
7089
|
-
}
|
|
7090
|
-
return keep;
|
|
7091
|
-
};
|
|
7092
|
-
// the box center and four points inside its corners
|
|
7093
|
-
const SAMPLES = [[0, 0], [-0.35, -0.3], [0.35, -0.3], [-0.35, 0.3], [0.35, 0.3]];
|
|
7094
|
-
return texts.map(t => {
|
|
7095
|
-
let sum = 0;
|
|
7096
|
-
for (const [sx, sy] of SAMPLES) {
|
|
7097
|
-
const x = t.x + sx * t.w, y = t.y + sy * t.h;
|
|
7098
|
-
const cell = grid.get(Math.floor(x / OCCLUSION_CELL_PX) + ',' + Math.floor(y / OCCLUSION_CELL_PX));
|
|
7099
|
-
let keep = 1;
|
|
7100
|
-
// cells list charts in draw order: only the later ones cover
|
|
7101
|
-
if (cell) for (let i = cell.length - 1; i >= 0 && cell[i] > t.order && keep > 0; i--) keep *= alphaAt(charts[cell[i]], x, y);
|
|
7102
|
-
sum += keep;
|
|
7103
|
-
}
|
|
7104
|
-
return sum / SAMPLES.length;
|
|
7105
|
-
});
|
|
7106
|
-
}
|
|
7107
|
-
// _getGlowIcon's radial gradient: 0.28 at the center, 0.09 at half the
|
|
7108
|
-
// radius, 0 at the edge (d = distance / radius)
|
|
7109
|
-
function glowProfileAlpha(d) {
|
|
7110
|
-
return d < 0.5 ? 0.28 - 0.19 * d / 0.5 : d < 1 ? 0.09 * (1 - (d - 0.5) / 0.5) : 0;
|
|
7111
|
-
}
|
|
7112
|
-
// inside drawSymbolPath's shape of radius r, (dx, dy) from its center;
|
|
7113
|
-
// the shapes without their own test as the circle
|
|
7114
|
-
function insideSymbolShape(shape, dx, dy, r) {
|
|
7115
|
-
switch (shape) {
|
|
7116
|
-
case 'squarefull': return Math.abs(dx) <= r && Math.abs(dy) <= r;
|
|
7117
|
-
case 'square': return Math.abs(dx) <= r * Math.SQRT1_2 && Math.abs(dy) <= r * Math.SQRT1_2;
|
|
7118
|
-
case 'roundrect': return Math.abs(dx) <= r * 0.8 && Math.abs(dy) <= r * 0.8;
|
|
7119
|
-
case 'diamond': case 'carot': return Math.abs(dx) + Math.abs(dy) <= r;
|
|
7120
|
-
case 'empty': return false;
|
|
7121
|
-
default: return dx * dx + dy * dy <= r * r;
|
|
7122
|
-
}
|
|
7123
|
-
}
|
|
7124
|
-
|
|
7125
7048
|
// A pixel offset [dx, dy] (y down) from lngLat at MapLibre zoom `zoom`,
|
|
7126
7049
|
// as lng/lat (Web Mercator, 512 px world tiles) — for shapes too large
|
|
7127
7050
|
// for an icon, drawn in map coordinates (rebuilt with every refresh).
|
|
@@ -11555,8 +11478,8 @@
|
|
|
11555
11478
|
// VALUES: bold value label centered on each bubble (see
|
|
11556
11479
|
// flatValueText/valuesFontSizePx above). Labels are their own
|
|
11557
11480
|
// layers, added last in the array so they draw on top of every icon
|
|
11558
|
-
// layer
|
|
11559
|
-
//
|
|
11481
|
+
// layer (a small bubble's label can still show over a bigger
|
|
11482
|
+
// neighboring bubble's icon — a known, accepted gap). Entries whose
|
|
11560
11483
|
// computed font size would be sub-pixel are dropped rather than
|
|
11561
11484
|
// rendered at getSize: 0, matching the real engine's own "too
|
|
11562
11485
|
// small to bother" gate.
|
|
@@ -11565,11 +11488,7 @@
|
|
|
11565
11488
|
const textOpts = this._valueTextOpts();
|
|
11566
11489
|
const pointValueOf = valueTextOf || this._chartValueTextFn();
|
|
11567
11490
|
|
|
11568
|
-
// draw order of each chart (see chartTextTransmittance below); an
|
|
11569
|
-
// item maxcharts left out draws no chart, so no text either
|
|
11570
|
-
const orderOf = new Map(combined.map((d, k) => [d, k]));
|
|
11571
11491
|
const pointLabels = individual.reduce((out, d) => {
|
|
11572
|
-
if (!orderOf.has(d)) return out;
|
|
11573
11492
|
const radius = vRadius(d.properties.value);
|
|
11574
11493
|
// flat's VALUES label prints the value (nValuesA), not the size;
|
|
11575
11494
|
// its font by flat's chart branch (_chartValueTextFn)
|
|
@@ -11577,7 +11496,7 @@
|
|
|
11577
11496
|
if (fontSize > VALUES_MIN_FONT_PX) {
|
|
11578
11497
|
const o = alignOf(d);
|
|
11579
11498
|
out.push({ geometry: d.geometry, text, fontSize, bold, color: resolveTextColor(this.style, contrastTextColor((this.categoryColorsRgb || [])[d.properties.cat])),
|
|
11580
|
-
pixelOffset: baseline ? [o[0], o[1] + baseline - 0.35 * fontSize] : o
|
|
11499
|
+
pixelOffset: baseline ? [o[0], o[1] + baseline - 0.35 * fontSize] : o });
|
|
11581
11500
|
}
|
|
11582
11501
|
return out;
|
|
11583
11502
|
}, []);
|
|
@@ -11593,7 +11512,6 @@
|
|
|
11593
11512
|
// would misrepresent every category's own count as one summed
|
|
11594
11513
|
// number nobody's individual bubble actually shows.
|
|
11595
11514
|
const groupLabels = groups.reduce((out, d) => {
|
|
11596
|
-
if (!orderOf.has(d)) return out;
|
|
11597
11515
|
const counts = d.properties.counts;
|
|
11598
11516
|
const outerRadiusPx = vRadius(d.properties.total);
|
|
11599
11517
|
const iconSizePx = outerRadiusPx * 2; // matches the cluster IconLayer's own getSize (*2) below
|
|
@@ -11608,7 +11526,7 @@
|
|
|
11608
11526
|
const o = alignOf(d);
|
|
11609
11527
|
out.push({ geometry: d.geometry, text: v.text, fontSize: v.fontSize, bold: v.bold,
|
|
11610
11528
|
color: resolveTextColor(this.style, contrastTextColor((this.categoryColorsRgb || [])[d.properties.cat])),
|
|
11611
|
-
pixelOffset: v.baseline ? [o[0], o[1] + v.baseline - 0.35 * v.fontSize] : o
|
|
11529
|
+
pixelOffset: v.baseline ? [o[0], o[1] + v.baseline - 0.35 * v.fontSize] : o });
|
|
11612
11530
|
}
|
|
11613
11531
|
return out;
|
|
11614
11532
|
}
|
|
@@ -11623,24 +11541,13 @@
|
|
|
11623
11541
|
out.push({
|
|
11624
11542
|
geometry: d.geometry, text, fontSize,
|
|
11625
11543
|
color: resolveTextColor(this.style, contrastTextColor((this.categoryColorsRgb || [])[p.i])),
|
|
11626
|
-
pixelOffset: [offsets[i].x * fitScale * pxPerCanvasUnit + alignOf(d)[0], offsets[i].y * fitScale * pxPerCanvasUnit + alignOf(d)[1]]
|
|
11627
|
-
order: orderOf.get(d)
|
|
11544
|
+
pixelOffset: [offsets[i].x * fitScale * pxPerCanvasUnit + alignOf(d)[0], offsets[i].y * fitScale * pxPerCanvasUnit + alignOf(d)[1]]
|
|
11628
11545
|
});
|
|
11629
11546
|
}
|
|
11630
11547
|
});
|
|
11631
11548
|
return out;
|
|
11632
11549
|
}, []);
|
|
11633
11550
|
|
|
11634
|
-
// a text under later charts fades by what still shows through them
|
|
11635
|
-
// (chartTextTransmittance), as flat's per-chart SVG groups paint
|
|
11636
|
-
// each later symbol and glow over it; nearly hidden ones are dropped
|
|
11637
|
-
const visible = this._occludeChartTexts(combined, pointLabels.concat(groupLabels), {
|
|
11638
|
-
liveZoom, globeCenter, alignOf, radiusOf, sizeValueOf, singleSymbol, shapeOf, fillOpacity, vRadius,
|
|
11639
|
-
singleBorderWidthPx
|
|
11640
|
-
});
|
|
11641
|
-
const keepVisible = labels => labels.filter(l => visible.has(l));
|
|
11642
|
-
const shownPointLabels = keepVisible(pointLabels), shownGroupLabels = keepVisible(groupLabels);
|
|
11643
|
-
|
|
11644
11551
|
const textLayerCommonProps = {
|
|
11645
11552
|
pickable: false,
|
|
11646
11553
|
getPosition: d => d.geometry.coordinates,
|
|
@@ -11658,10 +11565,10 @@
|
|
|
11658
11565
|
getAlignmentBaseline: 'center'
|
|
11659
11566
|
};
|
|
11660
11567
|
// flat's SYMBOL branch writes its values in normal weight
|
|
11661
|
-
if (
|
|
11662
|
-
fontWeight:
|
|
11663
|
-
if (
|
|
11664
|
-
fontWeight:
|
|
11568
|
+
if (pointLabels.length) layers.push(new TextLayer({ id: `ix-points-values-${this.name}`, data: pointLabels, getPixelOffset: d => d.pixelOffset, ...textLayerCommonProps,
|
|
11569
|
+
fontWeight: pointLabels[0].bold === false ? 'normal' : 'bold' }));
|
|
11570
|
+
if (groupLabels.length) layers.push(new TextLayer({ id: `ix-cluster-values-${this.name}`, data: groupLabels, getPixelOffset: d => d.pixelOffset, ...textLayerCommonProps,
|
|
11571
|
+
fontWeight: groupLabels[0].bold === false ? 'normal' : 'bold' }));
|
|
11665
11572
|
}
|
|
11666
11573
|
|
|
11667
11574
|
// LABEL: flat's chart label (maptheme.js 24533-24590) — a small text
|
|
@@ -12022,67 +11929,6 @@
|
|
|
12022
11929
|
// the item color (stroke font/7, opacity 0.5).
|
|
12023
11930
|
// Both moved by the chart position (flatChartAlignOffset). The text is
|
|
12024
11931
|
// the hover target of TEXTONLY, the box of LABEL.
|
|
12025
|
-
// The charts of _buildChartLayers' bubbles layer (and its GLOW / AURA)
|
|
12026
|
-
// as chartTextTransmittance's occluders, in draw order and screen
|
|
12027
|
-
// pixels, and each text's resulting alpha: the text's color gets it,
|
|
12028
|
-
// the returned Set holds the texts still visible. Screen pixels:
|
|
12029
|
-
// Web Mercator world pixels at the live zoom; under globe a local
|
|
12030
|
-
// equal-area frame around the view center at the scale MapLibre's
|
|
12031
|
-
// globe matches Mercator's there (the limb's foreshortening ignored).
|
|
12032
|
-
_occludeChartTexts(combined, texts, o) {
|
|
12033
|
-
const visible = new Set();
|
|
12034
|
-
if (!texts.length) return visible;
|
|
12035
|
-
const toRad = Math.PI / 180;
|
|
12036
|
-
let screenOf;
|
|
12037
|
-
if (o.globeCenter) {
|
|
12038
|
-
const s = 512 * Math.pow(2, o.liveZoom) / 360 / Math.cos(o.globeCenter.lat * toRad);
|
|
12039
|
-
screenOf = c => [(((c[0] - o.globeCenter.lng) % 360 + 540) % 360 - 180) * Math.cos(c[1] * toRad) * s, -c[1] * s];
|
|
12040
|
-
} else {
|
|
12041
|
-
screenOf = c => { const p = lngLatToWorldPixel(c[0], c[1], o.liveZoom); return [p.x, p.y]; };
|
|
12042
|
-
}
|
|
12043
|
-
const glowOn = this.flags.has('GLOW') && !glowHiddenByScale(this.style, o.liveZoom);
|
|
12044
|
-
const auraOn = !glowOn && this.flags.has('AURA') && !this.flags.has('ZOOM');
|
|
12045
|
-
const symbols = Array.isArray(this.style.symbols) ? this.style.symbols : null;
|
|
12046
|
-
const charts = combined.map(d => {
|
|
12047
|
-
const [x, y] = screenOf(d.geometry.coordinates);
|
|
12048
|
-
const a = o.alignOf(d), r = o.radiusOf(d), iconAlpha = this._iconAlpha(d) / 255;
|
|
12049
|
-
let parts;
|
|
12050
|
-
if (o.singleSymbol(d)) {
|
|
12051
|
-
// the icon's shape is inset by half its border (_buildSingleIcon)
|
|
12052
|
-
const shape = o.shapeOf(d.properties);
|
|
12053
|
-
parts = [{ dx: 0, dy: 0, r: r * (1 - o.singleBorderWidthPx / BUBBLE_ICON_SIZE), shape, alpha: o.fillOpacity * iconAlpha }];
|
|
12054
|
-
} else {
|
|
12055
|
-
// _buildBubbleIcon's packed parts, its baked 0.9 alpha scaled by an
|
|
12056
|
-
// explicit fillopacity like the bubbles layer's getColor
|
|
12057
|
-
const counts = d.properties.counts, iso = this._groupIsolation(d);
|
|
12058
|
-
const { present, radii, offsets, fitScale } = (iso && isolatedBubblePackLayout(counts, BUBBLE_ICON_SIZE, iso)) || computeBubblePackLayout(counts, BUBBLE_ICON_SIZE);
|
|
12059
|
-
const px = 2 * r / BUBBLE_ICON_SIZE * fitScale;
|
|
12060
|
-
const alpha = 0.9 * (this.style.fillopacity != null ? o.fillOpacity : 1) * iconAlpha;
|
|
12061
|
-
parts = present.map((p, i) => ({ dx: offsets[i].x * px, dy: offsets[i].y * px, r: radii[i] * px,
|
|
12062
|
-
shape: symbols ? normalizeSymbolShape(symbols[Math.min(p.i, symbols.length - 1)]) : 'circle', alpha }));
|
|
12063
|
-
}
|
|
12064
|
-
if (auraOn) parts.push({ dx: 0, dy: 0, r: r * 1.3, shape: 'circle', alpha: 0.3 * iconAlpha });
|
|
12065
|
-
return {
|
|
12066
|
-
x: x + a[0], y: y + a[1], parts,
|
|
12067
|
-
glowR: glowOn ? o.vRadius(o.sizeValueOf(d)) * (d.properties.counts ? 9 : 11) / 2 : 0,
|
|
12068
|
-
glowAlpha: iconAlpha
|
|
12069
|
-
};
|
|
12070
|
-
});
|
|
12071
|
-
// the text box: Arial digits are ~0.56 em wide, ~0.7 em high
|
|
12072
|
-
const boxes = texts.map(t => {
|
|
12073
|
-
const [x, y] = screenOf(t.geometry.coordinates);
|
|
12074
|
-
return { order: t.order, x: x + t.pixelOffset[0], y: y + t.pixelOffset[1], w: 0.56 * t.fontSize * String(t.text).length, h: 0.7 * t.fontSize };
|
|
12075
|
-
});
|
|
12076
|
-
const shown = chartTextTransmittance(charts, boxes);
|
|
12077
|
-
texts.forEach((t, i) => {
|
|
12078
|
-
if (shown[i] < CHART_TEXT_MIN_TRANSMITTANCE) return;
|
|
12079
|
-
const c = t.color || [0, 0, 0];
|
|
12080
|
-
if (shown[i] < 1) t.color = [c[0], c[1], c[2], Math.round((c[3] != null ? c[3] : 255) * shown[i])];
|
|
12081
|
-
visible.add(t);
|
|
12082
|
-
});
|
|
12083
|
-
return visible;
|
|
12084
|
-
}
|
|
12085
|
-
|
|
12086
11932
|
_buildLabelChartLayers(combined, zoom, liveZoom, sizeValueOf) {
|
|
12087
11933
|
const textOnly = this.flags.has('TEXTONLY');
|
|
12088
11934
|
const showValues = this.flags.has('VALUES') && !valuesHiddenByScale(this.style, zoom);
|
|
@@ -13139,7 +12985,7 @@
|
|
|
13139
12985
|
// test-only: lets test/unit/*.test.mjs call pure internals directly (the
|
|
13140
12986
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
13141
12987
|
global.__ixmapsGlInternals = {
|
|
13142
|
-
IXMAPS_GL_VERSION, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout,
|
|
12988
|
+
IXMAPS_GL_VERSION, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout, ringsLayout, multiQuadOffsets, pixelOffsetLngLat,
|
|
13143
12989
|
normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
13144
12990
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
13145
12991
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|