Skip to content

Commit

Permalink
Add support for TMS Tiles (#961)
Browse files Browse the repository at this point in the history
* Add initial xyz plugin

* update

* Update XYZ tiles

* comment update

* Add XYZTilesPlugin, share base implementation

* Remove log, delete origin DZI plugin

* no message

* Adjust levels behavior

* Fixes for TMS

* Fix bounds
  • Loading branch information
gkjohnson authored Feb 7, 2025
1 parent a021f72 commit 0ad44d5
Show file tree
Hide file tree
Showing 2 changed files with 197 additions and 18 deletions.
8 changes: 6 additions & 2 deletions src/plugins/three/CesiumIonAuthPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,16 @@ export class CesiumIonAuthPlugin {

}

loadRootTileSet( ...args ) {
loadRootTileSet() {

// ensure we have an up-to-date token and root url, then trigger the internal
// root tile set load function
return this._refreshToken()
.then( () => this.tiles.loadRootTileSet( this.tiles.rootURL ) );
.then( () => {

return this.tiles.invokeOnePlugin( plugin => plugin !== this && plugin.loadRootTileSet && plugin.loadRootTileSet() );

} );

}

Expand Down
207 changes: 191 additions & 16 deletions src/plugins/three/ImageFormatPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ const TILE_LEVEL = Symbol( 'TILE_LEVEL' );
// Base class for supporting tiled images with a consistent size / resolution per tile
class ImageFormatPlugin {

get maxLevel() {

return this.levels - 1;

}

constructor( options = {} ) {

const {
Expand All @@ -17,12 +23,16 @@ class ImageFormatPlugin {
this.priority = - 10;
this.tiles = null;

// tile dimensions in pixels
this.tileWidth = null;
this.tileHeight = null;

// full image dimensions in pixels
this.width = null;
this.height = null;
this.levels = null;
this.maxZoom = Infinity;

// amount of pixel overlap between tiles
this.overlap = 0;
this.pixelSize = pixelSize;
this.center = center;
Expand All @@ -39,12 +49,11 @@ class ImageFormatPlugin {
async parseToMesh( buffer, tile, extension, uri, abortSignal ) {

// Construct texture
const { flipY } = this;
const blob = new Blob( [ buffer ] );
const imageBitmap = await createImageBitmap( blob, {
premultiplyAlpha: 'none',
colorSpaceConversion: 'none',
imageOrientation: flipY ? 'flipY' : 'from-image',
imageOrientation: 'flipY',
} );
const texture = new Texture( imageBitmap );
texture.generateMipmaps = false;
Expand All @@ -66,12 +75,12 @@ class ImageFormatPlugin {
preprocessNode( tile, dir, parentTile ) {

// generate children
const { levels } = this;
const { maxLevel } = this;
const level = tile[ TILE_LEVEL ];
const x = tile[ TILE_X ];
const y = tile[ TILE_Y ];

if ( level < levels ) {
if ( level < maxLevel ) {

for ( let cx = 0; cx < 2; cx ++ ) {

Expand Down Expand Up @@ -99,9 +108,55 @@ class ImageFormatPlugin {
version: '1.1'
},
geometricError: 1e5,
root: this.expand( 0, 0, 0 ),
root: {
refine: 'REPLACE',
geometricError: 1e5,
boundingVolume: {},
children: [],
}
};

const { maxLevel, width, height, tileWidth, tileHeight } = this;
const levelFactor = 2 ** - maxLevel;
const tilesX = Math.ceil( levelFactor * width / tileWidth );
const tilesY = Math.ceil( levelFactor * height / tileHeight );

// generate all children for the root and construct a merged bounding box
let minX = Infinity;
let maxX = - Infinity;
let minY = Infinity;
let maxY = - Infinity;
for ( let x = 0; x < tilesX; x ++ ) {

for ( let y = 0; y < tilesY; y ++ ) {

const child = this.expand( 0, x, y );
tileset.root.children.push( child );

const { box } = child.boundingVolume;
const [ cx, cy ] = box;
const hx = box[ 3 ];
const hy = box[ 7 ];

minX = Math.min( minX, cx - hx );
maxX = Math.max( maxX, cx + hx );

minY = Math.min( minY, cy - hy );
maxY = Math.max( maxY, cy + hy );

}

}

const bw = maxX - minX;
const bh = maxY - minY;
tileset.root.boundingVolume.box = [
minX + bw / 2, minY + bh / 2, 0,
bw / 2, 0, 0,
0, bh / 2, 0,
0, 0, 0,
];

this.tiles.preprocessTileSet( tileset, baseUrl );
return tileset;

Expand All @@ -115,13 +170,13 @@ class ImageFormatPlugin {

expand( level, x, y ) {

const { levels, width, height, overlap, pixelSize, center, tileWidth, tileHeight, flipY } = this;
const { maxLevel, width, height, overlap, pixelSize, center, tileWidth, tileHeight, flipY } = this;

// offset for the image so it's center
const offsetX = center ? pixelSize * - width / 2 : 0;
const offsetY = center ? pixelSize * - height / 2 : 0;

const levelFactor = 2 ** - ( levels - level );
const levelFactor = 2 ** - ( maxLevel - level );
const levelWidth = Math.ceil( width * levelFactor );
const levelHeight = Math.ceil( height * levelFactor );

Expand Down Expand Up @@ -174,8 +229,8 @@ class ImageFormatPlugin {
const ratioY = height / levelHeight;
const flipFactor = flipY ? - 1 : 1;

// Generate the root node
return {
// Generate the node
refine: 'REPLACE',
geometricError: pixelSize * ( Math.max( width / levelWidth, height / levelHeight ) - 1 ),
boundingVolume: {
Expand All @@ -188,7 +243,7 @@ class ImageFormatPlugin {
// x, y, z half vectors
ratioX * pixelSize * tileWidthOverlap / 2, 0.0, 0.0,
0.0, ratioY * pixelSize * tileHeightOverlap / 2, 0.0,
0.0, 0.0, 0,
0.0, 0.0, 0.0,
],
},
content: {
Expand All @@ -213,7 +268,7 @@ export class XYZTilesPlugin extends ImageFormatPlugin {
constructor( options = {} ) {

const {
levels = 19,
levels = 20,
tileDimension = 256,
pixelSize = 1e-5,
...rest
Expand All @@ -233,12 +288,12 @@ export class XYZTilesPlugin extends ImageFormatPlugin {
async loadRootTileSet() {

// transform the url
const { tiles, tileWidth, tileHeight, levels } = this;
const { tiles, tileWidth, tileHeight, maxLevel } = this;
let url = tiles.rootURL;
tiles.invokeAllPlugins( plugin => url = plugin.preprocessURL ? plugin.preprocessURL( url, null ) : url );

this.width = tileWidth * ( 2 ** levels );
this.height = tileHeight * ( 2 ** levels );
this.width = tileWidth * ( 2 ** maxLevel );
this.height = tileHeight * ( 2 ** maxLevel );
this.url = url;

return this.getTileset( url );
Expand All @@ -253,6 +308,126 @@ export class XYZTilesPlugin extends ImageFormatPlugin {

}

// TODO
// - fix 3827
// - fix bing maps
// - fix sentinal-2 data set

// Support for TMS tiles
// https://wiki.osgeo.org/wiki/Tile_Map_Service_Specification
export class TMSTilesPlugin extends ImageFormatPlugin {

constructor( ...args ) {

super( ...args );

this.name = 'TMS_TILES_PLUGIN';
this.flipY = false;
this.url = null;
this.extension = null;

}

loadRootTileSet() {

const url = new URL( 'tilemapresource.xml', this.tiles.rootURL ).toString();
return this.tiles
.invokeOnePlugin( plugin => plugin.fetchData && plugin.fetchData( url, this.tiles.fetchOptions ) )
.then( res => res.text() )
.then( text => {

// TODO: "unitsPerPixel" may not be necessary
const xml = new DOMParser().parseFromString( text, 'text/xml' );
// const boundingBox = xml.querySelector( 'BoundingBox' );
// const origin = xml.querySelector( 'Origin' );
const tileFormat = xml.querySelector( 'TileFormat' );
const tileSets = xml.querySelector( 'TileSets' );
const tileSetList = [ ...tileSets.querySelectorAll( 'TileSet' ) ]
.map( ts => ( {
href: parseInt( ts.getAttribute( 'href' ) ),
unitsPerPixel: parseFloat( ts.getAttribute( 'units-per-pixel' ) ),
order: parseInt( ts.getAttribute( 'order' ) ),
} ) )
.sort( ( a, b ) => {

return a.order - b.order;

} );

// TODO: need to account for these values (origin and min values) when generating ellipsoid
// TODO: might want to account for this offset when positioning the tiles? Or expose it? Could be
// used for overlays.
// the extents of the tile set in lat / lon
// const minX = parseFloat( boundingBox.getAttribute( 'minx' ) );
// const maxX = parseFloat( boundingBox.getAttribute( 'maxx' ) );
// const minY = parseFloat( boundingBox.getAttribute( 'miny' ) );
// const maxY = parseFloat( boundingBox.getAttribute( 'maxy' ) );
// const width = maxX - minX;
// const height = maxY - minY;

// origin in lat / lon
// const x = parseFloat( origin.getAttribute( 'x' ) );
// const y = parseFloat( origin.getAttribute( 'y' ) );

// image dimensions in pixels
const tileWidth = parseInt( tileFormat.getAttribute( 'width' ) );
const tileHeight = parseInt( tileFormat.getAttribute( 'height' ) );
const extension = tileFormat.getAttribute( 'extension' );

const profile = tileSets.getAttribute( 'profile' );
// const srs = xml.querySelector( 'SRS' ).textContent;

// if ( srs !== 'EPSG:3857' ) {

// // EPSG:4326
// throw new Error( `TMSTilesPlugin: ${ srs } SRS not supported.` );

// }

// if ( profile !== 'mercator' && profile !== 'global-mercator' ) {

// // 'geodetic', 'global-geodetic'
// throw new Error( `TMSTilesPlugin: Profile ${ profile } not supported.` );

// }

// TODO: global-geodetic seems to require two horizontal root tiles. Is hardcoding the right way?
let widthMultiplier = 1;
// let heightMultiplier = 1;
switch ( profile ) {

case 'global-geodetic':
widthMultiplier = 2;
break;

}

const levels = tileSetList.length;
const maxLevel = levels - 1;
this.extension = extension;
this.width = widthMultiplier * tileWidth * ( 2 ** maxLevel );
this.height = tileHeight * ( 2 ** maxLevel );
this.tileWidth = tileWidth;
this.tileHeight = tileHeight;
this.levels = levels;
this.url = this.tiles.rootURL;
this.tileSets = tileSetList;

return this.getTileset( url );

} );

}

getUrl( level, x, y ) {

const { url, extension, tileSets } = this;
return new URL( `${ tileSets[ level ].href }/${ x }/${ y }.${ extension }`, url ).toString();

}

}

// Support for Deep Zoom Image format
// https://openseadragon.github.io/
// https://learn.microsoft.com/en-us/previous-versions/windows/silverlight/dotnet-windows-silverlight/cc645077(v=vs.95)
Expand Down Expand Up @@ -288,7 +463,7 @@ export class DeepZoomImagePlugin extends ImageFormatPlugin {
// can be immediately queried without any hierarchy traversal. Due the flexibility of camera
// type, rotation, and per-tile error calculation we generate a hierarchy.
return tiles
.invokeOnePlugin( plugin => plugin.fetchData && plugin.fetchData( url, this.fetchOptions ) )
.invokeOnePlugin( plugin => plugin.fetchData && plugin.fetchData( url, this.tiles.fetchOptions ) )
.then( res => res.text() )
.then( text => {

Expand All @@ -311,7 +486,7 @@ export class DeepZoomImagePlugin extends ImageFormatPlugin {
this.format = image.getAttribute( 'Format' );
this.width = parseInt( size.getAttribute( 'Width' ) );
this.height = parseInt( size.getAttribute( 'Height' ) );
this.levels = Math.ceil( Math.log2( Math.max( this.width, this.height ) ) );
this.levels = Math.ceil( Math.log2( Math.max( this.width, this.height ) ) ) + 1;
this.stem = url.split( /\.[^.]+$/g )[ 0 ];

return this.getTileset( url );
Expand Down

0 comments on commit 0ad44d5

Please sign in to comment.