import * as MapboxGL from 'mapbox-gl' import along from '@turf/along' import lineDistance from '@turf/length' import OverlayOptions from './base-overlay' import { Position } from './geo' interface MarkerOptions extends OverlayOptions { position: Position color?: string icon?: string width?: number height?: number } interface MarkerVelocity { lng: number lat: number } export default class Marker { private readonly options: MarkerOptions private readonly _marker: MapboxGL.Marker private route: any private velocity: MarkerVelocity private distance: number private moveSpeed: number private moveCounter: number private moveDestination: [number, number] constructor(options: MarkerOptions) { this.options = options if (isValidUrl(this.options.icon)) { const el = document.createElement('div') el.className = 'marker' el.style.backgroundImage = `url(${this.options.icon})` el.style.width = `${this.options.width || 50}px` el.style.height = `${this.options.height || 50}px` el.style.backgroundSize = '100%' this._marker = new MapboxGL.Marker(el) } else { const color = this.options.icon || this.options.color || `#0077fa` this._marker = new MapboxGL.Marker({ color, }) } this._marker.setLngLat([this.options.position.lng, this.options.position.lat]).addTo(this.options.map.map) this.velocity = { lng: this.options.position.lng, lat: this.options.position.lat } this.distance = 0 this.moveCounter = 0 this.moveSpeed = 0 this.moveDestination = [this.options.position.lng, this.options.position.lat] } get marker(): MapboxGL.Marker { return this._marker } get map(): MapboxGL.Map { return this.options.map.map } getElement(): HTMLElement { return this._marker.getElement() } moveTo(velocity: MarkerVelocity, speed: number = 7) { const clamp = (num: number, min: number, max: number) => Math.min(Math.max(num, min), max) this.moveSpeed = clamp(1000 - speed * 100, 10, 1000) this.moveDestination = [velocity.lng, velocity.lat] const currentPosition = this._marker.getLngLat() this.route = { type: 'FeatureCollection', features: [ { type: 'Feature', geometry: { type: 'LineString', coordinates: [[currentPosition.lng, currentPosition.lat], this.moveDestination], }, }, ], } this.distance = lineDistance(this.route.features[0], { units: 'kilometers' }) this.moveCounter = 0 requestAnimationFrame(this._animate.bind(this)) } _animate(timestamp: number) { this.moveCounter += 1 const nextPos = along(this.route.features[0], (this.moveCounter / this.moveSpeed) * this.distance, { units: 'kilometers', }) const [nextLng, nextLat] = nextPos.geometry.coordinates this._marker.setLngLat([nextLng, nextLat]).addTo(this.map) if (this.moveDestination !== [nextLng, nextLat]) { requestAnimationFrame(this._animate.bind(this)) } } remove() { this._marker.remove() } } const isValidUrl = (url: string | undefined) => { try { new URL(url + '') } catch (_) { return false } return true }