2023-06-14 20:39:36 +02:00
import { QueryParameters } from "../Web/QueryParameters"
import { BBox } from "../BBox"
2022-04-09 19:29:51 +02:00
import Constants from "../../Models/Constants"
2023-06-14 20:39:36 +02:00
import { GeoLocationState } from "../State/GeoLocationState"
import { UIEventSource } from "../UIEventSource"
import { Feature , LineString , Point } from "geojson"
import { FeatureSource , WritableFeatureSource } from "../FeatureSource/FeatureSource"
import { LocalStorageSource } from "../Web/LocalStorageSource"
import { GeoOperations } from "../GeoOperations"
import { OsmTags } from "../../Models/OsmFeature"
2023-03-24 19:21:15 +01:00
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
2023-06-14 20:39:36 +02:00
import { MapProperties } from "../../Models/MapProperties"
2022-12-22 04:13:52 +01:00
/ * *
* The geolocation - handler takes a map - location and a geolocation state .
* It ' ll move the map as appropriate given the state of the geolocation - API
2022-12-23 15:52:22 +01:00
* It will also copy the geolocation into the appropriate FeatureSource to display on the map
2022-12-22 04:13:52 +01:00
* /
export default class GeoLocationHandler {
public readonly geolocationState : GeoLocationState
2023-03-24 19:21:15 +01:00
/ * *
* The location as delivered by the GPS , wrapped as FeatureSource
* /
public currentUserLocation : FeatureSource
/ * *
* All previously visited points ( as 'Point' - objects ) , with their metadata
* /
2023-04-20 18:58:31 +02:00
public historicalUserLocations : WritableFeatureSource < Feature < Point > >
2023-03-24 19:21:15 +01:00
/ * *
* A featureSource containing a single linestring which has the GPS - history of the user .
* However , metadata ( such as when every single point was visited ) is lost here ( but is kept in ` historicalUserLocations ` .
* Note that this featureSource is _derived_ from 'historicalUserLocations'
* /
2023-04-20 18:58:31 +02:00
public readonly historicalUserLocationsTrack : FeatureSource
2023-05-07 23:25:28 +02:00
/ * *
* The last moment that the map has moved
* /
2023-06-14 20:39:36 +02:00
public readonly mapHasMoved : UIEventSource < Date | undefined > = new UIEventSource <
Date | undefined
> ( undefined )
2023-03-24 19:21:15 +01:00
private readonly selectedElement : UIEventSource < any >
private readonly mapProperties? : MapProperties
private readonly gpsLocationHistoryRetentionTime? : UIEventSource < number >
2022-12-22 04:13:52 +01:00
constructor (
geolocationState : GeoLocationState ,
2023-03-24 19:21:15 +01:00
selectedElement : UIEventSource < any > ,
mapProperties? : MapProperties ,
gpsLocationHistoryRetentionTime? : UIEventSource < number >
2022-12-22 04:13:52 +01:00
) {
this . geolocationState = geolocationState
2023-03-24 19:21:15 +01:00
const mapLocation = mapProperties . location
this . selectedElement = selectedElement
this . mapProperties = mapProperties
this . gpsLocationHistoryRetentionTime = gpsLocationHistoryRetentionTime
2022-12-22 04:13:52 +01:00
// Did an interaction move the map?
let self = this
let initTime = new Date ( )
mapLocation . addCallbackD ( ( _ ) = > {
if ( new Date ( ) . getTime ( ) - initTime . getTime ( ) < 250 ) {
return
2021-08-19 23:41:48 +02:00
}
2023-05-07 23:25:28 +02:00
self . mapHasMoved . setData ( new Date ( ) )
2022-12-22 04:13:52 +01:00
return true // Unsubscribe
2021-08-19 23:41:48 +02:00
} )
2022-04-09 19:29:51 +02:00
2022-12-22 04:13:52 +01:00
const latLonGivenViaUrl =
QueryParameters . wasInitialized ( "lat" ) || QueryParameters . wasInitialized ( "lon" )
if ( latLonGivenViaUrl ) {
// The URL counts as a 'user interaction'
2023-05-07 23:25:28 +02:00
this . mapHasMoved . setData ( new Date ( ) )
2022-12-22 04:13:52 +01:00
}
2022-04-09 19:29:51 +02:00
2023-03-24 19:21:15 +01:00
this . geolocationState . currentGPSLocation . addCallbackAndRunD ( ( _ ) = > {
2022-12-22 04:13:52 +01:00
const timeSinceLastRequest =
( new Date ( ) . getTime ( ) - geolocationState . requestMoment . data ? . getTime ( ) ? ? 0 ) / 1000
if ( ! this . mapHasMoved . data ) {
// The map hasn't moved yet; we received our first coordinates, so let's move there!
2023-02-09 03:12:21 +01:00
self . MoveMapToCurrentLocation ( )
}
2023-06-14 20:39:36 +02:00
if (
timeSinceLastRequest < Constants . zoomToLocationTimeout &&
( this . mapHasMoved . data === undefined ||
this . mapHasMoved . data . getTime ( ) <
geolocationState . requestMoment . data ? . getTime ( ) )
2023-05-07 23:25:28 +02:00
) {
// still within request time and the map hasn't moved since requesting to jump to the current location
2023-02-09 03:12:21 +01:00
self . MoveMapToCurrentLocation ( )
2022-12-22 04:13:52 +01:00
}
2021-07-19 16:23:13 +02:00
2023-03-24 19:21:15 +01:00
if ( ! this . geolocationState . allowMoving . data ) {
2022-12-22 04:13:52 +01:00
// Jup, the map is locked to the bound location: move automatically
self . MoveMapToCurrentLocation ( )
return
}
2021-07-23 15:56:22 +02:00
} )
2021-08-19 23:41:48 +02:00
2023-03-24 19:21:15 +01:00
geolocationState . allowMoving . syncWith ( mapProperties . allowMoving , true )
2021-08-19 23:41:48 +02:00
2022-12-22 04:13:52 +01:00
this . CopyGeolocationIntoMapstate ( )
2023-04-20 18:58:31 +02:00
this . historicalUserLocationsTrack = this . initUserLocationTrail ( )
2022-12-22 04:13:52 +01:00
}
2022-04-13 01:26:45 +02:00
2022-12-22 04:13:52 +01:00
/ * *
* Move the map to the GPS - location , except :
* - If there is a selected element
* - The location is out of the locked bound
* - The GPS - location iss NULL - island
* @constructor
* /
public MoveMapToCurrentLocation() {
const newLocation = this . geolocationState . currentGPSLocation . data
2023-03-24 19:21:15 +01:00
const mapLocation = this . mapProperties . location
2022-12-22 04:13:52 +01:00
// We got a new location.
// Do we move the map to it?
2023-03-24 19:21:15 +01:00
if ( this . selectedElement . data !== undefined ) {
2022-12-22 04:13:52 +01:00
// Nope, there is something selected, so we don't move to the current GPS-location
return
}
if ( newLocation . latitude === 0 && newLocation . longitude === 0 ) {
console . debug ( "Not moving to GPS-location: it is null island" )
return
}
2021-07-19 16:23:13 +02:00
2022-12-22 04:13:52 +01:00
// We check that the GPS location is not out of bounds
2023-03-24 19:21:15 +01:00
const bounds = this . mapProperties . maxbounds . data
if ( bounds !== undefined ) {
2022-12-22 04:13:52 +01:00
// B is an array with our lock-location
const inRange = new BBox ( bounds ) . contains ( [ newLocation . longitude , newLocation . latitude ] )
if ( ! inRange ) {
return
2021-08-19 23:41:48 +02:00
}
2022-12-22 04:13:52 +01:00
}
2023-03-24 19:21:15 +01:00
console . trace ( "Moving the map to the GPS-location" )
2022-12-22 04:13:52 +01:00
mapLocation . setData ( {
lon : newLocation.longitude ,
lat : newLocation.latitude ,
2021-08-19 23:41:48 +02:00
} )
2023-03-24 19:21:15 +01:00
const zoom = this . mapProperties . zoom
2023-05-17 13:10:22 +02:00
zoom . setData ( Math . min ( Math . max ( zoom . data , 14 ) , 18 ) )
2023-05-07 23:25:28 +02:00
this . mapHasMoved . setData ( new Date ( ) )
2023-02-09 03:12:21 +01:00
this . geolocationState . requestMoment . setData ( undefined )
2022-12-22 04:13:52 +01:00
}
2021-09-09 00:05:51 +02:00
2022-12-22 04:13:52 +01:00
private CopyGeolocationIntoMapstate() {
2023-03-24 19:21:15 +01:00
const features : UIEventSource < Feature [ ] > = new UIEventSource < Feature [ ] > ( [ ] )
this . currentUserLocation = new StaticFeatureSource ( features )
2023-03-22 16:25:24 +01:00
const keysToCopy = [ "speed" , "accuracy" , "altitude" , "altitudeAccuracy" , "heading" ]
2023-06-07 23:55:02 +02:00
let i = 0
2022-12-23 15:52:22 +01:00
this . geolocationState . currentGPSLocation . addCallbackAndRun ( ( location ) = > {
if ( location === undefined ) {
return
}
2023-03-22 16:25:24 +01:00
2023-06-14 20:39:36 +02:00
const properties = {
id : "gps-" + i ,
2023-06-07 23:55:02 +02:00
"user:location" : "yes" ,
date : new Date ( ) . toISOString ( ) ,
}
i ++
for ( const k in keysToCopy ) {
// For some weird reason, the 'Object.keys' method doesn't work for the 'location: GeolocationCoordinates'-object and will thus not copy all the properties when using {...location}
// As such, they are copied here
2023-06-14 20:39:36 +02:00
if ( location [ k ] ) {
2023-06-07 23:55:02 +02:00
properties [ k ] = location [ k ]
}
}
2023-03-24 19:21:15 +01:00
const feature = < Feature > {
2021-11-03 00:44:53 +01:00
type : "Feature" ,
2023-06-07 23:55:02 +02:00
properties ,
2021-11-07 16:34:51 +01:00
geometry : {
type : "Point" ,
2021-11-03 00:44:53 +01:00
coordinates : [ location . longitude , location . latitude ] ,
} ,
}
2021-11-07 16:34:51 +01:00
2023-03-24 19:21:15 +01:00
features . setData ( [ feature ] )
} )
}
private initUserLocationTrail() {
const features = LocalStorageSource . GetParsed < Feature [ ] > ( "gps_location_history" , [ ] )
const now = new Date ( ) . getTime ( )
features . data = features . data . filter ( ( ff ) = > {
if ( ff . properties === undefined ) {
return false
}
const point_time = new Date ( ff . properties [ "date" ] )
return (
now - point_time . getTime ( ) <
1000 * ( this . gpsLocationHistoryRetentionTime ? . data ? ? 24 * 60 * 60 * 1000 )
)
} )
features . ping ( )
let i = 0
this . currentUserLocation ? . features ? . addCallbackAndRunD ( ( [ location ] : [ Feature < Point > ] ) = > {
if ( location === undefined ) {
return
}
const previousLocation = < Feature < Point > > features . data [ features . data . length - 1 ]
if ( previousLocation !== undefined ) {
const previousLocationFreshness = new Date ( previousLocation . properties . date )
const d = GeoOperations . distanceBetween (
< [ number , number ] > previousLocation . geometry . coordinates ,
< [ number , number ] > location . geometry . coordinates
)
let timeDiff = Number . MAX_VALUE // in seconds
const olderLocation = features . data [ features . data . length - 2 ]
if ( olderLocation !== undefined ) {
const olderLocationFreshness = new Date ( olderLocation . properties . date )
timeDiff =
( new Date ( previousLocationFreshness ) . getTime ( ) -
new Date ( olderLocationFreshness ) . getTime ( ) ) /
1000
}
if ( d < 20 && timeDiff < 60 ) {
// Do not append changes less then 20m - it's probably noise anyway
return
}
}
const feature = JSON . parse ( JSON . stringify ( location ) )
feature . properties . id = "gps/" + features . data . length
i ++
features . data . push ( feature )
features . ping ( )
} )
2023-04-20 18:58:31 +02:00
this . historicalUserLocations = < any > new StaticFeatureSource ( features )
2023-03-24 19:21:15 +01:00
const asLine = features . map ( ( allPoints ) = > {
if ( allPoints === undefined || allPoints . length < 2 ) {
return [ ]
}
const feature : Feature < LineString , OsmTags > = {
type : "Feature" ,
properties : {
id : "location_track" ,
"_date:now" : new Date ( ) . toISOString ( ) ,
} ,
geometry : {
type : "LineString" ,
coordinates : allPoints.map (
( ff : Feature < Point > ) = > < [ number , number ] > ff . geometry . coordinates
) ,
} ,
}
return [ feature ]
2021-07-23 15:56:22 +02:00
} )
2023-04-20 18:58:31 +02:00
return new StaticFeatureSource ( asLine )
2020-06-28 02:42:22 +02:00
}
2021-07-19 16:23:13 +02:00
}