@navaramap/three-plugins 0.0.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/LICENSE +8 -0
- package/README.md +58 -0
- package/dist/index.d.ts +504 -0
- package/dist/index.js +553 -0
- package/dist/index.umd.cjs +1 -0
- package/package.json +43 -0
- package/src/CesiumIonPlugin.test.ts +113 -0
- package/src/CesiumIonPlugin.ts +161 -0
- package/src/OverlayPlugin.ts +202 -0
- package/src/PersonViewPlugin.ts +1044 -0
- package/src/TileJsonPlugin.test.ts +371 -0
- package/src/TileJsonPlugin.ts +301 -0
- package/src/index.ts +35 -0
- package/src/vite-env.d.ts +6 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { CesiumIonPlugin } from "./CesiumIonPlugin";
|
|
4
|
+
|
|
5
|
+
// Importing the real @navaramap/three touches WASM/os at module load, which fails
|
|
6
|
+
// in the test environment. CesiumIonPlugin only needs `Plugin` as a runtime base
|
|
7
|
+
// class (everything else it imports is type-only), so stub the module.
|
|
8
|
+
/* eslint-disable @typescript-eslint/no-extraneous-class */
|
|
9
|
+
vi.mock("@navaramap/three", () => ({
|
|
10
|
+
default: class ThreeView {},
|
|
11
|
+
Plugin: class Plugin {},
|
|
12
|
+
}));
|
|
13
|
+
/* eslint-enable @typescript-eslint/no-extraneous-class */
|
|
14
|
+
|
|
15
|
+
// A minimal fake of the pieces of ThreeView / Source / Layer that
|
|
16
|
+
// CesiumIonPlugin.addTerrain touches. It records the descriptions passed to
|
|
17
|
+
// addSource/addLayer and lets a test fire the layer's "deleted" event.
|
|
18
|
+
function makeFakeView() {
|
|
19
|
+
const source = {
|
|
20
|
+
id: "src",
|
|
21
|
+
type: "quantized-mesh",
|
|
22
|
+
delete: vi.fn(() => true),
|
|
23
|
+
};
|
|
24
|
+
let onDeleted: (() => void) | undefined;
|
|
25
|
+
const layer = {
|
|
26
|
+
id: "layer",
|
|
27
|
+
once: vi.fn((event: string, cb: () => void) => {
|
|
28
|
+
if (event === "deleted") onDeleted = cb;
|
|
29
|
+
}),
|
|
30
|
+
// Deleting the layer emits "deleted", mirroring the real Layer.
|
|
31
|
+
delete: vi.fn(() => onDeleted?.()),
|
|
32
|
+
};
|
|
33
|
+
const view = {
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
+
addSource: vi.fn((_desc: any) => source),
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
+
addLayer: vi.fn((_desc: any) => layer),
|
|
38
|
+
};
|
|
39
|
+
return { view, source, layer };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function initPlugin(view: unknown) {
|
|
43
|
+
const plugin = new CesiumIonPlugin({ assetId: 42, accessToken: "user-tok" });
|
|
44
|
+
vi.stubGlobal(
|
|
45
|
+
"fetch",
|
|
46
|
+
vi.fn(async () => ({
|
|
47
|
+
ok: true,
|
|
48
|
+
json: async () => ({
|
|
49
|
+
url: "https://tiles.example/",
|
|
50
|
+
accessToken: "asset-tok",
|
|
51
|
+
}),
|
|
52
|
+
})),
|
|
53
|
+
);
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
|
+
await plugin.init(view as any, {} as any);
|
|
56
|
+
return plugin;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
vi.unstubAllGlobals();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("CesiumIonPlugin.addTerrain", () => {
|
|
64
|
+
it("routes fetch options to the source and render options to the layer", async () => {
|
|
65
|
+
const { view, source } = makeFakeView();
|
|
66
|
+
const plugin = await initPlugin(view);
|
|
67
|
+
|
|
68
|
+
plugin.addTerrain({
|
|
69
|
+
maxZoom: 14,
|
|
70
|
+
requestVertexNormals: true,
|
|
71
|
+
castShadow: true,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Source carries the resolved endpoint URL/token and the fetch options,
|
|
75
|
+
// but not the render options.
|
|
76
|
+
expect(view.addSource).toHaveBeenCalledWith({
|
|
77
|
+
type: "quantized-mesh",
|
|
78
|
+
url: "https://tiles.example/{z}/{x}/{y}.terrain",
|
|
79
|
+
token: "asset-tok",
|
|
80
|
+
maxZoom: 14,
|
|
81
|
+
requestVertexNormals: true,
|
|
82
|
+
});
|
|
83
|
+
const sourceDesc = view.addSource.mock.calls[0][0];
|
|
84
|
+
expect(sourceDesc).not.toHaveProperty("castShadow");
|
|
85
|
+
|
|
86
|
+
// Layer references the source and receives the render options.
|
|
87
|
+
expect(view.addLayer).toHaveBeenCalledWith(
|
|
88
|
+
expect.objectContaining({
|
|
89
|
+
type: "terrain",
|
|
90
|
+
source,
|
|
91
|
+
terrain: expect.objectContaining({ castShadow: true }),
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
const layerDesc = view.addLayer.mock.calls[0][0];
|
|
95
|
+
expect(layerDesc.terrain).not.toHaveProperty("maxZoom");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("reclaims the implicit source when the layer is deleted", async () => {
|
|
99
|
+
const { view, source, layer } = makeFakeView();
|
|
100
|
+
const plugin = await initPlugin(view);
|
|
101
|
+
|
|
102
|
+
plugin.addTerrain();
|
|
103
|
+
|
|
104
|
+
expect(source.delete).not.toHaveBeenCalled();
|
|
105
|
+
layer.delete();
|
|
106
|
+
expect(source.delete).toHaveBeenCalledTimes(1);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("throws when called before init()", () => {
|
|
110
|
+
const plugin = new CesiumIonPlugin({ assetId: 1, accessToken: "t" });
|
|
111
|
+
expect(() => plugin.addTerrain()).toThrow(/after view\.init/);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CesiumIonPlugin — Navara Plugin for Cesium Ion quantized-mesh terrain.
|
|
3
|
+
*
|
|
4
|
+
* Resolves a Cesium Ion asset endpoint at `init()` time and exposes
|
|
5
|
+
* `addTerrain()` to register the asset as a terrain layer on the view.
|
|
6
|
+
*
|
|
7
|
+
* ## Usage
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import ThreeView from "@navaramap/three";
|
|
11
|
+
* import { DefaultPlugin } from "@navaramap/three-default-plugin";
|
|
12
|
+
* import { CesiumIonPlugin } from "@navaramap/three-plugins";
|
|
13
|
+
*
|
|
14
|
+
* const view = new ThreeView({ container, animation: true });
|
|
15
|
+
* const cesiumIon = new CesiumIonPlugin({
|
|
16
|
+
* assetId: 12345,
|
|
17
|
+
* accessToken: "<your cesium ion token>",
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* view.addPlugin(new DefaultPlugin());
|
|
21
|
+
* view.addPlugin(cesiumIon);
|
|
22
|
+
* await view.init();
|
|
23
|
+
*
|
|
24
|
+
* cesiumIon.addTerrain({
|
|
25
|
+
* maxZoom: 14,
|
|
26
|
+
* castShadow: true,
|
|
27
|
+
* receiveShadow: true,
|
|
28
|
+
* tms: true,
|
|
29
|
+
* geographic: true,
|
|
30
|
+
* requestVertexNormals: true,
|
|
31
|
+
* requestWaterMask: true,
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
import ThreeView, {
|
|
36
|
+
Plugin,
|
|
37
|
+
type QuantizedMeshSource,
|
|
38
|
+
type TerrainSourceLayer,
|
|
39
|
+
type ViewContext,
|
|
40
|
+
} from "@navaramap/three";
|
|
41
|
+
import type { DefaultDescriptions } from "@navaramap/three-default-plugin";
|
|
42
|
+
|
|
43
|
+
type View = ThreeView<DefaultDescriptions>;
|
|
44
|
+
|
|
45
|
+
const CESIUM_ION_API = "https://api.cesium.com/v1/assets";
|
|
46
|
+
|
|
47
|
+
export type CesiumIonConfig = {
|
|
48
|
+
/** Cesium Ion asset id. */
|
|
49
|
+
assetId: number | string;
|
|
50
|
+
/** Cesium Ion access token used to resolve the asset endpoint. */
|
|
51
|
+
accessToken: string;
|
|
52
|
+
endpoint?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Quantized-mesh fetch/decode options forwarded to the source.
|
|
57
|
+
* The `url` and `token` fields are provided by the plugin from the resolved
|
|
58
|
+
* Cesium Ion endpoint.
|
|
59
|
+
*/
|
|
60
|
+
export type CesiumIonSourceOptions = Omit<
|
|
61
|
+
QuantizedMeshSource,
|
|
62
|
+
"type" | "url" | "token" | "id"
|
|
63
|
+
>;
|
|
64
|
+
|
|
65
|
+
/** Terrain mesh rendering options forwarded to the terrain layer. */
|
|
66
|
+
export type CesiumIonTerrainRenderOptions = NonNullable<
|
|
67
|
+
TerrainSourceLayer["terrain"]
|
|
68
|
+
>;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Options for {@link CesiumIonPlugin.addTerrain}. A flat object combining the
|
|
72
|
+
* quantized-mesh source's fetch/decode options with the terrain layer's render
|
|
73
|
+
* options; the plugin routes each field to the source or the layer.
|
|
74
|
+
*/
|
|
75
|
+
export type CesiumIonTerrainOptions = CesiumIonSourceOptions &
|
|
76
|
+
CesiumIonTerrainRenderOptions;
|
|
77
|
+
|
|
78
|
+
type CesiumIonEndpoint = {
|
|
79
|
+
url: string;
|
|
80
|
+
accessToken: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export class CesiumIonPlugin extends Plugin<View, ViewContext> {
|
|
84
|
+
private view?: View;
|
|
85
|
+
private readonly config: CesiumIonConfig;
|
|
86
|
+
private endpoint?: CesiumIonEndpoint;
|
|
87
|
+
|
|
88
|
+
constructor(config: CesiumIonConfig) {
|
|
89
|
+
super();
|
|
90
|
+
this.config = config;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async init(view: View, _ctx: ViewContext): Promise<void> {
|
|
94
|
+
this.view = view;
|
|
95
|
+
|
|
96
|
+
const { assetId, accessToken, endpoint = CESIUM_ION_API } = this.config;
|
|
97
|
+
const url = `${endpoint}/${encodeURIComponent(String(assetId))}/endpoint?access_token=${encodeURIComponent(accessToken)}`;
|
|
98
|
+
|
|
99
|
+
const res = await fetch(url);
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`CesiumIonPlugin: failed to fetch asset ${assetId} endpoint (${res.status} ${res.statusText})`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const json = (await res.json()) as CesiumIonEndpoint;
|
|
106
|
+
this.endpoint = { url: json.url, accessToken: json.accessToken };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Register the resolved Cesium Ion asset as a quantized-mesh terrain layer.
|
|
111
|
+
* Must be called after `view.init()`.
|
|
112
|
+
*
|
|
113
|
+
* Internally this creates an implicit quantized-mesh source for the asset and
|
|
114
|
+
* a terrain layer that references it. The source is owned by the returned
|
|
115
|
+
* layer: deleting the layer reclaims the source too, so callers only manage
|
|
116
|
+
* the layer handle.
|
|
117
|
+
*/
|
|
118
|
+
addTerrain(options: CesiumIonTerrainOptions = {}) {
|
|
119
|
+
if (!this.view || !this.endpoint) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
"CesiumIonPlugin: addTerrain() must be called after view.init().",
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const {
|
|
126
|
+
castShadow,
|
|
127
|
+
receiveShadow,
|
|
128
|
+
show,
|
|
129
|
+
showBoundingBox,
|
|
130
|
+
skirt,
|
|
131
|
+
skirtExaggeration,
|
|
132
|
+
...sourceOptions
|
|
133
|
+
} = options;
|
|
134
|
+
|
|
135
|
+
const source = this.view.addSource({
|
|
136
|
+
type: "quantized-mesh",
|
|
137
|
+
url: `${this.endpoint.url}{z}/{x}/{y}.terrain`,
|
|
138
|
+
token: this.endpoint.accessToken,
|
|
139
|
+
...sourceOptions,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const layer = this.view.addLayer({
|
|
143
|
+
type: "terrain",
|
|
144
|
+
source,
|
|
145
|
+
terrain: {
|
|
146
|
+
castShadow,
|
|
147
|
+
receiveShadow,
|
|
148
|
+
show,
|
|
149
|
+
showBoundingBox,
|
|
150
|
+
skirt,
|
|
151
|
+
skirtExaggeration,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// The source is implicit — the caller only holds the layer handle — so
|
|
156
|
+
// reclaim it once the layer is deleted.
|
|
157
|
+
layer.once("deleted", () => source.delete());
|
|
158
|
+
|
|
159
|
+
return layer;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OverlayPlugin — Navara Plugin for world-to-screen overlay projection.
|
|
3
|
+
*
|
|
4
|
+
* Tracks a set of world positions (lat/lng/alt) and projects them to
|
|
5
|
+
* screen coordinates on every render frame. UI components subscribe via
|
|
6
|
+
* `onUpdate()` to render HTML overlays at the projected positions.
|
|
7
|
+
*
|
|
8
|
+
* ## Usage
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import ThreeView from "@navaramap/three";
|
|
12
|
+
* import { DefaultPlugin } from "@navaramap/three-default-plugin";
|
|
13
|
+
* import { OverlayPlugin, moveOverlayElement } from "@navaramap/three-plugins";
|
|
14
|
+
*
|
|
15
|
+
* const view = new ThreeView({ container, animation: true });
|
|
16
|
+
* const overlayPlugin = new OverlayPlugin({ maxDistance: 100_000 });
|
|
17
|
+
*
|
|
18
|
+
* view.addPlugin(overlayPlugin);
|
|
19
|
+
* await view.init();
|
|
20
|
+
*
|
|
21
|
+
* overlayPlugin.setPositions([
|
|
22
|
+
* { id: "marker-1", lng: 139.77, lat: 35.68, alt: 0 },
|
|
23
|
+
* ]);
|
|
24
|
+
*
|
|
25
|
+
* const unsub = overlayPlugin.onUpdate(({ projected }) => {
|
|
26
|
+
* for (const [id, pos] of projected) {
|
|
27
|
+
* const el = document.getElementById(id);
|
|
28
|
+
* if (el) moveOverlayElement(el, pos.x, pos.y);
|
|
29
|
+
* }
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* unsub();
|
|
33
|
+
* overlayPlugin.dispose();
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
import ThreeView, {
|
|
37
|
+
Plugin,
|
|
38
|
+
geodeticToVector3,
|
|
39
|
+
convertWorldToScreen,
|
|
40
|
+
degreeToRadian,
|
|
41
|
+
type ViewContext,
|
|
42
|
+
type Window,
|
|
43
|
+
} from "@navaramap/three";
|
|
44
|
+
import type { DefaultDescriptions } from "@navaramap/three-default-plugin";
|
|
45
|
+
|
|
46
|
+
type View = ThreeView<DefaultDescriptions>;
|
|
47
|
+
|
|
48
|
+
export type WorldPosition = {
|
|
49
|
+
id: string;
|
|
50
|
+
lng: number;
|
|
51
|
+
lat: number;
|
|
52
|
+
alt: number;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type ProjectedPosition = {
|
|
56
|
+
x: number;
|
|
57
|
+
y: number;
|
|
58
|
+
/** Distance from the camera in meters (ECEF euclidean). */
|
|
59
|
+
distance: number;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type CameraPosition = {
|
|
63
|
+
lng: number;
|
|
64
|
+
lat: number;
|
|
65
|
+
height: number;
|
|
66
|
+
heading: number;
|
|
67
|
+
pitch: number;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type OverlayState = {
|
|
71
|
+
projected: Map<string, ProjectedPosition>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type OverlayConfig = {
|
|
75
|
+
/** Positions farther than this (in meters) are skipped. @defaultValue `100_000` (100 km) */
|
|
76
|
+
maxDistance?: number;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type UpdateListener = (state: OverlayState) => void;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Position an absolutely-positioned HTML element using a GPU-accelerated
|
|
83
|
+
* CSS `translate()` transform.
|
|
84
|
+
*/
|
|
85
|
+
export function moveOverlayElement(
|
|
86
|
+
el: HTMLElement,
|
|
87
|
+
x: number,
|
|
88
|
+
y: number,
|
|
89
|
+
): void {
|
|
90
|
+
el.style.transform = `translate(${x}px, ${y}px)`;
|
|
91
|
+
el.style.willChange = "transform";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const DEFAULT_MAX_DISTANCE = 100_000;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Projects geographic positions to screen coordinates on every render frame.
|
|
98
|
+
*
|
|
99
|
+
* Register via `view.addPlugin(overlayPlugin)` **before** `view.init()`.
|
|
100
|
+
* The `preRender` hook is attached automatically during initialization.
|
|
101
|
+
*/
|
|
102
|
+
export class OverlayPlugin extends Plugin<View, ViewContext> {
|
|
103
|
+
private view?: View;
|
|
104
|
+
private maxDistance: number;
|
|
105
|
+
|
|
106
|
+
private positions: WorldPosition[] = [];
|
|
107
|
+
private projected = new Map<string, ProjectedPosition>();
|
|
108
|
+
|
|
109
|
+
private listeners = new Set<UpdateListener>();
|
|
110
|
+
private boundHandler: () => void;
|
|
111
|
+
|
|
112
|
+
constructor(config?: OverlayConfig) {
|
|
113
|
+
super();
|
|
114
|
+
this.maxDistance = config?.maxDistance ?? DEFAULT_MAX_DISTANCE;
|
|
115
|
+
this.boundHandler = this.handlePreRender.bind(this);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async init(view: View, _ctx: ViewContext): Promise<void> {
|
|
119
|
+
this.view = view;
|
|
120
|
+
view.on("preRender", this.boundHandler);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Replace the set of world positions to track.
|
|
125
|
+
* Positions are re-projected on the next render frame automatically.
|
|
126
|
+
*/
|
|
127
|
+
setPositions(positions: WorldPosition[]): void {
|
|
128
|
+
this.positions = positions;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
onUpdate(fn: UpdateListener): () => void {
|
|
132
|
+
this.listeners.add(fn);
|
|
133
|
+
return () => this.listeners.delete(fn);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
dispose(): void {
|
|
137
|
+
if (this.view) {
|
|
138
|
+
this.view.off("preRender", this.boundHandler);
|
|
139
|
+
}
|
|
140
|
+
this.listeners.clear();
|
|
141
|
+
this.positions = [];
|
|
142
|
+
this.projected.clear();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private worldToScreen(
|
|
146
|
+
lng: number,
|
|
147
|
+
lat: number,
|
|
148
|
+
alt: number,
|
|
149
|
+
): { x: number; y: number } | null {
|
|
150
|
+
if (!this.view) return null;
|
|
151
|
+
|
|
152
|
+
const ecef = geodeticToVector3({
|
|
153
|
+
lng: degreeToRadian(lng),
|
|
154
|
+
lat: degreeToRadian(lat),
|
|
155
|
+
height: alt,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const win: Window = {
|
|
159
|
+
width: this.view.screenSize.x,
|
|
160
|
+
height: this.view.screenSize.y,
|
|
161
|
+
pixelRatio: this.view.pixelRatio,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const result = convertWorldToScreen(win, this.view.camera.raw, ecef);
|
|
165
|
+
if (!result) return null;
|
|
166
|
+
|
|
167
|
+
return { x: result.x, y: result.y };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private handlePreRender(): void {
|
|
171
|
+
if (!this.view) return;
|
|
172
|
+
|
|
173
|
+
const geo = this.view.camera.positionGeographic;
|
|
174
|
+
const cameraEcef = geodeticToVector3({
|
|
175
|
+
lng: degreeToRadian(geo.lng),
|
|
176
|
+
lat: degreeToRadian(geo.lat),
|
|
177
|
+
height: geo.height,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const next = new Map<string, ProjectedPosition>();
|
|
181
|
+
for (const pos of this.positions) {
|
|
182
|
+
const markerEcef = geodeticToVector3({
|
|
183
|
+
lng: degreeToRadian(pos.lng),
|
|
184
|
+
lat: degreeToRadian(pos.lat),
|
|
185
|
+
height: pos.alt,
|
|
186
|
+
});
|
|
187
|
+
const distance = cameraEcef.distanceTo(markerEcef);
|
|
188
|
+
if (distance > this.maxDistance) continue;
|
|
189
|
+
|
|
190
|
+
const screen = this.worldToScreen(pos.lng, pos.lat, pos.alt);
|
|
191
|
+
if (screen) {
|
|
192
|
+
next.set(pos.id, { ...screen, distance });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
this.projected = next;
|
|
196
|
+
|
|
197
|
+
const state: OverlayState = {
|
|
198
|
+
projected: this.projected,
|
|
199
|
+
};
|
|
200
|
+
for (const fn of this.listeners) fn(state);
|
|
201
|
+
}
|
|
202
|
+
}
|