500 lines
12 KiB
JavaScript
500 lines
12 KiB
JavaScript
// @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.
|
|
*
|
|
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
|
|
*/
|
|
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;
|