@craft-native/ts-maps 0.0.37
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/LICENSE.md +21 -0
- package/README.md +76 -0
- package/package.json +61 -0
- package/src/FilesystemTileBackend.ts +243 -0
- package/src/MapView.ts +428 -0
- package/src/adapters.ts +221 -0
- package/src/bridge-protocol.ts +75 -0
- package/src/capabilities.ts +99 -0
- package/src/index.test.ts +109 -0
- package/src/index.ts +109 -0
- package/src/offlineRegion.ts +93 -0
- package/src/types.ts +237 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared protocol constants and types for the bridge between the Zig
|
|
3
|
+
* `MapView` core and the ts-maps runtime.
|
|
4
|
+
*
|
|
5
|
+
* @module @craft-native/ts-maps/bridge-protocol
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
Coordinate,
|
|
10
|
+
MapCamera,
|
|
11
|
+
MapCircle,
|
|
12
|
+
MapConfiguration,
|
|
13
|
+
MapMarker,
|
|
14
|
+
MapPolygon,
|
|
15
|
+
MapPolyline,
|
|
16
|
+
MapRegion,
|
|
17
|
+
} from './types'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Namespace used when emitting and listening for bridge messages. Matches
|
|
21
|
+
* the Zig enum tag for `MapProvider.ts_maps`.
|
|
22
|
+
*/
|
|
23
|
+
export const BRIDGE_NAMESPACE = 'ts_maps' as const
|
|
24
|
+
|
|
25
|
+
/** Method names the Zig core can invoke on the TypeScript side. */
|
|
26
|
+
export type MapBridgeMethod =
|
|
27
|
+
| 'setCamera'
|
|
28
|
+
| 'setRegion'
|
|
29
|
+
| 'addMarker'
|
|
30
|
+
| 'removeMarker'
|
|
31
|
+
| 'addPolyline'
|
|
32
|
+
| 'addPolygon'
|
|
33
|
+
| 'addCircle'
|
|
34
|
+
| 'fitToMarkers'
|
|
35
|
+
| 'selectMarker'
|
|
36
|
+
| 'deselectAll'
|
|
37
|
+
| 'setConfiguration'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Typed request envelope. The `method` discriminates the `params` shape.
|
|
41
|
+
* The union below gives per-method type narrowing.
|
|
42
|
+
*/
|
|
43
|
+
export interface MapBridgeRequest {
|
|
44
|
+
method: MapBridgeMethod
|
|
45
|
+
params: Record<string, unknown>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Strongly typed request variants for each supported method. */
|
|
49
|
+
export type TypedMapBridgeRequest =
|
|
50
|
+
| { method: 'setCamera', params: { camera: MapCamera, animated?: boolean } }
|
|
51
|
+
| { method: 'setRegion', params: { region: MapRegion, animated?: boolean } }
|
|
52
|
+
| { method: 'addMarker', params: { marker: MapMarker } }
|
|
53
|
+
| { method: 'removeMarker', params: { id: number } }
|
|
54
|
+
| { method: 'addPolyline', params: { polyline: MapPolyline } }
|
|
55
|
+
| { method: 'addPolygon', params: { polygon: MapPolygon } }
|
|
56
|
+
| { method: 'addCircle', params: { circle: MapCircle } }
|
|
57
|
+
| { method: 'fitToMarkers', params: { padding?: number } }
|
|
58
|
+
| { method: 'selectMarker', params: { id: number } }
|
|
59
|
+
| { method: 'deselectAll', params: Record<string, never> }
|
|
60
|
+
| { method: 'setConfiguration', params: { configuration: MapConfiguration } }
|
|
61
|
+
|
|
62
|
+
/** Events emitted by the ts-maps runtime back to the Zig core. */
|
|
63
|
+
export type MapBridgeEvent =
|
|
64
|
+
| { type: 'tap', coordinate: Coordinate }
|
|
65
|
+
| { type: 'markerTap', markerId: number, coordinate: Coordinate }
|
|
66
|
+
| { type: 'regionChanged', region: MapRegion }
|
|
67
|
+
| { type: 'cameraChanged', camera: MapCamera }
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build the full event name used on the bridge (e.g. `map:tap`).
|
|
71
|
+
* Centralized so both sides agree on the exact string.
|
|
72
|
+
*/
|
|
73
|
+
export function mapEventName(type: MapBridgeEvent['type']): string {
|
|
74
|
+
return `map:${type}`
|
|
75
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime capability probe for Craft apps hosting `ts-maps` inside a
|
|
3
|
+
* WebView. The map renderer prefers WebGL2 and falls back to Canvas2D
|
|
4
|
+
* when it isn't available; this helper exposes the probe so the host
|
|
5
|
+
* app can pick the right renderer up front (and surface the decision to
|
|
6
|
+
* users on older devices).
|
|
7
|
+
*
|
|
8
|
+
* All checks are side-effect-free and safe to call during render: the
|
|
9
|
+
* probe creates a throw-away canvas, asks for the relevant context, and
|
|
10
|
+
* discards it.
|
|
11
|
+
*
|
|
12
|
+
* @module @craft-native/ts-maps/capabilities
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface MapRendererCapabilities {
|
|
16
|
+
/** WebGL2 context is obtainable. */
|
|
17
|
+
webgl2: boolean
|
|
18
|
+
/** WebGL1 context is obtainable (fallback). */
|
|
19
|
+
webgl1: boolean
|
|
20
|
+
/** `OffscreenCanvas` exists on the global scope. */
|
|
21
|
+
offscreenCanvas: boolean
|
|
22
|
+
/** `navigator.hardwareConcurrency` — tile-worker parallelism hint. */
|
|
23
|
+
hardwareConcurrency: number
|
|
24
|
+
/** `window.devicePixelRatio` — retina-like displays report >1. */
|
|
25
|
+
devicePixelRatio: number
|
|
26
|
+
/** Pointer events are supported (required for the two-finger gestures). */
|
|
27
|
+
pointerEvents: boolean
|
|
28
|
+
/** Touch events are supported (used as a pointer-event fallback). */
|
|
29
|
+
touchEvents: boolean
|
|
30
|
+
/** Best renderer given the probe result. */
|
|
31
|
+
preferredRenderer: 'webgl' | 'canvas2d'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Probe the host runtime. Safe to call in any environment — when neither
|
|
36
|
+
* `window` nor `document` is available (Node / SSR) the probe returns a
|
|
37
|
+
* conservative zero-capabilities object.
|
|
38
|
+
*/
|
|
39
|
+
export function probeCapabilities(): MapRendererCapabilities {
|
|
40
|
+
if (typeof document === 'undefined' || typeof window === 'undefined') {
|
|
41
|
+
return {
|
|
42
|
+
webgl2: false,
|
|
43
|
+
webgl1: false,
|
|
44
|
+
offscreenCanvas: false,
|
|
45
|
+
hardwareConcurrency: 1,
|
|
46
|
+
devicePixelRatio: 1,
|
|
47
|
+
pointerEvents: false,
|
|
48
|
+
touchEvents: false,
|
|
49
|
+
preferredRenderer: 'canvas2d',
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const canvas = document.createElement('canvas')
|
|
54
|
+
// Narrow-scope try/catch per context kind — some browsers throw
|
|
55
|
+
// `SecurityError` when a context type is blocked by a permission policy.
|
|
56
|
+
let webgl2 = false
|
|
57
|
+
let webgl1 = false
|
|
58
|
+
try {
|
|
59
|
+
const gl2 = canvas.getContext('webgl2')
|
|
60
|
+
webgl2 = gl2 !== null && gl2 !== undefined
|
|
61
|
+
}
|
|
62
|
+
catch { /* webgl2 unavailable */ }
|
|
63
|
+
if (!webgl2) {
|
|
64
|
+
try {
|
|
65
|
+
const gl1 = canvas.getContext('webgl') ?? canvas.getContext('experimental-webgl')
|
|
66
|
+
webgl1 = gl1 !== null && gl1 !== undefined
|
|
67
|
+
}
|
|
68
|
+
catch { /* webgl1 unavailable */ }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const offscreenCanvas = typeof (globalThis as any).OffscreenCanvas === 'function'
|
|
72
|
+
const nav = (globalThis as any).navigator as Navigator | undefined
|
|
73
|
+
const hardwareConcurrency = Math.max(1, Math.floor(nav?.hardwareConcurrency ?? 1))
|
|
74
|
+
const devicePixelRatio = Math.max(1, (window as any).devicePixelRatio ?? 1)
|
|
75
|
+
const pointerEvents = typeof (globalThis as any).PointerEvent === 'function'
|
|
76
|
+
const touchEvents = 'ontouchstart' in window
|
|
77
|
+
|| (nav !== undefined && (nav as any).maxTouchPoints > 0)
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
webgl2,
|
|
81
|
+
webgl1,
|
|
82
|
+
offscreenCanvas,
|
|
83
|
+
hardwareConcurrency,
|
|
84
|
+
devicePixelRatio,
|
|
85
|
+
pointerEvents,
|
|
86
|
+
touchEvents,
|
|
87
|
+
preferredRenderer: webgl2 ? 'webgl' : 'canvas2d',
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* True when the probe thinks this host can run the WebGL-backed renderer
|
|
93
|
+
* comfortably. Callers typically use this to gate advanced features like
|
|
94
|
+
* fill-extrusion or hillshade on cheap older phones.
|
|
95
|
+
*/
|
|
96
|
+
export function supportsAdvancedRendering(caps?: MapRendererCapabilities): boolean {
|
|
97
|
+
const probe = caps ?? probeCapabilities()
|
|
98
|
+
return probe.webgl2 && probe.hardwareConcurrency >= 2 && probe.pointerEvents
|
|
99
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke tests for `@craft-native/ts-maps`. Runs under Bun's `bun:test`
|
|
3
|
+
* with `very-happy-dom` providing a DOM shim (see `test/preload.ts`).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from 'bun:test'
|
|
7
|
+
import {
|
|
8
|
+
cameraToCamera,
|
|
9
|
+
coordinateFromLatLng,
|
|
10
|
+
craftCoordToLatLng,
|
|
11
|
+
latLngFromCoordinate,
|
|
12
|
+
latLngToCraftCoord,
|
|
13
|
+
regionFromBounds,
|
|
14
|
+
tsMapsProvider,
|
|
15
|
+
} from './index'
|
|
16
|
+
import { LatLng } from 'ts-maps'
|
|
17
|
+
|
|
18
|
+
describe('tsMapsProvider', () => {
|
|
19
|
+
it('matches the Zig MapProvider.ts_maps tag', () => {
|
|
20
|
+
expect(tsMapsProvider).toBe('ts_maps')
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('adapters.craftCoordToLatLng', () => {
|
|
25
|
+
it('produces a LatLng at the right coordinates', () => {
|
|
26
|
+
const ll = craftCoordToLatLng({ latitude: 40, longitude: -74 })
|
|
27
|
+
expect(ll).toBeInstanceOf(LatLng)
|
|
28
|
+
expect(ll.lat).toBe(40)
|
|
29
|
+
expect(ll.lng).toBe(-74)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('round-trips losslessly via latLngToCraftCoord', () => {
|
|
33
|
+
const coord = { latitude: 37.7749, longitude: -122.4194 }
|
|
34
|
+
const back = latLngToCraftCoord(craftCoordToLatLng(coord))
|
|
35
|
+
expect(back.latitude).toBeCloseTo(coord.latitude, 10)
|
|
36
|
+
expect(back.longitude).toBeCloseTo(coord.longitude, 10)
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('spec-named aliases', () => {
|
|
41
|
+
it('latLngFromCoordinate is an alias for craftCoordToLatLng', () => {
|
|
42
|
+
const ll = latLngFromCoordinate({ latitude: 10, longitude: 20 })
|
|
43
|
+
expect(ll.lat).toBe(10)
|
|
44
|
+
expect(ll.lng).toBe(20)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('coordinateFromLatLng is an alias for latLngToCraftCoord', () => {
|
|
48
|
+
const coord = coordinateFromLatLng(new LatLng(1, 2))
|
|
49
|
+
expect(coord.latitude).toBe(1)
|
|
50
|
+
expect(coord.longitude).toBe(2)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('adapters.cameraToCamera', () => {
|
|
55
|
+
it('converts camera fields and re-maps heading -> bearing', () => {
|
|
56
|
+
const opts = cameraToCamera({
|
|
57
|
+
center: { latitude: 10, longitude: 20 },
|
|
58
|
+
zoom: 12,
|
|
59
|
+
pitch: 45,
|
|
60
|
+
heading: 90,
|
|
61
|
+
})
|
|
62
|
+
expect(opts.zoom).toBe(12)
|
|
63
|
+
expect(opts.pitch).toBe(45)
|
|
64
|
+
expect(opts.bearing).toBe(90)
|
|
65
|
+
expect(opts.center.lat).toBe(10)
|
|
66
|
+
expect(opts.center.lng).toBe(20)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('regionFromBounds', () => {
|
|
71
|
+
it('returns null for an empty list', () => {
|
|
72
|
+
expect(regionFromBounds([])).toBeNull()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('centers on the midpoint and spans the delta', () => {
|
|
76
|
+
const region = regionFromBounds([
|
|
77
|
+
{ latitude: 0, longitude: 0 },
|
|
78
|
+
{ latitude: 10, longitude: 10 },
|
|
79
|
+
])
|
|
80
|
+
expect(region).not.toBeNull()
|
|
81
|
+
expect(region!.center.latitude).toBe(5)
|
|
82
|
+
expect(region!.center.longitude).toBe(5)
|
|
83
|
+
expect(region!.span.latitude_delta).toBe(10)
|
|
84
|
+
expect(region!.span.longitude_delta).toBe(10)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('createMapView', () => {
|
|
89
|
+
// The DOM shim occasionally has loading issues (see preload.ts); if
|
|
90
|
+
// `document` isn't available we skip this block rather than fail.
|
|
91
|
+
const hasDom = typeof document !== 'undefined'
|
|
92
|
+
const maybe = hasDom ? it : it.skip
|
|
93
|
+
|
|
94
|
+
maybe('returns an instance with id, container, and map', async () => {
|
|
95
|
+
// Dynamic import keeps the module from being evaluated when the DOM
|
|
96
|
+
// shim couldn't register, which avoids noisy module-load errors in
|
|
97
|
+
// environments where `very-happy-dom` fails to bootstrap.
|
|
98
|
+
const { createMapView } = await import('./MapView')
|
|
99
|
+
const instance = createMapView()
|
|
100
|
+
expect(typeof instance.id).toBe('string')
|
|
101
|
+
// `very-happy-dom` returns a `VirtualElement` proxying `HTMLDivElement`,
|
|
102
|
+
// so `instanceof HTMLDivElement` fails. Feature-check the shape instead.
|
|
103
|
+
expect(instance.container).toBeDefined()
|
|
104
|
+
expect(typeof instance.container.appendChild).toBe('function')
|
|
105
|
+
expect(instance.container.tagName).toBe('DIV')
|
|
106
|
+
expect(instance.map).toBeDefined()
|
|
107
|
+
instance.destroy()
|
|
108
|
+
})
|
|
109
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@craft-native/ts-maps` — SDK binding that lets Craft apps render
|
|
3
|
+
* interactive maps via the in-house `ts-maps` runtime. Pairs with the
|
|
4
|
+
* `MapProvider.ts_maps` variant in `packages/zig/src/maps.zig`.
|
|
5
|
+
*
|
|
6
|
+
* Re-exports the full ts-maps public API plus Craft-native helpers
|
|
7
|
+
* (`createMapView`, type mirrors, adapters, bridge protocol).
|
|
8
|
+
*
|
|
9
|
+
* @module @craft-native/ts-maps
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Re-export the full ts-maps runtime so consumers only need to install a
|
|
13
|
+
// single package. Consumers can `import { TsMap, marker } from '@craft-native/ts-maps'`.
|
|
14
|
+
export * from 'ts-maps'
|
|
15
|
+
|
|
16
|
+
// Craft-native MapView component.
|
|
17
|
+
export {
|
|
18
|
+
createMapView,
|
|
19
|
+
type ComponentProps,
|
|
20
|
+
type MapViewInstance,
|
|
21
|
+
type MapViewProps,
|
|
22
|
+
} from './MapView'
|
|
23
|
+
|
|
24
|
+
// Type mirrors of the Zig structs.
|
|
25
|
+
export {
|
|
26
|
+
defaultMapCamera,
|
|
27
|
+
defaultMapConfiguration,
|
|
28
|
+
markerColorHex,
|
|
29
|
+
strokePatternDashes,
|
|
30
|
+
type BoundingBox,
|
|
31
|
+
type Coordinate,
|
|
32
|
+
type CoordinateSpan,
|
|
33
|
+
type MapCamera,
|
|
34
|
+
type MapCircle,
|
|
35
|
+
type MapConfiguration,
|
|
36
|
+
type MapEvent,
|
|
37
|
+
type MapMarker,
|
|
38
|
+
type MapPolygon,
|
|
39
|
+
type MapPolyline,
|
|
40
|
+
type MapProvider,
|
|
41
|
+
type MapRegion,
|
|
42
|
+
type MapType,
|
|
43
|
+
type MarkerColor,
|
|
44
|
+
type StrokePattern,
|
|
45
|
+
type UserTrackingMode,
|
|
46
|
+
} from './types'
|
|
47
|
+
|
|
48
|
+
// Adapters (pure fns) for round-tripping between Craft and ts-maps types.
|
|
49
|
+
export {
|
|
50
|
+
boundsToCraftRegion,
|
|
51
|
+
cameraToCamera,
|
|
52
|
+
craftBoundingBoxToBounds,
|
|
53
|
+
craftCameraToOptions,
|
|
54
|
+
craftCircleToCircle,
|
|
55
|
+
craftCoordToLatLng,
|
|
56
|
+
craftMarkerToMarker,
|
|
57
|
+
craftPolygonToPolygon,
|
|
58
|
+
craftPolylineToPolyline,
|
|
59
|
+
craftRegionToBounds,
|
|
60
|
+
latLngToCraftCoord,
|
|
61
|
+
regionFromBounds,
|
|
62
|
+
type TsMapsCameraOptions,
|
|
63
|
+
} from './adapters'
|
|
64
|
+
|
|
65
|
+
// Bridge protocol constants/types.
|
|
66
|
+
export {
|
|
67
|
+
BRIDGE_NAMESPACE,
|
|
68
|
+
mapEventName,
|
|
69
|
+
type MapBridgeEvent,
|
|
70
|
+
type MapBridgeMethod,
|
|
71
|
+
type MapBridgeRequest,
|
|
72
|
+
type TypedMapBridgeRequest,
|
|
73
|
+
} from './bridge-protocol'
|
|
74
|
+
|
|
75
|
+
// Sandbox-filesystem tile persistence + offline region helpers.
|
|
76
|
+
export {
|
|
77
|
+
createFilesystemBackend,
|
|
78
|
+
type FilesystemBackendOptions,
|
|
79
|
+
} from './FilesystemTileBackend'
|
|
80
|
+
export {
|
|
81
|
+
createFilesystemTileCache,
|
|
82
|
+
saveOfflineRegionToFilesystem,
|
|
83
|
+
type FilesystemOfflineRegionOptions,
|
|
84
|
+
} from './offlineRegion'
|
|
85
|
+
|
|
86
|
+
// Runtime capability probe — tells the host app which renderer the
|
|
87
|
+
// device can realistically drive.
|
|
88
|
+
export {
|
|
89
|
+
probeCapabilities,
|
|
90
|
+
supportsAdvancedRendering,
|
|
91
|
+
type MapRendererCapabilities,
|
|
92
|
+
} from './capabilities'
|
|
93
|
+
|
|
94
|
+
// createMap is the underlying ts-maps factory; re-export under its original
|
|
95
|
+
// name for parity with the spec which mentions `createMap` as a Craft export.
|
|
96
|
+
export { createMap } from 'ts-maps'
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Provider tag matching `MapProvider.ts_maps` in the Zig core. Useful when
|
|
100
|
+
* building a `MapConfiguration` programmatically without magic strings.
|
|
101
|
+
*/
|
|
102
|
+
export const tsMapsProvider = 'ts_maps' as const
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Alias preserved for callers that imported the two helpers by spec name.
|
|
106
|
+
* `coordinateFromLatLng(LatLng) -> Coordinate`, `latLngFromCoordinate(Coordinate) -> LatLng`.
|
|
107
|
+
*/
|
|
108
|
+
export { latLngToCraftCoord as coordinateFromLatLng } from './adapters'
|
|
109
|
+
export { craftCoordToLatLng as latLngFromCoordinate } from './adapters'
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin Craft-specific wrapper around ts-maps' `saveOfflineRegion` that
|
|
3
|
+
* writes tile bytes to the app's sandboxed filesystem via the
|
|
4
|
+
* {@link createFilesystemBackend} adapter.
|
|
5
|
+
*
|
|
6
|
+
* Typical usage from a Craft app:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { saveOfflineRegionToFilesystem } from '@craft-native/ts-maps'
|
|
10
|
+
* import { app } from 'craft-native'
|
|
11
|
+
*
|
|
12
|
+
* await saveOfflineRegionToFilesystem({
|
|
13
|
+
* baseDir: `${await app.dataDir()}/tiles`,
|
|
14
|
+
* tileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
|
15
|
+
* bounds: mapViewBounds,
|
|
16
|
+
* zoomRange: [10, 14],
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* The download runs with bounded concurrency and respects `AbortSignal`,
|
|
21
|
+
* so a user canceling the region pre-cache tears the job down cleanly.
|
|
22
|
+
*
|
|
23
|
+
* @module @craft-native/ts-maps/offlineRegion
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type {
|
|
27
|
+
OfflineRegionResult,
|
|
28
|
+
TileCacheOptions,
|
|
29
|
+
} from 'ts-maps'
|
|
30
|
+
import { saveOfflineRegion, TileCache } from 'ts-maps'
|
|
31
|
+
import { createFilesystemBackend } from './FilesystemTileBackend'
|
|
32
|
+
|
|
33
|
+
export type OfflineBoundsLike =
|
|
34
|
+
| { west: number, south: number, east: number, north: number }
|
|
35
|
+
| readonly [west: number, south: number, east: number, north: number]
|
|
36
|
+
|
|
37
|
+
export interface OfflineProgressEmitter {
|
|
38
|
+
fire: (type: string, data?: Record<string, unknown>) => unknown
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface FilesystemOfflineRegionOptions {
|
|
42
|
+
/** Sandbox directory for tile files. Created on first write. */
|
|
43
|
+
baseDir: string
|
|
44
|
+
/** Tile URL template — `{z}/{x}/{y}` placeholders are substituted. */
|
|
45
|
+
tileUrl: string
|
|
46
|
+
bounds: OfflineBoundsLike
|
|
47
|
+
zoomRange: [minZ: number, maxZ: number]
|
|
48
|
+
concurrency?: number
|
|
49
|
+
signal?: AbortSignal
|
|
50
|
+
/** Optional overrides for the `TileCache` wrapping the filesystem backend. */
|
|
51
|
+
cacheOptions?: Omit<TileCacheOptions, 'backend'>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Pre-download every tile inside `bounds` across `zoomRange` and persist
|
|
56
|
+
* it to disk under `baseDir`. Returns counts of saved / cached-already /
|
|
57
|
+
* failed tiles.
|
|
58
|
+
*/
|
|
59
|
+
export function saveOfflineRegionToFilesystem(
|
|
60
|
+
opts: FilesystemOfflineRegionOptions,
|
|
61
|
+
emitter?: OfflineProgressEmitter,
|
|
62
|
+
): Promise<OfflineRegionResult> {
|
|
63
|
+
const cache = new TileCache({
|
|
64
|
+
...opts.cacheOptions,
|
|
65
|
+
backend: createFilesystemBackend({ baseDir: opts.baseDir }),
|
|
66
|
+
})
|
|
67
|
+
return saveOfflineRegion(
|
|
68
|
+
{
|
|
69
|
+
bounds: opts.bounds,
|
|
70
|
+
zoomRange: opts.zoomRange,
|
|
71
|
+
tileUrl: opts.tileUrl,
|
|
72
|
+
concurrency: opts.concurrency,
|
|
73
|
+
signal: opts.signal,
|
|
74
|
+
cache,
|
|
75
|
+
},
|
|
76
|
+
emitter,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build a standalone {@link TileCache} whose persistence layer is the
|
|
82
|
+
* Craft sandbox filesystem. Useful when a map instance needs to serve
|
|
83
|
+
* pre-saved tiles at runtime (not just pre-download them).
|
|
84
|
+
*/
|
|
85
|
+
export function createFilesystemTileCache(
|
|
86
|
+
baseDir: string,
|
|
87
|
+
cacheOptions?: Omit<TileCacheOptions, 'backend'>,
|
|
88
|
+
): TileCache {
|
|
89
|
+
return new TileCache({
|
|
90
|
+
...cacheOptions,
|
|
91
|
+
backend: createFilesystemBackend({ baseDir }),
|
|
92
|
+
})
|
|
93
|
+
}
|