forked from MapComplete/MapComplete
Refactoring: fix GPX-track view
This commit is contained in:
parent
4172af6a72
commit
c6e12fdd6b
23 changed files with 217 additions and 347 deletions
|
@ -4,7 +4,7 @@ import Constants from "../../Models/Constants"
|
|||
import { GeoLocationPointProperties, GeoLocationState } from "../State/GeoLocationState"
|
||||
import { UIEventSource } from "../UIEventSource"
|
||||
import { Feature, LineString, Point } from "geojson"
|
||||
import { FeatureSource } from "../FeatureSource/FeatureSource"
|
||||
import { FeatureSource, WritableFeatureSource } from "../FeatureSource/FeatureSource"
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import { GeoOperations } from "../GeoOperations"
|
||||
import { OsmTags } from "../../Models/OsmFeature"
|
||||
|
@ -27,14 +27,14 @@ export default class GeoLocationHandler {
|
|||
/**
|
||||
* All previously visited points (as 'Point'-objects), with their metadata
|
||||
*/
|
||||
public historicalUserLocations: FeatureSource
|
||||
public historicalUserLocations: WritableFeatureSource<Feature<Point>>
|
||||
|
||||
/**
|
||||
* 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 historicalUserLocationsTrack: FeatureSource
|
||||
public readonly mapHasMoved: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
private readonly selectedElement: UIEventSource<any>
|
||||
private readonly mapProperties?: MapProperties
|
||||
|
@ -90,7 +90,7 @@ export default class GeoLocationHandler {
|
|||
geolocationState.allowMoving.syncWith(mapProperties.allowMoving, true)
|
||||
|
||||
this.CopyGeolocationIntoMapstate()
|
||||
this.initUserLocationTrail()
|
||||
this.historicalUserLocationsTrack = this.initUserLocationTrail()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -220,7 +220,7 @@ export default class GeoLocationHandler {
|
|||
features.ping()
|
||||
})
|
||||
|
||||
this.historicalUserLocations = new StaticFeatureSource(features)
|
||||
this.historicalUserLocations = <any>new StaticFeatureSource(features)
|
||||
|
||||
const asLine = features.map((allPoints) => {
|
||||
if (allPoints === undefined || allPoints.length < 2) {
|
||||
|
@ -242,6 +242,6 @@ export default class GeoLocationHandler {
|
|||
}
|
||||
return [feature]
|
||||
})
|
||||
this.historicalUserLocationsTrack = new StaticFeatureSource(asLine)
|
||||
return new StaticFeatureSource(asLine)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesSto
|
|||
import { Feature } from "geojson"
|
||||
import { OsmTags } from "../../Models/OsmFeature"
|
||||
import OsmObjectDownloader from "../Osm/OsmObjectDownloader"
|
||||
import { IndexedFeatureSource } from "../FeatureSource/FeatureSource"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
export default class SelectedElementTagsUpdater {
|
||||
private static readonly metatags = new Set([
|
||||
|
@ -28,11 +30,13 @@ export default class SelectedElementTagsUpdater {
|
|||
osmConnection: OsmConnection
|
||||
layout: LayoutConfig
|
||||
osmObjectDownloader: OsmObjectDownloader
|
||||
indexedFeatures: IndexedFeatureSource
|
||||
}
|
||||
|
||||
constructor(state: {
|
||||
selectedElement: UIEventSource<Feature>
|
||||
featureProperties: FeaturePropertiesStore
|
||||
indexedFeatures: IndexedFeatureSource
|
||||
changes: Changes
|
||||
osmConnection: OsmConnection
|
||||
layout: LayoutConfig
|
||||
|
@ -82,7 +86,16 @@ export default class SelectedElementTagsUpdater {
|
|||
return
|
||||
}
|
||||
const latestTags = osmObject.tags
|
||||
const newGeometry = osmObject.asGeoJson()?.geometry
|
||||
const oldFeature = state.indexedFeatures.featuresById.data.get(id)
|
||||
const oldGeometry = oldFeature?.geometry
|
||||
if (oldGeometry !== undefined && !Utils.SameObject(newGeometry, oldGeometry)) {
|
||||
console.log("Detected a difference in geometry for ", id)
|
||||
oldFeature.geometry = newGeometry
|
||||
state.featureProperties.getStore(id)?.ping()
|
||||
}
|
||||
this.applyUpdate(latestTags, id)
|
||||
|
||||
console.log("Updated", id)
|
||||
} catch (e) {
|
||||
console.warn("Could not update", id, " due to", e)
|
||||
|
|
|
@ -5,11 +5,19 @@ import { UIEventSource } from "../../UIEventSource"
|
|||
* Constructs a UIEventStore for the properties of every Feature, indexed by id
|
||||
*/
|
||||
export default class FeaturePropertiesStore {
|
||||
private readonly _source: FeatureSource & IndexedFeatureSource
|
||||
private readonly _elements = new Map<string, UIEventSource<Record<string, string>>>()
|
||||
|
||||
constructor(source: FeatureSource & IndexedFeatureSource) {
|
||||
this._source = source
|
||||
constructor(...sources: FeatureSource[]) {
|
||||
for (const source of sources) {
|
||||
this.trackFeatureSource(source)
|
||||
}
|
||||
}
|
||||
|
||||
public getStore(id: string): UIEventSource<Record<string, string>> {
|
||||
return this._elements.get(id)
|
||||
}
|
||||
|
||||
public trackFeatureSource(source: FeatureSource) {
|
||||
const self = this
|
||||
source.features.addCallbackAndRunD((features) => {
|
||||
console.log("Re-indexing features")
|
||||
|
@ -41,14 +49,6 @@ export default class FeaturePropertiesStore {
|
|||
})
|
||||
}
|
||||
|
||||
public getStore(id: string): UIEventSource<Record<string, string>> {
|
||||
return this._elements.get(id)
|
||||
}
|
||||
|
||||
public addSpecial(id: string, store: UIEventSource<Record<string, string>>) {
|
||||
this._elements.set(id, store)
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrites the tags of the old properties object, returns true if a change was made.
|
||||
* Metatags are overriden if they are in the new properties, but not removed
|
||||
|
@ -87,7 +87,6 @@ export default class FeaturePropertiesStore {
|
|||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
public addAlias(oldId: string, newId: string): void {
|
||||
console.log("FeaturePropertiesStore: adding alias for", oldId, newId)
|
||||
if (newId === undefined) {
|
||||
// We removed the node/way/relation with type 'type' and id 'oldId' on openstreetmap!
|
||||
const element = this._elements.get(oldId)
|
||||
|
|
|
@ -3,11 +3,11 @@ import FilteredLayer from "../../Models/FilteredLayer"
|
|||
import { BBox } from "../BBox"
|
||||
import { Feature } from "geojson"
|
||||
|
||||
export interface FeatureSource {
|
||||
features: Store<Feature[]>
|
||||
export interface FeatureSource<T extends Feature = Feature> {
|
||||
features: Store<T[]>
|
||||
}
|
||||
export interface WritableFeatureSource extends FeatureSource {
|
||||
features: UIEventSource<Feature[]>
|
||||
export interface WritableFeatureSource<T extends Feature = Feature> extends FeatureSource<T> {
|
||||
features: UIEventSource<T[]>
|
||||
}
|
||||
|
||||
export interface Tiled {
|
||||
|
|
|
@ -61,7 +61,7 @@ export default class FilteringFeatureSource implements FeatureSource {
|
|||
const includedFeatureIds = new Set<string>()
|
||||
const globalFilters = self._globalFilters?.data?.map((f) => f)
|
||||
const newFeatures = (features ?? []).filter((f) => {
|
||||
self.registerCallback(f)
|
||||
self.registerCallback(f.properties.id)
|
||||
|
||||
if (!layer.isShown(f.properties, globalFilters)) {
|
||||
return false
|
||||
|
@ -91,11 +91,11 @@ export default class FilteringFeatureSource implements FeatureSource {
|
|||
this.features.setData(newFeatures)
|
||||
}
|
||||
|
||||
private registerCallback(feature: any) {
|
||||
private registerCallback(featureId: string) {
|
||||
if (this._fetchStore === undefined) {
|
||||
return
|
||||
}
|
||||
const src = this._fetchStore(feature)
|
||||
const src = this._fetchStore(featureId)
|
||||
if (src == undefined) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { BBox } from "./BBox"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import * as turf from "@turf/turf"
|
||||
import { AllGeoJSON, booleanWithin, Coord, Lines } from "@turf/turf"
|
||||
import { AllGeoJSON, booleanWithin, Coord } from "@turf/turf"
|
||||
import {
|
||||
Feature,
|
||||
FeatureCollection,
|
||||
|
@ -14,9 +13,8 @@ import {
|
|||
Polygon,
|
||||
Position,
|
||||
} from "geojson"
|
||||
import togpx from "togpx"
|
||||
import Constants from "../Models/Constants"
|
||||
import { Tiles } from "../Models/TileRange"
|
||||
import { Utils } from "../Utils"
|
||||
|
||||
export class GeoOperations {
|
||||
private static readonly _earthRadius = 6378137
|
||||
|
@ -416,30 +414,55 @@ export class GeoOperations {
|
|||
.features.map((p) => <[number, number]>p.geometry.coordinates)
|
||||
}
|
||||
|
||||
public static AsGpx(
|
||||
feature: Feature,
|
||||
options?: { layer?: LayerConfig; gpxMetadata?: any }
|
||||
): string {
|
||||
const metadata = options?.gpxMetadata ?? {}
|
||||
metadata["time"] = metadata["time"] ?? new Date().toISOString()
|
||||
const tags = feature.properties
|
||||
|
||||
if (options?.layer !== undefined) {
|
||||
metadata["name"] = options?.layer.title?.GetRenderValue(tags)?.Subs(tags)?.txt
|
||||
metadata["desc"] = "Generated with MapComplete layer " + options?.layer.id
|
||||
if (tags._backend?.contains("openstreetmap")) {
|
||||
metadata["copyright"] =
|
||||
"Data copyrighted by OpenStreetMap-contributors, freely available under ODbL. See https://www.openstreetmap.org/copyright"
|
||||
metadata["author"] = tags["_last_edit:contributor"]
|
||||
metadata["link"] = "https://www.openstreetmap.org/" + tags.id
|
||||
metadata["time"] = tags["_last_edit:timestamp"]
|
||||
}
|
||||
public static toGpx(
|
||||
locations:
|
||||
| Feature<LineString>
|
||||
| Feature<Point, { date?: string; altitude?: number | string }>[],
|
||||
title?: string
|
||||
) {
|
||||
title = title?.trim()
|
||||
if (title === undefined || title === "") {
|
||||
title = "Uploaded with MapComplete"
|
||||
}
|
||||
|
||||
return togpx(feature, {
|
||||
creator: "MapComplete " + Constants.vNumber,
|
||||
metadata,
|
||||
})
|
||||
title = Utils.EncodeXmlValue(title)
|
||||
const trackPoints: string[] = []
|
||||
let locationsWithMeta: Feature<Point, { date?: string; altitude?: number | string }>[]
|
||||
if (Array.isArray(locations)) {
|
||||
locationsWithMeta = locations
|
||||
} else {
|
||||
locationsWithMeta = locations.geometry.coordinates.map(
|
||||
(p) =>
|
||||
<Feature<Point>>{
|
||||
type: "Feature",
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: p,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
for (const l of locationsWithMeta) {
|
||||
let trkpt = ` <trkpt lat="${l.geometry.coordinates[1]}" lon="${l.geometry.coordinates[0]}">`
|
||||
if (l.properties.date) {
|
||||
trkpt += ` <time>${l.properties.date}</time>`
|
||||
}
|
||||
if (l.properties.altitude) {
|
||||
trkpt += ` <ele>${l.properties.altitude}</ele>`
|
||||
}
|
||||
trkpt += " </trkpt>"
|
||||
trackPoints.push(trkpt)
|
||||
}
|
||||
const header =
|
||||
'<gpx version="1.1" creator="MapComplete.osm.be" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'
|
||||
return (
|
||||
header +
|
||||
"\n<name>" +
|
||||
title +
|
||||
"</name>\n<trk><trkseg>\n" +
|
||||
trackPoints.join("\n") +
|
||||
"\n</trkseg></trk></gpx>"
|
||||
)
|
||||
}
|
||||
|
||||
public static IdentifieCommonSegments(coordinatess: [number, number][][]): {
|
||||
|
@ -807,6 +830,31 @@ export class GeoOperations {
|
|||
return tiles
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a linestring object based on the outer ring of the given polygon
|
||||
*
|
||||
* Returns the argument if not a polygon
|
||||
* @param p
|
||||
*/
|
||||
public static outerRing<P>(p: Feature<Polygon | LineString, P>): Feature<LineString, P> {
|
||||
if (p.geometry.type !== "Polygon") {
|
||||
return <Feature<LineString, P>>p
|
||||
}
|
||||
return {
|
||||
type: "Feature",
|
||||
properties: p.properties,
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: p.geometry.coordinates[0],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
static centerpointCoordinatesObj(geojson: Feature) {
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(geojson)
|
||||
return { lon, lat }
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function which does the heavy lifting for 'inside'
|
||||
*/
|
||||
|
@ -956,29 +1004,4 @@ export class GeoOperations {
|
|||
}
|
||||
throw "CalculateIntersection fallthrough: can not calculate an intersection between features"
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a linestring object based on the outer ring of the given polygon
|
||||
*
|
||||
* Returns the argument if not a polygon
|
||||
* @param p
|
||||
*/
|
||||
public static outerRing<P>(p: Feature<Polygon | LineString, P>): Feature<LineString, P> {
|
||||
if (p.geometry.type !== "Polygon") {
|
||||
return <Feature<LineString, P>>p
|
||||
}
|
||||
return {
|
||||
type: "Feature",
|
||||
properties: p.properties,
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: p.geometry.coordinates[0],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
static centerpointCoordinatesObj(geojson: Feature) {
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(geojson)
|
||||
return { lon, lat }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import SimpleMetaTaggers, { SimpleMetaTagger } from "./SimpleMetaTagger"
|
||||
import SimpleMetaTaggers, { MetataggingState, SimpleMetaTagger } from "./SimpleMetaTagger"
|
||||
import { ExtraFuncParams, ExtraFunctions } from "./ExtraFunctions"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import { Feature } from "geojson"
|
||||
|
@ -6,6 +6,7 @@ import FeaturePropertiesStore from "./FeatureSource/Actors/FeaturePropertiesStor
|
|||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
||||
import { GeoIndexedStoreForLayer } from "./FeatureSource/Actors/GeoIndexedStore"
|
||||
import { IndexedFeatureSource } from "./FeatureSource/FeatureSource"
|
||||
import OsmObjectDownloader from "./Osm/OsmObjectDownloader"
|
||||
|
||||
/**
|
||||
* Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ...
|
||||
|
@ -19,6 +20,7 @@ export default class MetaTagging {
|
|||
|
||||
constructor(state: {
|
||||
layout: LayoutConfig
|
||||
osmObjectDownloader: OsmObjectDownloader
|
||||
perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
|
||||
indexedFeatures: IndexedFeatureSource
|
||||
featureProperties: FeaturePropertiesStore
|
||||
|
@ -39,6 +41,7 @@ export default class MetaTagging {
|
|||
params,
|
||||
layer,
|
||||
state.layout,
|
||||
state.osmObjectDownloader,
|
||||
state.featureProperties
|
||||
)
|
||||
})
|
||||
|
@ -56,6 +59,7 @@ export default class MetaTagging {
|
|||
params: ExtraFuncParams,
|
||||
layer: LayerConfig,
|
||||
layout: LayoutConfig,
|
||||
osmObjectDownloader: OsmObjectDownloader,
|
||||
featurePropertiesStores?: FeaturePropertiesStore,
|
||||
options?: {
|
||||
includeDates?: true | boolean
|
||||
|
@ -83,7 +87,7 @@ export default class MetaTagging {
|
|||
|
||||
// The calculated functions - per layer - which add the new keys
|
||||
const layerFuncs = this.createRetaggingFunc(layer)
|
||||
const state = { layout }
|
||||
const state: MetataggingState = { layout, osmObjectDownloader }
|
||||
|
||||
let atLeastOneFeatureChanged = false
|
||||
|
||||
|
|
|
@ -1,133 +0,0 @@
|
|||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"
|
||||
import { QueryParameters } from "../Web/QueryParameters"
|
||||
import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer"
|
||||
import { FeatureSource, FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource"
|
||||
import StaticFeatureSource, {
|
||||
TiledStaticFeatureSource,
|
||||
} from "../FeatureSource/Sources/StaticFeatureSource"
|
||||
import { Feature } from "geojson"
|
||||
import { MapProperties } from "../../Models/MapProperties"
|
||||
|
||||
/**
|
||||
* Contains all the leaflet-map related state
|
||||
*/
|
||||
export default class MapState {
|
||||
/**
|
||||
* Last location where a click was registered
|
||||
*/
|
||||
public readonly LastClickLocation: UIEventSource<{
|
||||
lat: number
|
||||
lon: number
|
||||
}> = new UIEventSource<{ lat: number; lon: number }>(undefined)
|
||||
|
||||
/**
|
||||
* The bounds of the current map view
|
||||
*/
|
||||
public currentView: FeatureSourceForLayer & Tiled
|
||||
|
||||
/**
|
||||
* A builtin layer which contains the selected element.
|
||||
* Loads 'selected_element.json'
|
||||
* This _might_ contain multiple points, e.g. every center of a multipolygon
|
||||
*/
|
||||
public selectedElementsLayer: FeatureSourceForLayer & Tiled
|
||||
|
||||
/**
|
||||
* Which overlays are shown
|
||||
*/
|
||||
public overlayToggles: { config: TilesourceConfig; isDisplayed: UIEventSource<boolean> }[]
|
||||
|
||||
constructor() {
|
||||
this.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(this.locationControl)
|
||||
|
||||
let defaultLayer = AvailableBaseLayers.osmCarto
|
||||
const available = this.availableBackgroundLayers.data
|
||||
for (const layer of available) {
|
||||
if (this.backgroundLayerId.data === layer.id) {
|
||||
defaultLayer = layer
|
||||
}
|
||||
}
|
||||
const self = this
|
||||
this.backgroundLayer = new UIEventSource<BaseLayer>(defaultLayer)
|
||||
this.backgroundLayer.addCallbackAndRunD((layer) => self.backgroundLayerId.setData(layer.id))
|
||||
|
||||
this.overlayToggles =
|
||||
this.layoutToUse?.tileLayerSources
|
||||
?.filter((c) => c.name !== undefined)
|
||||
?.map((c) => ({
|
||||
config: c,
|
||||
isDisplayed: QueryParameters.GetBooleanQueryParameter(
|
||||
"overlay-" + c.id,
|
||||
c.defaultState,
|
||||
"Wether or not the overlay " + c.id + " is shown"
|
||||
),
|
||||
})) ?? []
|
||||
|
||||
this.AddAllOverlaysToMap(this.leafletMap)
|
||||
|
||||
this.initCurrentView()
|
||||
this.initSelectedElement()
|
||||
}
|
||||
|
||||
public AddAllOverlaysToMap(leafletMap: UIEventSource<any>) {
|
||||
const initialized = new Set()
|
||||
for (const overlayToggle of this.overlayToggles) {
|
||||
new ShowOverlayLayer(overlayToggle.config, leafletMap, overlayToggle.isDisplayed)
|
||||
initialized.add(overlayToggle.config)
|
||||
}
|
||||
|
||||
for (const tileLayerSource of this.layoutToUse?.tileLayerSources ?? []) {
|
||||
if (initialized.has(tileLayerSource)) {
|
||||
continue
|
||||
}
|
||||
new ShowOverlayLayer(tileLayerSource, leafletMap)
|
||||
}
|
||||
}
|
||||
|
||||
private static initCurrentView(mapproperties: MapProperties): FeatureSource {
|
||||
let i = 0
|
||||
const features: Store<Feature[]> = mapproperties.bounds.map((bounds) => {
|
||||
if (bounds === undefined) {
|
||||
return []
|
||||
}
|
||||
i++
|
||||
return [
|
||||
bounds.asGeoJson({
|
||||
id: "current_view-" + i,
|
||||
current_view: "yes",
|
||||
zoom: "" + mapproperties.zoom.data,
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
||||
return new StaticFeatureSource(features)
|
||||
}
|
||||
|
||||
private initSelectedElement() {
|
||||
const layerDef: FilteredLayer = this.filteredLayers.data.filter(
|
||||
(l) => l.layerDef.id === "selected_element"
|
||||
)[0]
|
||||
const empty = []
|
||||
const store = this.selectedElement.map((feature) => {
|
||||
if (feature === undefined || feature === null) {
|
||||
return empty
|
||||
}
|
||||
return [
|
||||
{
|
||||
feature: {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
selected: "yes",
|
||||
id: "selected" + feature.properties.id,
|
||||
},
|
||||
geometry: feature.geometry,
|
||||
},
|
||||
freshness: new Date(),
|
||||
},
|
||||
]
|
||||
})
|
||||
this.selectedElementsLayer = new TiledStaticFeatureSource(store, layerDef)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue