UNPKG

3d-tiles-renderer

Version:

https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification

347 lines (218 loc) 7.65 kB
import { CanvasTexture, SRGBColorSpace } from 'three'; import { RegionImageSource } from './RegionImageSource.js'; import { DataCache } from '../utils/DataCache.js'; import { VectorShapeCanvasRenderer } from '../utils/VectorShapeCanvasRenderer.js'; import { TilingScheme } from '../utils/TilingScheme.js'; import { ProjectionScheme } from '../utils/ProjectionScheme.js'; import { forEachTileInBounds } from '../overlays/utils.js'; let _mvtImport = null; function importMVTDeps() { return _mvtImport ??= Promise.all( [ import( '@mapbox/vector-tile' ), import( 'pbf' ), ] ).then( ( [ { VectorTile }, { default: Protobuf } ] ) => { return { VectorTile, Protobuf }; } ); } // Protomaps "Light" theme — from protomaps/basemaps flavors.ts const DEFAULT_STYLES = { earth: { fill: '#e2dfda', order: 0 }, water: { fill: '#80deea', order: 1 }, landcover: { fill: '#c4e7d2', order: 2 }, landuse: { fill: '#cfddd5', order: 3 }, natural: { fill: '#e2e0d7', order: 4 }, buildings: { fill: '#cccccc', order: 5 }, roads: { stroke: '#ebebeb', order: 6 }, transit: { stroke: '#a7b1b3', order: 7 }, boundaries: { stroke: '#adadad', order: 8 }, places: { fill: '#5c5c5c', order: 9 }, pois: { fill: '#1a8cbd', radius: 3, order: 10 }, }; const DEFAULT_GET_STYLE = ( layer, properties ) => { return DEFAULT_STYLES[ layer ] ?? null; }; // Fetches and caches parsed MVT tile content (vectorTile + tileBounds) keyed by (tx, ty, tl). export class MVTContentCache extends DataCache { constructor( options = {} ) { super(); const { url = null, levels = 20, projection = 'EPSG:3857', } = options; this.url = url; this.levels = levels; this.projectionId = projection; this.tiling = new TilingScheme(); this.fetchData = ( ...args ) => fetch( ...args ); this.fetchOptions = {}; } init() { const { tiling, levels, url, projectionId } = this; tiling.flipY = ! /{\s*reverseY|-\s*y\s*}/g.test( url ); tiling.setProjection( new ProjectionScheme( projectionId ) ); tiling.setContentBounds( ...tiling.projection.getBounds() ); // 512 is used as the reference tile size for zoom level selection, matching the approach // used by Maplibre GL JS (see coveringZoomLevel in maplibre-gl-js/src/geo/projection/covering_tiles.ts). const TILE_SIZE = 512; if ( Array.isArray( levels ) ) { levels.forEach( ( info, level ) => { if ( info !== null ) { tiling.setLevel( level, { tilePixelWidth: TILE_SIZE, tilePixelHeight: TILE_SIZE, ...info, } ); } } ); } else { tiling.generateLevels( levels, tiling.projection.tileCountX, tiling.projection.tileCountY, { tilePixelWidth: TILE_SIZE, tilePixelHeight: TILE_SIZE, } ); } return Promise.resolve(); } async fetchItem( [ tx, ty, tl ], signal ) { const url = this.getUrl( tx, ty, tl ); const res = await this.fetchData( url, { ...this.fetchOptions, signal } ); const buffer = await res.arrayBuffer(); return this._parseVectorTile( buffer ); } async _parseVectorTile( buffer ) { if ( ! buffer || buffer.byteLength === 0 ) { return null; } const { VectorTile, Protobuf } = await importMVTDeps(); return new VectorTile( new Protobuf( buffer ) ); } // Parsed JS objects — nothing to dispose disposeItem() {} getUrl( x, y, level ) { return this.url .replace( /{\s*z\s*}/gi, level ) .replace( /{\s*x\s*}/gi, x ) .replace( /{\s*(y|reverseY|-\s*y)\s*}/gi, y ); } } export class MVTImageSource extends RegionImageSource { get tiling() { return this._contentCache.tiling; } get fetchData() { return this._contentCache.fetchData; } set fetchData( v ) { this._contentCache.fetchData = v; } get fetchOptions() { return this._contentCache.fetchOptions; } set fetchOptions( v ) { this._contentCache.fetchOptions = v; } constructor( options = {} ) { const { resolution = 512, getStyle = null, contentCache, ...rest } = options; super(); this.resolution = resolution; this.getStyle = getStyle; this._canvasRenderer = new VectorShapeCanvasRenderer( { tileExtent: 4096 } ); this._contentCache = contentCache ?? new MVTContentCache( rest ); } init() { return this._contentCache.init(); } hasContent( minX, minY, maxX, maxY, level ) { let count = 0; forEachTileInBounds( [ minX, minY, maxX, maxY ], level, this._contentCache.tiling, () => count ++ ); return count > 0; } async fetchItem( [ minX, minY, maxX, maxY, level ], signal ) { const { resolution, _contentCache } = this; const canvas = document.createElement( 'canvas' ); canvas.width = resolution; canvas.height = resolution; const regionBounds = [ minX, minY, maxX, maxY ]; const promises = []; forEachTileInBounds( regionBounds, level, _contentCache.tiling, ( tx, ty, tl ) => { promises.push( _contentCache.lock( tx, ty, tl ) ); } ); await Promise.all( promises ); signal?.throwIfAborted(); this._drawToCanvas( canvas, regionBounds, level ); const tex = new CanvasTexture( canvas ); tex.colorSpace = SRGBColorSpace; tex.generateMipmaps = false; tex.needsUpdate = true; return tex; } disposeItem( texture, [ minX, minY, maxX, maxY, level ] ) { forEachTileInBounds( [ minX, minY, maxX, maxY ], level, this._contentCache.tiling, ( tx, ty, tl ) => { this._contentCache.release( tx, ty, tl ); } ); if ( texture ) { texture.dispose(); } } redraw( ...args ) { const [ minX, minY, maxX, maxY, level ] = args; const tex = this.get( minX, minY, maxX, maxY, level ); if ( ! tex ) { return; } this._drawToCanvas( tex.image, [ minX, minY, maxX, maxY ], level ); tex.needsUpdate = true; } dispose() { super.dispose(); this._contentCache.dispose(); } _drawToCanvas( canvas, regionBounds, level ) { const { _contentCache, _canvasRenderer } = this; const ctx = canvas.getContext( '2d' ); forEachTileInBounds( regionBounds, level, _contentCache.tiling, ( tx, ty, tl ) => { const tileBounds = _contentCache.tiling.getTileBounds( tx, ty, tl, true, false ); _canvasRenderer.setFrame( ctx, tileBounds, regionBounds ); const vectorTile = _contentCache.get( tx, ty, tl ); if ( vectorTile ) { this._renderVectorTile( vectorTile ); } } ); } _renderVectorTile( vectorTile ) { const { _canvasRenderer } = this; const getStyle = this.getStyle || DEFAULT_GET_STYLE; // Sort layers by user-defined order, falling back to alphabetical. const layerNames = [ ...Object.keys( vectorTile.layers ) ].sort( ( a, b ) => { const orderA = getStyle( a, null )?.order ?? VectorShapeCanvasRenderer.DEFAULT_STYLE.order; const orderB = getStyle( b, null )?.order ?? VectorShapeCanvasRenderer.DEFAULT_STYLE.order; if ( orderA !== orderB ) return orderA - orderB; return a.localeCompare( b ); } ); // render each layer for ( const layerName of layerNames ) { const layer = vectorTile.layers[ layerName ]; for ( let i = 0; i < layer.length; i ++ ) { const feature = layer.feature( i ); const { properties, type } = feature; // Apply per-feature style; skip invisible features. const style = getStyle( layerName, properties ); _canvasRenderer.setStyle( style ); // Dispatch to the appropriate draw primitive (1=point, 2=line, 3=polygon). const geometry = feature.loadGeometry(); if ( type === 1 ) { _canvasRenderer._renderPoints( geometry ); } else if ( type === 2 ) { _canvasRenderer._renderLines( geometry ); } else if ( type === 3 ) { _canvasRenderer._renderPolygons( geometry ); } } } } }