@invana/graph-layer-maplibre 0.0.1
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/index.d.ts +232 -0
- package/dist/index.js +270 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { EventMap, Layer, LayerOptions, CanvasContext } from '@invana/canvas';
|
|
2
|
+
import maplibregl from 'maplibre-gl';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `@invana/graph-layer-maplibre` — public types.
|
|
6
|
+
*
|
|
7
|
+
* The MapLayer owns a MapLibre GL JS map mounted as a sibling DOM element
|
|
8
|
+
* underneath the Pixi canvas, and keeps the canvas camera mirrored to the
|
|
9
|
+
* map's transform every time the user pans / zooms the map. Domain layers
|
|
10
|
+
* (typically `GraphLayer` from `@invana/graph`) pin their content to
|
|
11
|
+
* geographic coordinates via {@link MapLayer.project}.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Geographic coordinate as `[longitude, latitude]` in degrees. */
|
|
15
|
+
type LngLat = readonly [number, number];
|
|
16
|
+
/** A world-coordinate point (mercator pixels at the layer's reference zoom). */
|
|
17
|
+
interface WorldPoint {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Construction-time options for {@link MapLayer}.
|
|
23
|
+
*
|
|
24
|
+
* The layer is intentionally thin: it hosts a MapLibre map, projects
|
|
25
|
+
* `[lng, lat]` to stable world coords (web-mercator pixels at zoom 0), and
|
|
26
|
+
* syncs the canvas camera so anything drawn at those world coords lines up
|
|
27
|
+
* pixel-accurately with the basemap as the user pans / zooms.
|
|
28
|
+
*/
|
|
29
|
+
interface MapLayerOptions {
|
|
30
|
+
/**
|
|
31
|
+
* MapLibre style URL. Defaults to the OpenFreeMap "liberty" style
|
|
32
|
+
* (https://openfreemap.org) — free, no-key, OSM-based vector tiles. Pass a
|
|
33
|
+
* different URL or a full StyleSpecification object to swap basemaps.
|
|
34
|
+
*/
|
|
35
|
+
styleUrl?: string | object;
|
|
36
|
+
/** Initial map centre as `[lng, lat]`. Default `[0, 20]`. */
|
|
37
|
+
center?: LngLat;
|
|
38
|
+
/** Initial MapLibre zoom level (0..22). Default `1.5`. */
|
|
39
|
+
zoom?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Minimum / maximum allowed MapLibre zoom. Defaults `0` / `22`. The canvas
|
|
42
|
+
* camera mirrors `2^zoom` as its scale, so these implicitly clamp how far
|
|
43
|
+
* the user can zoom the engine view too.
|
|
44
|
+
*/
|
|
45
|
+
minZoom?: number;
|
|
46
|
+
maxZoom?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Optional DOM element the map is mounted into. If omitted, the layer
|
|
49
|
+
* inserts a new `<div>` as the first child of the Pixi canvas's parent
|
|
50
|
+
* element (so the basemap renders *behind* the Pixi canvas).
|
|
51
|
+
*/
|
|
52
|
+
mountTarget?: HTMLElement;
|
|
53
|
+
/**
|
|
54
|
+
* Make the Pixi canvas pointer-event-transparent so MapLibre receives all
|
|
55
|
+
* mouse / touch input (pan, zoom, click). Default `true`. Set `false` if
|
|
56
|
+
* you want Pixi-layer behaviours (hover, click-select) to take input
|
|
57
|
+
* priority — you'll then have to drive map pan/zoom by other means.
|
|
58
|
+
*/
|
|
59
|
+
passInputToMap?: boolean;
|
|
60
|
+
}
|
|
61
|
+
/** {@link MapLayer} state — exposed via `layer.state.getState()`. */
|
|
62
|
+
interface MapLayerState {
|
|
63
|
+
/** True after the MapLibre `load` event has fired (style + first tiles in). */
|
|
64
|
+
ready: boolean;
|
|
65
|
+
}
|
|
66
|
+
/** Event payloads emitted by {@link MapLayer}. */
|
|
67
|
+
interface MapLayerEvents extends EventMap {
|
|
68
|
+
/** Fired once after MapLibre's `load` event — style + initial tiles ready. */
|
|
69
|
+
'map:ready': {
|
|
70
|
+
center: [number, number];
|
|
71
|
+
zoom: number;
|
|
72
|
+
};
|
|
73
|
+
/** Fired each time the map transform changes (move / zoom / resize). */
|
|
74
|
+
'map:move': {
|
|
75
|
+
center: [number, number];
|
|
76
|
+
zoom: number;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* `MapLayer` — hosts a MapLibre GL JS basemap underneath the Pixi canvas
|
|
82
|
+
* and mirrors its camera transform into `canvas.camera` every frame the map
|
|
83
|
+
* moves. Domain layers (graph, contours, anything drawing in world coords)
|
|
84
|
+
* pin their content to geographic positions via {@link MapLayer.project}.
|
|
85
|
+
*
|
|
86
|
+
* ## Why a Layer at all
|
|
87
|
+
*
|
|
88
|
+
* MapLibre owns its own canvas, camera, and input handling — none of which
|
|
89
|
+
* compose with PixiJS directly. We model the integration as a two-stack
|
|
90
|
+
* overlay:
|
|
91
|
+
*
|
|
92
|
+
* ```
|
|
93
|
+
* ┌─ host element (the user's container) ─────────────────────────────┐
|
|
94
|
+
* │ ┌─ MapLibre <div> ────────────────────────────────────────────┐ │
|
|
95
|
+
* │ │ basemap tiles (MapLibre's own webgl canvas) │ │
|
|
96
|
+
* │ └─────────────────────────────────────────────────────────────┘ │
|
|
97
|
+
* │ ┌─ Pixi <canvas> (this engine, transparent) ──────────────────┐ │
|
|
98
|
+
* │ │ graph nodes / edges / overlays │ │
|
|
99
|
+
* │ └─────────────────────────────────────────────────────────────┘ │
|
|
100
|
+
* └────────────────────────────────────────────────────────────────────┘
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* MapLibre drives all pan / zoom — the Pixi canvas is pointer-event-
|
|
104
|
+
* transparent by default so the map receives clicks and drags natively.
|
|
105
|
+
* The MapLayer subscribes to `map.on('move', ...)` and rewrites the
|
|
106
|
+
* `pixi-viewport` transform so the two canvases stay pixel-aligned. Result:
|
|
107
|
+
* a node at world `(x, y)` (= the mercator-pixel projection of some
|
|
108
|
+
* `[lng, lat]`) always lands on the same screen pixel as MapLibre's own
|
|
109
|
+
* `map.project([lng, lat])`.
|
|
110
|
+
*
|
|
111
|
+
* ## Coordinate model
|
|
112
|
+
*
|
|
113
|
+
* The MapLayer projects `[lng, lat]` to **web-mercator pixel coordinates at
|
|
114
|
+
* zoom 0**, where the entire world is a 512×512 px square (MapLibre's tile
|
|
115
|
+
* convention). Two consequences worth understanding:
|
|
116
|
+
*
|
|
117
|
+
* 1. **World positions are stable.** A node's world `(x, y)` doesn't change
|
|
118
|
+
* when the user zooms the map — only the camera transform does. That
|
|
119
|
+
* means downstream layers (graph, contours, layouts) don't need to be
|
|
120
|
+
* re-fed on zoom; they just keep their cached positions.
|
|
121
|
+
* 2. **Canvas scale = `2^zoom`.** The mirrored transform is
|
|
122
|
+
* `viewport.scale = 2^map.getZoom()`, and `viewport.position` is solved
|
|
123
|
+
* so the reference point `(lng=0, lat=0)` lands where MapLibre projects
|
|
124
|
+
* it. Bearing and pitch are locked to 0 because the canvas camera is
|
|
125
|
+
* affine + uniform-scale; map rotation/tilt would desync the two stacks.
|
|
126
|
+
*
|
|
127
|
+
* ## Constraints
|
|
128
|
+
*
|
|
129
|
+
* - Requires `canvas.init(...)` — the headless `initWithStage` path has no
|
|
130
|
+
* DOM, so there's nowhere to mount the map. The layer throws on mount
|
|
131
|
+
* in that case.
|
|
132
|
+
* - Don't register `DragPanBehaviour` / `WheelZoomBehaviour` /
|
|
133
|
+
* `PinchZoomBehaviour` alongside this layer. They'd fight the map for
|
|
134
|
+
* the camera, and on a pointer-events-none canvas they'd never see input
|
|
135
|
+
* anyway.
|
|
136
|
+
* - Cross-layer dependencies (e.g. a graph layer needing the projection)
|
|
137
|
+
* declare their dep with an explicit `mapLayerId` option and resolve it
|
|
138
|
+
* via `ctx.layers.get<MapLayer>(...)`. Don't reach for the layer by
|
|
139
|
+
* guessing — see `architecture-proposal.md` §2.4.
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
declare class MapLayer extends Layer<MapLayerOptions, MapLayerState, MapLayerEvents> {
|
|
143
|
+
private map;
|
|
144
|
+
private mapContainer;
|
|
145
|
+
private ownsMapContainer;
|
|
146
|
+
private originalCanvasPointerEvents;
|
|
147
|
+
private originalCanvasPosition;
|
|
148
|
+
private originalCanvasZIndex;
|
|
149
|
+
/**
|
|
150
|
+
* Last camera scale we pushed to the bus on a `camera:zoom` event.
|
|
151
|
+
* `syncCameraFromMap` writes the viewport directly (it has to, to mirror
|
|
152
|
+
* MapLibre's transform exactly), so the bus only learns about zoom
|
|
153
|
+
* changes when we emit them. Skip the emit when scale is unchanged
|
|
154
|
+
* (pan-only frames) so listeners like `ScreenSizeBehaviour` don't pay
|
|
155
|
+
* the O(N) reflow cost on every pan tick.
|
|
156
|
+
*/
|
|
157
|
+
private lastEmittedScale;
|
|
158
|
+
private readonly handleMapMove;
|
|
159
|
+
constructor(opts: LayerOptions<MapLayerOptions>);
|
|
160
|
+
/** The underlying MapLibre Map. `null` before mount / after unmount. */
|
|
161
|
+
get maplibre(): maplibregl.Map | null;
|
|
162
|
+
protected createState(): MapLayerState;
|
|
163
|
+
/**
|
|
164
|
+
* Project a geographic coordinate to canvas world coordinates.
|
|
165
|
+
*
|
|
166
|
+
* Returns mercator pixels at zoom 0 (a 512×512 square for the whole
|
|
167
|
+
* earth). Stable across map zoom — pin nodes once at setup and let the
|
|
168
|
+
* camera handle the rest.
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* const { x, y } = mapLayer.project([airport.lng, airport.lat]);
|
|
172
|
+
* graphLayer.setData({ nodes: [{ id, position: { x, y }, ... }], ... });
|
|
173
|
+
*/
|
|
174
|
+
project(lngLat: LngLat): WorldPoint;
|
|
175
|
+
/**
|
|
176
|
+
* Inverse of {@link project} — world coords back to `[lng, lat]`. Useful
|
|
177
|
+
* for hit-testing or reporting the geographic location under a cursor.
|
|
178
|
+
*/
|
|
179
|
+
unproject(world: WorldPoint): [number, number];
|
|
180
|
+
/** Pan/zoom the basemap to a new view. Camera follows automatically via `move`. */
|
|
181
|
+
flyTo(opts: {
|
|
182
|
+
center?: LngLat;
|
|
183
|
+
zoom?: number;
|
|
184
|
+
duration?: number;
|
|
185
|
+
}): void;
|
|
186
|
+
protected onMount(ctx: CanvasContext): void;
|
|
187
|
+
protected onUnmount(ctx: CanvasContext): void;
|
|
188
|
+
/**
|
|
189
|
+
* Solve for the pixi-viewport transform that lines our world axes up with
|
|
190
|
+
* MapLibre's screen pixels.
|
|
191
|
+
*
|
|
192
|
+
* Pixi viewport projects `world -> screen` as `s = w * scale + position`.
|
|
193
|
+
* We pick a fixed reference point (`lng=0, lat=0` — the mercator equator
|
|
194
|
+
* meridian intersection, world coord `(256, 256)` at our reference zoom)
|
|
195
|
+
* and solve:
|
|
196
|
+
*
|
|
197
|
+
* screen_of_lat0lng0 = worldRef * (2 ** map.zoom) + viewport.position
|
|
198
|
+
*
|
|
199
|
+
* `screen_of_lat0lng0` comes straight from `map.project([0, 0])` —
|
|
200
|
+
* MapLibre handles all the map's internal padding / world-wrap / pixel
|
|
201
|
+
* ratio for us. We then rewrite `viewport.position` to match.
|
|
202
|
+
*/
|
|
203
|
+
private syncCameraFromMap;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Spherical interpolation between two `[lng, lat]` points along the
|
|
208
|
+
* great-circle (shortest path on the sphere). Returns `n` evenly-spaced
|
|
209
|
+
* samples *including* both endpoints.
|
|
210
|
+
*
|
|
211
|
+
* Used by stories drawing flight routes / airline arcs: the projected
|
|
212
|
+
* polyline of these samples reads as a smooth curve on a mercator basemap.
|
|
213
|
+
* Pure function, no engine dependency — exposed from
|
|
214
|
+
* `@invana/graph-layer-maplibre` because it pairs with {@link MapLayer.project},
|
|
215
|
+
* but works fine without the layer too.
|
|
216
|
+
*
|
|
217
|
+
* Algorithm: classic Slerp on unit-sphere 3-vectors derived from
|
|
218
|
+
* `(lng, lat)`. Falls back to linear interpolation when the two points are
|
|
219
|
+
* effectively coincident (angle ≈ 0), which keeps tiny self-loops from
|
|
220
|
+
* dividing by zero in `sin(0)`.
|
|
221
|
+
*/
|
|
222
|
+
type LngLatTuple = readonly [number, number];
|
|
223
|
+
/**
|
|
224
|
+
* Sample `n` points (`n >= 2`) along the great circle from `from` to `to`.
|
|
225
|
+
*
|
|
226
|
+
* - `n = 2` returns just the endpoints.
|
|
227
|
+
* - `n = 32` (the typical default for flight arcs) gives a visually-smooth
|
|
228
|
+
* curve at most map zooms; bump to 64+ for long transoceanic routes.
|
|
229
|
+
*/
|
|
230
|
+
declare function greatCircleSamples(from: LngLatTuple, to: LngLatTuple, n: number): [number, number][];
|
|
231
|
+
|
|
232
|
+
export { type LngLat, type LngLatTuple, MapLayer, type MapLayerEvents, type MapLayerOptions, type MapLayerState, type WorldPoint, greatCircleSamples };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { Layer } from '@invana/canvas';
|
|
2
|
+
import maplibregl from 'maplibre-gl';
|
|
3
|
+
|
|
4
|
+
// src/MapLayer.ts
|
|
5
|
+
var WORLD_SIZE = 512;
|
|
6
|
+
var DEFAULT_STYLE_URL = "https://tiles.openfreemap.org/styles/liberty";
|
|
7
|
+
var MapLayer = class extends Layer {
|
|
8
|
+
map = null;
|
|
9
|
+
mapContainer = null;
|
|
10
|
+
ownsMapContainer = false;
|
|
11
|
+
originalCanvasPointerEvents = null;
|
|
12
|
+
originalCanvasPosition = null;
|
|
13
|
+
originalCanvasZIndex = null;
|
|
14
|
+
/**
|
|
15
|
+
* Last camera scale we pushed to the bus on a `camera:zoom` event.
|
|
16
|
+
* `syncCameraFromMap` writes the viewport directly (it has to, to mirror
|
|
17
|
+
* MapLibre's transform exactly), so the bus only learns about zoom
|
|
18
|
+
* changes when we emit them. Skip the emit when scale is unchanged
|
|
19
|
+
* (pan-only frames) so listeners like `ScreenSizeBehaviour` don't pay
|
|
20
|
+
* the O(N) reflow cost on every pan tick.
|
|
21
|
+
*/
|
|
22
|
+
lastEmittedScale = null;
|
|
23
|
+
handleMapMove = () => this.syncCameraFromMap();
|
|
24
|
+
constructor(opts) {
|
|
25
|
+
super({
|
|
26
|
+
...opts,
|
|
27
|
+
// The map is a basemap — clicks on empty map area should fall through
|
|
28
|
+
// to the (non-existent) layer below, not be claimed here. Domain
|
|
29
|
+
// layers above still hit-test normally.
|
|
30
|
+
hittable: opts.hittable ?? false,
|
|
31
|
+
// Always render — the map fills the whole viewport regardless of
|
|
32
|
+
// where graph content sits.
|
|
33
|
+
cullable: opts.cullable ?? false
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/** The underlying MapLibre Map. `null` before mount / after unmount. */
|
|
37
|
+
get maplibre() {
|
|
38
|
+
return this.map;
|
|
39
|
+
}
|
|
40
|
+
createState() {
|
|
41
|
+
return { ready: false };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Project a geographic coordinate to canvas world coordinates.
|
|
45
|
+
*
|
|
46
|
+
* Returns mercator pixels at zoom 0 (a 512×512 square for the whole
|
|
47
|
+
* earth). Stable across map zoom — pin nodes once at setup and let the
|
|
48
|
+
* camera handle the rest.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* const { x, y } = mapLayer.project([airport.lng, airport.lat]);
|
|
52
|
+
* graphLayer.setData({ nodes: [{ id, position: { x, y }, ... }], ... });
|
|
53
|
+
*/
|
|
54
|
+
project(lngLat) {
|
|
55
|
+
const [lng, lat] = lngLat;
|
|
56
|
+
const clampedLat = Math.max(-85.05112878, Math.min(85.05112878, lat));
|
|
57
|
+
const sin = Math.sin(clampedLat * Math.PI / 180);
|
|
58
|
+
const x = (lng + 180) / 360 * WORLD_SIZE;
|
|
59
|
+
const y = (0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI)) * WORLD_SIZE;
|
|
60
|
+
return { x, y };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Inverse of {@link project} — world coords back to `[lng, lat]`. Useful
|
|
64
|
+
* for hit-testing or reporting the geographic location under a cursor.
|
|
65
|
+
*/
|
|
66
|
+
unproject(world) {
|
|
67
|
+
const lng = world.x / WORLD_SIZE * 360 - 180;
|
|
68
|
+
const n = Math.PI - 2 * Math.PI * (world.y / WORLD_SIZE);
|
|
69
|
+
const lat = 180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)));
|
|
70
|
+
return [lng, lat];
|
|
71
|
+
}
|
|
72
|
+
/** Pan/zoom the basemap to a new view. Camera follows automatically via `move`. */
|
|
73
|
+
flyTo(opts) {
|
|
74
|
+
if (!this.map) return;
|
|
75
|
+
this.map.flyTo({
|
|
76
|
+
center: opts.center ? [opts.center[0], opts.center[1]] : void 0,
|
|
77
|
+
zoom: opts.zoom,
|
|
78
|
+
duration: opts.duration ?? 1e3
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
onMount(ctx) {
|
|
82
|
+
const canvasEl = ctx.canvasElement;
|
|
83
|
+
if (!canvasEl) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`MapLayer "${this.id}" requires a DOM-mounted Canvas (canvas.init), not the headless initWithStage path \u2014 there's no element to mount the basemap into.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const explicitTarget = this.options.mountTarget;
|
|
89
|
+
if (explicitTarget) {
|
|
90
|
+
this.mapContainer = explicitTarget;
|
|
91
|
+
} else {
|
|
92
|
+
const host = canvasEl.parentElement;
|
|
93
|
+
if (!host) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`MapLayer "${this.id}": Pixi canvas has no parent element to mount the basemap into.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (getComputedStyle(host).position === "static") {
|
|
99
|
+
host.style.position = "relative";
|
|
100
|
+
}
|
|
101
|
+
const div = document.createElement("div");
|
|
102
|
+
div.dataset.invanaMaplayerId = this.id;
|
|
103
|
+
div.style.cssText = "position:absolute; inset:0; width:100%; height:100%; z-index:0; pointer-events:auto;";
|
|
104
|
+
host.insertBefore(div, host.firstChild);
|
|
105
|
+
this.mapContainer = div;
|
|
106
|
+
this.ownsMapContainer = true;
|
|
107
|
+
}
|
|
108
|
+
this.originalCanvasPosition = canvasEl.style.position;
|
|
109
|
+
this.originalCanvasZIndex = canvasEl.style.zIndex;
|
|
110
|
+
if (getComputedStyle(canvasEl).position === "static") {
|
|
111
|
+
canvasEl.style.position = "relative";
|
|
112
|
+
}
|
|
113
|
+
canvasEl.style.zIndex = "1";
|
|
114
|
+
if (this.options.passInputToMap ?? true) {
|
|
115
|
+
this.originalCanvasPointerEvents = canvasEl.style.pointerEvents;
|
|
116
|
+
canvasEl.style.pointerEvents = "none";
|
|
117
|
+
}
|
|
118
|
+
const styleUrl = this.options.styleUrl ?? DEFAULT_STYLE_URL;
|
|
119
|
+
const center = this.options.center ?? [0, 20];
|
|
120
|
+
const zoom = this.options.zoom ?? 1.5;
|
|
121
|
+
this.map = new maplibregl.Map({
|
|
122
|
+
container: this.mapContainer,
|
|
123
|
+
// MapLibre's typings accept `string | StyleSpecification`; we widen to
|
|
124
|
+
// `object` in our public types and cast here.
|
|
125
|
+
style: styleUrl,
|
|
126
|
+
center: [center[0], center[1]],
|
|
127
|
+
zoom,
|
|
128
|
+
minZoom: this.options.minZoom ?? 0,
|
|
129
|
+
maxZoom: this.options.maxZoom ?? 22,
|
|
130
|
+
// Lock orientation: our pixi camera is uniform-scale, no rotation/tilt.
|
|
131
|
+
bearing: 0,
|
|
132
|
+
pitch: 0,
|
|
133
|
+
dragRotate: false,
|
|
134
|
+
pitchWithRotate: false,
|
|
135
|
+
touchPitch: false,
|
|
136
|
+
attributionControl: { compact: true }
|
|
137
|
+
});
|
|
138
|
+
this.map.on("move", this.handleMapMove);
|
|
139
|
+
this.map.on("load", () => {
|
|
140
|
+
this.state.setState((s) => {
|
|
141
|
+
s.ready = true;
|
|
142
|
+
});
|
|
143
|
+
this.events.emit("map:ready", {
|
|
144
|
+
center: [this.map.getCenter().lng, this.map.getCenter().lat],
|
|
145
|
+
zoom: this.map.getZoom()
|
|
146
|
+
});
|
|
147
|
+
this.syncCameraFromMap();
|
|
148
|
+
});
|
|
149
|
+
this.syncCameraFromMap();
|
|
150
|
+
}
|
|
151
|
+
onUnmount(ctx) {
|
|
152
|
+
if (this.map) {
|
|
153
|
+
this.map.off("move", this.handleMapMove);
|
|
154
|
+
this.map.remove();
|
|
155
|
+
this.map = null;
|
|
156
|
+
}
|
|
157
|
+
if (this.ownsMapContainer && this.mapContainer?.parentElement) {
|
|
158
|
+
this.mapContainer.parentElement.removeChild(this.mapContainer);
|
|
159
|
+
}
|
|
160
|
+
this.mapContainer = null;
|
|
161
|
+
this.ownsMapContainer = false;
|
|
162
|
+
if (ctx.canvasElement) {
|
|
163
|
+
const el = ctx.canvasElement;
|
|
164
|
+
if (this.originalCanvasPointerEvents !== null) {
|
|
165
|
+
el.style.pointerEvents = this.originalCanvasPointerEvents;
|
|
166
|
+
}
|
|
167
|
+
if (this.originalCanvasPosition !== null) {
|
|
168
|
+
el.style.position = this.originalCanvasPosition;
|
|
169
|
+
}
|
|
170
|
+
if (this.originalCanvasZIndex !== null) {
|
|
171
|
+
el.style.zIndex = this.originalCanvasZIndex;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
this.originalCanvasPointerEvents = null;
|
|
175
|
+
this.originalCanvasPosition = null;
|
|
176
|
+
this.originalCanvasZIndex = null;
|
|
177
|
+
this.lastEmittedScale = null;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Solve for the pixi-viewport transform that lines our world axes up with
|
|
181
|
+
* MapLibre's screen pixels.
|
|
182
|
+
*
|
|
183
|
+
* Pixi viewport projects `world -> screen` as `s = w * scale + position`.
|
|
184
|
+
* We pick a fixed reference point (`lng=0, lat=0` — the mercator equator
|
|
185
|
+
* meridian intersection, world coord `(256, 256)` at our reference zoom)
|
|
186
|
+
* and solve:
|
|
187
|
+
*
|
|
188
|
+
* screen_of_lat0lng0 = worldRef * (2 ** map.zoom) + viewport.position
|
|
189
|
+
*
|
|
190
|
+
* `screen_of_lat0lng0` comes straight from `map.project([0, 0])` —
|
|
191
|
+
* MapLibre handles all the map's internal padding / world-wrap / pixel
|
|
192
|
+
* ratio for us. We then rewrite `viewport.position` to match.
|
|
193
|
+
*/
|
|
194
|
+
syncCameraFromMap() {
|
|
195
|
+
const map = this.map;
|
|
196
|
+
const ctx = this.ctx;
|
|
197
|
+
if (!map || !ctx) return;
|
|
198
|
+
const zoom = map.getZoom();
|
|
199
|
+
const scale = Math.pow(2, zoom);
|
|
200
|
+
const ref = [0, 0];
|
|
201
|
+
const screenRef = map.project([ref[0], ref[1]]);
|
|
202
|
+
const worldRef = this.project(ref);
|
|
203
|
+
const tx = screenRef.x - worldRef.x * scale;
|
|
204
|
+
const ty = screenRef.y - worldRef.y * scale;
|
|
205
|
+
ctx.camera.viewport.scale.set(scale);
|
|
206
|
+
ctx.camera.viewport.position.set(tx, ty);
|
|
207
|
+
if (this.lastEmittedScale === null || this.lastEmittedScale !== scale) {
|
|
208
|
+
ctx.events.emit("camera:zoom", {
|
|
209
|
+
scale,
|
|
210
|
+
centerX: ctx.camera.screenWidth / 2,
|
|
211
|
+
centerY: ctx.camera.screenHeight / 2
|
|
212
|
+
});
|
|
213
|
+
this.lastEmittedScale = scale;
|
|
214
|
+
}
|
|
215
|
+
ctx.events.emit("camera:pan", { x: tx, y: ty });
|
|
216
|
+
this.events.emit("map:move", {
|
|
217
|
+
center: [map.getCenter().lng, map.getCenter().lat],
|
|
218
|
+
zoom
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// src/greatCircle.ts
|
|
224
|
+
var DEG = Math.PI / 180;
|
|
225
|
+
var RAD = 180 / Math.PI;
|
|
226
|
+
function toCartesian(lng, lat) {
|
|
227
|
+
const clng = Math.cos(lng * DEG);
|
|
228
|
+
const slng = Math.sin(lng * DEG);
|
|
229
|
+
const clat = Math.cos(lat * DEG);
|
|
230
|
+
const slat = Math.sin(lat * DEG);
|
|
231
|
+
return [clat * clng, clat * slng, slat];
|
|
232
|
+
}
|
|
233
|
+
function toLngLat(v) {
|
|
234
|
+
const [x, y, z] = v;
|
|
235
|
+
const lng = Math.atan2(y, x) * RAD;
|
|
236
|
+
const lat = Math.atan2(z, Math.sqrt(x * x + y * y)) * RAD;
|
|
237
|
+
return [lng, lat];
|
|
238
|
+
}
|
|
239
|
+
function greatCircleSamples(from, to, n) {
|
|
240
|
+
if (n < 2) throw new Error(`greatCircleSamples: n must be >= 2 (got ${n})`);
|
|
241
|
+
const a = toCartesian(from[0], from[1]);
|
|
242
|
+
const b = toCartesian(to[0], to[1]);
|
|
243
|
+
const dot = Math.max(-1, Math.min(1, a[0] * b[0] + a[1] * b[1] + a[2] * b[2]));
|
|
244
|
+
const omega = Math.acos(dot);
|
|
245
|
+
const sinOmega = Math.sin(omega);
|
|
246
|
+
const out = new Array(n);
|
|
247
|
+
if (sinOmega < 1e-9) {
|
|
248
|
+
for (let i = 0; i < n; i++) {
|
|
249
|
+
const t = i / (n - 1);
|
|
250
|
+
out[i] = [from[0] + (to[0] - from[0]) * t, from[1] + (to[1] - from[1]) * t];
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
for (let i = 0; i < n; i++) {
|
|
255
|
+
const t = i / (n - 1);
|
|
256
|
+
const s1 = Math.sin((1 - t) * omega) / sinOmega;
|
|
257
|
+
const s2 = Math.sin(t * omega) / sinOmega;
|
|
258
|
+
const v = [
|
|
259
|
+
s1 * a[0] + s2 * b[0],
|
|
260
|
+
s1 * a[1] + s2 * b[1],
|
|
261
|
+
s1 * a[2] + s2 * b[2]
|
|
262
|
+
];
|
|
263
|
+
out[i] = toLngLat(v);
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export { MapLayer, greatCircleSamples };
|
|
269
|
+
//# sourceMappingURL=index.js.map
|
|
270
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/MapLayer.ts","../src/greatCircle.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * `MapLayer` — hosts a MapLibre GL JS basemap underneath the Pixi canvas\n * and mirrors its camera transform into `canvas.camera` every frame the map\n * moves. Domain layers (graph, contours, anything drawing in world coords)\n * pin their content to geographic positions via {@link MapLayer.project}.\n *\n * ## Why a Layer at all\n *\n * MapLibre owns its own canvas, camera, and input handling — none of which\n * compose with PixiJS directly. We model the integration as a two-stack\n * overlay:\n *\n * ```\n * ┌─ host element (the user's container) ─────────────────────────────┐\n * │ ┌─ MapLibre <div> ────────────────────────────────────────────┐ │\n * │ │ basemap tiles (MapLibre's own webgl canvas) │ │\n * │ └─────────────────────────────────────────────────────────────┘ │\n * │ ┌─ Pixi <canvas> (this engine, transparent) ──────────────────┐ │\n * │ │ graph nodes / edges / overlays │ │\n * │ └─────────────────────────────────────────────────────────────┘ │\n * └────────────────────────────────────────────────────────────────────┘\n * ```\n *\n * MapLibre drives all pan / zoom — the Pixi canvas is pointer-event-\n * transparent by default so the map receives clicks and drags natively.\n * The MapLayer subscribes to `map.on('move', ...)` and rewrites the\n * `pixi-viewport` transform so the two canvases stay pixel-aligned. Result:\n * a node at world `(x, y)` (= the mercator-pixel projection of some\n * `[lng, lat]`) always lands on the same screen pixel as MapLibre's own\n * `map.project([lng, lat])`.\n *\n * ## Coordinate model\n *\n * The MapLayer projects `[lng, lat]` to **web-mercator pixel coordinates at\n * zoom 0**, where the entire world is a 512×512 px square (MapLibre's tile\n * convention). Two consequences worth understanding:\n *\n * 1. **World positions are stable.** A node's world `(x, y)` doesn't change\n * when the user zooms the map — only the camera transform does. That\n * means downstream layers (graph, contours, layouts) don't need to be\n * re-fed on zoom; they just keep their cached positions.\n * 2. **Canvas scale = `2^zoom`.** The mirrored transform is\n * `viewport.scale = 2^map.getZoom()`, and `viewport.position` is solved\n * so the reference point `(lng=0, lat=0)` lands where MapLibre projects\n * it. Bearing and pitch are locked to 0 because the canvas camera is\n * affine + uniform-scale; map rotation/tilt would desync the two stacks.\n *\n * ## Constraints\n *\n * - Requires `canvas.init(...)` — the headless `initWithStage` path has no\n * DOM, so there's nowhere to mount the map. The layer throws on mount\n * in that case.\n * - Don't register `DragPanBehaviour` / `WheelZoomBehaviour` /\n * `PinchZoomBehaviour` alongside this layer. They'd fight the map for\n * the camera, and on a pointer-events-none canvas they'd never see input\n * anyway.\n * - Cross-layer dependencies (e.g. a graph layer needing the projection)\n * declare their dep with an explicit `mapLayerId` option and resolve it\n * via `ctx.layers.get<MapLayer>(...)`. Don't reach for the layer by\n * guessing — see `architecture-proposal.md` §2.4.\n */\n\nimport { Layer, type CanvasContext, type LayerOptions } from '@invana/canvas';\nimport maplibregl from 'maplibre-gl';\n\nimport type {\n LngLat,\n MapLayerEvents,\n MapLayerOptions,\n MapLayerState,\n WorldPoint,\n} from './types';\n\n/** Width/height in world units of the whole earth at our reference zoom (= MapLibre tile size at zoom 0). */\nconst WORLD_SIZE = 512;\n\nconst DEFAULT_STYLE_URL = 'https://tiles.openfreemap.org/styles/liberty';\n\nexport class MapLayer extends Layer<MapLayerOptions, MapLayerState, MapLayerEvents> {\n private map: maplibregl.Map | null = null;\n private mapContainer: HTMLDivElement | null = null;\n private ownsMapContainer = false;\n private originalCanvasPointerEvents: string | null = null;\n private originalCanvasPosition: string | null = null;\n private originalCanvasZIndex: string | null = null;\n /**\n * Last camera scale we pushed to the bus on a `camera:zoom` event.\n * `syncCameraFromMap` writes the viewport directly (it has to, to mirror\n * MapLibre's transform exactly), so the bus only learns about zoom\n * changes when we emit them. Skip the emit when scale is unchanged\n * (pan-only frames) so listeners like `ScreenSizeBehaviour` don't pay\n * the O(N) reflow cost on every pan tick.\n */\n private lastEmittedScale: number | null = null;\n private readonly handleMapMove = (): void => this.syncCameraFromMap();\n\n constructor(opts: LayerOptions<MapLayerOptions>) {\n super({\n ...opts,\n // The map is a basemap — clicks on empty map area should fall through\n // to the (non-existent) layer below, not be claimed here. Domain\n // layers above still hit-test normally.\n hittable: opts.hittable ?? false,\n // Always render — the map fills the whole viewport regardless of\n // where graph content sits.\n cullable: opts.cullable ?? false,\n });\n }\n\n /** The underlying MapLibre Map. `null` before mount / after unmount. */\n get maplibre(): maplibregl.Map | null {\n return this.map;\n }\n\n protected createState(): MapLayerState {\n return { ready: false };\n }\n\n /**\n * Project a geographic coordinate to canvas world coordinates.\n *\n * Returns mercator pixels at zoom 0 (a 512×512 square for the whole\n * earth). Stable across map zoom — pin nodes once at setup and let the\n * camera handle the rest.\n *\n * @example\n * const { x, y } = mapLayer.project([airport.lng, airport.lat]);\n * graphLayer.setData({ nodes: [{ id, position: { x, y }, ... }], ... });\n */\n project(lngLat: LngLat): WorldPoint {\n const [lng, lat] = lngLat;\n const clampedLat = Math.max(-85.05112878, Math.min(85.05112878, lat));\n const sin = Math.sin((clampedLat * Math.PI) / 180);\n const x = ((lng + 180) / 360) * WORLD_SIZE;\n const y = (0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI)) * WORLD_SIZE;\n return { x, y };\n }\n\n /**\n * Inverse of {@link project} — world coords back to `[lng, lat]`. Useful\n * for hit-testing or reporting the geographic location under a cursor.\n */\n unproject(world: WorldPoint): [number, number] {\n const lng = (world.x / WORLD_SIZE) * 360 - 180;\n const n = Math.PI - 2 * Math.PI * (world.y / WORLD_SIZE);\n const lat = (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)));\n return [lng, lat];\n }\n\n /** Pan/zoom the basemap to a new view. Camera follows automatically via `move`. */\n flyTo(opts: { center?: LngLat; zoom?: number; duration?: number }): void {\n if (!this.map) return;\n this.map.flyTo({\n center: opts.center ? [opts.center[0], opts.center[1]] : undefined,\n zoom: opts.zoom,\n duration: opts.duration ?? 1000,\n });\n }\n\n protected override onMount(ctx: CanvasContext): void {\n const canvasEl = ctx.canvasElement;\n if (!canvasEl) {\n throw new Error(\n `MapLayer \"${this.id}\" requires a DOM-mounted Canvas (canvas.init), ` +\n `not the headless initWithStage path — there's no element to mount the basemap into.`,\n );\n }\n\n const explicitTarget = this.options.mountTarget;\n if (explicitTarget) {\n this.mapContainer = explicitTarget as HTMLDivElement;\n } else {\n const host = canvasEl.parentElement;\n if (!host) {\n throw new Error(\n `MapLayer \"${this.id}\": Pixi canvas has no parent element to mount the basemap into.`,\n );\n }\n // Ensure absolute children stack correctly against the host.\n if (getComputedStyle(host).position === 'static') {\n host.style.position = 'relative';\n }\n const div = document.createElement('div');\n div.dataset.invanaMaplayerId = this.id;\n // `z-index: 0` keeps the map below the canvas (which we hoist to\n // `z-index: 1` below). DOM-order alone isn't enough: an absolutely-\n // positioned sibling stacks above a statically-positioned one\n // regardless of source order, which would put the basemap on top of\n // the Pixi canvas and hide everything drawn there.\n div.style.cssText =\n 'position:absolute; inset:0; width:100%; height:100%; z-index:0; pointer-events:auto;';\n host.insertBefore(div, host.firstChild);\n this.mapContainer = div;\n this.ownsMapContainer = true;\n }\n\n // Hoist the Pixi canvas above the basemap. `z-index` is a no-op on a\n // statically-positioned element, so we also force `position: relative`\n // (preserves layout flow). Originals are stashed and restored on unmount.\n this.originalCanvasPosition = canvasEl.style.position;\n this.originalCanvasZIndex = canvasEl.style.zIndex;\n if (getComputedStyle(canvasEl).position === 'static') {\n canvasEl.style.position = 'relative';\n }\n canvasEl.style.zIndex = '1';\n\n // Pixi canvas → input-transparent so MapLibre's gestures reach the map.\n if (this.options.passInputToMap ?? true) {\n this.originalCanvasPointerEvents = canvasEl.style.pointerEvents;\n canvasEl.style.pointerEvents = 'none';\n }\n\n const styleUrl = this.options.styleUrl ?? DEFAULT_STYLE_URL;\n const center = this.options.center ?? [0, 20];\n const zoom = this.options.zoom ?? 1.5;\n\n this.map = new maplibregl.Map({\n container: this.mapContainer,\n // MapLibre's typings accept `string | StyleSpecification`; we widen to\n // `object` in our public types and cast here.\n style: styleUrl as string,\n center: [center[0], center[1]],\n zoom,\n minZoom: this.options.minZoom ?? 0,\n maxZoom: this.options.maxZoom ?? 22,\n // Lock orientation: our pixi camera is uniform-scale, no rotation/tilt.\n bearing: 0,\n pitch: 0,\n dragRotate: false,\n pitchWithRotate: false,\n touchPitch: false,\n attributionControl: { compact: true },\n });\n\n this.map.on('move', this.handleMapMove);\n this.map.on('load', () => {\n this.state.setState((s) => {\n s.ready = true;\n });\n this.events.emit('map:ready', {\n center: [this.map!.getCenter().lng, this.map!.getCenter().lat],\n zoom: this.map!.getZoom(),\n });\n this.syncCameraFromMap();\n });\n\n // Sync once up front so the canvas transform is in the right place\n // before tiles finish loading.\n this.syncCameraFromMap();\n }\n\n protected override onUnmount(ctx: CanvasContext): void {\n if (this.map) {\n this.map.off('move', this.handleMapMove);\n this.map.remove();\n this.map = null;\n }\n if (this.ownsMapContainer && this.mapContainer?.parentElement) {\n this.mapContainer.parentElement.removeChild(this.mapContainer);\n }\n this.mapContainer = null;\n this.ownsMapContainer = false;\n\n if (ctx.canvasElement) {\n const el = ctx.canvasElement;\n if (this.originalCanvasPointerEvents !== null) {\n el.style.pointerEvents = this.originalCanvasPointerEvents;\n }\n if (this.originalCanvasPosition !== null) {\n el.style.position = this.originalCanvasPosition;\n }\n if (this.originalCanvasZIndex !== null) {\n el.style.zIndex = this.originalCanvasZIndex;\n }\n }\n this.originalCanvasPointerEvents = null;\n this.originalCanvasPosition = null;\n this.originalCanvasZIndex = null;\n this.lastEmittedScale = null;\n }\n\n /**\n * Solve for the pixi-viewport transform that lines our world axes up with\n * MapLibre's screen pixels.\n *\n * Pixi viewport projects `world -> screen` as `s = w * scale + position`.\n * We pick a fixed reference point (`lng=0, lat=0` — the mercator equator\n * meridian intersection, world coord `(256, 256)` at our reference zoom)\n * and solve:\n *\n * screen_of_lat0lng0 = worldRef * (2 ** map.zoom) + viewport.position\n *\n * `screen_of_lat0lng0` comes straight from `map.project([0, 0])` —\n * MapLibre handles all the map's internal padding / world-wrap / pixel\n * ratio for us. We then rewrite `viewport.position` to match.\n */\n private syncCameraFromMap(): void {\n const map = this.map;\n const ctx = this.ctx;\n if (!map || !ctx) return;\n\n const zoom = map.getZoom();\n const scale = Math.pow(2, zoom);\n\n const ref: LngLat = [0, 0];\n const screenRef = map.project([ref[0], ref[1]]);\n const worldRef = this.project(ref);\n\n const tx = screenRef.x - worldRef.x * scale;\n const ty = screenRef.y - worldRef.y * scale;\n\n // Direct viewport writes — `camera.setZoom` / `setPosition` would\n // re-anchor at the viewport centre (their own math), which doesn't\n // match MapLibre's exact transform. We mirror the transform raw, then\n // bridge the resulting camera change onto the canvas event bus below\n // so behaviours subscribed to `camera:zoom` / `camera:pan` (e.g.\n // `ScreenSizeBehaviour`, `LabelResolutionLODBehaviour`) react to\n // MapLibre-driven pan/zoom too. Without this bridge those listeners\n // are silently never called when the map drives the camera.\n ctx.camera.viewport.scale.set(scale);\n ctx.camera.viewport.position.set(tx, ty);\n\n // `camera:zoom` only when scale actually changes — most map gestures\n // are pan-only and the reflow listeners are O(N) over their tracked\n // entities, so we don't want to fire on every move tick during a\n // long pan.\n if (this.lastEmittedScale === null || this.lastEmittedScale !== scale) {\n ctx.events.emit('camera:zoom', {\n scale,\n centerX: ctx.camera.screenWidth / 2,\n centerY: ctx.camera.screenHeight / 2,\n });\n this.lastEmittedScale = scale;\n }\n ctx.events.emit('camera:pan', { x: tx, y: ty });\n\n this.events.emit('map:move', {\n center: [map.getCenter().lng, map.getCenter().lat],\n zoom,\n });\n }\n}\n","/**\n * Spherical interpolation between two `[lng, lat]` points along the\n * great-circle (shortest path on the sphere). Returns `n` evenly-spaced\n * samples *including* both endpoints.\n *\n * Used by stories drawing flight routes / airline arcs: the projected\n * polyline of these samples reads as a smooth curve on a mercator basemap.\n * Pure function, no engine dependency — exposed from\n * `@invana/graph-layer-maplibre` because it pairs with {@link MapLayer.project},\n * but works fine without the layer too.\n *\n * Algorithm: classic Slerp on unit-sphere 3-vectors derived from\n * `(lng, lat)`. Falls back to linear interpolation when the two points are\n * effectively coincident (angle ≈ 0), which keeps tiny self-loops from\n * dividing by zero in `sin(0)`.\n */\n\nexport type LngLatTuple = readonly [number, number];\n\nconst DEG = Math.PI / 180;\nconst RAD = 180 / Math.PI;\n\nfunction toCartesian(lng: number, lat: number): [number, number, number] {\n const clng = Math.cos(lng * DEG);\n const slng = Math.sin(lng * DEG);\n const clat = Math.cos(lat * DEG);\n const slat = Math.sin(lat * DEG);\n return [clat * clng, clat * slng, slat];\n}\n\nfunction toLngLat(v: [number, number, number]): [number, number] {\n const [x, y, z] = v;\n const lng = Math.atan2(y, x) * RAD;\n const lat = Math.atan2(z, Math.sqrt(x * x + y * y)) * RAD;\n return [lng, lat];\n}\n\n/**\n * Sample `n` points (`n >= 2`) along the great circle from `from` to `to`.\n *\n * - `n = 2` returns just the endpoints.\n * - `n = 32` (the typical default for flight arcs) gives a visually-smooth\n * curve at most map zooms; bump to 64+ for long transoceanic routes.\n */\nexport function greatCircleSamples(\n from: LngLatTuple,\n to: LngLatTuple,\n n: number,\n): [number, number][] {\n if (n < 2) throw new Error(`greatCircleSamples: n must be >= 2 (got ${n})`);\n const a = toCartesian(from[0], from[1]);\n const b = toCartesian(to[0], to[1]);\n\n // Angle between the two unit vectors.\n const dot = Math.max(-1, Math.min(1, a[0] * b[0] + a[1] * b[1] + a[2] * b[2]));\n const omega = Math.acos(dot);\n const sinOmega = Math.sin(omega);\n\n const out: [number, number][] = new Array(n);\n\n // Coincident / near-coincident: slerp degenerates; linear-interpolate in\n // lng/lat directly — small enough that mercator distortion is invisible.\n if (sinOmega < 1e-9) {\n for (let i = 0; i < n; i++) {\n const t = i / (n - 1);\n out[i] = [from[0] + (to[0] - from[0]) * t, from[1] + (to[1] - from[1]) * t];\n }\n return out;\n }\n\n for (let i = 0; i < n; i++) {\n const t = i / (n - 1);\n const s1 = Math.sin((1 - t) * omega) / sinOmega;\n const s2 = Math.sin(t * omega) / sinOmega;\n const v: [number, number, number] = [\n s1 * a[0] + s2 * b[0],\n s1 * a[1] + s2 * b[1],\n s1 * a[2] + s2 * b[2],\n ];\n out[i] = toLngLat(v);\n }\n return out;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@invana/graph-layer-maplibre",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "MapLibre GL JS basemap Layer for @invana/canvas — projects graph nodes onto a real interactive world map.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"typedocOptions": {
|
|
9
|
+
"entryPoints": [
|
|
10
|
+
"src/index.ts"
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"maplibre-gl": "^4.7.1"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"pixi.js": "^8.18.1",
|
|
18
|
+
"@invana/canvas": "0.0.1"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"pixi.js": "^8.18.1",
|
|
22
|
+
"tsup": "^8.3.5",
|
|
23
|
+
"typescript": "5.9.2",
|
|
24
|
+
"@invana/canvas": "0.0.1",
|
|
25
|
+
"@repo/typescript-config": "0.0.0"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"canvas",
|
|
29
|
+
"graph",
|
|
30
|
+
"layer",
|
|
31
|
+
"maplibre",
|
|
32
|
+
"map",
|
|
33
|
+
"geo",
|
|
34
|
+
"basemap"
|
|
35
|
+
],
|
|
36
|
+
"license": "Apache-2.0",
|
|
37
|
+
"module": "./dist/index.js",
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
41
|
+
"import": "./dist/index.js",
|
|
42
|
+
"default": "./dist/index.js"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist"
|
|
47
|
+
],
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsup",
|
|
53
|
+
"dev": "tsup --watch",
|
|
54
|
+
"lint": "eslint src/",
|
|
55
|
+
"check-types": "tsc --noEmit",
|
|
56
|
+
"clean": "rm -rf dist"
|
|
57
|
+
}
|
|
58
|
+
}
|