@tileflow/svelte 0.1.0-alpha.14 → 0.1.0-alpha.17
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 +13 -0
- package/package.json +6 -4
- package/src/TileflowMap.svelte +118 -84
- package/src/TileflowMap.svelte.d.ts +1 -1
- package/src/index.d.ts +1 -0
package/README.md
CHANGED
|
@@ -26,4 +26,17 @@ Tileflow resolves from the map name, manifest, or explicit style props. Direct
|
|
|
26
26
|
Tileflow props such as `center`, `zoom`, and `interactive` take priority when
|
|
27
27
|
provided.
|
|
28
28
|
|
|
29
|
+
## Headless capture readiness
|
|
30
|
+
|
|
31
|
+
Use `captureId` to disambiguate repeated named maps:
|
|
32
|
+
|
|
33
|
+
```svelte
|
|
34
|
+
<TileflowMap map="madrid" captureId="checkout-map" />
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The root exposes `data-tileflow-map`, optional `data-tileflow-capture-id`, and
|
|
38
|
+
`data-tileflow-state="loading|idle|error"`. It becomes idle only after MapLibre idle plus two
|
|
39
|
+
animation frames (or image decode/load in image mode), returns to loading for new style/data work,
|
|
40
|
+
and marks terminal load errors. Application capture selects exactly one ready target.
|
|
41
|
+
|
|
29
42
|
Docs: https://tileflow.dev/docs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tileflow/svelte",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.17",
|
|
4
4
|
"description": "Svelte component for rendering Tileflow maps with MapLibre.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
"homepage": "https://tileflow.dev",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/tileflow/tileflow.git",
|
|
11
|
+
"url": "git+https://github.com/tileflow/tileflow-sdk.git",
|
|
12
12
|
"directory": "packages/svelte"
|
|
13
13
|
},
|
|
14
14
|
"bugs": {
|
|
15
|
-
"url": "https://github.com/tileflow/tileflow/issues"
|
|
15
|
+
"url": "https://github.com/tileflow/tileflow-sdk/issues"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
18
|
"tileflow",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"access": "public"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@tileflow/core": "0.1.0-alpha.
|
|
45
|
+
"@tileflow/core": ">=0.1.0-alpha.17 <0.1.0-beta.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"maplibre-gl": ">=5",
|
|
@@ -55,6 +55,8 @@
|
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"build": "svelte-check --tsconfig ./tsconfig.json",
|
|
58
|
+
"test": "node --test test/*.test.js",
|
|
59
|
+
"verify": "pnpm run build && pnpm run test",
|
|
58
60
|
"typecheck": "svelte-check --tsconfig ./tsconfig.json",
|
|
59
61
|
"lint": "svelte-check --tsconfig ./tsconfig.json"
|
|
60
62
|
}
|
package/src/TileflowMap.svelte
CHANGED
|
@@ -3,24 +3,21 @@
|
|
|
3
3
|
type LngLatLike,
|
|
4
4
|
type Map as MapLibreMap,
|
|
5
5
|
type MapOptions as MapLibreMapOptions,
|
|
6
|
-
type RequestParameters,
|
|
7
|
-
type RequestTransformFunction,
|
|
8
6
|
type StyleSpecification,
|
|
9
7
|
} from 'maplibre-gl';
|
|
10
|
-
import {createEventDispatcher, onMount} from 'svelte';
|
|
8
|
+
import {createEventDispatcher, onMount, tick} from 'svelte';
|
|
11
9
|
import {
|
|
12
10
|
createTileflowSessionId,
|
|
13
11
|
defaultTileflowManifestUrl,
|
|
14
12
|
loadTileflowManifest,
|
|
15
13
|
mergeTileflowAnalytics,
|
|
14
|
+
normalizeTileflowCaptureId,
|
|
16
15
|
normalizeTileflowStaticImageSize,
|
|
17
|
-
resolveTileflowAnalyticsRequestUrl,
|
|
18
16
|
resolveTileflowManifestMap,
|
|
19
17
|
resolveTileflowMapMode,
|
|
20
18
|
resolveTileflowRuntimeStyle,
|
|
21
19
|
resolveTileflowStaticImageUrl,
|
|
22
20
|
shouldLoadTileflowManifest,
|
|
23
|
-
startTileflowSession,
|
|
24
21
|
type MapLibreStyle,
|
|
25
22
|
type TileflowAnalytics,
|
|
26
23
|
type TileflowConfig,
|
|
@@ -28,6 +25,13 @@
|
|
|
28
25
|
type TileflowProjectThemes,
|
|
29
26
|
type TileflowRuntimeManifestMap,
|
|
30
27
|
} from '@tileflow/core';
|
|
28
|
+
import {
|
|
29
|
+
attachTileflowMapLifecycle,
|
|
30
|
+
createTileflowMarkerController,
|
|
31
|
+
createTileflowSessionStarter,
|
|
32
|
+
createTileflowTransformRequest,
|
|
33
|
+
type TileflowMapLifecycleAttachment,
|
|
34
|
+
} from '@tileflow/core/browser';
|
|
31
35
|
|
|
32
36
|
export let alt = '';
|
|
33
37
|
export let analytics: TileflowAnalytics | undefined = undefined;
|
|
@@ -37,6 +41,7 @@
|
|
|
37
41
|
const defaultZoom = 2;
|
|
38
42
|
|
|
39
43
|
export let center: [number, number] | undefined = undefined;
|
|
44
|
+
export let captureId: string | undefined = undefined;
|
|
40
45
|
export let className = '';
|
|
41
46
|
export let config: TileflowConfig | undefined = undefined;
|
|
42
47
|
export let height: number | string = 420;
|
|
@@ -57,21 +62,39 @@
|
|
|
57
62
|
export let zoom: number | undefined = undefined;
|
|
58
63
|
|
|
59
64
|
let container: HTMLDivElement;
|
|
65
|
+
let captureState: 'error' | 'idle' | 'loading' = 'loading';
|
|
60
66
|
let imageSize: {height: number; width: number} | null = null;
|
|
61
67
|
let imageResizeObserver: ResizeObserver | null = null;
|
|
62
68
|
let mapInstance: MapLibreMap | null = null;
|
|
63
|
-
let
|
|
69
|
+
let mapLifecycleAttachment: TileflowMapLifecycleAttachment | null = null;
|
|
64
70
|
let mapResizeObserver: ResizeObserver | null = null;
|
|
65
71
|
let manifestMap: TileflowRuntimeManifestMap | null = null;
|
|
66
72
|
let manifestLoadId = 0;
|
|
67
73
|
let mounted = false;
|
|
68
74
|
let refreshRunId = 0;
|
|
75
|
+
let readinessRunId = 0;
|
|
69
76
|
|
|
70
77
|
const dispatch = createEventDispatcher<{load: MapLibreMap}>();
|
|
71
78
|
const sessionId = createTileflowSessionId();
|
|
72
|
-
const
|
|
79
|
+
const sessionStarter = createTileflowSessionStarter({sessionId, source: 'svelte'});
|
|
80
|
+
const markerController = createTileflowMarkerController<
|
|
81
|
+
MapLibreMap,
|
|
82
|
+
TileflowMapMarker,
|
|
83
|
+
maplibregl.Marker
|
|
84
|
+
>({
|
|
85
|
+
attach(markerInstance, targetMap, definition) {
|
|
86
|
+
markerInstance.setLngLat(definition.coordinates).addTo(targetMap);
|
|
87
|
+
markerInstance.getElement().title = definition.label ?? definition.id;
|
|
88
|
+
},
|
|
89
|
+
create: (definition) =>
|
|
90
|
+
new maplibregl.Marker({
|
|
91
|
+
color: definition.color ?? '#C6A15B',
|
|
92
|
+
}),
|
|
93
|
+
remove: (markerInstance) => markerInstance.remove(),
|
|
94
|
+
});
|
|
73
95
|
|
|
74
96
|
$: resolvedMode = resolveTileflowMapMode({imageUrl, mode, preferLocalDev});
|
|
97
|
+
$: resolvedCaptureId = normalizeTileflowCaptureId(captureId);
|
|
75
98
|
$: isImageMode = resolvedMode === 'image';
|
|
76
99
|
$: imageCenter = center ?? defaultCenter;
|
|
77
100
|
$: imageZoom = zoom ?? defaultZoom;
|
|
@@ -134,6 +157,11 @@
|
|
|
134
157
|
syncMarkers();
|
|
135
158
|
}
|
|
136
159
|
|
|
160
|
+
$: if (mounted && isImageMode) {
|
|
161
|
+
runtimeImageUrl;
|
|
162
|
+
void markImageLoading();
|
|
163
|
+
}
|
|
164
|
+
|
|
137
165
|
onMount(() => {
|
|
138
166
|
mounted = true;
|
|
139
167
|
|
|
@@ -214,15 +242,28 @@
|
|
|
214
242
|
}
|
|
215
243
|
|
|
216
244
|
function destroyMap() {
|
|
245
|
+
readinessRunId += 1;
|
|
246
|
+
const lifecycleAttachment = mapLifecycleAttachment;
|
|
247
|
+
const currentMap = mapInstance;
|
|
248
|
+
mapLifecycleAttachment = null;
|
|
249
|
+
mapInstance = null;
|
|
217
250
|
mapResizeObserver?.disconnect();
|
|
218
251
|
mapResizeObserver = null;
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
lifecycleAttachment?.dispose();
|
|
255
|
+
} finally {
|
|
256
|
+
try {
|
|
257
|
+
markerController.dispose();
|
|
258
|
+
} finally {
|
|
259
|
+
currentMap?.remove();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
222
262
|
}
|
|
223
263
|
|
|
224
264
|
function recreateMap() {
|
|
225
265
|
destroyMap();
|
|
266
|
+
captureState = 'loading';
|
|
226
267
|
|
|
227
268
|
if (!container || !runtimeStyle || isImageMode) {
|
|
228
269
|
return;
|
|
@@ -236,11 +277,13 @@
|
|
|
236
277
|
container,
|
|
237
278
|
interactive: resolvedInteractive,
|
|
238
279
|
style: runtime.style as StyleSpecification | string,
|
|
239
|
-
transformRequest:
|
|
240
|
-
|
|
280
|
+
transformRequest: createTileflowTransformRequest({
|
|
281
|
+
always: true,
|
|
282
|
+
asyncAnalyticsTiming: 'request',
|
|
283
|
+
getAnalytics: () => resolvedAnalytics,
|
|
241
284
|
sessionId,
|
|
242
|
-
mapOptions?.transformRequest,
|
|
243
|
-
),
|
|
285
|
+
transformRequest: mapOptions?.transformRequest ?? undefined,
|
|
286
|
+
}),
|
|
244
287
|
zoom: resolvedZoom,
|
|
245
288
|
});
|
|
246
289
|
|
|
@@ -255,25 +298,29 @@
|
|
|
255
298
|
});
|
|
256
299
|
mapResizeObserver.observe(container);
|
|
257
300
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
301
|
+
mapLifecycleAttachment = attachTileflowMapLifecycle<MapLibreMap, number>({
|
|
302
|
+
getSession: () => {
|
|
303
|
+
const analyticsForLoad = resolvedAnalytics;
|
|
304
|
+
return {
|
|
305
|
+
analytics: analyticsForLoad,
|
|
306
|
+
styleId:
|
|
307
|
+
analyticsForLoad?.styleId ?? (typeof runtime.style === 'string' ? runtime.style : map),
|
|
308
|
+
};
|
|
309
|
+
},
|
|
310
|
+
map: maplibreMap,
|
|
311
|
+
onLoad: () => dispatch('load', maplibreMap),
|
|
312
|
+
scheduler: {
|
|
313
|
+
cancelFrame: (frame) => cancelAnimationFrame(frame),
|
|
314
|
+
requestFrame: (callback) => requestAnimationFrame(callback),
|
|
315
|
+
},
|
|
316
|
+
sessionStarter,
|
|
317
|
+
setState: (state) => {
|
|
318
|
+
captureState = state;
|
|
319
|
+
},
|
|
320
|
+
subscribe: (targetMap, event, listener) => {
|
|
321
|
+
const subscription = targetMap.on(event, listener);
|
|
322
|
+
return () => subscription.unsubscribe();
|
|
323
|
+
},
|
|
277
324
|
});
|
|
278
325
|
|
|
279
326
|
syncMarkers();
|
|
@@ -287,70 +334,48 @@
|
|
|
287
334
|
}
|
|
288
335
|
|
|
289
336
|
function syncMarkers() {
|
|
290
|
-
clearMarkers();
|
|
291
|
-
|
|
292
337
|
if (!mapInstance) {
|
|
338
|
+
markerController.clear();
|
|
293
339
|
return;
|
|
294
340
|
}
|
|
295
341
|
|
|
296
|
-
|
|
297
|
-
const markerInstance = new maplibregl.Marker({
|
|
298
|
-
color: marker.color ?? '#C6A15B',
|
|
299
|
-
})
|
|
300
|
-
.setLngLat(marker.coordinates)
|
|
301
|
-
.addTo(mapInstance!);
|
|
302
|
-
|
|
303
|
-
markerInstance.getElement().title = marker.label ?? marker.id;
|
|
304
|
-
return markerInstance;
|
|
305
|
-
});
|
|
342
|
+
markerController.replace(mapInstance, markers);
|
|
306
343
|
}
|
|
307
344
|
|
|
308
|
-
function
|
|
309
|
-
|
|
310
|
-
marker.remove();
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
markerInstances = [];
|
|
345
|
+
async function handleImageLoad(event: Event) {
|
|
346
|
+
await markImageReady(event.currentTarget as HTMLImageElement);
|
|
314
347
|
}
|
|
315
348
|
|
|
316
|
-
function
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (isPromiseLike(request)) {
|
|
326
|
-
return request.then((resolvedRequest) =>
|
|
327
|
-
applyTileflowAnalyticsRequest(url, resolvedRequest, analyticsInput, session) ??
|
|
328
|
-
resolvedRequest,
|
|
329
|
-
);
|
|
349
|
+
async function markImageReady(image: HTMLImageElement) {
|
|
350
|
+
const runId = ++readinessRunId;
|
|
351
|
+
try {
|
|
352
|
+
if (typeof image.decode === 'function') await image.decode();
|
|
353
|
+
} catch {
|
|
354
|
+
if (!image.complete || image.naturalWidth === 0) {
|
|
355
|
+
if (runId === readinessRunId) captureState = 'error';
|
|
356
|
+
return;
|
|
330
357
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
};
|
|
358
|
+
}
|
|
359
|
+
if (runId === readinessRunId) captureState = 'idle';
|
|
334
360
|
}
|
|
335
361
|
|
|
336
|
-
function
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
return request;
|
|
362
|
+
async function markImageLoading() {
|
|
363
|
+
const runId = ++readinessRunId;
|
|
364
|
+
captureState = 'loading';
|
|
365
|
+
await tick();
|
|
366
|
+
if (!mounted || runId !== readinessRunId) return;
|
|
367
|
+
const image = container?.querySelector('img');
|
|
368
|
+
if (!image?.complete) return;
|
|
369
|
+
if (image.naturalWidth === 0) {
|
|
370
|
+
captureState = 'error';
|
|
371
|
+
return;
|
|
347
372
|
}
|
|
348
|
-
|
|
349
|
-
return request ? {...request, url: nextUrl} : {url: nextUrl};
|
|
373
|
+
await markImageReady(image);
|
|
350
374
|
}
|
|
351
375
|
|
|
352
|
-
function
|
|
353
|
-
|
|
376
|
+
function handleImageError() {
|
|
377
|
+
readinessRunId += 1;
|
|
378
|
+
captureState = 'error';
|
|
354
379
|
}
|
|
355
380
|
|
|
356
381
|
function formatHeight(value: number | string): string {
|
|
@@ -358,12 +383,21 @@
|
|
|
358
383
|
}
|
|
359
384
|
</script>
|
|
360
385
|
|
|
361
|
-
<div
|
|
386
|
+
<div
|
|
387
|
+
bind:this={container}
|
|
388
|
+
class={className}
|
|
389
|
+
data-tileflow-capture-id={resolvedCaptureId}
|
|
390
|
+
data-tileflow-map={map}
|
|
391
|
+
data-tileflow-state={captureState}
|
|
392
|
+
style={frameStyle}
|
|
393
|
+
>
|
|
362
394
|
{#if isImageMode && runtimeImageUrl}
|
|
363
395
|
<img
|
|
364
396
|
{alt}
|
|
365
397
|
decoding="async"
|
|
366
398
|
loading={imageLoading}
|
|
399
|
+
on:error={handleImageError}
|
|
400
|
+
on:load={handleImageLoad}
|
|
367
401
|
src={runtimeImageUrl}
|
|
368
402
|
style="display: block; height: 100%; object-fit: cover; width: 100%;"
|
|
369
403
|
/>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {SvelteComponentTyped} from 'svelte';
|
|
2
1
|
import type {Map as MapLibreMap} from 'maplibre-gl';
|
|
2
|
+
import type {SvelteComponentTyped} from 'svelte';
|
|
3
3
|
import type {TileflowMapProps} from './index';
|
|
4
4
|
|
|
5
5
|
export default class TileflowMap extends SvelteComponentTyped<
|