@massif-maps/web 6.1.0-rc.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.
package/bridge.mjs ADDED
@@ -0,0 +1,197 @@
1
+ /*
2
+ * The web's NativeBridge for @massif-maps/api: the same interface the NativeScript plugin gives
3
+ * Android and iOS, over the facade's C ABI. "Native" objects are tokens naming a part of the one map.
4
+ */
5
+
6
+ const OK = 0;
7
+
8
+ /** A JavaScript handler lives in the page's wasm table only, so every event is delivered there. */
9
+ const DELIVERY_UI = 'ui';
10
+
11
+ export function createBridge(module) {
12
+ const fn = (name, returns, args) => module.cwrap(name, returns, args);
13
+ const c = {
14
+ ctx: fn('mm_context_default', 'number', [])(),
15
+ create: fn('mm_create', 'number', ['number', 'string', 'string', 'string', 'number']),
16
+ destroy: fn('mm_destroy', 'number', ['number', 'string', 'string']),
17
+ destroyHandle: fn('mm_destroy_handle', 'number', ['number', 'number']),
18
+ find: fn('mm_find', 'number', ['number', 'string', 'string', 'number']),
19
+ valid: fn('mm_valid', 'number', ['number', 'number']),
20
+ setBool: fn('mm_set_bool', 'number', ['number', 'number', 'string', 'number']),
21
+ setDouble: fn('mm_set_double', 'number', ['number', 'number', 'string', 'number']),
22
+ setString: fn('mm_set_string', 'number', ['number', 'number', 'string', 'string']),
23
+ setJson: fn('mm_set_json', 'number', ['number', 'number', 'string', 'string']),
24
+ setObject: fn('mm_set_object', 'number', ['number', 'number', 'string', 'number']),
25
+ getObject: fn('mm_get_object', 'number', ['number', 'number', 'string']),
26
+ getBool: fn('mm_get_bool', 'number', ['number', 'number', 'string', 'number']),
27
+ getDouble: fn('mm_get_double', 'number', ['number', 'number', 'string', 'number']),
28
+ getString: fn('mm_get_string', 'number', ['number', 'number', 'string', 'string', 'number', 'number', 'number']),
29
+ call: fn('mm_call', 'number', ['number', 'number', 'string', 'string', 'number']),
30
+ callAsync: fn('mm_call_async', 'number', ['number', 'number', 'string', 'string', 'string', 'number']),
31
+ cancelCall: fn('mm_cancel_call', 'number', ['number', 'number']),
32
+ cancelCalls: fn('mm_cancel_calls', 'number', ['number', 'number', 'number']),
33
+ doublesCount: fn('mm_doubles_count', 'number', ['number', 'number', 'number']),
34
+ doublesCopy: fn('mm_doubles_copy', 'number', ['number', 'number', 'number', 'number', 'number']),
35
+ dataSize: fn('mm_data_size', 'number', ['number', 'number', 'string', 'number']),
36
+ dataCopy: fn('mm_data_copy', 'number', ['number', 'number', 'string', 'number', 'number', 'number']),
37
+ on: fn('mm_on', 'number', ['number', 'number', 'string', 'number', 'number', 'string', 'number']),
38
+ off: fn('mm_off', 'number', ['number', 'number']),
39
+ offAll: fn('mm_off_all', 'number', ['number', 'number', 'number']),
40
+ resultName: fn('mm_result_name', 'string', ['number']),
41
+ adopt: fn('massifAdopt', 'number', ['string', 'string', 'string']),
42
+ attachMapEvents: fn('massifAttachMapEvents', null, ['number']),
43
+ bridgeLayerClicks: fn('massifBridgeLayerClicks', 'number', ['number']),
44
+ };
45
+ const ctx = c.ctx;
46
+ // Function-table slot of each live subscription, released on off.
47
+ const handlers = new Map();
48
+
49
+ // Zeroed scratch memory, always freed: an out-parameter the ABI leaves alone must not read as garbage.
50
+ const scratch = (size, body) => {
51
+ const pointer = module._malloc(size);
52
+ module.HEAPU8.fill(0, pointer, pointer + size);
53
+ try {
54
+ return body(pointer);
55
+ } finally {
56
+ module._free(pointer);
57
+ }
58
+ };
59
+ // Handles, subscriptions and call ids are uint32 with a generation in the top bits: read unsigned.
60
+ const unsigned = (out) => module.getValue(out, 'i32') >>> 0;
61
+ const outInt = (body) => scratch(4, (out) => (body(out) === OK ? unsigned(out) : 0));
62
+ const fail = (what, code) => {
63
+ throw new Error(`${what} failed: ${c.resultName(code)} (${code})`);
64
+ };
65
+ const readString = (handle, path, projection) => scratch(4, (needed) => {
66
+ if (c.getString(ctx, handle, path, projection ?? '', 0, 0, needed) !== OK) {
67
+ return null;
68
+ }
69
+ const size = module.getValue(needed, 'i32');
70
+ return scratch(size + 1, (buffer) =>
71
+ c.getString(ctx, handle, path, projection ?? '', buffer, size + 1, needed) === OK ? module.UTF8ToString(buffer) : null);
72
+ });
73
+ const readNumber = (getter, handle, path, fallback, type) => scratch(8, (out) =>
74
+ getter(ctx, handle, path, out) === OK ? module.getValue(out, type) : fallback);
75
+ // A token for a part of the map, which massifAdopt names.
76
+ const part = (what) => ({ webPart: what });
77
+
78
+ return {
79
+ available: true,
80
+ canConsume: false,
81
+
82
+ create(kind, id, json) {
83
+ let code = OK;
84
+ const handle = scratch(4, (out) => {
85
+ code = c.create(ctx, kind, id, json, out);
86
+ return unsigned(out);
87
+ });
88
+ return code === OK ? handle : fail(`create ${kind} "${id}"`, code);
89
+ },
90
+ destroy: (handle) => c.destroyHandle(ctx, handle) === OK,
91
+ isValid: (handle) => c.valid(ctx, handle) === OK,
92
+ findObject: (kind, id) => outInt((out) => c.find(ctx, kind, id, out)),
93
+ unregisterObject: (kind, id) => c.destroy(ctx, kind, id) === OK,
94
+
95
+ // One numeric type in JavaScript; the ABI coerces a double into an int property.
96
+ setFloat: (handle, path, value) => c.setDouble(ctx, handle, path, value),
97
+ setInt: (handle, path, value) => c.setDouble(ctx, handle, path, value),
98
+ setBool: (handle, path, value) => c.setBool(ctx, handle, path, value ? 1 : 0),
99
+ setString: (handle, path, value) => c.setString(ctx, handle, path, value),
100
+ setObject: (handle, path, value) => c.setObject(ctx, handle, path, value),
101
+ setAll: (handle, json, projection) => c.setJson(ctx, handle, json, projection ?? ''),
102
+ getObject: (handle, path) => c.getObject(ctx, handle, path) >>> 0,
103
+
104
+ getFloat: (handle, path, fallback) => readNumber(c.getDouble, handle, path, fallback, 'double'),
105
+ getInt: (handle, path, fallback) => Math.trunc(readNumber(c.getDouble, handle, path, fallback, 'double')),
106
+ getBool: (handle, path, fallback) => !!readNumber(c.getBool, handle, path, fallback ? 1 : 0, 'i32'),
107
+ getString: (handle, path) => readString(handle, path, ''),
108
+ getPos: (handle, path, projection) => readString(handle, path, projection) || null,
109
+
110
+ call(handle, method, argsJson) {
111
+ let code = OK;
112
+ const result = scratch(4, (out) => {
113
+ code = c.call(ctx, handle, method, argsJson, out);
114
+ return unsigned(out);
115
+ });
116
+ return code === OK ? result : fail(`call ${method}`, code);
117
+ },
118
+ callAsync(handle, method, argsJson, event) {
119
+ let code = OK;
120
+ const call = scratch(4, (out) => {
121
+ code = c.callAsync(ctx, handle, method, argsJson, event, out);
122
+ return unsigned(out);
123
+ });
124
+ return code === OK ? call : fail(`callAsync ${method}`, code);
125
+ },
126
+ cancelCall: (call) => c.cancelCall(ctx, call) === OK,
127
+ cancelCalls: (handle) => outInt((out) => c.cancelCalls(ctx, handle, out)),
128
+
129
+ getDoubles(handle) {
130
+ const count = outInt((out) => c.doublesCount(ctx, handle, out));
131
+ if (!count) {
132
+ return [];
133
+ }
134
+ return scratch(count * 8, (buffer) => scratch(4, (copied) => {
135
+ c.doublesCopy(ctx, handle, buffer, count, copied);
136
+ return Array.from(new Float64Array(module.HEAPF64.buffer, buffer, count));
137
+ }));
138
+ },
139
+ getData(handle, path) {
140
+ const size = outInt((out) => c.dataSize(ctx, handle, path, out));
141
+ if (!size) {
142
+ return null;
143
+ }
144
+ return scratch(size, (buffer) => scratch(4, (copied) =>
145
+ c.dataCopy(ctx, handle, path, buffer, size, copied) === OK ? module.HEAPU8.slice(buffer, buffer + size).buffer : null));
146
+ },
147
+
148
+ on(handle, event, handler, delivery, coalesce, projection) {
149
+ const pointer = module.addFunction((userData, target, eventName, payload) =>
150
+ (handler(target >>> 0, module.UTF8ToString(eventName), payload >>> 0) ? 1 : 0), 'iiiii');
151
+ const options = JSON.stringify({ delivery: DELIVERY_UI, coalesce: !!coalesce, ...(projection ? { projection } : {}) });
152
+ const subscription = outInt((out) => c.on(ctx, handle, event, pointer, 0, options, out));
153
+ if (subscription) {
154
+ handlers.set(subscription, pointer);
155
+ } else {
156
+ module.removeFunction(pointer);
157
+ }
158
+ return subscription;
159
+ },
160
+ off(subscription) {
161
+ const removed = c.off(ctx, subscription) === OK;
162
+ const pointer = handlers.get(subscription);
163
+ if (pointer !== undefined) {
164
+ module.removeFunction(pointer);
165
+ handlers.delete(subscription);
166
+ }
167
+ return removed;
168
+ },
169
+ // Table slots of these subscriptions stay until the page goes: the ABI does not say which they were.
170
+ offAll: (handle) => outInt((out) => c.offAll(ctx, handle, out)),
171
+
172
+ adoptOptions: (kind, id, native) => (native?.webPart === 'options' ? c.adopt(kind, 'options', id) : 0),
173
+ adoptLayers: (kind, id, native) => (native?.webPart === 'layers' ? c.adopt(kind, 'layers', id) : 0),
174
+ adoptView: (kind, id, native) => (native?.webPart === 'view' ? c.adopt(kind, 'view', id) : 0),
175
+ // Layers, sources and assets are only ever built from specs on the web: there is no object API.
176
+ adoptLayer: () => 0,
177
+ adoptSource: () => 0,
178
+ adoptAssets: () => 0,
179
+ getNativeLayer(id) {
180
+ const handle = outInt((out) => c.find(ctx, 'layer', id, out));
181
+ return handle ? { webLayer: handle } : null;
182
+ },
183
+ getNativeSource: () => null,
184
+ getNativeLayerByHandle: (handle) => ({ webLayer: handle }),
185
+ getNativeSourceByHandle: () => null,
186
+
187
+ attachMapEvents: (mapView, handle) => c.attachMapEvents(handle),
188
+ attachVectorTileEvents: (layer, handle) => void c.bridgeLayerClicks(handle),
189
+ attachVectorElementEvents: (layer, handle) => void c.bridgeLayerClicks(handle),
190
+ attachCelestialEvents: (layer, handle) => void c.bridgeLayerClicks(handle),
191
+
192
+ nativeShortClassName: () => null,
193
+
194
+ /** The tokens a MapViewLike hands to the adopt calls above. */
195
+ parts: { view: part('view'), options: part('options'), layers: part('layers') },
196
+ };
197
+ }
@@ -0,0 +1,69 @@
1
+ /*
2
+ * Cross-origin isolation on a host that cannot set headers.
3
+ *
4
+ * The SDK's tile and label workers are pthreads, so the page needs SharedArrayBuffer, which needs
5
+ * COOP/COEP. GitHub Pages serves no custom headers, so a service worker re-serves the documents
6
+ * with them instead. Same file twice: loaded as a page script it registers itself, loaded as a
7
+ * worker it does the rewriting.
8
+ *
9
+ * COEP is `credentialless`, not `require-corp`: a tile server does not send
10
+ * Cross-Origin-Resource-Policy, and require-corp would block every tile.
11
+ */
12
+
13
+ if (typeof window === 'undefined') {
14
+ self.addEventListener('install', () => self.skipWaiting());
15
+ self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim()));
16
+
17
+ self.addEventListener('fetch', (event) => {
18
+ const request = event.request;
19
+ if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
20
+ return; // Chrome rejects this combination before it reaches us
21
+ }
22
+ event.respondWith(
23
+ fetch(request)
24
+ .then((response) => {
25
+ if (response.status === 0) {
26
+ return response; // opaque, nothing to rewrite
27
+ }
28
+ const headers = new Headers(response.headers);
29
+ headers.set('Cross-Origin-Embedder-Policy', 'credentialless');
30
+ headers.set('Cross-Origin-Opener-Policy', 'same-origin');
31
+ return new Response(response.body, {
32
+ status: response.status,
33
+ statusText: response.statusText,
34
+ headers,
35
+ });
36
+ })
37
+ .catch((error) => {
38
+ console.error('coi-serviceworker:', error);
39
+ throw error;
40
+ })
41
+ );
42
+ });
43
+ } else if (!window.crossOriginIsolated) {
44
+ // One reload, once: without the guard a failed registration would reload forever.
45
+ const RELOADED = 'coi-reloaded';
46
+ if (!navigator.serviceWorker) {
47
+ console.error('coi-serviceworker: no service worker support, the map needs SharedArrayBuffer');
48
+ } else {
49
+ navigator.serviceWorker
50
+ .register(window.document.currentScript.src)
51
+ // ready, not the register() result: right after registering the worker is still installing,
52
+ // and only an ACTIVE one can serve the reload that makes the page isolated.
53
+ .then(() => navigator.serviceWorker.ready)
54
+ .then(() => {
55
+ if (navigator.serviceWorker.controller) {
56
+ return; // already serving this page, and it is still not isolated - nothing more to try
57
+ }
58
+ if (sessionStorage.getItem(RELOADED)) {
59
+ console.error('coi-serviceworker: still not isolated after a reload, giving up');
60
+ return;
61
+ }
62
+ sessionStorage.setItem(RELOADED, '1');
63
+ window.location.reload();
64
+ })
65
+ .catch((error) => console.error('coi-serviceworker:', error));
66
+ }
67
+ } else {
68
+ sessionStorage.removeItem('coi-reloaded');
69
+ }
package/index.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import type { MassifMap } from '@massif-maps/api';
2
+
3
+ export * from '@massif-maps/api';
4
+
5
+ export interface CreateMapOptions {
6
+ /** The registry id the map's options take. Defaults to `map`. */
7
+ id?: string;
8
+ /** What positions read and write in. Defaults to `EPSG:4326`, plain `[lon, lat]`. */
9
+ projection?: string;
10
+ /** Where massif-web.mjs is. Defaults to beside this package's own files. */
11
+ moduleUrl?: string;
12
+ /** Anything else goes to the emscripten module factory (print, printErr, locateFile...). */
13
+ [option: string]: unknown;
14
+ }
15
+
16
+ /** Loads the SDK module and starts a map on `canvas`. One per page. */
17
+ export declare function createMap(canvas: HTMLCanvasElement, options?: CreateMapOptions): Promise<MassifMap & { module: any }>;
package/index.mjs ADDED
@@ -0,0 +1,41 @@
1
+ /*
2
+ * @massif-maps/web: the typed surface API (@massif-maps/api) on a canvas.
3
+ *
4
+ * const map = await createMap(document.getElementById('map'));
5
+ * map.addLayer('osm', { type: 'raster', source: { type: 'http', url: '.../{z}/{x}/{y}.png' } });
6
+ * map.camera().moveTo([6.8652, 45.8326], { zoom: 11 });
7
+ */
8
+ import { attach, setBridge } from '@massif-maps/api';
9
+ import { createBridge } from './bridge.mjs';
10
+ import { canvasSelector, loadModule } from './massif.mjs';
11
+
12
+ export * from '@massif-maps/api';
13
+
14
+ let created = false;
15
+
16
+ /**
17
+ * Loads the SDK module and starts a map on `canvas`. One per page: the API talks to one module.
18
+ * `projection` is what positions read and write in; `moduleUrl` and the rest go to loadModule.
19
+ */
20
+ export async function createMap(canvas, { id = 'map', projection = 'EPSG:4326', ...moduleOptions } = {}) {
21
+ if (created) {
22
+ throw new Error('createMap: this page already has a map');
23
+ }
24
+ created = true;
25
+ const module = await loadModule(moduleOptions);
26
+ const bridge = createBridge(module);
27
+ setBridge(bridge);
28
+ if (!module.ccall('massifCreateMap', 'number', ['string'], [canvasSelector(canvas, id)])) {
29
+ throw new Error('createMap: this browser has no WebGL 2');
30
+ }
31
+ const view = {
32
+ mapView: { getLayers: () => bridge.parts.layers, getBaseMapView: () => bridge.parts.view },
33
+ getOptions: () => ({ getNative: () => bridge.parts.options }),
34
+ getMeasuredWidth: () => canvas.width,
35
+ getMeasuredHeight: () => canvas.height,
36
+ };
37
+ const map = attach(view, { id, projection });
38
+ // The emscripten module, for what the facade does not cover (its FS, to mount an IndexedDB cache).
39
+ map.module = module;
40
+ return map;
41
+ }