UNPKG

@arturataide/ngx-leaflet-movingmarker

Version:

Moving Marker for ngx-leaflet based on Leaflet.MovingMarker

307 lines (261 loc) 7.84 kB
import { Marker, MarkerOptions, Map, LatLng, latLng, Util } from 'leaflet'; enum MovingMarkerState { NOT_STARTED = 0, ENDED = 1, PAUSED = 2, RUN = 3, } export interface MovingMarkerOptions extends MarkerOptions { autostart: boolean; loop: boolean; } export class MovingMarker extends Marker { private latLngs: LatLng[]; private durations: number[]; private currentDuration = 0; private currentIndex = 0; private state: MovingMarkerState = MovingMarkerState.NOT_STARTED; private startTime = 0; private startTimeStamp = 0; // timestamp given by requestAnimFrame private pauseStartTime = 0; private animId = 0; private animRequested = false; private currentLine = []; private stations = {}; public options: MovingMarkerOptions; constructor(latLngs: LatLng[], durations: number[], options: MarkerOptions) { super(latLngs[0], options); this.latLngs = latLngs.map(e => { return latLng(e); }); if (durations instanceof Array) { this.durations = durations; } else { this.durations = this.createDurations(this.latLngs, durations); } } private interpolatePosition(p1, p2, duration, t) { let k = t / duration; k = k > 0 ? k : 0; k = k > 1 ? 1 : k; return latLng(p1.lat + k * (p2.lat - p1.lat), p1.lng + k * (p2.lng - p1.lng)); } public isRunning(): boolean { return this.state === MovingMarkerState.RUN; } public isEnded(): boolean { return this.state === MovingMarkerState.ENDED; } public isStarted(): boolean { return this.state !== MovingMarkerState.NOT_STARTED; } public isPaused(): boolean { return this.state === MovingMarkerState.PAUSED; } public start(): void { if (this.isRunning()) { return; } if (this.isPaused()) { this.resume(); } else { this.loadLine(0); this.startAnimation(); this.fire('start'); } } public resume(): void { if (!this.isPaused()) { return; } // update the current line this.currentLine[0] = this.getLatLng(); this.currentDuration -= this.pauseStartTime - this.startTime; this.startAnimation(); } public pause(): void { if (!this.isRunning()) { return; } this.pauseStartTime = Date.now(); this.state = MovingMarkerState.PAUSED; this.stopAnimation(); this.updatePosition(); } public stop(elapsedTime: number): void { if (this.isEnded()) { return; } this.stopAnimation(); if (typeof elapsedTime === 'undefined') { // user call elapsedTime = 0; this.updatePosition(); } this.state = MovingMarkerState.ENDED; this.fire('end', { elapsedTime }); } public addLatLng(latlng: L.LatLng, duration: number): void { this.latLngs = [...this.latLngs, latLng(latlng)]; this.durations = [...this.durations, duration]; } public moveTo(latlng: L.LatLng, duration: number): void { this.stopAnimation(); this.latLngs = [this.getLatLng(), latLng(latlng)]; this.durations = [duration]; this.state = MovingMarkerState.NOT_STARTED; this.start(); this.options.loop = false; } public addStation(pointIndex: number, duration: number): void { if (pointIndex > this.latLngs.length - 2 || pointIndex < 1) { return; } this.stations[pointIndex] = duration; } public onAdd(map: Map): this { super.onAdd(map); console.log('options', this.options); if (this.options.autostart && !this.isStarted()) { this.start(); return; } if (this.isRunning()) { this.resumeAnimation(); } } public onRemove(map: Map): any { // L.Marker.prototype.onRemove.call(this, map); super.onRemove(map); this.stopAnimation(); } private createDurations(latlngs: L.LatLng[], duration: number) { const lastIndex = latlngs.length - 1; const distances = []; let totalDistance = 0; let distance = 0; // compute array of distances between points for (let i = 0; i < lastIndex; i++) { distance = latlngs[i + 1].distanceTo(latlngs[i]); distances.push(distance); totalDistance += distance; } const ratioDuration = duration / totalDistance; let durations = []; distances.map(dist => (durations = [...durations, dist * ratioDuration])); return durations; } private startAnimation(): void { this.state = MovingMarkerState.RUN; this.animId = Util.requestAnimFrame( timestamp => { this.startTime = Date.now(); this.startTimeStamp = timestamp; this.animate(timestamp); }, this, true ); this.animRequested = true; } private resumeAnimation(): void { if (!this.animRequested) { this.animRequested = true; this.animId = Util.requestAnimFrame( timestamp => { this.animate(timestamp); }, this, true ); } } private stopAnimation(): void { if (this.animRequested) { Util.cancelAnimFrame(this.animId); this.animRequested = false; } } private updatePosition() { const elapsedTime = Date.now() - this.startTime; this.animate(this.startTimeStamp + elapsedTime, true); } private loadLine(index: number): void { this.currentIndex = index; this.currentDuration = this.durations[index]; this.currentLine = this.latLngs.slice(index, index + 2); } /** * Load the line where the marker is * @params timestamp * @return elapsed time on the current line or null if * we reached the end or marker is at a station */ private updateLine(timestamp: number): number { // time elapsed since the last latlng let elapsedTime = timestamp - this.startTimeStamp; // not enough time to update the line if (elapsedTime <= this.currentDuration) { return elapsedTime; } let lineIndex = this.currentIndex; let lineDuration = this.currentDuration; let stationDuration; while (elapsedTime > lineDuration) { // substract time of the current line elapsedTime -= lineDuration; stationDuration = this.stations[lineIndex + 1]; // test if there is a station at the end of the line if (stationDuration !== undefined) { if (elapsedTime < stationDuration) { this.setLatLng(this.latLngs[lineIndex + 1]); return null; } elapsedTime -= stationDuration; } lineIndex++; // test if we have reached the end of the polyline if (lineIndex >= this.latLngs.length - 1) { if (this.options.loop) { lineIndex = 0; this.fire('loop', { elapsedTime }); } else { // place the marker at the end, else it would be at // the last position this.setLatLng(this.latLngs[this.latLngs.length - 1]); this.stop(elapsedTime); return null; } } lineDuration = this.durations[lineIndex]; } this.loadLine(lineIndex); this.startTimeStamp = timestamp - elapsedTime; this.startTime = Date.now() - elapsedTime; return elapsedTime; } private animate(timestamp: number, noRequestAnim: boolean = false) { this.animRequested = false; // find the next line and compute the new elapsedTime const elapsedTime = this.updateLine(timestamp); console.log('ELAPSED TIME', elapsedTime); if (this.isEnded()) { // no need to animate return; } if (elapsedTime != null) { // compute the position const p = this.interpolatePosition( this.currentLine[0], this.currentLine[1], this.currentDuration, elapsedTime ); this.setLatLng(p); } if (!noRequestAnim) { this.animId = Util.requestAnimFrame(this.animate, this, false); this.animRequested = true; } } }