UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

364 lines (291 loc) 10.2 kB
/* * Copyright (c) 2015-2018, IGN France. * Copyright (c) 2018-2026, Giro3D team. * SPDX-License-Identifier: MIT */ import { ArrowHelper, AxesHelper, BufferAttribute, BufferGeometry, Color, LineSegments, MeshBasicMaterial, Object3D, Vector3, type ColorRepresentation, } from 'three'; import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js'; import type Disposable from '../core/Disposable'; import Ellipsoid from '../core/geographic/Ellipsoid'; import { Vector3Array } from '../core/VectorArray'; const tmp = { a: new Vector3(), b: new Vector3(), }; function createParallel( ellipsoid: Ellipsoid, latitude: number, segments: number, positions: Vector3Array, colors: Vector3Array, color: Color, ): void { const step = 360 / segments; let longitude = 0; const r = Math.round(color.r * 255); const g = Math.round(color.g * 255); const b = Math.round(color.b * 255); for (let i = 0; i <= segments; i++) { const v0 = ellipsoid.toCartesian(latitude, longitude, 0, tmp.a); const v1 = ellipsoid.toCartesian(latitude, longitude + step, 0, tmp.b); longitude += step; positions.pushVector(v0); positions.pushVector(v1); colors.push(r, g, b); colors.push(r, g, b); } } function createMeridian( ellipsoid: Ellipsoid, longitude: number, segments: number, target: Vector3Array, colors: Vector3Array, color: Color, ): void { const step = 360 / segments; let latitude = -90; const r = Math.round(color.r * 255); const g = Math.round(color.g * 255); const b = Math.round(color.b * 255); for (let i = 0; i <= segments / 2; i++) { const v0 = ellipsoid.toCartesian(latitude, longitude, 0, tmp.a); const v1 = ellipsoid.toCartesian(latitude + step, longitude, 0, tmp.b); latitude += step; target.pushVector(v0); target.pushVector(v1); colors.push(r, g, b); colors.push(r, g, b); } } function createLabel(text: string, color: ColorRepresentation): CSS2DObject { const div = document.createElement('div'); div.style.textAlign = 'center'; div.style.verticalAlign = 'middle'; div.style.textShadow = 'black 0 0 3px'; div.style.fontWeight = 'bold'; div.style.color = '#' + new Color(color).getHexString(); div.innerText = text; const label = new CSS2DObject(div); label.name = text; return label; } /** * Displays an ellipsoid along with its axes. */ export class EllipsoidHelper extends Object3D implements Disposable { public readonly isEllipsoidHelper = true as const; public override readonly type = 'EllipsoidHelper' as const; public readonly ellipsoid: Ellipsoid; private readonly _mesh: LineSegments<BufferGeometry, MeshBasicMaterial>; private readonly _axes: AxesHelper; private readonly _labels: CSS2DObject[]; private readonly _arrows: ArrowHelper[] = []; private _showNormals = false; private _disposed = false; /** * The color of the lines. */ public get color(): Color { return this._mesh.material.color; } public set color(c: Color) { this._mesh.material.color = c; } public get showLines(): boolean { return this._mesh.visible; } public set showLines(show: boolean) { this._mesh.visible = show; } public get showAxes(): boolean { return this._axes.visible; } public set showAxes(show: boolean) { this._axes.visible = show; } public get showNormals(): boolean { return this._showNormals; } public set showNormals(show: boolean) { if (this._showNormals !== show) { this._showNormals = show; if (show) { this.createNormalArrows(); } else { this.deleteNormalArrows(); } } } public get showLabels(): boolean { return this._labels[0].visible; } public set showLabels(show: boolean) { this._labels.forEach(l => (l.visible = show)); } public constructor(params?: { /** * The ellipsoid to use. * @defaultValue {@link Ellipsoid.WGS84} */ ellipsoid?: Ellipsoid; /** * The number of parallels, including the equator. Must be an odd number. 0 disable parallels. * @defaultValue 5 */ parallels?: number; /** * The number of meridians. * @defaultValue 24 (one per timezone) */ meridians?: number; /** * The number of segments. * @defaultValue 32 */ segments?: number; /** * The color of the lines (except equator and prime meridian). * @defaultValue grey */ lineColor?: ColorRepresentation; /** * The color of the equator line. * @defaultValue #FF4F93 */ equatorColor?: ColorRepresentation; /** * The color of the prime meridian line. * @defaultValue #75B1C7 */ primeMeridianColor?: ColorRepresentation; }) { super(); this.ellipsoid = params?.ellipsoid ?? Ellipsoid.WGS84; const meridianCount = params?.meridians ?? 24; const segments = params?.segments ?? 32; const parallelCount = params?.parallels ?? 5; const mainColor = params?.lineColor != null ? new Color(params?.lineColor) : new Color('grey'); const primeMeridianColor = params?.primeMeridianColor != null ? new Color(params.primeMeridianColor) : new Color('#75b1c7'); if (parallelCount % 2 === 0) { throw new Error(`parallels must be an odd number, got: ${parallelCount}`); } const vectors = new Vector3Array(new Float32Array(3000)); vectors.length = 0; const colors = new Vector3Array(new Uint8ClampedArray(3000)); colors.length = 0; // equator if (parallelCount > 0) { const equatorColor = params?.equatorColor != null ? new Color(params.equatorColor) : new Color('#ff4f93'); createParallel(this.ellipsoid, 0, segments, vectors, colors, equatorColor); } const parallelsPerHemisphere = (parallelCount - 1) / 2; const latitudeStep = 90 / (parallelsPerHemisphere + 1); let latitude = latitudeStep; for (let index = 0; index < parallelsPerHemisphere; index++) { createParallel(this.ellipsoid, +latitude, segments, vectors, colors, mainColor); createParallel(this.ellipsoid, -latitude, segments, vectors, colors, mainColor); latitude += latitudeStep; } let longitude = 0; const longitudeStep = 360 / meridianCount; for (let index = 0; index < meridianCount; index++) { const color = index === 0 ? primeMeridianColor : mainColor; createMeridian(this.ellipsoid, longitude, segments, vectors, colors, color); longitude += longitudeStep; } const positionBuffer = new BufferAttribute(vectors.toFloat32Array(), 3); colors.trim(); const colorBuffer = new BufferAttribute(colors.array, 3, true); const geometry = new BufferGeometry(); geometry.setAttribute('position', positionBuffer); geometry.setAttribute('color', colorBuffer); this._mesh = new LineSegments(geometry, new MeshBasicMaterial({ vertexColors: true })); this._mesh.name = 'lines'; this._axes = new AxesHelper(this.ellipsoid.semiMajorAxis * 1.3); this._axes.name = 'axes'; this._axes.scale.set(1, 1, this.ellipsoid.compressionFactor); const xLabel = createLabel('+X', new Color(1, 0.2, 0)); const yLabel = createLabel('+Y', new Color(0.2, 1, 0)); const zLabel = createLabel('+Z', new Color(0, 0.2, 1)); zLabel.position.copy( this.ellipsoid.toCartesian(+90, 0, this.ellipsoid.semiMinorAxis * 0.4), ); yLabel.position.copy( this.ellipsoid.toCartesian(0, +90, this.ellipsoid.semiMajorAxis * 0.4), ); xLabel.position.copy(this.ellipsoid.toCartesian(0, 0, this.ellipsoid.semiMajorAxis * 0.4)); this.add(xLabel); this.add(yLabel); this.add(zLabel); this._labels = [xLabel, yLabel, zLabel]; this.add(this._mesh); this.add(this._axes); this.updateMatrixWorld(true); } private deleteNormalArrows(): void { if (this._arrows.length > 0) { this._arrows.forEach(arrow => { arrow.dispose(); arrow.removeFromParent(); }); this._arrows.length = 0; } } private createNormalArrows(): void { const normal = new Vector3(); for (let i = 0; i < 10; i++) { for (let j = 0; j < 20; j++) { const lat = i * 18 - 90; const lon = j * 18 - 180; const origin = this.ellipsoid.toCartesian(lat, lon, 0); this.ellipsoid.getNormal(lat, lon, normal); const arrow = new ArrowHelper( normal, origin, this.ellipsoid.semiMajorAxis * 0.3, 'yellow', ); this.add(arrow); arrow.updateMatrixWorld(true); this._arrows.push(arrow); } } } public dispose(): void { if (this._disposed) { return; } this._disposed = true; this._mesh.geometry.dispose(); this._mesh.material.dispose(); this._axes.dispose(); this._labels.forEach(l => l.element.remove); this._labels.length = 0; this._arrows.forEach(a => { a.dispose(); a.removeFromParent(); }); this._arrows.length = 0; this.clear(); } } export default EllipsoidHelper;