UNPKG

terriajs

Version:

Geospatial data visualization platform.

254 lines (227 loc) 7.94 kB
import i18next from "i18next"; import { RefObject, createRef } from "react"; import ArcType from "terriajs-cesium/Source/Core/ArcType"; import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; import Ellipsoid from "terriajs-cesium/Source/Core/Ellipsoid"; import EllipsoidGeodesic from "terriajs-cesium/Source/Core/EllipsoidGeodesic"; import EllipsoidTangentPlane from "terriajs-cesium/Source/Core/EllipsoidTangentPlane"; import CesiumMath from "terriajs-cesium/Source/Core/Math"; import PolygonGeometryLibrary from "terriajs-cesium/Source/Core/PolygonGeometryLibrary"; import PolygonHierarchy from "terriajs-cesium/Source/Core/PolygonHierarchy"; import VertexFormat from "terriajs-cesium/Source/Core/VertexFormat"; import CustomDataSource from "terriajs-cesium/Source/DataSources/CustomDataSource"; import Terria from "../../../../Models/Terria"; import UserDrawing from "../../../../Models/UserDrawing"; import ViewerMode from "../../../../Models/ViewerMode"; import { GLYPHS } from "../../../../Styled/Icon"; import MapNavigationItemController from "../../../../ViewModels/MapNavigation/MapNavigationItemController"; interface MeasureToolOptions { terria: Terria; onClose(): void; } export class MeasureTool extends MapNavigationItemController { static id = "measure-tool"; static displayName = "MeasureTool"; private readonly terria: Terria; private totalDistanceMetres: number = 0; private totalAreaMetresSquared: number = 0; private userDrawing: UserDrawing; onClose: () => void; itemRef: RefObject<HTMLDivElement> = createRef(); constructor(props: MeasureToolOptions) { super(); this.terria = props.terria; this.userDrawing = new UserDrawing({ terria: props.terria, messageHeader: () => i18next.t("measure.measureTool"), allowPolygon: false, onPointClicked: this.onPointClicked.bind(this), onPointMoved: this.onPointMoved.bind(this), onCleanUp: this.onCleanUp.bind(this), onMakeDialogMessage: this.onMakeDialogMessage.bind(this) }); this.onClose = props.onClose; } get glyph(): any { return GLYPHS.measure; } get viewerMode(): ViewerMode | undefined { return undefined; } prettifyNumber(number: number, squared: boolean) { if (number <= 0) { return ""; } // Given a number representing a number in metres, make it human readable let label = "m"; if (squared) { if (number > 999999) { label = "km"; number = number / 1000000.0; } } else { if (number > 999) { label = "km"; number = number / 1000.0; } } let numberStr = number.toFixed(2); // http://stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript numberStr = numberStr.replace(/\B(?=(\d{3})+(?!\d))/g, ","); numberStr = `${numberStr} ${label}`; if (squared) { numberStr += "\u00B2"; } return numberStr; } updateDistance(pointEntities: CustomDataSource) { this.totalDistanceMetres = 0; if (pointEntities.entities.values.length < 1) { return; } let firstPointPos: Cartesian3 | undefined; let prevPointPos: Cartesian3 | undefined; for (let i = 0; i < pointEntities.entities.values.length; i++) { const currentPoint = pointEntities.entities.values[i]; const currentPointPos = currentPoint.position!.getValue( this.terria.timelineClock.currentTime ); if (currentPointPos === undefined) continue; if (prevPointPos === undefined) { prevPointPos = currentPointPos; firstPointPos = prevPointPos; continue; } this.totalDistanceMetres = this.totalDistanceMetres + this.getGeodesicDistance(prevPointPos, currentPointPos); prevPointPos = currentPointPos; } if (prevPointPos && firstPointPos && this.userDrawing.closeLoop) { this.totalDistanceMetres = this.totalDistanceMetres + this.getGeodesicDistance(prevPointPos, firstPointPos); } } updateArea(pointEntities: CustomDataSource) { this.totalAreaMetresSquared = 0; if (!this.userDrawing.closeLoop) { // Not a closed polygon? Don't calculate area. return; } if (pointEntities.entities.values.length < 3) { return; } const perPositionHeight = true; const positions = []; for (let i = 0; i < pointEntities.entities.values.length; i++) { const currentPoint = pointEntities.entities.values[i]; const currentPointPos = currentPoint.position!.getValue( this.terria.timelineClock.currentTime ); if (currentPointPos !== undefined) { positions.push(currentPointPos); } } // Request the triangles that make up the polygon from Cesium. const tangentPlane = EllipsoidTangentPlane.fromPoints( positions, Ellipsoid.WGS84 ); const polygons = PolygonGeometryLibrary.polygonsFromHierarchy( new PolygonHierarchy(positions), false, tangentPlane.projectPointsOntoPlane.bind(tangentPlane), !perPositionHeight, Ellipsoid.WGS84 ); const geom = PolygonGeometryLibrary.createGeometryFromPositions( Ellipsoid.WGS84, polygons.polygons[0], undefined, CesiumMath.RADIANS_PER_DEGREE, perPositionHeight, VertexFormat.POSITION_ONLY, ArcType.GEODESIC ); if ( geom.indices.length % 3 !== 0 || geom.attributes.position.values.length % 3 !== 0 ) { // Something has gone wrong. We expect triangles. Can't calcuate area. return; } const coords = []; for (let i = 0; i < geom.attributes.position.values.length; i += 3) { coords.push( new Cartesian3( geom.attributes.position.values[i], geom.attributes.position.values[i + 1], geom.attributes.position.values[i + 2] ) ); } let area = 0; for (let i = 0; i < geom.indices.length; i += 3) { const ind1 = geom.indices[i]; const ind2 = geom.indices[i + 1]; const ind3 = geom.indices[i + 2]; const a = Cartesian3.distance(coords[ind1], coords[ind2]); const b = Cartesian3.distance(coords[ind2], coords[ind3]); const c = Cartesian3.distance(coords[ind3], coords[ind1]); // Heron's formula const s = (a + b + c) / 2.0; area += Math.sqrt(s * (s - a) * (s - b) * (s - c)); } this.totalAreaMetresSquared = area; } getGeodesicDistance(pointOne: Cartesian3, pointTwo: Cartesian3) { // Note that Cartesian.distance gives the straight line distance between the two points, ignoring // curvature. This is not what we want. const pickedPointCartographic = Ellipsoid.WGS84.cartesianToCartographic(pointOne); const lastPointCartographic = Ellipsoid.WGS84.cartesianToCartographic(pointTwo); const geodesic = new EllipsoidGeodesic( pickedPointCartographic, lastPointCartographic ); return geodesic.surfaceDistance; } onCleanUp() { this.totalDistanceMetres = 0; this.totalAreaMetresSquared = 0; super.deactivate(); } onPointClicked(pointEntities: CustomDataSource) { this.updateDistance(pointEntities); this.updateArea(pointEntities); } onPointMoved(pointEntities: CustomDataSource) { // This is no different to clicking a point. this.onPointClicked(pointEntities); } onMakeDialogMessage = () => { const distance = this.prettifyNumber(this.totalDistanceMetres, false); let message = distance; if (this.totalAreaMetresSquared !== 0) { message += "<br>" + this.prettifyNumber(this.totalAreaMetresSquared, true); } return message; }; /** * @overrides */ deactivate() { this.userDrawing.endDrawing(); super.deactivate(); } /** * @overrides */ activate() { this.userDrawing.enterDrawMode(); super.activate(); } }