3d-tiles-renderer
Version:
https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification
333 lines (230 loc) • 8.17 kB
JavaScript
import { WGS84_RADIUS, LoaderBase } from '3d-tiles-renderer/core';
import { ProjectionScheme } from '../images/utils/ProjectionScheme.js';
import { MathUtils } from 'three';
const EQUATOR_CIRCUMFERENCE = WGS84_RADIUS * Math.PI * 2;
const mercatorProjection = /* @__PURE__ */ new ProjectionScheme( 'EPSG:3857' );
function isEPSG4326( crs ) {
return /:4326$/i.test( crs );
}
function isWebMercator( crs ) {
return /:3857$/i.test( crs );
}
// parse a series of space-separated numbers
function parseTuple( tuple ) {
return tuple.trim().split( /\s+/ ).map( v => parseFloat( v ) );
}
// swap the tuple order to lon, lat if crs is EPSG:4326 since the tiles renderer project
// expects lon, lat order
function correctTupleOrder( tuple, crs ) {
if ( isEPSG4326( crs ) ) {
[ tuple[ 1 ], tuple[ 0 ] ] = [ tuple[ 0 ], tuple[ 1 ] ];
}
}
// web mercator specifies bounding boxes etc as meters assuming the width and height of the full image span
// is aligned to the globe equator circumference.
function correctTupleUnits( tuple, crs ) {
if ( isWebMercator( crs ) ) {
tuple[ 0 ] = mercatorProjection.convertProjectionToLongitude( 0.5 + tuple[ 0 ] / EQUATOR_CIRCUMFERENCE );
tuple[ 1 ] = mercatorProjection.convertProjectionToLatitude( 0.5 + tuple[ 1 ] / EQUATOR_CIRCUMFERENCE );
// to degrees
tuple[ 0 ] *= MathUtils.RAD2DEG;
tuple[ 1 ] *= MathUtils.RAD2DEG;
return tuple;
}
}
function tupleToRadians( tuple ) {
tuple[ 0 ] *= MathUtils.DEG2RAD;
tuple[ 1 ] *= MathUtils.DEG2RAD;
}
export class WMTSCapabilitiesLoader extends LoaderBase {
parse( buffer ) {
const str = new TextDecoder( 'utf-8' ).decode( new Uint8Array( buffer ) );
const xml = new DOMParser().parseFromString( str, 'text/xml' );
const contents = xml.querySelector( 'Contents' );
const tileMatrixSets = getChildrenByTag( contents, 'TileMatrixSet' ).map( el => parseTileMatrixSet( el ) );
const layers = getChildrenByTag( contents, 'Layer' ).map( el => parseLayer( el ) );
const serviceIdentification = parseServiceIdentification( xml.querySelector( 'ServiceIdentification' ) );
layers.forEach( layer => {
layer.tileMatrixSets = layer.tileMatrixSetLinks.map( key => {
return tileMatrixSets.find( tms => tms.identifier === key );
} );
} );
return {
serviceIdentification,
tileMatrixSets,
layers,
};
}
}
// parse <ows:ServiceIdentification> tag
function parseServiceIdentification( el ) {
const title = el.querySelector( 'Title' ).textContent;
const abstract = el.querySelector( 'Abstract' )?.textContent || '';
const serviceType = el.querySelector( 'ServiceType' ).textContent;
const serviceTypeVersion = el.querySelector( 'ServiceTypeVersion' ).textContent;
return {
title,
abstract,
serviceType,
serviceTypeVersion,
};
}
// parse <Layers> tag
function parseLayer( el ) {
const title = el.querySelector( 'Title' ).textContent;
const identifier = el.querySelector( 'Identifier' ).textContent;
const format = el.querySelector( 'Format' ).textContent;
const resourceUrls = getChildrenByTag( el, 'ResourceURL' ).map( el => {
return parseResourceUrl( el );
} );
const tileMatrixSetLinks = getChildrenByTag( el, 'TileMatrixSetLink' ).map( el => {
return getChildrenByTag( el, 'TileMatrixSet' )[ 0 ].textContent;
} );
const styles = getChildrenByTag( el, 'Style' ).map( el => {
return parseStyle( el );
} );
const dimensions = getChildrenByTag( el, 'Dimension' ).map( el => {
return parseDimension( el );
} );
let boundingBox = parseBoundingBox( el.querySelector( 'WGS84BoundingBox' ) );
if ( ! boundingBox ) {
boundingBox = parseBoundingBox( el.querySelector( 'BoundingBox' ) );
}
return {
title,
identifier,
format,
dimensions,
tileMatrixSetLinks,
styles,
boundingBox,
resourceUrls,
};
}
// parse layer <ResourceURL> tag
function parseResourceUrl( el ) {
const template = el.getAttribute( 'template' );
const format = el.getAttribute( 'format' );
const resourceType = el.getAttribute( 'resourceType' );
return {
template,
format,
resourceType,
};
}
// parse layer <Dimension> tag
function parseDimension( el ) {
const identifier = el.querySelector( 'Identifier' ).textContent;
const uom = el.querySelector( 'UOM' )?.textContent || '';
const defaultValue = el.querySelector( 'Default' ).textContent;
const current = el.querySelector( 'Current' )?.textContent === 'true';
const values = getChildrenByTag( el, 'Value' ).map( el => el.textContent );
return {
identifier,
uom,
defaultValue,
current,
values,
};
}
// parse <ows:WGS84BoundingBox> and <BoundingBox> tags
function parseBoundingBox( el ) {
if ( ! el ) {
return null;
}
const crs = el.nodeName.endsWith( 'WGS84BoundingBox' ) ? 'urn:ogc:def:crs:CRS::84' : el.getAttribute( 'crs' );
const lowerCorner = parseTuple( el.querySelector( 'LowerCorner' ).textContent );
const upperCorner = parseTuple( el.querySelector( 'UpperCorner' ).textContent );
correctTupleOrder( lowerCorner, crs );
correctTupleOrder( upperCorner, crs );
correctTupleUnits( lowerCorner, crs );
correctTupleUnits( upperCorner, crs );
tupleToRadians( lowerCorner );
tupleToRadians( upperCorner );
return {
crs,
lowerCorner,
upperCorner,
bounds: [ ...lowerCorner, ...upperCorner ],
};
}
// parse layer <Style> tag
function parseStyle( el ) {
const title = el.querySelector( 'Title' )?.textContent || null;
const identifier = el.querySelector( 'Identifier' ).textContent;
const isDefault = el.getAttribute( 'isDefault' ) === 'true';
return {
title,
identifier,
isDefault,
};
}
// parse <TileMatrixSet> tag
function parseTileMatrixSet( el ) {
const supportedCRS = el.querySelector( 'SupportedCRS' ).textContent;
const title = el.querySelector( 'Title' )?.textContent || '';
const identifier = el.querySelector( 'Identifier' ).textContent;
const abstract = el.querySelector( 'Abstract' )?.textContent || '';
const tileMatrices = [];
el
.querySelectorAll( 'TileMatrix' )
.forEach( ( el, i ) => {
const tm = parseTileMatrix( el );
const pixelSpan = 0.00028 * tm.scaleDenominator;
const groundWidth = tm.tileWidth * tm.matrixWidth * pixelSpan;
const groundHeight = tm.tileHeight * tm.matrixHeight * pixelSpan;
let bottomRightCorner;
correctTupleOrder( tm.topLeftCorner, supportedCRS );
if ( isWebMercator( supportedCRS ) ) {
bottomRightCorner = [
tm.topLeftCorner[ 0 ] + groundWidth,
tm.topLeftCorner[ 1 ] - groundHeight,
];
} else {
bottomRightCorner = [
tm.topLeftCorner[ 0 ] + 360 * groundWidth / EQUATOR_CIRCUMFERENCE,
tm.topLeftCorner[ 1 ] - 360 * groundHeight / EQUATOR_CIRCUMFERENCE,
];
}
correctTupleUnits( bottomRightCorner, supportedCRS );
correctTupleUnits( tm.topLeftCorner, supportedCRS );
tupleToRadians( bottomRightCorner );
tupleToRadians( tm.topLeftCorner );
// construct the bounds
tm.bounds = [ ...tm.topLeftCorner, ...bottomRightCorner ];
// ensure min and max order is correct
[ tm.bounds[ 1 ], tm.bounds[ 3 ] ] = [ tm.bounds[ 3 ], tm.bounds[ 1 ] ];
tileMatrices.push( tm );
} );
return {
title,
identifier,
abstract,
supportedCRS,
tileMatrices,
};
}
// parse tile matrix set <TileMatrix> tag
function parseTileMatrix( el ) {
const identifier = el.querySelector( 'Identifier' ).textContent;
const tileWidth = parseFloat( el.querySelector( 'TileWidth' ).textContent );
const tileHeight = parseFloat( el.querySelector( 'TileHeight' ).textContent );
const matrixWidth = parseFloat( el.querySelector( 'MatrixWidth' ).textContent );
const matrixHeight = parseFloat( el.querySelector( 'MatrixHeight' ).textContent );
const scaleDenominator = parseFloat( el.querySelector( 'ScaleDenominator' ).textContent );
const topLeftCorner = parseTuple( el.querySelector( 'TopLeftCorner' ).textContent );
return {
identifier,
tileWidth,
tileHeight,
matrixWidth,
matrixHeight,
scaleDenominator,
topLeftCorner,
bounds: null,
};
}
// utility for finding immediate children by tag name
function getChildrenByTag( el, tag ) {
return [ ...el.children ].filter( c => c.tagName === tag );
}