@tileflow/vue 0.1.0-alpha.9

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,18 @@
1
+ # @tileflow/vue
2
+
3
+ Vue 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
+ ```vue
8
+ <script setup lang="ts">
9
+ import {TileflowMap} from '@tileflow/vue';
10
+ import 'maplibre-gl/dist/maplibre-gl.css';
11
+ </script>
12
+
13
+ <template>
14
+ <TileflowMap map="madrid" :center="[-3.7038, 40.4168]" :zoom="12" />
15
+ </template>
16
+ ```
17
+
18
+ Docs: https://tileflow.dev/docs
@@ -0,0 +1,150 @@
1
+ import * as vue from 'vue';
2
+ import { PropType } from 'vue';
3
+ import maplibregl, { Map } from 'maplibre-gl';
4
+ import { TileflowAnalytics, TileflowConfig, MapLibreStyle, TileflowMapMarker, TileflowProjectThemes } from '@tileflow/core';
5
+
6
+ type TileflowMapMode = 'interactive' | 'image';
7
+ type TileflowMapProps = {
8
+ alt?: string;
9
+ analytics?: TileflowAnalytics;
10
+ center?: [number, number];
11
+ className?: string;
12
+ config?: TileflowConfig;
13
+ height?: number | string;
14
+ imageLoading?: HTMLImageElement['loading'];
15
+ imageUrl?: string;
16
+ interactive?: boolean;
17
+ manifestUrl?: string;
18
+ map?: string;
19
+ mapStyle?: MapLibreStyle;
20
+ markers?: TileflowMapMarker[];
21
+ mode?: TileflowMapMode;
22
+ preferLocalDev?: boolean;
23
+ styleBaseUrl?: string;
24
+ styleUrl?: string;
25
+ themes?: TileflowProjectThemes;
26
+ tileBaseUrl?: string;
27
+ zoom?: number;
28
+ };
29
+ declare const TileflowMap: vue.DefineComponent<vue.ExtractPropTypes<{
30
+ alt: {
31
+ default: string;
32
+ type: StringConstructor;
33
+ };
34
+ analytics: PropType<TileflowAnalytics>;
35
+ center: {
36
+ default: () => [number, number];
37
+ type: PropType<[number, number]>;
38
+ };
39
+ className: StringConstructor;
40
+ config: PropType<TileflowConfig>;
41
+ height: {
42
+ default: number;
43
+ type: PropType<number | string>;
44
+ };
45
+ imageLoading: {
46
+ default: string;
47
+ type: PropType<HTMLImageElement["loading"]>;
48
+ };
49
+ imageUrl: StringConstructor;
50
+ interactive: {
51
+ default: boolean;
52
+ type: BooleanConstructor;
53
+ };
54
+ manifestUrl: {
55
+ default: string;
56
+ type: StringConstructor;
57
+ };
58
+ map: StringConstructor;
59
+ mapStyle: PropType<MapLibreStyle>;
60
+ markers: {
61
+ default: () => never[];
62
+ type: PropType<TileflowMapMarker[]>;
63
+ };
64
+ mode: {
65
+ default: string;
66
+ type: PropType<TileflowMapMode>;
67
+ };
68
+ preferLocalDev: {
69
+ default: boolean;
70
+ type: BooleanConstructor;
71
+ };
72
+ styleBaseUrl: StringConstructor;
73
+ styleUrl: StringConstructor;
74
+ themes: PropType<TileflowProjectThemes>;
75
+ tileBaseUrl: StringConstructor;
76
+ zoom: {
77
+ default: number;
78
+ type: NumberConstructor;
79
+ };
80
+ }>, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
81
+ [key: string]: any;
82
+ }>, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {
83
+ load: (_map: Map) => true;
84
+ }, string, vue.PublicProps, Readonly<vue.ExtractPropTypes<{
85
+ alt: {
86
+ default: string;
87
+ type: StringConstructor;
88
+ };
89
+ analytics: PropType<TileflowAnalytics>;
90
+ center: {
91
+ default: () => [number, number];
92
+ type: PropType<[number, number]>;
93
+ };
94
+ className: StringConstructor;
95
+ config: PropType<TileflowConfig>;
96
+ height: {
97
+ default: number;
98
+ type: PropType<number | string>;
99
+ };
100
+ imageLoading: {
101
+ default: string;
102
+ type: PropType<HTMLImageElement["loading"]>;
103
+ };
104
+ imageUrl: StringConstructor;
105
+ interactive: {
106
+ default: boolean;
107
+ type: BooleanConstructor;
108
+ };
109
+ manifestUrl: {
110
+ default: string;
111
+ type: StringConstructor;
112
+ };
113
+ map: StringConstructor;
114
+ mapStyle: PropType<MapLibreStyle>;
115
+ markers: {
116
+ default: () => never[];
117
+ type: PropType<TileflowMapMarker[]>;
118
+ };
119
+ mode: {
120
+ default: string;
121
+ type: PropType<TileflowMapMode>;
122
+ };
123
+ preferLocalDev: {
124
+ default: boolean;
125
+ type: BooleanConstructor;
126
+ };
127
+ styleBaseUrl: StringConstructor;
128
+ styleUrl: StringConstructor;
129
+ themes: PropType<TileflowProjectThemes>;
130
+ tileBaseUrl: StringConstructor;
131
+ zoom: {
132
+ default: number;
133
+ type: NumberConstructor;
134
+ };
135
+ }>> & Readonly<{
136
+ onLoad?: ((_map: maplibregl.Map) => any) | undefined;
137
+ }>, {
138
+ interactive: boolean;
139
+ alt: string;
140
+ center: [number, number];
141
+ height: string | number;
142
+ imageLoading: "eager" | "lazy";
143
+ manifestUrl: string;
144
+ markers: TileflowMapMarker[];
145
+ mode: TileflowMapMode;
146
+ preferLocalDev: boolean;
147
+ zoom: number;
148
+ }, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
149
+
150
+ export { TileflowMap, type TileflowMapMode, type TileflowMapProps, TileflowMap as default };
package/dist/index.js ADDED
@@ -0,0 +1,357 @@
1
+ // src/index.ts
2
+ import maplibregl from "maplibre-gl";
3
+ import {
4
+ computed,
5
+ defineComponent,
6
+ h,
7
+ onBeforeUnmount,
8
+ onMounted,
9
+ ref,
10
+ shallowRef,
11
+ watch
12
+ } from "vue";
13
+ import {
14
+ createTileflowSessionId,
15
+ defaultTileflowManifestUrl,
16
+ loadTileflowManifest,
17
+ mergeTileflowAnalytics,
18
+ normalizeTileflowStaticImageSize,
19
+ resolveTileflowAnalyticsRequestUrl,
20
+ resolveTileflowManifestMap,
21
+ resolveTileflowRuntimeStyle,
22
+ resolveTileflowStaticImageUrl,
23
+ shouldLoadTileflowManifest,
24
+ startTileflowSession
25
+ } from "@tileflow/core";
26
+ var defaultCenter = [0, 20];
27
+ var TileflowMap = defineComponent({
28
+ name: "TileflowMap",
29
+ inheritAttrs: false,
30
+ props: {
31
+ alt: {
32
+ default: "",
33
+ type: String
34
+ },
35
+ analytics: Object,
36
+ center: {
37
+ default: () => defaultCenter,
38
+ type: Array
39
+ },
40
+ className: String,
41
+ config: Object,
42
+ height: {
43
+ default: 420,
44
+ type: [Number, String]
45
+ },
46
+ imageLoading: {
47
+ default: "eager",
48
+ type: String
49
+ },
50
+ imageUrl: String,
51
+ interactive: {
52
+ default: true,
53
+ type: Boolean
54
+ },
55
+ manifestUrl: {
56
+ default: defaultTileflowManifestUrl,
57
+ type: String
58
+ },
59
+ map: String,
60
+ mapStyle: Object,
61
+ markers: {
62
+ default: () => [],
63
+ type: Array
64
+ },
65
+ mode: {
66
+ default: "interactive",
67
+ type: String
68
+ },
69
+ preferLocalDev: {
70
+ default: true,
71
+ type: Boolean
72
+ },
73
+ styleBaseUrl: String,
74
+ styleUrl: String,
75
+ themes: Object,
76
+ tileBaseUrl: String,
77
+ zoom: {
78
+ default: 2,
79
+ type: Number
80
+ }
81
+ },
82
+ emits: {
83
+ load: (_map) => true
84
+ },
85
+ setup(props, { attrs, emit }) {
86
+ const containerRef = ref(null);
87
+ const manifestMap = shallowRef(null);
88
+ const imageSize = shallowRef(null);
89
+ const mapRef = shallowRef(null);
90
+ const sessionId = createTileflowSessionId();
91
+ const markerInstances = [];
92
+ const sessionStarts = /* @__PURE__ */ new Set();
93
+ let imageResizeObserver = null;
94
+ let mapResizeObserver = null;
95
+ let manifestLoadId = 0;
96
+ const isImageMode = computed(() => props.mode === "image");
97
+ const normalizedCenter = computed(() => props.center ?? defaultCenter);
98
+ const runtimeStyle = computed(
99
+ () => isImageMode.value ? null : resolveTileflowRuntimeStyle({
100
+ config: props.config,
101
+ manifestMap: manifestMap.value,
102
+ map: props.map,
103
+ preferLocalDev: props.preferLocalDev,
104
+ style: props.mapStyle,
105
+ styleBaseUrl: props.styleBaseUrl,
106
+ styleUrl: props.styleUrl,
107
+ themes: props.themes,
108
+ tileBaseUrl: props.tileBaseUrl
109
+ })
110
+ );
111
+ const resolvedAnalytics = computed(
112
+ () => mergeTileflowAnalytics(props.analytics, runtimeStyle.value?.analytics)
113
+ );
114
+ const runtimeImageUrl = computed(
115
+ () => props.imageUrl ?? (isImageMode.value ? resolveTileflowStaticImageUrl({
116
+ center: normalizedCenter.value,
117
+ imageSize: imageSize.value,
118
+ manifestMap: manifestMap.value,
119
+ zoom: props.zoom
120
+ }) : void 0)
121
+ );
122
+ const frameStyle = computed(() => ({
123
+ height: formatHeight(props.height),
124
+ minHeight: "240px",
125
+ overflow: "hidden",
126
+ position: "relative",
127
+ width: "100%"
128
+ }));
129
+ const refreshManifest = async () => {
130
+ const shouldLoad = shouldLoadTileflowManifest({
131
+ config: props.config,
132
+ imageMode: isImageMode.value,
133
+ imageUrl: props.imageUrl,
134
+ map: props.map,
135
+ style: props.mapStyle,
136
+ styleBaseUrl: props.styleBaseUrl,
137
+ styleUrl: props.styleUrl
138
+ });
139
+ if (!shouldLoad) {
140
+ manifestMap.value = null;
141
+ return;
142
+ }
143
+ const loadId = ++manifestLoadId;
144
+ manifestMap.value = null;
145
+ try {
146
+ const manifest = await loadTileflowManifest(props.manifestUrl);
147
+ if (loadId === manifestLoadId) {
148
+ manifestMap.value = manifest && props.map ? resolveTileflowManifestMap(manifest, props.map) : null;
149
+ }
150
+ } catch (error) {
151
+ if (loadId === manifestLoadId) {
152
+ console.error("Failed to load Tileflow manifest", error);
153
+ manifestMap.value = null;
154
+ }
155
+ }
156
+ };
157
+ const updateImageResizeObserver = () => {
158
+ imageResizeObserver?.disconnect();
159
+ imageResizeObserver = null;
160
+ imageSize.value = null;
161
+ if (!isImageMode.value || props.imageUrl || !containerRef.value) {
162
+ return;
163
+ }
164
+ const element = containerRef.value;
165
+ const updateImageSize = () => {
166
+ const nextSize = normalizeTileflowStaticImageSize({
167
+ height: element.clientHeight,
168
+ width: element.clientWidth
169
+ });
170
+ imageSize.value = imageSize.value?.height === nextSize.height && imageSize.value.width === nextSize.width ? imageSize.value : nextSize;
171
+ };
172
+ imageResizeObserver = new ResizeObserver(updateImageSize);
173
+ imageResizeObserver.observe(element);
174
+ updateImageSize();
175
+ };
176
+ const destroyMap = () => {
177
+ mapResizeObserver?.disconnect();
178
+ mapResizeObserver = null;
179
+ clearMarkers(markerInstances);
180
+ mapRef.value?.remove();
181
+ mapRef.value = null;
182
+ };
183
+ const recreateMap = () => {
184
+ destroyMap();
185
+ if (!containerRef.value || !runtimeStyle.value || isImageMode.value) {
186
+ return;
187
+ }
188
+ const runtime = runtimeStyle.value;
189
+ const map = new maplibregl.Map({
190
+ attributionControl: { compact: true },
191
+ center: normalizedCenter.value,
192
+ container: containerRef.value,
193
+ interactive: props.interactive,
194
+ style: runtime.style,
195
+ transformRequest: createTransformRequest(resolvedAnalytics.value, sessionId),
196
+ zoom: props.zoom
197
+ });
198
+ mapRef.value = map;
199
+ if (props.interactive) {
200
+ map.addControl(new maplibregl.NavigationControl(), "top-right");
201
+ }
202
+ mapResizeObserver = new ResizeObserver(() => {
203
+ map.resize();
204
+ });
205
+ mapResizeObserver.observe(containerRef.value);
206
+ map.on("load", () => {
207
+ const analyticsForLoad = resolvedAnalytics.value;
208
+ const styleId = analyticsForLoad?.styleId ?? (typeof runtime.style === "string" ? runtime.style : props.map);
209
+ emit("load", map);
210
+ if (analyticsForLoad?.mapId) {
211
+ const sessionKey = `${sessionId}:${analyticsForLoad.mapId}:${styleId ?? ""}`;
212
+ if (!sessionStarts.has(sessionKey)) {
213
+ sessionStarts.add(sessionKey);
214
+ startTileflowSession(analyticsForLoad, {
215
+ mapId: analyticsForLoad.mapId,
216
+ sessionId,
217
+ source: "vue",
218
+ styleId
219
+ });
220
+ }
221
+ }
222
+ });
223
+ syncMarkers();
224
+ };
225
+ const syncView = () => {
226
+ mapRef.value?.jumpTo({
227
+ center: normalizedCenter.value,
228
+ zoom: props.zoom
229
+ });
230
+ };
231
+ const syncMarkers = () => {
232
+ const map = mapRef.value;
233
+ clearMarkers(markerInstances);
234
+ if (!map) {
235
+ return;
236
+ }
237
+ for (const marker of props.markers) {
238
+ const markerInstance = new maplibregl.Marker({
239
+ color: marker.color ?? "#C6A15B"
240
+ }).setLngLat(marker.coordinates).addTo(map);
241
+ markerInstance.getElement().title = marker.label ?? marker.id;
242
+ markerInstances.push(markerInstance);
243
+ }
244
+ };
245
+ onMounted(() => {
246
+ void refreshManifest();
247
+ updateImageResizeObserver();
248
+ recreateMap();
249
+ });
250
+ onBeforeUnmount(() => {
251
+ imageResizeObserver?.disconnect();
252
+ destroyMap();
253
+ });
254
+ watch(
255
+ () => [
256
+ props.config,
257
+ props.imageUrl,
258
+ props.manifestUrl,
259
+ props.map,
260
+ props.mapStyle,
261
+ props.mode,
262
+ props.styleBaseUrl,
263
+ props.styleUrl
264
+ ],
265
+ () => {
266
+ void refreshManifest();
267
+ }
268
+ );
269
+ watch(
270
+ () => [props.imageUrl, props.mode],
271
+ () => updateImageResizeObserver(),
272
+ { flush: "post" }
273
+ );
274
+ watch(
275
+ () => [
276
+ props.config,
277
+ props.interactive,
278
+ props.map,
279
+ props.mapStyle,
280
+ props.mode,
281
+ props.preferLocalDev,
282
+ props.styleBaseUrl,
283
+ props.styleUrl,
284
+ props.themes,
285
+ props.tileBaseUrl,
286
+ props.zoom,
287
+ manifestMap.value
288
+ ],
289
+ () => recreateMap(),
290
+ { flush: "post" }
291
+ );
292
+ watch(
293
+ () => [props.center, props.zoom],
294
+ () => syncView(),
295
+ { flush: "post" }
296
+ );
297
+ watch(
298
+ () => props.markers,
299
+ () => syncMarkers(),
300
+ { flush: "post" }
301
+ );
302
+ return () => {
303
+ const rootAttrs = { ...attrs };
304
+ const classValue = [attrs.class, props.className];
305
+ const styleValue = [attrs.style, frameStyle.value];
306
+ delete rootAttrs.class;
307
+ delete rootAttrs.style;
308
+ return h(
309
+ "div",
310
+ {
311
+ ...rootAttrs,
312
+ class: classValue,
313
+ ref: containerRef,
314
+ style: styleValue
315
+ },
316
+ isImageMode.value && runtimeImageUrl.value ? h("img", {
317
+ alt: props.alt,
318
+ decoding: "async",
319
+ loading: props.imageLoading,
320
+ src: runtimeImageUrl.value,
321
+ style: {
322
+ display: "block",
323
+ height: "100%",
324
+ objectFit: "cover",
325
+ width: "100%"
326
+ }
327
+ }) : void 0
328
+ );
329
+ };
330
+ }
331
+ });
332
+ var index_default = TileflowMap;
333
+ function createTransformRequest(analytics, sessionId) {
334
+ if (!analytics || analytics.enabled === false) {
335
+ return void 0;
336
+ }
337
+ return (url) => {
338
+ const nextUrl = resolveTileflowAnalyticsRequestUrl(url, analytics, sessionId);
339
+ if (!nextUrl) {
340
+ return void 0;
341
+ }
342
+ return { url: nextUrl };
343
+ };
344
+ }
345
+ function formatHeight(height) {
346
+ return typeof height === "number" ? `${height}px` : height;
347
+ }
348
+ function clearMarkers(markerInstances) {
349
+ for (const marker of markerInstances) {
350
+ marker.remove();
351
+ }
352
+ markerInstances.splice(0);
353
+ }
354
+ export {
355
+ TileflowMap,
356
+ index_default as default
357
+ };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@tileflow/vue",
3
+ "version": "0.1.0-alpha.9",
4
+ "description": "Vue component for rendering Tileflow maps with MapLibre.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "homepage": "https://tileflow.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/tileflow/tileflow.git",
11
+ "directory": "packages/vue"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/tileflow/tileflow/issues"
15
+ },
16
+ "keywords": [
17
+ "tileflow",
18
+ "vue",
19
+ "maps",
20
+ "maplibre",
21
+ "tiles"
22
+ ],
23
+ "main": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js",
32
+ "default": "./dist/index.js"
33
+ }
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "dependencies": {
39
+ "@tileflow/core": "0.1.0-alpha.9"
40
+ },
41
+ "peerDependencies": {
42
+ "maplibre-gl": ">=5",
43
+ "vue": ">=3.3"
44
+ },
45
+ "devDependencies": {
46
+ "maplibre-gl": "^5.24.0",
47
+ "vue": "^3.5.26"
48
+ },
49
+ "scripts": {
50
+ "build": "tsup src/index.ts --format esm --dts --clean --external vue --external maplibre-gl",
51
+ "typecheck": "tsc -p tsconfig.json --noEmit",
52
+ "lint": "tsc -p tsconfig.json --noEmit"
53
+ }
54
+ }