@loaders.gl/tiles 5.0.0-alpha.6 → 5.0.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dist.dev.js +5160 -2896
- package/dist/dist.min.js +4 -2
- package/dist/index.cjs +1509 -454
- package/dist/index.cjs.map +3 -3
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/spatial/format-spatial-reference.d.ts +1 -0
- package/dist/spatial/format-spatial-reference.d.ts.map +1 -1
- package/dist/spatial/format-spatial-reference.js +104 -46
- package/dist/spatial/format-spatial-reference.js.map +1 -1
- package/dist/spatial/get-spatial-coordinate-frame.d.ts +12 -1
- package/dist/spatial/get-spatial-coordinate-frame.d.ts.map +1 -1
- package/dist/spatial/get-spatial-coordinate-frame.js +105 -24
- package/dist/spatial/get-spatial-coordinate-frame.js.map +1 -1
- package/dist/spatial/normalize-crs-identifier.d.ts +15 -0
- package/dist/spatial/normalize-crs-identifier.d.ts.map +1 -0
- package/dist/spatial/normalize-crs-identifier.js +22 -0
- package/dist/spatial/normalize-crs-identifier.js.map +1 -0
- package/dist/spatial/spatial-coordinate-transformer.d.ts +4 -0
- package/dist/spatial/spatial-coordinate-transformer.d.ts.map +1 -1
- package/dist/spatial/spatial-coordinate-transformer.js +89 -12
- package/dist/spatial/spatial-coordinate-transformer.js.map +1 -1
- package/dist/spatial/tiles-3d-spatial-transformer.d.ts +32 -0
- package/dist/spatial/tiles-3d-spatial-transformer.d.ts.map +1 -0
- package/dist/spatial/tiles-3d-spatial-transformer.js +175 -0
- package/dist/spatial/tiles-3d-spatial-transformer.js.map +1 -0
- package/dist/tileset-3d/common/tile-3d-contracts.d.ts +63 -0
- package/dist/tileset-3d/common/tile-3d-contracts.d.ts.map +1 -0
- package/dist/tileset-3d/common/tile-3d-contracts.js +51 -0
- package/dist/tileset-3d/common/tile-3d-contracts.js.map +1 -0
- package/dist/tileset-3d/common/tile-3d.d.ts +29 -5
- package/dist/tileset-3d/common/tile-3d.d.ts.map +1 -1
- package/dist/tileset-3d/common/tile-3d.js +147 -18
- package/dist/tileset-3d/common/tile-3d.js.map +1 -1
- package/dist/tileset-3d/common/tileset-3d.d.ts +14 -0
- package/dist/tileset-3d/common/tileset-3d.d.ts.map +1 -1
- package/dist/tileset-3d/common/tileset-3d.js +27 -1
- package/dist/tileset-3d/common/tileset-3d.js.map +1 -1
- package/dist/tileset-3d/common/tileset-observability.d.ts +60 -0
- package/dist/tileset-3d/common/tileset-observability.d.ts.map +1 -0
- package/dist/tileset-3d/common/tileset-observability.js +39 -0
- package/dist/tileset-3d/common/tileset-observability.js.map +1 -0
- package/dist/tileset-3d/common/tileset-source.d.ts +3 -0
- package/dist/tileset-3d/common/tileset-source.d.ts.map +1 -1
- package/dist/tileset-3d/common/tileset-source.js.map +1 -1
- package/dist/tileset-3d/common/tileset-traverser.d.ts.map +1 -1
- package/dist/tileset-3d/common/tileset-traverser.js +4 -3
- package/dist/tileset-3d/common/tileset-traverser.js.map +1 -1
- package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts +47 -1
- package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts.map +1 -1
- package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js +327 -30
- package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js.map +1 -1
- package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts +32 -0
- package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts.map +1 -1
- package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js +265 -14
- package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js.map +1 -1
- package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts +5 -3
- package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts.map +1 -1
- package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js +7 -4
- package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js.map +1 -1
- package/dist/tileset-3d/helpers/bounding-volume.d.ts +7 -3
- package/dist/tileset-3d/helpers/bounding-volume.d.ts.map +1 -1
- package/dist/tileset-3d/helpers/bounding-volume.js +19 -61
- package/dist/tileset-3d/helpers/bounding-volume.js.map +1 -1
- package/dist/tileset-3d/helpers/frame-state.d.ts +4 -0
- package/dist/tileset-3d/helpers/frame-state.d.ts.map +1 -1
- package/dist/tileset-3d/helpers/frame-state.js +6 -1
- package/dist/tileset-3d/helpers/frame-state.js.map +1 -1
- package/dist/tileset-3d/helpers/s2-bounding-volume.d.ts +17 -0
- package/dist/tileset-3d/helpers/s2-bounding-volume.d.ts.map +1 -0
- package/dist/tileset-3d/helpers/s2-bounding-volume.js +20 -0
- package/dist/tileset-3d/helpers/s2-bounding-volume.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +13 -12
- package/src/index.ts +13 -1
- package/src/spatial/format-spatial-reference.ts +127 -53
- package/src/spatial/get-spatial-coordinate-frame.ts +126 -31
- package/src/spatial/normalize-crs-identifier.ts +26 -0
- package/src/spatial/spatial-coordinate-transformer.ts +106 -15
- package/src/spatial/tiles-3d-spatial-transformer.ts +206 -0
- package/src/tileset-3d/common/tile-3d-contracts.ts +115 -0
- package/src/tileset-3d/common/tile-3d.ts +172 -18
- package/src/tileset-3d/common/tileset-3d.ts +29 -1
- package/src/tileset-3d/common/tileset-observability.ts +100 -0
- package/src/tileset-3d/common/tileset-source.ts +3 -0
- package/src/tileset-3d/common/tileset-traverser.ts +4 -3
- package/src/tileset-3d/format-3d-tiles/implicit-tiling.ts +481 -34
- package/src/tileset-3d/format-3d-tiles/tiles-3d-source.ts +387 -14
- package/src/tileset-3d/format-3d-tiles/tileset-3d-traverser.ts +7 -4
- package/src/tileset-3d/helpers/bounding-volume.ts +29 -62
- package/src/tileset-3d/helpers/frame-state.ts +6 -1
- package/src/tileset-3d/helpers/s2-bounding-volume.ts +35 -0
- package/src/types.ts +2 -0
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
// Copyright (c) vis.gl contributors
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
5
|
+
import {
|
|
6
|
+
inferCRSRepresentation,
|
|
7
|
+
parsePROJString,
|
|
8
|
+
parseWKTCRS,
|
|
9
|
+
type ReadonlyCRSDefinition,
|
|
10
|
+
type SpatialReferenceCoordinateFrame,
|
|
11
|
+
type WKTCRSNode
|
|
12
|
+
} from '@math.gl/crs';
|
|
13
|
+
import {
|
|
14
|
+
normalizeCrsIdentifier,
|
|
15
|
+
WGS84_GEOCENTRIC_CRS,
|
|
16
|
+
WGS84_GEOGRAPHIC_CRS
|
|
17
|
+
} from './normalize-crs-identifier';
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Classify a CRS definition into the broad coordinate frame used by 3D format adapters.
|
|
@@ -26,25 +26,14 @@ export function getSpatialCoordinateFrame(
|
|
|
26
26
|
definition: ReadonlyCRSDefinition
|
|
27
27
|
): SpatialReferenceCoordinateFrame {
|
|
28
28
|
if (typeof definition === 'string') {
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
31
|
-
return
|
|
29
|
+
const representation = inferCRSRepresentation(definition);
|
|
30
|
+
if (representation === 'wkt') {
|
|
31
|
+
return getWktCoordinateFrame(definition);
|
|
32
32
|
}
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
normalized === 'EPSG:4490' ||
|
|
36
|
-
normalized === 'EPSG:4979' ||
|
|
37
|
-
normalized === 'OGC:CRS84' ||
|
|
38
|
-
normalized.includes('+PROJ=LONGLAT') ||
|
|
39
|
-
normalized.includes('+PROJ=LATLONG') ||
|
|
40
|
-
/^(?:GEOGCS|GEOGCRS|GEOGRAPHICCRS|GEOGRAPHIC2DCRS|GEOGRAPHIC3DCRS)\s*[\[(]/.test(normalized)
|
|
41
|
-
) {
|
|
42
|
-
return 'geographic';
|
|
33
|
+
if (representation === 'proj-string') {
|
|
34
|
+
return getProjStringCoordinateFrame(definition);
|
|
43
35
|
}
|
|
44
|
-
|
|
45
|
-
return /CS\s*[\[(]\s*CARTESIAN/.test(normalized) ? 'geocentric' : 'geographic';
|
|
46
|
-
}
|
|
47
|
-
return 'projected';
|
|
36
|
+
return getKnownCrsIdentifierCoordinateFrame(definition) || 'projected';
|
|
48
37
|
}
|
|
49
38
|
|
|
50
39
|
const projJsonDefinition = definition as {
|
|
@@ -70,5 +59,111 @@ export function getSpatialCoordinateFrame(
|
|
|
70
59
|
? 'geocentric'
|
|
71
60
|
: 'geographic';
|
|
72
61
|
}
|
|
73
|
-
return '
|
|
62
|
+
return 'unknown';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Classifies authority identifiers for which the tiles runtime has explicit frame knowledge.
|
|
67
|
+
*
|
|
68
|
+
* Unlike {@link getSpatialCoordinateFrame}, this function does not assume that an unrecognized
|
|
69
|
+
* authority identifier is projected. Draft formats use this strict result to avoid inventing
|
|
70
|
+
* semantics for unknown authorities or registry codes.
|
|
71
|
+
*
|
|
72
|
+
* @param definition - Serialized CRS identifier.
|
|
73
|
+
* @returns A known frame, or `undefined` when registry semantics are unavailable.
|
|
74
|
+
*/
|
|
75
|
+
export function getKnownCrsIdentifierCoordinateFrame(
|
|
76
|
+
definition: ReadonlyCRSDefinition | undefined
|
|
77
|
+
): SpatialReferenceCoordinateFrame | undefined {
|
|
78
|
+
if (typeof definition !== 'string') {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const normalized = normalizeCrsIdentifier(definition);
|
|
82
|
+
if (normalized === WGS84_GEOCENTRIC_CRS) {
|
|
83
|
+
return 'geocentric';
|
|
84
|
+
}
|
|
85
|
+
if (normalized === WGS84_GEOGRAPHIC_CRS || normalized === 'OGC:CRS84') {
|
|
86
|
+
return 'geographic';
|
|
87
|
+
}
|
|
88
|
+
const epsgMatch = /^EPSG:(\d+)$/.exec(normalized);
|
|
89
|
+
if (!epsgMatch) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const identifier = Number(epsgMatch[1]);
|
|
93
|
+
if (identifier === 7789) {
|
|
94
|
+
return 'geocentric';
|
|
95
|
+
}
|
|
96
|
+
if (identifier === 4490 || identifier === 4979) {
|
|
97
|
+
return 'geographic';
|
|
98
|
+
}
|
|
99
|
+
if (
|
|
100
|
+
identifier === 3857 ||
|
|
101
|
+
(identifier >= 32601 && identifier <= 32660) ||
|
|
102
|
+
(identifier >= 32701 && identifier <= 32760)
|
|
103
|
+
) {
|
|
104
|
+
return 'projected';
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Classifies a WKT definition using the syntax tree parsed by `@math.gl/crs`. */
|
|
110
|
+
function getWktCoordinateFrame(definition: string): SpatialReferenceCoordinateFrame {
|
|
111
|
+
let root: WKTCRSNode;
|
|
112
|
+
try {
|
|
113
|
+
root = parseWKTCRS(definition).root;
|
|
114
|
+
} catch {
|
|
115
|
+
return 'unknown';
|
|
116
|
+
}
|
|
117
|
+
switch (root.keyword.toUpperCase()) {
|
|
118
|
+
case 'GEOCCS':
|
|
119
|
+
return 'geocentric';
|
|
120
|
+
case 'GEOGCS':
|
|
121
|
+
case 'GEOGCRS':
|
|
122
|
+
case 'GEOGRAPHICCRS':
|
|
123
|
+
case 'GEOGRAPHIC2DCRS':
|
|
124
|
+
case 'GEOGRAPHIC3DCRS':
|
|
125
|
+
return 'geographic';
|
|
126
|
+
case 'PROJCS':
|
|
127
|
+
case 'PROJCRS':
|
|
128
|
+
case 'PROJECTEDCRS':
|
|
129
|
+
return 'projected';
|
|
130
|
+
case 'GEODCRS':
|
|
131
|
+
case 'GEODETICCRS': {
|
|
132
|
+
const coordinateSystem = root.values.find(
|
|
133
|
+
(value): value is WKTCRSNode =>
|
|
134
|
+
value.type === 'node' && value.keyword.toUpperCase() === 'CS'
|
|
135
|
+
);
|
|
136
|
+
const subtype = coordinateSystem?.values[0];
|
|
137
|
+
if (subtype?.type !== 'enumeration' && subtype?.type !== 'string') {
|
|
138
|
+
return 'unknown';
|
|
139
|
+
}
|
|
140
|
+
return subtype.value.toUpperCase() === 'CARTESIAN'
|
|
141
|
+
? 'geocentric'
|
|
142
|
+
: subtype.value.toUpperCase() === 'ELLIPSOIDAL'
|
|
143
|
+
? 'geographic'
|
|
144
|
+
: 'unknown';
|
|
145
|
+
}
|
|
146
|
+
default:
|
|
147
|
+
return 'unknown';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Classifies a PROJ string using the syntax tree parsed by `@math.gl/crs`. */
|
|
152
|
+
function getProjStringCoordinateFrame(definition: string): SpatialReferenceCoordinateFrame {
|
|
153
|
+
try {
|
|
154
|
+
const projection = parsePROJString(definition).parameters.find(
|
|
155
|
+
parameter => parameter.name.toLowerCase() === 'proj'
|
|
156
|
+
)?.value;
|
|
157
|
+
switch (projection?.toLowerCase()) {
|
|
158
|
+
case 'longlat':
|
|
159
|
+
case 'latlong':
|
|
160
|
+
return 'geographic';
|
|
161
|
+
case 'geocent':
|
|
162
|
+
return 'geocentric';
|
|
163
|
+
default:
|
|
164
|
+
return projection ? 'projected' : 'unknown';
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
return 'unknown';
|
|
168
|
+
}
|
|
74
169
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
/** Compact identifier for conventional WGS84 longitude/latitude coordinates. */
|
|
6
|
+
export const WGS84_GEOGRAPHIC_CRS = 'EPSG:4326';
|
|
7
|
+
|
|
8
|
+
/** Compact identifier for WGS84 Earth-centered, Earth-fixed coordinates. */
|
|
9
|
+
export const WGS84_GEOCENTRIC_CRS = 'EPSG:4978';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Normalizes common OGC URL and URN CRS spellings to compact authority identifiers.
|
|
13
|
+
*
|
|
14
|
+
* This intentionally performs syntax normalization only. Authority lookup and semantic
|
|
15
|
+
* equivalence remain outside the scope of `@math.gl/crs` and the tiles runtime.
|
|
16
|
+
*
|
|
17
|
+
* @param identifier - Serialized CRS identifier.
|
|
18
|
+
* @returns Uppercase compact identifier when an OGC spelling is recognized.
|
|
19
|
+
*/
|
|
20
|
+
export function normalizeCrsIdentifier(identifier: string): string {
|
|
21
|
+
const normalizedIdentifier = identifier.trim().toUpperCase();
|
|
22
|
+
const ogcMatch = normalizedIdentifier.match(
|
|
23
|
+
/(?:\/DEF\/CRS\/|URN:OGC:DEF:CRS:)([A-Z0-9_-]+)(?:\/|::)(?:[^/:]*[/:])?([A-Z0-9_.-]+)$/
|
|
24
|
+
);
|
|
25
|
+
return ogcMatch ? `${ogcMatch[1]}:${ogcMatch[2]}` : normalizedIdentifier;
|
|
26
|
+
}
|
|
@@ -8,6 +8,11 @@ import {Ellipsoid} from '@math.gl/geospatial';
|
|
|
8
8
|
import type {ReadonlyCRSDefinition} from '@math.gl/crs';
|
|
9
9
|
import {Proj4Projection, toProj4CRSDefinition, type Proj4CRSDefinition} from '@math.gl/proj4';
|
|
10
10
|
import {getGeoidModel} from './spatial-resource-registry';
|
|
11
|
+
import {
|
|
12
|
+
normalizeCrsIdentifier,
|
|
13
|
+
WGS84_GEOCENTRIC_CRS,
|
|
14
|
+
WGS84_GEOGRAPHIC_CRS
|
|
15
|
+
} from './normalize-crs-identifier';
|
|
11
16
|
import type {
|
|
12
17
|
TilesetHeightReference,
|
|
13
18
|
TilesetSpatialOptions,
|
|
@@ -16,9 +21,6 @@ import type {
|
|
|
16
21
|
} from './spatial-types';
|
|
17
22
|
export {getSpatialCoordinateFrame} from './get-spatial-coordinate-frame';
|
|
18
23
|
|
|
19
|
-
const GEOGRAPHIC_CRS = 'EPSG:4326';
|
|
20
|
-
const GEOCENTRIC_CRS = 'EPSG:4978';
|
|
21
|
-
|
|
22
24
|
/**
|
|
23
25
|
* Deterministic coordinate transformer used by 3D tile format adapters.
|
|
24
26
|
*
|
|
@@ -90,13 +92,13 @@ export class SpatialCoordinateTransformer {
|
|
|
90
92
|
if (!this.sourceIsGeographic && !this.sourceIsGeocentric) {
|
|
91
93
|
this.geographicProjection = new Proj4Projection({
|
|
92
94
|
from: getHorizontalProj4Definition(spatialReference.sourceCrs),
|
|
93
|
-
to:
|
|
95
|
+
to: WGS84_GEOGRAPHIC_CRS,
|
|
94
96
|
enforceAxis: false
|
|
95
97
|
});
|
|
96
98
|
}
|
|
97
99
|
if (!this.outputIsGeographic && !this.outputIsGeocentric) {
|
|
98
100
|
this.heightOutputProjection = new Proj4Projection({
|
|
99
|
-
from:
|
|
101
|
+
from: WGS84_GEOGRAPHIC_CRS,
|
|
100
102
|
to: getHorizontalProj4Definition(outputCrs),
|
|
101
103
|
enforceAxis: false
|
|
102
104
|
});
|
|
@@ -157,6 +159,59 @@ export class SpatialCoordinateTransformer {
|
|
|
157
159
|
return result;
|
|
158
160
|
}
|
|
159
161
|
|
|
162
|
+
/** Transform a packed sequence of xyz positions, preserving the input array type only when safe. */
|
|
163
|
+
transformPositions(positions: ArrayLike<number>): Float64Array {
|
|
164
|
+
if (positions.length % 3 !== 0) {
|
|
165
|
+
throw new Error('Packed spatial positions must contain a multiple of three components');
|
|
166
|
+
}
|
|
167
|
+
const transformedPositions = new Float64Array(positions.length);
|
|
168
|
+
for (let index = 0; index < positions.length; index += 3) {
|
|
169
|
+
const transformed = this.transformPosition([
|
|
170
|
+
Number(positions[index]),
|
|
171
|
+
Number(positions[index + 1]),
|
|
172
|
+
Number(positions[index + 2])
|
|
173
|
+
]);
|
|
174
|
+
transformedPositions[index] = transformed[0];
|
|
175
|
+
transformedPositions[index + 1] = transformed[1];
|
|
176
|
+
transformedPositions[index + 2] = transformed[2];
|
|
177
|
+
}
|
|
178
|
+
return transformedPositions;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Transform packed xyz normals using a local finite-difference tangent approximation. */
|
|
182
|
+
transformNormals(normals: ArrayLike<number>, positions: ArrayLike<number>): Float32Array {
|
|
183
|
+
if (normals.length !== positions.length || normals.length % 3 !== 0) {
|
|
184
|
+
throw new Error('Spatial normals and positions must have matching xyz component counts');
|
|
185
|
+
}
|
|
186
|
+
const transformedNormals = new Float32Array(normals.length);
|
|
187
|
+
const epsilon = 1e-5;
|
|
188
|
+
for (let index = 0; index < normals.length; index += 3) {
|
|
189
|
+
const position = [
|
|
190
|
+
Number(positions[index]),
|
|
191
|
+
Number(positions[index + 1]),
|
|
192
|
+
Number(positions[index + 2])
|
|
193
|
+
];
|
|
194
|
+
const normal = [
|
|
195
|
+
Number(normals[index]),
|
|
196
|
+
Number(normals[index + 1]),
|
|
197
|
+
Number(normals[index + 2])
|
|
198
|
+
];
|
|
199
|
+
const jacobian = getFiniteDifferenceJacobian(this, position, epsilon);
|
|
200
|
+
const transformedNormal = inverseTransposeMultiply(jacobian, normal);
|
|
201
|
+
const length = Math.hypot(...transformedNormal);
|
|
202
|
+
if (length > 0 && Number.isFinite(length)) {
|
|
203
|
+
transformedNormals[index] = transformedNormal[0] / length;
|
|
204
|
+
transformedNormals[index + 1] = transformedNormal[1] / length;
|
|
205
|
+
transformedNormals[index + 2] = transformedNormal[2] / length;
|
|
206
|
+
} else {
|
|
207
|
+
transformedNormals[index] = normal[0];
|
|
208
|
+
transformedNormals[index + 1] = normal[1];
|
|
209
|
+
transformedNormals[index + 2] = normal[2];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return transformedNormals;
|
|
213
|
+
}
|
|
214
|
+
|
|
160
215
|
/** Convert one source coordinate to conventional WGS84 longitude, latitude, and height. */
|
|
161
216
|
private toGeographic(coordinate: number[]): number[] {
|
|
162
217
|
if (this.sourceIsGeocentric) {
|
|
@@ -180,6 +235,47 @@ export class SpatialCoordinateTransformer {
|
|
|
180
235
|
}
|
|
181
236
|
}
|
|
182
237
|
|
|
238
|
+
function getFiniteDifferenceJacobian(
|
|
239
|
+
transformer: SpatialCoordinateTransformer,
|
|
240
|
+
position: number[],
|
|
241
|
+
epsilon: number
|
|
242
|
+
): number[][] {
|
|
243
|
+
const columns: number[][] = [];
|
|
244
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
245
|
+
const plus = position.slice();
|
|
246
|
+
const minus = position.slice();
|
|
247
|
+
plus[axis] += epsilon;
|
|
248
|
+
minus[axis] -= epsilon;
|
|
249
|
+
const plusPosition = transformer.transformPosition(plus);
|
|
250
|
+
const minusPosition = transformer.transformPosition(minus);
|
|
251
|
+
columns.push([
|
|
252
|
+
(plusPosition[0] - minusPosition[0]) / (2 * epsilon),
|
|
253
|
+
(plusPosition[1] - minusPosition[1]) / (2 * epsilon),
|
|
254
|
+
(plusPosition[2] - minusPosition[2]) / (2 * epsilon)
|
|
255
|
+
]);
|
|
256
|
+
}
|
|
257
|
+
return columns;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function inverseTransposeMultiply(jacobianColumns: number[][], vector: number[]): number[] {
|
|
261
|
+
const [a, b, c] = jacobianColumns;
|
|
262
|
+
const determinant =
|
|
263
|
+
a[0] * (b[1] * c[2] - b[2] * c[1]) -
|
|
264
|
+
b[0] * (a[1] * c[2] - a[2] * c[1]) +
|
|
265
|
+
c[0] * (a[1] * b[2] - a[2] * b[1]);
|
|
266
|
+
if (!Number.isFinite(determinant) || Math.abs(determinant) < 1e-15) {
|
|
267
|
+
return vector;
|
|
268
|
+
}
|
|
269
|
+
const inverseTranspose = [
|
|
270
|
+
[b[1] * c[2] - b[2] * c[1], a[2] * c[1] - a[1] * c[2], a[1] * b[2] - a[2] * b[1]],
|
|
271
|
+
[b[2] * c[0] - b[0] * c[2], a[0] * c[2] - a[2] * c[0], a[2] * b[0] - a[0] * b[2]],
|
|
272
|
+
[b[0] * c[1] - b[1] * c[0], a[1] * c[0] - a[0] * c[1], a[0] * b[1] - a[1] * b[0]]
|
|
273
|
+
];
|
|
274
|
+
return inverseTranspose.map(
|
|
275
|
+
row => (row[0] * vector[0] + row[1] * vector[1] + row[2] * vector[2]) / determinant
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
183
279
|
/** Select the horizontal component that the proj4js runtime can execute. */
|
|
184
280
|
function getHorizontalProj4Definition(
|
|
185
281
|
definition: ReadonlyCRSDefinition | undefined
|
|
@@ -224,7 +320,9 @@ function validateTransformRequest(spatialReference: TilesetSpatialReference): vo
|
|
|
224
320
|
|
|
225
321
|
/** Return whether a CRS definition identifies the WGS84 geocentric frame. */
|
|
226
322
|
function isWgs84Geocentric(definition: unknown): boolean {
|
|
227
|
-
return
|
|
323
|
+
return (
|
|
324
|
+
typeof definition === 'string' && normalizeCrsIdentifier(definition) === WGS84_GEOCENTRIC_CRS
|
|
325
|
+
);
|
|
228
326
|
}
|
|
229
327
|
|
|
230
328
|
/** Return whether a CRS definition uses conventional WGS84 longitude/latitude coordinates. */
|
|
@@ -233,16 +331,9 @@ function isWgs84Geographic(definition: unknown): boolean {
|
|
|
233
331
|
return false;
|
|
234
332
|
}
|
|
235
333
|
const identifier = normalizeCrsIdentifier(definition);
|
|
236
|
-
return
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
/** Normalize common OGC URL and URN CRS spellings to authority identifiers. */
|
|
240
|
-
function normalizeCrsIdentifier(identifier: string): string {
|
|
241
|
-
const normalizedIdentifier = identifier.trim().toUpperCase();
|
|
242
|
-
const ogcMatch = normalizedIdentifier.match(
|
|
243
|
-
/(?:\/DEF\/CRS\/|URN:OGC:DEF:CRS:)([A-Z0-9_-]+)(?:\/|::)(?:[^/:]*[/:])?([A-Z0-9_.-]+)$/
|
|
334
|
+
return (
|
|
335
|
+
identifier === WGS84_GEOGRAPHIC_CRS || identifier === 'EPSG:4979' || identifier === 'OGC:CRS84'
|
|
244
336
|
);
|
|
245
|
-
return ogcMatch ? `${ogcMatch[1]}:${ogcMatch[2]}` : normalizedIdentifier;
|
|
246
337
|
}
|
|
247
338
|
|
|
248
339
|
/** Return whether source and target height interpretations differ. */
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {SpatialCoordinateTransformer} from './spatial-coordinate-transformer';
|
|
6
|
+
import {getSpatialCoordinateFrame} from './get-spatial-coordinate-frame';
|
|
7
|
+
import {Matrix4} from '@math.gl/core';
|
|
8
|
+
import {Ellipsoid} from '@math.gl/geospatial';
|
|
9
|
+
import type {TilesetSpatialOptions, TilesetSpatialReference} from './spatial-types';
|
|
10
|
+
|
|
11
|
+
/** Structural 3D Tiles volume shape used by the source adapter. */
|
|
12
|
+
export type Tiles3DSpatialBoundingVolume = {
|
|
13
|
+
box?: number[];
|
|
14
|
+
sphere?: number[];
|
|
15
|
+
region?: number[];
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Applies nonlinear CRS operations to 3D Tiles geometry bounds.
|
|
21
|
+
*
|
|
22
|
+
* Bounds are rebuilt from conservative samples. This intentionally produces an axis-aligned box
|
|
23
|
+
* in the target frame; it is safe for culling and screen-space-error calculations even when the
|
|
24
|
+
* source volume is rotated or the projection is nonlinear.
|
|
25
|
+
*/
|
|
26
|
+
export class Tiles3DSpatialTransformer {
|
|
27
|
+
/** Normalized reference describing the completed operation. */
|
|
28
|
+
readonly spatialReference: TilesetSpatialReference;
|
|
29
|
+
private readonly coordinateTransformer: SpatialCoordinateTransformer;
|
|
30
|
+
private readonly geographicCoordinateTransformer: SpatialCoordinateTransformer;
|
|
31
|
+
|
|
32
|
+
/** Creates a 3D Tiles transformer for a requested target CRS. */
|
|
33
|
+
constructor(spatialReference: TilesetSpatialReference, options: TilesetSpatialOptions = {}) {
|
|
34
|
+
this.coordinateTransformer = new SpatialCoordinateTransformer(spatialReference, options);
|
|
35
|
+
// 3D Tiles regions are always encoded as WGS84 longitude/latitude radians, independently
|
|
36
|
+
// of the tileset's local CRS. Build a second pipeline for that fixed source frame.
|
|
37
|
+
const geographicSpatialReference = {
|
|
38
|
+
...spatialReference,
|
|
39
|
+
sourceCrs: 'EPSG:4326' as const,
|
|
40
|
+
targetCrs: spatialReference.targetCrs || spatialReference.sourceCrs,
|
|
41
|
+
coordinateFrame: 'geographic' as const,
|
|
42
|
+
axisOrder: 'xy' as const
|
|
43
|
+
};
|
|
44
|
+
this.geographicCoordinateTransformer = new SpatialCoordinateTransformer(
|
|
45
|
+
geographicSpatialReference,
|
|
46
|
+
options
|
|
47
|
+
);
|
|
48
|
+
this.spatialReference = spatialReference;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Transform packed POSITION values into the target CRS. */
|
|
52
|
+
transformPositions(positions: ArrayLike<number>): Float64Array {
|
|
53
|
+
return this.coordinateTransformer.transformPositions(positions);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Transform packed NORMAL values using the corresponding source positions. */
|
|
57
|
+
transformNormals(normals: ArrayLike<number>, positions: ArrayLike<number>): Float32Array {
|
|
58
|
+
return this.coordinateTransformer.transformNormals(normals, positions);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Rebuild a conservative target-frame volume from a source volume. */
|
|
62
|
+
transformBoundingVolume(
|
|
63
|
+
volume: Tiles3DSpatialBoundingVolume,
|
|
64
|
+
sourceTransform?: ArrayLike<number>
|
|
65
|
+
): Tiles3DSpatialBoundingVolume {
|
|
66
|
+
const transform = sourceTransform ? new Matrix4(Array.from(sourceTransform)) : undefined;
|
|
67
|
+
const samples = volume.region
|
|
68
|
+
? getRegionSamples(volume.region)
|
|
69
|
+
: getVolumeSamples(volume).map(sample =>
|
|
70
|
+
transform ? Array.from(transform.transformAsPoint(sample)) : sample
|
|
71
|
+
);
|
|
72
|
+
if (!samples.length) {
|
|
73
|
+
return volume;
|
|
74
|
+
}
|
|
75
|
+
const transformed = volume.region
|
|
76
|
+
? samples.map(sample => this.transformRegionSample(sample))
|
|
77
|
+
: samples.map(sample => this.coordinateTransformer.transformPosition(sample));
|
|
78
|
+
return {box: createAxisAlignedBox(transformed)};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Transforms one WGS84 region sample into the selected output frame. */
|
|
82
|
+
private transformRegionSample(sample: number[]): number[] {
|
|
83
|
+
const [longitudeDegrees, latitudeDegrees, height] = sample;
|
|
84
|
+
const targetCrs = this.spatialReference.targetCrs || this.spatialReference.sourceCrs;
|
|
85
|
+
if (targetCrs && getSpatialCoordinateFrame(targetCrs) === 'geocentric') {
|
|
86
|
+
const cartesian = Ellipsoid.WGS84.cartographicToCartesian([
|
|
87
|
+
longitudeDegrees,
|
|
88
|
+
latitudeDegrees,
|
|
89
|
+
height
|
|
90
|
+
]);
|
|
91
|
+
return Array.from(cartesian);
|
|
92
|
+
}
|
|
93
|
+
return this.geographicCoordinateTransformer.transformPosition(sample);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Samples region corners, center lines, and both height planes in degrees/meters. */
|
|
98
|
+
function getRegionSamples(region: number[]): number[][] {
|
|
99
|
+
if (region.length < 6 || region.some(value => !Number.isFinite(value))) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
const [west, south, east, north, minimumHeight, maximumHeight] = region;
|
|
103
|
+
const longitudeEnd = east < west ? east + Math.PI * 2 : east;
|
|
104
|
+
const longitudes = getIntervalSamples(west, longitudeEnd);
|
|
105
|
+
const latitudes = getIntervalSamples(south, north);
|
|
106
|
+
const samples: number[][] = [];
|
|
107
|
+
for (const longitude of longitudes) {
|
|
108
|
+
for (const latitude of latitudes) {
|
|
109
|
+
for (const height of [minimumHeight, maximumHeight]) {
|
|
110
|
+
samples.push([(longitude * 180) / Math.PI, (latitude * 180) / Math.PI, height]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return samples;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Includes interval endpoints and every quarter-turn where ECEF axes reach an extremum. */
|
|
118
|
+
function getIntervalSamples(start: number, end: number): number[] {
|
|
119
|
+
const samples = [start, (start + end) / 2, end];
|
|
120
|
+
const angularStep = Math.PI / 2;
|
|
121
|
+
const firstCriticalIndex = Math.ceil(start / angularStep);
|
|
122
|
+
const lastCriticalIndex = Math.floor(end / angularStep);
|
|
123
|
+
for (
|
|
124
|
+
let criticalIndex = firstCriticalIndex;
|
|
125
|
+
criticalIndex <= lastCriticalIndex;
|
|
126
|
+
criticalIndex++
|
|
127
|
+
) {
|
|
128
|
+
samples.push(criticalIndex * angularStep);
|
|
129
|
+
}
|
|
130
|
+
return samples;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getVolumeSamples(volume: Tiles3DSpatialBoundingVolume): number[][] {
|
|
134
|
+
if (volume.box && volume.box.length >= 12) {
|
|
135
|
+
const center = volume.box.slice(0, 3);
|
|
136
|
+
const axes = [volume.box.slice(3, 6), volume.box.slice(6, 9), volume.box.slice(9, 12)];
|
|
137
|
+
const samples: number[][] = [];
|
|
138
|
+
for (let firstIndex = 0; firstIndex <= 4; firstIndex++) {
|
|
139
|
+
for (let secondIndex = 0; secondIndex <= 4; secondIndex++) {
|
|
140
|
+
for (let thirdIndex = 0; thirdIndex <= 4; thirdIndex++) {
|
|
141
|
+
const first = firstIndex / 2 - 1;
|
|
142
|
+
const second = secondIndex / 2 - 1;
|
|
143
|
+
const third = thirdIndex / 2 - 1;
|
|
144
|
+
samples.push([
|
|
145
|
+
center[0] + first * axes[0][0] + second * axes[1][0] + third * axes[2][0],
|
|
146
|
+
center[1] + first * axes[0][1] + second * axes[1][1] + third * axes[2][1],
|
|
147
|
+
center[2] + first * axes[0][2] + second * axes[1][2] + third * axes[2][2]
|
|
148
|
+
]);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return samples;
|
|
153
|
+
}
|
|
154
|
+
if (volume.sphere && volume.sphere.length >= 4) {
|
|
155
|
+
const [x, y, z, radius] = volume.sphere;
|
|
156
|
+
const samples = [
|
|
157
|
+
[x - radius, y, z],
|
|
158
|
+
[x + radius, y, z],
|
|
159
|
+
[x, y - radius, z],
|
|
160
|
+
[x, y + radius, z],
|
|
161
|
+
[x, y, z - radius],
|
|
162
|
+
[x, y, z + radius]
|
|
163
|
+
];
|
|
164
|
+
for (let longitudeIndex = 0; longitudeIndex < 16; longitudeIndex++) {
|
|
165
|
+
const longitude = (longitudeIndex * Math.PI * 2) / 16;
|
|
166
|
+
samples.push([x + radius * Math.cos(longitude), y + radius * Math.sin(longitude), z]);
|
|
167
|
+
}
|
|
168
|
+
return samples;
|
|
169
|
+
}
|
|
170
|
+
// Regions are already WGS84 geographic volumes. Keep them unchanged until a dedicated
|
|
171
|
+
// geographic-region adapter can account for longitude wrapping and datum semantics.
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function createAxisAlignedBox(samples: number[][]): number[] {
|
|
176
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
177
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
178
|
+
for (const sample of samples) {
|
|
179
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
180
|
+
minimum[axis] = Math.min(minimum[axis], sample[axis]);
|
|
181
|
+
maximum[axis] = Math.max(maximum[axis], sample[axis]);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const padding = [
|
|
185
|
+
Math.max((maximum[0] - minimum[0]) * 0.01, 1e-9),
|
|
186
|
+
Math.max((maximum[1] - minimum[1]) * 0.01, 1e-9),
|
|
187
|
+
Math.max((maximum[2] - minimum[2]) * 0.01, 1e-9)
|
|
188
|
+
];
|
|
189
|
+
const center = [
|
|
190
|
+
(minimum[0] + maximum[0]) / 2,
|
|
191
|
+
(minimum[1] + maximum[1]) / 2,
|
|
192
|
+
(minimum[2] + maximum[2]) / 2
|
|
193
|
+
];
|
|
194
|
+
return [
|
|
195
|
+
...center,
|
|
196
|
+
(maximum[0] - minimum[0]) / 2 + padding[0],
|
|
197
|
+
0,
|
|
198
|
+
0,
|
|
199
|
+
0,
|
|
200
|
+
(maximum[1] - minimum[1]) / 2 + padding[1],
|
|
201
|
+
0,
|
|
202
|
+
0,
|
|
203
|
+
0,
|
|
204
|
+
(maximum[2] - minimum[2]) / 2 + padding[2]
|
|
205
|
+
];
|
|
206
|
+
}
|