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 { GeoLocationPointProperties, GeoLocationState } from "../State/GeoLocationState"
|
||||||
import { UIEventSource } from "../UIEventSource"
|
import { UIEventSource } from "../UIEventSource"
|
||||||
import { Feature, LineString, Point } from "geojson"
|
import { Feature, LineString, Point } from "geojson"
|
||||||
import { FeatureSource } from "../FeatureSource/FeatureSource"
|
import { FeatureSource, WritableFeatureSource } from "../FeatureSource/FeatureSource"
|
||||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||||
import { GeoOperations } from "../GeoOperations"
|
import { GeoOperations } from "../GeoOperations"
|
||||||
import { OsmTags } from "../../Models/OsmFeature"
|
import { OsmTags } from "../../Models/OsmFeature"
|
||||||
|
@ -27,14 +27,14 @@ export default class GeoLocationHandler {
|
||||||
/**
|
/**
|
||||||
* All previously visited points (as 'Point'-objects), with their metadata
|
* 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.
|
* 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`.
|
* 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'
|
* Note that this featureSource is _derived_ from 'historicalUserLocations'
|
||||||
*/
|
*/
|
||||||
public historicalUserLocationsTrack: FeatureSource
|
public readonly historicalUserLocationsTrack: FeatureSource
|
||||||
public readonly mapHasMoved: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
public readonly mapHasMoved: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||||
private readonly selectedElement: UIEventSource<any>
|
private readonly selectedElement: UIEventSource<any>
|
||||||
private readonly mapProperties?: MapProperties
|
private readonly mapProperties?: MapProperties
|
||||||
|
@ -90,7 +90,7 @@ export default class GeoLocationHandler {
|
||||||
geolocationState.allowMoving.syncWith(mapProperties.allowMoving, true)
|
geolocationState.allowMoving.syncWith(mapProperties.allowMoving, true)
|
||||||
|
|
||||||
this.CopyGeolocationIntoMapstate()
|
this.CopyGeolocationIntoMapstate()
|
||||||
this.initUserLocationTrail()
|
this.historicalUserLocationsTrack = this.initUserLocationTrail()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -220,7 +220,7 @@ export default class GeoLocationHandler {
|
||||||
features.ping()
|
features.ping()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.historicalUserLocations = new StaticFeatureSource(features)
|
this.historicalUserLocations = <any>new StaticFeatureSource(features)
|
||||||
|
|
||||||
const asLine = features.map((allPoints) => {
|
const asLine = features.map((allPoints) => {
|
||||||
if (allPoints === undefined || allPoints.length < 2) {
|
if (allPoints === undefined || allPoints.length < 2) {
|
||||||
|
@ -242,6 +242,6 @@ export default class GeoLocationHandler {
|
||||||
}
|
}
|
||||||
return [feature]
|
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 { Feature } from "geojson"
|
||||||
import { OsmTags } from "../../Models/OsmFeature"
|
import { OsmTags } from "../../Models/OsmFeature"
|
||||||
import OsmObjectDownloader from "../Osm/OsmObjectDownloader"
|
import OsmObjectDownloader from "../Osm/OsmObjectDownloader"
|
||||||
|
import { IndexedFeatureSource } from "../FeatureSource/FeatureSource"
|
||||||
|
import { Utils } from "../../Utils"
|
||||||
|
|
||||||
export default class SelectedElementTagsUpdater {
|
export default class SelectedElementTagsUpdater {
|
||||||
private static readonly metatags = new Set([
|
private static readonly metatags = new Set([
|
||||||
|
@ -28,11 +30,13 @@ export default class SelectedElementTagsUpdater {
|
||||||
osmConnection: OsmConnection
|
osmConnection: OsmConnection
|
||||||
layout: LayoutConfig
|
layout: LayoutConfig
|
||||||
osmObjectDownloader: OsmObjectDownloader
|
osmObjectDownloader: OsmObjectDownloader
|
||||||
|
indexedFeatures: IndexedFeatureSource
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(state: {
|
constructor(state: {
|
||||||
selectedElement: UIEventSource<Feature>
|
selectedElement: UIEventSource<Feature>
|
||||||
featureProperties: FeaturePropertiesStore
|
featureProperties: FeaturePropertiesStore
|
||||||
|
indexedFeatures: IndexedFeatureSource
|
||||||
changes: Changes
|
changes: Changes
|
||||||
osmConnection: OsmConnection
|
osmConnection: OsmConnection
|
||||||
layout: LayoutConfig
|
layout: LayoutConfig
|
||||||
|
@ -82,7 +86,16 @@ export default class SelectedElementTagsUpdater {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const latestTags = osmObject.tags
|
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)
|
this.applyUpdate(latestTags, id)
|
||||||
|
|
||||||
console.log("Updated", id)
|
console.log("Updated", id)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Could not update", id, " due to", 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
|
* Constructs a UIEventStore for the properties of every Feature, indexed by id
|
||||||
*/
|
*/
|
||||||
export default class FeaturePropertiesStore {
|
export default class FeaturePropertiesStore {
|
||||||
private readonly _source: FeatureSource & IndexedFeatureSource
|
|
||||||
private readonly _elements = new Map<string, UIEventSource<Record<string, string>>>()
|
private readonly _elements = new Map<string, UIEventSource<Record<string, string>>>()
|
||||||
|
|
||||||
constructor(source: FeatureSource & IndexedFeatureSource) {
|
constructor(...sources: FeatureSource[]) {
|
||||||
this._source = source
|
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
|
const self = this
|
||||||
source.features.addCallbackAndRunD((features) => {
|
source.features.addCallbackAndRunD((features) => {
|
||||||
console.log("Re-indexing 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.
|
* 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
|
* Metatags are overriden if they are in the new properties, but not removed
|
||||||
|
@ -87,7 +87,6 @@ export default class FeaturePropertiesStore {
|
||||||
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
// noinspection JSUnusedGlobalSymbols
|
||||||
public addAlias(oldId: string, newId: string): void {
|
public addAlias(oldId: string, newId: string): void {
|
||||||
console.log("FeaturePropertiesStore: adding alias for", oldId, newId)
|
|
||||||
if (newId === undefined) {
|
if (newId === undefined) {
|
||||||
// We removed the node/way/relation with type 'type' and id 'oldId' on openstreetmap!
|
// We removed the node/way/relation with type 'type' and id 'oldId' on openstreetmap!
|
||||||
const element = this._elements.get(oldId)
|
const element = this._elements.get(oldId)
|
||||||
|
|
|
@ -3,11 +3,11 @@ import FilteredLayer from "../../Models/FilteredLayer"
|
||||||
import { BBox } from "../BBox"
|
import { BBox } from "../BBox"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
|
|
||||||
export interface FeatureSource {
|
export interface FeatureSource<T extends Feature = Feature> {
|
||||||
features: Store<Feature[]>
|
features: Store<T[]>
|
||||||
}
|
}
|
||||||
export interface WritableFeatureSource extends FeatureSource {
|
export interface WritableFeatureSource<T extends Feature = Feature> extends FeatureSource<T> {
|
||||||
features: UIEventSource<Feature[]>
|
features: UIEventSource<T[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Tiled {
|
export interface Tiled {
|
||||||
|
|
|
@ -61,7 +61,7 @@ export default class FilteringFeatureSource implements FeatureSource {
|
||||||
const includedFeatureIds = new Set<string>()
|
const includedFeatureIds = new Set<string>()
|
||||||
const globalFilters = self._globalFilters?.data?.map((f) => f)
|
const globalFilters = self._globalFilters?.data?.map((f) => f)
|
||||||
const newFeatures = (features ?? []).filter((f) => {
|
const newFeatures = (features ?? []).filter((f) => {
|
||||||
self.registerCallback(f)
|
self.registerCallback(f.properties.id)
|
||||||
|
|
||||||
if (!layer.isShown(f.properties, globalFilters)) {
|
if (!layer.isShown(f.properties, globalFilters)) {
|
||||||
return false
|
return false
|
||||||
|
@ -91,11 +91,11 @@ export default class FilteringFeatureSource implements FeatureSource {
|
||||||
this.features.setData(newFeatures)
|
this.features.setData(newFeatures)
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerCallback(feature: any) {
|
private registerCallback(featureId: string) {
|
||||||
if (this._fetchStore === undefined) {
|
if (this._fetchStore === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const src = this._fetchStore(feature)
|
const src = this._fetchStore(featureId)
|
||||||
if (src == undefined) {
|
if (src == undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { BBox } from "./BBox"
|
import { BBox } from "./BBox"
|
||||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
|
||||||
import * as turf from "@turf/turf"
|
import * as turf from "@turf/turf"
|
||||||
import { AllGeoJSON, booleanWithin, Coord, Lines } from "@turf/turf"
|
import { AllGeoJSON, booleanWithin, Coord } from "@turf/turf"
|
||||||
import {
|
import {
|
||||||
Feature,
|
Feature,
|
||||||
FeatureCollection,
|
FeatureCollection,
|
||||||
|
@ -14,9 +13,8 @@ import {
|
||||||
Polygon,
|
Polygon,
|
||||||
Position,
|
Position,
|
||||||
} from "geojson"
|
} from "geojson"
|
||||||
import togpx from "togpx"
|
|
||||||
import Constants from "../Models/Constants"
|
|
||||||
import { Tiles } from "../Models/TileRange"
|
import { Tiles } from "../Models/TileRange"
|
||||||
|
import { Utils } from "../Utils"
|
||||||
|
|
||||||
export class GeoOperations {
|
export class GeoOperations {
|
||||||
private static readonly _earthRadius = 6378137
|
private static readonly _earthRadius = 6378137
|
||||||
|
@ -416,30 +414,55 @@ export class GeoOperations {
|
||||||
.features.map((p) => <[number, number]>p.geometry.coordinates)
|
.features.map((p) => <[number, number]>p.geometry.coordinates)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AsGpx(
|
public static toGpx(
|
||||||
feature: Feature,
|
locations:
|
||||||
options?: { layer?: LayerConfig; gpxMetadata?: any }
|
| Feature<LineString>
|
||||||
): string {
|
| Feature<Point, { date?: string; altitude?: number | string }>[],
|
||||||
const metadata = options?.gpxMetadata ?? {}
|
title?: string
|
||||||
metadata["time"] = metadata["time"] ?? new Date().toISOString()
|
) {
|
||||||
const tags = feature.properties
|
title = title?.trim()
|
||||||
|
if (title === undefined || title === "") {
|
||||||
if (options?.layer !== undefined) {
|
title = "Uploaded with MapComplete"
|
||||||
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"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
title = Utils.EncodeXmlValue(title)
|
||||||
return togpx(feature, {
|
const trackPoints: string[] = []
|
||||||
creator: "MapComplete " + Constants.vNumber,
|
let locationsWithMeta: Feature<Point, { date?: string; altitude?: number | string }>[]
|
||||||
metadata,
|
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][][]): {
|
public static IdentifieCommonSegments(coordinatess: [number, number][][]): {
|
||||||
|
@ -807,6 +830,31 @@ export class GeoOperations {
|
||||||
return tiles
|
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'
|
* 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"
|
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 { ExtraFuncParams, ExtraFunctions } from "./ExtraFunctions"
|
||||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
|
@ -6,6 +6,7 @@ import FeaturePropertiesStore from "./FeatureSource/Actors/FeaturePropertiesStor
|
||||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
||||||
import { GeoIndexedStoreForLayer } from "./FeatureSource/Actors/GeoIndexedStore"
|
import { GeoIndexedStoreForLayer } from "./FeatureSource/Actors/GeoIndexedStore"
|
||||||
import { IndexedFeatureSource } from "./FeatureSource/FeatureSource"
|
import { IndexedFeatureSource } from "./FeatureSource/FeatureSource"
|
||||||
|
import OsmObjectDownloader from "./Osm/OsmObjectDownloader"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ...
|
* Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ...
|
||||||
|
@ -19,6 +20,7 @@ export default class MetaTagging {
|
||||||
|
|
||||||
constructor(state: {
|
constructor(state: {
|
||||||
layout: LayoutConfig
|
layout: LayoutConfig
|
||||||
|
osmObjectDownloader: OsmObjectDownloader
|
||||||
perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
|
perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
|
||||||
indexedFeatures: IndexedFeatureSource
|
indexedFeatures: IndexedFeatureSource
|
||||||
featureProperties: FeaturePropertiesStore
|
featureProperties: FeaturePropertiesStore
|
||||||
|
@ -39,6 +41,7 @@ export default class MetaTagging {
|
||||||
params,
|
params,
|
||||||
layer,
|
layer,
|
||||||
state.layout,
|
state.layout,
|
||||||
|
state.osmObjectDownloader,
|
||||||
state.featureProperties
|
state.featureProperties
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -56,6 +59,7 @@ export default class MetaTagging {
|
||||||
params: ExtraFuncParams,
|
params: ExtraFuncParams,
|
||||||
layer: LayerConfig,
|
layer: LayerConfig,
|
||||||
layout: LayoutConfig,
|
layout: LayoutConfig,
|
||||||
|
osmObjectDownloader: OsmObjectDownloader,
|
||||||
featurePropertiesStores?: FeaturePropertiesStore,
|
featurePropertiesStores?: FeaturePropertiesStore,
|
||||||
options?: {
|
options?: {
|
||||||
includeDates?: true | boolean
|
includeDates?: true | boolean
|
||||||
|
@ -83,7 +87,7 @@ export default class MetaTagging {
|
||||||
|
|
||||||
// The calculated functions - per layer - which add the new keys
|
// The calculated functions - per layer - which add the new keys
|
||||||
const layerFuncs = this.createRetaggingFunc(layer)
|
const layerFuncs = this.createRetaggingFunc(layer)
|
||||||
const state = { layout }
|
const state: MetataggingState = { layout, osmObjectDownloader }
|
||||||
|
|
||||||
let atLeastOneFeatureChanged = false
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -891,7 +891,7 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
|
||||||
throw "A special layer cannot have presets"
|
throw "A special layer cannot have presets"
|
||||||
}
|
}
|
||||||
// Check that a preset will be picked up by the layer itself
|
// Check that a preset will be picked up by the layer itself
|
||||||
const baseTags = TagUtils.Tag(json.source.osmTags)
|
const baseTags = TagUtils.Tag(json.source["osmTags"])
|
||||||
for (let i = 0; i < json.presets.length; i++) {
|
for (let i = 0; i < json.presets.length; i++) {
|
||||||
const preset = json.presets[i]
|
const preset = json.presets[i]
|
||||||
const tags: { k: string; v: string }[] = new And(
|
const tags: { k: string; v: string }[] = new And(
|
||||||
|
|
|
@ -618,17 +618,26 @@ export default class LayerConfig extends WithContextLoader {
|
||||||
filterDocs.push(new Title("Filters", 4))
|
filterDocs.push(new Title("Filters", 4))
|
||||||
filterDocs.push(...this.filters.map((filter) => filter.GenerateDocs()))
|
filterDocs.push(...this.filters.map((filter) => filter.GenerateDocs()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tagsDescription = []
|
||||||
|
if (this.source === null) {
|
||||||
|
tagsDescription.push(
|
||||||
|
new Title("Basic tags for this layer", 2),
|
||||||
|
"Elements must have the all of following tags to be shown on this layer:",
|
||||||
|
new List(neededTags.map((t) => t.asHumanString(true, false, {}))),
|
||||||
|
overpassLink
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
tagsDescription.push("This is a special layer - data is not sourced from OpenStreetMap")
|
||||||
|
}
|
||||||
|
|
||||||
return new Combine([
|
return new Combine([
|
||||||
new Combine([new Title(this.id, 1), iconImg, this.description, "\n"]).SetClass(
|
new Combine([new Title(this.id, 1), iconImg, this.description, "\n"]).SetClass(
|
||||||
"flex flex-col"
|
"flex flex-col"
|
||||||
),
|
),
|
||||||
new List(extraProps),
|
new List(extraProps),
|
||||||
...usingLayer,
|
...usingLayer,
|
||||||
|
...tagsDescription,
|
||||||
new Title("Basic tags for this layer", 2),
|
|
||||||
"Elements must have the all of following tags to be shown on this layer:",
|
|
||||||
new List(neededTags.map((t) => t.asHumanString(true, false, {}))),
|
|
||||||
overpassLink,
|
|
||||||
new Title("Supported attributes", 2),
|
new Title("Supported attributes", 2),
|
||||||
quickOverview,
|
quickOverview,
|
||||||
...this.tagRenderings.map((tr) => tr.GenerateDocumentation()),
|
...this.tagRenderings.map((tr) => tr.GenerateDocumentation()),
|
||||||
|
|
|
@ -291,6 +291,9 @@ export default class LayoutConfig implements LayoutInformation {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
for (const layer of this.layers) {
|
for (const layer of this.layers) {
|
||||||
|
if (!layer.source) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (layer.source.osmTags.matchesProperties(tags)) {
|
if (layer.source.osmTags.matchesProperties(tags)) {
|
||||||
return layer
|
return layer
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||||
import { ExportableMap, MapProperties } from "./MapProperties"
|
import { ExportableMap, MapProperties } from "./MapProperties"
|
||||||
import LayerState from "../Logic/State/LayerState"
|
import LayerState from "../Logic/State/LayerState"
|
||||||
import { Feature } from "geojson"
|
import { Feature, Point } from "geojson"
|
||||||
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
||||||
import { Map as MlMap } from "maplibre-gl"
|
import { Map as MlMap } from "maplibre-gl"
|
||||||
import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning"
|
import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning"
|
||||||
|
@ -42,7 +42,7 @@ import { MenuState } from "./MenuState"
|
||||||
import MetaTagging from "../Logic/MetaTagging"
|
import MetaTagging from "../Logic/MetaTagging"
|
||||||
import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator"
|
import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator"
|
||||||
import { NewGeometryFromChangesFeatureSource } from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource"
|
import { NewGeometryFromChangesFeatureSource } from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource"
|
||||||
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader";
|
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -71,8 +71,8 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
readonly guistate: MenuState
|
readonly guistate: MenuState
|
||||||
readonly fullNodeDatabase?: FullNodeDatabaseSource // TODO
|
readonly fullNodeDatabase?: FullNodeDatabaseSource // TODO
|
||||||
|
|
||||||
readonly historicalUserLocations: WritableFeatureSource
|
readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>
|
||||||
readonly indexedFeatures: IndexedFeatureSource
|
readonly indexedFeatures: IndexedFeatureSource & LayoutSource
|
||||||
readonly newFeatures: WritableFeatureSource
|
readonly newFeatures: WritableFeatureSource
|
||||||
readonly layerState: LayerState
|
readonly layerState: LayerState
|
||||||
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
|
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
|
||||||
|
@ -152,6 +152,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
},
|
},
|
||||||
layout?.isLeftRightSensitive() ?? false
|
layout?.isLeftRightSensitive() ?? false
|
||||||
)
|
)
|
||||||
|
this.historicalUserLocations = this.geolocation.historicalUserLocations
|
||||||
this.newFeatures = new NewGeometryFromChangesFeatureSource(
|
this.newFeatures = new NewGeometryFromChangesFeatureSource(
|
||||||
this.changes,
|
this.changes,
|
||||||
indexedElements,
|
indexedElements,
|
||||||
|
@ -215,7 +216,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
this.layout
|
this.layout
|
||||||
))
|
))
|
||||||
|
|
||||||
this.osmObjectDownloader = new OsmObjectDownloader(this.osmConnection.Backend(), this.changes)
|
this.osmObjectDownloader = new OsmObjectDownloader(
|
||||||
|
this.osmConnection.Backend(),
|
||||||
|
this.changes
|
||||||
|
)
|
||||||
|
|
||||||
this.initActors()
|
this.initActors()
|
||||||
this.drawSpecialLayers(lastClick)
|
this.drawSpecialLayers(lastClick)
|
||||||
|
@ -274,7 +278,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the special layers to the map
|
* Add the special layers to the map
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
private drawSpecialLayers(last_click: LastClickFeatureSource) {
|
private drawSpecialLayers(last_click: LastClickFeatureSource) {
|
||||||
type AddedByDefaultTypes = typeof Constants.added_by_default[number]
|
type AddedByDefaultTypes = typeof Constants.added_by_default[number]
|
||||||
|
@ -283,10 +286,8 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
// The last_click gets a _very_ special treatment
|
// The last_click gets a _very_ special treatment
|
||||||
|
|
||||||
const last_click_layer = this.layerState.filteredLayers.get("last_click")
|
const last_click_layer = this.layerState.filteredLayers.get("last_click")
|
||||||
this.featureProperties.addSpecial(
|
this.featureProperties.trackFeatureSource(last_click)
|
||||||
"last_click",
|
this.indexedFeatures.addSource(last_click)
|
||||||
new UIEventSource<Record<string, string>>(last_click.properties)
|
|
||||||
)
|
|
||||||
new ShowDataLayer(this.map, {
|
new ShowDataLayer(this.map, {
|
||||||
features: new FilteringFeatureSource(last_click_layer, last_click),
|
features: new FilteringFeatureSource(last_click_layer, last_click),
|
||||||
doShowLayer: new ImmutableStore(true),
|
doShowLayer: new ImmutableStore(true),
|
||||||
|
@ -347,10 +348,13 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
?.isDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true)
|
?.isDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true)
|
||||||
|
|
||||||
this.layerState.filteredLayers.forEach((flayer) => {
|
this.layerState.filteredLayers.forEach((flayer) => {
|
||||||
const features = specialLayers[flayer.layerDef.id]
|
const features: FeatureSource = specialLayers[flayer.layerDef.id]
|
||||||
if (features === undefined) {
|
if (features === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.featureProperties.trackFeatureSource(features)
|
||||||
|
this.indexedFeatures.addSource(features)
|
||||||
new ShowDataLayer(this.map, {
|
new ShowDataLayer(this.map, {
|
||||||
features,
|
features,
|
||||||
doShowLayer: flayer.isDisplayed,
|
doShowLayer: flayer.isDisplayed,
|
||||||
|
|
|
@ -92,13 +92,13 @@ export default class UploadTraceToOsmUI extends LoginToggle {
|
||||||
)
|
)
|
||||||
const descriptionStr = UploadTraceToOsmUI.createDefault(
|
const descriptionStr = UploadTraceToOsmUI.createDefault(
|
||||||
description.GetValue().data,
|
description.GetValue().data,
|
||||||
"Track created with MapComplete with theme " + state?.layoutToUse?.id
|
"Track created with MapComplete with theme " + state?.layout?.id
|
||||||
)
|
)
|
||||||
await state?.osmConnection?.uploadGpxTrack(trace(title.GetValue().data), {
|
await state?.osmConnection?.uploadGpxTrack(trace(title.GetValue().data), {
|
||||||
visibility: dropdown.GetValue().data,
|
visibility: dropdown.GetValue().data,
|
||||||
description: descriptionStr,
|
description: descriptionStr,
|
||||||
filename: titleStr + ".gpx",
|
filename: titleStr + ".gpx",
|
||||||
labels: ["MapComplete", state?.layoutToUse?.id],
|
labels: ["MapComplete", state?.layout?.id],
|
||||||
})
|
})
|
||||||
|
|
||||||
if (options?.whenUploaded !== undefined) {
|
if (options?.whenUploaded !== undefined) {
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
import { InputElement } from "./InputElement"
|
|
||||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
|
||||||
import BaseUIElement from "../BaseUIElement"
|
|
||||||
import { Translation } from "../i18n/Translation"
|
|
||||||
import { SubstitutedTranslation } from "../SubstitutedTranslation"
|
|
||||||
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
|
|
||||||
|
|
||||||
export default class InputElementWrapper<T> extends InputElement<T> {
|
|
||||||
private readonly _inputElement: InputElement<T>
|
|
||||||
private readonly _renderElement: BaseUIElement
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
inputElement: InputElement<T>,
|
|
||||||
translation: Translation,
|
|
||||||
key: string,
|
|
||||||
tags: UIEventSource<any>,
|
|
||||||
state: FeaturePipelineState
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
this._inputElement = inputElement
|
|
||||||
const mapping = new Map<string, BaseUIElement>()
|
|
||||||
|
|
||||||
mapping.set(key, inputElement)
|
|
||||||
|
|
||||||
// Bit of a hack: the SubstitutedTranslation expects a special rendering, but those are formatted '{key()}' instead of '{key}', so we substitute it first
|
|
||||||
translation = translation.OnEveryLanguage((txt) =>
|
|
||||||
txt.replace("{" + key + "}", "{" + key + "()}")
|
|
||||||
)
|
|
||||||
this._renderElement = new SubstitutedTranslation(translation, tags, state, mapping)
|
|
||||||
}
|
|
||||||
|
|
||||||
GetValue(): UIEventSource<T> {
|
|
||||||
return this._inputElement.GetValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
IsValid(t: T): boolean {
|
|
||||||
return this._inputElement.IsValid(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected InnerConstructElement(): HTMLElement {
|
|
||||||
return this._renderElement.ConstructElement()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,7 +5,6 @@ import Img from "../Base/Img"
|
||||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||||
import Combine from "../Base/Combine"
|
import Combine from "../Base/Combine"
|
||||||
import Link from "../Base/Link"
|
import Link from "../Base/Link"
|
||||||
import { SubstitutedTranslation } from "../SubstitutedTranslation"
|
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||||
|
@ -25,6 +24,7 @@ import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
|
||||||
import ShowDataLayer from "../Map/ShowDataLayer"
|
import ShowDataLayer from "../Map/ShowDataLayer"
|
||||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||||
import MaplibreMap from "../Map/MaplibreMap.svelte"
|
import MaplibreMap from "../Map/MaplibreMap.svelte"
|
||||||
|
import SpecialVisualizations from "../SpecialVisualizations"
|
||||||
|
|
||||||
export interface AutoAction extends SpecialVisualization {
|
export interface AutoAction extends SpecialVisualization {
|
||||||
supportsAutoAction: boolean
|
supportsAutoAction: boolean
|
||||||
|
@ -148,19 +148,22 @@ class ApplyButton extends UIElement {
|
||||||
const featureTags = this.state.featureProperties.getStore(targetFeatureId)
|
const featureTags = this.state.featureProperties.getStore(targetFeatureId)
|
||||||
const rendering = this.tagRenderingConfig.GetRenderValue(featureTags.data).txt
|
const rendering = this.tagRenderingConfig.GetRenderValue(featureTags.data).txt
|
||||||
const specialRenderings = Utils.NoNull(
|
const specialRenderings = Utils.NoNull(
|
||||||
SubstitutedTranslation.ExtractSpecialComponents(rendering).map((x) => x.special)
|
SpecialVisualizations.constructSpecification(rendering)
|
||||||
).filter((v) => v.func["supportsAutoAction"] === true)
|
).filter((v) => typeof v !== "string" && v.func["supportsAutoAction"] === true)
|
||||||
|
|
||||||
if (specialRenderings.length == 0) {
|
if (specialRenderings.length == 0) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"AutoApply: feature " +
|
"AutoApply: feature " +
|
||||||
targetFeatureId +
|
targetFeatureId +
|
||||||
" got a rendering without supported auto actions:",
|
" got a rendering without supported auto actions:",
|
||||||
rendering
|
rendering
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const specialRendering of specialRenderings) {
|
for (const specialRendering of specialRenderings) {
|
||||||
|
if (typeof specialRendering === "string") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
const action = <AutoAction>specialRendering.func
|
const action = <AutoAction>specialRendering.func
|
||||||
await action.applyActionOn(this.state, featureTags, specialRendering.args)
|
await action.applyActionOn(this.state, featureTags, specialRendering.args)
|
||||||
}
|
}
|
||||||
|
@ -225,7 +228,7 @@ export default class AutoApplyButton implements SpecialVisualization {
|
||||||
"To effectively use this button, you'll need some ingredients:",
|
"To effectively use this button, you'll need some ingredients:",
|
||||||
new List([
|
new List([
|
||||||
"A target layer with features for which an action is defined in a tag rendering. The following special visualisations support an autoAction: " +
|
"A target layer with features for which an action is defined in a tag rendering. The following special visualisations support an autoAction: " +
|
||||||
supportedActions.join(", "),
|
supportedActions.join(", "),
|
||||||
"A host feature to place the auto-action on. This can be a big outline (such as a city). Another good option for this is the layer ",
|
"A host feature to place the auto-action on. This can be a big outline (such as a city). Another good option for this is the layer ",
|
||||||
new Link("current_view", "./BuiltinLayers.md#current_view"),
|
new Link("current_view", "./BuiltinLayers.md#current_view"),
|
||||||
"Then, use a calculated tag on the host feature to determine the overlapping object ids",
|
"Then, use a calculated tag on the host feature to determine the overlapping object ids",
|
||||||
|
@ -245,7 +248,7 @@ export default class AutoApplyButton implements SpecialVisualization {
|
||||||
!(
|
!(
|
||||||
state.featureSwitchIsTesting.data ||
|
state.featureSwitchIsTesting.data ||
|
||||||
state.osmConnection._oauth_config.url ===
|
state.osmConnection._oauth_config.url ===
|
||||||
OsmConnection.oauth_configs["osm-test"].url
|
OsmConnection.oauth_configs["osm-test"].url
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
const t = Translations.t.general.add.import
|
const t = Translations.t.general.add.import
|
||||||
|
|
|
@ -5,16 +5,26 @@ import Combine from "../Base/Combine"
|
||||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
|
import { Feature, LineString } from "geojson"
|
||||||
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||||
|
|
||||||
export class ExportAsGpxViz implements SpecialVisualization {
|
export class ExportAsGpxViz implements SpecialVisualization {
|
||||||
funcName = "export_as_gpx"
|
funcName = "export_as_gpx"
|
||||||
docs = "Exports the selected feature as GPX-file"
|
docs = "Exports the selected feature as GPX-file"
|
||||||
args = []
|
args = []
|
||||||
|
|
||||||
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>) {
|
constr(
|
||||||
|
state: SpecialVisualizationState,
|
||||||
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
|
argument: string[],
|
||||||
|
feature: Feature,
|
||||||
|
layer: LayerConfig
|
||||||
|
) {
|
||||||
const t = Translations.t.general.download
|
const t = Translations.t.general.download
|
||||||
|
if (feature.geometry.type !== "LineString") {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
return new SubtleButton(
|
return new SubtleButton(
|
||||||
Svg.download_ui(),
|
Svg.download_ui(),
|
||||||
new Combine([
|
new Combine([
|
||||||
|
@ -24,10 +34,8 @@ export class ExportAsGpxViz implements SpecialVisualization {
|
||||||
).onClick(() => {
|
).onClick(() => {
|
||||||
console.log("Exporting as GPX!")
|
console.log("Exporting as GPX!")
|
||||||
const tags = tagSource.data
|
const tags = tagSource.data
|
||||||
const feature = state.indexedFeatures.featuresById.data.get(tags.id)
|
|
||||||
const layer = state?.layout?.getMatchingLayer(tags)
|
|
||||||
const gpx = GeoOperations.AsGpx(feature, { layer })
|
|
||||||
const title = layer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "gpx_track"
|
const title = layer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "gpx_track"
|
||||||
|
const gpx = GeoOperations.toGpx(<Feature<LineString>>feature, title)
|
||||||
Utils.offerContentsAsDownloadableFile(gpx, title + "_mapcomplete_export.gpx", {
|
Utils.offerContentsAsDownloadableFile(gpx, title + "_mapcomplete_export.gpx", {
|
||||||
mimetype: "{gpx=application/gpx+xml}",
|
mimetype: "{gpx=application/gpx+xml}",
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,9 +9,7 @@ import InputElementMap from "../Input/InputElementMap"
|
||||||
import { SaveButton } from "./SaveButton"
|
import { SaveButton } from "./SaveButton"
|
||||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../i18n/Translations"
|
||||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
|
||||||
import { Translation } from "../i18n/Translation"
|
import { Translation } from "../i18n/Translation"
|
||||||
import Constants from "../../Models/Constants"
|
|
||||||
import { SubstitutedTranslation } from "../SubstitutedTranslation"
|
import { SubstitutedTranslation } from "../SubstitutedTranslation"
|
||||||
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
|
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
|
||||||
import { Tag } from "../../Logic/Tags/Tag"
|
import { Tag } from "../../Logic/Tags/Tag"
|
||||||
|
@ -19,7 +17,6 @@ import { And } from "../../Logic/Tags/And"
|
||||||
import { TagUtils, UploadableTag } from "../../Logic/Tags/TagUtils"
|
import { TagUtils, UploadableTag } from "../../Logic/Tags/TagUtils"
|
||||||
import BaseUIElement from "../BaseUIElement"
|
import BaseUIElement from "../BaseUIElement"
|
||||||
import { DropDown } from "../Input/DropDown"
|
import { DropDown } from "../Input/DropDown"
|
||||||
import InputElementWrapper from "../Input/InputElementWrapper"
|
|
||||||
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
|
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
|
||||||
import TagRenderingConfig, { Mapping } from "../../Models/ThemeConfig/TagRenderingConfig"
|
import TagRenderingConfig, { Mapping } from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||||
import { Unit } from "../../Models/Unit"
|
import { Unit } from "../../Models/Unit"
|
||||||
|
@ -626,25 +623,11 @@ export default class TagRenderingQuestion extends Combine {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let inputTagsFilter: InputElement<UploadableTag> = new InputElementMap(
|
return new InputElementMap(
|
||||||
input,
|
input,
|
||||||
(a, b) => a === b || (a?.shadows(b) ?? false),
|
(a, b) => a === b || (a?.shadows(b) ?? false),
|
||||||
pickString,
|
pickString,
|
||||||
toString
|
toString
|
||||||
)
|
)
|
||||||
|
|
||||||
if (freeform.inline) {
|
|
||||||
inputTagsFilter.SetClass("w-48-imp")
|
|
||||||
inputTagsFilter = new InputElementWrapper(
|
|
||||||
inputTagsFilter,
|
|
||||||
configuration.render,
|
|
||||||
freeform.key,
|
|
||||||
tags,
|
|
||||||
state
|
|
||||||
)
|
|
||||||
inputTagsFilter.SetClass("block")
|
|
||||||
}
|
|
||||||
|
|
||||||
return inputTagsFilter
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { GeoLocationPointProperties } from "../../Logic/State/GeoLocationState"
|
||||||
import UploadTraceToOsmUI from "../BigComponents/UploadTraceToOsmUI"
|
import UploadTraceToOsmUI from "../BigComponents/UploadTraceToOsmUI"
|
||||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||||
|
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper around 'UploadTraceToOsmUI'
|
* Wrapper around 'UploadTraceToOsmUI'
|
||||||
|
@ -20,38 +21,8 @@ export class UploadToOsmViz implements SpecialVisualization {
|
||||||
featureTags: UIEventSource<Record<string, string>>,
|
featureTags: UIEventSource<Record<string, string>>,
|
||||||
args: string[]
|
args: string[]
|
||||||
) {
|
) {
|
||||||
function getTrace(title: string) {
|
const locations = state.historicalUserLocations.features.data
|
||||||
title = title?.trim()
|
return new UploadTraceToOsmUI((title) => GeoOperations.toGpx(locations, title), state, {
|
||||||
if (title === undefined || title === "") {
|
|
||||||
title = "Uploaded with MapComplete"
|
|
||||||
}
|
|
||||||
title = Utils.EncodeXmlValue(title)
|
|
||||||
const userLocations = <Feature<Point, GeoLocationPointProperties>[]>(
|
|
||||||
state.historicalUserLocations.features.data
|
|
||||||
)
|
|
||||||
const trackPoints: string[] = []
|
|
||||||
for (const l of userLocations) {
|
|
||||||
let trkpt = ` <trkpt lat="${l.geometry.coordinates[1]}" lon="${l.geometry.coordinates[0]}">`
|
|
||||||
trkpt += ` <time>${l.properties.date}</time>`
|
|
||||||
if (l.properties.altitude !== null && l.properties.altitude !== undefined) {
|
|
||||||
trkpt += ` <ele>${l.properties.altitude}</ele>`
|
|
||||||
}
|
|
||||||
trkpt += " </trkpt>"
|
|
||||||
trackPoints.push(trkpt)
|
|
||||||
}
|
|
||||||
const header =
|
|
||||||
'<gpx version="1.1" creator="MapComplete track uploader" 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>"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return new UploadTraceToOsmUI(getTrace, state, {
|
|
||||||
whenUploaded: async () => {
|
whenUploaded: async () => {
|
||||||
state.historicalUserLocations.features.setData([])
|
state.historicalUserLocations.features.setData([])
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { OsmConnection } from "../Logic/Osm/OsmConnection";
|
||||||
import { Changes } from "../Logic/Osm/Changes";
|
import { Changes } from "../Logic/Osm/Changes";
|
||||||
import { ExportableMap, MapProperties } from "../Models/MapProperties";
|
import { ExportableMap, MapProperties } from "../Models/MapProperties";
|
||||||
import LayerState from "../Logic/State/LayerState";
|
import LayerState from "../Logic/State/LayerState";
|
||||||
import { Feature, Geometry } from "geojson";
|
import { Feature, Geometry, Point } from "geojson";
|
||||||
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
|
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
|
||||||
import { MangroveIdentity } from "../Logic/Web/MangroveReviews";
|
import { MangroveIdentity } from "../Logic/Web/MangroveReviews";
|
||||||
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore";
|
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore";
|
||||||
|
@ -34,7 +34,7 @@ export interface SpecialVisualizationState {
|
||||||
*/
|
*/
|
||||||
readonly newFeatures: WritableFeatureSource
|
readonly newFeatures: WritableFeatureSource
|
||||||
|
|
||||||
readonly historicalUserLocations: WritableFeatureSource
|
readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>
|
||||||
|
|
||||||
readonly osmConnection: OsmConnection
|
readonly osmConnection: OsmConnection
|
||||||
readonly featureSwitchUserbadge: Store<boolean>
|
readonly featureSwitchUserbadge: Store<boolean>
|
||||||
|
|
|
@ -13,7 +13,6 @@ import { MinimapViz } from "./Popup/MinimapViz"
|
||||||
import { ShareLinkViz } from "./Popup/ShareLinkViz"
|
import { ShareLinkViz } from "./Popup/ShareLinkViz"
|
||||||
import { UploadToOsmViz } from "./Popup/UploadToOsmViz"
|
import { UploadToOsmViz } from "./Popup/UploadToOsmViz"
|
||||||
import { MultiApplyViz } from "./Popup/MultiApplyViz"
|
import { MultiApplyViz } from "./Popup/MultiApplyViz"
|
||||||
import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz"
|
|
||||||
import { AddNoteCommentViz } from "./Popup/AddNoteCommentViz"
|
import { AddNoteCommentViz } from "./Popup/AddNoteCommentViz"
|
||||||
import { PlantNetDetectionViz } from "./Popup/PlantNetDetectionViz"
|
import { PlantNetDetectionViz } from "./Popup/PlantNetDetectionViz"
|
||||||
import { ConflateButton, ImportPointButton, ImportWayButton } from "./Popup/ImportButton"
|
import { ConflateButton, ImportPointButton, ImportWayButton } from "./Popup/ImportButton"
|
||||||
|
@ -80,6 +79,7 @@ import DeleteWizard from "./Popup/DeleteWizard"
|
||||||
import { OsmId, OsmTags, WayId } from "../Models/OsmFeature"
|
import { OsmId, OsmTags, WayId } from "../Models/OsmFeature"
|
||||||
import MoveWizard from "./Popup/MoveWizard"
|
import MoveWizard from "./Popup/MoveWizard"
|
||||||
import SplitRoadWizard from "./Popup/SplitRoadWizard"
|
import SplitRoadWizard from "./Popup/SplitRoadWizard"
|
||||||
|
import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz"
|
||||||
|
|
||||||
class NearbyImageVis implements SpecialVisualization {
|
class NearbyImageVis implements SpecialVisualization {
|
||||||
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
||||||
|
@ -597,9 +597,9 @@ export default class SpecialVisualizations {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new ShareLinkViz(),
|
new ShareLinkViz(),
|
||||||
|
new ExportAsGpxViz(),
|
||||||
new UploadToOsmViz(),
|
new UploadToOsmViz(),
|
||||||
new MultiApplyViz(),
|
new MultiApplyViz(),
|
||||||
new ExportAsGpxViz(),
|
|
||||||
new AddNoteCommentViz(),
|
new AddNoteCommentViz(),
|
||||||
{
|
{
|
||||||
funcName: "open_note",
|
funcName: "open_note",
|
||||||
|
@ -874,7 +874,7 @@ export default class SpecialVisualizations {
|
||||||
funcName: "export_as_geojson",
|
funcName: "export_as_geojson",
|
||||||
docs: "Exports the selected feature as GeoJson-file",
|
docs: "Exports the selected feature as GeoJson-file",
|
||||||
args: [],
|
args: [],
|
||||||
constr: (state, tagSource) => {
|
constr: (state, tagSource, tagsSource, feature, layer) => {
|
||||||
const t = Translations.t.general.download
|
const t = Translations.t.general.download
|
||||||
|
|
||||||
return new SubtleButton(
|
return new SubtleButton(
|
||||||
|
@ -886,10 +886,8 @@ export default class SpecialVisualizations {
|
||||||
).onClick(() => {
|
).onClick(() => {
|
||||||
console.log("Exporting as Geojson")
|
console.log("Exporting as Geojson")
|
||||||
const tags = tagSource.data
|
const tags = tagSource.data
|
||||||
const feature = state.indexedFeatures.featuresById.data.get(tags.id)
|
|
||||||
const matchingLayer = state?.layout?.getMatchingLayer(tags)
|
|
||||||
const title =
|
const title =
|
||||||
matchingLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "geojson"
|
layer?.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "geojson"
|
||||||
const data = JSON.stringify(feature, null, " ")
|
const data = JSON.stringify(feature, null, " ")
|
||||||
Utils.offerContentsAsDownloadableFile(
|
Utils.offerContentsAsDownloadableFile(
|
||||||
data,
|
data,
|
||||||
|
|
29
Utils.ts
29
Utils.ts
|
@ -1415,4 +1415,33 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
) {
|
) {
|
||||||
return Math.abs(c0.r - c1.r) + Math.abs(c0.g - c1.g) + Math.abs(c0.b - c1.b)
|
return Math.abs(c0.r - c1.r) + Math.abs(c0.g - c1.g) + Math.abs(c0.b - c1.b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static SameObject(a: any, b: any) {
|
||||||
|
if (a === b) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (a === undefined || a === null || b === null || b === undefined) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (typeof a === "object" && typeof b === "object") {
|
||||||
|
for (const aKey in a) {
|
||||||
|
if (!(aKey in b)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const bKey in b) {
|
||||||
|
if (!(bKey in a)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const k in a) {
|
||||||
|
if (!Utils.SameObject(a[k], b[k])) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,4 +44,4 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"syncSelection": "global"
|
"syncSelection": "global"
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,7 +101,6 @@
|
||||||
"showdown": "^2.1.0",
|
"showdown": "^2.1.0",
|
||||||
"svg-path-parser": "^1.1.0",
|
"svg-path-parser": "^1.1.0",
|
||||||
"tailwindcss": "^3.1.8",
|
"tailwindcss": "^3.1.8",
|
||||||
"togpx": "^0.5.4",
|
|
||||||
"vite-node": "^0.28.3",
|
"vite-node": "^0.28.3",
|
||||||
"vitest": "^0.28.3",
|
"vitest": "^0.28.3",
|
||||||
"wikibase-sdk": "^7.14.0",
|
"wikibase-sdk": "^7.14.0",
|
||||||
|
|
Loading…
Reference in a new issue