@tangram.gl/tangram-layers 0.0.0-stage → 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/README.md +36 -2
- package/dist/experimental/projected-basemaps.js +1310 -0
- package/dist/experimental/projected-basemaps.js.map +6 -0
- package/dist/experimental/terrain.js +379 -0
- package/dist/experimental/terrain.js.map +6 -0
- package/dist/experimental/webxr.js +1423 -0
- package/dist/experimental/webxr.js.map +6 -0
- package/dist/index.js +756 -0
- package/dist/index.js.map +6 -0
- package/dist/types/experimental/projected-basemaps.d.ts +120 -0
- package/dist/types/experimental/projected-detail.d.ts +33 -0
- package/dist/types/experimental/projected-navigation.d.ts +117 -0
- package/dist/types/experimental/terrain-display.d.ts +124 -0
- package/dist/types/experimental/terrain.d.ts +131 -0
- package/dist/types/first_person_view_adapter.d.ts +29 -0
- package/dist/types/globe_view_adapter.d.ts +33 -0
- package/dist/types/index.d.ts +84 -0
- package/dist/types/tangram-layer-lifecycle.d.ts +132 -0
- package/dist/types/tangram-layer.d.ts +157 -0
- package/dist/types/view_adapter_types.d.ts +42 -0
- package/dist/types/web_mercator_view_adapter.d.ts +77 -0
- package/package.json +80 -3
- package/src/experimental/projected-basemaps.ts +301 -0
- package/src/experimental/projected-detail.ts +116 -0
- package/src/experimental/projected-navigation.ts +323 -0
- package/src/experimental/terrain-display.ts +243 -0
- package/src/experimental/terrain.ts +240 -0
- package/src/experimental/webxr/grabbing.ts +166 -0
- package/src/experimental/webxr/index.d.ts +77 -0
- package/src/experimental/webxr/index.ts +72 -0
- package/src/experimental/webxr/interaction.ts +221 -0
- package/src/experimental/webxr/picking.ts +119 -0
- package/src/experimental/webxr/presentation.ts +457 -0
- package/src/experimental/webxr/projection.ts +371 -0
- package/src/experimental/webxr/types.ts +221 -0
- package/src/experimental/webxr/views.ts +190 -0
- package/src/first_person_view_adapter.ts +107 -0
- package/src/globe_view_adapter.ts +103 -0
- package/src/index.ts +64 -0
- package/src/tangram-layer-lifecycle.ts +130 -0
- package/src/tangram-layer.ts +679 -0
- package/src/view_adapter_types.ts +40 -0
- package/src/web_mercator_view_adapter.ts +185 -0
|
@@ -0,0 +1,1310 @@
|
|
|
1
|
+
// src/experimental/projected-basemaps.ts
|
|
2
|
+
import { Layer } from "@deck.gl/core";
|
|
3
|
+
import { Matrix4 as Matrix42 } from "@math.gl/core";
|
|
4
|
+
import { HostFrame, Renderer, normalizeProjectedBasemapOptions as normalizeProjectedBasemapOptions2, countProjectedTileCoordinates as countProjectedTileCoordinates3, getProjectedRoadUnit, validateProjectedLights } from "@tangram.gl/tangram-renderer/core";
|
|
5
|
+
|
|
6
|
+
// src/web_mercator_view_adapter.ts
|
|
7
|
+
import { PROJECTION_CONSTANTS } from "@tangram.gl/tangram-renderer/core";
|
|
8
|
+
var DECK_TO_TANGRAM_ZOOM_OFFSET = 1;
|
|
9
|
+
var DECK_WORLD_SIZE = 512;
|
|
10
|
+
var TANGRAM_HALF_WORLD_METERS = Math.PI * PROJECTION_CONSTANTS.mercatorRadius;
|
|
11
|
+
var WebMercatorViewAdapter = class _WebMercatorViewAdapter {
|
|
12
|
+
/**
|
|
13
|
+
* Checks that a deck viewport provides the public Web Mercator contract used
|
|
14
|
+
* by TangramLayer.
|
|
15
|
+
* @param viewport deck.gl viewport.
|
|
16
|
+
* @returns Validation error, or null when supported.
|
|
17
|
+
*/
|
|
18
|
+
static validateViewport(viewport) {
|
|
19
|
+
if (viewport?.isGeospatial === false) {
|
|
20
|
+
return new Error("a Web Mercator viewport is required");
|
|
21
|
+
}
|
|
22
|
+
if (_WebMercatorViewAdapter.validateGeographicAnchor(viewport, "Web Mercator viewport")) {
|
|
23
|
+
return new Error("a Web Mercator viewport is required");
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Validates the geographic anchor shared by all deck.gl view adapters.
|
|
29
|
+
* @param viewport deck.gl viewport.
|
|
30
|
+
* @param viewportName View name used in the error message.
|
|
31
|
+
* @returns Validation error, or null when the anchor is finite.
|
|
32
|
+
*/
|
|
33
|
+
static validateGeographicAnchor(viewport, viewportName = "viewport") {
|
|
34
|
+
if (!Number.isFinite(viewport?.longitude) || !Number.isFinite(viewport?.latitude) || !Number.isFinite(viewport?.zoom)) {
|
|
35
|
+
return new Error(`${viewportName} requires finite longitude, latitude, and zoom`);
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Converts deck.gl camera matrices to Tangram camera matrices.
|
|
41
|
+
*
|
|
42
|
+
* Tangram tile models use absolute EPSG:3857 meters while deck matrices
|
|
43
|
+
* consume zoom-zero common coordinates. Altitude uses deck's
|
|
44
|
+
* latitude-dependent distance scale.
|
|
45
|
+
* @param viewport deck.gl WebMercatorViewport.
|
|
46
|
+
* @returns Tangram camera matrices and eye position.
|
|
47
|
+
*/
|
|
48
|
+
static getCameraFrame(viewport) {
|
|
49
|
+
const distanceScales = typeof viewport.getDistanceScales === "function" ? viewport.getDistanceScales() : viewport.distanceScales;
|
|
50
|
+
const unitsPerMeter = distanceScales && distanceScales.unitsPerMeter;
|
|
51
|
+
const viewMatrix = viewport.viewMatrix;
|
|
52
|
+
const projectionMatrix = viewport.projectionMatrix;
|
|
53
|
+
if (!viewMatrix || viewMatrix.length !== 16 || !projectionMatrix || projectionMatrix.length !== 16 || !unitsPerMeter || !Number.isFinite(unitsPerMeter[2])) {
|
|
54
|
+
throw new Error("deck viewport camera matrices and distance scales are required");
|
|
55
|
+
}
|
|
56
|
+
const xyScale = DECK_WORLD_SIZE / (TANGRAM_HALF_WORLD_METERS * 2);
|
|
57
|
+
const metersToCommon = new Float64Array(16);
|
|
58
|
+
metersToCommon[0] = xyScale;
|
|
59
|
+
metersToCommon[5] = xyScale;
|
|
60
|
+
metersToCommon[10] = unitsPerMeter[2];
|
|
61
|
+
metersToCommon[12] = DECK_WORLD_SIZE / 2;
|
|
62
|
+
metersToCommon[13] = DECK_WORLD_SIZE / 2;
|
|
63
|
+
metersToCommon[15] = 1;
|
|
64
|
+
return {
|
|
65
|
+
view: multiplyMatrices(viewMatrix, metersToCommon),
|
|
66
|
+
projection: new Float32Array(projectionMatrix),
|
|
67
|
+
// The view matrix places the camera at the origin in eye coordinates.
|
|
68
|
+
position: [0, 0, 0]
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Builds Tangram's external-camera frame for a deck.gl MapView.
|
|
73
|
+
* @param viewport deck.gl WebMercatorViewport.
|
|
74
|
+
* @param dimensions Render-target size.
|
|
75
|
+
* @returns Tangram HostFrame legacy shape.
|
|
76
|
+
*/
|
|
77
|
+
static getFrame(viewport, { width, height }) {
|
|
78
|
+
const longitude = viewport.longitude;
|
|
79
|
+
const latitude = viewport.latitude;
|
|
80
|
+
const zoom = viewport.zoom;
|
|
81
|
+
if (typeof longitude !== "number" || !Number.isFinite(longitude) || typeof latitude !== "number" || !Number.isFinite(latitude) || typeof zoom !== "number" || !Number.isFinite(zoom)) {
|
|
82
|
+
throw new Error("a Web Mercator viewport is required");
|
|
83
|
+
}
|
|
84
|
+
const pitch = Math.abs(viewport.pitch || 0) * Math.PI / 180;
|
|
85
|
+
return {
|
|
86
|
+
viewport: { width, height },
|
|
87
|
+
view: {
|
|
88
|
+
longitude,
|
|
89
|
+
latitude,
|
|
90
|
+
zoom: zoom + DECK_TO_TANGRAM_ZOOM_OFFSET
|
|
91
|
+
},
|
|
92
|
+
projection: { type: "web-mercator" },
|
|
93
|
+
camera: _WebMercatorViewAdapter.getCameraFrame(viewport),
|
|
94
|
+
tileBuffer: Math.min(4, Math.ceil(Math.tan(pitch) * (viewport.height || height) / 256))
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
function multiplyMatrices(left, right) {
|
|
99
|
+
const result = new Float64Array(16);
|
|
100
|
+
for (let column = 0; column < 4; column++) {
|
|
101
|
+
for (let row = 0; row < 4; row++) {
|
|
102
|
+
let value = 0;
|
|
103
|
+
for (let index = 0; index < 4; index++) {
|
|
104
|
+
value += left[index * 4 + row] * right[column * 4 + index];
|
|
105
|
+
}
|
|
106
|
+
result[column * 4 + row] = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/first_person_view_adapter.ts
|
|
113
|
+
import { Matrix4 } from "@math.gl/core";
|
|
114
|
+
import { calculatePlanarVolumeBounds } from "@tangram.gl/tangram-renderer/core";
|
|
115
|
+
var DECK_WORLD_SIZE2 = 512;
|
|
116
|
+
var HALF_WORLD_METERS = 20037508342789244e-9;
|
|
117
|
+
var TILE_SIZE = 256;
|
|
118
|
+
var MAX_MERCATOR_LATITUDE = 85.05112878;
|
|
119
|
+
var DEFAULT_GROUND_EXTENT = 2e4;
|
|
120
|
+
var FirstPersonViewAdapter = class {
|
|
121
|
+
/** Converts the finite frustum/ground intersection without changing camera matrices. */
|
|
122
|
+
static getFrame = getFrame;
|
|
123
|
+
};
|
|
124
|
+
function getFrame(viewport, options = {}) {
|
|
125
|
+
return getFirstPersonFrameForCamera(viewport, WebMercatorViewAdapter.getCameraFrame(viewport), options);
|
|
126
|
+
}
|
|
127
|
+
function getFirstPersonFrameForCamera(viewport, camera, options = {}) {
|
|
128
|
+
const width = options.width ?? viewport.width;
|
|
129
|
+
const height = options.height ?? viewport.height;
|
|
130
|
+
const maxGroundExtent = options.maxGroundExtent ?? DEFAULT_GROUND_EXTENT;
|
|
131
|
+
if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) {
|
|
132
|
+
throw new Error("FirstPersonViewport requires positive width and height");
|
|
133
|
+
}
|
|
134
|
+
if (!Number.isFinite(maxGroundExtent) || maxGroundExtent <= 0) {
|
|
135
|
+
throw new Error("FirstPersonViewport maxGroundExtent must be finite and positive");
|
|
136
|
+
}
|
|
137
|
+
const { longitude, latitude } = viewport;
|
|
138
|
+
const unprojectFlat = viewport.unprojectFlat;
|
|
139
|
+
if (typeof longitude !== "number" || !Number.isFinite(longitude) || typeof latitude !== "number" || !Number.isFinite(latitude) || typeof unprojectFlat !== "function") {
|
|
140
|
+
throw new Error("FirstPersonViewport geographic center and ground projection method are required");
|
|
141
|
+
}
|
|
142
|
+
const inverseView = new Matrix4().copy(camera.view);
|
|
143
|
+
if (!Number.isFinite(inverseView.determinant()) || inverseView.determinant() === 0) {
|
|
144
|
+
throw new Error("FirstPersonViewport camera is singular");
|
|
145
|
+
}
|
|
146
|
+
const eye = inverseView.invert().transformAsPoint([0, 0, 0]);
|
|
147
|
+
if (!eye.every(Number.isFinite)) throw new Error("FirstPersonViewport eye position is invalid");
|
|
148
|
+
const latitudeScale = Math.cos(Math.max(
|
|
149
|
+
-MAX_MERCATOR_LATITUDE,
|
|
150
|
+
Math.min(MAX_MERCATOR_LATITUDE, latitude)
|
|
151
|
+
) * Math.PI / 180);
|
|
152
|
+
const extent = maxGroundExtent / latitudeScale;
|
|
153
|
+
const bounds = calculatePlanarVolumeBounds(camera, options.elevationRange ?? [0, 0], {
|
|
154
|
+
sw: { x: eye[0] - extent, y: eye[1] - extent },
|
|
155
|
+
ne: { x: eye[0] + extent, y: eye[1] + extent }
|
|
156
|
+
});
|
|
157
|
+
const xyScale = DECK_WORLD_SIZE2 / (HALF_WORLD_METERS * 2);
|
|
158
|
+
const toGeographic = (x, y) => unprojectFlat.call(viewport, [
|
|
159
|
+
x * xyScale + DECK_WORLD_SIZE2 / 2,
|
|
160
|
+
y * xyScale + DECK_WORLD_SIZE2 / 2
|
|
161
|
+
]);
|
|
162
|
+
const center = bounds ? toGeographic(
|
|
163
|
+
(bounds.sw.x + bounds.ne.x) / 2,
|
|
164
|
+
(bounds.sw.y + bounds.ne.y) / 2
|
|
165
|
+
) : [longitude, latitude];
|
|
166
|
+
const metersPerPixel = bounds ? Math.max(
|
|
167
|
+
(bounds.ne.x - bounds.sw.x) / width,
|
|
168
|
+
(bounds.ne.y - bounds.sw.y) / height
|
|
169
|
+
) : extent * 2 / Math.min(width, height);
|
|
170
|
+
let visibleBounds = null;
|
|
171
|
+
if (bounds) {
|
|
172
|
+
const southwest = toGeographic(bounds.sw.x, bounds.sw.y);
|
|
173
|
+
const northeast = toGeographic(bounds.ne.x, bounds.ne.y);
|
|
174
|
+
visibleBounds = [southwest[0], southwest[1], northeast[0], northeast[1]];
|
|
175
|
+
}
|
|
176
|
+
if (!center.every(Number.isFinite) || !Number.isFinite(metersPerPixel) || metersPerPixel <= 0 || visibleBounds && !visibleBounds.every(Number.isFinite)) {
|
|
177
|
+
throw new Error("FirstPersonViewport ground footprint is invalid");
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
viewport: { width, height },
|
|
181
|
+
view: {
|
|
182
|
+
longitude: center[0],
|
|
183
|
+
latitude: center[1],
|
|
184
|
+
altitude: eye[2],
|
|
185
|
+
zoom: Math.max(0, Math.log2(HALF_WORLD_METERS * 2 / (TILE_SIZE * metersPerPixel)))
|
|
186
|
+
},
|
|
187
|
+
projection: { type: "web-mercator", visibleBounds },
|
|
188
|
+
camera,
|
|
189
|
+
tileBuffer: 1
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/globe_view_adapter.ts
|
|
194
|
+
var DECK_TO_TANGRAM_ZOOM_OFFSET2 = 1;
|
|
195
|
+
var GlobeViewAdapter = class {
|
|
196
|
+
/** Converts globe common-space matrices and visible geographic bounds. */
|
|
197
|
+
static getFrame = getFrame2;
|
|
198
|
+
};
|
|
199
|
+
function getFrame2(viewport, options = {}) {
|
|
200
|
+
if (options.maxElevation !== void 0 && (!Number.isFinite(options.maxElevation) || options.maxElevation < 0)) {
|
|
201
|
+
throw new Error("GlobeView maxElevation must be a finite non-negative number");
|
|
202
|
+
}
|
|
203
|
+
if (!viewport || !Number.isFinite(viewport.width) || !Number.isFinite(viewport.height) || !viewport.viewMatrix || viewport.viewMatrix.length !== 16 || !viewport.projectionMatrix || viewport.projectionMatrix.length !== 16 || !viewport.cameraPosition || viewport.cameraPosition.length !== 3 || !Array.from(viewport.cameraPosition).every(Number.isFinite) || typeof viewport.getBounds !== "function") {
|
|
204
|
+
throw new Error("deck GlobeViewport matrices, camera position, size, and visible bounds are required");
|
|
205
|
+
}
|
|
206
|
+
const visibleBounds = options.visibleBounds ?? viewport.getBounds({ z: 0 });
|
|
207
|
+
if (!Array.isArray(visibleBounds) || visibleBounds.length !== 4 || visibleBounds.some((value) => !Number.isFinite(value))) {
|
|
208
|
+
throw new Error("deck GlobeViewport must provide finite geographic bounds");
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
viewport: { width: viewport.width, height: viewport.height },
|
|
212
|
+
view: {
|
|
213
|
+
longitude: viewport.longitude,
|
|
214
|
+
latitude: viewport.latitude,
|
|
215
|
+
zoom: Math.max(0, viewport.zoom + DECK_TO_TANGRAM_ZOOM_OFFSET2)
|
|
216
|
+
},
|
|
217
|
+
projection: {
|
|
218
|
+
type: "globe",
|
|
219
|
+
visibleBounds: [visibleBounds[0], visibleBounds[1], visibleBounds[2], visibleBounds[3]],
|
|
220
|
+
...options.maxElevation === void 0 ? {} : { maxElevation: options.maxElevation }
|
|
221
|
+
},
|
|
222
|
+
camera: {
|
|
223
|
+
view: new Float64Array(viewport.viewMatrix),
|
|
224
|
+
projection: new Float32Array(
|
|
225
|
+
multiplyMatrices2(viewport.projectionMatrix, viewport.viewMatrix)
|
|
226
|
+
),
|
|
227
|
+
position: [viewport.cameraPosition[0], viewport.cameraPosition[1], viewport.cameraPosition[2]]
|
|
228
|
+
},
|
|
229
|
+
tileBuffer: 0,
|
|
230
|
+
globePreloadZoom: options.preloadZoom
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function multiplyMatrices2(left, right) {
|
|
234
|
+
const result = new Float64Array(16);
|
|
235
|
+
for (let column = 0; column < 4; column++) {
|
|
236
|
+
for (let row = 0; row < 4; row++) {
|
|
237
|
+
let value = 0;
|
|
238
|
+
for (let index = 0; index < 4; index++) {
|
|
239
|
+
value += left[index * 4 + row] * right[column * 4 + index];
|
|
240
|
+
}
|
|
241
|
+
result[column * 4 + row] = value;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/tangram-layer.ts
|
|
248
|
+
var VIEW_EPSILON = 1e-7;
|
|
249
|
+
function injectNextzenApiKey(config, apiKey) {
|
|
250
|
+
if (!apiKey || !config || !config.sources) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
let updated = false;
|
|
254
|
+
for (const source of Object.values(config.sources)) {
|
|
255
|
+
if (source && typeof source.url === "string" && source.url.includes("nextzen.org")) {
|
|
256
|
+
source.url_params = source.url_params || {};
|
|
257
|
+
source.url_params.api_key = apiKey;
|
|
258
|
+
updated = true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return updated;
|
|
262
|
+
}
|
|
263
|
+
function getFirstPersonViewFrame(viewport, options = {}) {
|
|
264
|
+
return FirstPersonViewAdapter.getFrame(viewport, options);
|
|
265
|
+
}
|
|
266
|
+
function getGlobeViewFrame(viewport, options = {}) {
|
|
267
|
+
return GlobeViewAdapter.getFrame(viewport, options);
|
|
268
|
+
}
|
|
269
|
+
function createTangramLayerClass({ Layer: Layer2, ClassicWebGLRenderer, Renderer: Renderer2 }, viewAdapter) {
|
|
270
|
+
const rendererClass = ClassicWebGLRenderer || Renderer2;
|
|
271
|
+
if (!Layer2 || !rendererClass) {
|
|
272
|
+
throw new Error("createTangramLayerClass requires Layer and ClassicWebGLRenderer");
|
|
273
|
+
}
|
|
274
|
+
class TangramLayer extends Layer2 {
|
|
275
|
+
/** Forward deck.gl's property objects without changing its constructor semantics. */
|
|
276
|
+
constructor(...properties) {
|
|
277
|
+
super(...properties);
|
|
278
|
+
}
|
|
279
|
+
initializeState(_context) {
|
|
280
|
+
this.setState({ tangramRecord: null });
|
|
281
|
+
}
|
|
282
|
+
updateState({ props }) {
|
|
283
|
+
let record = this.state.tangramRecord;
|
|
284
|
+
const shouldCreateScene = !record || record.sceneSource !== props.scene || record.sceneBasePath !== props.sceneBasePath || record.apiKey !== props.apiKey || record.projectionEngine !== (props.projectionEngine ?? void 0) || record.projectionEngineExecution !== (props.projectionEngineExecution ?? void 0) || Boolean(record.terrain) !== Boolean(props.terrain) || record.terrainResourceBudget !== (props.terrainResourceBudget ?? void 0) || record.maxConcurrentTileLoadsPerWorker !== (props.maxConcurrentTileLoadsPerWorker ?? void 0);
|
|
285
|
+
if (shouldCreateScene) {
|
|
286
|
+
this._disposeTangramRecord(record);
|
|
287
|
+
record = this._createTangramRecord(props);
|
|
288
|
+
this.setState({ tangramRecord: record });
|
|
289
|
+
}
|
|
290
|
+
if (record) {
|
|
291
|
+
record.owner = this;
|
|
292
|
+
if (record.terrain !== props.terrain) {
|
|
293
|
+
record.terrain = props.terrain;
|
|
294
|
+
const terrain = props.terrain;
|
|
295
|
+
const revision = record.terrainRevision = (record.terrainRevision || 0) + 1;
|
|
296
|
+
record.terrainPendingRevision = revision;
|
|
297
|
+
Promise.resolve(record.loadPromise).then(async () => {
|
|
298
|
+
try {
|
|
299
|
+
if (!record.disposed && revision === record.terrainRevision && terrain) {
|
|
300
|
+
await record.renderer.setTerrain(terrain);
|
|
301
|
+
if (!record.disposed && revision === record.terrainRevision) {
|
|
302
|
+
record.terrainPendingRevision = 0;
|
|
303
|
+
record.renderer.setTerrainExaggeration(record.owner.props.terrainExaggeration ?? 1);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} finally {
|
|
307
|
+
if (record.terrainPendingRevision === revision) record.terrainPendingRevision = 0;
|
|
308
|
+
}
|
|
309
|
+
}).catch((error) => {
|
|
310
|
+
if (!record.disposed && revision === record.terrainRevision) this._raiseBridgeError(normalizeError(error));
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
if (props.terrain && !record.terrainPendingRevision) {
|
|
314
|
+
try {
|
|
315
|
+
record.renderer.setTerrainExaggeration(props.terrainExaggeration ?? 1);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
this._raiseBridgeError(normalizeError(error));
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
draw({ renderPass } = {}) {
|
|
323
|
+
const record = this.state.tangramRecord;
|
|
324
|
+
if (!record || record.disposed) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
record.owner = this;
|
|
328
|
+
this._synchronizeTangramScene(record);
|
|
329
|
+
if (!this._canRender(record, this.props)) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const renderTangram = () => {
|
|
333
|
+
const update_options = { force: true };
|
|
334
|
+
renderPass = renderPass || this.context.renderPass;
|
|
335
|
+
if (renderPass) {
|
|
336
|
+
update_options.renderPass = renderPass;
|
|
337
|
+
}
|
|
338
|
+
const rendered = record.renderer.render(update_options);
|
|
339
|
+
if (record.scene.config && record.scene.config.scene && record.scene.config.scene.animated === true) {
|
|
340
|
+
this.setNeedsRedraw();
|
|
341
|
+
}
|
|
342
|
+
if (rendered && record.gl) {
|
|
343
|
+
record.gl.depthMask(true);
|
|
344
|
+
record.gl.clear(record.gl.DEPTH_BUFFER_BIT | record.gl.STENCIL_BUFFER_BIT);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
if (record.device.type === "webgl") {
|
|
348
|
+
record.scene.withWebGLContext(renderTangram);
|
|
349
|
+
} else {
|
|
350
|
+
renderTangram();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
finalizeState(_context) {
|
|
354
|
+
this._disposeTangramRecord(this.state && this.state.tangramRecord);
|
|
355
|
+
}
|
|
356
|
+
/** Asynchronously query a view-local or canvas CSS pixel; the host must keep drawing. */
|
|
357
|
+
getFeatureAt(pixel, options = {}) {
|
|
358
|
+
const record = this.state.tangramRecord;
|
|
359
|
+
if (!record || record.disposed || record.loadFailed) return Promise.resolve(void 0);
|
|
360
|
+
const pending = record.renderer.getFeatureAt(pixel, options);
|
|
361
|
+
record.owner.setNeedsRedraw();
|
|
362
|
+
return pending;
|
|
363
|
+
}
|
|
364
|
+
/** Intersect the actual rendered eye with a host-owned terrain mesh in content coordinates. */
|
|
365
|
+
getTerrainAt(pixel, surface, options = {}) {
|
|
366
|
+
const record = this.state && this.state.tangramRecord;
|
|
367
|
+
if (!record || record.disposed || record.loadFailed || !record.loaded) return null;
|
|
368
|
+
return record.renderer.getTerrainAt(pixel, surface, options);
|
|
369
|
+
}
|
|
370
|
+
/** Exact admitted ground triangles at the last frame, including temporal relief and globe projection. */
|
|
371
|
+
getDrapedTerrainSurface() {
|
|
372
|
+
const record = this.state?.tangramRecord;
|
|
373
|
+
return record && !record.disposed && record.loaded && !record.loadFailed ? record.renderer.getDrapedTerrainSurface() : null;
|
|
374
|
+
}
|
|
375
|
+
/** Resident worker preparation costs; GPU, source and scratch allocations are separate. */
|
|
376
|
+
getTerrainDrapingStatistics() {
|
|
377
|
+
const record = this.state?.tangramRecord;
|
|
378
|
+
return record && !record.disposed ? record.renderer.getTerrainDrapingStatistics() : void 0;
|
|
379
|
+
}
|
|
380
|
+
get isLoaded() {
|
|
381
|
+
const record = this.state && this.state.tangramRecord;
|
|
382
|
+
return Boolean(record && record.loaded && !record.loadFailed && !record.disposed);
|
|
383
|
+
}
|
|
384
|
+
_createTangramRecord(props) {
|
|
385
|
+
if (!props.scene) {
|
|
386
|
+
this._raiseBridgeError(new Error("scene is required"));
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
const deckCanvas = this.context.deck && this.context.deck.getCanvas();
|
|
390
|
+
const device = this.context.device;
|
|
391
|
+
if (!deckCanvas) {
|
|
392
|
+
this._raiseBridgeError(new Error("deck canvas is required"));
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
if (!device || device.type !== "webgl" && device.type !== "webgpu" || typeof device.createBuffer !== "function" || typeof device.createShader !== "function" || typeof device.createTexture !== "function" || typeof device.createRenderPipeline !== "function" || typeof device.createVertexArray !== "function") {
|
|
396
|
+
this._raiseBridgeError(new Error("a deck.gl luma.gl Device is required"));
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
let gl = null;
|
|
400
|
+
if (device.type === "webgl") {
|
|
401
|
+
gl = device.handle;
|
|
402
|
+
if (!gl || typeof device.pushState !== "function" || typeof device.popState !== "function") {
|
|
403
|
+
this._raiseBridgeError(new Error("a deck.gl WebGLDevice is required"));
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
if (gl.canvas !== deckCanvas) {
|
|
407
|
+
this._raiseBridgeError(
|
|
408
|
+
new Error("deck canvas and WebGLDevice handle must share a context")
|
|
409
|
+
);
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const record = {
|
|
414
|
+
owner: this,
|
|
415
|
+
renderer: null,
|
|
416
|
+
scene: null,
|
|
417
|
+
device,
|
|
418
|
+
gl,
|
|
419
|
+
deckCanvas,
|
|
420
|
+
sceneSource: props.scene,
|
|
421
|
+
sceneBasePath: props.sceneBasePath,
|
|
422
|
+
apiKey: props.apiKey,
|
|
423
|
+
projectionEngine: props.projectionEngine ?? void 0,
|
|
424
|
+
terrain: props.terrain,
|
|
425
|
+
terrainResourceBudget: props.terrainResourceBudget ?? void 0,
|
|
426
|
+
terrainPendingRevision: 0,
|
|
427
|
+
projectionEngineExecution: props.projectionEngineExecution ?? void 0,
|
|
428
|
+
maxConcurrentTileLoadsPerWorker: props.maxConcurrentTileLoadsPerWorker ?? void 0,
|
|
429
|
+
canvasWidth: null,
|
|
430
|
+
canvasHeight: null,
|
|
431
|
+
disposed: false,
|
|
432
|
+
destroyed: false,
|
|
433
|
+
loadSettled: false,
|
|
434
|
+
loadFailed: false,
|
|
435
|
+
loaded: false,
|
|
436
|
+
lastSceneError: null,
|
|
437
|
+
lastViewportError: null,
|
|
438
|
+
reportedViewportError: null,
|
|
439
|
+
loadPromise: null,
|
|
440
|
+
webglScopeDepth: 0
|
|
441
|
+
};
|
|
442
|
+
let renderer;
|
|
443
|
+
try {
|
|
444
|
+
const renderer_options = {
|
|
445
|
+
device,
|
|
446
|
+
canvas: deckCanvas,
|
|
447
|
+
projectionEngine: record.projectionEngine,
|
|
448
|
+
terrain: record.terrain,
|
|
449
|
+
terrainResourceBudget: record.terrainResourceBudget,
|
|
450
|
+
projectionEngineExecution: record.projectionEngineExecution,
|
|
451
|
+
maxConcurrentTileLoadsPerWorker: record.maxConcurrentTileLoadsPerWorker,
|
|
452
|
+
requestRedraw: () => {
|
|
453
|
+
if (!record.disposed && record.owner.setNeedsRedraw) {
|
|
454
|
+
record.owner.setNeedsRedraw();
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
continuousZoom: true,
|
|
458
|
+
highDensityDisplay: true,
|
|
459
|
+
logLevel: "warn"
|
|
460
|
+
};
|
|
461
|
+
if (gl) {
|
|
462
|
+
renderer_options.webGLContext = gl;
|
|
463
|
+
renderer_options.webGLContextScope = (callback) => this._withDeviceState(record, callback);
|
|
464
|
+
}
|
|
465
|
+
renderer = rendererClass.create(props.scene, renderer_options);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
this._raiseBridgeError(normalizeError(error));
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
record.renderer = renderer;
|
|
471
|
+
record.scene = renderer.scene;
|
|
472
|
+
const liveRecord = record;
|
|
473
|
+
renderer.subscribe({
|
|
474
|
+
load: (message) => {
|
|
475
|
+
injectNextzenApiKey(message.config, record.apiKey);
|
|
476
|
+
},
|
|
477
|
+
update: () => this._updateAttributions(liveRecord),
|
|
478
|
+
error: (message) => this._reportSceneError(liveRecord, normalizeError(message), message.type !== "scene_import")
|
|
479
|
+
});
|
|
480
|
+
this._synchronizeTangramScene(liveRecord);
|
|
481
|
+
record.loadPromise = Promise.resolve().then(() => {
|
|
482
|
+
if (record.disposed) {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
return renderer.load(props.scene, {
|
|
486
|
+
// Creation already rejected missing scene input; this closure preserves that identity.
|
|
487
|
+
base_path: props.sceneBasePath,
|
|
488
|
+
blocking: false
|
|
489
|
+
});
|
|
490
|
+
}).then((result) => {
|
|
491
|
+
if (!record.disposed) {
|
|
492
|
+
record.loaded = true;
|
|
493
|
+
record.owner.setNeedsRedraw && record.owner.setNeedsRedraw();
|
|
494
|
+
record.owner.props.onSceneLoad(liveRecord.scene);
|
|
495
|
+
this._updateAttributions(liveRecord);
|
|
496
|
+
}
|
|
497
|
+
return result;
|
|
498
|
+
}).catch((error) => {
|
|
499
|
+
if (!record.disposed) {
|
|
500
|
+
record.loadFailed = true;
|
|
501
|
+
this._reportSceneError(liveRecord, normalizeError(error));
|
|
502
|
+
}
|
|
503
|
+
}).finally(() => {
|
|
504
|
+
record.loadSettled = true;
|
|
505
|
+
if (record.disposed) {
|
|
506
|
+
this._destroyTangramRecord(liveRecord);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
return liveRecord;
|
|
510
|
+
}
|
|
511
|
+
/** Refresh credits after source recreation; ignore stale metadata and disposed layers. */
|
|
512
|
+
_updateAttributions(record) {
|
|
513
|
+
if (record.disposed || typeof record.renderer.getAttributions !== "function") return;
|
|
514
|
+
const generation = record.attributionGeneration = (record.attributionGeneration || 0) + 1;
|
|
515
|
+
Promise.resolve().then(() => record.disposed ? void 0 : record.renderer.getAttributions()).then((credits) => {
|
|
516
|
+
if (credits && !record.disposed && generation === record.attributionGeneration) {
|
|
517
|
+
record.owner.props.onAttributionChange?.(credits, record.scene);
|
|
518
|
+
}
|
|
519
|
+
}).catch((error) => {
|
|
520
|
+
if (!record.disposed && generation === record.attributionGeneration) {
|
|
521
|
+
this._reportSceneError(record, normalizeError(error), false);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
_synchronizeTangramScene(record) {
|
|
526
|
+
const viewport = this.context.viewport;
|
|
527
|
+
const viewports = this.context.deck.getViewports ? this.context.deck.getViewports() : [viewport];
|
|
528
|
+
if (viewAdapter) {
|
|
529
|
+
try {
|
|
530
|
+
if (viewports.length !== 1) throw new Error("only one deck.gl viewport is supported");
|
|
531
|
+
const width2 = record.deckCanvas.clientWidth || viewport.width;
|
|
532
|
+
const height2 = record.deckCanvas.clientHeight || viewport.height;
|
|
533
|
+
const frame2 = viewAdapter.getFrame(viewport, this.props, { width: width2, height: height2 });
|
|
534
|
+
record.renderer.setFrame({ ...frame2, tileResources: this.props.tileResources ?? void 0 });
|
|
535
|
+
record.lastViewportError = null;
|
|
536
|
+
record.reportedViewportError = null;
|
|
537
|
+
} catch (error) {
|
|
538
|
+
record.lastViewportError = error.message;
|
|
539
|
+
this._raiseViewportError(record, normalizeError(error));
|
|
540
|
+
}
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const globeOptions = {
|
|
544
|
+
maxElevation: this.props.globeMaxElevation ?? void 0,
|
|
545
|
+
visibleBounds: this.props.globeVisibleBounds ?? void 0,
|
|
546
|
+
preloadZoom: this.props.globePreloadZoom ?? void 0
|
|
547
|
+
};
|
|
548
|
+
const firstPersonOptions = {
|
|
549
|
+
maxGroundExtent: this.props.firstPersonMaxGroundExtent ?? void 0,
|
|
550
|
+
elevationRange: this.props.firstPersonElevationRange ?? void 0
|
|
551
|
+
};
|
|
552
|
+
const viewportError = validateViewport(viewport, viewports, globeOptions, firstPersonOptions);
|
|
553
|
+
if (viewportError) {
|
|
554
|
+
record.lastViewportError = viewportError.message;
|
|
555
|
+
this._raiseViewportError(record, viewportError);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
record.lastViewportError = null;
|
|
559
|
+
record.reportedViewportError = null;
|
|
560
|
+
const width = record.deckCanvas.clientWidth || viewport.width;
|
|
561
|
+
const height = record.deckCanvas.clientHeight || viewport.height;
|
|
562
|
+
record.canvasWidth = width;
|
|
563
|
+
record.canvasHeight = height;
|
|
564
|
+
const frame = isGlobeViewport(viewport) ? { ...getGlobeViewFrame(viewport, globeOptions), viewport: { width, height } } : isFirstPersonViewport(viewport) ? getFirstPersonViewFrame(viewport, { width, height, ...firstPersonOptions }) : getMapViewFrame(viewport, { width, height });
|
|
565
|
+
record.renderer.setFrame({ ...frame, tileResources: this.props.tileResources ?? void 0 });
|
|
566
|
+
}
|
|
567
|
+
_canRender(record, props) {
|
|
568
|
+
return props.visible !== false && props.opacity !== 0 && record.loaded && !record.loadFailed && !record.lastViewportError;
|
|
569
|
+
}
|
|
570
|
+
_raiseViewportError(record, error) {
|
|
571
|
+
if (record.reportedViewportError === error.message) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
record.reportedViewportError = error.message;
|
|
575
|
+
this._raiseBridgeError(error);
|
|
576
|
+
}
|
|
577
|
+
_reportSceneError(record, error, isFatal = true) {
|
|
578
|
+
if (record.disposed || record.lastSceneError === error.message) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
record.lastSceneError = error.message;
|
|
582
|
+
if (isFatal) {
|
|
583
|
+
record.loadFailed = true;
|
|
584
|
+
}
|
|
585
|
+
record.owner.props.onSceneError(error, record.scene);
|
|
586
|
+
record.owner.raiseError(error, "TangramLayer scene");
|
|
587
|
+
}
|
|
588
|
+
_raiseBridgeError(error) {
|
|
589
|
+
this.raiseError(error, "TangramLayer bridge");
|
|
590
|
+
}
|
|
591
|
+
_disposeTangramRecord(record) {
|
|
592
|
+
if (!record || record.disposed) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
record.disposed = true;
|
|
596
|
+
if (!record.loadPromise || record.loadSettled) {
|
|
597
|
+
this._destroyTangramRecord(record);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
_destroyTangramRecord(record) {
|
|
601
|
+
if (!record.destroyed) {
|
|
602
|
+
record.destroyed = true;
|
|
603
|
+
record.renderer.destroy();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
_withDeviceState(record, callback) {
|
|
607
|
+
if (record.webglScopeDepth > 0) {
|
|
608
|
+
return callback();
|
|
609
|
+
}
|
|
610
|
+
const { device, gl } = record;
|
|
611
|
+
const lumaState = gl.lumaState;
|
|
612
|
+
const hasTrackedProgram = Boolean(lumaState && "program" in lumaState);
|
|
613
|
+
const previousProgram = hasTrackedProgram ? lumaState.program : gl.getParameter(gl.CURRENT_PROGRAM);
|
|
614
|
+
record.webglScopeDepth++;
|
|
615
|
+
device.pushState();
|
|
616
|
+
try {
|
|
617
|
+
return callback();
|
|
618
|
+
} finally {
|
|
619
|
+
try {
|
|
620
|
+
device.popState();
|
|
621
|
+
gl.useProgram(previousProgram);
|
|
622
|
+
} finally {
|
|
623
|
+
record.webglScopeDepth--;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
TangramLayer.layerName = "TangramLayer";
|
|
629
|
+
TangramLayer.defaultProps = {
|
|
630
|
+
projectionEngine: null,
|
|
631
|
+
projectionEngineExecution: null,
|
|
632
|
+
scene: null,
|
|
633
|
+
sceneBasePath: null,
|
|
634
|
+
apiKey: null,
|
|
635
|
+
globeMaxElevation: null,
|
|
636
|
+
globeVisibleBounds: null,
|
|
637
|
+
globePreloadZoom: null,
|
|
638
|
+
tileResources: null,
|
|
639
|
+
maxConcurrentTileLoadsPerWorker: null,
|
|
640
|
+
firstPersonMaxGroundExtent: 2e4,
|
|
641
|
+
terrain: { type: "object", value: null, compare: false },
|
|
642
|
+
terrainResourceBudget: { type: "object", value: null, compare: false },
|
|
643
|
+
terrainExaggeration: { type: "number", value: 1, min: 0, max: 1e3 },
|
|
644
|
+
firstPersonElevationRange: { type: "array", value: [0, 0], compare: true },
|
|
645
|
+
onSceneLoad: () => {
|
|
646
|
+
},
|
|
647
|
+
onAttributionChange: () => {
|
|
648
|
+
},
|
|
649
|
+
onSceneError: () => {
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
return TangramLayer;
|
|
653
|
+
}
|
|
654
|
+
function validateViewport(viewport, viewports, globeOptions = {}, firstPersonOptions = {}) {
|
|
655
|
+
if (viewports.length !== 1) {
|
|
656
|
+
return new Error("only one deck.gl viewport is supported");
|
|
657
|
+
}
|
|
658
|
+
const globe = isGlobeViewport(viewport);
|
|
659
|
+
const firstPerson = isFirstPersonViewport(viewport);
|
|
660
|
+
const geographicAnchorError = WebMercatorViewAdapter.validateGeographicAnchor(
|
|
661
|
+
viewport,
|
|
662
|
+
globe ? "GlobeViewport" : "Web Mercator viewport"
|
|
663
|
+
);
|
|
664
|
+
if (geographicAnchorError) {
|
|
665
|
+
return globe ? geographicAnchorError : new Error("a Web Mercator viewport is required");
|
|
666
|
+
}
|
|
667
|
+
if (!globe) {
|
|
668
|
+
const webMercatorError = WebMercatorViewAdapter.validateViewport(viewport);
|
|
669
|
+
if (webMercatorError) {
|
|
670
|
+
return webMercatorError;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (!Number.isFinite(viewport.bearing || 0) || !Number.isFinite(viewport.pitch || 0) || (viewport.pitch || 0) < (firstPerson ? -90 : -VIEW_EPSILON) || (firstPerson ? (viewport.pitch || 0) > 90 : (viewport.pitch || 0) >= 90)) {
|
|
674
|
+
return new Error("bearing and pitch must describe a finite deck.gl camera");
|
|
675
|
+
}
|
|
676
|
+
try {
|
|
677
|
+
if (globe) {
|
|
678
|
+
getGlobeViewFrame(viewport, globeOptions);
|
|
679
|
+
} else if (isFirstPersonViewport(viewport)) {
|
|
680
|
+
getFirstPersonViewFrame(viewport, firstPersonOptions);
|
|
681
|
+
} else {
|
|
682
|
+
WebMercatorViewAdapter.getCameraFrame(viewport);
|
|
683
|
+
}
|
|
684
|
+
} catch (error) {
|
|
685
|
+
return error;
|
|
686
|
+
}
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
function getMapViewFrame(viewport, { width, height }) {
|
|
690
|
+
return WebMercatorViewAdapter.getFrame(viewport, { width, height });
|
|
691
|
+
}
|
|
692
|
+
function isFirstPersonViewport(viewport) {
|
|
693
|
+
return Boolean(
|
|
694
|
+
viewport && viewport.constructor && viewport.constructor.displayName === "FirstPersonViewport"
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
function isGlobeViewport(viewport) {
|
|
698
|
+
return Boolean(
|
|
699
|
+
viewport && (viewport.constructor?.displayName === "GlobeViewport" || viewport.constructor?.name === "GlobeViewport")
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
function normalizeError(value) {
|
|
703
|
+
const fields = value;
|
|
704
|
+
if (value instanceof Error) {
|
|
705
|
+
return value;
|
|
706
|
+
}
|
|
707
|
+
if (value && fields.error instanceof Error) {
|
|
708
|
+
return fields.error;
|
|
709
|
+
}
|
|
710
|
+
if (value && fields.message) {
|
|
711
|
+
return new Error(fields.message);
|
|
712
|
+
}
|
|
713
|
+
return new Error(String(value));
|
|
714
|
+
}
|
|
715
|
+
var tangram_layer_default = createTangramLayerClass;
|
|
716
|
+
|
|
717
|
+
// src/experimental/projected-navigation.ts
|
|
718
|
+
import {
|
|
719
|
+
getProjectedCoordinateOptions,
|
|
720
|
+
PROJECTED_COMMON_SCALE,
|
|
721
|
+
countProjectedTileCoordinates,
|
|
722
|
+
normalizeProjectedBasemapOptions,
|
|
723
|
+
ProjectionBatchExecutor
|
|
724
|
+
} from "@tangram.gl/tangram-renderer/core";
|
|
725
|
+
function getProjectedGeographicBounds(type) {
|
|
726
|
+
normalizeProjectedBasemapOptions({ type });
|
|
727
|
+
return type === "albers" ? [-170, 5, -40, 75] : [-180, -85.0511287798066, 180, 85.0511287798066];
|
|
728
|
+
}
|
|
729
|
+
function validateProjectedNavigationViewport(viewport) {
|
|
730
|
+
if (viewport.isGeospatial !== false || viewport.constructor.displayName !== "OrthographicViewport" && viewport.constructor.name !== "OrthographicViewport" || ![viewport.width, viewport.height].every((value) => Number.isFinite(value) && value > 0)) {
|
|
731
|
+
throw new Error("Projected navigation requires a finite deck.gl OrthographicViewport");
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function validateProjectedGeographicBounds(bounds, type) {
|
|
735
|
+
countProjectedTileCoordinates(bounds, 0);
|
|
736
|
+
const domain = getProjectedGeographicBounds(type);
|
|
737
|
+
if (bounds[0] < domain[0] || bounds[1] < domain[1] || bounds[2] > domain[2] || bounds[3] > domain[3]) {
|
|
738
|
+
throw new Error("Projected navigation bounds must remain within the projection domain");
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
var ProjectedBasemapNavigation = class {
|
|
742
|
+
/** Retain a factory without taking ownership of its registrations, grids or lifetime. */
|
|
743
|
+
constructor(engine, options = {}) {
|
|
744
|
+
this.engine = engine;
|
|
745
|
+
if (!engine || typeof engine.createProjection !== "function" || typeof engine.createProjectionAsync !== "function") {
|
|
746
|
+
throw new Error("Projected navigation requires a ProjectionEngine factory");
|
|
747
|
+
}
|
|
748
|
+
this.executor = new ProjectionBatchExecutor(options);
|
|
749
|
+
}
|
|
750
|
+
engine;
|
|
751
|
+
/** Independent compiled forward/reverse CRS pairs, including lazy factory initialization. */
|
|
752
|
+
transforms = /* @__PURE__ */ new Map();
|
|
753
|
+
/** Prevent late asynchronous work from publishing after example teardown. */
|
|
754
|
+
disposed = false;
|
|
755
|
+
/** Bounded host kernel calls shared by forward and reverse requests. */
|
|
756
|
+
executor;
|
|
757
|
+
/** Project a detached longitude/latitude batch into north-positive common coordinates. */
|
|
758
|
+
async projectPositions(coordinates, type, options = {}) {
|
|
759
|
+
const domain = getProjectedGeographicBounds(type);
|
|
760
|
+
if (!(coordinates instanceof Float64Array) || coordinates.length % 2 !== 0) {
|
|
761
|
+
throw new Error("Projected navigation requires finite geographic pairs inside the projection domain");
|
|
762
|
+
}
|
|
763
|
+
if (!coordinates.every((value, index) => Number.isFinite(value) && value >= domain[index % 2] && value <= domain[index % 2 + 2])) {
|
|
764
|
+
throw new Error("Projected navigation requires finite geographic pairs inside the projection domain");
|
|
765
|
+
}
|
|
766
|
+
return this.executor.execute(
|
|
767
|
+
coordinates,
|
|
768
|
+
() => this.getTransform(type, false),
|
|
769
|
+
{ signal: options.signal, scale: PROJECTED_COMMON_SCALE }
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
/** Project a geographic focus point without mutating the supplied pair. */
|
|
773
|
+
async projectPosition(position, type, options = {}) {
|
|
774
|
+
if (position.length !== 2) throw new Error("Projected focus requires a longitude/latitude pair");
|
|
775
|
+
const result = await this.projectPositions(new Float64Array(position), type, options);
|
|
776
|
+
return [result[0], result[1], 0];
|
|
777
|
+
}
|
|
778
|
+
/** Invert ground; finite outside-domain/folded results return null. Engine errors propagate. */
|
|
779
|
+
async unprojectPosition(position, type, options = {}) {
|
|
780
|
+
if (position.length !== 2 || !position.every(Number.isFinite)) throw new Error("Projected inverse requires a finite common pair");
|
|
781
|
+
const point = [position[0], position[1]];
|
|
782
|
+
const requestOptions = { signal: options.signal };
|
|
783
|
+
const result = await this.executor.execute(
|
|
784
|
+
new Float64Array(point.map((value) => value / PROJECTED_COMMON_SCALE)),
|
|
785
|
+
() => this.getTransform(type, true),
|
|
786
|
+
{ ...requestOptions, allowNonfinite: true }
|
|
787
|
+
);
|
|
788
|
+
const domain = getProjectedGeographicBounds(type);
|
|
789
|
+
if (!result.every((value, index) => Number.isFinite(value) && value >= domain[index] - 1e-7 && value <= domain[index + 2] + 1e-7)) return null;
|
|
790
|
+
const geographic = [
|
|
791
|
+
Math.max(domain[0], Math.min(domain[2], result[0])),
|
|
792
|
+
Math.max(domain[1], Math.min(domain[3], result[1]))
|
|
793
|
+
];
|
|
794
|
+
const restored = await this.projectPosition(geographic, type, requestOptions);
|
|
795
|
+
if (Math.hypot(restored[0] - point[0], restored[1] - point[1]) <= 1e-5) return geographic;
|
|
796
|
+
if (domain[0] === -180 && domain[2] === 180 && Math.abs(Math.abs(geographic[0]) - 180) <= 1e-7) {
|
|
797
|
+
const opposite = [geographic[0] < 0 ? 180 : -180, geographic[1]];
|
|
798
|
+
const alternative = await this.projectPosition(opposite, type, requestOptions);
|
|
799
|
+
if (Math.hypot(alternative[0] - point[0], alternative[1] - point[1]) <= 1e-5) return opposite;
|
|
800
|
+
}
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
/** Probe geographic coordinates on ground, not a rendered feature or depth/picking buffer. */
|
|
804
|
+
async unprojectScreenPosition(viewport, pixel, type, options = {}) {
|
|
805
|
+
validateProjectedNavigationViewport(viewport);
|
|
806
|
+
if (pixel.length !== 2 || !pixel.every(Number.isFinite)) throw new Error("Projected probe requires a finite screen pair");
|
|
807
|
+
if (pixel[0] < 0 || pixel[1] < 0 || pixel[0] > viewport.width || pixel[1] > viewport.height) return null;
|
|
808
|
+
const position = viewport.unproject([...pixel], { targetZ: 0 });
|
|
809
|
+
return this.unprojectPosition([position[0], position[1]], type, options);
|
|
810
|
+
}
|
|
811
|
+
/** Bound the viewport's ground rectangle using the fixed projections' separable latitude and longitude scales.
|
|
812
|
+
* Equal Earth's longitude factor decreases with absolute latitude, so both latitude endpoints bound its envelope.
|
|
813
|
+
* Albers uses radial/angular interval bounds about the conic apex, not sparse inverse samples.
|
|
814
|
+
* This does not support wrapped worlds or arbitrary custom projection kernels.
|
|
815
|
+
*/
|
|
816
|
+
async getCameraCoverage(viewport, type, sourceBounds = getProjectedGeographicBounds(type)) {
|
|
817
|
+
if (this.disposed) throw new Error("Projected navigation is disposed");
|
|
818
|
+
validateProjectedNavigationViewport(viewport);
|
|
819
|
+
validateProjectedGeographicBounds(sourceBounds, type);
|
|
820
|
+
const region = [...sourceBounds];
|
|
821
|
+
const corners = [[0, 0], [viewport.width, 0], [0, viewport.height], [viewport.width, viewport.height]].map((pixel) => viewport.unproject(pixel, { targetZ: 0 }));
|
|
822
|
+
if (!corners.every((point) => point.length >= 2 && point.slice(0, 2).every(Number.isFinite))) {
|
|
823
|
+
throw new Error("Projected coverage requires finite ground corners");
|
|
824
|
+
}
|
|
825
|
+
if (type === "albers") return this.getAlbersCoverage(corners, region);
|
|
826
|
+
const minimumX = Math.min(...corners.map((point) => point[0]));
|
|
827
|
+
const maximumX = Math.max(...corners.map((point) => point[0]));
|
|
828
|
+
const minimumY = Math.min(...corners.map((point) => point[1]));
|
|
829
|
+
const maximumY = Math.max(...corners.map((point) => point[1]));
|
|
830
|
+
const southernEdge = await this.projectPosition([0, region[1]], type);
|
|
831
|
+
const northernEdge = await this.projectPosition([0, region[3]], type);
|
|
832
|
+
if (maximumY < southernEdge[1] || minimumY > northernEdge[1]) return { bounds: null, domainFallback: false };
|
|
833
|
+
const south = await this.unprojectPosition([0, Math.max(minimumY, southernEdge[1])], type);
|
|
834
|
+
const north = await this.unprojectPosition([0, Math.min(maximumY, northernEdge[1])], type);
|
|
835
|
+
if (!south || !north) throw new Error("Projection engine could not invert the bounded latitude envelope");
|
|
836
|
+
const southernScale = (await this.projectPosition([180, south[1]], type))[0] / 180;
|
|
837
|
+
const northernScale = (await this.projectPosition([180, north[1]], type))[0] / 180;
|
|
838
|
+
if (![southernScale, northernScale].every((value) => Number.isFinite(value) && value > 0)) {
|
|
839
|
+
throw new Error("Projected coverage requires a positive longitude scale");
|
|
840
|
+
}
|
|
841
|
+
const scales = [southernScale, northernScale];
|
|
842
|
+
if (south[1] < 0 && north[1] > 0) scales.push((await this.projectPosition([180, 0], type))[0] / 180);
|
|
843
|
+
const west = Math.max(region[0], Math.min(...scales.map((scale) => minimumX / scale)));
|
|
844
|
+
const east = Math.min(region[2], Math.max(...scales.map((scale) => maximumX / scale)));
|
|
845
|
+
if (west > east) return { bounds: null, domainFallback: false };
|
|
846
|
+
const margin = 1e-7;
|
|
847
|
+
return { bounds: [
|
|
848
|
+
Math.max(region[0], west - margin),
|
|
849
|
+
Math.max(region[1], south[1] - margin),
|
|
850
|
+
Math.min(region[2], east + margin),
|
|
851
|
+
Math.min(region[3], north[1] + margin)
|
|
852
|
+
], domainFallback: false };
|
|
853
|
+
}
|
|
854
|
+
/** Bound the fixed northern Albers cone: radius decreases with latitude, angle increases with longitude. */
|
|
855
|
+
async getAlbersCoverage(corners, region) {
|
|
856
|
+
const reference = await this.projectPositions(new Float64Array([-96, 5, -86, 5, -96, 75]), "albers");
|
|
857
|
+
const apex = (reference[2] ** 2 + reference[3] ** 2 - reference[1] ** 2) / (2 * (reference[3] - reference[1]));
|
|
858
|
+
const cone = (Math.sin(29.5 * Math.PI / 180) + Math.sin(45.5 * Math.PI / 180)) / 2;
|
|
859
|
+
if (!Number.isFinite(apex) || apex <= reference[5]) throw new Error("Projected coverage requires a northern Albers cone");
|
|
860
|
+
const south = await this.projectPosition([-96, region[1]], "albers");
|
|
861
|
+
const north = await this.projectPosition([-96, region[3]], "albers");
|
|
862
|
+
const minimumRadius = apex - north[1], maximumRadius = apex - south[1];
|
|
863
|
+
const firstAngle = (region[0] + 96) * Math.PI / 180 * cone;
|
|
864
|
+
const lastAngle = (region[2] + 96) * Math.PI / 180 * cone;
|
|
865
|
+
const angles = [firstAngle, lastAngle];
|
|
866
|
+
if (firstAngle <= 0 && lastAngle >= 0) angles.push(0);
|
|
867
|
+
const minimumCosine = Math.min(...angles.map(Math.cos));
|
|
868
|
+
const maximumCosine = Math.max(...angles.map(Math.cos));
|
|
869
|
+
const x = [Math.min(...corners.map((point) => point[0])), Math.max(...corners.map((point) => point[0]))];
|
|
870
|
+
const down = [
|
|
871
|
+
Math.max(minimumRadius * minimumCosine, apex - Math.max(...corners.map((point) => point[1]))),
|
|
872
|
+
Math.min(maximumRadius * maximumCosine, apex - Math.min(...corners.map((point) => point[1])))
|
|
873
|
+
];
|
|
874
|
+
if (down[0] > down[1]) return { bounds: null, domainFallback: false };
|
|
875
|
+
const closestX = Math.max(x[0], Math.min(x[1], 0));
|
|
876
|
+
const nearRadius = Math.max(minimumRadius, Math.hypot(closestX, down[0]));
|
|
877
|
+
const farRadius = Math.min(maximumRadius, Math.max(...x.map((value) => Math.hypot(value, down[1]))));
|
|
878
|
+
const longitude = x.flatMap((value) => down.map((distance) => Math.atan2(value, distance) / cone * 180 / Math.PI - 96));
|
|
879
|
+
const west = Math.max(region[0], Math.min(...longitude)), east = Math.min(region[2], Math.max(...longitude));
|
|
880
|
+
if (nearRadius > farRadius || west > east) return { bounds: null, domainFallback: false };
|
|
881
|
+
const southern = await this.unprojectPosition([0, apex - farRadius], "albers");
|
|
882
|
+
const northern = await this.unprojectPosition([0, apex - nearRadius], "albers");
|
|
883
|
+
if (!southern || !northern) throw new Error("Projection engine could not invert the Albers radius envelope");
|
|
884
|
+
const margin = 1e-7;
|
|
885
|
+
return { bounds: [
|
|
886
|
+
Math.max(region[0], west - margin),
|
|
887
|
+
Math.max(region[1], southern[1] - margin),
|
|
888
|
+
Math.min(region[2], east + margin),
|
|
889
|
+
Math.min(region[3], northern[1] + margin)
|
|
890
|
+
], domainFallback: false };
|
|
891
|
+
}
|
|
892
|
+
/** Preserve geographic focus and numeric zoom; clamp to a smaller destination domain, never mutate the input.
|
|
893
|
+
* A finite outside-domain target falls back to the destination domain center. Engine errors still propagate.
|
|
894
|
+
* Keeping zoom does not imply preserving local ground scale across differently distorted projections.
|
|
895
|
+
*/
|
|
896
|
+
async reprojectViewState(state, from, to) {
|
|
897
|
+
if (state.target.length !== 3 || ![...state.target, state.zoom].every(Number.isFinite)) {
|
|
898
|
+
throw new Error("Projection transition requires a finite target and zoom");
|
|
899
|
+
}
|
|
900
|
+
const point = [state.target[0], state.target[1]];
|
|
901
|
+
const zoom = state.zoom;
|
|
902
|
+
const domain = getProjectedGeographicBounds(to);
|
|
903
|
+
const previous = await this.unprojectPosition(point, from);
|
|
904
|
+
const focus = previous ? [
|
|
905
|
+
Math.max(domain[0], Math.min(domain[2], previous[0])),
|
|
906
|
+
Math.max(domain[1], Math.min(domain[3], previous[1]))
|
|
907
|
+
] : [(domain[0] + domain[2]) / 2, (domain[1] + domain[3]) / 2];
|
|
908
|
+
return {
|
|
909
|
+
viewState: { target: await this.projectPosition(focus, to), zoom },
|
|
910
|
+
focus,
|
|
911
|
+
clamped: Boolean(previous && (focus[0] !== previous[0] || focus[1] !== previous[1])),
|
|
912
|
+
domainFallback: previous === null
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
/** Fit a sampled geographic extent; this is navigation, not a conservative visibility proof. */
|
|
916
|
+
async fitBounds(bounds, dimensions, type, options = {}) {
|
|
917
|
+
validateProjectedGeographicBounds(bounds, type);
|
|
918
|
+
const { padding = 24, minZoom = -10, maxZoom = 10 } = options;
|
|
919
|
+
if (![dimensions.width, dimensions.height, padding, minZoom, maxZoom].every(Number.isFinite) || padding < 0 || dimensions.width <= 2 * padding || dimensions.height <= 2 * padding || minZoom > maxZoom) {
|
|
920
|
+
throw new Error("Projected fit requires usable CSS dimensions, padding and an ordered zoom range");
|
|
921
|
+
}
|
|
922
|
+
const coordinates = [];
|
|
923
|
+
for (let column = 0; column <= 32; column++) for (let row = 0; row <= 32; row++) {
|
|
924
|
+
coordinates.push(
|
|
925
|
+
bounds[0] + (bounds[2] - bounds[0]) * column / 32,
|
|
926
|
+
bounds[1] + (bounds[3] - bounds[1]) * row / 32
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
const positions = await this.projectPositions(new Float64Array(coordinates), type);
|
|
930
|
+
let west = Infinity, south = Infinity, east = -Infinity, north = -Infinity;
|
|
931
|
+
for (let index = 0; index < positions.length; index += 2) {
|
|
932
|
+
west = Math.min(west, positions[index]);
|
|
933
|
+
east = Math.max(east, positions[index]);
|
|
934
|
+
south = Math.min(south, positions[index + 1]);
|
|
935
|
+
north = Math.max(north, positions[index + 1]);
|
|
936
|
+
}
|
|
937
|
+
const zoom = Math.log2(Math.min(
|
|
938
|
+
(dimensions.width - 2 * padding) / Math.max(east - west, 1e-9),
|
|
939
|
+
(dimensions.height - 2 * padding) / Math.max(north - south, 1e-9)
|
|
940
|
+
));
|
|
941
|
+
return { target: [(west + east) / 2, (south + north) / 2, 0], zoom: Math.max(minZoom, Math.min(maxZoom, zoom)) };
|
|
942
|
+
}
|
|
943
|
+
/** Release navigation-owned caches; leave the shared engine usable by the renderer. */
|
|
944
|
+
dispose() {
|
|
945
|
+
this.disposed = true;
|
|
946
|
+
this.executor.dispose();
|
|
947
|
+
this.transforms.clear();
|
|
948
|
+
}
|
|
949
|
+
/** Detached host execution work, independent of decoded/GPU cache residency. */
|
|
950
|
+
getExecutionStatistics() {
|
|
951
|
+
return this.executor.getStatistics();
|
|
952
|
+
}
|
|
953
|
+
/** Share concurrent compilation, allow retry after failure and reject late results on teardown. */
|
|
954
|
+
async getTransform(type, reverse) {
|
|
955
|
+
if (this.disposed) throw new Error("Projected navigation is disposed");
|
|
956
|
+
const options = getProjectedCoordinateOptions(type);
|
|
957
|
+
const key = `${type}:${reverse}`;
|
|
958
|
+
let pending = this.transforms.get(key);
|
|
959
|
+
if (!pending) {
|
|
960
|
+
pending = Promise.resolve().then(() => this.engine.createProjectionAsync(reverse ? { from: options.to, to: options.from } : options));
|
|
961
|
+
this.transforms.set(key, pending);
|
|
962
|
+
pending.catch(() => {
|
|
963
|
+
if (this.transforms.get(key) === pending) this.transforms.delete(key);
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
const transform = await pending;
|
|
967
|
+
if (this.disposed) throw new Error("Projected navigation is disposed");
|
|
968
|
+
return transform;
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
// src/experimental/projected-detail.ts
|
|
973
|
+
import {
|
|
974
|
+
countProjectedTileCoordinates as countProjectedTileCoordinates2,
|
|
975
|
+
WebMercatorVisibilityAdapter,
|
|
976
|
+
projectGeographicPosition,
|
|
977
|
+
getTileGeographicBounds
|
|
978
|
+
} from "@tangram.gl/tangram-renderer/core";
|
|
979
|
+
async function selectProjectedTileDetail(navigation, viewport, type, options) {
|
|
980
|
+
validateProjectedNavigationViewport(viewport);
|
|
981
|
+
const {
|
|
982
|
+
visibleBounds,
|
|
983
|
+
minZoom = 0,
|
|
984
|
+
maxZoom = 6,
|
|
985
|
+
targetTilePixels = 256,
|
|
986
|
+
maxTiles = 256,
|
|
987
|
+
currentTileZoom,
|
|
988
|
+
hysteresis = 0.15
|
|
989
|
+
} = options;
|
|
990
|
+
validateProjectedGeographicBounds(visibleBounds, type);
|
|
991
|
+
if (![minZoom, maxZoom].every((value) => Number.isSafeInteger(value) && value >= 0 && value <= 6) || minZoom > maxZoom || !Number.isFinite(targetTilePixels) || targetTilePixels <= 0 || !Number.isSafeInteger(maxTiles) || maxTiles < 1 || !Number.isFinite(hysteresis) || hysteresis < 0 || hysteresis >= 0.5 || currentTileZoom !== void 0 && (!Number.isSafeInteger(currentTileZoom) || currentTileZoom < 0 || currentTileZoom > 6)) {
|
|
992
|
+
throw new Error("Projected tile detail requires ordered levels 0\u20136, positive pixel/candidate limits and finite hysteresis");
|
|
993
|
+
}
|
|
994
|
+
const candidates = [];
|
|
995
|
+
for (let zoom = minZoom; zoom <= maxZoom; zoom++) {
|
|
996
|
+
const count = countProjectedTileCoordinates2(visibleBounds, zoom);
|
|
997
|
+
if (count > maxTiles) break;
|
|
998
|
+
candidates.push({ zoom, count });
|
|
999
|
+
}
|
|
1000
|
+
if (!candidates.length) throw new Error("Projected minimum tile detail exceeds the candidate budget");
|
|
1001
|
+
const southwest = projectGeographicPosition([visibleBounds[0], visibleBounds[1], 0], "web-mercator");
|
|
1002
|
+
const northeast = projectGeographicPosition([visibleBounds[2], visibleBounds[3], 0], "web-mercator");
|
|
1003
|
+
const traversal = new WebMercatorVisibilityAdapter();
|
|
1004
|
+
const measurements = /* @__PURE__ */ new Map();
|
|
1005
|
+
const measure = async (zoom) => {
|
|
1006
|
+
const cached = measurements.get(zoom);
|
|
1007
|
+
if (cached !== void 0) return cached;
|
|
1008
|
+
const tiles = traversal.findVisibleTileCoordinates({
|
|
1009
|
+
center: { lng: 0, lat: 0 },
|
|
1010
|
+
zoom,
|
|
1011
|
+
tile_zoom: zoom,
|
|
1012
|
+
size: { css: { width: viewport.width, height: viewport.height } },
|
|
1013
|
+
buffer: 0,
|
|
1014
|
+
wrap: false,
|
|
1015
|
+
bounds: { sw: { x: southwest[0], y: southwest[1] }, ne: { x: northeast[0], y: northeast[1] } }
|
|
1016
|
+
});
|
|
1017
|
+
const coordinates = [];
|
|
1018
|
+
for (const tile of tiles) {
|
|
1019
|
+
const bounds = getTileGeographicBounds(tile);
|
|
1020
|
+
const west = Math.max(visibleBounds[0], bounds.west), south = Math.max(visibleBounds[1], bounds.south);
|
|
1021
|
+
const east = Math.min(visibleBounds[2], bounds.east), north = Math.min(visibleBounds[3], bounds.north);
|
|
1022
|
+
for (let column = 0; column <= 2; column++) for (let row = 0; row <= 2; row++) {
|
|
1023
|
+
coordinates.push(west + (east - west) * column / 2, south + (north - south) * row / 2);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const positions = await navigation.projectPositions(new Float64Array(coordinates), type);
|
|
1027
|
+
let maximum = 0;
|
|
1028
|
+
for (let offset = 0; offset < positions.length; offset += 18) {
|
|
1029
|
+
let west = Infinity, south = Infinity, east = -Infinity, north = -Infinity;
|
|
1030
|
+
for (let index = offset; index < offset + 18; index += 2) {
|
|
1031
|
+
const screen = viewport.project([positions[index], positions[index + 1], 0]);
|
|
1032
|
+
if (![screen[0], screen[1]].every(Number.isFinite)) throw new Error("Projected tile detail produced nonfinite screen positions");
|
|
1033
|
+
west = Math.min(west, screen[0]);
|
|
1034
|
+
east = Math.max(east, screen[0]);
|
|
1035
|
+
south = Math.min(south, screen[1]);
|
|
1036
|
+
north = Math.max(north, screen[1]);
|
|
1037
|
+
}
|
|
1038
|
+
maximum = Math.max(maximum, east - west, north - south);
|
|
1039
|
+
}
|
|
1040
|
+
measurements.set(zoom, maximum);
|
|
1041
|
+
return maximum;
|
|
1042
|
+
};
|
|
1043
|
+
let selected = candidates[candidates.length - 1];
|
|
1044
|
+
const current = candidates.find((candidate) => candidate.zoom === currentTileZoom);
|
|
1045
|
+
if (current) {
|
|
1046
|
+
const pixels = await measure(current.zoom);
|
|
1047
|
+
const coarser = candidates.find((candidate) => candidate.zoom === current.zoom - 1);
|
|
1048
|
+
if (pixels <= targetTilePixels * (1 + hysteresis) && (!coarser || await measure(coarser.zoom) > targetTilePixels * (1 - hysteresis))) selected = current;
|
|
1049
|
+
else {
|
|
1050
|
+
for (const candidate of candidates) if (await measure(candidate.zoom) <= targetTilePixels) {
|
|
1051
|
+
selected = candidate;
|
|
1052
|
+
break;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
} else {
|
|
1056
|
+
for (const candidate of candidates) if (await measure(candidate.zoom) <= targetTilePixels) {
|
|
1057
|
+
selected = candidate;
|
|
1058
|
+
break;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
const estimatedTilePixels = await measure(selected.zoom);
|
|
1062
|
+
const unmet = estimatedTilePixels > targetTilePixels * (1 + hysteresis);
|
|
1063
|
+
return {
|
|
1064
|
+
tileZoom: selected.zoom,
|
|
1065
|
+
candidateCount: selected.count,
|
|
1066
|
+
estimatedTilePixels,
|
|
1067
|
+
budgetLimited: unmet && candidates[candidates.length - 1].zoom < maxZoom,
|
|
1068
|
+
detailLimited: unmet && selected.zoom === maxZoom
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// src/experimental/projected-basemaps.ts
|
|
1073
|
+
function getProjectedViewFrame(viewport, options, dimensions = { width: viewport.width, height: viewport.height }) {
|
|
1074
|
+
if (viewport.isGeospatial !== false || viewport.constructor.displayName !== "OrthographicViewport" && viewport.constructor.name !== "OrthographicViewport") {
|
|
1075
|
+
throw new Error("CPU-projected basemaps require deck.gl OrthographicView");
|
|
1076
|
+
}
|
|
1077
|
+
const cameraProjection = new Matrix42(Array.from(viewport.projectionMatrix)).multiplyRight(Array.from(viewport.viewMatrix));
|
|
1078
|
+
const [west, south, east, north] = options.visibleBounds;
|
|
1079
|
+
const projection = normalizeProjectedBasemapOptions2(options.projection);
|
|
1080
|
+
const styleZoom = options.styleZoom ?? options.tileZoom;
|
|
1081
|
+
if (!Number.isSafeInteger(styleZoom) || styleZoom < 0 || styleZoom > 22) {
|
|
1082
|
+
throw new Error("Projected styleZoom must be an integer in [0, 22]");
|
|
1083
|
+
}
|
|
1084
|
+
if (options.maxTiles !== void 0 && (!Number.isSafeInteger(options.maxTiles) || options.maxTiles < 1)) {
|
|
1085
|
+
throw new Error("Projected maxTiles must be a positive safe integer");
|
|
1086
|
+
}
|
|
1087
|
+
if (projection.type === "albers" && (west < -170 || east > -40 || south < 5 || north > 75)) {
|
|
1088
|
+
throw new Error("Initial Albers coverage must remain within [-170, 5, -40, 75] degrees");
|
|
1089
|
+
}
|
|
1090
|
+
const frame = {
|
|
1091
|
+
viewport: dimensions,
|
|
1092
|
+
projection: { type: "projected", visibleBounds: options.visibleBounds },
|
|
1093
|
+
geographicAnchor: { longitude: (west + east) / 2, latitude: (south + north) / 2, zoom: styleZoom },
|
|
1094
|
+
tileZoom: options.tileZoom,
|
|
1095
|
+
tileBuffer: 0,
|
|
1096
|
+
...options.tileResources === void 0 ? {} : { tileResources: options.tileResources },
|
|
1097
|
+
renderViews: [{ id: "projected", camera: {
|
|
1098
|
+
view: new Float64Array(new Matrix42()),
|
|
1099
|
+
projection: new Float64Array(cameraProjection),
|
|
1100
|
+
position: [0, 0, 1]
|
|
1101
|
+
} }]
|
|
1102
|
+
};
|
|
1103
|
+
new HostFrame(frame);
|
|
1104
|
+
if (options.maxTiles !== void 0) {
|
|
1105
|
+
const count = countProjectedTileCoordinates3(options.visibleBounds, options.tileZoom);
|
|
1106
|
+
if (count > options.maxTiles) {
|
|
1107
|
+
throw new Error(`Projected footprint requires ${count} tiles per source, exceeding maxTiles ${options.maxTiles}`);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
return frame;
|
|
1111
|
+
}
|
|
1112
|
+
function createProjectedBasemapScene(scene, projection, workerUrl) {
|
|
1113
|
+
const options = normalizeProjectedBasemapOptions2(projection);
|
|
1114
|
+
validateProjectedLights(scene.lights);
|
|
1115
|
+
const url = new URL(workerUrl);
|
|
1116
|
+
if (!["http:", "https:", "blob:"].includes(url.protocol)) throw new Error("Projection worker requires an HTTP(S) or Blob URL");
|
|
1117
|
+
if (scene.import !== void 0) throw new Error("Initial projected basemaps require a self-contained inline scene");
|
|
1118
|
+
const styles = readRecord(scene.styles ?? {}, "styles");
|
|
1119
|
+
for (const value of Object.values(styles)) {
|
|
1120
|
+
const style = readRecord(value, "style");
|
|
1121
|
+
if (!["polygons", "raster", "lines", "points", "text"].includes(String(style.base)) || style.mix !== void 0 || style.shaders !== void 0 || style.lighting !== void 0 && style.lighting !== false && (!["polygons", "raster"].includes(String(style.base)) || !["vertex", "fragment"].includes(String(style.lighting)))) {
|
|
1122
|
+
throw new Error("Projected styles require ground/annotation bases or vertex/fragment-lit surfaces without mixins or shaders");
|
|
1123
|
+
}
|
|
1124
|
+
if (style.material !== void 0) {
|
|
1125
|
+
const material = readRecord(style.material, "material");
|
|
1126
|
+
if (material.normal !== void 0 || material.specular !== void 0 && material.specular !== 0) {
|
|
1127
|
+
throw new Error("Projected lighting supports diffuse materials without normal maps or specular terms");
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
if (style.draw !== void 0) validateFlatDraw(
|
|
1131
|
+
readRecord(style.draw, "style draw defaults"),
|
|
1132
|
+
Boolean(options.allowElevation && ["polygons", "raster", "points", "text"].includes(String(style.base)))
|
|
1133
|
+
);
|
|
1134
|
+
if (style.draw !== void 0 && ["points", "text"].includes(String(style.base))) {
|
|
1135
|
+
validateAnnotationDraw(readRecord(style.draw, "annotation defaults"));
|
|
1136
|
+
}
|
|
1137
|
+
if (style.base === "raster" && style.draw !== void 0 && readRecord(style.draw, "raster defaults").interactive === true) {
|
|
1138
|
+
throw new Error("Raster imagery does not expose selectable source features");
|
|
1139
|
+
}
|
|
1140
|
+
if (style.base === "lines") {
|
|
1141
|
+
validateRoadDraw(style, false);
|
|
1142
|
+
if (style.draw !== void 0) validateRoadDraw(readRecord(style.draw, "style draw defaults"), false);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
for (const layer of Object.values(readRecord(scene.layers ?? {}, "layers"))) {
|
|
1146
|
+
validateProjectedDraws(readRecord(layer, "root layer"), styles, {}, Boolean(options.allowElevation));
|
|
1147
|
+
}
|
|
1148
|
+
const settings = scene.scene;
|
|
1149
|
+
if (settings !== void 0 && (!settings || typeof settings !== "object" || Array.isArray(settings))) {
|
|
1150
|
+
throw new Error("Projected scene settings must be an object");
|
|
1151
|
+
}
|
|
1152
|
+
const scripts = settings && "scripts" in settings ? settings.scripts : [];
|
|
1153
|
+
if (!Array.isArray(scripts) || !scripts.every((script) => typeof script === "string")) {
|
|
1154
|
+
throw new Error("Projected scene scripts must be an array of URLs");
|
|
1155
|
+
}
|
|
1156
|
+
return { ...scene, scene: { ...settings, scripts: [.../* @__PURE__ */ new Set([...scripts, url.href])], cpu_projection: options } };
|
|
1157
|
+
}
|
|
1158
|
+
function validateProjectedDraws(node, styles, inheritedDraws = {}, allowElevation = false) {
|
|
1159
|
+
const effectiveDraws = { ...inheritedDraws };
|
|
1160
|
+
if (node.draw !== void 0) {
|
|
1161
|
+
for (const [name, value] of Object.entries(readRecord(node.draw, "draw"))) {
|
|
1162
|
+
const draw = mergeAnnotationDraws(effectiveDraws[name] ?? {}, readRecord(value, "draw style"));
|
|
1163
|
+
effectiveDraws[name] = draw;
|
|
1164
|
+
const styleName = String(draw.style ?? name);
|
|
1165
|
+
const style = styleName in styles ? readRecord(styles[styleName], "draw style definition") : { base: styleName };
|
|
1166
|
+
if (!["polygons", "raster", "lines", "points", "text"].includes(String(style.base))) {
|
|
1167
|
+
throw new Error("Projected basemaps require supported surface, road or annotation bases");
|
|
1168
|
+
}
|
|
1169
|
+
validateFlatDraw(draw, allowElevation && ["polygons", "raster", "points", "text"].includes(String(style.base)));
|
|
1170
|
+
if (style.base === "raster" && draw.interactive === true) throw new Error("Raster imagery does not expose selectable source features");
|
|
1171
|
+
if (style.base === "points" || style.base === "text") {
|
|
1172
|
+
const defaults = style.draw === void 0 ? {} : readRecord(style.draw, "annotation defaults");
|
|
1173
|
+
const annotation = mergeAnnotationDraws(defaults, draw);
|
|
1174
|
+
validateFlatDraw(annotation, allowElevation);
|
|
1175
|
+
validateAnnotationDraw(annotation);
|
|
1176
|
+
if (annotation.collide !== void 0 && typeof annotation.collide !== "boolean") throw new Error("Projected collide must be boolean");
|
|
1177
|
+
if (annotation.text !== void 0) {
|
|
1178
|
+
const text = readRecord(annotation.text, "attached text");
|
|
1179
|
+
if (text.collide !== void 0 && typeof text.collide !== "boolean") throw new Error("Projected collide must be boolean");
|
|
1180
|
+
validateFlatDraw(text);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
if (style.base === "lines") {
|
|
1184
|
+
const defaults = style.draw === void 0 ? {} : readRecord(style.draw, "style draw defaults");
|
|
1185
|
+
validateRoadDraw({ ...defaults, ...draw }, true);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1190
|
+
if (!["filter", "draw", "visible", "enabled", "data", "exclusive", "priority"].includes(key) && value && typeof value === "object" && !Array.isArray(value)) {
|
|
1191
|
+
validateProjectedDraws(readRecord(value, "layer"), styles, effectiveDraws, allowElevation);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
function mergeAnnotationDraws(defaults, draw) {
|
|
1196
|
+
const merged = { ...defaults, ...draw };
|
|
1197
|
+
if (defaults.text && typeof defaults.text === "object" && !Array.isArray(defaults.text) && draw.text && typeof draw.text === "object" && !Array.isArray(draw.text)) {
|
|
1198
|
+
merged.text = { ...readRecord(defaults.text, "attached text defaults"), ...readRecord(draw.text, "attached text") };
|
|
1199
|
+
}
|
|
1200
|
+
return merged;
|
|
1201
|
+
}
|
|
1202
|
+
function validateFlatDraw(draw, allowElevation = false) {
|
|
1203
|
+
if (!allowElevation && (draw.extrude !== void 0 && draw.extrude !== false || draw.z !== void 0)) {
|
|
1204
|
+
throw new Error("Projected basemaps require flat draws unless allowElevation is enabled");
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
function validateAnnotationDraw(draw) {
|
|
1208
|
+
if (draw.extrude !== void 0 && draw.extrude !== false) throw new Error("Projected annotations do not support extrusion");
|
|
1209
|
+
if (draw.text !== void 0 && draw.text !== false) validateFlatDraw(readRecord(draw.text, "attached text"));
|
|
1210
|
+
}
|
|
1211
|
+
function readRecord(value, label) {
|
|
1212
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Projected ${label} must be an object`);
|
|
1213
|
+
return Object.fromEntries(Object.entries(value));
|
|
1214
|
+
}
|
|
1215
|
+
function validateRoadDraw(draw, requireWidth) {
|
|
1216
|
+
if (draw.outline !== void 0) readRecord(draw.outline, "road outline");
|
|
1217
|
+
if (["next_width", "next_offset", "texture"].some((key) => draw[key] !== void 0)) {
|
|
1218
|
+
throw new Error("Projected roads require static ribbons without dynamic widths or external textures");
|
|
1219
|
+
}
|
|
1220
|
+
if (draw.width !== void 0 || requireWidth) {
|
|
1221
|
+
const unit = getProjectedRoadUnit(draw.width);
|
|
1222
|
+
if (draw.offset !== void 0 && getProjectedRoadUnit(draw.offset, false) !== unit) throw new Error("Projected road offsets must use the width unit");
|
|
1223
|
+
if (draw.outline !== void 0) {
|
|
1224
|
+
const outline = readRecord(draw.outline, "road outline");
|
|
1225
|
+
validateFlatDraw(outline);
|
|
1226
|
+
if (outline.style !== void 0) throw new Error("Projected road outlines must use the parent line style");
|
|
1227
|
+
if (getProjectedRoadUnit(outline.width) !== unit) throw new Error("Projected road outlines must use the width unit");
|
|
1228
|
+
validateRoadDraw(outline, true);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
if (draw.animated !== void 0 && typeof draw.animated !== "boolean") throw new Error("Projected road animated must be boolean");
|
|
1232
|
+
if (draw.dash !== void 0 && (!Array.isArray(draw.dash) || draw.dash.length === 0 || !draw.dash.every((value) => typeof value === "number" && Number.isFinite(value) && value > 0))) {
|
|
1233
|
+
throw new Error("Projected road dash must contain positive finite lengths");
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
var BaseProjectedBasemapLayer = tangram_layer_default({
|
|
1237
|
+
Layer,
|
|
1238
|
+
ClassicWebGLRenderer: Renderer,
|
|
1239
|
+
Renderer: void 0
|
|
1240
|
+
}, {
|
|
1241
|
+
getFrame(viewport, properties, dimensions) {
|
|
1242
|
+
const scene = readRecord(properties.scene, "scene");
|
|
1243
|
+
const settings = readRecord(scene.scene, "scene settings");
|
|
1244
|
+
const projection = normalizeProjectedBasemapOptions2(properties.projectedProjection ?? settings.cpu_projection);
|
|
1245
|
+
return getProjectedViewFrame(viewport, {
|
|
1246
|
+
projection,
|
|
1247
|
+
visibleBounds: properties.projectedVisibleBounds ?? (projection.type === "albers" ? [-170, 5, -40, 75] : [-180, -85.0511287798066, 180, 85.0511287798066]),
|
|
1248
|
+
tileZoom: properties.projectedTileZoom ?? 2,
|
|
1249
|
+
...properties.projectedStyleZoom == null ? {} : { styleZoom: properties.projectedStyleZoom },
|
|
1250
|
+
...properties.projectedMaxTiles == null ? {} : { maxTiles: properties.projectedMaxTiles },
|
|
1251
|
+
...properties.tileResources == null ? {} : { tileResources: properties.tileResources }
|
|
1252
|
+
}, dimensions);
|
|
1253
|
+
}
|
|
1254
|
+
});
|
|
1255
|
+
var ProjectedBasemapLayer = class extends BaseProjectedBasemapLayer {
|
|
1256
|
+
/**
|
|
1257
|
+
* Query interactive Tangram geometry asynchronously in viewport-local CSS pixels.
|
|
1258
|
+
* This is not deck.gl's synchronous picking API. The host must keep rendering
|
|
1259
|
+
* until the selection pass and worker lookup finish; the layer requests a redraw.
|
|
1260
|
+
*/
|
|
1261
|
+
getFeatureAt(pixel, options = {}) {
|
|
1262
|
+
return super.getFeatureAt(pixel, options);
|
|
1263
|
+
}
|
|
1264
|
+
/** deck.gl's stable layer identity for state transfer between property updates. */
|
|
1265
|
+
static layerName = "ProjectedBasemapLayer";
|
|
1266
|
+
/** Optional projection override and completion notification, independent of source scene identity. */
|
|
1267
|
+
static defaultProps = {
|
|
1268
|
+
...BaseProjectedBasemapLayer.defaultProps,
|
|
1269
|
+
projectedVisibleBounds: null,
|
|
1270
|
+
projectedTileZoom: 2,
|
|
1271
|
+
projectedProjection: null,
|
|
1272
|
+
projectedStyleZoom: null,
|
|
1273
|
+
projectedMaxTiles: null,
|
|
1274
|
+
onProjectionChange: { type: "function", value: () => {
|
|
1275
|
+
} }
|
|
1276
|
+
};
|
|
1277
|
+
/** Keep the ordinary scene lifecycle, then queue an opt-in projection-only update after loading. */
|
|
1278
|
+
updateState(parameters) {
|
|
1279
|
+
super.updateState(parameters);
|
|
1280
|
+
const record = this.state.tangramRecord;
|
|
1281
|
+
if (!record) return;
|
|
1282
|
+
const settings = readRecord(readRecord(record.sceneSource, "scene").scene, "scene settings");
|
|
1283
|
+
const projection = normalizeProjectedBasemapOptions2(parameters.props.projectedProjection ?? settings.cpu_projection);
|
|
1284
|
+
const key = JSON.stringify(projection);
|
|
1285
|
+
if (record.projectedProjectionKey === key) return;
|
|
1286
|
+
record.projectedProjectionKey = key;
|
|
1287
|
+
Promise.resolve(record.loadPromise).then(async () => {
|
|
1288
|
+
if (record.disposed || record.loadFailed || record.projectedProjectionKey !== key) return;
|
|
1289
|
+
await record.renderer.setProjectedBasemapProjection(projection);
|
|
1290
|
+
if (!record.disposed && record.projectedProjectionKey === key) {
|
|
1291
|
+
record.owner.props.onProjectionChange(projection);
|
|
1292
|
+
record.owner.setNeedsRedraw();
|
|
1293
|
+
}
|
|
1294
|
+
}).catch((error) => {
|
|
1295
|
+
if (!record.disposed && record.projectedProjectionKey === key) {
|
|
1296
|
+
record.projectedProjectionKey = null;
|
|
1297
|
+
record.owner._reportSceneError(record, error);
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
export {
|
|
1303
|
+
ProjectedBasemapLayer,
|
|
1304
|
+
ProjectedBasemapNavigation,
|
|
1305
|
+
createProjectedBasemapScene,
|
|
1306
|
+
getProjectedGeographicBounds,
|
|
1307
|
+
getProjectedViewFrame,
|
|
1308
|
+
selectProjectedTileDetail
|
|
1309
|
+
};
|
|
1310
|
+
//# sourceMappingURL=projected-basemaps.js.map
|