From c6e12fdd6b401b03afbc9adb5728b6f665a64b15 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 20 Apr 2023 18:58:31 +0200 Subject: [PATCH] Refactoring: fix GPX-track view --- Logic/Actors/GeoLocationHandler.ts | 12 +- Logic/Actors/SelectedElementTagsUpdater.ts | 13 ++ .../Actors/FeaturePropertiesStore.ts | 23 ++- Logic/FeatureSource/FeatureSource.ts | 8 +- .../Sources/FilteringFeatureSource.ts | 6 +- Logic/GeoOperations.ts | 127 ++++++++++------- Logic/MetaTagging.ts | 8 +- Logic/State/MapState.ts | 133 ------------------ Models/ThemeConfig/Conversion/Validation.ts | 2 +- Models/ThemeConfig/LayerConfig.ts | 19 ++- Models/ThemeConfig/LayoutConfig.ts | 3 + Models/ThemeViewState.ts | 26 ++-- UI/BigComponents/UploadTraceToOsmUI.ts | 4 +- UI/Input/InputElementWrapper.ts | 43 ------ UI/Popup/AutoApplyButton.ts | 17 ++- UI/Popup/ExportAsGpxViz.ts | 20 ++- UI/Popup/TagRenderingQuestion.ts | 19 +-- UI/Popup/UploadToOsmViz.ts | 35 +---- UI/SpecialVisualization.ts | 4 +- UI/SpecialVisualizations.ts | 10 +- Utils.ts | 29 ++++ assets/layers/gps_track/gps_track.json | 2 +- package.json | 1 - 23 files changed, 217 insertions(+), 347 deletions(-) delete mode 100644 Logic/State/MapState.ts delete mode 100644 UI/Input/InputElementWrapper.ts diff --git a/Logic/Actors/GeoLocationHandler.ts b/Logic/Actors/GeoLocationHandler.ts index b754d6f60..e5a1b195d 100644 --- a/Logic/Actors/GeoLocationHandler.ts +++ b/Logic/Actors/GeoLocationHandler.ts @@ -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> /** * 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 = new UIEventSource(false) private readonly selectedElement: UIEventSource 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 = 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) } } diff --git a/Logic/Actors/SelectedElementTagsUpdater.ts b/Logic/Actors/SelectedElementTagsUpdater.ts index 135fbd6e4..f3a146bb3 100644 --- a/Logic/Actors/SelectedElementTagsUpdater.ts +++ b/Logic/Actors/SelectedElementTagsUpdater.ts @@ -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 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) diff --git a/Logic/FeatureSource/Actors/FeaturePropertiesStore.ts b/Logic/FeatureSource/Actors/FeaturePropertiesStore.ts index 9cadb5df3..3d33040a8 100644 --- a/Logic/FeatureSource/Actors/FeaturePropertiesStore.ts +++ b/Logic/FeatureSource/Actors/FeaturePropertiesStore.ts @@ -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>>() - constructor(source: FeatureSource & IndexedFeatureSource) { - this._source = source + constructor(...sources: FeatureSource[]) { + for (const source of sources) { + this.trackFeatureSource(source) + } + } + + public getStore(id: string): UIEventSource> { + 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> { - return this._elements.get(id) - } - - public addSpecial(id: string, store: UIEventSource>) { - 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) diff --git a/Logic/FeatureSource/FeatureSource.ts b/Logic/FeatureSource/FeatureSource.ts index def7d53e5..afe0b6092 100644 --- a/Logic/FeatureSource/FeatureSource.ts +++ b/Logic/FeatureSource/FeatureSource.ts @@ -3,11 +3,11 @@ import FilteredLayer from "../../Models/FilteredLayer" import { BBox } from "../BBox" import { Feature } from "geojson" -export interface FeatureSource { - features: Store +export interface FeatureSource { + features: Store } -export interface WritableFeatureSource extends FeatureSource { - features: UIEventSource +export interface WritableFeatureSource extends FeatureSource { + features: UIEventSource } export interface Tiled { diff --git a/Logic/FeatureSource/Sources/FilteringFeatureSource.ts b/Logic/FeatureSource/Sources/FilteringFeatureSource.ts index 8d1636bee..9a55a51d1 100644 --- a/Logic/FeatureSource/Sources/FilteringFeatureSource.ts +++ b/Logic/FeatureSource/Sources/FilteringFeatureSource.ts @@ -61,7 +61,7 @@ export default class FilteringFeatureSource implements FeatureSource { const includedFeatureIds = new Set() 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 } diff --git a/Logic/GeoOperations.ts b/Logic/GeoOperations.ts index 741affcb9..a26261194 100644 --- a/Logic/GeoOperations.ts +++ b/Logic/GeoOperations.ts @@ -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 + | Feature[], + 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[] + if (Array.isArray(locations)) { + locationsWithMeta = locations + } else { + locationsWithMeta = locations.geometry.coordinates.map( + (p) => + >{ + type: "Feature", + properties: {}, + geometry: { + type: "Point", + coordinates: p, + }, + } + ) + } + for (const l of locationsWithMeta) { + let trkpt = ` ` + if (l.properties.date) { + trkpt += ` ` + } + if (l.properties.altitude) { + trkpt += ` ${l.properties.altitude}` + } + trkpt += " " + trackPoints.push(trkpt) + } + const header = + '' + return ( + header + + "\n" + + title + + "\n\n" + + trackPoints.join("\n") + + "\n" + ) } 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: Feature): Feature { + if (p.geometry.type !== "Polygon") { + return >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: Feature): Feature { - if (p.geometry.type !== "Polygon") { - return >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 } - } } diff --git a/Logic/MetaTagging.ts b/Logic/MetaTagging.ts index ce1d143fa..6a91b0838 100644 --- a/Logic/MetaTagging.ts +++ b/Logic/MetaTagging.ts @@ -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 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 diff --git a/Logic/State/MapState.ts b/Logic/State/MapState.ts deleted file mode 100644 index 12295cfb8..000000000 --- a/Logic/State/MapState.ts +++ /dev/null @@ -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 }[] - - 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(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) { - 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 = 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) - } -} diff --git a/Models/ThemeConfig/Conversion/Validation.ts b/Models/ThemeConfig/Conversion/Validation.ts index da89260a8..1a14d735d 100644 --- a/Models/ThemeConfig/Conversion/Validation.ts +++ b/Models/ThemeConfig/Conversion/Validation.ts @@ -891,7 +891,7 @@ export class ValidateLayer extends DesugaringStep { throw "A special layer cannot have presets" } // 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++) { const preset = json.presets[i] const tags: { k: string; v: string }[] = new And( diff --git a/Models/ThemeConfig/LayerConfig.ts b/Models/ThemeConfig/LayerConfig.ts index 13212878c..b81bf244d 100644 --- a/Models/ThemeConfig/LayerConfig.ts +++ b/Models/ThemeConfig/LayerConfig.ts @@ -618,17 +618,26 @@ export default class LayerConfig extends WithContextLoader { filterDocs.push(new Title("Filters", 4)) 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([ new Combine([new Title(this.id, 1), iconImg, this.description, "\n"]).SetClass( "flex flex-col" ), new List(extraProps), ...usingLayer, - - 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, + ...tagsDescription, new Title("Supported attributes", 2), quickOverview, ...this.tagRenderings.map((tr) => tr.GenerateDocumentation()), diff --git a/Models/ThemeConfig/LayoutConfig.ts b/Models/ThemeConfig/LayoutConfig.ts index a85240120..5a7e6bb69 100644 --- a/Models/ThemeConfig/LayoutConfig.ts +++ b/Models/ThemeConfig/LayoutConfig.ts @@ -291,6 +291,9 @@ export default class LayoutConfig implements LayoutInformation { return undefined } for (const layer of this.layers) { + if (!layer.source) { + continue + } if (layer.source.osmTags.matchesProperties(tags)) { return layer } diff --git a/Models/ThemeViewState.ts b/Models/ThemeViewState.ts index 3ac0d7a2c..8154ff970 100644 --- a/Models/ThemeViewState.ts +++ b/Models/ThemeViewState.ts @@ -10,7 +10,7 @@ import { import { OsmConnection } from "../Logic/Osm/OsmConnection" import { ExportableMap, MapProperties } from "./MapProperties" import LayerState from "../Logic/State/LayerState" -import { Feature } from "geojson" +import { Feature, Point } from "geojson" import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" import { Map as MlMap } from "maplibre-gl" import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning" @@ -42,7 +42,7 @@ import { MenuState } from "./MenuState" import MetaTagging from "../Logic/MetaTagging" import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator" 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 fullNodeDatabase?: FullNodeDatabaseSource // TODO - readonly historicalUserLocations: WritableFeatureSource - readonly indexedFeatures: IndexedFeatureSource + readonly historicalUserLocations: WritableFeatureSource> + readonly indexedFeatures: IndexedFeatureSource & LayoutSource readonly newFeatures: WritableFeatureSource readonly layerState: LayerState readonly perLayer: ReadonlyMap @@ -152,6 +152,7 @@ export default class ThemeViewState implements SpecialVisualizationState { }, layout?.isLeftRightSensitive() ?? false ) + this.historicalUserLocations = this.geolocation.historicalUserLocations this.newFeatures = new NewGeometryFromChangesFeatureSource( this.changes, indexedElements, @@ -215,7 +216,10 @@ export default class ThemeViewState implements SpecialVisualizationState { this.layout )) - this.osmObjectDownloader = new OsmObjectDownloader(this.osmConnection.Backend(), this.changes) + this.osmObjectDownloader = new OsmObjectDownloader( + this.osmConnection.Backend(), + this.changes + ) this.initActors() this.drawSpecialLayers(lastClick) @@ -274,7 +278,6 @@ export default class ThemeViewState implements SpecialVisualizationState { /** * Add the special layers to the map - * @private */ private drawSpecialLayers(last_click: LastClickFeatureSource) { 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 const last_click_layer = this.layerState.filteredLayers.get("last_click") - this.featureProperties.addSpecial( - "last_click", - new UIEventSource>(last_click.properties) - ) + this.featureProperties.trackFeatureSource(last_click) + this.indexedFeatures.addSource(last_click) new ShowDataLayer(this.map, { features: new FilteringFeatureSource(last_click_layer, last_click), doShowLayer: new ImmutableStore(true), @@ -347,10 +348,13 @@ export default class ThemeViewState implements SpecialVisualizationState { ?.isDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true) this.layerState.filteredLayers.forEach((flayer) => { - const features = specialLayers[flayer.layerDef.id] + const features: FeatureSource = specialLayers[flayer.layerDef.id] if (features === undefined) { return } + + this.featureProperties.trackFeatureSource(features) + this.indexedFeatures.addSource(features) new ShowDataLayer(this.map, { features, doShowLayer: flayer.isDisplayed, diff --git a/UI/BigComponents/UploadTraceToOsmUI.ts b/UI/BigComponents/UploadTraceToOsmUI.ts index 3d7c43910..be0ca46ba 100644 --- a/UI/BigComponents/UploadTraceToOsmUI.ts +++ b/UI/BigComponents/UploadTraceToOsmUI.ts @@ -92,13 +92,13 @@ export default class UploadTraceToOsmUI extends LoginToggle { ) const descriptionStr = UploadTraceToOsmUI.createDefault( 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), { visibility: dropdown.GetValue().data, description: descriptionStr, filename: titleStr + ".gpx", - labels: ["MapComplete", state?.layoutToUse?.id], + labels: ["MapComplete", state?.layout?.id], }) if (options?.whenUploaded !== undefined) { diff --git a/UI/Input/InputElementWrapper.ts b/UI/Input/InputElementWrapper.ts deleted file mode 100644 index 99fa3309e..000000000 --- a/UI/Input/InputElementWrapper.ts +++ /dev/null @@ -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 extends InputElement { - private readonly _inputElement: InputElement - private readonly _renderElement: BaseUIElement - - constructor( - inputElement: InputElement, - translation: Translation, - key: string, - tags: UIEventSource, - state: FeaturePipelineState - ) { - super() - this._inputElement = inputElement - const mapping = new Map() - - 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 { - return this._inputElement.GetValue() - } - - IsValid(t: T): boolean { - return this._inputElement.IsValid(t) - } - - protected InnerConstructElement(): HTMLElement { - return this._renderElement.ConstructElement() - } -} diff --git a/UI/Popup/AutoApplyButton.ts b/UI/Popup/AutoApplyButton.ts index 47d1b633f..e30fe6a04 100644 --- a/UI/Popup/AutoApplyButton.ts +++ b/UI/Popup/AutoApplyButton.ts @@ -5,7 +5,6 @@ import Img from "../Base/Img" import { FixedUiElement } from "../Base/FixedUiElement" import Combine from "../Base/Combine" import Link from "../Base/Link" -import { SubstitutedTranslation } from "../SubstitutedTranslation" import { Utils } from "../../Utils" import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" import { VariableUiElement } from "../Base/VariableUIElement" @@ -25,6 +24,7 @@ import { MapLibreAdaptor } from "../Map/MapLibreAdaptor" import ShowDataLayer from "../Map/ShowDataLayer" import SvelteUIElement from "../Base/SvelteUIElement" import MaplibreMap from "../Map/MaplibreMap.svelte" +import SpecialVisualizations from "../SpecialVisualizations" export interface AutoAction extends SpecialVisualization { supportsAutoAction: boolean @@ -148,19 +148,22 @@ class ApplyButton extends UIElement { const featureTags = this.state.featureProperties.getStore(targetFeatureId) const rendering = this.tagRenderingConfig.GetRenderValue(featureTags.data).txt const specialRenderings = Utils.NoNull( - SubstitutedTranslation.ExtractSpecialComponents(rendering).map((x) => x.special) - ).filter((v) => v.func["supportsAutoAction"] === true) + SpecialVisualizations.constructSpecification(rendering) + ).filter((v) => typeof v !== "string" && v.func["supportsAutoAction"] === true) if (specialRenderings.length == 0) { console.warn( "AutoApply: feature " + - targetFeatureId + - " got a rendering without supported auto actions:", + targetFeatureId + + " got a rendering without supported auto actions:", rendering ) } for (const specialRendering of specialRenderings) { + if (typeof specialRendering === "string") { + continue + } const action = specialRendering.func 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:", new List([ "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 ", new Link("current_view", "./BuiltinLayers.md#current_view"), "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.osmConnection._oauth_config.url === - OsmConnection.oauth_configs["osm-test"].url + OsmConnection.oauth_configs["osm-test"].url ) ) { const t = Translations.t.general.add.import diff --git a/UI/Popup/ExportAsGpxViz.ts b/UI/Popup/ExportAsGpxViz.ts index eb5241c51..314f9ac21 100644 --- a/UI/Popup/ExportAsGpxViz.ts +++ b/UI/Popup/ExportAsGpxViz.ts @@ -5,16 +5,26 @@ import Combine from "../Base/Combine" import { GeoOperations } from "../../Logic/GeoOperations" import { Utils } from "../../Utils" 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 { funcName = "export_as_gpx" docs = "Exports the selected feature as GPX-file" args = [] - constr(state: SpecialVisualizationState, tagSource: UIEventSource>) { + constr( + state: SpecialVisualizationState, + tagSource: UIEventSource>, + argument: string[], + feature: Feature, + layer: LayerConfig + ) { const t = Translations.t.general.download - + if (feature.geometry.type !== "LineString") { + return undefined + } return new SubtleButton( Svg.download_ui(), new Combine([ @@ -24,10 +34,8 @@ export class ExportAsGpxViz implements SpecialVisualization { ).onClick(() => { console.log("Exporting as GPX!") 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 gpx = GeoOperations.toGpx(>feature, title) Utils.offerContentsAsDownloadableFile(gpx, title + "_mapcomplete_export.gpx", { mimetype: "{gpx=application/gpx+xml}", }) diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index 296c451b6..1530a1b40 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -9,9 +9,7 @@ import InputElementMap from "../Input/InputElementMap" import { SaveButton } from "./SaveButton" import { VariableUiElement } from "../Base/VariableUIElement" import Translations from "../i18n/Translations" -import { FixedUiElement } from "../Base/FixedUiElement" import { Translation } from "../i18n/Translation" -import Constants from "../../Models/Constants" import { SubstitutedTranslation } from "../SubstitutedTranslation" import { TagsFilter } from "../../Logic/Tags/TagsFilter" import { Tag } from "../../Logic/Tags/Tag" @@ -19,7 +17,6 @@ import { And } from "../../Logic/Tags/And" import { TagUtils, UploadableTag } from "../../Logic/Tags/TagUtils" import BaseUIElement from "../BaseUIElement" import { DropDown } from "../Input/DropDown" -import InputElementWrapper from "../Input/InputElementWrapper" import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" import TagRenderingConfig, { Mapping } from "../../Models/ThemeConfig/TagRenderingConfig" import { Unit } from "../../Models/Unit" @@ -626,25 +623,11 @@ export default class TagRenderingQuestion extends Combine { } }) - let inputTagsFilter: InputElement = new InputElementMap( + return new InputElementMap( input, (a, b) => a === b || (a?.shadows(b) ?? false), pickString, toString ) - - if (freeform.inline) { - inputTagsFilter.SetClass("w-48-imp") - inputTagsFilter = new InputElementWrapper( - inputTagsFilter, - configuration.render, - freeform.key, - tags, - state - ) - inputTagsFilter.SetClass("block") - } - - return inputTagsFilter } } diff --git a/UI/Popup/UploadToOsmViz.ts b/UI/Popup/UploadToOsmViz.ts index f25f72386..27f102008 100644 --- a/UI/Popup/UploadToOsmViz.ts +++ b/UI/Popup/UploadToOsmViz.ts @@ -5,6 +5,7 @@ import { GeoLocationPointProperties } from "../../Logic/State/GeoLocationState" import UploadTraceToOsmUI from "../BigComponents/UploadTraceToOsmUI" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" import { UIEventSource } from "../../Logic/UIEventSource" +import { GeoOperations } from "../../Logic/GeoOperations" /** * Wrapper around 'UploadTraceToOsmUI' @@ -20,38 +21,8 @@ export class UploadToOsmViz implements SpecialVisualization { featureTags: UIEventSource>, args: string[] ) { - function getTrace(title: string) { - title = title?.trim() - if (title === undefined || title === "") { - title = "Uploaded with MapComplete" - } - title = Utils.EncodeXmlValue(title) - const userLocations = []>( - state.historicalUserLocations.features.data - ) - const trackPoints: string[] = [] - for (const l of userLocations) { - let trkpt = ` ` - trkpt += ` ` - if (l.properties.altitude !== null && l.properties.altitude !== undefined) { - trkpt += ` ${l.properties.altitude}` - } - trkpt += " " - trackPoints.push(trkpt) - } - const header = - '' - return ( - header + - "\n" + - title + - "\n\n" + - trackPoints.join("\n") + - "\n" - ) - } - - return new UploadTraceToOsmUI(getTrace, state, { + const locations = state.historicalUserLocations.features.data + return new UploadTraceToOsmUI((title) => GeoOperations.toGpx(locations, title), state, { whenUploaded: async () => { state.historicalUserLocations.features.setData([]) }, diff --git a/UI/SpecialVisualization.ts b/UI/SpecialVisualization.ts index 9e64d16ac..5968f4b4e 100644 --- a/UI/SpecialVisualization.ts +++ b/UI/SpecialVisualization.ts @@ -6,7 +6,7 @@ import { OsmConnection } from "../Logic/Osm/OsmConnection"; import { Changes } from "../Logic/Osm/Changes"; import { ExportableMap, MapProperties } from "../Models/MapProperties"; 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 { MangroveIdentity } from "../Logic/Web/MangroveReviews"; import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"; @@ -34,7 +34,7 @@ export interface SpecialVisualizationState { */ readonly newFeatures: WritableFeatureSource - readonly historicalUserLocations: WritableFeatureSource + readonly historicalUserLocations: WritableFeatureSource> readonly osmConnection: OsmConnection readonly featureSwitchUserbadge: Store diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 3f5d119d6..b556136c3 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -13,7 +13,6 @@ import { MinimapViz } from "./Popup/MinimapViz" import { ShareLinkViz } from "./Popup/ShareLinkViz" import { UploadToOsmViz } from "./Popup/UploadToOsmViz" import { MultiApplyViz } from "./Popup/MultiApplyViz" -import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz" import { AddNoteCommentViz } from "./Popup/AddNoteCommentViz" import { PlantNetDetectionViz } from "./Popup/PlantNetDetectionViz" import { ConflateButton, ImportPointButton, ImportWayButton } from "./Popup/ImportButton" @@ -80,6 +79,7 @@ import DeleteWizard from "./Popup/DeleteWizard" import { OsmId, OsmTags, WayId } from "../Models/OsmFeature" import MoveWizard from "./Popup/MoveWizard" import SplitRoadWizard from "./Popup/SplitRoadWizard" +import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz" class NearbyImageVis implements SpecialVisualization { // 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 ExportAsGpxViz(), new UploadToOsmViz(), new MultiApplyViz(), - new ExportAsGpxViz(), new AddNoteCommentViz(), { funcName: "open_note", @@ -874,7 +874,7 @@ export default class SpecialVisualizations { funcName: "export_as_geojson", docs: "Exports the selected feature as GeoJson-file", args: [], - constr: (state, tagSource) => { + constr: (state, tagSource, tagsSource, feature, layer) => { const t = Translations.t.general.download return new SubtleButton( @@ -886,10 +886,8 @@ export default class SpecialVisualizations { ).onClick(() => { console.log("Exporting as Geojson") const tags = tagSource.data - const feature = state.indexedFeatures.featuresById.data.get(tags.id) - const matchingLayer = state?.layout?.getMatchingLayer(tags) const title = - matchingLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "geojson" + layer?.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "geojson" const data = JSON.stringify(feature, null, " ") Utils.offerContentsAsDownloadableFile( data, diff --git a/Utils.ts b/Utils.ts index 20246c84d..aef5d8d92 100644 --- a/Utils.ts +++ b/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) } + + 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 + } } diff --git a/assets/layers/gps_track/gps_track.json b/assets/layers/gps_track/gps_track.json index eb0110171..94bb2a40b 100644 --- a/assets/layers/gps_track/gps_track.json +++ b/assets/layers/gps_track/gps_track.json @@ -44,4 +44,4 @@ } ], "syncSelection": "global" -} \ No newline at end of file +} diff --git a/package.json b/package.json index 319dc4aea..e96301bfc 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,6 @@ "showdown": "^2.1.0", "svg-path-parser": "^1.1.0", "tailwindcss": "^3.1.8", - "togpx": "^0.5.4", "vite-node": "^0.28.3", "vitest": "^0.28.3", "wikibase-sdk": "^7.14.0",