forked from MapComplete/MapComplete
refactoring(maplibre): WIP
This commit is contained in:
parent
231d67361e
commit
4d48b1cf2b
89 changed files with 1166 additions and 3973 deletions
|
@ -3,9 +3,13 @@ import { BBox } from "../BBox"
|
|||
import Constants from "../../Models/Constants"
|
||||
import { GeoLocationPointProperties, GeoLocationState } from "../State/GeoLocationState"
|
||||
import { UIEventSource } from "../UIEventSource"
|
||||
import Loc from "../../Models/Loc"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource"
|
||||
import { Feature, LineString, Point } from "geojson"
|
||||
import FeatureSource from "../FeatureSource/FeatureSource"
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import { GeoOperations } from "../GeoOperations"
|
||||
import { OsmTags } from "../../Models/OsmFeature"
|
||||
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
|
||||
import { MapProperties } from "../../Models/MapProperties"
|
||||
|
||||
/**
|
||||
* The geolocation-handler takes a map-location and a geolocation state.
|
||||
|
@ -14,28 +18,39 @@ import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource"
|
|||
*/
|
||||
export default class GeoLocationHandler {
|
||||
public readonly geolocationState: GeoLocationState
|
||||
private readonly _state: {
|
||||
currentUserLocation: SimpleFeatureSource
|
||||
layoutToUse: LayoutConfig
|
||||
locationControl: UIEventSource<Loc>
|
||||
selectedElement: UIEventSource<any>
|
||||
leafletMap?: UIEventSource<any>
|
||||
}
|
||||
|
||||
/**
|
||||
* The location as delivered by the GPS, wrapped as FeatureSource
|
||||
*/
|
||||
public currentUserLocation: FeatureSource
|
||||
|
||||
/**
|
||||
* All previously visited points (as 'Point'-objects), with their metadata
|
||||
*/
|
||||
public historicalUserLocations: FeatureSource
|
||||
|
||||
/**
|
||||
* 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'
|
||||
*/
|
||||
public historicalUserLocationsTrack: FeatureSource
|
||||
public readonly mapHasMoved: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
private readonly selectedElement: UIEventSource<any>
|
||||
private readonly mapProperties?: MapProperties
|
||||
private readonly gpsLocationHistoryRetentionTime?: UIEventSource<number>
|
||||
|
||||
constructor(
|
||||
geolocationState: GeoLocationState,
|
||||
state: {
|
||||
locationControl: UIEventSource<Loc>
|
||||
currentUserLocation: SimpleFeatureSource
|
||||
layoutToUse: LayoutConfig
|
||||
selectedElement: UIEventSource<any>
|
||||
leafletMap?: UIEventSource<any>
|
||||
}
|
||||
selectedElement: UIEventSource<any>,
|
||||
mapProperties?: MapProperties,
|
||||
gpsLocationHistoryRetentionTime?: UIEventSource<number>
|
||||
) {
|
||||
this.geolocationState = geolocationState
|
||||
this._state = state
|
||||
const mapLocation = state.locationControl
|
||||
const mapLocation = mapProperties.location
|
||||
this.selectedElement = selectedElement
|
||||
this.mapProperties = mapProperties
|
||||
this.gpsLocationHistoryRetentionTime = gpsLocationHistoryRetentionTime
|
||||
// Did an interaction move the map?
|
||||
let self = this
|
||||
let initTime = new Date()
|
||||
|
@ -54,7 +69,7 @@ export default class GeoLocationHandler {
|
|||
this.mapHasMoved.setData(true)
|
||||
}
|
||||
|
||||
this.geolocationState.currentGPSLocation.addCallbackAndRunD((newLocation) => {
|
||||
this.geolocationState.currentGPSLocation.addCallbackAndRunD((_) => {
|
||||
const timeSinceLastRequest =
|
||||
(new Date().getTime() - geolocationState.requestMoment.data?.getTime() ?? 0) / 1000
|
||||
if (!this.mapHasMoved.data) {
|
||||
|
@ -65,25 +80,17 @@ export default class GeoLocationHandler {
|
|||
self.MoveMapToCurrentLocation()
|
||||
}
|
||||
|
||||
if (this.geolocationState.isLocked.data) {
|
||||
if (!this.geolocationState.allowMoving.data) {
|
||||
// Jup, the map is locked to the bound location: move automatically
|
||||
self.MoveMapToCurrentLocation()
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
geolocationState.isLocked.map(
|
||||
(isLocked) => {
|
||||
if (isLocked) {
|
||||
state.leafletMap?.data?.dragging?.disable()
|
||||
} else {
|
||||
state.leafletMap?.data?.dragging?.enable()
|
||||
}
|
||||
},
|
||||
[state.leafletMap]
|
||||
)
|
||||
geolocationState.allowMoving.syncWith(mapProperties.allowMoving, true)
|
||||
|
||||
this.CopyGeolocationIntoMapstate()
|
||||
this.initUserLocationTrail()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -95,12 +102,11 @@ export default class GeoLocationHandler {
|
|||
*/
|
||||
public MoveMapToCurrentLocation() {
|
||||
const newLocation = this.geolocationState.currentGPSLocation.data
|
||||
const mapLocation = this._state.locationControl
|
||||
const state = this._state
|
||||
const mapLocation = this.mapProperties.location
|
||||
// We got a new location.
|
||||
// Do we move the map to it?
|
||||
|
||||
if (state.selectedElement.data !== undefined) {
|
||||
if (this.selectedElement.data !== undefined) {
|
||||
// Nope, there is something selected, so we don't move to the current GPS-location
|
||||
return
|
||||
}
|
||||
|
@ -110,8 +116,8 @@ export default class GeoLocationHandler {
|
|||
}
|
||||
|
||||
// We check that the GPS location is not out of bounds
|
||||
const bounds = state.layoutToUse.lockLocation
|
||||
if (bounds && bounds !== true) {
|
||||
const bounds = this.mapProperties.maxbounds.data
|
||||
if (bounds !== undefined) {
|
||||
// B is an array with our lock-location
|
||||
const inRange = new BBox(bounds).contains([newLocation.longitude, newLocation.latitude])
|
||||
if (!inRange) {
|
||||
|
@ -119,22 +125,25 @@ export default class GeoLocationHandler {
|
|||
}
|
||||
}
|
||||
|
||||
console.trace("Moving the map to the GPS-location")
|
||||
mapLocation.setData({
|
||||
zoom: Math.max(mapLocation.data.zoom, 16),
|
||||
lon: newLocation.longitude,
|
||||
lat: newLocation.latitude,
|
||||
})
|
||||
const zoom = this.mapProperties.zoom
|
||||
zoom.setData(Math.max(zoom.data, 16))
|
||||
this.mapHasMoved.setData(true)
|
||||
this.geolocationState.requestMoment.setData(undefined)
|
||||
}
|
||||
|
||||
private CopyGeolocationIntoMapstate() {
|
||||
const state = this._state
|
||||
const features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([])
|
||||
this.currentUserLocation = new StaticFeatureSource(features)
|
||||
this.geolocationState.currentGPSLocation.addCallbackAndRun((location) => {
|
||||
if (location === undefined) {
|
||||
return
|
||||
}
|
||||
const feature = {
|
||||
const feature = <Feature>{
|
||||
type: "Feature",
|
||||
properties: <GeoLocationPointProperties>{
|
||||
id: "gps",
|
||||
|
@ -148,7 +157,82 @@ export default class GeoLocationHandler {
|
|||
},
|
||||
}
|
||||
|
||||
state.currentUserLocation?.features?.setData([{ feature, freshness: new Date() }])
|
||||
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()
|
||||
})
|
||||
|
||||
this.historicalUserLocations = new StaticFeatureSource(features)
|
||||
|
||||
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]
|
||||
})
|
||||
this.historicalUserLocationsTrack = new StaticFeatureSource(asLine)
|
||||
}
|
||||
}
|
||||
|
|
64
Logic/Actors/InitialMapPositioning.ts
Normal file
64
Logic/Actors/InitialMapPositioning.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { UIEventSource } from "../UIEventSource"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import { QueryParameters } from "../Web/QueryParameters"
|
||||
|
||||
/**
|
||||
* This actor is responsible to set the map location.
|
||||
* It will attempt to
|
||||
* - Set the map to the position as passed in by the query parameters (if available)
|
||||
* - Set the map to the position remembered in LocalStorage (if available)
|
||||
* - Set the map to the layout default
|
||||
*
|
||||
* Additionally, it will save the map location to local storage
|
||||
*/
|
||||
export default class InitialMapPositioning {
|
||||
public zoom: UIEventSource<number>
|
||||
public location: UIEventSource<{ lon: number; lat: number }>
|
||||
constructor(layoutToUse: LayoutConfig) {
|
||||
function localStorageSynced(
|
||||
key: string,
|
||||
deflt: number,
|
||||
docs: string
|
||||
): UIEventSource<number> {
|
||||
const localStorage = LocalStorageSource.Get(key)
|
||||
const previousValue = localStorage.data
|
||||
const src = UIEventSource.asFloat(
|
||||
QueryParameters.GetQueryParameter(key, "" + deflt, docs).syncWith(localStorage)
|
||||
)
|
||||
|
||||
if (src.data === deflt) {
|
||||
const prev = Number(previousValue)
|
||||
if (!isNaN(prev)) {
|
||||
src.setData(prev)
|
||||
}
|
||||
}
|
||||
|
||||
return src
|
||||
}
|
||||
|
||||
// -- Location control initialization
|
||||
this.zoom = localStorageSynced(
|
||||
"z",
|
||||
layoutToUse?.startZoom ?? 1,
|
||||
"The initial/current zoom level"
|
||||
)
|
||||
const lat = localStorageSynced(
|
||||
"lat",
|
||||
layoutToUse?.startLat ?? 0,
|
||||
"The initial/current latitude"
|
||||
)
|
||||
const lon = localStorageSynced(
|
||||
"lon",
|
||||
layoutToUse?.startLon ?? 0,
|
||||
"The initial/current longitude of the app"
|
||||
)
|
||||
|
||||
this.location = new UIEventSource({ lon: lon.data, lat: lat.data })
|
||||
this.location.addCallbackD((loc) => {
|
||||
lat.setData(loc.lat)
|
||||
lon.setData(loc.lon)
|
||||
})
|
||||
// Note: this syncs only in one direction
|
||||
}
|
||||
}
|
|
@ -179,13 +179,6 @@ export class BBox {
|
|||
])
|
||||
}
|
||||
|
||||
toLeaflet(): [[number, number], [number, number]] {
|
||||
return [
|
||||
[this.minLat, this.minLon],
|
||||
[this.maxLat, this.maxLon],
|
||||
]
|
||||
}
|
||||
|
||||
toLngLat(): [[number, number], [number, number]] {
|
||||
return [
|
||||
[this.minLon, this.minLat],
|
||||
|
@ -193,7 +186,6 @@ export class BBox {
|
|||
]
|
||||
}
|
||||
|
||||
|
||||
public asGeoJson<T>(properties: T): Feature<Polygon, T> {
|
||||
return {
|
||||
type: "Feature",
|
||||
|
|
|
@ -5,7 +5,7 @@ import BaseUIElement from "../UI/BaseUIElement"
|
|||
import List from "../UI/Base/List"
|
||||
import Title from "../UI/Base/Title"
|
||||
import { BBox } from "./BBox"
|
||||
import { Feature, Geometry, MultiPolygon, Polygon } from "@turf/turf"
|
||||
import { Feature, Geometry, MultiPolygon, Polygon } from "geojson"
|
||||
|
||||
export interface ExtraFuncParams {
|
||||
/**
|
||||
|
@ -68,7 +68,7 @@ class EnclosingFunc implements ExtraFunction {
|
|||
}
|
||||
if (
|
||||
GeoOperations.completelyWithin(
|
||||
feat,
|
||||
<Feature>feat,
|
||||
<Feature<Polygon | MultiPolygon, any>>otherFeature
|
||||
)
|
||||
) {
|
||||
|
|
|
@ -1,168 +0,0 @@
|
|||
import { Store } from "../../UIEventSource"
|
||||
import { GeoOperations } from "../../GeoOperations"
|
||||
import FeatureSource from "../FeatureSource"
|
||||
import PointRenderingConfig from "../../../Models/ThemeConfig/PointRenderingConfig"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import LineRenderingConfig from "../../../Models/ThemeConfig/LineRenderingConfig"
|
||||
/**
|
||||
* This feature source helps the ShowDataLayer class: it introduces the necessary extra features and indicates with what renderConfig it should be rendered.
|
||||
*/
|
||||
export default class RenderingMultiPlexerFeatureSource {
|
||||
public readonly features: Store<
|
||||
(any & {
|
||||
pointRenderingIndex: number | undefined
|
||||
lineRenderingIndex: number | undefined
|
||||
})[]
|
||||
>
|
||||
private readonly pointRenderings: { rendering: PointRenderingConfig; index: number }[]
|
||||
private readonly centroidRenderings: { rendering: PointRenderingConfig; index: number }[]
|
||||
private readonly projectedCentroidRenderings: {
|
||||
rendering: PointRenderingConfig
|
||||
index: number
|
||||
}[]
|
||||
private readonly startRenderings: { rendering: PointRenderingConfig; index: number }[]
|
||||
private readonly endRenderings: { rendering: PointRenderingConfig; index: number }[]
|
||||
private readonly hasCentroid: boolean
|
||||
private lineRenderObjects: LineRenderingConfig[]
|
||||
|
||||
constructor(upstream: FeatureSource, layer: LayerConfig) {
|
||||
const pointRenderObjects: { rendering: PointRenderingConfig; index: number }[] =
|
||||
layer.mapRendering.map((r, i) => ({
|
||||
rendering: r,
|
||||
index: i,
|
||||
}))
|
||||
this.pointRenderings = pointRenderObjects.filter((r) => r.rendering.location.has("point"))
|
||||
this.centroidRenderings = pointRenderObjects.filter((r) =>
|
||||
r.rendering.location.has("centroid")
|
||||
)
|
||||
this.projectedCentroidRenderings = pointRenderObjects.filter((r) =>
|
||||
r.rendering.location.has("projected_centerpoint")
|
||||
)
|
||||
this.startRenderings = pointRenderObjects.filter((r) => r.rendering.location.has("start"))
|
||||
this.endRenderings = pointRenderObjects.filter((r) => r.rendering.location.has("end"))
|
||||
this.hasCentroid =
|
||||
this.centroidRenderings.length > 0 || this.projectedCentroidRenderings.length > 0
|
||||
this.lineRenderObjects = layer.lineRendering
|
||||
|
||||
this.features = upstream.features.map((features) => {
|
||||
if (features === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const withIndex: any[] = []
|
||||
|
||||
function addAsPoint(feat, rendering, coordinate) {
|
||||
const patched = {
|
||||
...feat,
|
||||
pointRenderingIndex: rendering.index,
|
||||
}
|
||||
patched.geometry = {
|
||||
type: "Point",
|
||||
coordinates: coordinate,
|
||||
}
|
||||
withIndex.push(patched)
|
||||
}
|
||||
|
||||
for (const feat of features) {
|
||||
if (feat === undefined) {
|
||||
continue
|
||||
}
|
||||
this.inspectFeature(feat, addAsPoint, withIndex)
|
||||
}
|
||||
|
||||
return withIndex
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* For every source feature, adds the necessary rendering-features
|
||||
*/
|
||||
private inspectFeature(
|
||||
feat,
|
||||
addAsPoint: (feat, rendering, centerpoint: [number, number]) => void,
|
||||
withIndex: any[]
|
||||
) {
|
||||
if (feat.geometry.type === "Point") {
|
||||
for (const rendering of this.pointRenderings) {
|
||||
withIndex.push({
|
||||
...feat,
|
||||
pointRenderingIndex: rendering.index,
|
||||
})
|
||||
}
|
||||
} else if (feat.geometry.type === "MultiPolygon") {
|
||||
if (this.centroidRenderings.length > 0 || this.projectedCentroidRenderings.length > 0) {
|
||||
const centerpoints: [number, number][] = (<[number, number][][][]>(
|
||||
feat.geometry.coordinates
|
||||
)).map((rings) =>
|
||||
GeoOperations.centerpointCoordinates({
|
||||
type: "Feature",
|
||||
properties: {},
|
||||
geometry: { type: "Polygon", coordinates: rings },
|
||||
})
|
||||
)
|
||||
for (const centroidRendering of this.centroidRenderings) {
|
||||
for (const centerpoint of centerpoints) {
|
||||
addAsPoint(feat, centroidRendering, centerpoint)
|
||||
}
|
||||
}
|
||||
|
||||
for (const centroidRendering of this.projectedCentroidRenderings) {
|
||||
for (const centerpoint of centerpoints) {
|
||||
addAsPoint(feat, centroidRendering, centerpoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AT last, add it 'as is' to what we should render
|
||||
for (let i = 0; i < this.lineRenderObjects.length; i++) {
|
||||
withIndex.push({
|
||||
...feat,
|
||||
lineRenderingIndex: i,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// This is a a line or polygon: add the centroids
|
||||
let centerpoint: [number, number] = undefined
|
||||
let projectedCenterPoint: [number, number] = undefined
|
||||
if (this.hasCentroid) {
|
||||
centerpoint = GeoOperations.centerpointCoordinates(feat)
|
||||
if (this.projectedCentroidRenderings.length > 0) {
|
||||
projectedCenterPoint = <[number, number]>(
|
||||
GeoOperations.nearestPoint(feat, centerpoint).geometry.coordinates
|
||||
)
|
||||
}
|
||||
}
|
||||
for (const rendering of this.centroidRenderings) {
|
||||
addAsPoint(feat, rendering, centerpoint)
|
||||
}
|
||||
|
||||
if (feat.geometry.type === "LineString") {
|
||||
for (const rendering of this.projectedCentroidRenderings) {
|
||||
addAsPoint(feat, rendering, projectedCenterPoint)
|
||||
}
|
||||
|
||||
// Add start- and endpoints
|
||||
const coordinates = feat.geometry.coordinates
|
||||
for (const rendering of this.startRenderings) {
|
||||
addAsPoint(feat, rendering, coordinates[0])
|
||||
}
|
||||
for (const rendering of this.endRenderings) {
|
||||
const coordinate = coordinates[coordinates.length - 1]
|
||||
addAsPoint(feat, rendering, coordinate)
|
||||
}
|
||||
} else {
|
||||
for (const rendering of this.projectedCentroidRenderings) {
|
||||
addAsPoint(feat, rendering, centerpoint)
|
||||
}
|
||||
}
|
||||
|
||||
// AT last, add it 'as is' to what we should render
|
||||
for (let i = 0; i < this.lineRenderObjects.length; i++) {
|
||||
withIndex.push({
|
||||
...feat,
|
||||
lineRenderingIndex: i,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +1,8 @@
|
|||
import { BBox } from "./BBox"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import * as turf from "@turf/turf"
|
||||
import {
|
||||
AllGeoJSON,
|
||||
booleanWithin,
|
||||
Coord,
|
||||
Feature,
|
||||
Geometry,
|
||||
MultiPolygon,
|
||||
Polygon,
|
||||
} from "@turf/turf"
|
||||
import { AllGeoJSON, booleanWithin, Coord } from "@turf/turf"
|
||||
import { Feature, Geometry, MultiPolygon, Polygon } from "geojson"
|
||||
import { GeoJSON, LineString, Point, Position } from "geojson"
|
||||
import togpx from "togpx"
|
||||
import Constants from "../Models/Constants"
|
||||
|
@ -263,7 +256,10 @@ export class GeoOperations {
|
|||
* @param way The road on which you want to find a point
|
||||
* @param point Point defined as [lon, lat]
|
||||
*/
|
||||
public static nearestPoint(way: Feature<LineString | Polygon>, point: [number, number]) {
|
||||
public static nearestPoint(
|
||||
way: Feature<LineString | Polygon>,
|
||||
point: [number, number]
|
||||
): Feature<Point> {
|
||||
if (way.geometry.type === "Polygon") {
|
||||
way = { ...way }
|
||||
way.geometry = { ...way.geometry }
|
||||
|
@ -710,6 +706,63 @@ export class GeoOperations {
|
|||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* const f = (type, feature: Feature) => GeoOperations.featureToCoordinateWithRenderingType(feature, type)
|
||||
* const g = geometry => (<Feature> {type: "Feature", properties: {}, geometry})
|
||||
* f("point", g({type:"Point", coordinates:[1,2]})) // => [1,2]
|
||||
* f("centroid", g({type:"Point", coordinates:[1,2]})) // => undefined
|
||||
* f("start", g({type:"Point", coordinates:[1,2]})) // => undefined
|
||||
* f("centroid", g({type:"LineString", coordinates:[[1,2], [3,4]]})) // => [2,3]
|
||||
* f("centroid", g({type:"Polygon", coordinates:[[[1,2], [3,4], [1,2]]]})) // => [2,3]
|
||||
* f("projected_centerpoint", g({type:"LineString", coordinates:[[1,2], [3,4]]})) // => [1.9993137596003214,2.999313759600321]
|
||||
* f("start", g({type:"LineString", coordinates:[[1,2], [3,4]]})) // => [1,2]
|
||||
* f("end", g({type:"LineString", coordinates:[[1,2], [3,4]]})) // => [3,4]
|
||||
*
|
||||
*/
|
||||
public static featureToCoordinateWithRenderingType(
|
||||
feature: Feature,
|
||||
location: "point" | "centroid" | "start" | "end" | "projected_centerpoint" | string
|
||||
): [number, number] | undefined {
|
||||
switch (location) {
|
||||
case "point":
|
||||
if (feature.geometry.type === "Point") {
|
||||
return <[number, number]>feature.geometry.coordinates
|
||||
}
|
||||
return undefined
|
||||
case "centroid":
|
||||
if (feature.geometry.type === "Point") {
|
||||
return undefined
|
||||
}
|
||||
return GeoOperations.centerpointCoordinates(feature)
|
||||
case "projected_centerpoint":
|
||||
if (
|
||||
feature.geometry.type === "LineString" ||
|
||||
feature.geometry.type === "MultiLineString"
|
||||
) {
|
||||
const centerpoint = GeoOperations.centerpointCoordinates(feature)
|
||||
const projected = GeoOperations.nearestPoint(
|
||||
<Feature<LineString>>feature,
|
||||
centerpoint
|
||||
)
|
||||
return <[number, number]>projected.geometry.coordinates
|
||||
}
|
||||
return undefined
|
||||
case "start":
|
||||
if (feature.geometry.type === "LineString") {
|
||||
return <[number, number]>feature.geometry.coordinates[0]
|
||||
}
|
||||
return undefined
|
||||
case "end":
|
||||
if (feature.geometry.type === "LineString") {
|
||||
return <[number, number]>feature.geometry.coordinates.at(-1)
|
||||
}
|
||||
return undefined
|
||||
default:
|
||||
throw "Unkown location type: " + location
|
||||
}
|
||||
}
|
||||
private static pointWithinRing(x: number, y: number, ring: [number, number][]) {
|
||||
let inside = false
|
||||
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
||||
|
|
|
@ -29,8 +29,9 @@ export default class Maproulette {
|
|||
/**
|
||||
* The API key to use for all requests
|
||||
*/
|
||||
private apiKey: string
|
||||
private readonly apiKey: string
|
||||
|
||||
public static singleton = new Maproulette()
|
||||
/**
|
||||
* Creates a new Maproulette instance
|
||||
* @param endpoint The API endpoint to use
|
||||
|
|
|
@ -59,15 +59,6 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct
|
|||
}
|
||||
}
|
||||
|
||||
public async getPreview(): Promise<FeatureSource> {
|
||||
const outerPreview = await this.createOuterWay.getPreview()
|
||||
outerPreview.features.data.push({
|
||||
freshness: new Date(),
|
||||
feature: this.geojsonPreview,
|
||||
})
|
||||
return outerPreview
|
||||
}
|
||||
|
||||
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
console.log("Running CMPWPRA")
|
||||
const descriptions: ChangeDescription[] = []
|
||||
|
|
|
@ -22,24 +22,25 @@ export class Changes {
|
|||
/**
|
||||
* All the newly created features as featureSource + all the modified features
|
||||
*/
|
||||
public features = new UIEventSource<{ feature: any; freshness: Date }[]>([])
|
||||
public readonly features = new UIEventSource<{ feature: any; freshness: Date }[]>([])
|
||||
public readonly pendingChanges: UIEventSource<ChangeDescription[]> =
|
||||
LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
|
||||
public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined)
|
||||
public readonly state: { allElements: ElementStorage; osmConnection: OsmConnection }
|
||||
public readonly extraComment: UIEventSource<string> = new UIEventSource(undefined)
|
||||
|
||||
private historicalUserLocations: FeatureSource
|
||||
private readonly historicalUserLocations: FeatureSource
|
||||
private _nextId: number = -1 // Newly assigned ID's are negative
|
||||
private readonly isUploading = new UIEventSource(false)
|
||||
private readonly previouslyCreated: OsmObject[] = []
|
||||
private readonly _leftRightSensitive: boolean
|
||||
private _changesetHandler: ChangesetHandler
|
||||
private readonly _changesetHandler: ChangesetHandler
|
||||
|
||||
constructor(
|
||||
state?: {
|
||||
allElements: ElementStorage
|
||||
osmConnection: OsmConnection
|
||||
historicalUserLocations: FeatureSource
|
||||
},
|
||||
leftRightSensitive: boolean = false
|
||||
) {
|
||||
|
@ -53,6 +54,7 @@ export class Changes {
|
|||
state.allElements,
|
||||
this
|
||||
)
|
||||
this.historicalUserLocations = state.historicalUserLocations
|
||||
|
||||
// Note: a changeset might be reused which was opened just before and might have already used some ids
|
||||
// This doesn't matter however, as the '-1' is per piecewise upload, not global per changeset
|
||||
|
@ -164,7 +166,6 @@ export class Changes {
|
|||
|
||||
const now = new Date()
|
||||
const recentLocationPoints = locations
|
||||
.map((ff) => ff.feature)
|
||||
.filter((feat) => feat.geometry.type === "Point")
|
||||
.filter((feat) => {
|
||||
const visitTime = new Date(
|
||||
|
@ -582,8 +583,4 @@ export class Changes {
|
|||
)
|
||||
return result
|
||||
}
|
||||
|
||||
public setHistoricalUserLocations(locations: FeatureSource) {
|
||||
this.historicalUserLocations = locations
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import State from "../../State"
|
||||
import { Utils } from "../../Utils"
|
||||
import { BBox } from "../BBox"
|
||||
|
||||
|
@ -14,8 +13,8 @@ export interface GeoCodeResult {
|
|||
export class Geocoding {
|
||||
private static readonly host = "https://nominatim.openstreetmap.org/search?"
|
||||
|
||||
static async Search(query: string): Promise<GeoCodeResult[]> {
|
||||
const b = State?.state?.currentBounds?.data ?? BBox.global
|
||||
static async Search(query: string, bbox: BBox): Promise<GeoCodeResult[]> {
|
||||
const b = bbox ?? BBox.global
|
||||
const url =
|
||||
Geocoding.host +
|
||||
"format=json&limit=1&viewbox=" +
|
||||
|
|
|
@ -1,91 +0,0 @@
|
|||
import FeatureSwitchState from "./FeatureSwitchState"
|
||||
import { ElementStorage } from "../ElementStorage"
|
||||
import { Changes } from "../Osm/Changes"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { UIEventSource } from "../UIEventSource"
|
||||
import Loc from "../../Models/Loc"
|
||||
import { BBox } from "../BBox"
|
||||
import { QueryParameters } from "../Web/QueryParameters"
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import { Utils } from "../../Utils"
|
||||
import ChangeToElementsActor from "../Actors/ChangeToElementsActor"
|
||||
import PendingChangesUploader from "../Actors/PendingChangesUploader"
|
||||
|
||||
/**
|
||||
* The part of the state keeping track of where the elements, loading them, configuring the feature pipeline etc
|
||||
*/
|
||||
export default class ElementsState extends FeatureSwitchState {
|
||||
/**
|
||||
The mapping from id -> UIEventSource<properties>
|
||||
*/
|
||||
public allElements: ElementStorage = new ElementStorage()
|
||||
|
||||
/**
|
||||
The latest element that was selected
|
||||
*/
|
||||
public readonly selectedElement = new UIEventSource<any>(undefined, "Selected element")
|
||||
|
||||
/**
|
||||
* The map location: currently centered lat, lon and zoom
|
||||
*/
|
||||
public readonly locationControl = new UIEventSource<Loc>(undefined, "locationControl")
|
||||
|
||||
/**
|
||||
* The current visible extent of the screen
|
||||
*/
|
||||
public readonly currentBounds = new UIEventSource<BBox>(undefined)
|
||||
|
||||
constructor(layoutToUse: LayoutConfig) {
|
||||
super(layoutToUse)
|
||||
|
||||
function localStorageSynced(
|
||||
key: string,
|
||||
deflt: number,
|
||||
docs: string
|
||||
): UIEventSource<number> {
|
||||
const localStorage = LocalStorageSource.Get(key)
|
||||
const previousValue = localStorage.data
|
||||
const src = UIEventSource.asFloat(
|
||||
QueryParameters.GetQueryParameter(key, "" + deflt, docs).syncWith(localStorage)
|
||||
)
|
||||
|
||||
if (src.data === deflt) {
|
||||
const prev = Number(previousValue)
|
||||
if (!isNaN(prev)) {
|
||||
src.setData(prev)
|
||||
}
|
||||
}
|
||||
|
||||
return src
|
||||
}
|
||||
|
||||
// -- Location control initialization
|
||||
const zoom = localStorageSynced(
|
||||
"z",
|
||||
layoutToUse?.startZoom ?? 1,
|
||||
"The initial/current zoom level"
|
||||
)
|
||||
const lat = localStorageSynced(
|
||||
"lat",
|
||||
layoutToUse?.startLat ?? 0,
|
||||
"The initial/current latitude"
|
||||
)
|
||||
const lon = localStorageSynced(
|
||||
"lon",
|
||||
layoutToUse?.startLon ?? 0,
|
||||
"The initial/current longitude of the app"
|
||||
)
|
||||
|
||||
this.locationControl.setData({
|
||||
zoom: Utils.asFloat(zoom.data),
|
||||
lat: Utils.asFloat(lat.data),
|
||||
lon: Utils.asFloat(lon.data),
|
||||
})
|
||||
this.locationControl.addCallback((latlonz) => {
|
||||
// Sync the location controls
|
||||
zoom.setData(latlonz.zoom)
|
||||
lat.setData(latlonz.lat)
|
||||
lon.setData(latlonz.lon)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,9 +1,7 @@
|
|||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import FeaturePipeline from "../FeatureSource/FeaturePipeline"
|
||||
import { Tiles } from "../../Models/TileRange"
|
||||
import ShowDataLayer from "../../UI/ShowDataLayer/ShowDataLayer"
|
||||
import { TileHierarchyAggregator } from "../../UI/ShowDataLayer/TileHierarchyAggregator"
|
||||
import ShowTileInfo from "../../UI/ShowDataLayer/ShowTileInfo"
|
||||
import { UIEventSource } from "../UIEventSource"
|
||||
import MapState from "./MapState"
|
||||
import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler"
|
||||
|
@ -14,6 +12,7 @@ import { FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource"
|
|||
import MetaTagRecalculator from "../FeatureSource/Actors/MetaTagRecalculator"
|
||||
import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import ShowDataLayer from "../../UI/Map/ShowDataLayer"
|
||||
|
||||
export default class FeaturePipelineState extends MapState {
|
||||
/**
|
||||
|
@ -116,14 +115,12 @@ export default class FeaturePipelineState extends MapState {
|
|||
[self.currentBounds, source.layer.isDisplayed, sourceBBox]
|
||||
)
|
||||
|
||||
new ShowDataLayer({
|
||||
new ShowDataLayer(self.maplibreMap, {
|
||||
features: source,
|
||||
leafletMap: self.leafletMap,
|
||||
layerToShow: source.layer.layerDef,
|
||||
layer: source.layer.layerDef,
|
||||
doShowLayer: doShowFeatures,
|
||||
selectedElement: self.selectedElement,
|
||||
state: self,
|
||||
popup: (tags, layer) => self.CreatePopup(tags, layer),
|
||||
buildPopup: (tags, layer) => self.CreatePopup(tags, layer),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -136,8 +133,6 @@ export default class FeaturePipelineState extends MapState {
|
|||
sourcesToRegister.forEach((source) => self.metatagRecalculator.registerSource(source))
|
||||
|
||||
new SelectedFeatureHandler(Hash.hash, this)
|
||||
|
||||
this.AddClusteringToMap(this.leafletMap)
|
||||
}
|
||||
|
||||
public CreatePopup(tags: UIEventSource<any>, layer: LayerConfig): ScrollableFullScreen {
|
||||
|
@ -148,27 +143,4 @@ export default class FeaturePipelineState extends MapState {
|
|||
this.popups.set(tags.data.id, popup)
|
||||
return popup
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the cluster-tiles to the given map
|
||||
* @param leafletMap: a UIEventSource possible having a leaflet map
|
||||
* @constructor
|
||||
*/
|
||||
public AddClusteringToMap(leafletMap: UIEventSource<any>) {
|
||||
const clustering = this.layoutToUse.clustering
|
||||
const self = this
|
||||
new ShowDataLayer({
|
||||
features: this.featureAggregator.getCountsForZoom(
|
||||
clustering,
|
||||
this.locationControl,
|
||||
clustering.minNeededElements
|
||||
),
|
||||
leafletMap: leafletMap,
|
||||
layerToShow: ShowTileInfo.styling,
|
||||
popup: this.featureSwitchIsDebugging.data
|
||||
? (tags, layer) => new FeatureInfoBox(tags, layer, self)
|
||||
: undefined,
|
||||
state: this,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ export class GeoLocationState {
|
|||
/**
|
||||
* If true: the map will center (and re-center) to this location
|
||||
*/
|
||||
public readonly isLocked: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
public readonly allowMoving: UIEventSource<boolean> = new UIEventSource<boolean>(true)
|
||||
|
||||
public readonly currentGPSLocation: UIEventSource<GeolocationCoordinates | undefined> =
|
||||
new UIEventSource<GeolocationCoordinates | undefined>(undefined)
|
||||
|
@ -72,7 +72,6 @@ export class GeoLocationState {
|
|||
self._previousLocationGrant.setData("false")
|
||||
}
|
||||
})
|
||||
|
||||
console.log("Previous location grant:", this._previousLocationGrant.data)
|
||||
if (this._previousLocationGrant.data === "true") {
|
||||
// A previous visit successfully granted permission. Chance is high that we are allowed to use it again!
|
||||
|
@ -87,7 +86,6 @@ export class GeoLocationState {
|
|||
}
|
||||
this.requestPermission()
|
||||
}
|
||||
window["geolocation_state"] = this
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,51 +1,31 @@
|
|||
import UserRelatedState from "./UserRelatedState"
|
||||
import { Store, Stores, UIEventSource } from "../UIEventSource"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import Attribution from "../../UI/BigComponents/Attribution"
|
||||
import Minimap, { MinimapObj } from "../../UI/Base/Minimap"
|
||||
import { Tiles } from "../../Models/TileRange"
|
||||
import BaseUIElement from "../../UI/BaseUIElement"
|
||||
import FilteredLayer, { FilterState } from "../../Models/FilteredLayer"
|
||||
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"
|
||||
import { QueryParameters } from "../Web/QueryParameters"
|
||||
import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer"
|
||||
import { FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource"
|
||||
import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource"
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import { GeoOperations } from "../GeoOperations"
|
||||
import TitleHandler from "../Actors/TitleHandler"
|
||||
import { BBox } from "../BBox"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { TiledStaticFeatureSource } from "../FeatureSource/Sources/StaticFeatureSource"
|
||||
import { Translation, TypedTranslation } from "../../UI/i18n/Translation"
|
||||
import { Tag } from "../Tags/Tag"
|
||||
import StaticFeatureSource, {
|
||||
TiledStaticFeatureSource,
|
||||
} from "../FeatureSource/Sources/StaticFeatureSource"
|
||||
import { OsmConnection } from "../Osm/OsmConnection"
|
||||
import { Feature, LineString } from "geojson"
|
||||
import { OsmTags } from "../../Models/OsmFeature"
|
||||
|
||||
export interface GlobalFilter {
|
||||
filter: FilterState
|
||||
id: string
|
||||
onNewPoint: {
|
||||
safetyCheck: Translation
|
||||
confirmAddNew: TypedTranslation<{ preset: Translation }>
|
||||
tags: Tag[]
|
||||
}
|
||||
}
|
||||
import { Feature } from "geojson"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import { GlobalFilter } from "../../Models/GlobalFilter"
|
||||
import { MapProperties } from "../../Models/MapProperties"
|
||||
import ShowDataLayer from "../../UI/Map/ShowDataLayer"
|
||||
|
||||
/**
|
||||
* Contains all the leaflet-map related state
|
||||
*/
|
||||
export default class MapState extends UserRelatedState {
|
||||
/**
|
||||
The leaflet instance of the big basemap
|
||||
*/
|
||||
public leafletMap = new UIEventSource<any /*L.Map*/>(undefined, "leafletmap")
|
||||
export default class MapState {
|
||||
|
||||
|
||||
/**
|
||||
* The current background layer
|
||||
*/
|
||||
public backgroundLayer: UIEventSource<BaseLayer>
|
||||
/**
|
||||
* Last location where a click was registered
|
||||
*/
|
||||
|
@ -58,34 +38,6 @@ export default class MapState extends UserRelatedState {
|
|||
* The bounds of the current map view
|
||||
*/
|
||||
public currentView: FeatureSourceForLayer & Tiled
|
||||
/**
|
||||
* The location as delivered by the GPS
|
||||
*/
|
||||
public currentUserLocation: SimpleFeatureSource
|
||||
|
||||
/**
|
||||
* All previously visited points, with their metadata
|
||||
*/
|
||||
public historicalUserLocations: SimpleFeatureSource
|
||||
/**
|
||||
* The number of seconds that the GPS-locations are stored in memory.
|
||||
* Time in seconds
|
||||
*/
|
||||
public gpsLocationHistoryRetentionTime = new UIEventSource(
|
||||
7 * 24 * 60 * 60,
|
||||
"gps_location_retention"
|
||||
)
|
||||
/**
|
||||
* 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'
|
||||
*/
|
||||
public historicalUserLocationsTrack: FeatureSourceForLayer & Tiled
|
||||
|
||||
/**
|
||||
* A feature source containing the current home location of the user
|
||||
*/
|
||||
public homeLocation: FeatureSourceForLayer & Tiled
|
||||
|
||||
/**
|
||||
* A builtin layer which contains the selected element.
|
||||
|
@ -94,7 +46,7 @@ export default class MapState extends UserRelatedState {
|
|||
*/
|
||||
public selectedElementsLayer: FeatureSourceForLayer & Tiled
|
||||
|
||||
public readonly mainMapObject: BaseUIElement & MinimapObj
|
||||
public readonly mainMapObject: BaseUIElement
|
||||
|
||||
/**
|
||||
* Which layers are enabled in the current theme and what filters are applied onto them
|
||||
|
@ -114,9 +66,7 @@ export default class MapState extends UserRelatedState {
|
|||
*/
|
||||
public overlayToggles: { config: TilesourceConfig; isDisplayed: UIEventSource<boolean> }[]
|
||||
|
||||
constructor(layoutToUse: LayoutConfig, options?: { attemptLogin: true | boolean }) {
|
||||
super(layoutToUse, options)
|
||||
|
||||
constructor() {
|
||||
this.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(this.locationControl)
|
||||
|
||||
let defaultLayer = AvailableBaseLayers.osmCarto
|
||||
|
@ -130,13 +80,6 @@ export default class MapState extends UserRelatedState {
|
|||
this.backgroundLayer = new UIEventSource<BaseLayer>(defaultLayer)
|
||||
this.backgroundLayer.addCallbackAndRunD((layer) => self.backgroundLayerId.setData(layer.id))
|
||||
|
||||
const attr = new Attribution(
|
||||
this.locationControl,
|
||||
this.osmConnection.userDetails,
|
||||
this.layoutToUse,
|
||||
this.currentBounds
|
||||
)
|
||||
|
||||
// Will write into this.leafletMap
|
||||
this.mainMapObject = Minimap.createMiniMap({
|
||||
background: this.backgroundLayer,
|
||||
|
@ -162,12 +105,8 @@ export default class MapState extends UserRelatedState {
|
|||
MapState.InitializeFilteredLayers(this.layoutToUse, this.osmConnection)
|
||||
)
|
||||
|
||||
this.lockBounds()
|
||||
this.AddAllOverlaysToMap(this.leafletMap)
|
||||
|
||||
this.initHomeLocation()
|
||||
this.initGpsLocation()
|
||||
this.initUserLocationTrail()
|
||||
this.initCurrentView()
|
||||
this.initSelectedElement()
|
||||
|
||||
|
@ -189,17 +128,6 @@ export default class MapState extends UserRelatedState {
|
|||
}
|
||||
}
|
||||
|
||||
private lockBounds() {
|
||||
const layout = this.layoutToUse
|
||||
if (!layout?.lockLocation) {
|
||||
return
|
||||
}
|
||||
console.warn("Locking the bounds to ", layout.lockLocation)
|
||||
this.mainMapObject.installBounds(
|
||||
new BBox(layout.lockLocation),
|
||||
this.featureSwitchIsTesting.data
|
||||
)
|
||||
}
|
||||
|
||||
private initCurrentView() {
|
||||
let currentViewLayer: FilteredLayer = this.filteredLayers.data.filter(
|
||||
|
@ -244,17 +172,6 @@ export default class MapState extends UserRelatedState {
|
|||
this.currentView = new TiledStaticFeatureSource(features, currentViewLayer)
|
||||
}
|
||||
|
||||
private initGpsLocation() {
|
||||
// Initialize the gps layer data. This is emtpy for now, the actual writing happens in the Geolocationhandler
|
||||
const gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter(
|
||||
(l) => l.layerDef.id === "gps_location"
|
||||
)[0]
|
||||
if (gpsLayerDef === undefined) {
|
||||
return
|
||||
}
|
||||
this.currentUserLocation = new SimpleFeatureSource(gpsLayerDef, Tiles.tile_index(0, 0, 0))
|
||||
}
|
||||
|
||||
private initSelectedElement() {
|
||||
const layerDef: FilteredLayer = this.filteredLayers.data.filter(
|
||||
(l) => l.layerDef.id === "selected_element"
|
||||
|
@ -281,145 +198,6 @@ export default class MapState extends UserRelatedState {
|
|||
this.selectedElementsLayer = new TiledStaticFeatureSource(store, layerDef)
|
||||
}
|
||||
|
||||
private initUserLocationTrail() {
|
||||
const features = LocalStorageSource.GetParsed<{ feature: any; freshness: Date }[]>(
|
||||
"gps_location_history",
|
||||
[]
|
||||
)
|
||||
const now = new Date().getTime()
|
||||
features.data = features.data
|
||||
.map((ff) => ({ feature: ff.feature, freshness: new Date(ff.freshness) }))
|
||||
.filter(
|
||||
(ff) =>
|
||||
now - ff.freshness.getTime() < 1000 * this.gpsLocationHistoryRetentionTime.data
|
||||
)
|
||||
features.ping()
|
||||
const self = this
|
||||
let i = 0
|
||||
this.currentUserLocation?.features?.addCallbackAndRunD(([location]) => {
|
||||
if (location === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const previousLocation = features.data[features.data.length - 1]
|
||||
if (previousLocation !== undefined) {
|
||||
const d = GeoOperations.distanceBetween(
|
||||
previousLocation.feature.geometry.coordinates,
|
||||
location.feature.geometry.coordinates
|
||||
)
|
||||
let timeDiff = Number.MAX_VALUE // in seconds
|
||||
const olderLocation = features.data[features.data.length - 2]
|
||||
if (olderLocation !== undefined) {
|
||||
timeDiff =
|
||||
(new Date(previousLocation.freshness).getTime() -
|
||||
new Date(olderLocation.freshness).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))
|
||||
feature.properties.id = "gps/" + features.data.length
|
||||
i++
|
||||
features.data.push({ feature, freshness: new Date() })
|
||||
features.ping()
|
||||
})
|
||||
|
||||
let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter(
|
||||
(l) => l.layerDef.id === "gps_location_history"
|
||||
)[0]
|
||||
if (gpsLayerDef !== undefined) {
|
||||
this.historicalUserLocations = new SimpleFeatureSource(
|
||||
gpsLayerDef,
|
||||
Tiles.tile_index(0, 0, 0),
|
||||
features
|
||||
)
|
||||
this.changes.setHistoricalUserLocations(this.historicalUserLocations)
|
||||
}
|
||||
|
||||
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) => ff.feature.geometry.coordinates),
|
||||
},
|
||||
}
|
||||
|
||||
self.allElements.ContainingFeatures.set(feature.properties.id, feature)
|
||||
|
||||
return [
|
||||
{
|
||||
feature,
|
||||
freshness: new Date(),
|
||||
},
|
||||
]
|
||||
})
|
||||
let gpsLineLayerDef: FilteredLayer = this.filteredLayers.data.filter(
|
||||
(l) => l.layerDef.id === "gps_track"
|
||||
)[0]
|
||||
if (gpsLineLayerDef !== undefined) {
|
||||
this.historicalUserLocationsTrack = new TiledStaticFeatureSource(
|
||||
asLine,
|
||||
gpsLineLayerDef
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private initHomeLocation() {
|
||||
const empty = []
|
||||
const feature = Stores.ListStabilized(
|
||||
this.osmConnection.userDetails.map((userDetails) => {
|
||||
if (userDetails === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const home = userDetails.home
|
||||
if (home === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return [home.lon, home.lat]
|
||||
})
|
||||
).map((homeLonLat) => {
|
||||
if (homeLonLat === undefined) {
|
||||
return empty
|
||||
}
|
||||
return [
|
||||
{
|
||||
feature: {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
id: "home",
|
||||
"user:home": "yes",
|
||||
_lon: homeLonLat[0],
|
||||
_lat: homeLonLat[1],
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: homeLonLat,
|
||||
},
|
||||
},
|
||||
freshness: new Date(),
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const flayer = this.filteredLayers.data.filter((l) => l.layerDef.id === "home_location")[0]
|
||||
if (flayer !== undefined) {
|
||||
this.homeLocation = new TiledStaticFeatureSource(feature, flayer)
|
||||
}
|
||||
}
|
||||
|
||||
private static getPref(
|
||||
osmConnection: OsmConnection,
|
||||
key: string,
|
||||
|
|
|
@ -1,21 +1,17 @@
|
|||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { OsmConnection } from "../Osm/OsmConnection"
|
||||
import { MangroveIdentity } from "../Web/MangroveReviews"
|
||||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import { QueryParameters } from "../Web/QueryParameters"
|
||||
import { Store, Stores, UIEventSource } from "../UIEventSource"
|
||||
import Locale from "../../UI/i18n/Locale"
|
||||
import ElementsState from "./ElementsState"
|
||||
import SelectedElementTagsUpdater from "../Actors/SelectedElementTagsUpdater"
|
||||
import { Changes } from "../Osm/Changes"
|
||||
import ChangeToElementsActor from "../Actors/ChangeToElementsActor"
|
||||
import PendingChangesUploader from "../Actors/PendingChangesUploader"
|
||||
import Maproulette from "../Maproulette"
|
||||
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
|
||||
import FeatureSource from "../FeatureSource/FeatureSource"
|
||||
|
||||
/**
|
||||
* The part of the state which keeps track of user-related stuff, e.g. the OSM-connection,
|
||||
* which layers they enabled, ...
|
||||
*/
|
||||
export default class UserRelatedState extends ElementsState {
|
||||
export default class UserRelatedState {
|
||||
/**
|
||||
The user credentials
|
||||
*/
|
||||
|
@ -29,29 +25,22 @@ export default class UserRelatedState extends ElementsState {
|
|||
*/
|
||||
public mangroveIdentity: MangroveIdentity
|
||||
|
||||
/**
|
||||
* Maproulette connection
|
||||
*/
|
||||
public maprouletteConnection: Maproulette
|
||||
|
||||
public readonly installedUserThemes: Store<string[]>
|
||||
|
||||
public readonly showAllQuestionsAtOnce: UIEventSource<boolean>
|
||||
public readonly homeLocation: FeatureSource
|
||||
|
||||
constructor(layoutToUse: LayoutConfig, options?: { attemptLogin: true | boolean }) {
|
||||
super(layoutToUse)
|
||||
/**
|
||||
* The number of seconds that the GPS-locations are stored in memory.
|
||||
* Time in seconds
|
||||
*/
|
||||
public gpsLocationHistoryRetentionTime = new UIEventSource(
|
||||
7 * 24 * 60 * 60,
|
||||
"gps_location_retention"
|
||||
)
|
||||
|
||||
this.osmConnection = new OsmConnection({
|
||||
dryRun: this.featureSwitchIsTesting,
|
||||
fakeUser: this.featureSwitchFakeUser.data,
|
||||
oauth_token: QueryParameters.GetQueryParameter(
|
||||
"oauth_token",
|
||||
undefined,
|
||||
"Used to complete the login"
|
||||
),
|
||||
osmConfiguration: <"osm" | "osm-test">this.featureSwitchApiURL.data,
|
||||
attemptLogin: options?.attemptLogin,
|
||||
})
|
||||
constructor(osmConnection: OsmConnection, availableLanguages?: string[]) {
|
||||
this.osmConnection = osmConnection
|
||||
{
|
||||
const translationMode: UIEventSource<undefined | "true" | "false" | "mobile" | string> =
|
||||
this.osmConnection.GetPreference("translation-mode")
|
||||
|
@ -72,49 +61,22 @@ export default class UserRelatedState extends ElementsState {
|
|||
})
|
||||
}
|
||||
|
||||
this.changes = new Changes(this, layoutToUse?.isLeftRightSensitive() ?? false)
|
||||
this.showAllQuestionsAtOnce = UIEventSource.asBoolean(
|
||||
this.osmConnection.GetPreference("show-all-questions", "false", {
|
||||
documentation:
|
||||
"Either 'true' or 'false'. If set, all questions will be shown all at once",
|
||||
})
|
||||
)
|
||||
new ChangeToElementsActor(this.changes, this.allElements)
|
||||
new PendingChangesUploader(this.changes, this.selectedElement)
|
||||
|
||||
this.mangroveIdentity = new MangroveIdentity(
|
||||
this.osmConnection.GetLongPreference("identity", "mangrove")
|
||||
)
|
||||
|
||||
this.maprouletteConnection = new Maproulette()
|
||||
this.InitializeLanguage(availableLanguages)
|
||||
|
||||
if (layoutToUse?.hideFromOverview) {
|
||||
this.osmConnection.isLoggedIn.addCallbackAndRunD((loggedIn) => {
|
||||
if (loggedIn) {
|
||||
this.osmConnection
|
||||
.GetPreference("hidden-theme-" + layoutToUse?.id + "-enabled")
|
||||
.setData("true")
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (this.layoutToUse !== undefined && !this.layoutToUse.official) {
|
||||
console.log("Marking unofficial theme as visited")
|
||||
this.osmConnection.GetLongPreference("unofficial-theme-" + this.layoutToUse.id).setData(
|
||||
JSON.stringify({
|
||||
id: this.layoutToUse.id,
|
||||
icon: this.layoutToUse.icon,
|
||||
title: this.layoutToUse.title.translations,
|
||||
shortDescription: this.layoutToUse.shortDescription.translations,
|
||||
definition: this.layoutToUse["definition"],
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
this.InitializeLanguage()
|
||||
new SelectedElementTagsUpdater(this)
|
||||
this.installedUserThemes = this.InitInstalledUserThemes()
|
||||
|
||||
this.homeLocation = this.initHomeLocation()
|
||||
}
|
||||
|
||||
public GetUnofficialTheme(id: string):
|
||||
|
@ -159,26 +121,50 @@ export default class UserRelatedState extends ElementsState {
|
|||
}
|
||||
}
|
||||
|
||||
private InitializeLanguage() {
|
||||
const layoutToUse = this.layoutToUse
|
||||
public markLayoutAsVisited(layout: LayoutConfig) {
|
||||
if (!layout) {
|
||||
console.error("Trying to mark a layout as visited, but ", layout, " got passed")
|
||||
return
|
||||
}
|
||||
if (layout.hideFromOverview) {
|
||||
this.osmConnection.isLoggedIn.addCallbackAndRunD((loggedIn) => {
|
||||
if (loggedIn) {
|
||||
this.osmConnection
|
||||
.GetPreference("hidden-theme-" + layout?.id + "-enabled")
|
||||
.setData("true")
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
if (!layout.official) {
|
||||
this.osmConnection.GetLongPreference("unofficial-theme-" + layout.id).setData(
|
||||
JSON.stringify({
|
||||
id: layout.id,
|
||||
icon: layout.icon,
|
||||
title: layout.title.translations,
|
||||
shortDescription: layout.shortDescription.translations,
|
||||
definition: layout["definition"],
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private InitializeLanguage(availableLanguages?: string[]) {
|
||||
Locale.language.syncWith(this.osmConnection.GetPreference("language"))
|
||||
Locale.language.addCallback((currentLanguage) => {
|
||||
if (layoutToUse === undefined) {
|
||||
return
|
||||
}
|
||||
if (Locale.showLinkToWeblate.data) {
|
||||
return true // Disable auto switching as we are in translators mode
|
||||
}
|
||||
if (this.layoutToUse.language.indexOf(currentLanguage) < 0) {
|
||||
if (availableLanguages?.indexOf(currentLanguage) < 0) {
|
||||
console.log(
|
||||
"Resetting language to",
|
||||
layoutToUse.language[0],
|
||||
availableLanguages[0],
|
||||
"as",
|
||||
currentLanguage,
|
||||
" is unsupported"
|
||||
)
|
||||
// The current language is not supported -> switch to a supported one
|
||||
Locale.language.setData(layoutToUse.language[0])
|
||||
Locale.language.setData(availableLanguages[0])
|
||||
}
|
||||
})
|
||||
Locale.language.ping()
|
||||
|
@ -193,4 +179,43 @@ export default class UserRelatedState extends ElementsState {
|
|||
.map((k) => k.substring(prefix.length, k.length - postfix.length))
|
||||
)
|
||||
}
|
||||
|
||||
private initHomeLocation(): FeatureSource {
|
||||
const empty = []
|
||||
const feature = Stores.ListStabilized(
|
||||
this.osmConnection.userDetails.map((userDetails) => {
|
||||
if (userDetails === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const home = userDetails.home
|
||||
if (home === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return [home.lon, home.lat]
|
||||
})
|
||||
).map((homeLonLat) => {
|
||||
if (homeLonLat === undefined) {
|
||||
return empty
|
||||
}
|
||||
return [
|
||||
{
|
||||
feature: {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
id: "home",
|
||||
"user:home": "yes",
|
||||
_lon: homeLonLat[0],
|
||||
_lat: homeLonLat[1],
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: homeLonLat,
|
||||
},
|
||||
},
|
||||
freshness: new Date(),
|
||||
},
|
||||
]
|
||||
})
|
||||
return new StaticFeatureSource(feature)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue