| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499 |
- // @ts-check
-
- import Cartographic from "../Core/Cartographic.js";
- import Cesium3DTileset from "./Cesium3DTileset.js";
- import DeveloperError from "../Core/DeveloperError.js";
- import Ellipsoid from "../Core/Ellipsoid.js";
- import Rectangle from "../Core/Rectangle.js";
- import Resource from "../Core/Resource.js";
- import getAbsoluteUri from "../Core/getAbsoluteUri.js";
- import WebMercatorTilingScheme from "../Core/WebMercatorTilingScheme.js";
- import defined from "../Core/defined.js";
- import destroyObject from "../Core/destroyObject.js";
- import CesiumMath from "../Core/Math.js";
-
- /** @import FrameState from "./FrameState.js"; */
- /** @import PassState from "../Renderer/PassState.js"; */
- /** @import TilingScheme from "../Core/TilingScheme.js"; */
-
- /**
- * @typedef {object} TilesetJsonRoot
- * @property {object} [boundingVolume]
- * @property {number} [geometricError]
- * @property {"REPLACE"|"ADD"} [refine]
- * @property {unknown} [content]
- * @property {unknown[]} [children]
- * @ignore
- */
-
- const DEFAULT_MIN_ZOOM = 0;
- const DEFAULT_MAX_ZOOM = 14;
- const DEFAULT_REGION_MINIMUM_HEIGHT = -1000.0;
- const DEFAULT_REGION_MAXIMUM_HEIGHT = 10000.0;
- const EARTH_CIRCUMFERENCE_METERS =
- 2.0 * Math.PI * Ellipsoid.WGS84.maximumRadius;
- const WEB_MERCATOR_TILE_SIZE = 256.0;
-
- const scratchTileRectangle = new Rectangle();
- const scratchIntersectionRectangle = new Rectangle();
-
- /**
- * Base provider for URL-template vector sources that are rendered through a
- * runtime-generated 3D Tiles tileset.
- *
- * @private
- */
- class UrlTemplate3DTilesDataProvider {
- /**
- * @param {Resource|string} urlTemplate URL template containing {z}, {x}, and {y} placeholders.
- * @param {object} [options] Provider options.
- * @param {number} [options.minZoom=0] Minimum zoom level represented in the generated tileset.
- * @param {number} [options.maxZoom=14] Maximum zoom level represented in the generated tileset.
- * @param {Rectangle} [options.extent] Optional geographic extent in radians to constrain the generated tile tree.
- * @param {string} [options.featureIdProperty] Feature property name to use as feature ID when supported by content decoding.
- */
- constructor(urlTemplate, options) {
- options = options ?? {};
- this._resource = Resource.createIfNeeded(urlTemplate);
- // @ts-expect-error Missing types.
- this._urlTemplate = this._resource.url;
- this._extent = Rectangle.clone(options.extent);
- this._minZoom = options.minZoom ?? DEFAULT_MIN_ZOOM;
- this._maxZoom = options.maxZoom ?? DEFAULT_MAX_ZOOM;
- this._featureIdProperty = options.featureIdProperty;
- this._show = true;
- this._tileset = undefined;
- this._tilesetJsonUrl = undefined;
- }
-
- /**
- * Creates a provider from a URL template.
- *
- * @param {Resource|string} url URL template containing {z}, {x}, and {y} placeholders.
- * @param {object} [options] Provider options.
- * @returns {Promise<UrlTemplate3DTilesDataProvider>}
- */
- static async fromUrl(url, options) {
- const provider = new this(url, options);
- await provider._initializeTileset();
- return provider;
- }
-
- /**
- * URL template containing {z}/{x}/{y}.
- *
- * @type {string}
- * @readonly
- */
- get urlTemplate() {
- return this._urlTemplate;
- }
-
- /**
- * Resource derived from the URL template.
- *
- * @type {Resource}
- * @readonly
- */
- get resource() {
- return this._resource;
- }
-
- /**
- * Optional geographic extent in radians used to generate tile headers.
- *
- * @type {Rectangle|undefined}
- * @readonly
- */
- get extent() {
- return this._extent;
- }
-
- /**
- * Backing 3D Tileset.
- *
- * @type {Cesium3DTileset|undefined}
- * @readonly
- */
- get tileset() {
- return this._tileset;
- }
-
- /**
- * Determines if the generated tileset is shown.
- *
- * @type {boolean}
- */
- get show() {
- return this._show;
- }
-
- set show(value) {
- this._show = value;
- if (defined(this._tileset)) {
- this._tileset.show = value;
- }
- }
-
- /** @protected */
- _createRuntimeTilesetOptions() {
- return {
- minZoom: this._minZoom,
- maxZoom: this._maxZoom,
- extent: this._extent,
- };
- }
-
- /**
- * @protected
- * @returns {object}
- */
- _createTilesetLoadOptions() {
- return {};
- }
-
- /**
- * @protected
- * @param {Cesium3DTileset} _tileset
- */
- _configureTileset(_tileset) {}
-
- /**
- * Subclasses must return a runtime content codec describing how to turn
- * a downloaded tile payload into a {@link Cesium3DTileContent}. See
- * {@link Cesium3DTileset#_runtimeContentCodec} for the expected shape.
- *
- * @protected
- * @returns {object}
- */
- _createCodec() {
- //>>includeStart('debug', pragmas.debug);
- throw new DeveloperError(
- "UrlTemplate3DTilesDataProvider subclasses must implement _createCodec().",
- );
- //>>includeEnd('debug');
- }
-
- /**
- * @private
- */
- async _initializeTileset() {
- const tilesetJson = buildRuntimeTilesetJson(
- this._resource,
- this._createRuntimeTilesetOptions(),
- );
- const tilesetBlob = new Blob([JSON.stringify(tilesetJson)], {
- type: "application/json",
- });
- const tilesetUrl = URL.createObjectURL(tilesetBlob);
- this._tilesetJsonUrl = tilesetUrl;
-
- try {
- this._tileset = await Cesium3DTileset.fromUrl(
- tilesetUrl,
- this._createTilesetLoadOptions(),
- );
- } catch (error) {
- URL.revokeObjectURL(tilesetUrl);
- this._tilesetJsonUrl = undefined;
- throw error;
- }
- URL.revokeObjectURL(tilesetUrl);
- this._tilesetJsonUrl = undefined;
-
- this._configureTileset(this._tileset);
- this._tileset._runtimeContentCodec = this._createCodec();
- this._tileset.show = this._show;
- }
-
- /**
- * @private
- * @param {FrameState} frameState
- */
- update(frameState) {
- if (defined(this._tileset)) {
- this._tileset.update(frameState);
- }
- }
-
- /**
- * @private
- * @param {FrameState} frameState
- */
- prePassesUpdate(frameState) {
- if (defined(this._tileset)) {
- this._tileset.prePassesUpdate(frameState);
- }
- }
-
- /**
- * @private
- * @param {FrameState} frameState
- */
- postPassesUpdate(frameState) {
- if (defined(this._tileset)) {
- this._tileset.postPassesUpdate(frameState);
- }
- }
-
- /**
- * @private
- * @param {FrameState} frameState
- * @param {PassState} passState
- */
- updateForPass(frameState, passState) {
- if (defined(this._tileset)) {
- this._tileset.updateForPass(frameState, passState);
- }
- }
-
- isDestroyed() {
- return false;
- }
-
- destroy() {
- if (defined(this._tileset)) {
- this._tileset.destroy();
- this._tileset = undefined;
- }
- if (defined(this._tilesetJsonUrl)) {
- URL.revokeObjectURL(this._tilesetJsonUrl);
- this._tilesetJsonUrl = undefined;
- }
- this._resource = undefined;
- this._extent = undefined;
- this._featureIdProperty = undefined;
- return destroyObject(this);
- }
- }
-
- /**
- * @param {Resource} resource
- * @param {object} options
- * @param {number} options.minZoom
- * @param {number} options.maxZoom
- * @param {Rectangle} [options.extent]
- * @returns {object}
- * @ignore
- */
- function buildRuntimeTilesetJson(resource, options) {
- const tilingScheme = new WebMercatorTilingScheme();
- const extent = defined(options.extent)
- ? Rectangle.clone(options.extent)
- : Rectangle.clone(tilingScheme.rectangle);
- const minLevelRange = computeTileRangeForExtent(
- tilingScheme,
- extent,
- options.minZoom,
- );
-
- /** @type {TilesetJsonRoot} */
- const root = {
- boundingVolume: {
- region: rectangleToRegion(extent),
- },
- // Root has no renderable content, so keep a coarse error to ensure
- // refinement reaches the first renderable zoom even when minZoom is high.
- geometricError: computeGeometricError(0),
- refine: "REPLACE",
- children: [],
- };
- for (let y = minLevelRange.minY; y <= minLevelRange.maxY; y++) {
- for (let x = minLevelRange.minX; x <= minLevelRange.maxX; x++) {
- const child = buildTileNode(
- tilingScheme,
- resource,
- extent,
- options.minZoom,
- options.maxZoom,
- x,
- y,
- );
- if (defined(child)) {
- root.children.push(child);
- }
- }
- }
- if (root.children.length === 0) {
- root.geometricError = 0.0;
- }
- return {
- asset: {
- version: "1.1",
- },
- geometricError: root.geometricError,
- root: root,
- };
- }
-
- /**
- * @param {TilingScheme} tilingScheme
- * @param {Resource} resource
- * @param {Rectangle} extent
- * @param {number} level
- * @param {number} maxZoom
- * @param {number} x
- * @param {number} y
- * @ignore
- */
- function buildTileNode(tilingScheme, resource, extent, level, maxZoom, x, y) {
- if (
- !tileIntersectsExtent(
- tilingScheme,
- level,
- x,
- y,
- extent,
- scratchTileRectangle,
- scratchIntersectionRectangle,
- )
- ) {
- return undefined;
- }
- const tileRectangle = tilingScheme.tileXYToRectangle(
- x,
- y,
- level,
- new Rectangle(),
- );
-
- /** @type {TilesetJsonRoot} */
- const node = {
- boundingVolume: {
- region: rectangleToRegion(tileRectangle),
- },
- geometricError: level < maxZoom ? computeGeometricError(level) : 0.0,
- refine: "REPLACE",
- content: {
- uri: resolveTileUrl(resource, level, x, y),
- },
- };
- if (level >= maxZoom) {
- return node;
- }
- const childLevel = level + 1;
- const children = [];
- for (let childY = y * 2; childY <= y * 2 + 1; childY++) {
- for (let childX = x * 2; childX <= x * 2 + 1; childX++) {
- const child = buildTileNode(
- tilingScheme,
- resource,
- extent,
- childLevel,
- maxZoom,
- childX,
- childY,
- );
- if (defined(child)) {
- children.push(child);
- }
- }
- }
- if (children.length > 0) {
- node.children = children;
- } else {
- node.geometricError = 0.0;
- }
- return node;
- }
-
- /**
- * @param {Resource} resource
- * @param {number} level
- * @param {number} x
- * @param {number} y
- * @ignore
- */
- function resolveTileUrl(resource, level, x, y) {
- // @ts-expect-error Missing types.
- const template = resource.url;
- const tileUrl = template
- .replace(/\{z\}/gi, `${level}`)
- .replace(/\{x\}/gi, `${x}`)
- .replace(/\{y\}/gi, `${y}`);
- return getAbsoluteUri(tileUrl);
- }
-
- /**
- * @param {number} level
- * @ignore
- */
- function computeGeometricError(level) {
- return EARTH_CIRCUMFERENCE_METERS / ((1 << level) * WEB_MERCATOR_TILE_SIZE);
- }
-
- /**
- * @param {Rectangle} rectangle
- * @returns {number[]}
- * @ignore
- */
- function rectangleToRegion(rectangle) {
- return [
- rectangle.west,
- rectangle.south,
- rectangle.east,
- rectangle.north,
- DEFAULT_REGION_MINIMUM_HEIGHT,
- DEFAULT_REGION_MAXIMUM_HEIGHT,
- ];
- }
-
- /**
- * @param {TilingScheme} tilingScheme
- * @param {Rectangle} extent
- * @param {number} level
- * @ignore
- */
- function computeTileRangeForExtent(tilingScheme, extent, level) {
- const maxIndex = (1 << level) - 1;
- const nw = Cartographic.fromRadians(extent.west, extent.north);
- const se = Cartographic.fromRadians(extent.east, extent.south);
- const nwTile = tilingScheme.positionToTileXY(nw, level);
- const seTile = tilingScheme.positionToTileXY(se, level);
- if (!defined(nwTile) || !defined(seTile) || extent.west > extent.east) {
- return {
- minX: 0,
- maxX: maxIndex,
- minY: 0,
- maxY: maxIndex,
- };
- }
- return {
- minX: CesiumMath.clamp(Math.min(nwTile.x, seTile.x), 0, maxIndex),
- maxX: CesiumMath.clamp(Math.max(nwTile.x, seTile.x), 0, maxIndex),
- minY: CesiumMath.clamp(Math.min(nwTile.y, seTile.y), 0, maxIndex),
- maxY: CesiumMath.clamp(Math.max(nwTile.y, seTile.y), 0, maxIndex),
- };
- }
-
- /**
- * @param {TilingScheme} tilingScheme
- * @param {number} level
- * @param {number} x
- * @param {number} y
- * @param {Rectangle} extent
- * @param {Rectangle} tileRectangleScratch
- * @param {Rectangle} intersectionScratch
- * @ignore
- */
- function tileIntersectsExtent(
- tilingScheme,
- level,
- x,
- y,
- extent,
- tileRectangleScratch,
- intersectionScratch,
- ) {
- const tileRectangle = tilingScheme.tileXYToRectangle(
- x,
- y,
- level,
- tileRectangleScratch,
- );
- return defined(
- Rectangle.intersection(tileRectangle, extent, intersectionScratch),
- );
- }
-
- export default UrlTemplate3DTilesDataProvider;
|