ixmaps-gl 0.1.0 → 0.2.1

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 +13 -3
  2. package/ixmaps-gl.js +87 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -181,7 +181,9 @@ script. For brokers that query by view, like flat's bbox data providers, ixmaps-
181
181
  `ixmaps.getBoundingBox()`, `ixmaps.refreshTheme(id)` (loads the theme's data again, in place),
182
182
  `ixmaps.setTitle(html)`/`setTitleBox(text, color)`, `ixmaps.getThemeObj(id).fVisible` (false for a
183
183
  FEATURE theme out of scale) and calls a page's `ixmaps.htmlgui_onZoomAndPan(zoom)` after each zoom
184
- or pan. Scripts that reach further into flat's internals (`ixmaps.parentApi`, other `htmlgui_*`
184
+ or pan — and also during a zoom, at the pace of the layer refresh (about every 150 ms, zoom changes only),
185
+ so zoom-dependent page state (e.g. a basemap opacity ramp) changes in step with the scale gates
186
+ (`featurelower`, `chartupper`, …) instead of after the gesture has ended. Scripts that reach further into flat's internals (`ixmaps.parentApi`, other `htmlgui_*`
185
187
  hooks) aren't supported. `test/project-report.mjs` shows which themes
186
188
  of your own project files gl can render.
187
189
 
@@ -190,13 +192,21 @@ of your own project files gl can render.
190
192
  Load the engine from jsDelivr, pinned to a release tag (or `@main` for the latest commit):
191
193
 
192
194
  ```html
193
- <script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.1.0/ixmaps-gl.js"></script>
195
+ <script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.1/ixmaps-gl.js"></script>
196
+ ```
197
+
198
+ or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmaps-gl)
199
+ (`https://unpkg.com/ixmaps-gl` for the latest version; `npm install ixmaps-gl` installs the same
200
+ browser script, which defines the global `ixmaps`):
201
+
202
+ ```html
203
+ <script src="https://unpkg.com/ixmaps-gl@0.2.1/ixmaps-gl.js"></script>
194
204
  ```
195
205
 
196
206
  or use a local copy (`<script src="ixmaps-gl.js"></script>`). A minimal page:
197
207
 
198
208
  ```html
199
- <script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.1.0/ixmaps-gl.js"></script>
209
+ <script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.1/ixmaps-gl.js"></script>
200
210
  <div id="map_div" style="position:absolute;inset:0;"></div>
201
211
 
202
212
  <script>
package/ixmaps-gl.js CHANGED
@@ -78,14 +78,27 @@
78
78
  // Same CDN builds every example page already pinned; centralized here
79
79
  // so bumping a version is a one-file edit, not a 13-file one.
80
80
  // ---------------------------------------------------------------
81
+ // this engine's release (= package.json "version", checked by
82
+ // test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
83
+ const IXMAPS_GL_VERSION = '0.2.1';
84
+
81
85
  const LIB_URLS = {
82
86
  // Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
83
87
  // globe projection (map.setProjection({type:'globe'})) needs >=5.0.1
84
88
  // (5.0.0 shipped a style-spec regression, reverted in 5.0.1). Requires
85
89
  // MapLibre GL JS v4.5.1, v5, or v6 on the deck.gl side (see `deck`
86
90
  // below) — v5 satisfies that.
87
- maplibreCss: 'https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.css',
88
- maplibreJs: 'https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.js',
91
+ // 5.x -> 6.x (2026-09-29): MapLibre 6 is published only as an ES
92
+ // module (dist/maplibre-gl.mjs, named exports, no UMD bundle and no
93
+ // window.maplibregl) — loadMaplibre imports it and provides the global.
94
+ // Its worker (maplibre-gl-worker.mjs, resolved next to the module) is
95
+ // cross-origin from the CDN; MapLibre wraps it in a same-origin blob URL
96
+ // itself. Pinned to an exact version: unpkg answers a version range
97
+ // (@6) with a redirect, and a browser refuses a worker script that
98
+ // redirects ("Refused to cross-origin redirects of the top-level worker
99
+ // script" — the map never loaded on a file:// page).
100
+ maplibreCss: 'https://unpkg.com/maplibre-gl@6.11.2/dist/maplibre-gl.css',
101
+ maplibreJs: 'https://unpkg.com/maplibre-gl@6.11.2/dist/maplibre-gl.mjs',
89
102
  // Bumped 8.9.35 -> 9.4.0 (2026-09-21, globe-reprojection fix): v8's
90
103
  // interleaving (MapboxOverlay) always computed flat Web-Mercator
91
104
  // screen positions regardless of the map's actual projection —
@@ -117,6 +130,49 @@
117
130
  document.head.appendChild(el);
118
131
  });
119
132
  }
133
+ // MapLibre 6's ES module → the window.maplibregl the engine and pages use
134
+ // (a copy of its named exports: a module namespace is read-only)
135
+ function loadMaplibre(src) {
136
+ installFileWorkerShim();
137
+ return import(src).then(mod => {
138
+ global.maplibregl = Object.assign({}, mod.default || {}, mod);
139
+ }, err => { throw new Error(`[ixmaps-gl] failed to load ${src}: ${err && err.message}`); });
140
+ }
141
+ // MapLibre 6 starts its worker as a module worker; for a worker on the
142
+ // CDN it wraps it in a blob URL (`import "…maplibre-gl-worker.mjs"`).
143
+ // On a file:// page Chrome refuses every module worker from a blob URL
144
+ // ("Refused to cross-origin redirects of the top-level worker script" —
145
+ // the map never loaded), while a module worker from a data: URL works.
146
+ // Only there, and only for MapLibre's own wrapper, the Worker is started
147
+ // from the same import as a data: URL. Other workers are untouched.
148
+ function installFileWorkerShim() {
149
+ if (!global.location || global.location.protocol !== 'file:' || !global.Worker || global.Worker.__ixmapsGlFileShim) return;
150
+ const NativeBlob = global.Blob, NativeWorker = global.Worker;
151
+ const maplibreWorkerImport = new WeakMap(); // Blob → its import statement
152
+ const blobUrls = new Map(); // blob: URL → import statement
153
+ const isMaplibreImport = t => typeof t === 'string' && /^import "https?:[^"]*\/maplibre-gl-worker(-dev)?\.mjs"$/.test(t);
154
+ function Blob(parts, opts) {
155
+ const b = new NativeBlob(parts, opts);
156
+ if (Array.isArray(parts) && parts.length === 1 && isMaplibreImport(parts[0])) maplibreWorkerImport.set(b, parts[0]);
157
+ return b;
158
+ }
159
+ Blob.prototype = NativeBlob.prototype;
160
+ global.Blob = Blob;
161
+ const nativeCreate = global.URL.createObjectURL;
162
+ global.URL.createObjectURL = function (obj) {
163
+ const u = nativeCreate.call(this, obj);
164
+ if (maplibreWorkerImport.has(obj)) blobUrls.set(u, maplibreWorkerImport.get(obj));
165
+ return u;
166
+ };
167
+ function Worker(url, opts) {
168
+ const imp = blobUrls.get(String(url));
169
+ if (imp && opts && opts.type === 'module') return new NativeWorker('data:text/javascript,' + encodeURIComponent(imp), opts);
170
+ return new NativeWorker(url, opts);
171
+ }
172
+ Worker.prototype = NativeWorker.prototype;
173
+ Worker.__ixmapsGlFileShim = true;
174
+ global.Worker = Worker;
175
+ }
120
176
  function loadStylesheet(href) {
121
177
  return new Promise((resolve, reject) => {
122
178
  const el = document.createElement('link');
@@ -188,7 +244,7 @@
188
244
  }
189
245
  if (i === 2) return true;
190
246
  }
191
- console.warn(`[ixmaps-gl] this page loads maplibre-gl ${v || '(unknown version)'}; deck.gl needs >= ${MAPLIBRE_MIN_VERSION.join('.')} — loading ${LIB_URLS.maplibreJs.match(/maplibre-gl@([\d.]+)/)[1]}.x instead (it replaces window.maplibregl); remove the page's own maplibre-gl <script> tag`);
247
+ console.warn(`[ixmaps-gl] this page loads maplibre-gl ${v || '(unknown version)'}; deck.gl needs >= ${MAPLIBRE_MIN_VERSION.join('.')} — loading ${LIB_URLS.maplibreJs.match(/maplibre-gl@([\d.]+)/)[1]} instead (it replaces window.maplibregl); remove the page's own maplibre-gl <script> tag`);
192
248
  return false;
193
249
  }
194
250
  // data.js, unless the page has it already: .options({datajs: url}) loads
@@ -213,12 +269,16 @@
213
269
  function ensureLibrariesLoaded() {
214
270
  if (!_librariesPromise) {
215
271
  _librariesPromise = Promise.all([
216
- pageMaplibreUsable() ? Promise.resolve() : loadScript(LIB_URLS.maplibreJs),
272
+ pageMaplibreUsable() ? Promise.resolve() : loadMaplibre(LIB_URLS.maplibreJs),
217
273
  pageDeckUsable() ? Promise.resolve() : loadScript(LIB_URLS.deck),
218
274
  global.Mustache ? Promise.resolve() : loadScript(LIB_URLS.mustache),
219
275
  [...document.styleSheets].some(s => s.href === LIB_URLS.maplibreCss) ? Promise.resolve() : loadStylesheet(LIB_URLS.maplibreCss)
220
276
  ]).then(() => {
221
277
  ({ IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer } = global.deck);
278
+ // which engine and libraries this page really runs (a page may
279
+ // bring its own MapLibre/deck.gl — the loaded ones are named)
280
+ const ml = global.maplibregl && typeof global.maplibregl.getVersion === 'function' ? global.maplibregl.getVersion() : '?';
281
+ console.info(`[ixmaps-gl] ${IXMAPS_GL_VERSION} · MapLibre ${ml} · deck.gl ${global.deck.VERSION || '?'}`);
222
282
  // Matches the real engine's own ui/js/tools/tooltip_mustache.js,
223
283
  // which overrides Mustache.escape to identity: tooltip HTML (the
224
284
  // template itself, and this engine's own chart/data-table
@@ -2668,6 +2728,16 @@
2668
2728
  // broker re-queries its data from it. Read at call time: pages wrap
2669
2729
  // it after loading. flat passes its SVG zoom scale; this passes the
2670
2730
  // flat zoom level, the one value gl has.
2731
+ //
2732
+ // gl also calls it DURING a zoom (notifyZoomAndPanMidGesture, wired
2733
+ // into the throttled refresh below), zoom changes only: the scale
2734
+ // gates (featurelower/chartupper ...) switch on the live zoom while
2735
+ // the gesture runs, so a page that ties something zoom-dependent to
2736
+ // this hook (e.g. a basemap opacity ramp) must be told in the same
2737
+ // tick — from 'moveend' alone it arrived after the gate had already
2738
+ // flipped (FEATURE gone, basemap still at the old, faint opacity: a
2739
+ // blank background until the zoom ended).
2740
+ let notifyZoomAndPanMidGesture = () => {};
2671
2741
  {
2672
2742
  let last = null;
2673
2743
  map.on('moveend', () => {
@@ -2679,6 +2749,14 @@
2679
2749
  if (z === prev.z && Math.abs(p.x - q.x) <= 10 && Math.abs(p.y - q.y) <= 10) return;
2680
2750
  try { hook.call(global.ixmaps, mapLibreToFlatZoom(z)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onZoomAndPan:', e); }
2681
2751
  });
2752
+ notifyZoomAndPanMidGesture = () => {
2753
+ const hook = pageIxmaps() && pageIxmaps().htmlgui_onZoomAndPan;
2754
+ if (typeof hook !== 'function' || !last) return;
2755
+ const z = map.getZoom();
2756
+ if (z === last.z) return; // a pan alone waits for 'moveend' (and its 10 px rule)
2757
+ last = { z, c: map.getCenter() };
2758
+ try { hook.call(global.ixmaps, mapLibreToFlatZoom(z)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onZoomAndPan:', e); }
2759
+ };
2682
2760
  // flat calls it on the first draw too (its old zoom is unset then)
2683
2761
  map.once('load', () => {
2684
2762
  last = { z: map.getZoom(), c: map.getCenter() };
@@ -3818,10 +3896,11 @@
3818
3896
  const elapsed = now - lastRefreshAt;
3819
3897
  if (elapsed >= REFRESH_INTERVAL_MS) {
3820
3898
  lastRefreshAt = now;
3899
+ notifyZoomAndPanMidGesture();
3821
3900
  refreshLayers();
3822
3901
  } else {
3823
3902
  clearTimeout(refreshTimer);
3824
- refreshTimer = setTimeout(() => { lastRefreshAt = Date.now(); refreshLayers(); }, REFRESH_INTERVAL_MS - elapsed);
3903
+ refreshTimer = setTimeout(() => { lastRefreshAt = Date.now(); notifyZoomAndPanMidGesture(); refreshLayers(); }, REFRESH_INTERVAL_MS - elapsed);
3825
3904
  }
3826
3905
  scheduleNotifyRedraw();
3827
3906
  }
@@ -9887,6 +9966,8 @@
9887
9966
  szResourceBase: ixmapsSzResourceBase, getProjectString, setProjectJSON, loadProject,
9888
9967
  markThemeClass, unmarkThemeClass, getBoundingBox, setTitle, setTitleBox, refreshTheme, formatValue: flatFormatValue,
9889
9968
  map: mapHandle, getZoom,
9969
+ // this engine's version (flat's own ixmaps.version numbers flat)
9970
+ glVersion: IXMAPS_GL_VERSION,
9890
9971
  // flat always has this page hook (its layer legend tool defines it,
9891
9972
  // ui/js/tools/layer_legend.js 702-712): pages wrap it and call the one
9892
9973
  // they replaced — the chain ends here
@@ -9905,6 +9986,7 @@
9905
9986
  // test-only: lets test/unit/*.test.mjs call pure internals directly (the
9906
9987
  // engine runs in a Node vm there); deliberately NOT on the ixmaps object
9907
9988
  global.__ixmapsGlInternals = {
9989
+ IXMAPS_GL_VERSION,
9908
9990
  normalizeTheme, projectThemeToDefinition, LayerBuilder, LayerRuntime, typeStyleNumbers, styleNum,
9909
9991
  resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
9910
9992
  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.1.0",
3
+ "version": "0.2.1",
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",