@itwin/core-frontend
Version:
iTwin.js frontend components
412 lines • 22.6 kB
JavaScript
"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.getCesiumAssetUrl = getCesiumAssetUrl;
exports.getCesiumOSMBuildingsUrl = getCesiumOSMBuildingsUrl;
exports.getCesiumAccessTokenAndEndpointUrl = getCesiumAccessTokenAndEndpointUrl;
exports.getCesiumTerrainProvider = getCesiumTerrainProvider;
/** @packageDocumentation
* @module Tiles
*/
const core_bentley_1 = require("@itwin/core-bentley");
const core_geometry_1 = require("@itwin/core-geometry");
const core_common_1 = require("@itwin/core-common");
const appui_abstract_1 = require("@itwin/appui-abstract");
const Request_1 = require("../../request/Request");
const ApproximateTerrainHeights_1 = require("../../ApproximateTerrainHeights");
const IModelApp_1 = require("../../IModelApp");
const RealityMeshParams_1 = require("../../render/RealityMeshParams");
const internal_1 = require("../internal");
/** @internal */
var QuantizedMeshExtensionIds;
(function (QuantizedMeshExtensionIds) {
QuantizedMeshExtensionIds[QuantizedMeshExtensionIds["OctEncodedNormals"] = 1] = "OctEncodedNormals";
QuantizedMeshExtensionIds[QuantizedMeshExtensionIds["WaterMask"] = 2] = "WaterMask";
QuantizedMeshExtensionIds[QuantizedMeshExtensionIds["Metadata"] = 4] = "Metadata";
})(QuantizedMeshExtensionIds || (QuantizedMeshExtensionIds = {}));
/** Return the URL for a Cesium ion asset from its asset ID and request Key.
* @public
*/
function getCesiumAssetUrl(osmAssetId, requestKey) {
return `$CesiumIonAsset=${osmAssetId}:${requestKey}`;
}
/** @internal */
function getCesiumOSMBuildingsUrl() {
const key = IModelApp_1.IModelApp.tileAdmin.cesiumIonKey;
if (undefined === key)
return undefined;
return getCesiumAssetUrl(+core_common_1.CesiumIonAssetId.OSMBuildings, key);
}
/** @internal */
async function getCesiumAccessTokenAndEndpointUrl(assetId, requestKey) {
if (undefined === requestKey) {
requestKey = IModelApp_1.IModelApp.tileAdmin.cesiumIonKey;
if (undefined === requestKey)
return {};
}
const requestTemplate = `https://api.cesium.com/v1/assets/${assetId}/endpoint?access_token={CesiumRequestToken}`;
const apiUrl = requestTemplate.replace("{CesiumRequestToken}", requestKey);
try {
const apiResponse = await (0, Request_1.request)(apiUrl, "json");
if (undefined === apiResponse || undefined === apiResponse.url) {
(0, core_bentley_1.assert)(false);
return {};
}
return { token: apiResponse.accessToken, url: apiResponse.url };
}
catch {
(0, core_bentley_1.assert)(false);
return {};
}
}
let notifiedTerrainError = false;
// Notify - once per session - of failure to obtain Cesium terrain provider.
function notifyTerrainError(detailedDescription) {
if (notifiedTerrainError)
return;
notifiedTerrainError = true;
IModelApp_1.IModelApp.notifications.displayMessage(appui_abstract_1.MessageSeverity.Information, IModelApp_1.IModelApp.localization.getLocalizedString(`iModelJs:BackgroundMap.CannotObtainTerrain`), detailedDescription);
}
/** @internal */
async function getCesiumTerrainProvider(opts) {
const accessTokenAndEndpointUrl = await getCesiumAccessTokenAndEndpointUrl(opts.dataSource || core_common_1.CesiumTerrainAssetId.Default);
if (!accessTokenAndEndpointUrl.token || !accessTokenAndEndpointUrl.url) {
notifyTerrainError(IModelApp_1.IModelApp.localization.getLocalizedString(`iModelJs:BackgroundMap.MissingCesiumToken`));
return undefined;
}
let layers;
try {
const layerRequestOptions = { headers: { authorization: `Bearer ${accessTokenAndEndpointUrl.token}` } };
const layerUrl = `${accessTokenAndEndpointUrl.url}layer.json`;
layers = await (0, Request_1.request)(layerUrl, "json", layerRequestOptions);
}
catch {
notifyTerrainError();
return undefined;
}
if (undefined === layers || undefined === layers.tiles || undefined === layers.version) {
notifyTerrainError();
return undefined;
}
const tilingScheme = new internal_1.GeographicTilingScheme();
let tileAvailability;
// When collecting tiles, only the highest resolution tiles are downloaded.
// Because of that, the tile availability is often only populated by the
// "layer" metadata. (i.e. not from higher resolution tiles metadata).
// Unfortunately the "layer" metadata only cover the first 16 levels,
// preventing the geometry collector from accessing to higher resolution tiles.
// For now, the solution is to turn off tile availability check when collecting geometries.
if (undefined !== layers.available && !opts.produceGeometry) {
const availableTiles = layers.available;
tileAvailability = new internal_1.TileAvailability(tilingScheme, availableTiles.length);
for (let level = 0; level < layers.available.length; level++) {
const rangesAtLevel = availableTiles[level];
for (const range of rangesAtLevel) {
tileAvailability.addAvailableTileRange(level, range.startX, range.startY, range.endX, range.endY);
}
}
}
let tileUrlTemplate = accessTokenAndEndpointUrl.url + layers.tiles[0].replace("{version}", layers.version);
if (opts.wantNormals)
tileUrlTemplate = tileUrlTemplate.replace("?", "?extensions=octvertexnormals-watermask-metadata&");
const maxDepth = core_bentley_1.JsonUtils.asInt(layers.maxzoom, 19);
// TBD -- When we have an API extract the heights for the project from the terrain tiles - for use temporary Bing elevation.
return new CesiumTerrainProvider(opts, accessTokenAndEndpointUrl.token, tileUrlTemplate, maxDepth, tilingScheme, tileAvailability, layers.metadataAvailability);
}
function zigZagDecode(value) {
return (value >> 1) ^ (-(value & 1));
}
/**
* Decodes delta and ZigZag encoded vertices. This modifies the buffers in place.
*
* @see {@link https://github.com/AnalyticalGraphicsInc/quantized-mesh|quantized-mesh-1.0 terrain format}
*/
function zigZagDeltaDecode(uBuffer, vBuffer, heightBuffer) {
const count = uBuffer.length;
let u = 0;
let v = 0;
let height = 0;
for (let i = 0; i < count; ++i) {
u += zigZagDecode(uBuffer[i]);
v += zigZagDecode(vBuffer[i]);
uBuffer[i] = u;
vBuffer[i] = v;
height += zigZagDecode(heightBuffer[i]);
heightBuffer[i] = height;
}
}
/** @internal */
class CesiumTerrainProvider extends internal_1.TerrainMeshProvider {
_accessToken;
_tileUrlTemplate;
_maxDepth;
_wantSkirts;
_tilingScheme;
_tileAvailability;
_metaDataAvailableLevel;
_exaggeration;
_assetId;
static _scratchQPoint2d = core_common_1.QPoint2d.fromScalars(0, 0);
static _scratchPoint2d = core_geometry_1.Point2d.createZero();
static _scratchPoint = core_geometry_1.Point3d.createZero();
static _scratchNormal = core_geometry_1.Vector3d.createZero();
static _scratchHeightRange = core_geometry_1.Range1d.createNull();
static _tokenTimeoutInterval = core_bentley_1.BeDuration.fromSeconds(60 * 30); // Request a new access token every 30 minutes...
_tokenTimeOut;
forceTileLoad(tile) {
// Force loading of the metadata availability tiles as these are required for determining the availability of descendants.
const mapTile = tile;
return undefined !== this._metaDataAvailableLevel && mapTile.quadId.level === this._metaDataAvailableLevel && !mapTile.everLoaded;
}
constructor(opts, accessToken, tileUrlTemplate, maxDepth, tilingScheme, tileAvailability, metaDataAvailableLevel) {
super();
this._wantSkirts = opts.wantSkirts;
this._exaggeration = opts.exaggeration;
this._accessToken = accessToken;
this._tileUrlTemplate = tileUrlTemplate;
this._maxDepth = maxDepth;
this._tilingScheme = tilingScheme;
this._tileAvailability = tileAvailability;
this._metaDataAvailableLevel = metaDataAvailableLevel;
this._assetId = opts.dataSource || core_common_1.CesiumTerrainAssetId.Default;
this._tokenTimeOut = core_bentley_1.BeTimePoint.now().plus(CesiumTerrainProvider._tokenTimeoutInterval);
}
/** @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [addAttributions] instead. */
addLogoCards(cards) {
if (cards.dataset.cesiumIonLogoCard)
return;
cards.dataset.cesiumIonLogoCard = "true";
let notice = IModelApp_1.IModelApp.localization.getLocalizedString("iModelJs:BackgroundMap.CesiumWorldTerrainAttribution");
if (this._assetId === core_common_1.CesiumTerrainAssetId.Bathymetry)
notice = `${notice}\n${IModelApp_1.IModelApp.localization.getLocalizedString("iModelJs:BackgroundMap.CesiumBathymetryAttribution")}`;
const card = IModelApp_1.IModelApp.makeLogoCard({ iconSrc: `${IModelApp_1.IModelApp.publicPath}images/cesium-ion.svg`, heading: "Cesium Ion", notice });
cards.appendChild(card);
}
async addAttributions(cards, _vp) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
return Promise.resolve(this.addLogoCards(cards));
}
get maxDepth() { return this._maxDepth; }
get tilingScheme() { return this._tilingScheme; }
isTileAvailable(quadId) {
if (quadId.level > this.maxDepth)
return false;
return this._tileAvailability ? this._tileAvailability.isTileAvailable(quadId.level, quadId.column, quadId.row) : true;
}
async requestMeshData(args) {
const tile = args.tile;
const quadId = tile.quadId;
const tileUrl = this.constructUrl(quadId.row, quadId.column, quadId.level);
const requestOptions = {
headers: {
authorization: `Bearer ${this._accessToken}`,
accept: "application/vnd.quantized-mesh;" /* extensions=octvertexnormals, */ + "application/octet-stream;q=0.9,*/*;q=0.01",
},
};
try {
const response = await (0, Request_1.request)(tileUrl, "arraybuffer", requestOptions);
return new Uint8Array(response);
}
catch {
return undefined;
}
}
async readMesh(args) {
// ###TODO why does he update the access token when reading the mesh instead of when requesting it?
// This function only returns undefined if it fails to acquire token - but it doesn't need the token...
if (core_bentley_1.BeTimePoint.now().milliseconds > this._tokenTimeOut.milliseconds) {
const accessTokenAndEndpointUrl = await getCesiumAccessTokenAndEndpointUrl(this._assetId);
if (!accessTokenAndEndpointUrl.token || args.isCanceled())
return undefined;
this._accessToken = accessTokenAndEndpointUrl.token;
this._tokenTimeOut = core_bentley_1.BeTimePoint.now().plus(CesiumTerrainProvider._tokenTimeoutInterval);
}
const { data, tile } = args;
(0, core_bentley_1.assert)(data instanceof Uint8Array);
(0, core_bentley_1.assert)(tile instanceof internal_1.MapTile);
const blob = data;
const streamBuffer = core_bentley_1.ByteStream.fromUint8Array(blob);
const center = (0, core_common_1.nextPoint3d64FromByteStream)(streamBuffer);
const quadId = internal_1.QuadId.createFromContentId(tile.contentId);
const skirtHeight = this.getLevelMaximumGeometricError(quadId.level + 1) * 10.0; // Add 1 to level to restore height calculation to before the quadId level was from root. (4326 unification)
const minHeight = this._exaggeration * streamBuffer.readFloat32();
const maxHeight = this._exaggeration * streamBuffer.readFloat32();
const boundCenter = (0, core_common_1.nextPoint3d64FromByteStream)(streamBuffer);
const boundRadius = streamBuffer.readFloat64();
const horizonOcclusion = (0, core_common_1.nextPoint3d64FromByteStream)(streamBuffer);
const terrainTile = tile;
terrainTile.adjustHeights(minHeight, maxHeight);
if (undefined === center || undefined === boundCenter || undefined === boundRadius || undefined === horizonOcclusion) { }
const pointCount = streamBuffer.readUint32();
const encodedVertexBuffer = new Uint16Array(blob.buffer, streamBuffer.curPos, pointCount * 3);
streamBuffer.advance(pointCount * 6);
const uBuffer = encodedVertexBuffer.subarray(0, pointCount);
const vBuffer = encodedVertexBuffer.subarray(pointCount, 2 * pointCount);
const heightBuffer = encodedVertexBuffer.subarray(pointCount * 2, 3 * pointCount);
zigZagDeltaDecode(uBuffer, vBuffer, heightBuffer);
// ###TODO: This alleges to handle 32-bit indices, but RealityMeshParams uses a Uint16Array to store indices...
const typedArray = pointCount > 0xffff ? Uint32Array : Uint16Array;
const bytesPerIndex = typedArray.BYTES_PER_ELEMENT;
const triangleElements = 3;
// skip over any additional padding that was added for 2/4 byte alignment
if (streamBuffer.curPos % bytesPerIndex !== 0)
streamBuffer.advance(bytesPerIndex - (streamBuffer.curPos % bytesPerIndex));
const triangleCount = streamBuffer.readUint32();
const indexCount = triangleCount * triangleElements;
const getIndexArray = (numIndices) => {
const indexArray = new typedArray(streamBuffer.arrayBuffer, streamBuffer.curPos, numIndices);
streamBuffer.advance(numIndices * bytesPerIndex);
return indexArray;
};
const indices = getIndexArray(indexCount);
// High water mark decoding based on decompressIndices_ in webgl-loader's loader.js.
// https://code.google.com/p/webgl-loader/source/browse/trunk/samples/loader.js?r=99#55
// Copyright 2012 Google Inc., Apache 2.0 license.
let highest = 0;
const length = indices.length;
for (let i = 0; i < length; ++i) {
const code = indices[i];
indices[i] = highest - code;
if (code === 0) {
++highest;
}
}
CesiumTerrainProvider._scratchHeightRange.low = minHeight - skirtHeight;
CesiumTerrainProvider._scratchHeightRange.high = maxHeight;
const projection = terrainTile.getProjection(CesiumTerrainProvider._scratchHeightRange);
const uvScale = 1.0 / 32767.0;
const heightScale = uvScale * (maxHeight - minHeight);
const westCount = streamBuffer.readUint32(), westIndices = getIndexArray(westCount), southCount = streamBuffer.readUint32(), southIndices = getIndexArray(southCount), eastCount = streamBuffer.readUint32(), eastIndices = getIndexArray(eastCount), northCount = streamBuffer.readUint32(), northIndices = getIndexArray(northCount);
// Extensions...
let encodedNormalsBuffer;
while (streamBuffer.curPos < streamBuffer.length) {
const extensionId = streamBuffer.readUint8();
const extensionLength = streamBuffer.readUint32();
switch (extensionId) {
case QuantizedMeshExtensionIds.OctEncodedNormals:
(0, core_bentley_1.assert)(pointCount * 2 === extensionLength);
encodedNormalsBuffer = new Uint8Array(streamBuffer.arrayBuffer, streamBuffer.curPos, extensionLength);
streamBuffer.advance(extensionLength);
break;
case QuantizedMeshExtensionIds.Metadata:
const stringLength = streamBuffer.readUint32();
if (stringLength > 0) {
const strData = streamBuffer.nextBytes(stringLength);
const str = (0, core_bentley_1.utf8ToString)(strData);
if (undefined !== str) {
const metaData = JSON.parse(str);
if (undefined !== metaData.available && undefined !== this._tileAvailability) {
const availableTiles = metaData.available;
for (let offset = 0; offset < availableTiles.length; ++offset) {
const availableLevel = tile.depth + offset; // Our depth is includes root (1 + cesium Depth)
const rangesAtLevel = availableTiles[offset];
for (const range of rangesAtLevel)
this._tileAvailability.addAvailableTileRange(availableLevel, range.startX, range.startY, range.endX, range.endY);
}
}
}
}
break;
default:
streamBuffer.advance(extensionLength);
break;
}
}
let initialIndexCapacity = indexCount;
let initialVertexCapacity = pointCount;
if (this._wantSkirts) {
initialIndexCapacity += 6 * (Math.max(0, northCount - 1) + Math.max(0, southCount - 1) + Math.max(0, eastCount - 1) + Math.max(0, westCount - 1));
initialVertexCapacity += (northCount + southCount + eastCount + westCount);
}
const wantNormals = undefined !== encodedNormalsBuffer;
const builder = new RealityMeshParams_1.RealityMeshParamsBuilder({
positionRange: projection.localRange,
initialIndexCapacity,
initialVertexCapacity,
wantNormals,
});
for (let i = 0; i < indexCount; i += 3)
builder.addTriangle(indices[i], indices[i + 1], indices[i + 2]);
const position = new core_geometry_1.Point3d();
const uv = new core_common_1.QPoint2d();
const normal = new core_geometry_1.Vector3d();
const worldToEcef = tile.iModel.getEcefTransform().matrix;
for (let i = 0; i < pointCount; i++) {
const u = uBuffer[i];
const v = vBuffer[i];
projection.getPoint(uvScale * u, uvScale * v, minHeight + heightBuffer[i] * heightScale, position);
uv.setFromScalars(u * 2, v * 2);
let oen;
if (encodedNormalsBuffer) {
const normalIndex = i * 2;
core_common_1.OctEncodedNormal.decodeValue(encodedNormalsBuffer[normalIndex + 1] << 8 | encodedNormalsBuffer[normalIndex], normal);
worldToEcef.multiplyTransposeVector(normal, normal);
oen = core_common_1.OctEncodedNormal.encode(normal);
}
builder.addVertex(position, uv, oen);
}
if (!this._wantSkirts)
return builder.finish();
westIndices.sort((a, b) => vBuffer[a] - vBuffer[b]);
eastIndices.sort((a, b) => vBuffer[a] - vBuffer[b]);
northIndices.sort((a, b) => uBuffer[a] - uBuffer[b]);
southIndices.sort((a, b) => uBuffer[a] - uBuffer[b]);
const generateSkirts = (indexes) => {
const quv = new core_common_1.QPoint2d();
const param = new core_geometry_1.Point2d();
for (let i = 0; i < indexes.length; i++) {
const index = indexes[i];
const uvIndex = index * 2;
const height = minHeight + heightBuffer[index] * heightScale;
quv.setFromScalars(builder.uvs.buffer.at(uvIndex), builder.uvs.buffer.at(uvIndex + 1));
builder.uvs.params.unquantize(quv.x, quv.y, param);
const oen = wantNormals && builder.normals ? builder.normals.at(index) : undefined;
builder.addVertex(projection.getPoint(param.x, param.y, height - skirtHeight), quv, oen);
if (i !== 0) {
const nextPointIndex = builder.positions.length;
builder.addTriangle(index, indexes[i - 1], nextPointIndex - 2);
builder.addTriangle(index, nextPointIndex - 2, nextPointIndex - 1);
}
}
};
generateSkirts(westIndices);
generateSkirts(eastIndices);
generateSkirts(southIndices);
generateSkirts(northIndices);
return builder.finish();
}
constructUrl(row, column, zoomLevel) {
return this._tileUrlTemplate.replace("{z}", zoomLevel.toString()).replace("{x}", column.toString()).replace("{y}", row.toString());
}
/**
* Specifies the quality of terrain created from heightmaps. A value of 1.0 will
* ensure that adjacent heightmap vertices are separated by no more than
* screen pixels and will probably go very slowly.
* A value of 0.5 will cut the estimated level zero geometric error in half, allowing twice the
* screen pixels between adjacent heightmap vertices and thus rendering more quickly.
* @type {Number}
*/
heightmapTerrainQuality = 0.25;
/**
* Determines an appropriate geometric error estimate when the geometry comes from a heightmap.
*
* @param {Ellipsoid} ellipsoid The ellipsoid to which the terrain is attached.
* @param {Number} tileImageWidth The width, in pixels, of the heightmap associated with a single tile.
* @param {Number} numberOfTilesAtLevelZero The number of tiles in the horizontal direction at tile level zero.
* @returns {Number} An estimated geometric error.
*/
getEstimatedLevelZeroGeometricErrorForAHeightmap(ellipsoidMaximumRadius = 6378137, tileImageWidth = 65, numberOfTilesAtLevelZero = 2) {
return ellipsoidMaximumRadius * 2 * Math.PI * this.heightmapTerrainQuality / (tileImageWidth * numberOfTilesAtLevelZero);
}
getLevelMaximumGeometricError(level) {
return this.getEstimatedLevelZeroGeometricErrorForAHeightmap() / (1 << level);
}
getHeightRange(parentRange, quadId) {
const heightRange = quadId.level <= 6 ? ApproximateTerrainHeights_1.ApproximateTerrainHeights.instance.getTileHeightRange(quadId) : undefined;
return undefined === heightRange ? parentRange : heightRange;
}
}
//# sourceMappingURL=CesiumTerrainProvider.js.map