| import {Deferred} from '../../Deferred'; |
| import {createEvents, type EventsHandler} from '../../Events'; |
| import {Vec3} from "../../math/Vec3"; |
| import {Planet} from "../../scene/Planet"; |
| import {LonLat} from "../../LonLat"; |
| |
| const DEFAULT_WARNING_HEIGHT_LEVEL = 5; |
| |
| export interface ElevationProfileParams { |
| planet?: Planet |
| } |
| |
| export interface IProfileData { |
| dist: number; |
| minY: number; |
| maxY: number; |
| trackCoords: TrackItem[]; |
| groundCoords: GroundItem[]; |
| } |
| |
| export type ElevationProfileDrawData = [TrackItem[], GroundItem[]]; |
| |
| type ElevationProfileEventsList = ["startcollecting", "profilecollected", "clear"]; |
| |
| const ELEVATIONPROFILE_EVENTS: ElevationProfileEventsList = ["startcollecting", "profilecollected", "clear"]; |
| |
| |
| |
| |
| export const SAFE = 0; |
| export const WARNING = 1; |
| export const COLLISION = 2; |
| |
| |
| |
| |
| const TRACK = 0; |
| const GROUND = 1; |
| |
| const SEGMMENT_LENGTH = 1.0; |
| const GROUND_OFFSET = 1.0; |
| const BOTTOM_PADDING = 0.1; |
| const TOP_PADDING = 0.2; |
| const HEIGHT_EPS = 0.1; |
| |
| type WarningLevel = typeof SAFE | typeof WARNING | typeof COLLISION; |
| |
| |
| |
| export type TrackItem = [number, number, number]; |
| |
| |
| |
| export type GroundItem = [number, number, WarningLevel, number, number]; |
| |
| export class ElevationProfile { |
| public events: EventsHandler<ElevationProfileEventsList>; |
| public planet: Planet | null; |
| protected _warningHeightLevel: number; |
| protected _pointsReady: boolean; |
| protected _isWarning: boolean; |
| |
| protected _planeDistance: number; |
| protected _minX: number; |
| protected _maxX: number; |
| protected _minY: number; |
| protected _maxY: number; |
| protected _drawData: ElevationProfileDrawData; |
| protected _promiseArr: Promise<void | number>[]; |
| protected _promiseCounter: number; |
| protected _pMaxY: number; |
| protected _pMinY: number; |
| protected _pDist: number; |
| protected _pTrackCoords: TrackItem[]; |
| protected _pGroundCoords: GroundItem[]; |
| protected _pIndex: number; |
| |
| constructor(options: ElevationProfileParams = {}) { |
| this.events = createEvents(ELEVATIONPROFILE_EVENTS); |
| |
| this.planet = options.planet || null; |
| |
| this._warningHeightLevel = DEFAULT_WARNING_HEIGHT_LEVEL; |
| this._pointsReady = false; |
| this._isWarning = false; |
| |
| this._minX = 0; |
| this._planeDistance = this._maxX = 1000; |
| this._minY = 0; |
| this._maxY = 200; |
| |
| this._drawData = [[], []]; |
| |
| this._promiseArr = []; |
| this._promiseCounter = 0; |
| this._pMaxY = 0; |
| this._pMinY = 0; |
| this._pDist = 0; |
| this._pTrackCoords = []; |
| this._pGroundCoords = []; |
| |
| this._pIndex = 0; |
| } |
| |
| public bindPlanet(planet: Planet) { |
| this.planet = planet; |
| } |
| |
| public setWarningHeightLevel(warningHeight: number = 0) { |
| this._warningHeightLevel = warningHeight; |
| } |
| |
| public setRange(minX: number, maxX: number, minY?: number, maxY?: number) { |
| this._minX = minX; |
| this._maxX = maxX; |
| if (minY) { |
| this._minY = minY; |
| } |
| if (maxY) { |
| this._maxY = maxY; |
| } |
| } |
| |
| protected _getHeightAsync(ll: LonLat, pIndex: number, promiseCounter: number): Promise<number> { |
| let def = new Deferred<number>(); |
| if (this.planet) { |
| let msl = this.planet.terrain!.geoid.getHeightLonLat(ll); |
| this.planet.terrain!.getHeightAsync(ll, (elv: number) => { |
| if (this.planet && promiseCounter === this._promiseCounter) { |
| elv += msl; |
| this._pGroundCoords[pIndex][1] = elv; |
| this._pGroundCoords[pIndex][2] = SAFE; |
| this._pGroundCoords[pIndex][3] = ll.height; |
| if (elv > this._pMaxY) this._pMaxY = elv; |
| if (elv < this._pMinY) this._pMinY = elv; |
| this._updatePointType(pIndex); |
| def.resolve(elv); |
| } else { |
| def.reject(); |
| } |
| }); |
| } else { |
| def.reject(); |
| } |
| return def.promise; |
| } |
| |
| protected _collectCoordsBetweenTwoTrackPoints(index: number, internalPoints: number, scaleFactor: number, p0: Vec3, trackDir: Vec3, promiseCounter: number) { |
| |
| if (!this.planet) return; |
| |
| for (let j = 1; j <= internalPoints; j++) { |
| this._pDist += SEGMMENT_LENGTH; |
| this._pIndex++; |
| |
| |
| let dirSegLen = j * scaleFactor; |
| let pjd = p0.add(trackDir.scaleTo(dirSegLen)); |
| let llx = this.planet.ellipsoid.cartesianToLonLat(pjd); |
| |
| this._pGroundCoords[this._pIndex] = [this._pDist, 0, SAFE, 0, index]; |
| |
| ((lonlat: LonLat, index: number) => { |
| this._promiseArr.push(this._getHeightAsync(lonlat, index, promiseCounter)); |
| })(llx, this._pIndex); |
| } |
| } |
| |
| protected _collectAllPoints(pointsLonLat: LonLat[], promiseCounter: number) { |
| |
| if (!this.planet) return; |
| if (promiseCounter !== this._promiseCounter) return; |
| |
| let p0 = new Vec3(), |
| p1 = new Vec3(); |
| |
| for (let i = 1, len = pointsLonLat.length; i < len; i++) { |
| let lonlat0 = pointsLonLat[i - 1], |
| lonlat1 = pointsLonLat[i]; |
| |
| this.planet.ellipsoid.lonLatToCartesianRes(lonlat0, p0); |
| this.planet.ellipsoid.lonLatToCartesianRes(lonlat1, p1); |
| |
| let trackDir = p1.sub(p0); |
| let dirLength = trackDir.length(); |
| let n0 = this.planet.ellipsoid.getSurfaceNormal3v(p0); |
| let proj = Vec3.proj_b_to_plane(trackDir, n0); |
| let projLen = proj.length(); |
| let internalPoints = Math.floor(projLen / SEGMMENT_LENGTH); |
| let scaleFactor = SEGMMENT_LENGTH * dirLength / projLen; |
| |
| this._getGroundElevation(lonlat0, i - 1, promiseCounter); |
| |
| proj.normalize(); |
| trackDir.normalize(); |
| |
| |
| this._collectCoordsBetweenTwoTrackPoints(i - 1, internalPoints, scaleFactor, p0, trackDir, promiseCounter); |
| |
| this._pDist += projLen - internalPoints * SEGMMENT_LENGTH; |
| this._pIndex++; |
| |
| let elv = lonlat1.height; |
| if (elv > this._pMaxY) this._pMaxY = elv; |
| if (elv < this._pMinY) this._pMinY = elv; |
| |
| this._pTrackCoords[i] = [this._pDist, elv, this._pIndex]; |
| } |
| } |
| |
| protected _getGroundElevation(lonLat: LonLat, index: number, promiseCounter: number) { |
| this._pGroundCoords[this._pIndex] = [this._pDist, 0, SAFE, 0, index]; |
| this._promiseArr.push(this._getHeightAsync(lonLat, this._pIndex, promiseCounter)); |
| } |
| |
| protected _calcPointsAsync(pointsLonLat: LonLat[], promiseCounter: number) { |
| return new Promise<IProfileData>((resolve: (p: IProfileData) => void, reject) => { |
| this._pTrackCoords = [[0, pointsLonLat[0].height, 0]]; |
| this._pMaxY = pointsLonLat[0].height; |
| this._pMinY = this._pMaxY; |
| this._pDist = 0; |
| this._pGroundCoords = []; |
| this._pIndex = 0; |
| this._promiseArr = []; |
| |
| this._collectAllPoints(pointsLonLat, promiseCounter); |
| |
| this._getGroundElevation(pointsLonLat[pointsLonLat.length - 1], pointsLonLat.length - 1, promiseCounter); |
| |
| Promise.all(this._promiseArr).then(() => { |
| resolve({ |
| dist: this._pDist, |
| minY: this._pMinY, |
| maxY: this._pMaxY, |
| trackCoords: this._pTrackCoords, |
| groundCoords: this._pGroundCoords |
| }); |
| }); |
| }); |
| } |
| |
| public get minX(): number { |
| return this._minX; |
| } |
| |
| public get planeDistance(): number { |
| return this._planeDistance; |
| } |
| |
| public get maxX(): number { |
| return this._maxX; |
| } |
| |
| public get minY(): number { |
| return this._minY; |
| } |
| |
| public get maxY(): number { |
| return this._maxY; |
| } |
| |
| public get pointsReady(): boolean { |
| return this._pointsReady; |
| } |
| |
| public get isWarningOrCollision(): boolean { |
| return this._isWarning; |
| } |
| |
| public get drawData(): ElevationProfileDrawData { |
| return this._drawData; |
| } |
| |
| public collectProfile(pointsLonLat: LonLat[]): Promise<ElevationProfileDrawData> { |
| |
| let def = new Deferred<ElevationProfileDrawData>(); |
| |
| if (!this.planet) def.reject(); |
| |
| this._pointsReady = false; |
| this._isWarning = false; |
| |
| if (!pointsLonLat || !pointsLonLat.length) { |
| def.reject(); |
| return def.promise; |
| } |
| |
| this.events.dispatch(this.events.startcollecting, this); |
| |
| this._promiseCounter++; |
| |
| ((counter: number) => { |
| this._calcPointsAsync(pointsLonLat, counter).then((p: IProfileData) => { |
| if (counter === this._promiseCounter) { |
| this._planeDistance = p.dist; |
| this.setRange(0, p.dist, p.minY - BOTTOM_PADDING * Math.abs(p.minY), p.maxY + Math.abs(p.maxY) * TOP_PADDING); |
| this._pointsReady = true; |
| this._drawData = [p.trackCoords, p.groundCoords]; |
| this.events.dispatch(this.events.profilecollected, this._drawData, this); |
| def.resolve(this._drawData); |
| } |
| }); |
| })(this._promiseCounter); |
| |
| return def.promise; |
| } |
| |
| protected _updatePointType(pIndex: number) { |
| if ((this._pGroundCoords[pIndex][3] >= this._pGroundCoords[pIndex][1]) && |
| (this._pGroundCoords[pIndex][3] < this._pGroundCoords[pIndex][1] + this._warningHeightLevel - HEIGHT_EPS)) { |
| this._pGroundCoords[pIndex][2] = WARNING; |
| } |
| |
| if (this._pGroundCoords[pIndex][3] <= this._pGroundCoords[pIndex][1] + GROUND_OFFSET) { |
| this._pGroundCoords[pIndex][2] = COLLISION; |
| } |
| |
| if (this._pGroundCoords[pIndex][2] === WARNING || this._pGroundCoords[pIndex][2] === COLLISION) { |
| this._isWarning = true; |
| } |
| } |
| |
| |
| |
| |
| protected _setPointsType() { |
| this._isWarning = false; |
| |
| this._pTrackCoords = this._drawData[TRACK]; |
| this._pGroundCoords = this._drawData[GROUND]; |
| |
| for (let i = 0; i < this._pGroundCoords.length; i++) { |
| this._updatePointType(i); |
| } |
| |
| this._drawData[GROUND] = this._pGroundCoords; |
| this.events.dispatch(this.events.profilecollected, this._drawData, this); |
| } |
| |
| public clear() { |
| this._promiseCounter = 0; |
| this._pointsReady = false; |
| this._isWarning = false; |
| this._drawData = [[], []]; |
| this._pMaxY = 0; |
| this._pMinY = 0; |
| this._pDist = 0; |
| this._pTrackCoords = []; |
| this._pGroundCoords = []; |
| this._pIndex = 0; |
| this.events.dispatch(this.events.clear, this._drawData, this); |
| } |
| } |