UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

1,105 lines (924 loc) 36 kB
/* * Copyright (c) 2015-2018, IGN France. * Copyright (c) 2018-2026, Giro3D team. * SPDX-License-Identifier: MIT */ import type { Coordinate } from 'ol/coordinate'; import { LineString, type Geometry, type LinearRing, type MultiLineString, type MultiPoint, type MultiPolygon, type Point, type Polygon, } from 'ol/geom'; import { BufferAttribute, BufferGeometry, EventDispatcher, MeshBasicMaterial, MeshLambertMaterial, SpriteMaterial, SRGBColorSpace, Vector3, type Material, type Texture, } from 'three'; import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js'; import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js'; import type SimpleGeometryMesh from './SimpleGeometryMesh'; import type { DefaultUserData } from './SimpleGeometryMesh'; import { getFullFillStyle, getFullPointStyle, getFullStrokeStyle, hashStyle, type BaseStyle, type FeatureElevation, type FeatureExtrusionOffset, type FillStyle, type LineMaterialGenerator, type PointMaterialGenerator, type PointStyle, type StrokeStyle, type SurfaceMaterialGenerator, } from '../../core/FeatureTypes'; import RequestQueue from '../../core/RequestQueue'; import { Vector3Array } from '../../core/VectorArray'; import Fetcher from '../../utils/Fetcher'; import { triangulate } from '../../utils/tessellator'; import LineStringMesh from './LineStringMesh'; import MultiLineStringMesh from './MultiLineStringMesh'; import MultiPointMesh from './MultiPointMesh'; import MultiPolygonMesh from './MultiPolygonMesh'; import PointMesh from './PointMesh'; import PolygonMesh from './PolygonMesh'; import SurfaceMesh from './SurfaceMesh'; const VERT_STRIDE = 3; // 3 elements per vertex position (X, Y, Z) const X = 0; const Y = 1; const Z = 2; export interface InputMap { Point: Point; MultiPoint: MultiPoint; LineString: LineString; MultiLineString: MultiLineString; Polygon: Polygon; MultiPolygon: MultiPolygon; } export interface OutputMap<UserData extends DefaultUserData = DefaultUserData> { Point: PointMesh<UserData>; MultiPoint: MultiPointMesh<UserData>; LineString: LineStringMesh<UserData>; MultiLineString: MultiLineStringMesh<UserData>; Polygon: PolygonMesh<UserData>; MultiPolygon: MultiPolygonMesh<UserData>; } export interface BaseOptions { /** * The point of origin for relative coordinates. */ origin?: Vector3; /** * Ignores the Z component of coordinates. */ ignoreZ?: boolean; } export interface PointOptions extends BaseOptions, Partial<PointStyle> {} export interface PolygonOptions extends BaseOptions { fill?: FillStyle; stroke?: StrokeStyle; extrusionOffset?: FeatureExtrusionOffset; elevation?: FeatureElevation; } export interface LineOptions extends BaseOptions, StrokeStyle {} export interface OptionMap { Point: PointOptions; MultiPoint: PointOptions; LineString: LineOptions; MultiLineString: LineOptions; Polygon: PolygonOptions; MultiPolygon: PolygonOptions; } function isTexture(o: unknown): o is Texture { return (o as Texture)?.isTexture ?? false; } const ZERO = new Vector3(0, 0, 0); /** * This methods prepares vertices for three.js with coordinates coming from openlayers. * * It does 2 things: * * - flatten the array while removing the last vertex of each rings * - builds the new hole indices taking into account vertex removals */ function createFloorVertices(params: { polygon: Polygon; /** The offset to apply to vertex positions */ offset: Vector3; elevation?: FeatureElevation; ignoreZ: boolean; }): { vertices: Vector3Array; holes: number[] } { const { offset, ignoreZ, elevation, polygon } = params; const stride = polygon.getStride(); // Note that we are forcing the ordering of coordinates to // ensure that we always have clockwise coordinates. This // is vital to ensure a proper triangulation. const USE_RIGHT_HAND_RULE = true; const rings = polygon.getCoordinates(USE_RIGHT_HAND_RULE); const coordinateCount = polygon.getFlatCoordinates().length / polygon.getStride(); // There is one less vertex since we are removing // the duplicated first/last coordinate for each ring. const vertexCount = coordinateCount - polygon.getLinearRingCount(); // iterate on polygon and holes const holesIndices: number[] = []; let currentIndex = 0; const positions = new Vector3Array(new Float32Array(vertexCount * 3)); positions.length = 0; for (const ring of rings) { // NOTE: rings coming from openlayers are auto-closing, // so we need to remove the last vertex of each ring here if (currentIndex > 0) { holesIndices.push(currentIndex); } const callback: (i: number) => void = i => { const coord = ring[i]; const x = coord[X] - offset.x; const y = coord[Y] - offset.y; let z = 0; if (!ignoreZ && stride === 3) { z = coord[Z]; } else if (elevation != null) { z = Array.isArray(elevation) ? elevation[i] : elevation; } z -= offset.z; positions.push(x, y, z); }; for (let i = 0; i < ring.length - 1; i++) { currentIndex++; callback(i); } } return { vertices: positions, holes: holesIndices }; } /** * Create a roof, basically a copy of the floor with faces shifted by 'pointcount' elem * * NOTE: at the moment, this method must be executed before `createWallForRings`, because we copy * the indices array as it is. * * @param positions - a flat array of coordinates * @param pointCount - the number of points to read from position, starting with the first vertex * @param indices - the indices to duplicate for the roof * @param extrusionOffset - the extrusion offset(s) to apply to the roof element. */ function createRoof( positions: Vector3Array, pointCount: number, indices: Array<number>, extrusionOffset: FeatureExtrusionOffset, ): void { const array = positions.array; for (let i = 0; i < pointCount; i++) { const zOffset = Array.isArray(extrusionOffset) ? extrusionOffset[i] : extrusionOffset; const x = array[i * VERT_STRIDE + X]; const y = array[i * VERT_STRIDE + Y]; const z = array[i * VERT_STRIDE + Z] + zOffset; positions.push(x, y, z); } const iLength = indices.length; for (let i = 0; i < iLength; i++) { indices.push(indices[i] + pointCount); } } /** * This methods creates vertex and faces for the walls * * @param positions - The array containing the positions of the vertices. * @param start - vertex in positions to start with * @param end - vertex in positions to end with * @param indices - The index array. * @param extrusionOffset - The extrusion distance. */ function createWallForRings( vertices: Vector3Array, start: number, end: number, indices: Array<number>, extrusionOffset: FeatureExtrusionOffset, ): void { // Each side is formed by the A, B, C, D vertices, where A is the current coordinate, // and B is the next coordinate (thus the segment AB is one side of the polygon). // C and D are the same points but with a Z offset. // Note that each side has its own vertices, as vertices of sides are not shared with // other sides (i.e duplicated) in order to have faceted normals for each side. let vertexOffset = 0; const pointCount = vertices.length; const positions = vertices.array; const isNegativeExtrusion = typeof extrusionOffset === 'number' && extrusionOffset < 0; // If extrusion is negative (goes downward), then all triangles // must be inverted so that normals keep pointing in the correct direction. const reverse = isNegativeExtrusion; const count = end - start; const bOffset = reverse ? -1 : +1; for (let i = 0; i < count; i++) { // Should we move along the ring in forward or reverse order ? const iA = reverse ? end - i - 1 : start + i + 0; // const iB = ccw ? end - i - 1 : start + i + 1; let iB = iA + bOffset; if (iB < start) { iB = end - 1; } else if (iB > end - 1) { iB = start; } const idxA = iA * VERT_STRIDE; const idxB = iB * VERT_STRIDE; const Ax = positions[idxA + X]; const Ay = positions[idxA + Y]; const Az = positions[idxA + Z]; const Bx = positions[idxB + X]; const By = positions[idxB + Y]; const Bz = positions[idxB + Z]; const zOffsetA = Array.isArray(extrusionOffset) ? extrusionOffset[iA] : extrusionOffset; const zOffsetB = Array.isArray(extrusionOffset) ? extrusionOffset[iB] : extrusionOffset; // +Z top // A B // (Ax, Ay, zMax) ---- (Bx, By, zMax) // | | // | | // (Ax, Ay, zMin) ---- (Bx, By, zMin) // C D // -Z bottom vertices.push(Ax, Ay, Az); // A vertices.push(Bx, By, Bz); // B vertices.push(Ax, Ay, Az + zOffsetA); // C vertices.push(Bx, By, Bz + zOffsetB); // D // The indices of the side are the following // [A, B, C, C, B, D] to form the two triangles. const A = 0; const B = 1; const C = 2; const D = 3; const idx = pointCount + vertexOffset; indices.push(idx + A); indices.push(idx + B); indices.push(idx + C); indices.push(idx + C); indices.push(idx + B); indices.push(idx + D); vertexOffset += 4; } } function createSurfaces( polygon: Polygon, options: PolygonOptions, ): { positions: Float32Array; indices: Uint16Array | Uint32Array } { // First we compute the positions of the top vertices (that make the 'floor'). // note that in some dataset, it's the roof and user needs to extrusionOffset down. const { vertices, holes } = createFloorVertices({ polygon, ignoreZ: options.ignoreZ ?? false, offset: options.origin ?? ZERO, elevation: options.elevation, }); const forceFlat = options.extrusionOffset != null; const triangles = triangulate(vertices.array, holes, forceFlat); const pointCount = vertices.length; if (options.extrusionOffset != null) { createRoof(vertices, vertices.length, triangles, options.extrusionOffset); createWallForRings(vertices, 0, holes[0] || pointCount, triangles, options.extrusionOffset); for (let i = 0; i < holes.length; i++) { createWallForRings( vertices, holes[i], holes[i + 1] || pointCount, triangles, options.extrusionOffset, ); } } const indices = vertices.length <= 65536 ? new Uint16Array(triangles) : new Uint32Array(triangles); return { positions: vertices.toFloat32Array(), indices }; } const tempOrigin = new Vector3(); function createPositionBuffer(coordinates: Coordinate[], options: BaseOptions): Float32Array { const bufferSize = 3 * coordinates.length; const result = new Float32Array(bufferSize); const origin = tempOrigin; const ignoreZ = options.ignoreZ; if (options.origin) { origin.copy(options.origin); } else { origin.set(0, 0, 0); } for (let i = 0; i < coordinates.length; i++) { const p = coordinates[i]; const i0 = i * 3; const x = p[0]; const y = p[1]; const z = ignoreZ === true ? 0 : (p[2] ?? 0); result[i0 + 0] = x - origin.x; result[i0 + 1] = y - origin.y; result[i0 + 2] = z - origin.z; } return result; } interface GeometryGeneratorEventMap { 'texture-loaded': { texture: Texture }; } /** * Generates three.js meshes from OpenLayers geometries. * * Supported geometries: * - Point / MultiPoint * - LineString / MultiLineString * - Polygon / MultiPolygon, 2D or 3D (extruded). * * Important note: features with the same styles will share the same material instance, to * avoid duplication and improve performance. This means that modifying the material will * affect all geometries that use it. */ export default class GeometryConverter< UserData extends DefaultUserData = DefaultUserData, > extends EventDispatcher<GeometryGeneratorEventMap> { private readonly _materialCache: Map<unknown, Material> = new Map(); private readonly _downloadQueue = new RequestQueue(); private readonly _downloadedTextures: Map<string, Texture> = new Map(); private readonly _shadedSurfaceMaterialGenerator: SurfaceMaterialGenerator; private readonly _unshadedSurfaceMaterialGenerator: SurfaceMaterialGenerator; private readonly _lineMaterialGenerator: LineMaterialGenerator; private readonly _pointMaterialGenerator: PointMaterialGenerator; private _disposed = false; public constructor(options?: { shadedSurfaceMaterialGenerator?: SurfaceMaterialGenerator; unshadedSurfaceMaterialGenerator?: SurfaceMaterialGenerator; lineMaterialGenerator?: LineMaterialGenerator; pointMaterialGenerator?: PointMaterialGenerator; }) { super(); this._shadedSurfaceMaterialGenerator = options?.shadedSurfaceMaterialGenerator ?? this.getShadedSurfaceMaterial.bind(this); this._unshadedSurfaceMaterialGenerator = options?.unshadedSurfaceMaterialGenerator ?? this.getUnshadedSurfaceMaterial.bind(this); this._lineMaterialGenerator = options?.lineMaterialGenerator ?? this.getLineMaterial.bind(this); this._pointMaterialGenerator = options?.pointMaterialGenerator ?? this.getSpriteMaterial.bind(this); } /** * Gets whether this generator is disposed. A disposed generator can no longer be used. */ public get disposed(): boolean { return this._disposed; } public get materialCount(): number { return this._materialCache.size; } // Convenience overloads /** * Converts a {@link Point}. * @param geometry - The `Point` to convert. * @param options - The options. */ public build(geometry: Point, options?: PointOptions): PointMesh<UserData>; /** * Converts a {@link MultiPoint}. * @param geometry - The `MultiPoint` to convert. * @param options - The options. */ public build(geometry: MultiPoint, options?: PointOptions): MultiPointMesh<UserData>; /** * Converts a {@link MultiPoint} or {@link Point}. * @param geometry - The `MultiPoint` or `Point` to convert. * @param options - The options. */ public build( geometry: Point | MultiPoint, options?: PointOptions, ): SimpleGeometryMesh<UserData>; /** * Converts a {@link Polygon}. * @param geometry - The `Polygon` to convert. * @param options - The options. */ public build(geometry: Polygon, options?: PolygonOptions): PolygonMesh<UserData>; /** * Converts a {@link MultiPolygon}. * * Note: if the `MultiPolygon` has only one polygon, then a {@link PolygonMesh} is returned instead of a {@link MultiPolygonMesh}. * @param geometry - The `MultiPolygon` to convert. * @param options - The options. */ public build( geometry: MultiPolygon, options?: PolygonOptions, ): PolygonMesh<UserData> | MultiPolygonMesh<UserData>; /** * Converts a {@link Polygon}. * @param geometry - The `Polygon` to convert. * @param options - The options. */ public build( geometry: Polygon | MultiPolygon, options?: PolygonOptions, ): SimpleGeometryMesh<UserData>; /** * Converts a {@link LineString}. * @param geometry - The `LineString` to convert. * @param options - The options. */ public build(geometry: LineString, options?: LineOptions): LineStringMesh<UserData>; /** * Converts a {@link MultiLineString}. * * Note: if the `MultiLineString` has only one polygon, then a {@link LineStringMesh} is returned instead of a {@link MultiLineStringMesh}. * @param geometry - The `MultiLineString` to convert. * @param options - The options. */ public build( geometry: MultiLineString, options?: LineOptions, ): MultiLineStringMesh<UserData> | LineStringMesh<UserData>; /** * Converts a {@link MultiLineString} or {@link LineString}. * * Note: if the `MultiLineString` has only one polygon, then a {@link LineStringMesh} is returned instead of a {@link MultiLineStringMesh}. * @param geometry - The `MultiLineString` or `LineString` to convert. * @param options - The options. */ public build( geometry: LineString | MultiLineString, options?: LineOptions, ): SimpleGeometryMesh<UserData>; /** * Create 3D objects from the input geometry and options. * @param geometry - The geometry to transform. * @param options - The options. * @returns The generated 3D object(s). */ public build<K extends keyof OutputMap>( geometry: InputMap[K], options?: OptionMap[K], ): OutputMap<UserData>[K] { type ReturnType = OutputMap<UserData>[K]; options = options ?? {}; this.setDefaultOrigin(geometry, options); let result: ReturnType; switch (geometry.getType()) { case 'LineString': result = this.buildLineString( geometry as LineString, options as LineOptions, ) as ReturnType; break; case 'MultiLineString': result = this.buildMultiLineString( geometry as MultiLineString, options as LineOptions, ) as ReturnType; break; case 'Point': result = this.buildPoint(geometry as Point, options as PointOptions) as ReturnType; break; case 'MultiPoint': result = this.buildMultiPoint( geometry as MultiPoint, options as PointOptions, ) as ReturnType; break; case 'Polygon': result = this.buildPolygon( geometry as Polygon, options as PolygonOptions, ) as ReturnType; break; case 'MultiPolygon': result = this.buildMultiPolygon( geometry as MultiPolygon, options as PolygonOptions, ) as ReturnType; break; default: throw new Error('unimplemented'); } this.finalize(result, options); return result; } public updatePolygonMesh(mesh: PolygonMesh, options: PolygonOptions): void { if (options.stroke && mesh.linearRings == null) { // If the style is added, we have to create the rings const rings = this.getPolygonRings(mesh.source, options); mesh.linearRings = rings; } else if (!options.stroke && mesh.linearRings != null) { // If the style is removed, we have to remove the rings mesh.linearRings = null; } else if (mesh.linearRings) { // Else, just update the existing rings with the new style const stroke = getFullStrokeStyle(options.stroke); const lineMaterial = this._lineMaterialGenerator(stroke); mesh.linearRings.forEach(ring => ring.update({ material: lineMaterial, opacity: stroke.opacity, renderOrder: stroke.renderOrder, }), ); } if (!options.fill && mesh.surface != null) { // If there is a surface, but no surface style, we must hide the existing surface mesh.surface.visible = false; } else if (options.fill && mesh.surface == null) { // If the surface does not exist, we have to create it const surface = this.getSurfaceMesh(mesh.source, options); mesh.surface = surface; } else if (options.fill && mesh.surface) { // Rebuild mesh if extrusion offset / elevation change if ( mesh.surface.extrusionOffset !== options.extrusionOffset || mesh.surface.elevation !== options.elevation ) { const surface = this.getSurfaceMesh(mesh.source, options); mesh.surface = surface; } else { const fill = getFullFillStyle(options.fill); const surfacematerial = fill.shading === true ? this._shadedSurfaceMaterialGenerator(fill) : this._unshadedSurfaceMaterialGenerator(fill); mesh.surface.update({ material: surfacematerial, opacity: fill.opacity, renderOrder: fill.renderOrder, }); } } } public updateMultiPolygonMesh(mesh: MultiPolygonMesh, options: PolygonOptions): void { mesh.traversePolygons(obj => this.updatePolygonMesh(obj, options)); } public updateMultiLineStringMesh(mesh: MultiLineStringMesh, options: LineOptions): void { mesh.traverseLineStrings(obj => this.updateLineStringMesh(obj, options)); } public updateLineStringMesh(mesh: LineStringMesh, options: LineOptions): void { const style = getFullStrokeStyle(options); const lineMaterial = this._lineMaterialGenerator(style); mesh.update({ material: lineMaterial, opacity: style.opacity, renderOrder: style.renderOrder, }); } public updatePointMesh(mesh: PointMesh, style: Partial<PointStyle>): void { const fullStyle = getFullPointStyle(style); const material = this._pointMaterialGenerator(fullStyle); mesh.update({ material, pointSize: fullStyle.pointSize, opacity: fullStyle.opacity, renderOrder: fullStyle.renderOrder, }); } public updateSurfaceMesh(mesh: SurfaceMesh, options: PolygonOptions): void { if (mesh.parent == null) { throw new Error('mesh has no parent polygon'); } this.updatePolygonMesh(mesh.parent, options); } /** * Perform the last transformation on generated objects. * @param object - The object to finalize. * @param options - Options */ private finalize(object: SimpleGeometryMesh, options: BaseOptions & BaseStyle): void { if (options.origin) { object.geometryOrigin = options.origin; object.position.copy(options.origin); } object.traverse(desc => { desc.updateMatrix(); }); object.updateMatrixWorld(true); } private getSurfaceGeometry(polygon: Polygon, options: PolygonOptions): BufferGeometry { const { positions, indices } = createSurfaces(polygon, options); const surfaceGeometry = new BufferGeometry(); surfaceGeometry.setAttribute('position', new BufferAttribute(positions, 3)); surfaceGeometry.setIndex(new BufferAttribute(indices, 1)); surfaceGeometry.computeBoundingBox(); surfaceGeometry.computeBoundingSphere(); return surfaceGeometry; } /** * If origin has not be set, compute a default origin point by taking the first * coordinate of the geometry. */ private setDefaultOrigin(geometry: Geometry, options: BaseOptions): void { if (options.origin != null) { return; } let first: Coordinate; switch (geometry.getType()) { case 'LineString': case 'LinearRing': case 'Polygon': case 'MultiLineString': case 'MultiPolygon': first = ( geometry as LineString | LinearRing | Polygon | MultiLineString | MultiPolygon ).getFirstCoordinate(); break; default: // TODO What to do with other types (GeometryCollection) ? return; } if (first != null) { const x = first[0] ?? 0; const y = first[1] ?? 0; const z = first[2] ?? 0; options.origin = new Vector3(x, y, z); } } private getSurfaceMesh(polygon: Polygon, options: PolygonOptions): SurfaceMesh { const fill = getFullFillStyle(options.fill); const material = fill.shading === true ? this._shadedSurfaceMaterialGenerator(fill) : this._unshadedSurfaceMaterialGenerator(fill); const geometry = this.getSurfaceGeometry(polygon, options); const surface = new SurfaceMesh({ geometry, material, opacity: fill.opacity }); // Surfaces can either be extruded (3D) or non-extruded (2D). if (fill.shading === true) { geometry.computeVertexNormals(); } surface.renderOrder = fill.renderOrder; // Store extrusionOffset / elevation to be able to regenerate surfaces if values change surface.extrusionOffset = options.extrusionOffset; surface.elevation = options.elevation; return surface; } private getPolygonRings(polygon: Polygon, options: PolygonOptions): LineStringMesh[] { const ringCount = polygon.getLinearRingCount(); const linearRings: LineStringMesh[] = []; for (let i = 0; i < ringCount; i++) { const inputRing = polygon.getLinearRing(i); if (inputRing) { const lineString = new LineString(inputRing.getCoordinates()); const ring = this.buildLineString(lineString, { origin: options.origin, ignoreZ: options.ignoreZ, ...options.stroke, }); linearRings.push(ring); } } return linearRings; } private buildPolygon(polygon: Polygon, options: PolygonOptions): PolygonMesh { let surface: SurfaceMesh | undefined = undefined; let linearRings: LineStringMesh[] | undefined = undefined; if (options.fill) { surface = this.getSurfaceMesh(polygon, options); } // If line style is specified, we draw the linear rings of the polygon if (options.stroke) { linearRings = this.getPolygonRings(polygon, options); } const result = new PolygonMesh({ source: polygon, surface, linearRings, isExtruded: options.extrusionOffset != null, }); return result; } private buildMultiPolygon( multiPolygon: MultiPolygon, options: PolygonOptions, ): MultiPolygonMesh | PolygonMesh { const inputGeometries = multiPolygon.getPolygons(); // Optimization if (inputGeometries.length === 1) { return this.buildPolygon(inputGeometries[0], options); } const polygons: PolygonMesh[] = []; for (const polygon of inputGeometries) { const p = this.buildPolygon(polygon, options); polygons.push(p); } const result = new MultiPolygonMesh(polygons); return result; } private buildPointMesh(point: Point, options: PointOptions): PointMesh { const style = getFullPointStyle(options); const material = this._pointMaterialGenerator(style); const coordinate = point.getCoordinates(); const pointMesh = new PointMesh({ material, opacity: style.opacity, pointSize: style.pointSize, }); pointMesh.renderOrder = style.renderOrder; pointMesh.position.setX(coordinate[0] ?? 0); pointMesh.position.setY(coordinate[1] ?? 0); pointMesh.position.setZ(coordinate[2] ?? 0); return pointMesh; } private buildPoint(point: Point, options: PointOptions): PointMesh { return this.buildPointMesh(point, options); } private buildMultiPoint(multiPoint: MultiPoint, options: PointOptions): MultiPointMesh { return new MultiPointMesh(multiPoint.getPoints().map(p => this.buildPointMesh(p, options))); } private getShadedSurfaceMaterial(style: Required<FillStyle>): MeshLambertMaterial { if (style == null) { throw new Error('missing style'); } const key = hashStyle('shaded-surface', style); if (this._materialCache.has(key)) { return this._materialCache.get(key) as MeshLambertMaterial; } const { color, opacity, depthTest } = style; const material = new MeshLambertMaterial({ color, opacity, transparent: opacity < 1, side: style.side, depthTest, depthWrite: depthTest, }); this._materialCache.set(key, material); return material; } private getUnshadedSurfaceMaterial(style: Required<FillStyle>): MeshBasicMaterial { if (style == null) { throw new Error('missing style'); } const key = hashStyle('unshaded-surface', style); if (this._materialCache.has(key)) { return this._materialCache.get(key) as MeshBasicMaterial; } const { color, opacity, depthTest } = style; const material = new MeshBasicMaterial({ color, opacity, transparent: opacity < 1, side: style.side, depthTest, depthWrite: depthTest, }); this._materialCache.set(key, material); return material; } private getSpriteMaterial(style: Required<PointStyle>): SpriteMaterial { if (style == null) { throw new Error('missing style'); } // TODO support point shapes // TODO support image placement (hotspot) const styleKey = hashStyle('sprite', style); if (this._materialCache.has(styleKey)) { return this._materialCache.get(styleKey) as SpriteMaterial; } const { color, opacity, sizeAttenuation, depthTest } = style; const result = new SpriteMaterial({ color, opacity, transparent: true, sizeAttenuation, depthTest, depthWrite: depthTest, map: style.image != null ? isTexture(style.image) ? style.image : this.getCachedTexture(style.image) : null, }); // Download image from URL if (typeof style.image === 'string' && result.map == null) { // Hide material until the image is loaded to avoid displaying a blank square. result.visible = false; // Download the image this.loadRemoteTexture(style.image) .then(texture => { result.map = texture; result.needsUpdate = true; result.visible = true; result.transparent = true; }) .catch(console.error); } this._materialCache.set(styleKey, result); return result; } private getCachedTexture(url: string): Texture | null { const cached = this._downloadedTextures.get(url); if (cached) { return cached; } return null; } private loadRemoteTexture(url: string): Promise<Texture> { const cached = this._downloadedTextures.get(url); if (cached) { return Promise.resolve(cached); } return this._downloadQueue.enqueue({ id: url, request: () => this.fetchTexture(url), }); } private fetchTexture(url: string): Promise<Texture> { // Download the image return Fetcher.texture(url, { flipY: true }).then(texture => { texture.colorSpace = SRGBColorSpace; this._downloadedTextures.set(url, texture); texture.generateMipmaps = true; this.dispatchEvent({ type: 'texture-loaded', texture }); return texture; }); } private getLineMaterial(style: Required<StrokeStyle>): LineMaterial { if (style == null) { throw new Error('missing style'); } const styleKey = hashStyle('line', style); if (this._materialCache.has(styleKey)) { return this._materialCache.get(styleKey) as LineMaterial; } const { color, lineWidth, opacity, lineWidthUnits, depthTest } = style; const material = new LineMaterial({ color, linewidth: lineWidth, // Notice the different case opacity, transparent: opacity < 1, worldUnits: lineWidthUnits === 'world', depthTest, depthWrite: depthTest, }); this._materialCache.set(styleKey, material); return material; } private getLineGeometry(coordinates: Coordinate[], options: BaseOptions): LineGeometry { const result = new LineGeometry(); result.setPositions(createPositionBuffer(coordinates, options)); result.computeBoundingBox(); return result; } private buildLineString( geometry: LineString, options: OptionMap['LineString'], ): LineStringMesh { const fullStyle = getFullStrokeStyle(options); const lineStringMesh = new LineStringMesh( this.getLineGeometry(geometry.getCoordinates(), options), this._lineMaterialGenerator(fullStyle), fullStyle.opacity, ); lineStringMesh.renderOrder = fullStyle.renderOrder; return lineStringMesh; } private buildMultiLineString( geometry: MultiLineString, options: OptionMap['MultiLineString'], ): MultiLineStringMesh | LineStringMesh { const lineStrings = geometry.getLineStrings(); // Optimization if (lineStrings.length === 1) { return this.buildLineString(lineStrings[0], options); } const meshes: LineStringMesh[] = []; for (const line of lineStrings) { const lineStringMesh = this.buildLineString(line, options); meshes.push(lineStringMesh); } return new MultiLineStringMesh(meshes); } /** * Disposes this generator and all cached materials. Once disposed, this generator cannot be used anymore. */ public dispose({ disposeTextures = true, disposeMaterials = true, }: { /** Dispose the textures created by this generator */ disposeTextures?: boolean; /** Dispose the materials created by this generator */ disposeMaterials?: boolean; }): void { if (this._disposed) { return; } if (disposeTextures) { this._downloadedTextures.forEach(texture => texture.dispose()); } if (disposeMaterials) { this._materialCache.forEach(material => material.dispose()); } this._downloadedTextures.clear(); this._materialCache.clear(); } }