@giro3d/giro3d
Version:
A JS/WebGL framework for 3D geospatial data visualization
1,105 lines (924 loc) • 36 kB
text/typescript
/*
* 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();
}
}