lecodes-sdk 0.19.1 → 0.20.0
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/dist/global.d.ts +31 -0
- package/dist/host.d.ts +3 -0
- package/dist/inject.js +260 -361
- package/dist/types/gl/Locomotion.d.ts +3 -1
- package/dist/types/gl/animation/Animator.d.ts +51 -183
- package/dist/types/gl/animation/Feet.d.ts +85 -0
- package/dist/types/gl/animation/Warp.d.ts +53 -0
- package/dist/types/gl/animation/core.d.ts +46 -7
- package/dist/types/inject.d.ts +7 -2
- package/dist/types/plugins/map.d.ts +174 -0
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/dist/2d-game.md +197 -408
- package/prompts/dist/3d-app.md +166 -491
- package/prompts/dist/ar-app.md +163 -373
- package/prompts/dist/design.md +87 -83
- package/prompts/dist/ui-app.md +136 -325
- package/src/bridges.d.ts +13 -6
- package/src/compile/__tests__/compile.test.ts +12 -0
- package/src/compile/compileProject.ts +5 -0
- package/src/core/Aspect.ts +1 -1
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/Locomotion.ts +7 -5
- package/src/gl/Particles.ts +3 -3
- package/src/gl/Ragdoll.ts +270 -270
- package/src/gl/Scene.ts +1 -1
- package/src/gl/Trigger.ts +45 -45
- package/src/gl/animation/Animator.ts +137 -329
- package/src/gl/animation/Feet.ts +134 -0
- package/src/gl/animation/Loop.ts +3 -1
- package/src/gl/animation/Warp.ts +96 -0
- package/src/gl/animation/core.ts +736 -670
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/state.ts +6 -6
- package/src/host.d.ts +3 -0
- package/src/inject.ts +11 -2
- package/src/plugins/map.ts +396 -0
- package/src/scene/gizmos.ts +148 -148
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
// MapView — a vector map (maplibre-native on iOS/Android) as a Presentable. A typed wrapper over
|
|
2
|
+
// the `NativeView("map")` capability (docs/map-plugin-plan.md): push it (`Router.push(map)`),
|
|
3
|
+
// open it fullscreen (`map.open()`), or embed it among a screen's children where the host builds
|
|
4
|
+
// the "native" layout node. The style is a URL, a bundled `asset()`, a read file or the style
|
|
5
|
+
// object (see MapStyle). Data goes in as named layers (`map.markers(name)`, `map.line(name)`)
|
|
6
|
+
// whose `set()` replaces their content; the camera is driven with `flyTo` / `fitPoints`; the user
|
|
7
|
+
// puck is fed by the app (`setUserLocation`) — the plugin never touches the location hardware.
|
|
8
|
+
//
|
|
9
|
+
// Host-OPTIONAL: it exists where the host registers a "map" view (the `map` plugin from
|
|
10
|
+
// lecodes-plugins) — gate on `MapView.isSupported`. Everything here is sugar over ONE JSON channel
|
|
11
|
+
// (`call` / `on`); the wire contract is the method/event table in docs/map-plugin-plan.md.
|
|
12
|
+
//
|
|
13
|
+
// Ordering: the native map exists only once presented and its style loaded — the plugin emits
|
|
14
|
+
// `ready` then. Data and camera calls issued before that are queued and replayed in order, so an
|
|
15
|
+
// app can build the map declaratively and push it later without a ready-dance of its own.
|
|
16
|
+
|
|
17
|
+
import { NativeView, NativeViewElement } from "../ui/NativeView"
|
|
18
|
+
import { _channelOn } from "../runtime/channel"
|
|
19
|
+
import type { FetchResponse } from "../runtime/fetch"
|
|
20
|
+
|
|
21
|
+
/** The ready-made styles a map falls back on — [OpenFreeMap](https://openfreemap.org): OSM data,
|
|
22
|
+
* no key, no registration, no request limits, commercial use allowed, and the whole stack is
|
|
23
|
+
* open-source if you'd rather self-host. `liberty` is the default. Credit them (and OSM) in your
|
|
24
|
+
* app: "© OpenFreeMap © OpenMapTiles, data from OpenStreetMap". A public free instance is a
|
|
25
|
+
* donation-funded service with no SLA — for a product with real traffic, run your own tiles and
|
|
26
|
+
* pass that style's URL instead. */
|
|
27
|
+
export type MapStyleName = "liberty" | "bright" | "positron" | "dark" | "fiord"
|
|
28
|
+
|
|
29
|
+
const NAMED_STYLES: Record<string, string> = {
|
|
30
|
+
liberty: "https://tiles.openfreemap.org/styles/liberty",
|
|
31
|
+
bright: "https://tiles.openfreemap.org/styles/bright",
|
|
32
|
+
positron: "https://tiles.openfreemap.org/styles/positron",
|
|
33
|
+
dark: "https://tiles.openfreemap.org/styles/dark",
|
|
34
|
+
fiord: "https://tiles.openfreemap.org/styles/fiord",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Where the map's style comes from:
|
|
39
|
+
*
|
|
40
|
+
* - a **name** — `"liberty"` (the default), `"positron"`, … see {@link MapStyleName}: a ready-made
|
|
41
|
+
* style on a free public tile server, so `MapView()` alone already draws a world map;
|
|
42
|
+
* - a **URL** — `"https://tiles.example.com/styles/city/style.json"`, the map fetches it;
|
|
43
|
+
* - a **bundled style** — `asset("./map/style.json")`: the file ships inside the app and the
|
|
44
|
+
* wrapper hands its text to the map, so the style itself needs no server (tiles, sprites and
|
|
45
|
+
* glyphs are still fetched from whatever urls it names);
|
|
46
|
+
* - an **already-read file** — a `FetchResponse` from `fetchLocal("style.json")` or
|
|
47
|
+
* `await fetch(url)` (a style downloaded once and cached in `files`);
|
|
48
|
+
* - the **style object** itself — the natural way to substitute a tile-server address at runtime:
|
|
49
|
+
* `{ ...style, sources: { openmaptiles: { type: "vector", url: `${server}/data/v3.json` } } }`.
|
|
50
|
+
*
|
|
51
|
+
* Whichever form: **every url INSIDE the style (`sources[].url`, `sprite`, `glyphs`) must be
|
|
52
|
+
* absolute.** maplibre-native, unlike maplibre-gl-js, resolves no relative ones — a style with
|
|
53
|
+
* them loads to an empty basemap (your layers still draw). A tileserver-gl instance emits
|
|
54
|
+
* relative urls until its `publicUrl` is configured.
|
|
55
|
+
*/
|
|
56
|
+
// `string & {}` keeps the named suggestions in the editor while still accepting any URL.
|
|
57
|
+
export type MapStyle = MapStyleName | (string & {}) | FetchResponse | object
|
|
58
|
+
|
|
59
|
+
/** `[longitude, latitude]` — GeoJSON order, the same as the style, the tiles and your data. */
|
|
60
|
+
export type LngLat = [number, number]
|
|
61
|
+
|
|
62
|
+
export interface MapCamera {
|
|
63
|
+
center: LngLat
|
|
64
|
+
zoom: number
|
|
65
|
+
/** Degrees clockwise from north. */
|
|
66
|
+
bearing: number
|
|
67
|
+
/** Degrees from the vertical. */
|
|
68
|
+
pitch: number
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface MapOptions {
|
|
72
|
+
/** The MapLibre style: a ready-made name (`"liberty"` — the default, `"positron"`, …), a URL,
|
|
73
|
+
* a style bundled with the app (`asset("./style.json")`), an already-read file
|
|
74
|
+
* (`fetchLocal("style.json")`, `await fetch(url)`) or the style object itself — see
|
|
75
|
+
* {@link MapStyle}. */
|
|
76
|
+
style?: MapStyle
|
|
77
|
+
center?: LngLat
|
|
78
|
+
zoom?: number
|
|
79
|
+
minZoom?: number
|
|
80
|
+
maxZoom?: number
|
|
81
|
+
bearing?: number
|
|
82
|
+
pitch?: number
|
|
83
|
+
/** Two-finger rotate gesture (default true). */
|
|
84
|
+
rotate?: boolean
|
|
85
|
+
/** Two-finger tilt gesture (default false — most city maps stay flat). */
|
|
86
|
+
tilt?: boolean
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A tap on the map itself — not on a feature of a managed layer. */
|
|
90
|
+
export interface MapTap {
|
|
91
|
+
lngLat: LngLat
|
|
92
|
+
/** View-space point, px. */
|
|
93
|
+
point: [number, number]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface CameraMove {
|
|
97
|
+
zoom?: number
|
|
98
|
+
bearing?: number
|
|
99
|
+
pitch?: number
|
|
100
|
+
/** Animation length, ms (flyTo only; default 600). */
|
|
101
|
+
duration?: number
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface MapPaddingValues {
|
|
105
|
+
top?: number | string
|
|
106
|
+
left?: number | string
|
|
107
|
+
bottom?: number | string
|
|
108
|
+
right?: number | string
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface FitOptions {
|
|
112
|
+
/** Px around the points, or per edge. Added to the view padding set by `setPadding`. */
|
|
113
|
+
padding?: number | MapPaddingValues
|
|
114
|
+
maxZoom?: number
|
|
115
|
+
/** Default true. */
|
|
116
|
+
animate?: boolean
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** One marker. `id` comes back in `onTap`; `icon` names an image of the style's sprite; `color` /
|
|
120
|
+
* `title` feed the default layers; extra keys become feature properties. */
|
|
121
|
+
export interface MarkerItem {
|
|
122
|
+
id: string | number
|
|
123
|
+
lngLat: LngLat
|
|
124
|
+
icon?: string
|
|
125
|
+
color?: string
|
|
126
|
+
title?: string
|
|
127
|
+
[property: string]: any
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface MarkerTap {
|
|
131
|
+
id: string | number
|
|
132
|
+
lngLat: LngLat
|
|
133
|
+
/** Every property of the tapped feature (the item's keys, `id` and `lngLat` excluded). */
|
|
134
|
+
properties: Record<string, any>
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface MarkerLayerOptions {
|
|
138
|
+
/** Group nearby markers into clusters (a cluster tap zooms in). Default false. */
|
|
139
|
+
cluster?: boolean
|
|
140
|
+
/** Cluster radius, px (default 50). */
|
|
141
|
+
clusterRadius?: number
|
|
142
|
+
/** Zoom at which clusters stop forming (default: maxZoom − 1). */
|
|
143
|
+
clusterMaxZoom?: number
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface LineLayerOptions {
|
|
147
|
+
color?: string
|
|
148
|
+
/** Px (default 4). */
|
|
149
|
+
width?: number
|
|
150
|
+
/** 0–1 (default 1). */
|
|
151
|
+
opacity?: number
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface UserLocationOptions {
|
|
155
|
+
/** Horizontal accuracy radius, meters — drawn as the halo around the dot. */
|
|
156
|
+
accuracy?: number | null
|
|
157
|
+
/** Degrees clockwise from north — drawn as the direction wedge; null hides it. */
|
|
158
|
+
heading?: number | null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface MarkerLayer {
|
|
162
|
+
readonly name: string
|
|
163
|
+
/** Replace the layer's markers. */
|
|
164
|
+
set(items: MarkerItem[]): this
|
|
165
|
+
clear(): this
|
|
166
|
+
/** A marker (or any feature of this layer's source) was tapped. */
|
|
167
|
+
onTap(callback: (marker: MarkerTap) => void): this
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface LineLayer {
|
|
171
|
+
readonly name: string
|
|
172
|
+
/** Replace the line with these vertices. */
|
|
173
|
+
set(coordinates: LngLat[]): this
|
|
174
|
+
clear(): this
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface MapView extends NativeView {
|
|
178
|
+
/** The style loaded and the map is interactive (queued calls have been replayed). */
|
|
179
|
+
onReady(callback: () => void): this
|
|
180
|
+
/** A tap that hit no feature of a managed layer. */
|
|
181
|
+
onTap(callback: (tap: MapTap) => void): this
|
|
182
|
+
/** The camera settled after a gesture or an animation. */
|
|
183
|
+
onMove(callback: (camera: MapCamera) => void): this
|
|
184
|
+
/** The map reported a problem — a style that wouldn't load, a source it couldn't reach. Never
|
|
185
|
+
* fatal; with no handler the message goes to `console.error`, so it is never silent. */
|
|
186
|
+
onError(callback: (error: { message: string }) => void): this
|
|
187
|
+
|
|
188
|
+
/** A named marker layer (one GeoJSON source). If the style already declares a source with this
|
|
189
|
+
* name, its layers are used as-is and only the data is pushed; otherwise the plugin creates the
|
|
190
|
+
* source and default marker layers (colored dot, `icon`, `title` label; clusters on request). */
|
|
191
|
+
markers(name: string, options?: MarkerLayerOptions): MarkerLayer
|
|
192
|
+
/** A named line layer (one GeoJSON source) — same style-first rule as `markers`. */
|
|
193
|
+
line(name: string, options?: LineLayerOptions): LineLayer
|
|
194
|
+
/** Raw escape hatch: replace the data of any GeoJSON source in the style. */
|
|
195
|
+
setData(source: string, geojson: object): this
|
|
196
|
+
|
|
197
|
+
flyTo(center: LngLat, options?: CameraMove): this
|
|
198
|
+
jumpTo(center: LngLat, options?: CameraMove): this
|
|
199
|
+
/** Fit the camera to these points (padding + the view padding respected). */
|
|
200
|
+
fitPoints(points: LngLat[], options?: FitOptions): this
|
|
201
|
+
/** Content inset: the part of the view covered by your UI (`"40%"` = of the view's size).
|
|
202
|
+
* Camera operations center inside the remaining area. */
|
|
203
|
+
setPadding(padding: MapPaddingValues): this
|
|
204
|
+
getCamera(): Promise<MapCamera>
|
|
205
|
+
|
|
206
|
+
/** Move the user puck (the map draws it; the position comes from you — `Geolocation.watch`).
|
|
207
|
+
* `null` hides it. */
|
|
208
|
+
setUserLocation(lngLat: LngLat | null, options?: UserLocationOptions): this
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
type Queued = { method: string, args: any[] }
|
|
212
|
+
type LayerSpec = { kind: "markers" | "line", options: object }
|
|
213
|
+
|
|
214
|
+
/** Methods whose effect lives in the STYLE, not in the view: a style (re)load wipes them, so the
|
|
215
|
+
* wrapper keeps their last value and re-applies it on every `ready` instead of replaying them
|
|
216
|
+
* from the queue. Camera calls are not here — the camera survives a style reload. */
|
|
217
|
+
const STATEFUL = ["ensureLayer", "setData", "setPadding", "setUserLocation"]
|
|
218
|
+
|
|
219
|
+
class MapViewElement extends NativeViewElement {
|
|
220
|
+
private _ready = false
|
|
221
|
+
private _queue: Queued[] = []
|
|
222
|
+
private readonly _layers = new Map<string, MapLayerHandle>()
|
|
223
|
+
/** Style-lifetime state, re-applied on every `ready` (see STATEFUL). */
|
|
224
|
+
private readonly _specs = new Map<string, LayerSpec>()
|
|
225
|
+
private readonly _data = new Map<string, object>()
|
|
226
|
+
private _padding?: MapPaddingValues
|
|
227
|
+
private _user?: any[]
|
|
228
|
+
private readonly _readyCallbacks: (() => void)[] = []
|
|
229
|
+
private readonly _tapCallbacks: ((tap: MapTap) => void)[] = []
|
|
230
|
+
private readonly _moveCallbacks: ((camera: MapCamera) => void)[] = []
|
|
231
|
+
private readonly _errorCallbacks: ((error: { message: string }) => void)[] = []
|
|
232
|
+
|
|
233
|
+
constructor(options: MapOptions = {}) {
|
|
234
|
+
super("map", _styleParams(options))
|
|
235
|
+
_channelOn(this.nvl, "ready", () => this._onReady())
|
|
236
|
+
_channelOn(this.nvl, "tap", (data: any) => this._onTap(data))
|
|
237
|
+
_channelOn(this.nvl, "moveEnd", (data: any) => { for (const cb of [...this._moveCallbacks]) cb(data) })
|
|
238
|
+
_channelOn(this.nvl, "error", (data: any) => this._onError(data))
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
onReady(callback: () => void): this { this._readyCallbacks.push(callback); return this }
|
|
242
|
+
onTap(callback: (tap: MapTap) => void): this { this._tapCallbacks.push(callback); return this }
|
|
243
|
+
onMove(callback: (camera: MapCamera) => void): this { this._moveCallbacks.push(callback); return this }
|
|
244
|
+
onError(callback: (error: { message: string }) => void): this { this._errorCallbacks.push(callback); return this }
|
|
245
|
+
|
|
246
|
+
markers(name: string, options: MarkerLayerOptions = {}): MarkerLayer {
|
|
247
|
+
return this._layer(name, "markers", options) as MarkerLayer
|
|
248
|
+
}
|
|
249
|
+
line(name: string, options: LineLayerOptions = {}): LineLayer {
|
|
250
|
+
return this._layer(name, "line", options) as LineLayer
|
|
251
|
+
}
|
|
252
|
+
setData(source: string, geojson: object): this {
|
|
253
|
+
this._data.set(source, geojson)
|
|
254
|
+
this._send("setData", [source, geojson])
|
|
255
|
+
return this
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
flyTo(center: LngLat, options: CameraMove = {}): this { this._send("flyTo", [center, options]); return this }
|
|
259
|
+
jumpTo(center: LngLat, options: CameraMove = {}): this { this._send("jumpTo", [center, options]); return this }
|
|
260
|
+
fitPoints(points: LngLat[], options: FitOptions = {}): this { this._send("fitPoints", [points, options]); return this }
|
|
261
|
+
setPadding(padding: MapPaddingValues): this { this._padding = padding; this._send("setPadding", [padding]); return this }
|
|
262
|
+
getCamera(): Promise<MapCamera> { return this.call("getCamera") }
|
|
263
|
+
|
|
264
|
+
setUserLocation(lngLat: LngLat | null, options: UserLocationOptions = {}): this {
|
|
265
|
+
this._user = [lngLat, options]
|
|
266
|
+
this._send("setUserLocation", [lngLat, options])
|
|
267
|
+
return this
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** @internal queue until `ready`, then straight through. Rejections are logged, never thrown:
|
|
271
|
+
* a fire-and-forget camera/data call has no caller to reject to. */
|
|
272
|
+
_send(method: string, args: any[]): void {
|
|
273
|
+
if (!this._ready) { this._queue.push({ method, args }); return }
|
|
274
|
+
this.call(method, ...args).catch((e: Error) => console.error(`MapView.${method}: ${e.message}`))
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private _layer(name: string, kind: "markers" | "line", options: object): MapLayerHandle {
|
|
278
|
+
let layer = this._layers.get(name)
|
|
279
|
+
if (!layer) {
|
|
280
|
+
layer = new MapLayerHandle(this, name)
|
|
281
|
+
this._layers.set(name, layer)
|
|
282
|
+
this._specs.set(name, { kind, options })
|
|
283
|
+
this._send("ensureLayer", [name, kind, options])
|
|
284
|
+
}
|
|
285
|
+
return layer
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** A style load — the first one or a reload — starts from a style that knows nothing about the
|
|
289
|
+
* app's layers, so re-apply them all, then whatever else waited in the queue. Without this a
|
|
290
|
+
* second `ready` (a host that loads its default style before the app's, a style swap) leaves a
|
|
291
|
+
* correct-looking map with no data on it. */
|
|
292
|
+
private _onReady(): void {
|
|
293
|
+
this._ready = true
|
|
294
|
+
const queue = this._queue
|
|
295
|
+
this._queue = []
|
|
296
|
+
for (const [name, spec] of this._specs) this._send("ensureLayer", [name, spec.kind, spec.options])
|
|
297
|
+
for (const [source, geojson] of this._data) this._send("setData", [source, geojson])
|
|
298
|
+
if (this._padding) this._send("setPadding", [this._padding])
|
|
299
|
+
if (this._user) this._send("setUserLocation", this._user)
|
|
300
|
+
for (const q of queue) if (STATEFUL.indexOf(q.method) < 0) this._send(q.method, q.args)
|
|
301
|
+
for (const cb of [...this._readyCallbacks]) cb()
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private _onError(data: any): void {
|
|
305
|
+
// Unhandled is not unheard: a style the host couldn't load is exactly the failure that looks
|
|
306
|
+
// like "the map is blank and nothing happened".
|
|
307
|
+
if (this._errorCallbacks.length === 0) { console.error(`MapView: ${data?.message ?? "unknown error"}`); return }
|
|
308
|
+
for (const cb of [...this._errorCallbacks]) cb(data)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private _onTap(data: any): void {
|
|
312
|
+
const feature = data?.feature
|
|
313
|
+
const layer = feature ? this._layers.get(feature.source) : undefined
|
|
314
|
+
if (layer) {
|
|
315
|
+
layer._tap({ id: feature.id, lngLat: data.lngLat, properties: feature.properties ?? {} })
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
for (const cb of [...this._tapCallbacks]) cb({ lngLat: data.lngLat, point: data.point })
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** One named GeoJSON source on the map — the object behind both `MarkerLayer` and `LineLayer`. */
|
|
323
|
+
class MapLayerHandle {
|
|
324
|
+
private readonly _tapCallbacks: ((marker: MarkerTap) => void)[] = []
|
|
325
|
+
private readonly _map: MapViewElement
|
|
326
|
+
readonly name: string
|
|
327
|
+
|
|
328
|
+
constructor(map: MapViewElement, name: string) {
|
|
329
|
+
this._map = map
|
|
330
|
+
this.name = name
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
set(items: MarkerItem[] | LngLat[]): this {
|
|
334
|
+
this._map.setData(this.name, _toGeoJson(items))
|
|
335
|
+
return this
|
|
336
|
+
}
|
|
337
|
+
clear(): this {
|
|
338
|
+
this._map.setData(this.name, { type: "FeatureCollection", features: [] })
|
|
339
|
+
return this
|
|
340
|
+
}
|
|
341
|
+
onTap(callback: (marker: MarkerTap) => void): this {
|
|
342
|
+
this._tapCallbacks.push(callback)
|
|
343
|
+
return this
|
|
344
|
+
}
|
|
345
|
+
/** @internal */
|
|
346
|
+
_tap(marker: MarkerTap): void {
|
|
347
|
+
for (const cb of [...this._tapCallbacks]) cb(marker)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** @internal `MapOptions` → host params. A style URL stays a url the map fetches itself; every
|
|
352
|
+
* other form is read HERE and crosses as `styleJson` text — one string, once, at creation. A
|
|
353
|
+
* bundled `asset()` is `"id:N"` in shell compiles (the host already holds the bytes) and a plain
|
|
354
|
+
* url in server compiles, so the same call works on every host. */
|
|
355
|
+
export const _styleParams = (options: MapOptions = {}): any => {
|
|
356
|
+
const { style = "liberty", ...rest } = options
|
|
357
|
+
if (typeof style === "string") {
|
|
358
|
+
if (!style.startsWith("id:")) return { ...rest, style: NAMED_STYLES[style] ?? style }
|
|
359
|
+
const text = _creatorUtils.fetchToText(+style.slice(3))
|
|
360
|
+
if (!text) throw new Error("MapView: the style asset is empty or missing")
|
|
361
|
+
return { ...rest, styleJson: text }
|
|
362
|
+
}
|
|
363
|
+
const response = style as FetchResponse
|
|
364
|
+
return { ...rest, styleJson: typeof response.text === "function" ? response.text() : JSON.stringify(style) }
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** @internal marker items → a FeatureCollection of Points; a coordinate list → one LineString. */
|
|
368
|
+
export const _toGeoJson = (items: MarkerItem[] | LngLat[]): object => {
|
|
369
|
+
if (items.length > 0 && Array.isArray(items[0])) {
|
|
370
|
+
return { type: "Feature", properties: {}, geometry: { type: "LineString", coordinates: items } }
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
type: "FeatureCollection",
|
|
374
|
+
features: (items as MarkerItem[]).map(item => {
|
|
375
|
+
const { id, lngLat, ...properties } = item
|
|
376
|
+
return { type: "Feature", id, properties: { ...properties, id }, geometry: { type: "Point", coordinates: lngLat } }
|
|
377
|
+
}),
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Create a map view. `MapView.isSupported` reports whether this host registered a "map" view —
|
|
383
|
+
* check it before offering the feature (web, headless and shells without the plugin have none).
|
|
384
|
+
*/
|
|
385
|
+
// PURE IIFE so an app that never uses the map tree-shakes the whole plugin away (see NativeView).
|
|
386
|
+
export const MapView: {
|
|
387
|
+
(options?: MapOptions): MapView
|
|
388
|
+
/** Whether this host registered a "map" view. */
|
|
389
|
+
readonly isSupported: boolean
|
|
390
|
+
} = /*#__PURE__*/ (() => {
|
|
391
|
+
const factory = (options: MapOptions = {}): MapView => new MapViewElement(options) as unknown as MapView
|
|
392
|
+
Object.defineProperty(factory, "isSupported", {
|
|
393
|
+
get: (): boolean => NativeView.isSupported("map"),
|
|
394
|
+
})
|
|
395
|
+
return factory as any
|
|
396
|
+
})()
|