@tileflow/svelte 0.1.0-alpha.10

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 ADDED
@@ -0,0 +1,29 @@
1
+ # @tileflow/svelte
2
+
3
+ Svelte component for rendering Tileflow maps with MapLibre.
4
+
5
+ Install `maplibre-gl` alongside the package and import its CSS once in your app.
6
+
7
+ ```svelte
8
+ <script lang="ts">
9
+ import {TileflowMap} from '@tileflow/svelte';
10
+ import 'maplibre-gl/dist/maplibre-gl.css';
11
+ </script>
12
+
13
+ <TileflowMap
14
+ map="madrid"
15
+ center={[-3.7038, 40.4168]}
16
+ zoom={12}
17
+ mapOptions={{
18
+ cooperativeGestures: true,
19
+ maxZoom: 18,
20
+ }}
21
+ />
22
+ ```
23
+
24
+ `mapOptions` accepts native MapLibre options except `container` and `style`, which
25
+ Tileflow resolves from the map name, manifest, or explicit style props. Direct
26
+ Tileflow props such as `center`, `zoom`, and `interactive` take priority when
27
+ provided.
28
+
29
+ Docs: https://tileflow.dev/docs
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@tileflow/svelte",
3
+ "version": "0.1.0-alpha.10",
4
+ "description": "Svelte component for rendering Tileflow maps with MapLibre.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "svelte": "./src/index.js",
8
+ "homepage": "https://tileflow.dev",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/tileflow/tileflow.git",
12
+ "directory": "packages/svelte"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/tileflow/tileflow/issues"
16
+ },
17
+ "keywords": [
18
+ "tileflow",
19
+ "svelte",
20
+ "maps",
21
+ "maplibre",
22
+ "tiles"
23
+ ],
24
+ "types": "./src/index.d.ts",
25
+ "files": [
26
+ "src"
27
+ ],
28
+ "exports": {
29
+ ".": {
30
+ "types": "./src/index.d.ts",
31
+ "svelte": "./src/index.js",
32
+ "import": "./src/index.js",
33
+ "default": "./src/index.js"
34
+ },
35
+ "./TileflowMap.svelte": {
36
+ "types": "./src/TileflowMap.svelte.d.ts",
37
+ "svelte": "./src/TileflowMap.svelte",
38
+ "default": "./src/TileflowMap.svelte"
39
+ }
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "dependencies": {
45
+ "@tileflow/core": "0.1.0-alpha.10"
46
+ },
47
+ "peerDependencies": {
48
+ "maplibre-gl": ">=5",
49
+ "svelte": ">=5"
50
+ },
51
+ "devDependencies": {
52
+ "maplibre-gl": "^5.24.0",
53
+ "svelte": "^5.43.17",
54
+ "svelte-check": "^4.3.4"
55
+ },
56
+ "scripts": {
57
+ "build": "svelte-check --tsconfig ./tsconfig.json",
58
+ "typecheck": "svelte-check --tsconfig ./tsconfig.json",
59
+ "lint": "svelte-check --tsconfig ./tsconfig.json"
60
+ }
61
+ }
@@ -0,0 +1,371 @@
1
+ <script lang="ts">
2
+ import maplibregl, {
3
+ type LngLatLike,
4
+ type Map as MapLibreMap,
5
+ type MapOptions as MapLibreMapOptions,
6
+ type RequestParameters,
7
+ type RequestTransformFunction,
8
+ type StyleSpecification,
9
+ } from 'maplibre-gl';
10
+ import {createEventDispatcher, onMount} from 'svelte';
11
+ import {
12
+ createTileflowSessionId,
13
+ defaultTileflowManifestUrl,
14
+ loadTileflowManifest,
15
+ mergeTileflowAnalytics,
16
+ normalizeTileflowStaticImageSize,
17
+ resolveTileflowAnalyticsRequestUrl,
18
+ resolveTileflowManifestMap,
19
+ resolveTileflowMapMode,
20
+ resolveTileflowRuntimeStyle,
21
+ resolveTileflowStaticImageUrl,
22
+ shouldLoadTileflowManifest,
23
+ startTileflowSession,
24
+ type MapLibreStyle,
25
+ type TileflowAnalytics,
26
+ type TileflowConfig,
27
+ type TileflowMapMarker,
28
+ type TileflowProjectThemes,
29
+ type TileflowRuntimeManifestMap,
30
+ } from '@tileflow/core';
31
+
32
+ export let alt = '';
33
+ export let analytics: TileflowAnalytics | undefined = undefined;
34
+ type TileflowMapOptions = Omit<MapLibreMapOptions, 'container' | 'style'>;
35
+
36
+ const defaultCenter: [number, number] = [0, 20];
37
+ const defaultZoom = 2;
38
+
39
+ export let center: [number, number] | undefined = undefined;
40
+ export let className = '';
41
+ export let config: TileflowConfig | undefined = undefined;
42
+ export let height: number | string = 420;
43
+ export let imageLoading: HTMLImageElement['loading'] = 'eager';
44
+ export let imageUrl: string | undefined = undefined;
45
+ export let interactive: boolean | undefined = undefined;
46
+ export let manifestUrl = defaultTileflowManifestUrl;
47
+ export let map: string | undefined = undefined;
48
+ export let mapOptions: TileflowMapOptions | undefined = undefined;
49
+ export let mapStyle: MapLibreStyle | undefined = undefined;
50
+ export let markers: TileflowMapMarker[] = [];
51
+ export let mode: 'interactive' | 'image' = 'interactive';
52
+ export let preferLocalDev = true;
53
+ export let styleBaseUrl: string | undefined = undefined;
54
+ export let styleUrl: string | undefined = undefined;
55
+ export let themes: TileflowProjectThemes | undefined = undefined;
56
+ export let tileBaseUrl: string | undefined = undefined;
57
+ export let zoom: number | undefined = undefined;
58
+
59
+ let container: HTMLDivElement;
60
+ let imageSize: {height: number; width: number} | null = null;
61
+ let imageResizeObserver: ResizeObserver | null = null;
62
+ let mapInstance: MapLibreMap | null = null;
63
+ let markerInstances: maplibregl.Marker[] = [];
64
+ let mapResizeObserver: ResizeObserver | null = null;
65
+ let manifestMap: TileflowRuntimeManifestMap | null = null;
66
+ let manifestLoadId = 0;
67
+ let mounted = false;
68
+ let refreshRunId = 0;
69
+
70
+ const dispatch = createEventDispatcher<{load: MapLibreMap}>();
71
+ const sessionId = createTileflowSessionId();
72
+ const sessionStarts = new Set<string>();
73
+
74
+ $: resolvedMode = resolveTileflowMapMode({imageUrl, mode, preferLocalDev});
75
+ $: isImageMode = resolvedMode === 'image';
76
+ $: imageCenter = center ?? defaultCenter;
77
+ $: imageZoom = zoom ?? defaultZoom;
78
+ $: resolvedCenter = center ?? mapOptions?.center ?? defaultCenter;
79
+ $: resolvedZoom = zoom ?? mapOptions?.zoom ?? defaultZoom;
80
+ $: resolvedInteractive = interactive ?? mapOptions?.interactive ?? true;
81
+ $: runtimeStyle = isImageMode
82
+ ? null
83
+ : resolveTileflowRuntimeStyle({
84
+ config,
85
+ manifestMap,
86
+ map,
87
+ preferLocalDev,
88
+ style: mapStyle,
89
+ styleBaseUrl,
90
+ styleUrl,
91
+ themes,
92
+ tileBaseUrl,
93
+ });
94
+ $: resolvedAnalytics = mergeTileflowAnalytics(analytics, runtimeStyle?.analytics);
95
+ $: runtimeImageUrl =
96
+ imageUrl ??
97
+ (isImageMode
98
+ ? resolveTileflowStaticImageUrl({
99
+ center: imageCenter,
100
+ imageSize,
101
+ manifestMap,
102
+ zoom: imageZoom,
103
+ })
104
+ : undefined);
105
+ $: frameStyle = `height: ${formatHeight(height)}; min-height: 240px; overflow: hidden; position: relative; width: 100%;`;
106
+
107
+ $: if (mounted) {
108
+ config;
109
+ imageUrl;
110
+ interactive;
111
+ manifestUrl;
112
+ map;
113
+ mapOptions;
114
+ mapStyle;
115
+ mode;
116
+ preferLocalDev;
117
+ styleBaseUrl;
118
+ styleUrl;
119
+ themes;
120
+ tileBaseUrl;
121
+ void refresh();
122
+ }
123
+
124
+ $: if (mounted) {
125
+ center;
126
+ zoom;
127
+ mapOptions?.center;
128
+ mapOptions?.zoom;
129
+ syncView();
130
+ }
131
+
132
+ $: if (mounted) {
133
+ markers;
134
+ syncMarkers();
135
+ }
136
+
137
+ onMount(() => {
138
+ mounted = true;
139
+
140
+ return () => {
141
+ mounted = false;
142
+ refreshRunId += 1;
143
+ manifestLoadId += 1;
144
+ imageResizeObserver?.disconnect();
145
+ destroyMap();
146
+ };
147
+ });
148
+
149
+ async function refresh() {
150
+ const runId = ++refreshRunId;
151
+ await refreshManifest();
152
+
153
+ if (!mounted || runId !== refreshRunId) {
154
+ return;
155
+ }
156
+
157
+ updateImageResizeObserver();
158
+ recreateMap();
159
+ }
160
+
161
+ async function refreshManifest() {
162
+ const shouldLoad = shouldLoadTileflowManifest({
163
+ config,
164
+ imageMode: isImageMode,
165
+ imageUrl,
166
+ map,
167
+ style: mapStyle,
168
+ styleBaseUrl,
169
+ styleUrl,
170
+ });
171
+
172
+ if (!shouldLoad) {
173
+ manifestLoadId += 1;
174
+ manifestMap = null;
175
+ return;
176
+ }
177
+
178
+ const loadId = ++manifestLoadId;
179
+ manifestMap = null;
180
+
181
+ try {
182
+ const manifest = await loadTileflowManifest(manifestUrl);
183
+
184
+ if (mounted && loadId === manifestLoadId) {
185
+ manifestMap = manifest && map ? resolveTileflowManifestMap(manifest, map) : null;
186
+ }
187
+ } catch (error) {
188
+ if (mounted && loadId === manifestLoadId) {
189
+ console.error('Failed to load Tileflow manifest', error);
190
+ manifestMap = null;
191
+ }
192
+ }
193
+ }
194
+
195
+ function updateImageResizeObserver() {
196
+ imageResizeObserver?.disconnect();
197
+ imageResizeObserver = null;
198
+ imageSize = null;
199
+
200
+ if (!isImageMode || imageUrl || !container) {
201
+ return;
202
+ }
203
+
204
+ const updateImageSize = () => {
205
+ imageSize = normalizeTileflowStaticImageSize({
206
+ height: container.clientHeight,
207
+ width: container.clientWidth,
208
+ });
209
+ };
210
+
211
+ imageResizeObserver = new ResizeObserver(updateImageSize);
212
+ imageResizeObserver.observe(container);
213
+ updateImageSize();
214
+ }
215
+
216
+ function destroyMap() {
217
+ mapResizeObserver?.disconnect();
218
+ mapResizeObserver = null;
219
+ clearMarkers();
220
+ mapInstance?.remove();
221
+ mapInstance = null;
222
+ }
223
+
224
+ function recreateMap() {
225
+ destroyMap();
226
+
227
+ if (!container || !runtimeStyle || isImageMode) {
228
+ return;
229
+ }
230
+
231
+ const runtime = runtimeStyle;
232
+ const maplibreMap = new maplibregl.Map({
233
+ ...mapOptions,
234
+ attributionControl: mapOptions?.attributionControl ?? {compact: true},
235
+ center: resolvedCenter as LngLatLike,
236
+ container,
237
+ interactive: resolvedInteractive,
238
+ style: runtime.style as StyleSpecification | string,
239
+ transformRequest: createTransformRequest(
240
+ () => resolvedAnalytics,
241
+ sessionId,
242
+ mapOptions?.transformRequest,
243
+ ),
244
+ zoom: resolvedZoom,
245
+ });
246
+
247
+ mapInstance = maplibreMap;
248
+
249
+ if (resolvedInteractive) {
250
+ maplibreMap.addControl(new maplibregl.NavigationControl(), 'top-right');
251
+ }
252
+
253
+ mapResizeObserver = new ResizeObserver(() => {
254
+ maplibreMap.resize();
255
+ });
256
+ mapResizeObserver.observe(container);
257
+
258
+ maplibreMap.on('load', () => {
259
+ dispatch('load', maplibreMap);
260
+ const analyticsForLoad = resolvedAnalytics;
261
+ const styleId =
262
+ analyticsForLoad?.styleId ?? (typeof runtime.style === 'string' ? runtime.style : map);
263
+
264
+ if (analyticsForLoad?.mapId) {
265
+ const sessionKey = `${sessionId}:${analyticsForLoad.mapId}:${styleId ?? ''}`;
266
+
267
+ if (!sessionStarts.has(sessionKey)) {
268
+ sessionStarts.add(sessionKey);
269
+ startTileflowSession(analyticsForLoad, {
270
+ mapId: analyticsForLoad.mapId,
271
+ sessionId,
272
+ source: 'svelte',
273
+ styleId,
274
+ });
275
+ }
276
+ }
277
+ });
278
+
279
+ syncMarkers();
280
+ }
281
+
282
+ function syncView() {
283
+ mapInstance?.jumpTo({
284
+ center: resolvedCenter as LngLatLike,
285
+ zoom: resolvedZoom,
286
+ });
287
+ }
288
+
289
+ function syncMarkers() {
290
+ clearMarkers();
291
+
292
+ if (!mapInstance) {
293
+ return;
294
+ }
295
+
296
+ markerInstances = markers.map((marker) => {
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
+ });
306
+ }
307
+
308
+ function clearMarkers() {
309
+ for (const marker of markerInstances) {
310
+ marker.remove();
311
+ }
312
+
313
+ markerInstances = [];
314
+ }
315
+
316
+ function createTransformRequest(
317
+ getAnalytics: () => TileflowAnalytics | undefined,
318
+ session: string,
319
+ userTransformRequest?: TileflowMapOptions['transformRequest'],
320
+ ): RequestTransformFunction | undefined {
321
+ return (url, resourceType) => {
322
+ const request = userTransformRequest?.(url, resourceType);
323
+ const analyticsInput = getAnalytics();
324
+
325
+ if (isPromiseLike(request)) {
326
+ return request.then((resolvedRequest) =>
327
+ applyTileflowAnalyticsRequest(url, resolvedRequest, analyticsInput, session) ??
328
+ resolvedRequest,
329
+ );
330
+ }
331
+
332
+ return applyTileflowAnalyticsRequest(url, request, analyticsInput, session);
333
+ };
334
+ }
335
+
336
+ function applyTileflowAnalyticsRequest(
337
+ url: string,
338
+ request: RequestParameters | undefined,
339
+ analyticsInput: TileflowAnalytics | undefined,
340
+ session: string,
341
+ ): RequestParameters | undefined {
342
+ const requestUrl = request?.url ?? url;
343
+ const nextUrl = resolveTileflowAnalyticsRequestUrl(requestUrl, analyticsInput, session);
344
+
345
+ if (!nextUrl) {
346
+ return request;
347
+ }
348
+
349
+ return request ? {...request, url: nextUrl} : {url: nextUrl};
350
+ }
351
+
352
+ function isPromiseLike<T>(value: T | Promise<T> | undefined): value is Promise<T> {
353
+ return Boolean(value && typeof (value as Promise<T>).then === 'function');
354
+ }
355
+
356
+ function formatHeight(value: number | string): string {
357
+ return typeof value === 'number' ? `${value}px` : value;
358
+ }
359
+ </script>
360
+
361
+ <div bind:this={container} class={className} style={frameStyle}>
362
+ {#if isImageMode && runtimeImageUrl}
363
+ <img
364
+ {alt}
365
+ decoding="async"
366
+ loading={imageLoading}
367
+ src={runtimeImageUrl}
368
+ style="display: block; height: 100%; object-fit: cover; width: 100%;"
369
+ />
370
+ {/if}
371
+ </div>
@@ -0,0 +1,8 @@
1
+ import type {SvelteComponentTyped} from 'svelte';
2
+ import type {Map as MapLibreMap} from 'maplibre-gl';
3
+ import type {TileflowMapProps} from './index';
4
+
5
+ export default class TileflowMap extends SvelteComponentTyped<
6
+ TileflowMapProps,
7
+ {load: CustomEvent<MapLibreMap>}
8
+ > {}
package/src/index.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ import type {Map as MapLibreMap, MapOptions as MapLibreMapOptions} from 'maplibre-gl';
2
+ import type {SvelteComponentTyped} from 'svelte';
3
+ import type {
4
+ MapLibreStyle,
5
+ TileflowAnalytics,
6
+ TileflowConfig,
7
+ TileflowMapMarker,
8
+ TileflowProjectThemes,
9
+ } from '@tileflow/core';
10
+
11
+ export type TileflowMapMode = 'interactive' | 'image';
12
+ export type TileflowMapOptions = Omit<MapLibreMapOptions, 'container' | 'style'>;
13
+
14
+ export type TileflowMapProps = {
15
+ alt?: string;
16
+ analytics?: TileflowAnalytics;
17
+ center?: [number, number];
18
+ className?: string;
19
+ config?: TileflowConfig;
20
+ height?: number | string;
21
+ imageLoading?: HTMLImageElement['loading'];
22
+ imageUrl?: string;
23
+ interactive?: boolean;
24
+ manifestUrl?: string;
25
+ map?: string;
26
+ mapOptions?: TileflowMapOptions;
27
+ mapStyle?: MapLibreStyle;
28
+ markers?: TileflowMapMarker[];
29
+ mode?: TileflowMapMode;
30
+ preferLocalDev?: boolean;
31
+ styleBaseUrl?: string;
32
+ styleUrl?: string;
33
+ themes?: TileflowProjectThemes;
34
+ tileBaseUrl?: string;
35
+ zoom?: number;
36
+ };
37
+
38
+ export default class TileflowMap extends SvelteComponentTyped<
39
+ TileflowMapProps,
40
+ {load: CustomEvent<MapLibreMap>}
41
+ > {}
42
+
43
+ export {TileflowMap};
package/src/index.js ADDED
@@ -0,0 +1 @@
1
+ export {default, default as TileflowMap} from './TileflowMap.svelte';