UNPKG

3d-tiles-renderer

Version:

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

373 lines (259 loc) 9.34 kB
import { MathUtils, Vector3, Vector2, Matrix4 } from 'three'; import { OccupancyAnnotation } from '../ScreenOccupationManager.js'; // Share path annotation used for text anchors export class LineAnnotation extends OccupancyAnnotation { // number of points in the path get count() { return this.lat.length; } // number of anchors get anchorCount() { return this.anchorPositions.length; } constructor() { super(); // display text for this path this.text = ''; this.characterWidths = []; this.characterRadius = 0; this.totalTextWidth = 0; // the range of the tile this line is associated with this.range = null; // per-sample cartographic coordinates in radians this.lat = []; this.lon = []; // per-sample settled positions in tiles.group local space, filled during settling this.positions = []; // anchors placed along the path, each { i0, i1, alpha, lat, lon, ref } this.anchorPositions = []; // screen positions, cumulative length used for calculating text layout this.screenPositions = []; this.cumulativeLen = []; // cache variables this.cachedMatrix = new Matrix4(); this.cachedResolution = new Vector2(); // set true when settling updates "positions" so updateTransform recomputes the screen // projection even when the camera hasn't moved. this.needsUpdate = false; } // overrides evaluate() { throw new Error(); } // update screen space points and cumulative values for text placement updateTransform( matrix, resolution, cameraPosition ) { const { positions, screenPositions, cachedMatrix, cachedResolution, cumulativeLen, } = this; if ( ! this.needsUpdate && cachedMatrix.equals( matrix ) && cachedResolution.equals( resolution ) ) { return; } this.needsUpdate = false; cachedMatrix.copy( matrix ); cachedResolution.copy( resolution ); while ( screenPositions.length < positions.length ) { screenPositions.push( new Vector3() ); } for ( let i = 0, l = screenPositions.length; i < l; i ++ ) { const position = positions[ i ]; const screenPos = screenPositions[ i ]; // project to screen space screenPos.copy( position ).applyMatrix4( matrix ); // transform to resolution coordinates screenPos.x = ( screenPos.x * 0.5 + 0.5 ) * resolution.width; screenPos.y = ( - screenPos.y * 0.5 + 0.5 ) * resolution.height; screenPos.z = MathUtils.mapLinear( screenPos.z, - 1, 1, 0, 1 ); } // roll up the cumulative placement cumulativeLen.length = screenPositions.length; cumulativeLen[ 0 ] = 0; for ( let i = 1; i < screenPositions.length; i ++ ) { const p0 = screenPositions[ i - 1 ]; const p1 = screenPositions[ i ]; const dx = p1.x - p0.x; const dy = p1.y - p0.y; const len = Math.sqrt( dx * dx + dy * dy ); cumulativeLen[ i ] = cumulativeLen[ i - 1 ] + len; } } // updateCharacterWidthCache( measureChar ) { const { text, characterWidths, properties, layer } = this; characterWidths.length = text.length; let total = 0; for ( let i = 0, l = text.length; i < l; i ++ ) { const width = measureChar( text[ i ], layer, properties ); characterWidths[ i ] = width; total += width; } // compute the radius as the full width of M to add some // margin around the labels this.totalTextWidth = total; this.characterRadius = measureChar( 'M', layer, properties ); } // whether a lat / lon falls within the same tile as this line hasCoverage( lat, lon ) { // e const [ minLon, minLat, maxLon, maxLat ] = this.range; return lon >= minLon && lon <= maxLon && lat >= minLat && lat <= maxLat; } // Place anchors along a path at a fixed "spacing" (geographic, in radians), recording the // bounding sample indices. Short paths receive a single anchor at their midpoint. generateAnchors( spacing ) { const { lat, lon } = this; // segment lengths and total length in cartographic space so anchor count tracks the // path's real-world length rather than the tile's size const segLengths = []; let totalLength = 0; for ( let i = 0, l = lat.length - 1; i < l; i ++ ) { const lat0 = lat[ i ]; const lat1 = lat[ i + 1 ]; const lon0 = lon[ i ]; const lon1 = lon[ i + 1 ]; // TODO: this is using some rough sphere math to space the anchors evenly regardless of // lat but we should figure out a better way to handle this spacing const latMid = 0.5 * ( lat0 + lat1 ); const dLat = lat1 - lat0; const dLon = ( lon1 - lon0 ) * Math.cos( latMid ); const d = Math.sqrt( dLat * dLat + dLon * dLon ); segLengths.push( d ); totalLength += d; } // first anchor offset half a spacing in, fall back to the midpoint for short paths let target = spacing * 0.5; if ( target > totalLength ) { target = totalLength * 0.5; } let currLength = 0; let currIndex = 0; const anchorCandidates = []; while ( target <= totalLength ) { // advance to the segment containing "target" while ( currIndex < segLengths.length && currLength + segLengths[ currIndex ] < target ) { currLength += segLengths[ currIndex ]; currIndex ++; } if ( currIndex >= segLengths.length ) { break; } const i0 = currIndex; const i1 = currIndex + 1; const d0 = segLengths[ i0 ]; const alpha = d0 > 0 ? ( target - currLength ) / d0 : 0; anchorCandidates.push( { i0, i1, alpha, ref: null, lat: MathUtils.lerp( lat[ i0 ], lat[ i1 ], alpha ), lon: MathUtils.lerp( lon[ i0 ], lon[ i1 ], alpha ), } ); target += spacing; } this.anchorPositions = anchorCandidates; } } // Densify a polyline in tile coordinate space so no gap between consecutive samples // exceeds "spacing", preserving the original vertices. "spacing" is constant in tile // space, the geographic sample density scales with the tile's LoD automatically. function subsamplePath( points, spacing ) { const result = []; for ( let i = 0, l = points.length - 1; i < l; i ++ ) { const p0 = points[ i ]; const p1 = points[ i + 1 ]; result.push( p0 ); const dx = p1.x - p0.x; const dy = p1.y - p0.y; const dist = Math.sqrt( dx * dx + dy * dy ); const steps = Math.ceil( dist / spacing ); for ( let s = 1; s < steps; s ++ ) { const t = s / steps; result.push( { x: MathUtils.lerp( p0.x, p1.x, t ), y: MathUtils.lerp( p0.y, p1.y, t ), } ); } } result.push( points[ points.length - 1 ] ); return result; } // parse the vector tile geometry into line annotations export function parseLineAnnotations( vectorTile, x, y, level, tiling, filter, target = [] ) { // TODO: this needs to scale based on LoD rather than a fixed - this is hackily-scaled below // anchor spacing in radians. Density tracks real-world length, independent of the tile's // zoom / size const anchorSpacing = 500000 / 6378137; const subsampleFraction = 1 / 64; const tileBounds = tiling.getTileBounds( x, y, level, true, false ); const [ tMinX, tMinY, tMaxX, tMaxY ] = tileBounds; const { flipY } = tiling; const range = tiling.getTileBounds( x, y, level, false, false ); for ( const layerName in vectorTile.layers ) { const layer = vectorTile.layers[ layerName ]; const extent = layer.extent; const spacing = extent * subsampleFraction; // collect every line fragment in the layer in tile coordinate space const segments = []; for ( let i = 0; i < layer.length; i ++ ) { const feature = layer.feature( i ); // skip non-line features if ( feature.type !== 2 ) { continue; } // skip lines that don't match the filter if ( ! filter( layerName, feature.properties, feature.type ) ) { continue; } // feature.id is the OSM element id preserved across LoDs — the paths's stable key const id = `${ layerName }:${ feature.properties.name || feature.id }`; const geometry = feature.loadGeometry(); for ( const line of geometry ) { segments.push( { key: id, id, properties: feature.properties, points: line, } ); } } for ( const seg of segments ) { const subSampledPoints = subsamplePath( seg.points, spacing ); // init the annotation const annotation = new LineAnnotation(); annotation.id = seg.id; annotation.layer = layerName; annotation.properties = seg.properties; annotation.lodLevel = level; annotation.range = range; // construct the lat / lon points for ( const point of subSampledPoints ) { // tile Y=0 is geographic north; with flipY the V axis increases northward const u = MathUtils.lerp( tMinX, tMaxX, point.x / extent ); const vf = point.y / extent; // TODO: is this not already accounted for in the toCartographicPoint? Is this supposed to // just be ALWAYS true? This seems to be a flip of the internal content rather than the // overall tiling? const v = flipY ? MathUtils.lerp( tMaxY, tMinY, vf ) : MathUtils.lerp( tMinY, tMaxY, vf ); const [ lon, lat ] = tiling.toCartographicPoint( u, v ); annotation.lon.push( lon ); annotation.lat.push( lat ); annotation.positions.push( new Vector3() ); } // construct the anchors annotation.generateAnchors( anchorSpacing * ( range[ 2 ] - range[ 0 ] ) ); // append the annotation target.push( annotation ); } } return target; }