@giro3d/giro3d
Version:
A JS/WebGL framework for 3D geospatial data visualization
1,049 lines (882 loc) • 33.8 kB
text/typescript
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,
DoubleSide,
EventDispatcher,
MeshBasicMaterial,
MeshLambertMaterial,
SpriteMaterial,
SRGBColorSpace,
Vector3,
type Material,
type Object3D,
type Texture,
} from 'three';
import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js';
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js';
import {
getFullFillStyle,
getFullPointStyle,
getFullStrokeStyle,
hashStyle,
type BaseStyle,
type FillStyle,
type LineMaterialGenerator,
type PointMaterialGenerator,
type PointStyle,
type StrokeStyle,
type SurfaceMaterialGenerator,
} from '../../core/FeatureTypes';
import RequestQueue from '../../core/RequestQueue';
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 type SimpleGeometryMesh from './SimpleGeometryMesh';
import type { DefaultUserData } from './SimpleGeometryMesh';
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 type BaseOptions = {
/**
* The point of origin for relative coordinates.
*/
origin?: Vector3;
/**
* Ignores the Z component of coordinates.
*/
ignoreZ?: boolean;
};
export type PointOptions = BaseOptions & Partial<PointStyle>;
export type PolygonOptions = BaseOptions & {
fill?: FillStyle;
stroke?: StrokeStyle;
extrusionOffset?: number[] | number;
elevation?: number[] | number;
};
export type LineOptions = 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
*
* @param coordinates - The coordinate of the closed shape that form the roof.
* @param stride - The stride in the coordinate array (2 for XY, 3 for XYZ)
* @param offset - The offset to apply to vertex positions.
* the first/last point
* @param elevation - The elevation.
*/
function createFloorVertices(params: {
coordinates: Array<Array<Array<number>>>;
stride: number;
offset: Vector3;
elevation?: Array<number> | number;
ignoreZ: boolean;
}) {
// iterate on polygon and holes
const holesIndices: number[] = [];
let currentIndex = 0;
const positions: number[] = [];
const { coordinates, offset, ignoreZ, elevation, stride } = params;
for (const ring of coordinates) {
// 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);
}
for (let i = 0; i < ring.length - 1; i++) {
currentIndex++;
const coord = ring[i];
positions.push(coord[X] - offset.x);
positions.push(coord[Y] - offset.y);
let z = 0;
if (!ignoreZ) {
if (stride === 3) {
z = coord[Z];
} else if (elevation != null) {
z = Array.isArray(elevation) ? elevation[i] : elevation;
}
}
z -= offset.z;
positions.push(z);
}
}
return { flatCoordinates: 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: Array<number>,
pointCount: number,
indices: Array<number>,
extrusionOffset: Array<number> | number,
) {
for (let i = 0; i < pointCount; i++) {
positions.push(positions[i * VERT_STRIDE + X]);
positions.push(positions[i * VERT_STRIDE + Y]);
const zOffset = Array.isArray(extrusionOffset) ? extrusionOffset[i] : extrusionOffset;
positions.push(positions[i * VERT_STRIDE + Z] + zOffset);
}
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(
positions: Array<number>,
start: number,
end: number,
indices: Array<number>,
extrusionOffset: Array<number> | number,
) {
// 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 = positions.length / 3;
for (let i = start; i < end; i++) {
const idxA = i * VERT_STRIDE;
const iB = i + 1 === end ? start : i + 1;
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[i] : 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
positions.push(Ax, Ay, Az); // A
positions.push(Bx, By, Bz); // B
positions.push(Ax, Ay, Az + zOffsetA); // C
positions.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) {
const stride = polygon.getStride();
// 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 coordinates = polygon.getCoordinates();
const { flatCoordinates, holes } = createFloorVertices({
coordinates,
stride,
ignoreZ: options.ignoreZ ?? false,
offset: options.origin ?? ZERO,
elevation: options.elevation,
});
const pointCount = flatCoordinates.length / 3;
const triangles = triangulate(flatCoordinates, holes);
if (options.extrusionOffset != null) {
createRoof(flatCoordinates, pointCount, triangles, options.extrusionOffset);
createWallForRings(
flatCoordinates,
0,
holes[0] || pointCount,
triangles,
options.extrusionOffset,
);
for (let i = 0; i < holes.length; i++) {
createWallForRings(
flatCoordinates,
holes[i],
holes[i + 1] || pointCount,
triangles,
options.extrusionOffset,
);
}
}
const positions = new Float32Array(flatCoordinates);
const indices =
positions.length <= 65536 ? new Uint16Array(triangles) : new Uint32Array(triangles);
return { positions, 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;
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.
*/
get disposed() {
return this._disposed;
}
get materialCount() {
return this._materialCache.size;
}
// Convenience overloads
/**
* Converts a {@link Point}.
* @param geometry - The `Point` to convert.
* @param options - The options.
*/
build(geometry: Point, options?: PointOptions): PointMesh<UserData>;
/**
* Converts a {@link MultiPoint}.
* @param geometry - The `MultiPoint` to convert.
* @param options - The options.
*/
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.
*/
build(geometry: Point | MultiPoint, options?: PointOptions): SimpleGeometryMesh<UserData>;
/**
* Converts a {@link Polygon}.
* @param geometry - The `Polygon` to convert.
* @param options - The options.
*/
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.
*/
build(
geometry: MultiPolygon,
options?: PolygonOptions,
): PolygonMesh<UserData> | MultiPolygonMesh<UserData>;
/**
* Converts a {@link Polygon}.
* @param geometry - The `Polygon` to convert.
* @param options - The options.
*/
build(geometry: Polygon | MultiPolygon, options?: PolygonOptions): SimpleGeometryMesh<UserData>;
/**
* Converts a {@link LineString}.
* @param geometry - The `LineString` to convert.
* @param options - The options.
*/
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.
*/
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.
*/
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).
*/
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;
}
updatePolygonMesh(mesh: PolygonMesh, options: PolygonOptions) {
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) {
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,
});
}
}
updateMultiPolygonMesh(mesh: MultiPolygonMesh, options: PolygonOptions) {
mesh.traversePolygons(obj => this.updatePolygonMesh(obj, options));
}
updateMultiLineStringMesh(mesh: MultiLineStringMesh, options: LineOptions) {
mesh.traverseLineStrings(obj => this.updateLineStringMesh(obj, options));
}
updateLineStringMesh(mesh: LineStringMesh, options: LineOptions) {
const style = getFullStrokeStyle(options);
const lineMaterial = this._lineMaterialGenerator(style);
mesh.update({
material: lineMaterial,
opacity: style.opacity,
renderOrder: style.renderOrder,
});
}
updatePointMesh(mesh: PointMesh, style: Partial<PointStyle>) {
const fullStyle = getFullPointStyle(style);
const material = this._pointMaterialGenerator(fullStyle);
mesh.update({
material,
pointSize: fullStyle.pointSize,
opacity: fullStyle.opacity,
renderOrder: fullStyle.renderOrder,
});
}
updateSurfaceMesh(mesh: SurfaceMesh, options: PolygonOptions) {
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: Object3D, options: BaseOptions & BaseStyle) {
if (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) {
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;
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: DoubleSide,
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: DoubleSide,
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.
*/
dispose({
disposeTextures = true,
disposeMaterials = true,
}: {
/** Dispose the textures created by this generator */
disposeTextures?: boolean;
/** Dispose the materials created by this generator */
disposeMaterials?: boolean;
}) {
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();
}
}