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.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/ixmaps-gl.js +11 -165
  3. 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.18/ixmaps-gl.js"></script>
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.18/ixmaps-gl.js"></script>
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.18/ixmaps-gl.js"></script>
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.18';
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; a label under later-drawn charts is faded or dropped by
11559
- // _occludeChartTexts, as flat's later chart groups cover it. Entries whose
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, order: orderOf.get(d) });
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, order: orderOf.get(d) });
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 (shownPointLabels.length) layers.push(new TextLayer({ id: `ix-points-values-${this.name}`, data: shownPointLabels, getPixelOffset: d => d.pixelOffset, ...textLayerCommonProps,
11662
- fontWeight: shownPointLabels[0].bold === false ? 'normal' : 'bold' }));
11663
- if (shownGroupLabels.length) layers.push(new TextLayer({ id: `ix-cluster-values-${this.name}`, data: shownGroupLabels, getPixelOffset: d => d.pixelOffset, ...textLayerCommonProps,
11664
- fontWeight: shownGroupLabels[0].bold === false ? 'normal' : 'bold' }));
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, chartTextTransmittance, glowProfileAlpha, insideSymbolShape, sequenceLayout, ringsLayout, multiQuadOffsets, pixelOffsetLngLat,
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ixmaps-gl",
3
- "version": "0.2.18",
3
+ "version": "0.2.19",
4
4
  "description": "MapLibre GL + deck.gl reimplementation of the ixmaps-flat declarative mapping engine",
5
5
  "main": "ixmaps-gl.js",
6
6
  "browser": "ixmaps-gl.js",