UNPKG

@itwin/core-frontend

Version:
412 lines • 22.6 kB
"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