forked from MapComplete/MapComplete
		
	refactoring: fix basic flow to add a new point
This commit is contained in:
		
							parent
							
								
									52a0810ea9
								
							
						
					
					
						commit
						0241f89d3d
					
				
					 109 changed files with 1931 additions and 1446 deletions
				
			
		|  | @ -4,7 +4,7 @@ import Constants from "../../Models/Constants" | |||
| import { GeoLocationPointProperties, GeoLocationState } from "../State/GeoLocationState" | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| import { Feature, LineString, Point } from "geojson" | ||||
| import FeatureSource from "../FeatureSource/FeatureSource" | ||||
| import { FeatureSource } from "../FeatureSource/FeatureSource" | ||||
| import { LocalStorageSource } from "../Web/LocalStorageSource" | ||||
| import { GeoOperations } from "../GeoOperations" | ||||
| import { OsmTags } from "../../Models/OsmFeature" | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | |||
| import SimpleMetaTagger from "../SimpleMetaTagger" | ||||
| import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" | ||||
| import { Feature } from "geojson" | ||||
| import { OsmTags } from "../../Models/OsmFeature" | ||||
| 
 | ||||
| export default class SelectedElementTagsUpdater { | ||||
|     private static readonly metatags = new Set([ | ||||
|  | @ -87,7 +88,7 @@ export default class SelectedElementTagsUpdater { | |||
|             } | ||||
|         }) | ||||
|     } | ||||
|     private applyUpdate(latestTags: any, id: string) { | ||||
|     private applyUpdate(latestTags: OsmTags, id: string) { | ||||
|         const state = this.state | ||||
|         try { | ||||
|             const leftRightSensitive = state.layoutToUse.isLeftRightSensitive() | ||||
|  |  | |||
|  | @ -26,11 +26,15 @@ export default class TitleHandler { | |||
| 
 | ||||
|                 const tags = selected.properties | ||||
|                 const layer = selectedLayer.data | ||||
|                 if (layer.title === undefined) { | ||||
|                     return defaultTitle | ||||
|                 } | ||||
|                 const tagsSource = | ||||
|                     allElements.getStore(tags.id) ?? new UIEventSource<Record<string, string>>(tags) | ||||
|                 const title = new SvelteUIElement(TagRenderingAnswer, { | ||||
|                     tags: tagsSource, | ||||
|                     state, | ||||
|                     config: layer.title, | ||||
|                     selectedElement: selectedElement.data, | ||||
|                     layer, | ||||
|                 }) | ||||
|  |  | |||
|  | @ -138,6 +138,45 @@ export class BBox { | |||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     squarify(): BBox { | ||||
|         const w = this.maxLon - this.minLon | ||||
|         const h = this.maxLat - this.minLat | ||||
|         const s = Math.sqrt(w * h) | ||||
|         const lon = (this.maxLon + this.minLon) / 2 | ||||
|         const lat = (this.maxLat + this.minLat) / 2 | ||||
|         // we want to have a more-or-less equal surface, so the new side 's' should be
 | ||||
|         // w * h = s * s
 | ||||
|         // The ratio for w is:
 | ||||
| 
 | ||||
|         return new BBox([ | ||||
|             [lon - s / 2, lat - s / 2], | ||||
|             [lon + s / 2, lat + s / 2], | ||||
|         ]) | ||||
|     } | ||||
| 
 | ||||
|     isNearby(location: [number, number], maxRange: number): boolean { | ||||
|         if (this.contains(location)) { | ||||
|             return true | ||||
|         } | ||||
|         const [lon, lat] = location | ||||
|         // We 'project' the point onto the near edges. If they are close to a horizontal _and_ vertical edge, it is nearby
 | ||||
|         // Vertically nearby: either wihtin minLat range or at most maxRange away
 | ||||
|         const nearbyVertical = | ||||
|             (this.minLat <= lat && | ||||
|                 this.maxLat >= lat && | ||||
|                 GeoOperations.distanceBetween(location, [lon, this.minLat]) <= maxRange) || | ||||
|             GeoOperations.distanceBetween(location, [lon, this.maxLat]) <= maxRange | ||||
|         if (!nearbyVertical) { | ||||
|             return false | ||||
|         } | ||||
|         const nearbyHorizontal = | ||||
|             (this.minLon <= lon && | ||||
|                 this.maxLon >= lon && | ||||
|                 GeoOperations.distanceBetween(location, [this.minLon, lat]) <= maxRange) || | ||||
|             GeoOperations.distanceBetween(location, [this.maxLon, lat]) <= maxRange | ||||
|         return nearbyHorizontal | ||||
|     } | ||||
| 
 | ||||
|     getEast() { | ||||
|         return this.maxLon | ||||
|     } | ||||
|  | @ -214,7 +253,7 @@ export class BBox { | |||
|      * @param zoomlevel | ||||
|      */ | ||||
|     expandToTileBounds(zoomlevel: number): BBox { | ||||
|         if(zoomlevel === undefined){ | ||||
|         if (zoomlevel === undefined) { | ||||
|             return this | ||||
|         } | ||||
|         const ul = Tiles.embedded_tile(this.minLat, this.minLon, zoomlevel) | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import FeatureSource, { IndexedFeatureSource } from "../FeatureSource" | ||||
| import { FeatureSource, IndexedFeatureSource } from "../FeatureSource" | ||||
| import { UIEventSource } from "../../UIEventSource" | ||||
| 
 | ||||
| /** | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import FeatureSource, { FeatureSourceForLayer } from "../FeatureSource" | ||||
| import { FeatureSource , FeatureSourceForLayer } from "../FeatureSource" | ||||
| import { Feature } from "geojson" | ||||
| import { BBox } from "../../BBox" | ||||
| import { GeoOperations } from "../../GeoOperations" | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import FeatureSource from "../FeatureSource" | ||||
| import { FeatureSource } from "../FeatureSource" | ||||
| import { Feature } from "geojson" | ||||
| import TileLocalStorage from "./TileLocalStorage" | ||||
| import { GeoOperations } from "../../GeoOperations" | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import FilteredLayer from "../../Models/FilteredLayer" | |||
| import { BBox } from "../BBox" | ||||
| import { Feature } from "geojson" | ||||
| 
 | ||||
| export default interface FeatureSource { | ||||
| export interface FeatureSource { | ||||
|     features: Store<Feature[]> | ||||
| } | ||||
| export interface WritableFeatureSource extends FeatureSource { | ||||
|  |  | |||
|  | @ -1,10 +1,9 @@ | |||
| import FeatureSource, { FeatureSourceForLayer } from "./FeatureSource" | ||||
| import { FeatureSource, FeatureSourceForLayer } from "./FeatureSource" | ||||
| import FilteredLayer from "../../Models/FilteredLayer" | ||||
| import SimpleFeatureSource from "./Sources/SimpleFeatureSource" | ||||
| import { Feature } from "geojson" | ||||
| import { Utils } from "../../Utils" | ||||
| import { UIEventSource } from "../UIEventSource" | ||||
| import { feature } from "@turf/turf" | ||||
| 
 | ||||
| /** | ||||
|  * In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled) | ||||
|  | @ -26,7 +25,7 @@ export default class PerLayerFeatureSourceSplitter< | |||
|         const knownLayers = new Map<string, T>() | ||||
|         this.perLayer = knownLayers | ||||
|         const layerSources = new Map<string, UIEventSource<Feature[]>>() | ||||
| 
 | ||||
|         console.log("PerLayerFeatureSourceSplitter got layers", layers) | ||||
|         const constructStore = | ||||
|             options?.constructStore ?? ((store, layer) => new SimpleFeatureSource(layer, store)) | ||||
|         for (const layer of layers) { | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import FeatureSource from "../FeatureSource" | ||||
| import { FeatureSource } from "../FeatureSource" | ||||
| import { Feature, Polygon } from "geojson" | ||||
| import StaticFeatureSource from "./StaticFeatureSource" | ||||
| import { GeoOperations } from "../../GeoOperations" | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import { Store, UIEventSource } from "../../UIEventSource" | ||||
| import FeatureSource, { IndexedFeatureSource } from "../FeatureSource" | ||||
| import { FeatureSource ,  IndexedFeatureSource } from "../FeatureSource" | ||||
| import { Feature } from "geojson" | ||||
| import { Utils } from "../../../Utils" | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { Store, UIEventSource } from "../../UIEventSource" | ||||
| import FilteredLayer from "../../../Models/FilteredLayer" | ||||
| import FeatureSource from "../FeatureSource" | ||||
| import { FeatureSource } from "../FeatureSource" | ||||
| import { TagsFilter } from "../../Tags/TagsFilter" | ||||
| import { Feature } from "geojson" | ||||
| import { GlobalFilter } from "../../../Models/GlobalFilter" | ||||
|  | @ -73,21 +73,9 @@ export default class FilteringFeatureSource implements FeatureSource { | |||
|                 return false | ||||
|             } | ||||
| 
 | ||||
|             for (const filter of layer.layerDef.filters) { | ||||
|                 const state = layer.appliedFilters.get(filter.id).data | ||||
|                 if (state === undefined) { | ||||
|                     continue | ||||
|                 } | ||||
|                 let neededTags: TagsFilter | ||||
|                 if (typeof state === "string") { | ||||
|                     // This filter uses fields
 | ||||
|                 } else { | ||||
|                     neededTags = filter.options[state].osmTags | ||||
|                 } | ||||
|                 if (neededTags !== undefined && !neededTags.matchesProperties(f.properties)) { | ||||
|                     // Hidden by the filter on the layer itself - we want to hide it no matter what
 | ||||
|                     return false | ||||
|                 } | ||||
|             let neededTags: TagsFilter = layer.currentFilter.data | ||||
|             if (neededTags !== undefined && !neededTags.matchesProperties(f.properties)) { | ||||
|                 return false | ||||
|             } | ||||
| 
 | ||||
|             for (const globalFilter of globalFilters ?? []) { | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
|  */ | ||||
| import { Store, UIEventSource } from "../../UIEventSource" | ||||
| import { Utils } from "../../../Utils" | ||||
| import FeatureSource from "../FeatureSource" | ||||
| import { FeatureSource } from "../FeatureSource" | ||||
| import { BBox } from "../../BBox" | ||||
| import { GeoOperations } from "../../GeoOperations" | ||||
| import { Feature } from "geojson" | ||||
|  |  | |||
|  | @ -1,21 +1,23 @@ | |||
| import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig" | ||||
| import FeatureSource from "../FeatureSource" | ||||
| import { ImmutableStore, Store } from "../../UIEventSource" | ||||
| import { WritableFeatureSource } from "../FeatureSource" | ||||
| import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" | ||||
| import { Feature, Point } from "geojson" | ||||
| import { TagUtils } from "../../Tags/TagUtils" | ||||
| import BaseUIElement from "../../../UI/BaseUIElement" | ||||
| import { Utils } from "../../../Utils" | ||||
| import { regex_not_newline_characters } from "svelte/types/compiler/utils/patterns" | ||||
| import { render } from "sass" | ||||
| 
 | ||||
| /** | ||||
|  * Highly specialized feature source. | ||||
|  * Based on a lon/lat UIEVentSource, will generate the corresponding feature with the correct properties | ||||
|  */ | ||||
| export class LastClickFeatureSource implements FeatureSource { | ||||
|     features: Store<Feature[]> | ||||
| export class LastClickFeatureSource implements WritableFeatureSource { | ||||
|     public readonly features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([]) | ||||
| 
 | ||||
|     /** | ||||
|      * Must be public: passed as tags into the selected view | ||||
|      */ | ||||
|     public properties: Record<string, string> | ||||
| 
 | ||||
|     constructor(location: Store<{ lon: number; lat: number }>, layout: LayoutConfig) { | ||||
|         const allPresets: BaseUIElement[] = [] | ||||
|         for (const layer of layout.layers) | ||||
|  | @ -43,15 +45,16 @@ export class LastClickFeatureSource implements FeatureSource { | |||
|             first_preset: renderings[0], | ||||
|         } | ||||
|         this.properties = properties | ||||
|         this.features = location.mapD(({ lon, lat }) => [ | ||||
|             <Feature<Point>>{ | ||||
|         location.addCallbackAndRunD(({ lon, lat }) => { | ||||
|             const point = <Feature<Point>>{ | ||||
|                 type: "Feature", | ||||
|                 properties, | ||||
|                 geometry: { | ||||
|                     type: "Point", | ||||
|                     coordinates: [lon, lat], | ||||
|                 }, | ||||
|             }, | ||||
|         ]) | ||||
|             } | ||||
|             this.features.setData([point]) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,15 +1,16 @@ | |||
| import GeoJsonSource from "./GeoJsonSource" | ||||
| import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | ||||
| import FeatureSource from "../FeatureSource" | ||||
| import { FeatureSource } from "../FeatureSource" | ||||
| import { Or } from "../../Tags/Or" | ||||
| import FeatureSwitchState from "../../State/FeatureSwitchState" | ||||
| import OverpassFeatureSource from "./OverpassFeatureSource" | ||||
| import { Store } from "../../UIEventSource" | ||||
| import { ImmutableStore, Store } from "../../UIEventSource" | ||||
| import OsmFeatureSource from "./OsmFeatureSource" | ||||
| import FeatureSourceMerger from "./FeatureSourceMerger" | ||||
| import DynamicGeoJsonTileSource from "../TiledFeatureSource/DynamicGeoJsonTileSource" | ||||
| import { BBox } from "../../BBox" | ||||
| import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeatureSource" | ||||
| import StaticFeatureSource from "./StaticFeatureSource" | ||||
| 
 | ||||
| /** | ||||
|  * This source will fetch the needed data from various sources for the given layout. | ||||
|  | @ -78,6 +79,9 @@ export default class LayoutSource extends FeatureSourceMerger { | |||
|         backend: string, | ||||
|         featureSwitches: FeatureSwitchState | ||||
|     ): FeatureSource { | ||||
|         if (osmLayers.length == 0) { | ||||
|             return new StaticFeatureSource(new ImmutableStore([])) | ||||
|         } | ||||
|         const minzoom = Math.min(...osmLayers.map((layer) => layer.minzoom)) | ||||
|         const isActive = zoom.mapD((z) => { | ||||
|             if (z < minzoom) { | ||||
|  | @ -107,6 +111,9 @@ export default class LayoutSource extends FeatureSourceMerger { | |||
|         zoom: Store<number>, | ||||
|         featureSwitches: FeatureSwitchState | ||||
|     ): FeatureSource { | ||||
|         if (osmLayers.length == 0) { | ||||
|             return new StaticFeatureSource(new ImmutableStore([])) | ||||
|         } | ||||
|         const minzoom = Math.min(...osmLayers.map((layer) => layer.minzoom)) | ||||
|         const isActive = zoom.mapD((z) => { | ||||
|             if (z < minzoom) { | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { Changes } from "../../Osm/Changes" | ||||
| import { OsmNode, OsmObject, OsmRelation, OsmWay } from "../../Osm/OsmObject" | ||||
| import FeatureSource from "../FeatureSource" | ||||
| import { FeatureSource } from "../FeatureSource" | ||||
| import { UIEventSource } from "../../UIEventSource" | ||||
| import { ChangeDescription } from "../../Osm/Actions/ChangeDescription" | ||||
| import { ElementStorage } from "../../ElementStorage" | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import { Feature } from "geojson" | ||||
| import FeatureSource from "../FeatureSource" | ||||
| import { FeatureSource } from "../FeatureSource" | ||||
| import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" | ||||
| import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | ||||
| import { Or } from "../../Tags/Or" | ||||
|  |  | |||
|  | @ -1,37 +1,69 @@ | |||
| import FeatureSource from "../FeatureSource" | ||||
| import { Store } from "../../UIEventSource" | ||||
| import { FeatureSource } from "../FeatureSource" | ||||
| import { Store, UIEventSource } from "../../UIEventSource" | ||||
| import { Feature, Point } from "geojson" | ||||
| import { GeoOperations } from "../../GeoOperations" | ||||
| import { BBox } from "../../BBox" | ||||
| 
 | ||||
| export interface SnappingOptions { | ||||
|     /** | ||||
|      * If the distance is bigger then this amount, don't snap. | ||||
|      * In meter | ||||
|      */ | ||||
|     maxDistance?: number | ||||
|     maxDistance: number | ||||
| 
 | ||||
|     allowUnsnapped?: false | boolean | ||||
| 
 | ||||
|     /** | ||||
|      * The snapped-to way will be written into this | ||||
|      */ | ||||
|     snappedTo?: UIEventSource<string> | ||||
| 
 | ||||
|     /** | ||||
|      * The resulting snap coordinates will be written into this UIEventSource | ||||
|      */ | ||||
|     snapLocation?: UIEventSource<{ lon: number; lat: number }> | ||||
| } | ||||
| 
 | ||||
| export default class SnappingFeatureSource implements FeatureSource { | ||||
|     public readonly features: Store<Feature<Point>[]> | ||||
| 
 | ||||
|     private readonly _snappedTo: UIEventSource<string> | ||||
|     public readonly snappedTo: Store<string> | ||||
| 
 | ||||
|     constructor( | ||||
|         snapTo: FeatureSource, | ||||
|         location: Store<{ lon: number; lat: number }>, | ||||
|         options?: SnappingOptions | ||||
|         options: SnappingOptions | ||||
|     ) { | ||||
|         const simplifiedFeatures = snapTo.features.mapD((features) => | ||||
|             features | ||||
|                 .filter((feature) => feature.geometry.type !== "Point") | ||||
|                 .map((f) => GeoOperations.forceLineString(<any>f)) | ||||
|         ) | ||||
|         const maxDistance = options?.maxDistance | ||||
|         this._snappedTo = options.snappedTo ?? new UIEventSource<string>(undefined) | ||||
|         this.snappedTo = this._snappedTo | ||||
|         const simplifiedFeatures = snapTo.features | ||||
|             .mapD((features) => | ||||
|                 features | ||||
|                     .filter((feature) => feature.geometry.type !== "Point") | ||||
|                     .map((f) => GeoOperations.forceLineString(<any>f)) | ||||
|             ) | ||||
|             .map( | ||||
|                 (features) => { | ||||
|                     const { lon, lat } = location.data | ||||
|                     const loc: [number, number] = [lon, lat] | ||||
|                     return features.filter((f) => BBox.get(f).isNearby(loc, maxDistance)) | ||||
|                 }, | ||||
|                 [location] | ||||
|             ) | ||||
| 
 | ||||
|         location.mapD( | ||||
|         this.features = location.mapD( | ||||
|             ({ lon, lat }) => { | ||||
|                 const features = snapTo.features.data | ||||
|                 const features = simplifiedFeatures.data | ||||
|                 const loc: [number, number] = [lon, lat] | ||||
|                 const maxDistance = (options?.maxDistance ?? 1000) * 1000 | ||||
|                 const maxDistance = (options?.maxDistance ?? 1000) / 1000 | ||||
|                 let bestSnap: Feature<Point, { "snapped-to": string; dist: number }> = undefined | ||||
|                 for (const feature of features) { | ||||
|                     if (feature.geometry.type !== "LineString") { | ||||
|                         // TODO handle Polygons with holes
 | ||||
|                         continue | ||||
|                     } | ||||
|                     const snapped = GeoOperations.nearestPoint(<any>feature, loc) | ||||
|                     if (snapped.properties.dist > maxDistance) { | ||||
|                         continue | ||||
|  | @ -44,7 +76,23 @@ export default class SnappingFeatureSource implements FeatureSource { | |||
|                         bestSnap = <any>snapped | ||||
|                     } | ||||
|                 } | ||||
|                 return bestSnap | ||||
|                 this._snappedTo.setData(bestSnap?.properties?.["snapped-to"]) | ||||
|                 if (bestSnap === undefined && options?.allowUnsnapped) { | ||||
|                     bestSnap = { | ||||
|                         type: "Feature", | ||||
|                         geometry: { | ||||
|                             type: "Point", | ||||
|                             coordinates: [lon, lat], | ||||
|                         }, | ||||
|                         properties: { | ||||
|                             "snapped-to": undefined, | ||||
|                             dist: -1, | ||||
|                         }, | ||||
|                     } | ||||
|                 } | ||||
|                 const c = bestSnap.geometry.coordinates | ||||
|                 options?.snapLocation?.setData({ lon: c[0], lat: c[1] }) | ||||
|                 return [bestSnap] | ||||
|             }, | ||||
|             [snapTo.features] | ||||
|         ) | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import FeatureSource, { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||
| import { FeatureSource , FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||
| import { ImmutableStore, Store } from "../../UIEventSource" | ||||
| import FilteredLayer from "../../../Models/FilteredLayer" | ||||
| import { BBox } from "../../BBox" | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import FeatureSource, { FeatureSourceForLayer } from "../FeatureSource" | ||||
| import {FeatureSource, FeatureSourceForLayer } from "../FeatureSource" | ||||
| import StaticFeatureSource from "./StaticFeatureSource" | ||||
| import { GeoOperations } from "../../GeoOperations" | ||||
| import { BBox } from "../../BBox" | ||||
|  |  | |||
|  | @ -81,6 +81,7 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { | |||
|                 return new GeoJsonSource(layer, { | ||||
|                     zxy, | ||||
|                     featureIdBlacklist: blackList, | ||||
|                     isActive: options?.isActive, | ||||
|                 }) | ||||
|             }, | ||||
|             mapProperties, | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import { Store, Stores } from "../../UIEventSource" | ||||
| import { Tiles } from "../../../Models/TileRange" | ||||
| import { BBox } from "../../BBox" | ||||
| import FeatureSource from "../FeatureSource" | ||||
| import { FeatureSource } from "../FeatureSource" | ||||
| import FeatureSourceMerger from "../Sources/FeatureSourceMerger" | ||||
| 
 | ||||
| /*** | ||||
|  | @ -26,10 +26,6 @@ export default class DynamicTileSource extends FeatureSourceMerger { | |||
|             mapProperties.bounds | ||||
|                 .mapD( | ||||
|                     (bounds) => { | ||||
|                         if (options?.isActive?.data === false) { | ||||
|                             // No need to download! - the layer is disabled
 | ||||
|                             return undefined | ||||
|                         } | ||||
|                         const tileRange = Tiles.TileRangeBetween( | ||||
|                             zoomlevel, | ||||
|                             bounds.getNorth(), | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import FeatureSource, { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||
| import {FeatureSource, FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||
| import { OsmNode, OsmObject, OsmWay } from "../../Osm/OsmObject" | ||||
| import SimpleFeatureSource from "../Sources/SimpleFeatureSource" | ||||
| import FilteredLayer from "../../../Models/FilteredLayer" | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import { BBox } from "./BBox" | ||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig" | ||||
| import * as turf from "@turf/turf" | ||||
| import { AllGeoJSON, booleanWithin, Coord } from "@turf/turf" | ||||
| import { AllGeoJSON, booleanWithin, Coord, Lines } from "@turf/turf" | ||||
| import { | ||||
|     Feature, | ||||
|     GeoJSON, | ||||
|  | @ -273,7 +273,7 @@ export class GeoOperations { | |||
|      * @param point Point defined as [lon, lat] | ||||
|      */ | ||||
|     public static nearestPoint( | ||||
|         way: Feature<LineString | MultiLineString | Polygon | MultiPolygon>, | ||||
|         way: Feature<LineString>, | ||||
|         point: [number, number] | ||||
|     ): Feature< | ||||
|         Point, | ||||
|  | @ -951,4 +951,24 @@ export class GeoOperations { | |||
|         } | ||||
|         throw "CalculateIntersection fallthrough: can not calculate an intersection between features" | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Creates a linestring object based on the outer ring of the given polygon | ||||
|      * | ||||
|      * Returns the argument if not a polygon | ||||
|      * @param p | ||||
|      */ | ||||
|     public static outerRing<P>(p: Feature<Polygon | LineString, P>): Feature<LineString, P> { | ||||
|         if (p.geometry.type !== "Polygon") { | ||||
|             return <Feature<LineString, P>>p | ||||
|         } | ||||
|         return { | ||||
|             type: "Feature", | ||||
|             properties: p.properties, | ||||
|             geometry: { | ||||
|                 type: "LineString", | ||||
|                 coordinates: p.geometry.coordinates[0], | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ import CreateWayWithPointReuseAction, { MergePointConfig } from "./CreateWayWith | |||
| import { And } from "../../Tags/And" | ||||
| import { TagUtils } from "../../Tags/TagUtils" | ||||
| import { SpecialVisualizationState } from "../../../UI/SpecialVisualization" | ||||
| import FeatureSource from "../../FeatureSource/FeatureSource" | ||||
| import { FeatureSource } from "../../FeatureSource/FeatureSource" | ||||
| 
 | ||||
| /** | ||||
|  * More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points | ||||
|  |  | |||
|  | @ -104,9 +104,13 @@ export default class CreateNewNodeAction extends OsmCreateAction { | |||
|         // Project the point onto the way
 | ||||
|         console.log("Snapping a node onto an existing way...") | ||||
|         const geojson = this._snapOnto.asGeoJson() | ||||
|         const projected = GeoOperations.nearestPoint(geojson, [this._lon, this._lat]) | ||||
|         const projected = GeoOperations.nearestPoint(GeoOperations.outerRing(geojson), [ | ||||
|             this._lon, | ||||
|             this._lat, | ||||
|         ]) | ||||
|         const projectedCoor = <[number, number]>projected.geometry.coordinates | ||||
|         const index = projected.properties.index | ||||
|         console.log("Attempting to snap:", { geojson, projected, projectedCoor, index }) | ||||
|         // We check that it isn't close to an already existing point
 | ||||
|         let reusedPointId = undefined | ||||
|         let outerring: [number, number][] | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ import { ChangeDescription } from "./ChangeDescription" | |||
| import { BBox } from "../../BBox" | ||||
| import { TagsFilter } from "../../Tags/TagsFilter" | ||||
| import { GeoOperations } from "../../GeoOperations" | ||||
| import FeatureSource from "../../FeatureSource/FeatureSource" | ||||
| import { FeatureSource } from "../../FeatureSource/FeatureSource" | ||||
| import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource" | ||||
| import CreateNewNodeAction from "./CreateNewNodeAction" | ||||
| import CreateNewWayAction from "./CreateNewWayAction" | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ import OsmChangeAction from "./OsmChangeAction" | |||
| import { Changes } from "../Changes" | ||||
| import { ChangeDescription } from "./ChangeDescription" | ||||
| import { Tag } from "../../Tags/Tag" | ||||
| import FeatureSource from "../../FeatureSource/FeatureSource" | ||||
| import { FeatureSource } from "../../FeatureSource/FeatureSource" | ||||
| import { OsmNode, OsmObject, OsmWay } from "../OsmObject" | ||||
| import { GeoOperations } from "../../GeoOperations" | ||||
| import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource" | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ import { ChangeDescription, ChangeDescriptionTools } from "./Actions/ChangeDescr | |||
| import { Utils } from "../../Utils" | ||||
| import { LocalStorageSource } from "../Web/LocalStorageSource" | ||||
| import SimpleMetaTagger from "../SimpleMetaTagger" | ||||
| import FeatureSource, { IndexedFeatureSource } from "../FeatureSource/FeatureSource" | ||||
| import {FeatureSource, IndexedFeatureSource } from "../FeatureSource/FeatureSource" | ||||
| import { GeoLocationPointProperties } from "../State/GeoLocationState" | ||||
| import { GeoOperations } from "../GeoOperations" | ||||
| import { ChangesetHandler, ChangesetTag } from "./ChangesetHandler" | ||||
|  |  | |||
|  | @ -368,7 +368,7 @@ export class OsmConnection { | |||
|             "Content-Type": "application/json", | ||||
|         }) | ||||
|         const parsed = JSON.parse(response) | ||||
|         const id = parsed.properties.id | ||||
|         const id = parsed.properties | ||||
|         console.log("OPENED NOTE", id) | ||||
|         return id | ||||
|     } | ||||
|  |  | |||
|  | @ -73,7 +73,8 @@ export abstract class OsmObject { | |||
|         if (rawData["error"] !== undefined && rawData["statuscode"] === 410) { | ||||
|             return "deleted" | ||||
|         } | ||||
|         return rawData["content"].elements[0].tags | ||||
|         // Tags is undefined if the element does not have any tags
 | ||||
|         return rawData["content"].elements[0].tags ?? {} | ||||
|     } | ||||
| 
 | ||||
|     static async DownloadObjectAsync( | ||||
|  |  | |||
|  | @ -21,7 +21,7 @@ export default class LayerState { | |||
|     /** | ||||
|      * Which layers are enabled in the current theme and what filters are applied onto them | ||||
|      */ | ||||
|     public readonly filteredLayers: Map<string, FilteredLayer> | ||||
|     public readonly filteredLayers: ReadonlyMap<string, FilteredLayer> | ||||
|     private readonly osmConnection: OsmConnection | ||||
| 
 | ||||
|     /** | ||||
|  | @ -32,14 +32,15 @@ export default class LayerState { | |||
|      */ | ||||
|     constructor(osmConnection: OsmConnection, layers: LayerConfig[], context: string) { | ||||
|         this.osmConnection = osmConnection | ||||
|         this.filteredLayers = new Map() | ||||
|         const filteredLayers = new Map() | ||||
|         for (const layer of layers) { | ||||
|             this.filteredLayers.set( | ||||
|             filteredLayers.set( | ||||
|                 layer.id, | ||||
|                 FilteredLayer.initLinkedState(layer, context, this.osmConnection) | ||||
|             ) | ||||
|         } | ||||
|         layers.forEach((l) => this.linkFilterStates(l)) | ||||
|         this.filteredLayers = filteredLayers | ||||
|         layers.forEach((l) => LayerState.linkFilterStates(l, filteredLayers)) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -48,11 +49,14 @@ export default class LayerState { | |||
|      * | ||||
|      * This methods links those states for the given layer | ||||
|      */ | ||||
|     private linkFilterStates(layer: LayerConfig) { | ||||
|     private static linkFilterStates( | ||||
|         layer: LayerConfig, | ||||
|         filteredLayers: Map<string, FilteredLayer> | ||||
|     ) { | ||||
|         if (layer.filterIsSameAs === undefined) { | ||||
|             return | ||||
|         } | ||||
|         const toReuse = this.filteredLayers.get(layer.filterIsSameAs) | ||||
|         const toReuse = filteredLayers.get(layer.filterIsSameAs) | ||||
|         if (toReuse === undefined) { | ||||
|             throw ( | ||||
|                 "Error in layer " + | ||||
|  | @ -65,6 +69,6 @@ export default class LayerState { | |||
|         console.warn( | ||||
|             "Linking filter and isDisplayed-states of " + layer.id + " and " + layer.filterIsSameAs | ||||
|         ) | ||||
|         this.filteredLayers.set(layer.id, toReuse) | ||||
|         filteredLayers.set(layer.id, toReuse) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ 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 { FeatureSource, FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource" | ||||
| import StaticFeatureSource, { | ||||
|     TiledStaticFeatureSource, | ||||
| } from "../FeatureSource/Sources/StaticFeatureSource" | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ import { MangroveIdentity } from "../Web/MangroveReviews" | |||
| import { Store, Stores, UIEventSource } from "../UIEventSource" | ||||
| import Locale from "../../UI/i18n/Locale" | ||||
| import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource" | ||||
| import FeatureSource from "../FeatureSource/FeatureSource" | ||||
| import { FeatureSource } from "../FeatureSource/FeatureSource" | ||||
| import { Feature } from "geojson" | ||||
| 
 | ||||
| /** | ||||
|  |  | |||
|  | @ -4,7 +4,6 @@ import { TagsFilter } from "./TagsFilter" | |||
| export class Tag extends TagsFilter { | ||||
|     public key: string | ||||
|     public value: string | ||||
|     public static newlyCreated = new Tag("_newly_created", "yes") | ||||
|     constructor(key: string, value: string) { | ||||
|         super() | ||||
|         this.key = key | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import { Utils } from "../Utils" | ||||
| 
 | ||||
| export default class Constants { | ||||
|     public static vNumber = "0.27.0" | ||||
|     public static vNumber = "0.30.0" | ||||
| 
 | ||||
|     public static ImgurApiKey = "7070e7167f0a25a" | ||||
|     public static readonly mapillary_client_token_v4 = | ||||
|  |  | |||
|  | @ -1,8 +1,13 @@ | |||
| import { UIEventSource } from "../Logic/UIEventSource" | ||||
| import { Store, UIEventSource } from "../Logic/UIEventSource" | ||||
| import LayerConfig from "./ThemeConfig/LayerConfig" | ||||
| import { OsmConnection } from "../Logic/Osm/OsmConnection" | ||||
| import { LocalStorageSource } from "../Logic/Web/LocalStorageSource" | ||||
| import { QueryParameters } from "../Logic/Web/QueryParameters" | ||||
| import { FilterConfigOption } from "./ThemeConfig/FilterConfig" | ||||
| import { TagsFilter } from "../Logic/Tags/TagsFilter" | ||||
| import { Utils } from "../Utils" | ||||
| import { TagUtils } from "../Logic/Tags/TagUtils" | ||||
| import { And } from "../Logic/Tags/And" | ||||
| 
 | ||||
| export default class FilteredLayer { | ||||
|     /** | ||||
|  | @ -10,11 +15,22 @@ export default class FilteredLayer { | |||
|      */ | ||||
|     readonly isDisplayed: UIEventSource<boolean> | ||||
|     /** | ||||
|      * Maps the filter.option.id onto the actual used state | ||||
|      * Maps the filter.option.id onto the actual used state. | ||||
|      * This state is either the chosen option (as number) or a representation of the fields | ||||
|      */ | ||||
|     readonly appliedFilters: Map<string, UIEventSource<undefined | number | string>> | ||||
|     readonly appliedFilters: ReadonlyMap<string, UIEventSource<undefined | number | string>> | ||||
|     readonly layerDef: LayerConfig | ||||
| 
 | ||||
|     /** | ||||
|      * Indicates if some filter is set. | ||||
|      * If this is the case, adding a new element of this type might be a bad idea | ||||
|      */ | ||||
|     readonly hasFilter: Store<boolean> | ||||
| 
 | ||||
|     /** | ||||
|      * Contains the current properties a feature should fulfill in order to match the filter | ||||
|      */ | ||||
|     readonly currentFilter: Store<TagsFilter | undefined> | ||||
|     constructor( | ||||
|         layer: LayerConfig, | ||||
|         appliedFilters?: Map<string, UIEventSource<undefined | number | string>>, | ||||
|  | @ -24,6 +40,105 @@ export default class FilteredLayer { | |||
|         this.isDisplayed = isDisplayed ?? new UIEventSource(true) | ||||
|         this.appliedFilters = | ||||
|             appliedFilters ?? new Map<string, UIEventSource<number | string | undefined>>() | ||||
| 
 | ||||
|         const hasFilter = new UIEventSource<boolean>(false) | ||||
|         const self = this | ||||
|         const currentTags = new UIEventSource<TagsFilter>(undefined) | ||||
| 
 | ||||
|         this.appliedFilters.forEach((filterSrc) => { | ||||
|             filterSrc.addCallbackAndRun((filter) => { | ||||
|                 if ((filter ?? 0) !== 0) { | ||||
|                     hasFilter.setData(true) | ||||
|                     currentTags.setData(self.calculateCurrentTags()) | ||||
|                     return | ||||
|                 } | ||||
| 
 | ||||
|                 const hf = Array.from(self.appliedFilters.values()).some((f) => (f.data ?? 0) !== 0) | ||||
|                 if (hf) { | ||||
|                     currentTags.setData(self.calculateCurrentTags()) | ||||
|                 } else { | ||||
|                     currentTags.setData(undefined) | ||||
|                 } | ||||
|                 hasFilter.setData(hf) | ||||
|             }) | ||||
|         }) | ||||
| 
 | ||||
|         currentTags.addCallbackAndRunD((t) => console.log("Current filter is", t)) | ||||
| 
 | ||||
|         this.currentFilter = currentTags | ||||
|     } | ||||
| 
 | ||||
|     private calculateCurrentTags(): TagsFilter { | ||||
|         let needed: TagsFilter[] = [] | ||||
|         for (const filter of this.layerDef.filters) { | ||||
|             const state = this.appliedFilters.get(filter.id) | ||||
|             if (state.data === undefined) { | ||||
|                 continue | ||||
|             } | ||||
|             if (filter.options[0].fields.length > 0) { | ||||
|                 const fieldProperties = FilteredLayer.stringToFieldProperties(<string>state.data) | ||||
|                 const asTags = FilteredLayer.fieldsToTags(filter.options[0], fieldProperties) | ||||
|                 needed.push(asTags) | ||||
|                 continue | ||||
|             } | ||||
|             needed.push(filter.options[state.data].osmTags) | ||||
|         } | ||||
|         needed = Utils.NoNull(needed) | ||||
|         if (needed.length == 0) { | ||||
|             return undefined | ||||
|         } | ||||
|         let tags: TagsFilter | ||||
| 
 | ||||
|         if (needed.length == 1) { | ||||
|             tags = needed[1] | ||||
|         } else { | ||||
|             tags = new And(needed) | ||||
|         } | ||||
|         let optimized = tags.optimize() | ||||
|         if (optimized === true) { | ||||
|             return undefined | ||||
|         } | ||||
|         if (optimized === false) { | ||||
|             return tags | ||||
|         } | ||||
|         return optimized | ||||
|     } | ||||
| 
 | ||||
|     public static fieldsToString(values: Record<string, string>): string { | ||||
|         return JSON.stringify(values) | ||||
|     } | ||||
| 
 | ||||
|     public static stringToFieldProperties(value: string): Record<string, string> { | ||||
|         return JSON.parse(value) | ||||
|     } | ||||
| 
 | ||||
|     private static fieldsToTags( | ||||
|         option: FilterConfigOption, | ||||
|         fieldstate: string | Record<string, string> | ||||
|     ): TagsFilter { | ||||
|         let properties: Record<string, string> | ||||
|         if (typeof fieldstate === "string") { | ||||
|             properties = FilteredLayer.stringToFieldProperties(fieldstate) | ||||
|         } else { | ||||
|             properties = fieldstate | ||||
|         } | ||||
|         console.log("Building tagsspec with properties", properties) | ||||
|         const tagsSpec = Utils.WalkJson(option.originalTagsSpec, (v) => { | ||||
|             if (typeof v !== "string") { | ||||
|                 return v | ||||
|             } | ||||
| 
 | ||||
|             for (const key in properties) { | ||||
|                 v = (<string>v).replace("{" + key + "}", properties[key]) | ||||
|             } | ||||
| 
 | ||||
|             return v | ||||
|         }) | ||||
|         return TagUtils.Tag(tagsSpec) | ||||
|     } | ||||
| 
 | ||||
|     public disableAllFilters(): void { | ||||
|         this.appliedFilters.forEach((value) => value.setData(undefined)) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import { RasterLayerPolygon } from "./RasterLayers" | |||
| export interface MapProperties { | ||||
|     readonly location: UIEventSource<{ lon: number; lat: number }> | ||||
|     readonly zoom: UIEventSource<number> | ||||
|     readonly minzoom: UIEventSource<number> | ||||
|     readonly bounds: UIEventSource<BBox> | ||||
|     readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined> | ||||
|     readonly maxbounds: UIEventSource<undefined | BBox> | ||||
|  |  | |||
							
								
								
									
										64
									
								
								Models/MenuState.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								Models/MenuState.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,64 @@ | |||
| import LayerConfig from "./ThemeConfig/LayerConfig" | ||||
| import { UIEventSource } from "../Logic/UIEventSource" | ||||
| 
 | ||||
| /** | ||||
|  * Indicates if a menu is open, and if so, which tab is selected; | ||||
|  * Some tabs allow to highlight an element. | ||||
|  * | ||||
|  * Some convenience methods are provided for this as well | ||||
|  */ | ||||
| export class MenuState { | ||||
|     private static readonly _themeviewTabs = ["intro", "filters"] as const | ||||
|     public readonly themeIsOpened = new UIEventSource(true) | ||||
|     public readonly themeViewTabIndex: UIEventSource<number> | ||||
|     public readonly themeViewTab: UIEventSource<typeof MenuState._themeviewTabs[number]> | ||||
| 
 | ||||
|     private static readonly _menuviewTabs = ["about", "settings", "community", "privacy"] as const | ||||
|     public readonly menuIsOpened = new UIEventSource(false) | ||||
|     public readonly menuViewTabIndex: UIEventSource<number> | ||||
|     public readonly menuViewTab: UIEventSource<typeof MenuState._menuviewTabs[number]> | ||||
| 
 | ||||
|     public readonly highlightedLayerInFilters: UIEventSource<string> = new UIEventSource<string>( | ||||
|         undefined | ||||
|     ) | ||||
|     constructor() { | ||||
|         this.themeViewTabIndex = new UIEventSource(0) | ||||
|         this.themeViewTab = this.themeViewTabIndex.sync( | ||||
|             (i) => MenuState._themeviewTabs[i], | ||||
|             [], | ||||
|             (str) => MenuState._themeviewTabs.indexOf(<any>str) | ||||
|         ) | ||||
| 
 | ||||
|         this.menuViewTabIndex = new UIEventSource(1) | ||||
|         this.menuViewTab = this.menuViewTabIndex.sync( | ||||
|             (i) => MenuState._menuviewTabs[i], | ||||
|             [], | ||||
|             (str) => MenuState._menuviewTabs.indexOf(<any>str) | ||||
|         ) | ||||
|         this.themeIsOpened.addCallbackAndRun((isOpen) => { | ||||
|             if (!isOpen) { | ||||
|                 this.highlightedLayerInFilters.setData(undefined) | ||||
|             } | ||||
|         }) | ||||
|         this.themeViewTab.addCallbackAndRun((tab) => { | ||||
|             if (tab !== "filters") { | ||||
|                 this.highlightedLayerInFilters.setData(undefined) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|     public openFilterView(highlightLayer?: LayerConfig | string) { | ||||
|         this.themeIsOpened.setData(true) | ||||
|         this.themeViewTab.setData("filters") | ||||
|         if (highlightLayer) { | ||||
|             if (typeof highlightLayer !== "string") { | ||||
|                 highlightLayer = highlightLayer.id | ||||
|             } | ||||
|             this.highlightedLayerInFilters.setData(highlightLayer) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public closeAll() { | ||||
|         this.menuIsOpened.setData(false) | ||||
|         this.themeIsOpened.setData(false) | ||||
|     } | ||||
| } | ||||
|  | @ -11,15 +11,16 @@ import { RegexTag } from "../../Logic/Tags/RegexTag" | |||
| import BaseUIElement from "../../UI/BaseUIElement" | ||||
| import Table from "../../UI/Base/Table" | ||||
| import Combine from "../../UI/Base/Combine" | ||||
| 
 | ||||
| export type FilterConfigOption = { | ||||
|     question: Translation | ||||
|     osmTags: TagsFilter | undefined | ||||
|     /* Only set if fields are present. Used to create `osmTags` (which are used to _actually_ filter) when the field is written*/ | ||||
|     readonly originalTagsSpec: TagConfigJson | ||||
|     fields: { name: string; type: string }[] | ||||
| } | ||||
| export default class FilterConfig { | ||||
|     public readonly id: string | ||||
|     public readonly options: { | ||||
|         question: Translation | ||||
|         osmTags: TagsFilter | undefined | ||||
|         originalTagsSpec: TagConfigJson | ||||
|         fields: { name: string; type: string }[] | ||||
|     }[] | ||||
|     public readonly options: FilterConfigOption[] | ||||
|     public readonly defaultSelection?: number | ||||
| 
 | ||||
|     constructor(json: FilterConfigJson, context: string) { | ||||
|  |  | |||
|  | @ -2,12 +2,12 @@ import LayoutConfig from "./ThemeConfig/LayoutConfig" | |||
| import { SpecialVisualizationState } from "../UI/SpecialVisualization" | ||||
| import { Changes } from "../Logic/Osm/Changes" | ||||
| import { ImmutableStore, Store, UIEventSource } from "../Logic/UIEventSource" | ||||
| import FeatureSource, { | ||||
| import { | ||||
|     FeatureSource, | ||||
|     IndexedFeatureSource, | ||||
|     WritableFeatureSource, | ||||
| } from "../Logic/FeatureSource/FeatureSource" | ||||
| import { OsmConnection } from "../Logic/Osm/OsmConnection" | ||||
| import { DefaultGuiState } from "../UI/DefaultGuiState" | ||||
| import { MapProperties } from "./MapProperties" | ||||
| import LayerState from "../Logic/State/LayerState" | ||||
| import { Feature } from "geojson" | ||||
|  | @ -39,6 +39,8 @@ import Hotkeys from "../UI/Base/Hotkeys" | |||
| import Translations from "../UI/i18n/Translations" | ||||
| import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore" | ||||
| import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource" | ||||
| import SimpleFeatureSource from "../Logic/FeatureSource/Sources/SimpleFeatureSource" | ||||
| import { MenuState } from "./MenuState" | ||||
| 
 | ||||
| /** | ||||
|  * | ||||
|  | @ -63,11 +65,12 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|     readonly mapProperties: MapProperties | ||||
| 
 | ||||
|     readonly dataIsLoading: Store<boolean> // TODO
 | ||||
|     readonly guistate: DefaultGuiState | ||||
|     readonly guistate: MenuState | ||||
|     readonly fullNodeDatabase?: FullNodeDatabaseSource // TODO
 | ||||
| 
 | ||||
|     readonly historicalUserLocations: WritableFeatureSource | ||||
|     readonly indexedFeatures: IndexedFeatureSource | ||||
|     readonly newFeatures: WritableFeatureSource | ||||
|     readonly layerState: LayerState | ||||
|     readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | ||||
|     readonly availableLayers: Store<RasterLayerPolygon[]> | ||||
|  | @ -75,9 +78,10 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|     readonly userRelatedState: UserRelatedState | ||||
|     readonly geolocation: GeoLocationHandler | ||||
| 
 | ||||
|     readonly lastClickObject: WritableFeatureSource | ||||
|     constructor(layout: LayoutConfig) { | ||||
|         this.layout = layout | ||||
|         this.guistate = new DefaultGuiState() | ||||
|         this.guistate = new MenuState() | ||||
|         this.map = new UIEventSource<MlMap>(undefined) | ||||
|         const initial = new InitialMapPositioning(layout) | ||||
|         this.mapProperties = new MapLibreAdaptor(this.map, initial) | ||||
|  | @ -109,20 +113,26 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
| 
 | ||||
|         this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location) | ||||
| 
 | ||||
|         const self = this | ||||
|         this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id) | ||||
|         this.newFeatures = new SimpleFeatureSource(undefined) | ||||
|         this.indexedFeatures = new LayoutSource( | ||||
|             layout.layers, | ||||
|             this.featureSwitches, | ||||
|             new StaticFeatureSource([]), | ||||
|             this.newFeatures, | ||||
|             this.mapProperties, | ||||
|             this.osmConnection.Backend(), | ||||
|             (id) => this.layerState.filteredLayers.get(id).isDisplayed | ||||
|             (id) => self.layerState.filteredLayers.get(id).isDisplayed | ||||
|         ) | ||||
|         const lastClick = (this.lastClickObject = new LastClickFeatureSource( | ||||
|             this.mapProperties.lastClickLocation, | ||||
|             this.layout | ||||
|         )) | ||||
|         const indexedElements = this.indexedFeatures | ||||
|         this.featureProperties = new FeaturePropertiesStore(indexedElements) | ||||
|         const perLayer = new PerLayerFeatureSourceSplitter( | ||||
|             Array.from(this.layerState.filteredLayers.values()).filter( | ||||
|                 (l) => l.layerDef.source !== null | ||||
|                 (l) => l.layerDef?.source !== null | ||||
|             ), | ||||
|             indexedElements, | ||||
|             { | ||||
|  | @ -176,9 +186,10 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|         ) | ||||
| 
 | ||||
|         this.initActors() | ||||
|         this.drawSpecialLayers() | ||||
|         this.drawSpecialLayers(lastClick) | ||||
|         this.initHotkeys() | ||||
|         this.miscSetup() | ||||
|         console.log("State setup completed", this) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -197,21 +208,30 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|                 this.guistate.closeAll() | ||||
|             } | ||||
|         ) | ||||
| 
 | ||||
|         Hotkeys.RegisterHotkey( | ||||
|             { | ||||
|                 nomod: "b", | ||||
|             }, | ||||
|             Translations.t.hotkeyDocumentation.openLayersPanel, | ||||
|             () => { | ||||
|                 if (this.featureSwitches.featureSwitchFilter.data) { | ||||
|                     this.guistate.openFilterView() | ||||
|                 } | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Add the special layers to the map | ||||
|      * @private | ||||
|      */ | ||||
|     private drawSpecialLayers() { | ||||
|     private drawSpecialLayers(last_click: LastClickFeatureSource) { | ||||
|         type AddedByDefaultTypes = typeof Constants.added_by_default[number] | ||||
|         const empty = [] | ||||
|         { | ||||
|             // The last_click gets a _very_ special treatment
 | ||||
|             const last_click = new LastClickFeatureSource( | ||||
|                 this.mapProperties.lastClickLocation, | ||||
|                 this.layout | ||||
|             ) | ||||
| 
 | ||||
|             const last_click_layer = this.layerState.filteredLayers.get("last_click") | ||||
|             this.featureProperties.addSpecial( | ||||
|                 "last_click", | ||||
|  |  | |||
|  | @ -84,7 +84,7 @@ export class Tiles { | |||
|      * Return x, y of the tile containing (lat, lon) on the given zoom level | ||||
|      */ | ||||
|     static embedded_tile(lat: number, lon: number, z: number): { x: number; y: number; z: number } { | ||||
|         return { x: Tiles.lon2tile(lon, z), y: Tiles.lat2tile(lat, z), z: z } | ||||
|         return { x: Tiles.lon2tile(lon, z), y: Tiles.lat2tile(lat, z), z } | ||||
|     } | ||||
| 
 | ||||
|     static tileRangeFrom(bbox: BBox, zoomlevel: number) { | ||||
|  |  | |||
|  | @ -11,18 +11,17 @@ | |||
|   let mainElem: HTMLElement; | ||||
|   export let hideSignal: Store<any>; | ||||
|   function hide(){ | ||||
|     console.trace("Hiding...") | ||||
|       mainElem.style.visibility = "hidden"; | ||||
|   } | ||||
|   if (hideSignal) { | ||||
|     onDestroy(hideSignal.addCallbackD(() => { | ||||
|       console.trace("Hiding invitation") | ||||
|       console.log("Received hide signal") | ||||
|       hide() | ||||
|       return true; | ||||
|     })); | ||||
|   } | ||||
|    | ||||
| $: { | ||||
|     console.log("Binding listeners on", mainElem) | ||||
|   mainElem?.addEventListener("click",_ => hide()) | ||||
|   mainElem?.addEventListener("touchstart",_ => hide()) | ||||
| } | ||||
|  | @ -30,8 +29,8 @@ $: { | |||
| 
 | ||||
| 
 | ||||
| <div bind:this={mainElem} class="absolute bottom-0 right-0 w-full h-full"> | ||||
|   <div id="hand-container"> | ||||
|     <ToSvelte construct={Svg.hand_ui}></ToSvelte> | ||||
|   <div id="hand-container" class="pointer-events-none"> | ||||
|     <img src="./assets/svg/hand.svg"/> | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,11 +1,20 @@ | |||
| <script lang="ts"> | ||||
|   import { createEventDispatcher } from "svelte"; | ||||
|   import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||
| 
 | ||||
|   /** | ||||
|    * The slotted element will be shown on top, with a lower-opacity border | ||||
|    */ | ||||
|   const dispatch = createEventDispatcher<{ close }>(); | ||||
| </script> | ||||
| 
 | ||||
| <div class="absolute top-0 right-0 w-screen h-screen overflow-auto" style="background-color: #00000088"> | ||||
|   <div class="flex flex-col m-4 sm:m-6 md:m-8 p-4 sm:p-6 md:m-8 normal-background rounded normal-background"> | ||||
|     <slot name="close-button"> | ||||
|       <div class="w-8 h-8 absolute right-10 top-10 cursor-pointer" on:click={() => dispatch("close")}> | ||||
|         <XCircleIcon /> | ||||
|       </div> | ||||
|     </slot> | ||||
|     <slot></slot> | ||||
|   </div> | ||||
| </div> | ||||
|  |  | |||
							
								
								
									
										15
									
								
								UI/Base/LoginButton.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								UI/Base/LoginButton.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| <script lang="ts"> | ||||
|   import { OsmConnection } from "../../Logic/Osm/OsmConnection"; | ||||
|   import SubtleButton from "./SubtleButton.svelte"; | ||||
|   import Translations from "../i18n/Translations.js"; | ||||
|   import Tr from "./Tr.svelte"; | ||||
| 
 | ||||
|   export let osmConnection: OsmConnection | ||||
| </script> | ||||
| 
 | ||||
| <SubtleButton on:click={() => osmConnection.AttemptLogin()}> | ||||
|   <img slot="image" src="./assets/svg/login.svg" class="w-8"/> | ||||
|   <slot name="message" slot="message"> | ||||
|     <Tr  t={Translations.t.general.loginWithOpenStreetMap}/> | ||||
|   </slot> | ||||
| </SubtleButton> | ||||
							
								
								
									
										45
									
								
								UI/Base/LoginToggle.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								UI/Base/LoginToggle.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | |||
| <script lang="ts"> | ||||
|   import Loading from "./Loading.svelte"; | ||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization"; | ||||
|   import type { OsmServiceState } from "../../Logic/Osm/OsmConnection"; | ||||
|   import { Translation } from "../i18n/Translation"; | ||||
|   import Translations from "../i18n/Translations"; | ||||
|   import Tr from "./Tr.svelte"; | ||||
| 
 | ||||
|   export let state: SpecialVisualizationState; | ||||
|   /** | ||||
|    * If set, 'loading' will act as if we are already logged in. | ||||
|    */ | ||||
|   export let ignoreLoading: boolean = false | ||||
|   let loadingStatus = state.osmConnection.loadingStatus; | ||||
|   let badge = state.featureSwitches.featureSwitchUserbadge; | ||||
|   const t = Translations.t.general; | ||||
|   const offlineModes: Partial<Record<OsmServiceState, Translation>> = { | ||||
|     offline: t.loginFailedOfflineMode, | ||||
|     unreachable: t.loginFailedUnreachableMode, | ||||
|     readonly: t.loginFailedReadonlyMode | ||||
|   }; | ||||
|   const apiState = state.osmConnection.apiIsOnline; | ||||
| 
 | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| {#if $badge} | ||||
|   {#if !ignoreLoading && $loadingStatus === "loading"} | ||||
|     <slot name="loading"> | ||||
|       <Loading></Loading> | ||||
|     </slot> | ||||
|   {:else if $loadingStatus === "error"} | ||||
|     <div class="flex items-center alert max-w-64"> | ||||
|       <img src="./assets/svg/invalid.svg" class="w-8 h-8 m-2 shrink-0"> | ||||
|       <Tr t={offlineModes[$apiState]} /> | ||||
|     </div> | ||||
| 
 | ||||
|   {:else if $loadingStatus === "logged-in"} | ||||
|     <slot></slot> | ||||
|   {:else if $loadingStatus === "not-attempted"} | ||||
|     <slot name="not-logged-in"> | ||||
|        | ||||
|     </slot> | ||||
|   {/if} | ||||
| {/if} | ||||
|  | @ -8,6 +8,6 @@ | |||
| </script> | ||||
| 
 | ||||
| 
 | ||||
| <div on:click={e => dispatch("click", e)} class="subtle-background rounded-full min-w-10 w-fit h-10 m-0.5 md:m-1 p-1"> | ||||
| <div on:click={e => dispatch("click", e)} class="subtle-background rounded-full min-w-10 w-fit h-10 m-0.5 md:m-1 p-1 cursor-pointer"> | ||||
|   <slot class="m-4"></slot> | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <script lang="ts"> | ||||
|   import { onMount } from "svelte"; | ||||
|   import { createEventDispatcher, onMount } from "svelte"; | ||||
|   import { Store } from "../../Logic/UIEventSource"; | ||||
|   import BaseUIElement from "../BaseUIElement"; | ||||
|   import Img from "./Img"; | ||||
|  | @ -24,7 +24,7 @@ | |||
|   let imgElem: HTMLElement; | ||||
|   let msgElem: HTMLElement; | ||||
|   let imgClasses = "block justify-center shrink-0 mr-4 " + (options?.imgSize ?? "h-11 w-11"); | ||||
| 
 | ||||
|   const dispatch = createEventDispatcher<{click}>() | ||||
|   onMount(() => { | ||||
|     // Image | ||||
|     if (imgElem && imageUrl) { | ||||
|  | @ -47,15 +47,16 @@ | |||
| </script> | ||||
| 
 | ||||
| <svelte:element | ||||
|   class={(options.extraClasses??"") + 'flex hover:shadow-xl transition-[color,background-color,box-shadow] hover:bg-unsubtle'} | ||||
|   class={(options.extraClasses??"") + 'flex hover:shadow-xl transition-[color,background-color,box-shadow] hover:bg-unsubtle cursor-pointer'} | ||||
|   href={$href} | ||||
|   target={options?.newTab ? "_blank" : ""} | ||||
|   this={href === undefined ? "span" : "a"} | ||||
|   on:click={(e) => dispatch("click", e)} | ||||
| > | ||||
|   <slot name="image"> | ||||
|     {#if imageUrl !== undefined} | ||||
|       {#if typeof imageUrl === "string"} | ||||
|         <Img src={imageUrl} class={imgClasses+ " bg-red border border-black"}></Img> | ||||
|         <Img src={imageUrl} class={imgClasses}></Img> | ||||
|       {:else } | ||||
|         <template bind:this={imgElem} /> | ||||
|       {/if} | ||||
|  |  | |||
|  | @ -20,11 +20,15 @@ export default class SvelteUIElement< | |||
|         }): SvelteComponentTyped<Props, Events, Slots> | ||||
|     } | ||||
|     private readonly _props: Props | ||||
|     private readonly _events: Events | ||||
|     private readonly _slots: Slots | ||||
| 
 | ||||
|     constructor(svelteElement, props: Props) { | ||||
|     constructor(svelteElement, props: Props, events?: Events, slots?: Slots) { | ||||
|         super() | ||||
|         this._svelteComponent = svelteElement | ||||
|         this._props = props | ||||
|         this._events = events | ||||
|         this._slots = slots | ||||
|     } | ||||
| 
 | ||||
|     protected InnerConstructElement(): HTMLElement { | ||||
|  | @ -32,6 +36,8 @@ export default class SvelteUIElement< | |||
|         new this._svelteComponent({ | ||||
|             target: el, | ||||
|             props: this._props, | ||||
|             events: this._events, | ||||
|             slots: this._slots, | ||||
|         }) | ||||
|         return el | ||||
|     } | ||||
|  |  | |||
							
								
								
									
										68
									
								
								UI/Base/TabbedGroup.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								UI/Base/TabbedGroup.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,68 @@ | |||
| <script lang="ts"> | ||||
|   /** | ||||
|    * Thin wrapper around 'TabGroup' which binds the state | ||||
|    */ | ||||
| 
 | ||||
|   import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui"; | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource"; | ||||
| 
 | ||||
|   export let tab: UIEventSource<number>; | ||||
|   let tabElements: HTMLElement[] = []; | ||||
|   $: tabElements[$tab]?.click(); | ||||
|   $: { | ||||
|     if (tabElements[tab.data]) { | ||||
|       window.setTimeout(() =>   tabElements[tab.data].click(), 50) | ||||
|     } | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
| <TabGroup defaultIndex={1} on:change={(e) =>{if(e.detail >= 0){tab.setData( e.detail); }} }> | ||||
|   <TabList> | ||||
|     <Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}> | ||||
|       <div bind:this={tabElements[0]} class="flex"> | ||||
|         <slot name="title0"> | ||||
|           Tab 0 | ||||
|         </slot> | ||||
|       </div> | ||||
|     </Tab> | ||||
|     <Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}> | ||||
|       <div bind:this={tabElements[1]} class="flex"> | ||||
|         <slot name="title1" /> | ||||
|       </div> | ||||
|     </Tab> | ||||
|     <Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}> | ||||
|       <div bind:this={tabElements[2]} class="flex"> | ||||
|         <slot name="title2" /> | ||||
|       </div> | ||||
|     </Tab> | ||||
|     <Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}> | ||||
|       <div bind:this={tabElements[3]} class="flex"> | ||||
|         <slot name="title3" /> | ||||
|       </div> | ||||
|     </Tab> | ||||
|     <Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}> | ||||
|       <div bind:this={tabElements[4]} class="flex"> | ||||
|         <slot name="title4" /> | ||||
|       </div> | ||||
|     </Tab> | ||||
|   </TabList> | ||||
|   <TabPanels defaultIndex={$tab}> | ||||
|     <TabPanel> | ||||
|       <slot name="content0"> | ||||
|         <div>Empty</div> | ||||
|       </slot> | ||||
|     </TabPanel> | ||||
|     <TabPanel> | ||||
|       <slot name="content1" /> | ||||
|     </TabPanel> | ||||
|     <TabPanel> | ||||
|       <slot name="content2" /> | ||||
|     </TabPanel> | ||||
|     <TabPanel> | ||||
|       <slot name="content3" /> | ||||
|     </TabPanel> | ||||
|     <TabPanel> | ||||
|       <slot name="content4" /> | ||||
|     </TabPanel> | ||||
|   </TabPanels> | ||||
| </TabGroup> | ||||
|  | @ -1,75 +0,0 @@ | |||
| import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource"; | ||||
| import Combine from "../Base/Combine" | ||||
| import Translations from "../i18n/Translations" | ||||
| import { VariableUiElement } from "../Base/VariableUIElement" | ||||
| import FilteredLayer from "../../Models/FilteredLayer" | ||||
| import { TagUtils } from "../../Logic/Tags/TagUtils" | ||||
| import Svg from "../../Svg" | ||||
| 
 | ||||
| /** | ||||
|  * The icon with the 'plus'-sign and the preset icons spinning | ||||
|  * | ||||
|  */ | ||||
| export default class AddNewMarker extends Combine { | ||||
|     constructor(filteredLayers: UIEventSource<FilteredLayer[]>) { | ||||
|         const icons = new VariableUiElement( | ||||
|             filteredLayers.map((filteredLayers) => { | ||||
|                 const icons = [] | ||||
|                 let last = undefined | ||||
|                 for (const filteredLayer of filteredLayers) { | ||||
|                     const layer = filteredLayer.layerDef | ||||
|                     if (layer.name === undefined && !filteredLayer.isDisplayed.data) { | ||||
|                         continue | ||||
|                     } | ||||
|                     for (const preset of filteredLayer.layerDef.presets) { | ||||
|                         const tags = TagUtils.KVtoProperties(preset.tags) | ||||
|                         const icon = layer.mapRendering[0] | ||||
|                             .RenderIcon(new ImmutableStore<any>(tags), false) | ||||
|                             .html.SetClass("block relative") | ||||
|                             .SetStyle("width: 42px; height: 42px;") | ||||
|                         icons.push(icon) | ||||
|                         if (last === undefined) { | ||||
|                             last = layer.mapRendering[0] | ||||
|                                 .RenderIcon(new ImmutableStore<any>(tags), false) | ||||
|                                 .html.SetClass("block relative") | ||||
|                                 .SetStyle("width: 42px; height: 42px;") | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 if (icons.length === 0) { | ||||
|                     return undefined | ||||
|                 } | ||||
|                 if (icons.length === 1) { | ||||
|                     return icons[0] | ||||
|                 } | ||||
|                 icons.push(last) | ||||
|                 const elem = new Combine(icons).SetClass("flex") | ||||
|                 elem.SetClass("slide min-w-min").SetStyle( | ||||
|                     "animation: slide " + icons.length + "s linear infinite;" | ||||
|                 ) | ||||
|                 return elem | ||||
|             }) | ||||
|         ) | ||||
|         const label = Translations.t.general.add.addNewMapLabel | ||||
|             .Clone() | ||||
|             .SetClass( | ||||
|                 "block center absolute text-sm min-w-min pl-1 pr-1 bg-gray-400 rounded-3xl text-white opacity-65 whitespace-nowrap" | ||||
|             ) | ||||
|             .SetStyle("top: 65px; transform: translateX(-50%)") | ||||
|         super([ | ||||
|             new Combine([ | ||||
|                 Svg.add_pin_svg() | ||||
|                     .SetClass("absolute") | ||||
|                     .SetStyle("width: 50px; filter: drop-shadow(grey 0 0 10px"), | ||||
|                 new Combine([icons]) | ||||
|                     .SetStyle("width: 50px") | ||||
|                     .SetClass("absolute p-1 rounded-full overflow-hidden"), | ||||
|                 Svg.addSmall_svg() | ||||
|                     .SetClass("absolute animate-pulse") | ||||
|                     .SetStyle("width: 30px; left: 30px; top: 35px;"), | ||||
|             ]).SetClass("absolute"), | ||||
|             new Combine([label]).SetStyle("position: absolute; left: 50%"), | ||||
|         ]) | ||||
|         this.SetClass("block relative") | ||||
|     } | ||||
| } | ||||
|  | @ -1,223 +0,0 @@ | |||
| import Combine from "../Base/Combine" | ||||
| import { UIEventSource } from "../../Logic/UIEventSource" | ||||
| import Loc from "../../Models/Loc" | ||||
| import Svg from "../../Svg" | ||||
| import Toggle from "../Input/Toggle" | ||||
| import BaseUIElement from "../BaseUIElement" | ||||
| import { GeoOperations } from "../../Logic/GeoOperations" | ||||
| import Hotkeys from "../Base/Hotkeys" | ||||
| import Translations from "../i18n/Translations" | ||||
| 
 | ||||
| class SingleLayerSelectionButton extends Toggle { | ||||
|     public readonly activate: () => void | ||||
| 
 | ||||
|     /** | ||||
|      * | ||||
|      * The SingeLayerSelectionButton also acts as an actor to keep the layers in check | ||||
|      * | ||||
|      * It works the following way: | ||||
|      * | ||||
|      * - It has a boolean state to indicate wether or not the button is active | ||||
|      * - It keeps track of the available layers | ||||
|      */ | ||||
|     constructor( | ||||
|         locationControl: UIEventSource<Loc>, | ||||
|         options: { | ||||
|             currentBackground: UIEventSource<BaseLayer> | ||||
|             preferredType: string | ||||
|             preferredLayer?: BaseLayer | ||||
|             notAvailable?: () => void | ||||
|         } | ||||
|     ) { | ||||
|         const prefered = options.preferredType | ||||
|         const previousLayer = new UIEventSource(options.preferredLayer) | ||||
| 
 | ||||
|         const unselected = SingleLayerSelectionButton.getIconFor(prefered).SetClass( | ||||
|             "rounded-lg p-1 h-12 w-12 overflow-hidden subtle-background border-invisible" | ||||
|         ) | ||||
| 
 | ||||
|         const selected = SingleLayerSelectionButton.getIconFor(prefered).SetClass( | ||||
|             "rounded-lg p-1 h-12 w-12 overflow-hidden subtle-background border-attention-catch" | ||||
|         ) | ||||
| 
 | ||||
|         const available = AvailableBaseLayers.SelectBestLayerAccordingTo( | ||||
|             locationControl, | ||||
|             new UIEventSource<string | string[]>(options.preferredType) | ||||
|         ) | ||||
| 
 | ||||
|         let toggle: BaseUIElement = new Toggle( | ||||
|             selected, | ||||
|             unselected, | ||||
|             options.currentBackground.map((bg) => bg?.category === options.preferredType) | ||||
|         ) | ||||
| 
 | ||||
|         super( | ||||
|             toggle, | ||||
|             undefined, | ||||
|             available.map((av) => av?.category === options.preferredType) | ||||
|         ) | ||||
| 
 | ||||
|         /** | ||||
|          * Checks that the previous layer is still usable on the current location. | ||||
|          * If not, clears the 'previousLayer' | ||||
|          */ | ||||
|         function checkPreviousLayer() { | ||||
|             if (previousLayer.data === undefined) { | ||||
|                 return | ||||
|             } | ||||
|             if (previousLayer.data.feature === null || previousLayer.data.feature === undefined) { | ||||
|                 // Global layer
 | ||||
|                 return | ||||
|             } | ||||
|             const loc = locationControl.data | ||||
|             if (!GeoOperations.inside([loc.lon, loc.lat], previousLayer.data.feature)) { | ||||
|                 // The previous layer is out of bounds
 | ||||
|                 previousLayer.setData(undefined) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         unselected.onClick(() => { | ||||
|             // Note: a check if 'available' has the correct type is not needed:
 | ||||
|             // Unselected will _not_ be visible if availableBaseLayer has a wrong type!
 | ||||
|             checkPreviousLayer() | ||||
| 
 | ||||
|             previousLayer.setData(previousLayer.data ?? available.data) | ||||
|             options.currentBackground.setData(previousLayer.data) | ||||
|         }) | ||||
| 
 | ||||
|         options.currentBackground.addCallbackAndRunD((background) => { | ||||
|             if (background.category === options.preferredType) { | ||||
|                 previousLayer.setData(background) | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|         available.addCallbackD((availableLayer) => { | ||||
|             // Called whenever a better layer is available
 | ||||
| 
 | ||||
|             if (previousLayer.data === undefined) { | ||||
|                 // PreviousLayer is unset -> we definitively weren't using this category -> no need to switch
 | ||||
|                 return | ||||
|             } | ||||
|             if (options.currentBackground.data?.id !== previousLayer.data?.id) { | ||||
|                 // The previously used layer doesn't match the current layer -> no need to switch
 | ||||
|                 return | ||||
|             } | ||||
| 
 | ||||
|             // Is the previous layer still valid? If so, we don't bother to switch
 | ||||
|             if ( | ||||
|                 previousLayer.data.feature === null || | ||||
|                 GeoOperations.inside( | ||||
|                     [locationControl.data.lon, locationControl.data.lat], | ||||
|                     previousLayer.data.feature | ||||
|                 ) | ||||
|             ) { | ||||
|                 return | ||||
|             } | ||||
| 
 | ||||
|             if (availableLayer.category === options.preferredType) { | ||||
|                 // Allright, we can set this different layer
 | ||||
|                 options.currentBackground.setData(availableLayer) | ||||
|                 previousLayer.setData(availableLayer) | ||||
|             } else { | ||||
|                 // Uh oh - no correct layer is available... We pass the torch!
 | ||||
|                 if (options.notAvailable !== undefined) { | ||||
|                     options.notAvailable() | ||||
|                 } else { | ||||
|                     // Fallback to OSM carto
 | ||||
|                     options.currentBackground.setData(AvailableBaseLayers.osmCarto) | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|         this.activate = () => { | ||||
|             checkPreviousLayer() | ||||
|             if (available.data.category !== options.preferredType) { | ||||
|                 // This object can't help either - pass the torch!
 | ||||
|                 if (options.notAvailable !== undefined) { | ||||
|                     options.notAvailable() | ||||
|                 } else { | ||||
|                     // Fallback to OSM carto
 | ||||
|                     options.currentBackground.setData(AvailableBaseLayers.osmCarto) | ||||
|                 } | ||||
|                 return | ||||
|             } | ||||
| 
 | ||||
|             previousLayer.setData(previousLayer.data ?? available.data) | ||||
|             options.currentBackground.setData(previousLayer.data) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static getIconFor(type: string) { | ||||
|         switch (type) { | ||||
|             case "map": | ||||
|                 return Svg.generic_map_svg() | ||||
|             case "photo": | ||||
|                 return Svg.satellite_svg() | ||||
|             case "osmbasedmap": | ||||
|                 return Svg.osm_logo_svg() | ||||
|             default: | ||||
|                 return Svg.generic_map_svg() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default class BackgroundMapSwitch extends Combine { | ||||
|     /** | ||||
|      * Three buttons to easily switch map layers between OSM, aerial and some map. | ||||
|      * @param state | ||||
|      * @param currentBackground | ||||
|      * @param options | ||||
|      */ | ||||
|     constructor( | ||||
|         state: { | ||||
|             locationControl: UIEventSource<Loc> | ||||
|             backgroundLayer: UIEventSource<BaseLayer> | ||||
|         }, | ||||
|         currentBackground: UIEventSource<BaseLayer>, | ||||
|         options?: { | ||||
|             preferredCategory?: string | ||||
|             allowedCategories?: ("osmbasedmap" | "photo" | "map")[] | ||||
|             enableHotkeys?: boolean | ||||
|         } | ||||
|     ) { | ||||
|         const allowedCategories = options?.allowedCategories ?? ["osmbasedmap", "photo", "map"] | ||||
| 
 | ||||
|         const previousLayer = state.backgroundLayer.data | ||||
|         const buttons = [] | ||||
|         let activatePrevious: () => void = undefined | ||||
|         for (const category of allowedCategories) { | ||||
|             let preferredLayer = undefined | ||||
|             if (previousLayer?.category === category) { | ||||
|                 preferredLayer = previousLayer | ||||
|             } | ||||
| 
 | ||||
|             const button = new SingleLayerSelectionButton(state.locationControl, { | ||||
|                 preferredType: category, | ||||
|                 preferredLayer: preferredLayer, | ||||
|                 currentBackground: currentBackground, | ||||
|                 notAvailable: activatePrevious, | ||||
|             }) | ||||
|             // Fall back to the first option: OSM
 | ||||
|             activatePrevious = activatePrevious ?? button.activate | ||||
|             if (category === options?.preferredCategory) { | ||||
|                 button.activate() | ||||
|             } | ||||
| 
 | ||||
|             if (options?.enableHotkeys) { | ||||
|                 Hotkeys.RegisterHotkey( | ||||
|                     { nomod: category.charAt(0).toUpperCase() }, | ||||
|                     Translations.t.hotkeyDocumentation.selectBackground.Subs({ category }), | ||||
|                     () => { | ||||
|                         button.activate() | ||||
|                     } | ||||
|                 ) | ||||
|             } | ||||
|             buttons.push(button) | ||||
|         } | ||||
| 
 | ||||
|         // Selects the initial map
 | ||||
| 
 | ||||
|         super(buttons) | ||||
|         this.SetClass("flex") | ||||
|     } | ||||
| } | ||||
|  | @ -1,4 +1,3 @@ | |||
| import { Utils } from "../../Utils" | ||||
| import { VariableUiElement } from "../Base/VariableUIElement" | ||||
| import Toggle from "../Input/Toggle" | ||||
| import Combine from "../Base/Combine" | ||||
|  | @ -6,18 +5,9 @@ import Translations from "../i18n/Translations" | |||
| import { Translation } from "../i18n/Translation" | ||||
| import Svg from "../../Svg" | ||||
| import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
| import BaseUIElement from "../BaseUIElement" | ||||
| import FilteredLayer from "../../Models/FilteredLayer" | ||||
| import FilterConfig from "../../Models/ThemeConfig/FilterConfig" | ||||
| import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig" | ||||
| import { SubstitutedTranslation } from "../SubstitutedTranslation" | ||||
| import ValidatedTextField from "../Input/ValidatedTextField" | ||||
| import { QueryParameters } from "../../Logic/Web/QueryParameters" | ||||
| import { TagUtils } from "../../Logic/Tags/TagUtils" | ||||
| import { InputElement } from "../Input/InputElement" | ||||
| import { FixedUiElement } from "../Base/FixedUiElement" | ||||
| import Loc from "../../Models/Loc" | ||||
| import { BackToThemeOverview } from "./ActionButtons" | ||||
| 
 | ||||
| export default class FilterView extends VariableUiElement { | ||||
|     constructor( | ||||
|  | @ -31,11 +21,6 @@ export default class FilterView extends VariableUiElement { | |||
|             readonly featureSwitchMoreQuests: Store<boolean> | ||||
|         } | ||||
|     ) { | ||||
|         const backgroundSelector = new Toggle( | ||||
|             new BackgroundSelector(state), | ||||
|             undefined, | ||||
|             state.featureSwitchBackgroundSelection ?? new ImmutableStore(false) | ||||
|         ) | ||||
|         super( | ||||
|             filteredLayer.map((filteredLayers) => { | ||||
|                 // Create the views which toggle layers (and filters them) ...
 | ||||
|  | @ -51,10 +36,6 @@ export default class FilterView extends VariableUiElement { | |||
|                     tileLayers.map((tl) => FilterView.createOverlayToggle(state, tl)) | ||||
|                 ) | ||||
| 
 | ||||
|                 elements.push( | ||||
|                     backgroundSelector, | ||||
|                     new BackToThemeOverview(state, { imgSize: "h-6 w-6" }).SetClass("block mt-12") | ||||
|                 ) | ||||
|                 return elements | ||||
|             }) | ||||
|         ) | ||||
|  | @ -73,17 +54,8 @@ export default class FilterView extends VariableUiElement { | |||
|         const styledNameChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-2") | ||||
|         const styledNameUnChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-2") | ||||
| 
 | ||||
|         const zoomStatus = new Toggle( | ||||
|             undefined, | ||||
|             Translations.t.general.layerSelection.zoomInToSeeThisLayer | ||||
|                 .SetClass("alert") | ||||
|                 .SetStyle("display: block ruby;width:min-content;"), | ||||
|             state.locationControl?.map((location) => location.zoom >= config.config.minzoom) ?? | ||||
|                 new ImmutableStore(false) | ||||
|         ) | ||||
| 
 | ||||
|         const style = "display:flex;align-items:center;padding:0.5rem 0;" | ||||
|         const layerChecked = new Combine([icon, styledNameChecked, zoomStatus]) | ||||
|         const layerChecked = new Combine([icon, styledNameChecked]) | ||||
|             .SetStyle(style) | ||||
|             .onClick(() => config.isDisplayed.setData(false)) | ||||
| 
 | ||||
|  | @ -93,188 +65,4 @@ export default class FilterView extends VariableUiElement { | |||
| 
 | ||||
|         return new Toggle(layerChecked, layerNotChecked, config.isDisplayed) | ||||
|     } | ||||
| 
 | ||||
|     private static createOneFilteredLayerElement( | ||||
|         filteredLayer: FilteredLayer, | ||||
|         state: { featureSwitchIsDebugging?: Store<boolean>; locationControl?: Store<Loc> } | ||||
|     ) { | ||||
|         if (filteredLayer.layerDef.name === undefined) { | ||||
|             // Name is not defined: we hide this one
 | ||||
|             return new Toggle( | ||||
|                 new FixedUiElement(filteredLayer?.layerDef?.id).SetClass("block"), | ||||
|                 undefined, | ||||
|                 state?.featureSwitchIsDebugging ?? new ImmutableStore(false) | ||||
|             ) | ||||
|         } | ||||
|         const iconStyle = "width:1.5rem;height:1.5rem;margin-left:1.25rem;flex-shrink: 0;" | ||||
| 
 | ||||
|         const icon = new Combine([Svg.checkbox_filled]).SetStyle(iconStyle) | ||||
|         const layer = filteredLayer.layerDef | ||||
| 
 | ||||
|         const iconUnselected = new Combine([Svg.checkbox_empty]).SetStyle(iconStyle) | ||||
| 
 | ||||
|         const name: Translation = filteredLayer.layerDef.name.Clone() | ||||
| 
 | ||||
|         const styledNameChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-3") | ||||
| 
 | ||||
|         const styledNameUnChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-3") | ||||
| 
 | ||||
|         const zoomStatus = new Toggle( | ||||
|             undefined, | ||||
|             Translations.t.general.layerSelection.zoomInToSeeThisLayer | ||||
|                 .SetClass("alert") | ||||
|                 .SetStyle("display: block ruby;width:min-content;"), | ||||
|             state?.locationControl?.map( | ||||
|                 (location) => location.zoom >= filteredLayer.layerDef.minzoom | ||||
|             ) ?? new ImmutableStore(false) | ||||
|         ) | ||||
| 
 | ||||
|         const toggleClasses = "layer-toggle flex flex-wrap items-center pt-2 pb-2 px-0" | ||||
|         const layerIcon = layer.defaultIcon()?.SetClass("flex-shrink-0 w-8 h-8 ml-2") | ||||
|         const layerIconUnchecked = layer | ||||
|             .defaultIcon() | ||||
|             ?.SetClass("flex-shrink-0 opacity-50  w-8 h-8 ml-2") | ||||
| 
 | ||||
|         const layerChecked = new Combine([icon, layerIcon, styledNameChecked, zoomStatus]) | ||||
|             .SetClass(toggleClasses) | ||||
|             .onClick(() => filteredLayer.isDisplayed.setData(false)) | ||||
| 
 | ||||
|         const layerNotChecked = new Combine([ | ||||
|             iconUnselected, | ||||
|             layerIconUnchecked, | ||||
|             styledNameUnChecked, | ||||
|         ]) | ||||
|             .SetClass(toggleClasses) | ||||
|             .onClick(() => filteredLayer.isDisplayed.setData(true)) | ||||
| 
 | ||||
|         const filterPanel: BaseUIElement = new LayerFilterPanel(state, filteredLayer) | ||||
| 
 | ||||
|         return new Toggle( | ||||
|             new Combine([layerChecked, filterPanel]), | ||||
|             layerNotChecked, | ||||
|             filteredLayer.isDisplayed | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class LayerFilterPanel extends Combine { | ||||
|     public constructor(state: any, flayer: FilteredLayer) { | ||||
|         const layer = flayer.layerDef | ||||
|         if (layer.filters.length === 0) { | ||||
|             super([]) | ||||
|             return undefined | ||||
|         } | ||||
| 
 | ||||
|         const toShow: BaseUIElement[] = [] | ||||
| 
 | ||||
|         for (const filter of layer.filters) { | ||||
|             const [ui, actualTags] = LayerFilterPanel.createFilter(state, filter) | ||||
| 
 | ||||
|             ui.SetClass("mt-1") | ||||
|             toShow.push(ui) | ||||
|             actualTags.addCallbackAndRun((tagsToFilterFor) => { | ||||
|                 flayer.appliedFilters.data.set(filter.id, tagsToFilterFor) | ||||
|                 flayer.appliedFilters.ping() | ||||
|             }) | ||||
|             flayer.appliedFilters | ||||
|                 .map((dict) => dict.get(filter.id)) | ||||
|                 .addCallbackAndRun((filters) => actualTags.setData(filters)) | ||||
|         } | ||||
| 
 | ||||
|         super(toShow) | ||||
|         this.SetClass("flex flex-col p-2 ml-12 pl-1 pt-0 layer-filters") | ||||
|     } | ||||
| 
 | ||||
|     // Filter which uses one or more textfields
 | ||||
|     private static createFilterWithFields( | ||||
|         state: any, | ||||
|         filterConfig: FilterConfig | ||||
|     ): [BaseUIElement, UIEventSource<FilterState>] { | ||||
|         const filter = filterConfig.options[0] | ||||
|         const mappings = new Map<string, BaseUIElement>() | ||||
|         let allValid: Store<boolean> = new ImmutableStore(true) | ||||
|         var allFields: InputElement<string>[] = [] | ||||
|         const properties = new UIEventSource<any>({}) | ||||
|         for (const { name, type } of filter.fields) { | ||||
|             const value = QueryParameters.GetQueryParameter( | ||||
|                 "filter-" + filterConfig.id + "-" + name, | ||||
|                 "", | ||||
|                 "Value for filter " + filterConfig.id | ||||
|             ) | ||||
| 
 | ||||
|             const field = ValidatedTextField.ForType(type) | ||||
|                 .ConstructInputElement({ | ||||
|                     value, | ||||
|                 }) | ||||
|                 .SetClass("inline-block") | ||||
|             mappings.set(name, field) | ||||
|             const stable = value.stabilized(250) | ||||
|             stable.addCallbackAndRunD((v) => { | ||||
|                 properties.data[name] = v.toLowerCase() | ||||
|                 properties.ping() | ||||
|             }) | ||||
|             allFields.push(field) | ||||
|             allValid = allValid.map( | ||||
|                 (previous) => previous && field.IsValid(stable.data) && stable.data !== "", | ||||
|                 [stable] | ||||
|             ) | ||||
|         } | ||||
|         const tr = new SubstitutedTranslation( | ||||
|             filter.question, | ||||
|             new UIEventSource<any>({ id: filterConfig.id }), | ||||
|             state, | ||||
|             mappings | ||||
|         ) | ||||
|         const trigger: Store<FilterState> = allValid.map( | ||||
|             (isValid) => { | ||||
|                 if (!isValid) { | ||||
|                     return undefined | ||||
|                 } | ||||
|                 const props = properties.data | ||||
|                 // Replace all the field occurences in the tags...
 | ||||
|                 const tagsSpec = Utils.WalkJson(filter.originalTagsSpec, (v) => { | ||||
|                     if (typeof v !== "string") { | ||||
|                         return v | ||||
|                     } | ||||
| 
 | ||||
|                     for (const key in props) { | ||||
|                         v = (<string>v).replace("{" + key + "}", props[key]) | ||||
|                     } | ||||
| 
 | ||||
|                     return v | ||||
|                 }) | ||||
|                 const tagsFilter = TagUtils.Tag(tagsSpec) | ||||
|                 return { | ||||
|                     currentFilter: tagsFilter, | ||||
|                     state: JSON.stringify(props), | ||||
|                 } | ||||
|             }, | ||||
|             [properties] | ||||
|         ) | ||||
| 
 | ||||
|         const settableFilter = new UIEventSource<FilterState>(undefined) | ||||
|         trigger.addCallbackAndRun((state) => settableFilter.setData(state)) | ||||
|         settableFilter.addCallback((state) => { | ||||
|             if (state === undefined) { | ||||
|                 // still initializing
 | ||||
|                 return | ||||
|             } | ||||
|             if (state.currentFilter === undefined) { | ||||
|                 allFields.forEach((f) => f.GetValue().setData(undefined)) | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|         return [tr, settableFilter] | ||||
|     } | ||||
| 
 | ||||
|     private static createFilter( | ||||
|         state: {}, | ||||
|         filterConfig: FilterConfig | ||||
|     ): [BaseUIElement, UIEventSource<FilterState>] { | ||||
|         if (filterConfig.options[0].fields.length > 0) { | ||||
|             return LayerFilterPanel.createFilterWithFields(state, filterConfig) | ||||
|         } | ||||
| 
 | ||||
|         return undefined | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <script lang="ts">/** | ||||
|  * The FilterView shows the various options to enable/disable a single layer. | ||||
|  * The FilterView shows the various options to enable/disable a single layer or to only show a subset of the data. | ||||
|  */ | ||||
| import type FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
|  | @ -10,14 +10,19 @@ import type { Writable } from "svelte/store"; | |||
| import If from "../Base/If.svelte"; | ||||
| import Dropdown from "../Base/Dropdown.svelte"; | ||||
| import { onDestroy } from "svelte"; | ||||
| import { UIEventSource } from "../../Logic/UIEventSource"; | ||||
| import FilterviewWithFields from "./FilterviewWithFields.svelte"; | ||||
| import Tr from "../Base/Tr.svelte"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| 
 | ||||
| export let filteredLayer: FilteredLayer; | ||||
| export let zoomlevel: number; | ||||
| export let highlightedLayer: UIEventSource<string> | undefined; | ||||
| export let zoomlevel: UIEventSource<number>; | ||||
| let layer: LayerConfig = filteredLayer.layerDef; | ||||
| let isDisplayed: boolean = filteredLayer.isDisplayed.data; | ||||
| onDestroy(filteredLayer.isDisplayed.addCallbackAndRunD(d => { | ||||
|   isDisplayed = d; | ||||
|   return false | ||||
|   return false; | ||||
| })); | ||||
| 
 | ||||
| /** | ||||
|  | @ -34,9 +39,20 @@ function getBooleanStateFor(option: FilterConfig): Writable<boolean> { | |||
| function getStateFor(option: FilterConfig): Writable<number> { | ||||
|   return filteredLayer.appliedFilters.get(option.id); | ||||
| } | ||||
| 
 | ||||
| let mainElem: HTMLElement; | ||||
| $:  onDestroy( | ||||
|   highlightedLayer.addCallbackAndRun(highlightedLayer => { | ||||
|     if (highlightedLayer === filteredLayer.layerDef.id) { | ||||
|       mainElem?.classList?.add("glowing-shadow"); | ||||
|     } else { | ||||
|       mainElem?.classList?.remove("glowing-shadow"); | ||||
|     } | ||||
|   }) | ||||
| ); | ||||
| </script> | ||||
| {#if filteredLayer.layerDef.name} | ||||
|   <div> | ||||
|   <div bind:this={mainElem}> | ||||
|     <label class="flex gap-1"> | ||||
|       <Checkbox selected={filteredLayer.isDisplayed} /> | ||||
|       <If condition={filteredLayer.isDisplayed}> | ||||
|  | @ -45,6 +61,13 @@ function getStateFor(option: FilterConfig): Writable<number> { | |||
|       </If> | ||||
| 
 | ||||
|       {filteredLayer.layerDef.name} | ||||
| 
 | ||||
|       {#if $zoomlevel < layer.minzoom} | ||||
|         <span class="alert"> | ||||
|           <Tr t={Translations.t.general.layerSelection.zoomInToSeeThisLayer} /> | ||||
|         </span> | ||||
|       {/if} | ||||
| 
 | ||||
|     </label> | ||||
|     <If condition={filteredLayer.isDisplayed}> | ||||
|       <div id="subfilters" class="flex flex-col gap-y-1 mb-4 ml-4"> | ||||
|  | @ -59,6 +82,12 @@ function getStateFor(option: FilterConfig): Writable<number> { | |||
|               </label> | ||||
|             {/if} | ||||
| 
 | ||||
|             {#if filter.options.length === 1 && filter.options[0].fields.length > 0} | ||||
|               <FilterviewWithFields id={filter.id} filteredLayer={filteredLayer} | ||||
|                                     option={filter.options[0]}></FilterviewWithFields> | ||||
| 
 | ||||
|             {/if} | ||||
| 
 | ||||
|             {#if filter.options.length > 1} | ||||
|               <Dropdown value={getStateFor(filter)}> | ||||
|                 {#each filter.options as option, i} | ||||
|  |  | |||
							
								
								
									
										57
									
								
								UI/BigComponents/FilterviewWithFields.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								UI/BigComponents/FilterviewWithFields.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,57 @@ | |||
| <script lang="ts"> | ||||
|   import FilteredLayer from "../../Models/FilteredLayer"; | ||||
|   import type { FilterConfigOption } from "../../Models/ThemeConfig/FilterConfig"; | ||||
|   import Locale from "../i18n/Locale"; | ||||
|   import ValidatedInput from "../InputElement/ValidatedInput.svelte"; | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource"; | ||||
|   import { onDestroy } from "svelte"; | ||||
| 
 | ||||
|   export let filteredLayer: FilteredLayer; | ||||
|   export let option: FilterConfigOption; | ||||
|   export let id: string; | ||||
|   let parts: string[]; | ||||
|   let language = Locale.language; | ||||
|   $: { | ||||
|     parts = option.question.textFor($language).split("{"); | ||||
|   } | ||||
|   let fieldValues: Record<string, UIEventSource<string>> = {}; | ||||
|   let fieldTypes: Record<string, string> = {}; | ||||
|   let appliedFilter = <UIEventSource<string>>filteredLayer.appliedFilters.get(id); | ||||
|   let initialState: Record<string, string> = JSON.parse(appliedFilter.data ?? "{}"); | ||||
| 
 | ||||
|   function setFields() { | ||||
|     const properties: Record<string, string> = {}; | ||||
|     for (const key in fieldValues) { | ||||
|       const v = fieldValues[key].data; | ||||
|       const k = key.substring(0, key.length - 1); | ||||
|       if (v === undefined) { | ||||
|         properties[k] = undefined; | ||||
|       } else { | ||||
|         properties[k] = v; | ||||
|       } | ||||
|     } | ||||
|     appliedFilter.setData(FilteredLayer.fieldsToString(properties)); | ||||
|   } | ||||
| 
 | ||||
|   for (const field of option.fields) { | ||||
|     // A bit of cheating: the 'parts' will have '}' suffixed for fields | ||||
|     fieldTypes[field.name + "}"] = field.type; | ||||
|     const src = new UIEventSource<string>(initialState[field.name] ?? ""); | ||||
|     fieldValues[field.name + "}"] = src; | ||||
|     onDestroy(src.addCallback(v => { | ||||
|       setFields(); | ||||
|     })); | ||||
|   } | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <div> | ||||
|   {#each parts as part, i} | ||||
|     {#if part.endsWith("}")} | ||||
|       <!-- This is a field! --> | ||||
|       <ValidatedInput value={fieldValues[part]} type={fieldTypes[part]} /> | ||||
|     {:else} | ||||
|       {part} | ||||
|     {/if} | ||||
|   {/each} | ||||
| </div> | ||||
|  | @ -15,10 +15,7 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | |||
| import { Utils } from "../../Utils" | ||||
| import UserRelatedState from "../../Logic/State/UserRelatedState" | ||||
| import Loc from "../../Models/Loc" | ||||
| import BaseLayer from "../../Models/BaseLayer" | ||||
| import FilteredLayer from "../../Models/FilteredLayer" | ||||
| import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" | ||||
| import PrivacyPolicy from "./PrivacyPolicy" | ||||
| import Hotkeys from "../Base/Hotkeys" | ||||
| 
 | ||||
| export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { | ||||
|  | @ -84,12 +81,6 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { | |||
|             tabs.push({ header: Svg.share_img, content: new ShareScreen(state) }) | ||||
|         } | ||||
| 
 | ||||
|         const privacy = { | ||||
|             header: Svg.eye_svg(), | ||||
|             content: new PrivacyPolicy(), | ||||
|         } | ||||
|         tabs.push(privacy) | ||||
| 
 | ||||
|         return tabs | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| <script lang="ts"> | ||||
| 
 | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource"; | ||||
|   import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
|   import type { Feature } from "geojson"; | ||||
|   import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
|   import ToSvelte from "../Base/ToSvelte.svelte"; | ||||
|  | @ -11,9 +10,9 @@ | |||
|   import Hotkeys from "../Base/Hotkeys"; | ||||
|   import { Geocoding } from "../../Logic/Osm/Geocoding"; | ||||
|   import { BBox } from "../../Logic/BBox"; | ||||
|   import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore"; | ||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization"; | ||||
| 
 | ||||
|   Translations.t; | ||||
|   export let state: SpecialVisualizationState | ||||
|   export let bounds: UIEventSource<BBox> | ||||
|   export let selectedElement: UIEventSource<Feature>; | ||||
|   export let selectedLayer: UIEventSource<LayerConfig>; | ||||
|  | @ -50,6 +49,7 @@ | |||
|       const [lat0, lat1, lon0, lon1] = poi.boundingbox | ||||
|       bounds.set(new BBox([[lon0, lat0], [lon1, lat1]]).pad(0.01)) | ||||
|       const id = poi.osm_type + "/" + poi.osm_id | ||||
|       const perLayer = state.perLayer | ||||
|       const layers = Array.from(perLayer.values()) | ||||
|       for (const layer of layers) { | ||||
|         const found = layer.features.data.find(f => f.properties.id === id) | ||||
|  |  | |||
|  | @ -1,18 +1,13 @@ | |||
| import Combine from "../Base/Combine" | ||||
| import ScrollableFullScreen from "../Base/ScrollableFullScreen" | ||||
| import Translations from "../i18n/Translations" | ||||
| import Toggle from "../Input/Toggle" | ||||
| import MapControlButton from "../MapControlButton" | ||||
| import Svg from "../../Svg" | ||||
| import AllDownloads from "./AllDownloads" | ||||
| import FilterView from "./FilterView" | ||||
| import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
| import BackgroundMapSwitch from "./BackgroundMapSwitch" | ||||
| import Lazy from "../Base/Lazy" | ||||
| import { VariableUiElement } from "../Base/VariableUIElement" | ||||
| import FeatureInfoBox from "../Popup/FeatureInfoBox" | ||||
| import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" | ||||
| import Hotkeys from "../Base/Hotkeys" | ||||
| import { DefaultGuiState } from "../DefaultGuiState" | ||||
| 
 | ||||
| export default class LeftControls extends Combine { | ||||
|  | @ -74,32 +69,7 @@ export default class LeftControls extends Combine { | |||
|             ) | ||||
|         ) | ||||
| 
 | ||||
|         new ScrollableFullScreen( | ||||
|             () => Translations.t.general.layerSelection.title.Clone(), | ||||
|             () => | ||||
|                 new FilterView(state.filteredLayers, state.overlayToggles, state).SetClass( | ||||
|                     "block p-1" | ||||
|                 ), | ||||
|             "filters", | ||||
|             guiState.filterViewIsOpened | ||||
|         ) | ||||
|         state.featureSwitchFilter.addCallbackAndRun((f) => { | ||||
|             Hotkeys.RegisterHotkey( | ||||
|                 { nomod: "B" }, | ||||
|                 Translations.t.hotkeyDocumentation.openLayersPanel, | ||||
|                 () => { | ||||
|                     guiState.filterViewIsOpened.setData(!guiState.filterViewIsOpened.data) | ||||
|                 } | ||||
|             ) | ||||
|         }) | ||||
| 
 | ||||
|         const mapSwitch = new Toggle( | ||||
|             new BackgroundMapSwitch(state, state.backgroundLayer, { enableHotkeys: true }), | ||||
|             undefined, | ||||
|             state.featureSwitchBackgroundSelection | ||||
|         ) | ||||
| 
 | ||||
|         super([currentViewAction, filterButton, downloadButton, mapSwitch]) | ||||
|         super([currentViewAction, downloadButton]) | ||||
| 
 | ||||
|         this.SetClass("flex flex-col") | ||||
|     } | ||||
|  |  | |||
							
								
								
									
										100
									
								
								UI/BigComponents/NewPointLocationInput.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								UI/BigComponents/NewPointLocationInput.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,100 @@ | |||
| <script lang="ts"> | ||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization"; | ||||
|   import LocationInput from "../InputElement/Helpers/LocationInput.svelte"; | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource"; | ||||
|   import { Tiles } from "../../Models/TileRange"; | ||||
|   import { Map as MlMap } from "maplibre-gl"; | ||||
|   import { BBox } from "../../Logic/BBox"; | ||||
|   import type { MapProperties } from "../../Models/MapProperties"; | ||||
|   import ShowDataLayer from "../Map/ShowDataLayer"; | ||||
|   import type { FeatureSource, FeatureSourceForLayer } from "../../Logic/FeatureSource/FeatureSource"; | ||||
| 
 | ||||
|   import SnappingFeatureSource from "../../Logic/FeatureSource/Sources/SnappingFeatureSource"; | ||||
|   import FeatureSourceMerger from "../../Logic/FeatureSource/Sources/FeatureSourceMerger"; | ||||
|   import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
|   import { Utils } from "../../Utils"; | ||||
| 
 | ||||
|   /** | ||||
|    * An advanced location input, which has support to: | ||||
|    * - Show more layers | ||||
|    * - Snap to layers | ||||
|    * | ||||
|    * This one is mostly used to insert new points | ||||
|    */ | ||||
|   export let state: SpecialVisualizationState; | ||||
|   /** | ||||
|    * The start coordinate | ||||
|    */ | ||||
|   export let coordinate: { lon: number, lat: number }; | ||||
|   export let snapToLayers: string[] | undefined; | ||||
|   export let targetLayer: LayerConfig; | ||||
|   export let maxSnapDistance: number = undefined; | ||||
| 
 | ||||
|   export let snappedTo: UIEventSource<string | undefined>; | ||||
|   export let value: UIEventSource<{ lon: number, lat: number }>; | ||||
|   if (value.data === undefined) { | ||||
|     value.setData(coordinate); | ||||
|   } | ||||
| 
 | ||||
|   let preciseLocation: UIEventSource<{ lon: number, lat: number }> = new UIEventSource<{ lon: number; lat: number }>(coordinate); | ||||
|   const xyz = Tiles.embedded_tile(coordinate.lat, coordinate.lon, 16); | ||||
|   const map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined); | ||||
|   let initialMapProperties: Partial<MapProperties> = { | ||||
|     zoom: new UIEventSource<number>(19), | ||||
|     maxbounds: new UIEventSource(undefined), | ||||
|     /*If no snapping needed: the value is simply the map location; | ||||
|     * If snapping is needed: the value will be set later on by the snapping feature source | ||||
|     * */ | ||||
|     location: snapToLayers.length === 0 ? value : new UIEventSource<{ lon: number; lat: number }>(coordinate), | ||||
|     bounds: new UIEventSource<BBox>(undefined), | ||||
|     allowMoving: new UIEventSource<boolean>(true), | ||||
|     allowZooming: new UIEventSource<boolean>(true), | ||||
|     minzoom: new UIEventSource<number>(18) | ||||
|   }; | ||||
| 
 | ||||
|   initialMapProperties.bounds.addCallbackAndRunD((bounds: BBox) => { | ||||
|     const max = bounds.pad(3).squarify(); | ||||
|     initialMapProperties.maxbounds.setData(max); | ||||
|     return true; // unregister | ||||
|   }); | ||||
| 
 | ||||
|   if (snapToLayers?.length > 0) { | ||||
| 
 | ||||
|     const snapSources: FeatureSource[] = []; | ||||
|     for (const layerId of (snapToLayers ?? [])) { | ||||
|       const layer: FeatureSourceForLayer = state.perLayer.get(layerId); | ||||
|       snapSources.push(layer); | ||||
|       if (layer.features === undefined) { | ||||
|         continue; | ||||
|       } | ||||
|       new ShowDataLayer(map, { | ||||
|         layer: layer.layer.layerDef, | ||||
|         zoomToFeatures: false, | ||||
|         features: layer | ||||
|       }); | ||||
|     } | ||||
|     const snappedLocation = new SnappingFeatureSource( | ||||
|       new FeatureSourceMerger(...Utils.NoNull(snapSources)), | ||||
|       // We snap to the (constantly updating) map location | ||||
|       initialMapProperties.location, | ||||
|       { | ||||
|         maxDistance: maxSnapDistance ?? 15, | ||||
|         allowUnsnapped: true, | ||||
|         snappedTo, | ||||
|         snapLocation: value | ||||
|       } | ||||
|     ); | ||||
| 
 | ||||
|     new ShowDataLayer(map, { | ||||
|       layer: targetLayer, | ||||
|       features: snappedLocation | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <div class="w-full h-64"> | ||||
|   <LocationInput {map} mapProperties={initialMapProperties} | ||||
|                  value={preciseLocation}></LocationInput> | ||||
| </div> | ||||
|  | @ -9,21 +9,16 @@ import BaseUIElement from "../BaseUIElement" | |||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||
| import Loc from "../../Models/Loc" | ||||
| import BaseLayer from "../../Models/BaseLayer" | ||||
| import FilteredLayer from "../../Models/FilteredLayer" | ||||
| import { InputElement } from "../Input/InputElement" | ||||
| import { CheckBox } from "../Input/Checkboxes" | ||||
| import { SubtleButton } from "../Base/SubtleButton" | ||||
| import LZString from "lz-string" | ||||
| import { SpecialVisualizationState } from "../SpecialVisualization" | ||||
| 
 | ||||
| export default class ShareScreen extends Combine { | ||||
|     constructor(state: { | ||||
|         layoutToUse: LayoutConfig | ||||
|         locationControl: UIEventSource<Loc> | ||||
|         backgroundLayer: UIEventSource<BaseLayer> | ||||
|         filteredLayers: UIEventSource<FilteredLayer[]> | ||||
|     }) { | ||||
|         const layout = state?.layoutToUse | ||||
|     constructor(state: SpecialVisualizationState) { | ||||
|         const layout = state?.layout | ||||
|         const tr = Translations.t.general.sharescreen | ||||
| 
 | ||||
|         const optionCheckboxes: InputElement<boolean>[] = [] | ||||
|  | @ -32,7 +27,8 @@ export default class ShareScreen extends Combine { | |||
|         const includeLocation = new CheckBox(tr.fsIncludeCurrentLocation, true) | ||||
|         optionCheckboxes.push(includeLocation) | ||||
| 
 | ||||
|         const currentLocation = state.locationControl | ||||
|         const currentLocation = state.mapProperties.location | ||||
|         const zoom = state.mapProperties.zoom | ||||
| 
 | ||||
|         optionParts.push( | ||||
|             includeLocation.GetValue().map( | ||||
|  | @ -42,7 +38,7 @@ export default class ShareScreen extends Combine { | |||
|                     } | ||||
|                     if (includeL) { | ||||
|                         return [ | ||||
|                             ["z", currentLocation.data?.zoom], | ||||
|                             ["z", zoom.data], | ||||
|                             ["lat", currentLocation.data?.lat], | ||||
|                             ["lon", currentLocation.data?.lon], | ||||
|                         ] | ||||
|  | @ -53,7 +49,7 @@ export default class ShareScreen extends Combine { | |||
|                         return null | ||||
|                     } | ||||
|                 }, | ||||
|                 [currentLocation] | ||||
|                 [currentLocation, zoom] | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
|  | @ -67,8 +63,8 @@ export default class ShareScreen extends Combine { | |||
|             return "layer-" + flayer.layerDef.id + "=" + flayer.isDisplayed.data | ||||
|         } | ||||
| 
 | ||||
|         const currentLayer: UIEventSource<{ id: string; name: string; layer: any }> = | ||||
|             state.backgroundLayer | ||||
|         const currentLayer: Store<{ id: string; name: string } | undefined> = | ||||
|             state.mapProperties.rasterLayer.map((l) => l?.properties) | ||||
|         const currentBackground = new VariableUiElement( | ||||
|             currentLayer.map((layer) => { | ||||
|                 return tr.fsIncludeCurrentBackgroundMap.Subs({ name: layer?.name ?? "" }) | ||||
|  | @ -96,7 +92,9 @@ export default class ShareScreen extends Combine { | |||
|             includeLayerChoices.GetValue().map( | ||||
|                 (includeLayerSelection) => { | ||||
|                     if (includeLayerSelection) { | ||||
|                         return Utils.NoNull(state.filteredLayers.data.map(fLayerToParam)).join("&") | ||||
|                         return Utils.NoNull( | ||||
|                             state.layerState.filteredLayers.map(fLayerToParam) | ||||
|                         ).join("&") | ||||
|                     } else { | ||||
|                         return null | ||||
|                     } | ||||
|  |  | |||
|  | @ -1,29 +1,22 @@ | |||
| /** | ||||
|  * Asks to add a feature at the last clicked location, at least if zoom is sufficient | ||||
|  */ | ||||
| import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource" | ||||
| import Svg from "../../Svg" | ||||
| import { SubtleButton } from "../Base/SubtleButton" | ||||
| import Combine from "../Base/Combine" | ||||
| import { UIEventSource } from "../../Logic/UIEventSource" | ||||
| import Translations from "../i18n/Translations" | ||||
| import Constants from "../../Models/Constants" | ||||
| import { TagUtils } from "../../Logic/Tags/TagUtils" | ||||
| import BaseUIElement from "../BaseUIElement" | ||||
| import { VariableUiElement } from "../Base/VariableUIElement" | ||||
| import Toggle from "../Input/Toggle" | ||||
| import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection" | ||||
| import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction" | ||||
| import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject" | ||||
| import PresetConfig from "../../Models/ThemeConfig/PresetConfig" | ||||
| import FilteredLayer from "../../Models/FilteredLayer" | ||||
| import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint" | ||||
| import Loading from "../Base/Loading" | ||||
| import Hash from "../../Logic/Web/Hash" | ||||
| import { WayId } from "../../Models/OsmFeature" | ||||
| import { Tag } from "../../Logic/Tags/Tag" | ||||
| import { LoginToggle } from "../Popup/LoginButton" | ||||
| import { SpecialVisualizationState } from "../SpecialVisualization" | ||||
| import { Feature } from "geojson" | ||||
| import { FixedUiElement } from "../Base/FixedUiElement" | ||||
| 
 | ||||
| /* | ||||
|  * The SimpleAddUI is a single panel, which can have multiple states: | ||||
|  | @ -40,33 +33,18 @@ export interface PresetInfo extends PresetConfig { | |||
|     boundsFactor?: 0.25 | number | ||||
| } | ||||
| 
 | ||||
| export default class SimpleAddUI extends LoginToggle { | ||||
|     /** | ||||
|      * | ||||
|      */ | ||||
| export default class SimpleAddUI extends Toggle { | ||||
|     constructor(state: SpecialVisualizationState) { | ||||
|         const readYourMessages = new Combine([ | ||||
|             Translations.t.general.readYourMessages.Clone().SetClass("alert"), | ||||
|             new SubtleButton(Svg.envelope_ui(), Translations.t.general.goToInbox, { | ||||
|                 url: "https://www.openstreetmap.org/messages/inbox", | ||||
|                 newTab: false, | ||||
|             }), | ||||
|         ]) | ||||
| 
 | ||||
|         const filterViewIsOpened = state.guistate.filterViewIsOpened | ||||
|         const takeLocationFrom = state.mapProperties.lastClickLocation | ||||
|         const selectedPreset = new UIEventSource<PresetInfo>(undefined) | ||||
| 
 | ||||
|         takeLocationFrom.addCallback((_) => selectedPreset.setData(undefined)) | ||||
| 
 | ||||
|         const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset, state) | ||||
| 
 | ||||
|         async function createNewPoint( | ||||
|             tags: Tag[], | ||||
|             location: { lat: number; lon: number }, | ||||
|             snapOntoWay?: OsmWay | ||||
|         ): Promise<void> { | ||||
|             tags.push(new Tag(Tag.newlyCreated.key, new Date().toISOString())) | ||||
|             if (snapOntoWay) { | ||||
|                 tags.push(new Tag("_referencing_ways", "way/" + snapOntoWay.id)) | ||||
|             } | ||||
|  | @ -86,10 +64,6 @@ export default class SimpleAddUI extends LoginToggle { | |||
| 
 | ||||
|         const addUi = new VariableUiElement( | ||||
|             selectedPreset.map((preset) => { | ||||
|                 if (preset === undefined) { | ||||
|                     return presetsOverview | ||||
|                 } | ||||
| 
 | ||||
|                 function confirm( | ||||
|                     tags: any[], | ||||
|                     location: { lat: number; lon: number }, | ||||
|  | @ -113,7 +87,7 @@ export default class SimpleAddUI extends LoginToggle { | |||
|                     { category: preset.name }, | ||||
|                     preset.name["context"] | ||||
|                 ) | ||||
|                 return new ConfirmLocationOfPoint( | ||||
|                 return new FixedUiElement("ConfirmLocationOfPoint...") /*ConfirmLocationOfPoint( | ||||
|                     state, | ||||
|                     filterViewIsOpened, | ||||
|                     preset, | ||||
|  | @ -128,140 +102,14 @@ export default class SimpleAddUI extends LoginToggle { | |||
|                         cancelIcon: Svg.back_svg(), | ||||
|                         cancelText: Translations.t.general.add.backToSelect, | ||||
|                     } | ||||
|                 ) | ||||
|                 )*/ | ||||
|             }) | ||||
|         ) | ||||
| 
 | ||||
|         super( | ||||
|             new Toggle( | ||||
|                 new Toggle( | ||||
|                     new Toggle( | ||||
|                         new Loading(Translations.t.general.add.stillLoading).SetClass("alert"), | ||||
|                         addUi, | ||||
|                         state.dataIsLoading | ||||
|                     ), | ||||
|                     Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"), | ||||
|                     state.mapProperties.zoom.map( | ||||
|                         (zoom) => zoom >= Constants.minZoomLevelToAddNewPoint | ||||
|                     ) | ||||
|                 ), | ||||
|                 readYourMessages, | ||||
|                 state.osmConnection.userDetails.map( | ||||
|                     (userdetails: UserDetails) => | ||||
|                         userdetails.csCount >= | ||||
|                             Constants.userJourney.addNewPointWithUnreadMessagesUnlock || | ||||
|                         userdetails.unreadMessages == 0 | ||||
|                 ) | ||||
|             ), | ||||
|             Translations.t.general.add.pleaseLogin, | ||||
|             state | ||||
|             new Loading(Translations.t.general.add.stillLoading).SetClass("alert"), | ||||
|             addUi, | ||||
|             state.dataIsLoading | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     public static CreateTagInfoFor( | ||||
|         preset: PresetInfo, | ||||
|         osmConnection: OsmConnection, | ||||
|         optionallyLinkToWiki = true | ||||
|     ) { | ||||
|         const csCount = osmConnection.userDetails.data.csCount | ||||
|         return new Toggle( | ||||
|             Translations.t.general.add.presetInfo | ||||
|                 .Subs({ | ||||
|                     tags: preset.tags | ||||
|                         .map((t) => | ||||
|                             t.asHumanString( | ||||
|                                 optionallyLinkToWiki && | ||||
|                                     csCount > Constants.userJourney.tagsVisibleAndWikiLinked, | ||||
|                                 true | ||||
|                             ) | ||||
|                         ) | ||||
|                         .join("&"), | ||||
|                 }) | ||||
|                 .SetStyle("word-break: break-all"), | ||||
| 
 | ||||
|             undefined, | ||||
|             osmConnection.userDetails.map( | ||||
|                 (userdetails) => userdetails.csCount >= Constants.userJourney.tagsVisibleAt | ||||
|             ) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     private static CreateAllPresetsPanel( | ||||
|         selectedPreset: UIEventSource<PresetInfo>, | ||||
|         state: SpecialVisualizationState | ||||
|     ): BaseUIElement { | ||||
|         const presetButtons = SimpleAddUI.CreatePresetButtons(state, selectedPreset) | ||||
|         let intro: BaseUIElement = Translations.t.general.add.intro | ||||
| 
 | ||||
|         let testMode: BaseUIElement = new Toggle( | ||||
|             Translations.t.general.testing.SetClass("alert"), | ||||
|             undefined, | ||||
|             state.featureSwitchIsTesting | ||||
|         ) | ||||
| 
 | ||||
|         return new Combine([intro, testMode, presetButtons]).SetClass("flex flex-col") | ||||
|     } | ||||
| 
 | ||||
|     private static CreatePresetSelectButton(preset: PresetInfo) { | ||||
|         const title = Translations.t.general.add.addNew.Subs( | ||||
|             { | ||||
|                 category: preset.name, | ||||
|             }, | ||||
|             preset.name["context"] | ||||
|         ) | ||||
|         return new SubtleButton( | ||||
|             preset.icon(), | ||||
|             new Combine([ | ||||
|                 title.SetClass("font-bold"), | ||||
|                 preset.description?.FirstSentence(), | ||||
|             ]).SetClass("flex flex-col") | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /* | ||||
|      * Generates the list with all the buttons.*/ | ||||
|     private static CreatePresetButtons( | ||||
|         state: SpecialVisualizationState, | ||||
|         selectedPreset: UIEventSource<PresetInfo> | ||||
|     ): BaseUIElement { | ||||
|         const allButtons = [] | ||||
|         for (const layer of Array.from(state.layerState.filteredLayers.values())) { | ||||
|             if (layer.isDisplayed.data === false) { | ||||
|                 // The layer is not displayed...
 | ||||
|                 if (!state.featureSwitches.featureSwitchFilter.data) { | ||||
|                     // ...and we cannot enable the layer control -> we skip, as these presets can never be shown anyway
 | ||||
|                     continue | ||||
|                 } | ||||
| 
 | ||||
|                 if (layer.layerDef.name === undefined) { | ||||
|                     // this layer can never be toggled on in any case, so we skip the presets
 | ||||
|                     continue | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             const presets = layer.layerDef.presets | ||||
|             for (const preset of presets) { | ||||
|                 const tags = TagUtils.KVtoProperties(preset.tags ?? []) | ||||
|                 let icon: () => BaseUIElement = () => | ||||
|                     layer.layerDef.mapRendering[0] | ||||
|                         .RenderIcon(new ImmutableStore<any>(tags), false) | ||||
|                         .html.SetClass("w-12 h-12 block relative") | ||||
|                 const presetInfo: PresetInfo = { | ||||
|                     layerToAddTo: layer, | ||||
|                     name: preset.title, | ||||
|                     title: preset.title, | ||||
|                     icon: icon, | ||||
|                     preciseInput: preset.preciseInput, | ||||
|                     ...preset, | ||||
|                 } | ||||
| 
 | ||||
|                 const button = SimpleAddUI.CreatePresetSelectButton(presetInfo) | ||||
|                 button.onClick(() => { | ||||
|                     selectedPreset.setData(presetInfo) | ||||
|                 }) | ||||
|                 allButtons.push(button) | ||||
|             } | ||||
|         } | ||||
|         return new Combine(allButtons).SetClass("flex flex-col") | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -16,7 +16,6 @@ import StrayClickHandler from "../Logic/Actors/StrayClickHandler" | |||
| import { DefaultGuiState } from "./DefaultGuiState" | ||||
| import NewNoteUi from "./Popup/NewNoteUi" | ||||
| import Combine from "./Base/Combine" | ||||
| import AddNewMarker from "./BigComponents/AddNewMarker" | ||||
| import FilteredLayer from "../Models/FilteredLayer" | ||||
| import ExtraLinkButton from "./BigComponents/ExtraLinkButton" | ||||
| import { VariableUiElement } from "./Base/VariableUIElement" | ||||
|  | @ -108,13 +107,6 @@ export default class DefaultGUI { | |||
|                 newPointDialogIsShown | ||||
|             ) | ||||
| 
 | ||||
|             addNewPoint.isShown.addCallback((isShown) => { | ||||
|                 if (!isShown) { | ||||
|                     // Clear the 'last-click'-location when the dialog is closed - this causes the popup and the marker to be removed
 | ||||
|                     state.LastClickLocation.setData(undefined) | ||||
|                 } | ||||
|             }) | ||||
| 
 | ||||
|             let noteMarker = undefined | ||||
|             if (!hasPresets && addNewNoteDialog !== undefined) { | ||||
|                 noteMarker = new Combine([ | ||||
|  | @ -126,15 +118,6 @@ export default class DefaultGUI { | |||
|                     .SetClass("block relative h-full") | ||||
|                     .SetStyle("left: calc( 50% - 15px )") // This is a bit hacky, yes I know!
 | ||||
|             } | ||||
| 
 | ||||
|             StrayClickHandler.construct( | ||||
|                 state, | ||||
|                 addNewPoint, | ||||
|                 hasPresets ? new AddNewMarker(state.filteredLayers) : noteMarker | ||||
|             ) | ||||
|             state.LastClickLocation.addCallbackAndRunD((_) => { | ||||
|                 ScrollableFullScreen.collapse() | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         if (noteLayer !== undefined) { | ||||
|  | @ -208,22 +191,6 @@ export default class DefaultGUI { | |||
|             self.InitWelcomeMessage() | ||||
|         ) | ||||
| 
 | ||||
|         const communityIndex = Toggle.If(state.featureSwitchCommunityIndex, () => { | ||||
|             const communityIndexControl = new MapControlButton(Svg.community_svg()) | ||||
|             const communityIndex = new ScrollableFullScreen( | ||||
|                 () => Translations.t.communityIndex.title, | ||||
|                 () => new SvelteUIElement(CommunityIndexView, { ...state }), | ||||
|                 "community_index" | ||||
|             ) | ||||
|             communityIndexControl.onClick(() => { | ||||
|                 communityIndex.Activate() | ||||
|             }) | ||||
|             return communityIndexControl | ||||
|         }) | ||||
| 
 | ||||
|         const testingBadge = Toggle.If(state.featureSwitchIsTesting, () => | ||||
|             new FixedUiElement("TESTING").SetClass("alert m-2 border-2 border-black") | ||||
|         ) | ||||
|         new ScrollableFullScreen( | ||||
|             () => Translations.t.general.attribution.attributionTitle, | ||||
|             () => new CopyrightPanel(state), | ||||
|  | @ -233,14 +200,7 @@ export default class DefaultGUI { | |||
|         const copyright = new MapControlButton(Svg.copyright_svg()).onClick(() => | ||||
|             guiState.copyrightViewIsOpened.setData(true) | ||||
|         ) | ||||
|         new Combine([ | ||||
|             welcomeMessageMapControl, | ||||
|             userInfoMapControl, | ||||
|             copyright, | ||||
|             communityIndex, | ||||
|             extraLink, | ||||
|             testingBadge, | ||||
|         ]) | ||||
|         new Combine([welcomeMessageMapControl, userInfoMapControl, copyright, extraLink]) | ||||
|             .SetClass("flex flex-col") | ||||
|             .AttachTo("top-left") | ||||
| 
 | ||||
|  | @ -264,32 +224,11 @@ export default class DefaultGUI { | |||
|     } | ||||
| 
 | ||||
|     private InitWelcomeMessage(): BaseUIElement { | ||||
|         const isOpened = this.guiState.welcomeMessageIsOpened | ||||
|         new FullWelcomePaneWithTabs( | ||||
|             isOpened, | ||||
|         return new FullWelcomePaneWithTabs( | ||||
|             new UIEventSource<boolean>(false), | ||||
|             this.guiState.welcomeMessageOpenedTab, | ||||
|             this.state, | ||||
|             this.guiState | ||||
|         ) | ||||
| 
 | ||||
|         // ?-Button on Desktop, opens panel with close-X.
 | ||||
|         const help = new MapControlButton(Svg.help_svg()) | ||||
|         help.onClick(() => isOpened.setData(true)) | ||||
| 
 | ||||
|         const openedTime = new Date().getTime() | ||||
|         this.state.locationControl.addCallback(() => { | ||||
|             if (new Date().getTime() - openedTime < 15 * 1000) { | ||||
|                 // Don't autoclose the first 15 secs when the map is moving
 | ||||
|                 return | ||||
|             } | ||||
|             isOpened.setData(false) | ||||
|             return true // Unregister this caller - we only autoclose once
 | ||||
|         }) | ||||
| 
 | ||||
|         this.state.selectedElement.addCallbackAndRunD((_) => { | ||||
|             isOpened.setData(false) | ||||
|         }) | ||||
| 
 | ||||
|         return help.SetClass("pointer-events-auto") | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,36 +1,35 @@ | |||
| import { BBox } from "../../Logic/BBox" | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
| import Combine from "../Base/Combine" | ||||
| import Title from "../Base/Title" | ||||
| import { Overpass } from "../../Logic/Osm/Overpass" | ||||
| import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
| import Constants from "../../Models/Constants" | ||||
| import RelationsTracker from "../../Logic/Osm/RelationsTracker" | ||||
| import { VariableUiElement } from "../Base/VariableUIElement" | ||||
| import { FlowStep } from "./FlowStep" | ||||
| import Loading from "../Base/Loading" | ||||
| import { SubtleButton } from "../Base/SubtleButton" | ||||
| import Svg from "../../Svg" | ||||
| import { Utils } from "../../Utils" | ||||
| import { IdbLocalStorage } from "../../Logic/Web/IdbLocalStorage" | ||||
| import Minimap from "../Base/Minimap" | ||||
| import BaseLayer from "../../Models/BaseLayer" | ||||
| import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers" | ||||
| import Loc from "../../Models/Loc" | ||||
| import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" | ||||
| import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" | ||||
| import ValidatedTextField from "../Input/ValidatedTextField" | ||||
| import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource" | ||||
| import import_candidate from "../../assets/layers/import_candidate/import_candidate.json" | ||||
| import { GeoOperations } from "../../Logic/GeoOperations" | ||||
| import FeatureInfoBox from "../Popup/FeatureInfoBox" | ||||
| import { ImportUtils } from "./ImportUtils" | ||||
| import Translations from "../i18n/Translations" | ||||
| import currentview from "../../assets/layers/current_view/current_view.json" | ||||
| import { CheckBox } from "../Input/Checkboxes" | ||||
| import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch" | ||||
| import { Feature, FeatureCollection, Point } from "geojson" | ||||
| import DivContainer from "../Base/DivContainer" | ||||
| import { BBox } from "../../Logic/BBox"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import Title from "../Base/Title"; | ||||
| import { Overpass } from "../../Logic/Osm/Overpass"; | ||||
| import { Store, UIEventSource } from "../../Logic/UIEventSource"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import RelationsTracker from "../../Logic/Osm/RelationsTracker"; | ||||
| import { VariableUiElement } from "../Base/VariableUIElement"; | ||||
| import { FlowStep } from "./FlowStep"; | ||||
| import Loading from "../Base/Loading"; | ||||
| import { SubtleButton } from "../Base/SubtleButton"; | ||||
| import Svg from "../../Svg"; | ||||
| import { Utils } from "../../Utils"; | ||||
| import { IdbLocalStorage } from "../../Logic/Web/IdbLocalStorage"; | ||||
| import Minimap from "../Base/Minimap"; | ||||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; | ||||
| import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; | ||||
| import ValidatedTextField from "../Input/ValidatedTextField"; | ||||
| import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"; | ||||
| import import_candidate from "../../assets/layers/import_candidate/import_candidate.json"; | ||||
| import { GeoOperations } from "../../Logic/GeoOperations"; | ||||
| import FeatureInfoBox from "../Popup/FeatureInfoBox"; | ||||
| import { ImportUtils } from "./ImportUtils"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import currentview from "../../assets/layers/current_view/current_view.json"; | ||||
| import { CheckBox } from "../Input/Checkboxes"; | ||||
| import { Feature, FeatureCollection, Point } from "geojson"; | ||||
| import DivContainer from "../Base/DivContainer"; | ||||
| 
 | ||||
| /** | ||||
|  * Given the data to import, the bbox and the layer, will query overpass for similar items | ||||
|  | @ -323,13 +322,7 @@ export default class ConflationChecker | |||
|             ), | ||||
|             t.setRangeToZero, | ||||
|             matchedFeaturesMap, | ||||
|             new Combine([ | ||||
|                 new BackgroundMapSwitch( | ||||
|                     { backgroundLayer: background, locationControl: matchedFeaturesMap.location }, | ||||
|                     background | ||||
|                 ), | ||||
|                 showOsmLayer, | ||||
|             ]).SetClass("flex"), | ||||
|             showOsmLayer, | ||||
|         ]).SetClass("flex flex-col") | ||||
|         super([ | ||||
|             new Title(t.title), | ||||
|  |  | |||
|  | @ -17,7 +17,6 @@ import ScrollableFullScreen from "../Base/ScrollableFullScreen" | |||
| import Title from "../Base/Title" | ||||
| import CheckBoxes from "../Input/Checkboxes" | ||||
| import AllTagsPanel from "../Popup/AllTagsPanel.svelte" | ||||
| import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch" | ||||
| import { Feature, Point } from "geojson" | ||||
| import DivContainer from "../Base/DivContainer" | ||||
| import SvelteUIElement from "../Base/SvelteUIElement" | ||||
|  | @ -112,13 +111,7 @@ export class MapPreview | |||
|         const currentBounds = new UIEventSource<BBox>(undefined) | ||||
|         const { ui, mapproperties, map } = MapLibreAdaptor.construct() | ||||
| 
 | ||||
|         const layerControl = new BackgroundMapSwitch( | ||||
|             { | ||||
|                 backgroundLayer: background, | ||||
|                 locationControl: location, | ||||
|             }, | ||||
|             background | ||||
|         ) | ||||
| 
 | ||||
|         ui.SetClass("w-full").SetStyle("height: 500px") | ||||
| 
 | ||||
|         layerPicker.GetValue().addCallbackAndRunD((layerToShow) => { | ||||
|  | @ -160,7 +153,6 @@ export class MapPreview | |||
|             mismatchIndicator, | ||||
|             ui, | ||||
|             new DivContainer("fullscreen"), | ||||
|             layerControl, | ||||
|             confirm, | ||||
|         ]) | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,12 +1,11 @@ | |||
| import { InputElement } from "./InputElement" | ||||
| import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
| import Combine from "../Base/Combine" | ||||
| import Svg from "../../Svg" | ||||
| import Loc from "../../Models/Loc" | ||||
| import { GeoOperations } from "../../Logic/GeoOperations" | ||||
| import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch" | ||||
| import BaseUIElement from "../BaseUIElement" | ||||
| import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers" | ||||
| import { InputElement } from "./InputElement"; | ||||
| import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import Svg from "../../Svg"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import { GeoOperations } from "../../Logic/GeoOperations"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"; | ||||
| 
 | ||||
| /** | ||||
|  * Selects a length after clicking on the minimap, in meters | ||||
|  | @ -38,7 +37,7 @@ export default class LengthInput extends InputElement<string> { | |||
|     } | ||||
| 
 | ||||
|     protected InnerConstructElement(): HTMLElement { | ||||
|         let map: BaseUIElement & MinimapObj = undefined | ||||
|         let map: BaseUIElement = undefined | ||||
|         let layerControl: BaseUIElement = undefined | ||||
|         map = Minimap.createMiniMap({ | ||||
|             background: this.background, | ||||
|  | @ -50,16 +49,6 @@ export default class LengthInput extends InputElement<string> { | |||
|             }, | ||||
|         }) | ||||
| 
 | ||||
|         layerControl = new BackgroundMapSwitch( | ||||
|             { | ||||
|                 locationControl: this._location, | ||||
|                 backgroundLayer: this.background, | ||||
|             }, | ||||
|             this.background, | ||||
|             { | ||||
|                 allowedCategories: ["map", "photo"], | ||||
|             } | ||||
|         ) | ||||
|         const crosshair = new Combine([ | ||||
|             Svg.length_crosshair_svg().SetStyle( | ||||
|                 `position: absolute;top: 0;left: 0;transform:rotate(${this.value.data ?? 0}deg);` | ||||
|  | @ -70,9 +59,6 @@ export default class LengthInput extends InputElement<string> { | |||
| 
 | ||||
|         const element = new Combine([ | ||||
|             crosshair, | ||||
|             layerControl?.SetStyle( | ||||
|                 "position: absolute; bottom: 0.25rem; left: 0.25rem; z-index: 1000" | ||||
|             ), | ||||
|             map?.SetClass("w-full h-full block absolute top-0 left-O overflow-hidden"), | ||||
|         ]) | ||||
|             .SetClass("relative block bg-white border border-black rounded-xl overflow-hidden") | ||||
|  |  | |||
|  | @ -12,31 +12,29 @@ | |||
|    * A visualisation to pick a direction on a map background | ||||
|    */ | ||||
|   export let value: UIEventSource<{lon: number, lat: number}>; | ||||
|   export let mapProperties: Partial<MapProperties> & { readonly location: UIEventSource<{ lon: number; lat: number }> }; | ||||
|   export let mapProperties: Partial<MapProperties> & { readonly location: UIEventSource<{ lon: number; lat: number }> } = undefined; | ||||
|   /** | ||||
|    * Called when setup is done, cna be used to add layrs to the map | ||||
|    */ | ||||
|   export let onCreated : (value: Store<{lon: number, lat: number}> , map: Store<MlMap>, mapProperties: MapProperties ) => void | ||||
|    | ||||
|   let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined); | ||||
|   export let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined); | ||||
|   let mla = new MapLibreAdaptor(map, mapProperties); | ||||
|   mla.allowMoving.setData(true) | ||||
|   mla.allowZooming.setData(true) | ||||
|    | ||||
|   if(onCreated){ | ||||
|     onCreated(value, map, mla) | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
| <div class="relative h-32 cursor-pointer overflow-hidden"> | ||||
| <div class="relative h-full min-h-32 cursor-pointer overflow-hidden"> | ||||
|   <div class="w-full h-full absolute top-0 left-0 cursor-pointer"> | ||||
|     <MaplibreMap {map} attribution={false}></MaplibreMap> | ||||
|   </div> | ||||
| 
 | ||||
|   <div class="w-full h-full absolute top-0 left-0 p-8 pointer-events-none opacity-50"> | ||||
|       <ToSvelte construct={() => Svg.move_arrows_svg().SetClass("h-full")}></ToSvelte> | ||||
|   <div class="w-full h-full absolute top-0 left-0 p-8 pointer-events-none opacity-50 flex items-center"> | ||||
|     <img src="./assets/svg/move-arrows.svg" class="h-full max-h-24"/> | ||||
|   </div> | ||||
|    | ||||
|   <DragInvitation></DragInvitation> | ||||
|   <DragInvitation hideSignal={mla.location.stabilized(3000)}></DragInvitation> | ||||
| 
 | ||||
| </div> | ||||
|  |  | |||
|  | @ -33,10 +33,10 @@ | |||
| 
 | ||||
|   let dispatch = createEventDispatcher<{ selected }>(); | ||||
|   $: { | ||||
|     console.log(htmlElem) | ||||
|     console.log(htmlElem); | ||||
|     if (htmlElem !== undefined) { | ||||
|       htmlElem.onfocus = () => { | ||||
|         console.log("Dispatching selected event") | ||||
|         console.log("Dispatching selected event"); | ||||
|         return dispatch("selected"); | ||||
|       }; | ||||
|     } | ||||
|  | @ -44,12 +44,12 @@ | |||
| </script> | ||||
| 
 | ||||
| {#if validator.textArea} | ||||
|   <textarea bind:value={$_value} inputmode={validator.inputmode ?? "text"}></textarea> | ||||
|   <textarea class="w-full" bind:value={$_value} inputmode={validator.inputmode ?? "text"}></textarea> | ||||
| {:else } | ||||
|   <div class="flex"> | ||||
|   <span class="flex"> | ||||
|     <input bind:this={htmlElem} bind:value={$_value} inputmode={validator.inputmode ?? "text"}> | ||||
|     {#if !$isValid} | ||||
|       <ExclamationIcon class="h-6 w-6 -ml-6"></ExclamationIcon> | ||||
|     {/if} | ||||
|   </div> | ||||
|   </span> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -35,8 +35,8 @@ export class MapLibreAdaptor implements MapProperties { | |||
|     readonly allowMoving: UIEventSource<true | boolean | undefined> | ||||
|     readonly allowZooming: UIEventSource<true | boolean | undefined> | ||||
|     readonly lastClickLocation: Store<undefined | { lon: number; lat: number }> | ||||
|     readonly minzoom: UIEventSource<number> | ||||
|     private readonly _maplibreMap: Store<MLMap> | ||||
|     private readonly _bounds: UIEventSource<BBox> | ||||
|     /** | ||||
|      * Used for internal bookkeeping (to remove a rasterLayer when done loading) | ||||
|      * @private | ||||
|  | @ -48,9 +48,10 @@ export class MapLibreAdaptor implements MapProperties { | |||
| 
 | ||||
|         this.location = state?.location ?? new UIEventSource({ lon: 0, lat: 0 }) | ||||
|         this.zoom = state?.zoom ?? new UIEventSource(1) | ||||
|         this.minzoom = state?.minzoom ?? new UIEventSource(0) | ||||
|         this.zoom.addCallbackAndRunD((z) => { | ||||
|             if (z < 0) { | ||||
|                 this.zoom.setData(0) | ||||
|             if (z < this.minzoom.data) { | ||||
|                 this.zoom.setData(this.minzoom.data) | ||||
|             } | ||||
|             if (z > 24) { | ||||
|                 this.zoom.setData(24) | ||||
|  | @ -59,8 +60,7 @@ export class MapLibreAdaptor implements MapProperties { | |||
|         this.maxbounds = state?.maxbounds ?? new UIEventSource(undefined) | ||||
|         this.allowMoving = state?.allowMoving ?? new UIEventSource(true) | ||||
|         this.allowZooming = state?.allowZooming ?? new UIEventSource(true) | ||||
|         this._bounds = new UIEventSource(undefined) | ||||
|         this.bounds = this._bounds | ||||
|         this.bounds = state?.bounds ?? new UIEventSource(undefined) | ||||
|         this.rasterLayer = | ||||
|             state?.rasterLayer ?? new UIEventSource<RasterLayerPolygon | undefined>(undefined) | ||||
| 
 | ||||
|  | @ -69,32 +69,28 @@ export class MapLibreAdaptor implements MapProperties { | |||
|         const self = this | ||||
|         maplibreMap.addCallbackAndRunD((map) => { | ||||
|             map.on("load", () => { | ||||
|                 this.updateStores() | ||||
|                 self.setBackground() | ||||
|                 self.MoveMapToCurrentLoc(self.location.data) | ||||
|                 self.SetZoom(self.zoom.data) | ||||
|                 self.setMaxBounds(self.maxbounds.data) | ||||
|                 self.setAllowMoving(self.allowMoving.data) | ||||
|                 self.setAllowZooming(self.allowZooming.data) | ||||
|                 self.setMinzoom(self.minzoom.data) | ||||
|             }) | ||||
|             self.MoveMapToCurrentLoc(self.location.data) | ||||
|             self.SetZoom(self.zoom.data) | ||||
|             self.setMaxBounds(self.maxbounds.data) | ||||
|             self.setAllowMoving(self.allowMoving.data) | ||||
|             self.setAllowZooming(self.allowZooming.data) | ||||
|             map.on("moveend", () => { | ||||
|                 const dt = this.location.data | ||||
|                 dt.lon = map.getCenter().lng | ||||
|                 dt.lat = map.getCenter().lat | ||||
|                 this.location.ping() | ||||
|                 this.zoom.setData(Math.round(map.getZoom() * 10) / 10) | ||||
|                 const bounds = map.getBounds() | ||||
|                 const bbox = new BBox([ | ||||
|                     [bounds.getEast(), bounds.getNorth()], | ||||
|                     [bounds.getWest(), bounds.getSouth()], | ||||
|                 ]) | ||||
|                 self._bounds.setData(bbox) | ||||
|             }) | ||||
|             self.setMinzoom(self.minzoom.data) | ||||
|             this.updateStores() | ||||
|             map.on("moveend", () => this.updateStores()) | ||||
|             map.on("click", (e) => { | ||||
|                 if (e.originalEvent["consumed"]) { | ||||
|                     // Workaround, 'ShowPointLayer' sets this flag
 | ||||
|                     return | ||||
|                 } | ||||
|                 const lon = e.lngLat.lng | ||||
|                 const lat = e.lngLat.lat | ||||
|                 lastClickLocation.setData({ lon, lat }) | ||||
|  | @ -117,6 +113,23 @@ export class MapLibreAdaptor implements MapProperties { | |||
|         this.bounds.addCallbackAndRunD((bounds) => self.setBounds(bounds)) | ||||
|     } | ||||
| 
 | ||||
|     private updateStores() { | ||||
|         const map = this._maplibreMap.data | ||||
|         if (map === undefined) { | ||||
|             return | ||||
|         } | ||||
|         const dt = this.location.data | ||||
|         dt.lon = map.getCenter().lng | ||||
|         dt.lat = map.getCenter().lat | ||||
|         this.location.ping() | ||||
|         this.zoom.setData(Math.round(map.getZoom() * 10) / 10) | ||||
|         const bounds = map.getBounds() | ||||
|         const bbox = new BBox([ | ||||
|             [bounds.getEast(), bounds.getNorth()], | ||||
|             [bounds.getWest(), bounds.getSouth()], | ||||
|         ]) | ||||
|         this.bounds.setData(bbox) | ||||
|     } | ||||
|     /** | ||||
|      * Convenience constructor | ||||
|      */ | ||||
|  | @ -191,7 +204,7 @@ export class MapLibreAdaptor implements MapProperties { | |||
|         if (map === undefined) { | ||||
|             return | ||||
|         } | ||||
|         while (!map.isStyleLoaded()) { | ||||
|         while (!map?.isStyleLoaded()) { | ||||
|             await Utils.waitFor(250) | ||||
|         } | ||||
|     } | ||||
|  | @ -265,9 +278,9 @@ export class MapLibreAdaptor implements MapProperties { | |||
|             return | ||||
|         } | ||||
|         if (bbox) { | ||||
|             map.setMaxBounds(bbox.toLngLat()) | ||||
|             map?.setMaxBounds(bbox.toLngLat()) | ||||
|         } else { | ||||
|             map.setMaxBounds(null) | ||||
|             map?.setMaxBounds(null) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -287,6 +300,14 @@ export class MapLibreAdaptor implements MapProperties { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private setMinzoom(minzoom: number) { | ||||
|         const map = this._maplibreMap.data | ||||
|         if (map === undefined) { | ||||
|             return | ||||
|         } | ||||
|         map.setMinZoom(minzoom) | ||||
|     } | ||||
| 
 | ||||
|     private setAllowZooming(allow: true | boolean | undefined) { | ||||
|         const map = this._maplibreMap.data | ||||
|         if (map === undefined) { | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ import { GeoOperations } from "../../Logic/GeoOperations" | |||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
| import PointRenderingConfig from "../../Models/ThemeConfig/PointRenderingConfig" | ||||
| import { OsmTags } from "../../Models/OsmFeature" | ||||
| import FeatureSource from "../../Logic/FeatureSource/FeatureSource" | ||||
| import { FeatureSource } from "../../Logic/FeatureSource/FeatureSource" | ||||
| import { BBox } from "../../Logic/BBox" | ||||
| import { Feature } from "geojson" | ||||
| import ScrollableFullScreen from "../Base/ScrollableFullScreen" | ||||
|  | @ -124,8 +124,11 @@ class PointRenderingLayer { | |||
| 
 | ||||
|         if (this._onClick) { | ||||
|             const self = this | ||||
|             el.addEventListener("click", function () { | ||||
|             el.addEventListener("click", function (ev) { | ||||
|                 self._onClick(feature) | ||||
|                 ev.preventDefault() | ||||
|                 // Workaround to signal the MapLibreAdaptor to ignore this click
 | ||||
|                 ev["consumed"] = true | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|  | @ -164,6 +167,7 @@ class LineRenderingLayer { | |||
|     private readonly _layername: string | ||||
|     private readonly _listenerInstalledOn: Set<string> = new Set<string>() | ||||
| 
 | ||||
|     private static missingIdTriggered = false | ||||
|     constructor( | ||||
|         map: MlMap, | ||||
|         features: FeatureSource, | ||||
|  | @ -281,11 +285,14 @@ class LineRenderingLayer { | |||
|             const feature = features[i] | ||||
|             const id = feature.properties.id ?? feature.id | ||||
|             if (id === undefined) { | ||||
|                 console.trace( | ||||
|                     "Got a feature without ID; this causes rendering bugs:", | ||||
|                     feature, | ||||
|                     "from" | ||||
|                 ) | ||||
|                 if (!LineRenderingLayer.missingIdTriggered) { | ||||
|                     console.trace( | ||||
|                         "Got a feature without ID; this causes rendering bugs:", | ||||
|                         feature, | ||||
|                         "from" | ||||
|                     ) | ||||
|                     LineRenderingLayer.missingIdTriggered = true | ||||
|                 } | ||||
|                 continue | ||||
|             } | ||||
|             if (this._listenerInstalledOn.has(id)) { | ||||
|  | @ -334,7 +341,7 @@ export default class ShowDataLayer { | |||
|         options?: Partial<ShowDataLayerOptions> | ||||
|     ) { | ||||
|         const perLayer = new PerLayerFeatureSourceSplitter( | ||||
|             layers.map((l) => new FilteredLayer(l)), | ||||
|             layers.filter((l) => l.source !== null).map((l) => new FilteredLayer(l)), | ||||
|             new StaticFeatureSource(features) | ||||
|         ) | ||||
|         perLayer.forEach((fs) => { | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import FeatureSource from "../../Logic/FeatureSource/FeatureSource" | ||||
| import { FeatureSource } from "../../Logic/FeatureSource/FeatureSource" | ||||
| import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
| import { Feature } from "geojson" | ||||
|  |  | |||
|  | @ -8,8 +8,7 @@ import Combine from "../Base/Combine" | |||
| import Translations from "../i18n/Translations" | ||||
| import Svg from "../../Svg" | ||||
| import Toggle from "../Input/Toggle" | ||||
| import SimpleAddUI, { PresetInfo } from "../BigComponents/SimpleAddUI" | ||||
| import Img from "../Base/Img" | ||||
| import { PresetInfo } from "../BigComponents/SimpleAddUI" | ||||
| import Title from "../Base/Title" | ||||
| import { VariableUiElement } from "../Base/VariableUIElement" | ||||
| import { Tag } from "../../Logic/Tags/Tag" | ||||
|  | @ -115,10 +114,6 @@ export default class ConfirmLocationOfPoint extends Combine { | |||
|         ) | ||||
|             .SetClass("font-bold break-words") | ||||
|             .onClick(() => { | ||||
|                 console.log( | ||||
|                     "The confirmLocationPanel - precise input yielded ", | ||||
|                     preciseInput?.GetValue()?.data | ||||
|                 ) | ||||
|                 const globalFilterTagsToAdd: Tag[][] = state.globalFilters.data | ||||
|                     .filter((gf) => gf.onNewPoint !== undefined) | ||||
|                     .map((gf) => gf.onNewPoint.tags) | ||||
|  | @ -131,30 +126,13 @@ export default class ConfirmLocationOfPoint extends Combine { | |||
|                 ) | ||||
|             }) | ||||
| 
 | ||||
|         const warn = Translations.t.general.add.warnVisibleForEveryone | ||||
|             .Clone() | ||||
|             .SetClass("alert w-full block") | ||||
|         if (preciseInput !== undefined) { | ||||
|             confirmButton = new Combine([preciseInput, warn, confirmButton]) | ||||
|             confirmButton = new Combine([preciseInput, confirmButton]) | ||||
|         } else { | ||||
|             confirmButton = new Combine([warn, confirmButton]) | ||||
|             confirmButton = new Combine([confirmButton]) | ||||
|         } | ||||
| 
 | ||||
|         const openLayerControl = new SubtleButton( | ||||
|             Svg.layers_ui(), | ||||
|             new Combine([ | ||||
|                 Translations.t.general.add.layerNotEnabled | ||||
|                     .Subs({ layer: preset.layerToAddTo.layerDef.name }) | ||||
|                     .SetClass("alert"), | ||||
|                 Translations.t.general.add.openLayerControl, | ||||
|             ]) | ||||
|         ).onClick(() => filterViewIsOpened.setData(true)) | ||||
| 
 | ||||
|         let openLayerOrConfirm = new Toggle( | ||||
|             confirmButton, | ||||
|             openLayerControl, | ||||
|             preset.layerToAddTo.isDisplayed | ||||
|         ) | ||||
|         let openLayerOrConfirm = confirmButton | ||||
| 
 | ||||
|         const disableFilter = new SubtleButton( | ||||
|             new Combine([ | ||||
|  | @ -200,21 +178,8 @@ export default class ConfirmLocationOfPoint extends Combine { | |||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         const hasActiveFilter = preset.layerToAddTo.appliedFilters.map((appliedFilters) => { | ||||
|             const activeFilters = Array.from(appliedFilters.values()).filter( | ||||
|                 (f) => f?.currentFilter !== undefined | ||||
|             ) | ||||
|             return activeFilters.length === 0 | ||||
|         }) | ||||
| 
 | ||||
|         // If at least one filter is active which _might_ hide a newly added item, this blocks the preset and requests the filter to be disabled
 | ||||
|         const disableFiltersOrConfirm = new Toggle( | ||||
|             openLayerOrConfirm, | ||||
|             disableFilter, | ||||
|             hasActiveFilter | ||||
|         ) | ||||
| 
 | ||||
|         const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, state.osmConnection) | ||||
|         const disableFiltersOrConfirm = new Toggle(openLayerOrConfirm, disableFilter) | ||||
| 
 | ||||
|         const cancelButton = new SubtleButton( | ||||
|             options?.cancelIcon ?? Svg.close_ui(), | ||||
|  | @ -223,18 +188,7 @@ export default class ConfirmLocationOfPoint extends Combine { | |||
| 
 | ||||
|         let examples: BaseUIElement = undefined | ||||
|         if (preset.exampleImages !== undefined && preset.exampleImages.length > 0) { | ||||
|             examples = new Combine([ | ||||
|                 new Title( | ||||
|                     preset.exampleImages.length == 1 | ||||
|                         ? Translations.t.general.example | ||||
|                         : Translations.t.general.examples | ||||
|                 ), | ||||
|                 new Combine( | ||||
|                     preset.exampleImages.map((img) => | ||||
|                         new Img(img).SetClass("h-64 m-1 w-auto rounded-lg") | ||||
|                     ) | ||||
|                 ).SetClass("flex flex-wrap items-stretch"), | ||||
|             ]) | ||||
|             examples = new Combine([new Title()]) | ||||
|         } | ||||
| 
 | ||||
|         super([ | ||||
|  | @ -247,7 +201,6 @@ export default class ConfirmLocationOfPoint extends Combine { | |||
|             cancelButton, | ||||
|             preset.description, | ||||
|             examples, | ||||
|             tagInfo, | ||||
|         ]) | ||||
| 
 | ||||
|         this.SetClass("flex flex-col") | ||||
|  |  | |||
							
								
								
									
										239
									
								
								UI/Popup/AddNewPoint/AddNewPoint.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										239
									
								
								UI/Popup/AddNewPoint/AddNewPoint.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,239 @@ | |||
| <script lang="ts"> | ||||
|   /** | ||||
|    * This component ties together all the steps that are needed to create a new point. | ||||
|    * There are many subcomponents which help with that | ||||
|    */ | ||||
|   import type { SpecialVisualizationState } from "../../SpecialVisualization"; | ||||
|   import PresetList from "./PresetList.svelte"; | ||||
|   import type PresetConfig from "../../../Models/ThemeConfig/PresetConfig"; | ||||
|   import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; | ||||
|   import Tr from "../../Base/Tr.svelte"; | ||||
|   import SubtleButton from "../../Base/SubtleButton.svelte"; | ||||
|   import FromHtml from "../../Base/FromHtml.svelte"; | ||||
|   import Translations from "../../i18n/Translations.js"; | ||||
|   import TagHint from "../TagHint.svelte"; | ||||
|   import { And } from "../../../Logic/Tags/And.js"; | ||||
|   import LoginToggle from "../../Base/LoginToggle.svelte"; | ||||
|   import Constants from "../../../Models/Constants.js"; | ||||
|   import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
|   import { Store, UIEventSource } from "../../../Logic/UIEventSource"; | ||||
|   import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||
|   import LoginButton from "../../Base/LoginButton.svelte"; | ||||
|   import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte"; | ||||
|   import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction"; | ||||
|   import { OsmObject } from "../../../Logic/Osm/OsmObject"; | ||||
|   import { Tag } from "../../../Logic/Tags/Tag"; | ||||
|   import type { WayId } from "../../../Models/OsmFeature"; | ||||
|   import { TagUtils } from "../../../Logic/Tags/TagUtils"; | ||||
|   import Loading from "../../Base/Loading.svelte"; | ||||
| 
 | ||||
|   export let coordinate: { lon: number, lat: number }; | ||||
|   export let state: SpecialVisualizationState; | ||||
| 
 | ||||
|   let selectedPreset: { preset: PresetConfig, layer: LayerConfig, icon: string, tags: Record<string, string> } = undefined; | ||||
| 
 | ||||
|   let confirmedCategory = false; | ||||
|   $: if (selectedPreset === undefined) { | ||||
|     confirmedCategory = false; | ||||
|     creating = false | ||||
|   } | ||||
| 
 | ||||
|   let flayer: FilteredLayer = undefined; | ||||
|   let layerIsDisplayed: UIEventSource<boolean> | undefined = undefined; | ||||
|   let layerHasFilters: Store<boolean> | undefined = undefined; | ||||
| 
 | ||||
|   $:{ | ||||
|     flayer = state.layerState.filteredLayers.get(selectedPreset?.layer?.id); | ||||
|     layerIsDisplayed = flayer?.isDisplayed; | ||||
|     layerHasFilters = flayer?.hasFilter; | ||||
|   } | ||||
|   const t = Translations.t.general.add; | ||||
| 
 | ||||
|   const zoom = state.mapProperties.zoom; | ||||
| 
 | ||||
|   let preciseCoordinate: UIEventSource<{ lon: number, lat: number }> = new UIEventSource(undefined); | ||||
|   let snappedToObject: UIEventSource<string> = new UIEventSource<string>(undefined); | ||||
| 
 | ||||
|   let creating = false; | ||||
| 
 | ||||
|   /** | ||||
|    * Call when the user should restart the flow by clicking on the map, e.g. because they disabled filters. | ||||
|    * Will delete the lastclick-location | ||||
|    */ | ||||
|   function abort() { | ||||
|     state.selectedElement.setData(undefined); | ||||
|     // When aborted, we force the contributors to place the pin _again_ | ||||
|     // This is because there might be a nearby object that was disabled; this forces them to re-evaluate the map | ||||
|     state.lastClickObject.features.setData([]); | ||||
|   } | ||||
| 
 | ||||
|   async function confirm() { | ||||
|     creating = true; | ||||
|     const location: { lon: number; lat: number } = preciseCoordinate.data; | ||||
|     const snapTo: WayId | undefined = <WayId>snappedToObject.data; | ||||
|     const tags: Tag[] = selectedPreset.preset.tags; | ||||
|     console.log("Creating new point at", location, "snapped to", snapTo, "with tags", tags); | ||||
| 
 | ||||
|     const snapToWay = snapTo === undefined ? undefined : await OsmObject.DownloadObjectAsync(snapTo, 0); | ||||
| 
 | ||||
|     const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, { | ||||
|       theme: state.layout?.id ?? "unkown", | ||||
|       changeType: "create", | ||||
|       snapOnto: snapToWay | ||||
|     }); | ||||
|     await state.changes.applyAction(newElementAction); | ||||
|     const newId = newElementAction.newElementId; | ||||
|     state.newFeatures.features.data.push({ | ||||
|       type: "Feature", | ||||
|       properties: { | ||||
|         id: newId, | ||||
|         ...TagUtils.KVtoProperties(tags) | ||||
|       }, | ||||
|       geometry: { | ||||
|         type: "Point", | ||||
|         coordinates: [location.lon, location.lat] | ||||
|       } | ||||
|     }); | ||||
|     state.newFeatures.features.ping(); | ||||
|   console.log("New features:", state.newFeatures.features.data ) | ||||
|     { | ||||
|       // Set some metainfo | ||||
|       const tagsStore = state.featureProperties.getStore(newId); | ||||
|       const properties = tagsStore.data; | ||||
|       if (snapTo) { | ||||
|         // metatags (starting with underscore) are not uploaded, so we can safely mark this | ||||
|         properties["_referencing_ways"] = `["${snapTo}"]`; | ||||
|       } | ||||
|       properties["_last_edit:timestamp"] = new Date().toISOString(); | ||||
|       const userdetails = state.osmConnection.userDetails.data; | ||||
|       properties["_last_edit:contributor"] = userdetails.name; | ||||
|       properties["_last_edit:uid"] = "" + userdetails.uid; | ||||
|       tagsStore.ping(); | ||||
|     } | ||||
|     const feature = state.indexedFeatures.featuresById.data.get(newId); | ||||
|     abort(); | ||||
|     state.selectedElement.setData(feature); | ||||
|     state.selectedLayer.setData(selectedPreset.layer); | ||||
|    | ||||
|   } | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <LoginToggle ignoreLoading={true} {state}> | ||||
|   <LoginButton osmConnection={state.osmConnection} slot="not-logged-in"> | ||||
|     <Tr slot="message" t={Translations.t.general.add.pleaseLogin} /> | ||||
|   </LoginButton> | ||||
| 
 | ||||
|   {#if $zoom < Constants.minZoomLevelToAddNewPoint} | ||||
|     <div class="alert"> | ||||
|       <Tr t={Translations.t.general.add.zoomInFurther}></Tr> | ||||
|     </div> | ||||
|   {:else if selectedPreset === undefined} | ||||
|     <!-- First, select the correct preset --> | ||||
|     <PresetList {state} on:select={event => {selectedPreset = event.detail}}></PresetList> | ||||
| 
 | ||||
| 
 | ||||
|   {:else if !$layerIsDisplayed} | ||||
|     <!-- Check that the layer is enabled, so that we don't add a duplicate --> | ||||
|     <div class="alert flex justify-center items-center"> | ||||
|       <EyeOffIcon class="w-8" /> | ||||
|       <Tr t={Translations.t.general.add.layerNotEnabled | ||||
|                     .Subs({ layer: selectedPreset.layer.name }) | ||||
|                    } /> | ||||
|     </div> | ||||
| 
 | ||||
|     <SubtleButton on:click={() => { | ||||
|       layerIsDisplayed.setData(true) | ||||
|       abort() | ||||
|     }}> | ||||
|       <EyeIcon slot="image" class="w-8" /> | ||||
|       <Tr slot="message" t={Translations.t.general.add.enableLayer.Subs({name: selectedPreset.layer.name})} /> | ||||
|     </SubtleButton> | ||||
|     <SubtleButton on:click={() => { | ||||
|       abort() | ||||
|       state.guistate.openFilterView(selectedPreset.layer)    }    }> | ||||
|       <img src="./assets/svg/layers.svg" slot="image" class="w-6"> | ||||
|       <Tr slot="message" t={Translations.t.general.add.openLayerControl}></Tr> | ||||
|     </SubtleButton> | ||||
| 
 | ||||
| 
 | ||||
|   {:else if $layerHasFilters} | ||||
|     <!-- Some filters are enabled. The feature to add might already be mapped, but hiddne --> | ||||
|     <div class="alert flex justify-center items-center"> | ||||
|       <EyeOffIcon class="w-8" /> | ||||
|       <Tr t={Translations.t.general.add.disableFiltersExplanation} /> | ||||
|     </div> | ||||
| 
 | ||||
|     <SubtleButton on:click={() => { | ||||
|       abort() | ||||
|       const flayer = state.layerState.filteredLayers.get(selectedPreset.layer.id) | ||||
|       flayer.disableAllFilters() | ||||
|     } | ||||
|     }> | ||||
|       <EyeOffIcon class="w-8" /> | ||||
|       <Tr slot="message" t={Translations.t.general.add.disableFilters}></Tr> | ||||
|     </SubtleButton> | ||||
| 
 | ||||
| 
 | ||||
|     <SubtleButton on:click={() => { | ||||
|       abort() | ||||
|       state.guistate.openFilterView(selectedPreset.layer) | ||||
|     } | ||||
|     }> | ||||
|       <img src="./assets/svg/layers.svg" slot="image" class="w-6"> | ||||
|       <Tr slot="message" t={Translations.t.general.add.openLayerControl}></Tr> | ||||
|     </SubtleButton> | ||||
| 
 | ||||
|   {:else if !confirmedCategory  } | ||||
|     <!-- Second, confirm the category --> | ||||
|     <Tr t={Translations.t.general.add.confirmIntro.Subs({title: selectedPreset.preset.title})}></Tr> | ||||
| 
 | ||||
| 
 | ||||
|     {#if selectedPreset.preset.description} | ||||
|       <Tr t={selectedPreset.preset.description} /> | ||||
|     {/if} | ||||
| 
 | ||||
|     {#if selectedPreset.preset.exampleImages} | ||||
|       <h4> | ||||
|         {#if selectedPreset.preset.exampleImages.length == 1} | ||||
|           <Tr t={Translations.t.general.example} /> | ||||
|         {:else} | ||||
|           <Tr t={Translations.t.general.examples } /> | ||||
|         {/if} | ||||
|       </h4> | ||||
|       <span class="flex flex-wrap items-stretch"> | ||||
|       {#each selectedPreset.preset.exampleImages as src} | ||||
|         <img {src} class="h-64 m-1 w-auto rounded-lg"> | ||||
|       {/each} | ||||
|       </span> | ||||
|     {/if} | ||||
|     <TagHint embedIn={tags => t.presetInfo.Subs({tags})} osmConnection={state.osmConnection} | ||||
|              tags={new And(selectedPreset.preset.tags)}></TagHint> | ||||
| 
 | ||||
| 
 | ||||
|     <SubtleButton on:click={() => confirmedCategory = true}> | ||||
|       <div slot="image" class="relative"> | ||||
|         <FromHtml src={selectedPreset.icon}></FromHtml> | ||||
|         <img class="absolute bottom-0 right-0 w-4 h-4" src="./assets/svg/confirm.svg"> | ||||
|       </div> | ||||
|       <div slot="message"> | ||||
|         <Tr t={selectedPreset.text}></Tr> | ||||
|       </div> | ||||
|     </SubtleButton> | ||||
|     <SubtleButton on:click={() => selectedPreset = undefined}> | ||||
|       <img src="./assets/svg/back.svg" class="w-8 h-8" slot="image"> | ||||
|       <div slot="message"> | ||||
|         <Tr t={t.backToSelect} /> | ||||
|       </div> | ||||
|     </SubtleButton> | ||||
|   {:else if !creating} | ||||
|     <NewPointLocationInput value={preciseCoordinate} snappedTo={snappedToObject} {state} {coordinate} | ||||
|                            targetLayer={selectedPreset.layer} | ||||
|                            snapToLayers={selectedPreset.preset.preciseInput.snapToLayers}></NewPointLocationInput> | ||||
|     <SubtleButton on:click={confirm}> | ||||
|       <span slot="message">Confirm location</span> | ||||
|     </SubtleButton> | ||||
|   {:else} | ||||
|     <Loading>Creating point...</Loading> | ||||
|   {/if} | ||||
| </LoginToggle> | ||||
							
								
								
									
										88
									
								
								UI/Popup/AddNewPoint/PresetList.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								UI/Popup/AddNewPoint/PresetList.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,88 @@ | |||
| <script lang="ts"> | ||||
|   import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"; | ||||
|   import { createEventDispatcher } from "svelte"; | ||||
|   import type PresetConfig from "../../../Models/ThemeConfig/PresetConfig"; | ||||
|   import Tr from "../../Base/Tr.svelte"; | ||||
|   import Translations from "../../i18n/Translations.js"; | ||||
|   import SubtleButton from "../../Base/SubtleButton.svelte"; | ||||
|   import { Translation } from "../../i18n/Translation"; | ||||
|   import type { SpecialVisualizationState } from "../../SpecialVisualization"; | ||||
|   import { ImmutableStore } from "../../../Logic/UIEventSource"; | ||||
|   import { TagUtils } from "../../../Logic/Tags/TagUtils"; | ||||
|   import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; | ||||
|   import FromHtml from "../../Base/FromHtml.svelte"; | ||||
| 
 | ||||
|   /** | ||||
|    * This component lists all the presets and allows the user to select one | ||||
|    */ | ||||
|   export let state: SpecialVisualizationState; | ||||
|   let layout: LayoutConfig = state.layout; | ||||
|   let presets: { | ||||
|     preset: PresetConfig, | ||||
|     layer: LayerConfig, | ||||
|     text: Translation, | ||||
|     icon: string, | ||||
|     tags: Record<string, string> | ||||
|   }[] = []; | ||||
| 
 | ||||
|   for (const layer of layout.layers) { | ||||
|     const flayer = state.layerState.filteredLayers.get(layer.id); | ||||
|     if (flayer.isDisplayed.data === false) { | ||||
|       // The layer is not displayed... | ||||
|       if (!state.featureSwitches.featureSwitchFilter.data) { | ||||
|         // ...and we cannot enable the layer control -> we skip, as these presets can never be shown anyway | ||||
|         continue; | ||||
|       } | ||||
| 
 | ||||
|       if (layer.name === undefined) { | ||||
|         // this layer can never be toggled on in any case, so we skip the presets | ||||
|         continue; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     for (const preset of layer.presets) { | ||||
|       const tags = TagUtils.KVtoProperties(preset.tags ?? []); | ||||
| 
 | ||||
|       const icon: string = | ||||
|         layer.mapRendering[0] | ||||
|           .RenderIcon(new ImmutableStore<any>(tags), false) | ||||
|           .html.SetClass("w-12 h-12 block relative") | ||||
|           .ConstructElement().innerHTML; | ||||
| 
 | ||||
|       const description = preset.description?.FirstSentence(); | ||||
| 
 | ||||
|       const simplified = { | ||||
|         preset, | ||||
|         layer, | ||||
|         icon, | ||||
|         description, | ||||
|         tags, | ||||
|         text: Translations.t.general.add.addNew.Subs({ category: preset.title }, preset.title["context"]) | ||||
|       }; | ||||
|       presets.push(simplified); | ||||
|     } | ||||
| 
 | ||||
|   } | ||||
| 
 | ||||
|   const dispatch = createEventDispatcher<{ select: {preset: PresetConfig, layer: LayerConfig, icon: string, tags: Record<string, string>} }>(); | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <div> | ||||
|   <Tr t={Translations.t.general.add.intro} /> | ||||
|   {#each presets as preset} | ||||
|     <SubtleButton on:click={() => dispatch("select", preset)}> | ||||
|       <FromHtml slot="image" src={preset.icon}></FromHtml> | ||||
|       <div slot="message"> | ||||
| 
 | ||||
|         <b> | ||||
|           <Tr t={preset.text} /> | ||||
|         </b> | ||||
|         {#if preset.description} | ||||
|           <Tr t={preset.description}/> | ||||
|         {/if} | ||||
|       </div> | ||||
| 
 | ||||
|     </SubtleButton> | ||||
|   {/each} | ||||
| </div> | ||||
							
								
								
									
										139
									
								
								UI/Popup/CreateNewNote.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								UI/Popup/CreateNewNote.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,139 @@ | |||
| <script lang="ts"> | ||||
|   /** | ||||
|    * UIcomponent to create a new note at the given location | ||||
|    */ | ||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization"; | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource"; | ||||
|   import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"; | ||||
|   import ValidatedInput from "../InputElement/ValidatedInput.svelte"; | ||||
|   import SubtleButton from "../Base/SubtleButton.svelte"; | ||||
|   import Tr from "../Base/Tr.svelte"; | ||||
|   import Translations from "../i18n/Translations.js"; | ||||
|   import type { Feature, Point } from "geojson"; | ||||
|   import LoginToggle from "../Base/LoginToggle.svelte"; | ||||
|   import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| 
 | ||||
|   export let coordinate: { lon: number, lat: number }; | ||||
|   export let state: SpecialVisualizationState; | ||||
| 
 | ||||
|   let comment: UIEventSource<string> = LocalStorageSource.Get("note-text"); | ||||
|   let created = false; | ||||
| 
 | ||||
|   let notelayer: FilteredLayer = state.layerState.filteredLayers.get("note"); | ||||
| 
 | ||||
|   let hasFilter = notelayer?.hasFilter; | ||||
|   let isDisplayed = notelayer?.isDisplayed; | ||||
|    | ||||
|   function enableNoteLayer() { | ||||
|     state.guistate.closeAll(); | ||||
|     isDisplayed.setData(true); | ||||
|   } | ||||
| 
 | ||||
|   async function uploadNote() { | ||||
|     let txt = comment.data; | ||||
|     if (txt === undefined || txt === "") { | ||||
|       return; | ||||
|     } | ||||
|     const loc = coordinate; | ||||
|     txt += "\n\n #MapComplete #" + state?.layout?.id; | ||||
|     const id = await state?.osmConnection?.openNote(loc.lat, loc.lon, txt); | ||||
|     console.log("Created a note, got id",id) | ||||
|     const feature = <Feature<Point>>{ | ||||
|       type: "Feature", | ||||
|       geometry: { | ||||
|         type: "Point", | ||||
|         coordinates: [loc.lon, loc.lat] | ||||
|       }, | ||||
|       properties: { | ||||
|         id: "" + id.id, | ||||
|         date_created: new Date().toISOString(), | ||||
|         _first_comment: txt, | ||||
|         comments: JSON.stringify([ | ||||
|           { | ||||
|             text: txt, | ||||
|             html: txt, | ||||
|             user: state.osmConnection?.userDetails?.data?.name, | ||||
|             uid: state.osmConnection?.userDetails?.data?.uid | ||||
|           } | ||||
|         ]) | ||||
|       } | ||||
|     }; | ||||
|     state.newFeatures.features.data.push(feature); | ||||
|     state.newFeatures.features.ping(); | ||||
|     state.selectedElement?.setData(feature); | ||||
|     comment.setData(""); | ||||
|     created = true; | ||||
|   } | ||||
| 
 | ||||
| </script> | ||||
| {#if notelayer === undefined} | ||||
|   <div class="alert"> | ||||
|     This theme does not include the layer 'note'. As a result, no nodes can be created | ||||
|   </div> | ||||
| {:else if created} | ||||
|   <div class="thanks"> | ||||
|     <Tr t={Translations.t.notes.isCreated} /> | ||||
|   </div> | ||||
| {:else} | ||||
|   <h3> | ||||
|     <Tr t={Translations.t.notes.createNoteTitle}></Tr> | ||||
|   </h3> | ||||
| 
 | ||||
|   {#if $isDisplayed} | ||||
|     <!-- The layer is displayed, so we can add a note without worrying for duplicates --> | ||||
|     {#if $hasFilter} | ||||
|       <div class="flex flex-col"> | ||||
| 
 | ||||
|         <!-- ...but a filter is set ...--> | ||||
|         <div class="alert"> | ||||
|           <Tr t={ Translations.t.notes.noteLayerHasFilters}></Tr> | ||||
|         </div> | ||||
|         <SubtleButton on:click={() => notelayer.disableAllFilters()}> | ||||
|           <img slot="image" src="./assets/svg/filter.svg" class="w-8 h-8 mr-4"> | ||||
|           <Tr slot="message" t={Translations.t.notes.disableAllNoteFilters}></Tr> | ||||
|         </SubtleButton> | ||||
|       </div> | ||||
|     {:else} | ||||
|       <div> | ||||
|         <Tr t={Translations.t.notes.createNoteIntro}></Tr> | ||||
|         <div class="border rounded-sm border-grey-500"> | ||||
|           <div class="w-full p-1"> | ||||
|             <ValidatedInput type="text" value={comment}></ValidatedInput> | ||||
|           </div> | ||||
| 
 | ||||
|           <LoginToggle {state}> | ||||
|             <span slot="loading"><!--empty: don't show a loading message--></span> | ||||
|             <div slot="not-logged-in" class="alert"> | ||||
|               <Tr t={Translations.t.notes.warnAnonymous} /> | ||||
|             </div> | ||||
|           </LoginToggle> | ||||
| 
 | ||||
|           {#if $comment.length >= 3} | ||||
|             <SubtleButton on:click={uploadNote}> | ||||
|               <img slot="image" src="./assets/svg/addSmall.svg" class="w-8 h-8 mr-4"> | ||||
|               <Tr slot="message" t={ Translations.t.notes.createNote}></Tr> | ||||
|             </SubtleButton> | ||||
|           {:else} | ||||
|             <div class="alert"> | ||||
|               <Tr t={ Translations.t.notes.textNeeded}></Tr> | ||||
|             </div> | ||||
| 
 | ||||
|           {/if} | ||||
| 
 | ||||
|         </div> | ||||
|       </div> | ||||
|     {/if} | ||||
| 
 | ||||
|   {:else} | ||||
|     <div class="flex flex-col"> | ||||
|       <div class="alert"> | ||||
|         <Tr t={Translations.t.notes.noteLayerNotEnabled}></Tr> | ||||
|       </div> | ||||
|       <SubtleButton on:click={enableNoteLayer}> | ||||
|         <img slot="image" src="./assets/svg/layers.svg" class="w-8 h-8 mr-4"> | ||||
|         <Tr slot="message" t={Translations.t.notes.noteLayerDoEnable}></Tr> | ||||
|       </SubtleButton> | ||||
|     </div> | ||||
|   {/if} | ||||
| 
 | ||||
| {/if} | ||||
|  | @ -127,7 +127,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen { | |||
|         const allRenderings: BaseUIElement[] = [ | ||||
|             new VariableUiElement( | ||||
|                 tags | ||||
|                     .map((data) => data[Tag.newlyCreated.key]) | ||||
|                     .map((data) => data["_newly_created"]) | ||||
|                     .map((isCreated) => { | ||||
|                         if (isCreated === undefined) { | ||||
|                             return undefined | ||||
|  |  | |||
|  | @ -20,7 +20,7 @@ import CreateWayWithPointReuseAction, { | |||
|     MergePointConfig, | ||||
| } from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction" | ||||
| import OsmChangeAction, { OsmCreateAction } from "../../Logic/Osm/Actions/OsmChangeAction" | ||||
| import FeatureSource from "../../Logic/FeatureSource/FeatureSource" | ||||
| import { FeatureSource } from "../../Logic/FeatureSource/FeatureSource" | ||||
| import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject" | ||||
| import { PresetInfo } from "../BigComponents/SimpleAddUI" | ||||
| import { TagUtils } from "../../Logic/Tags/TagUtils" | ||||
|  |  | |||
|  | @ -36,6 +36,9 @@ export class MinimapViz implements SpecialVisualization { | |||
|         keys.splice(0, 1) | ||||
|         const featuresToShow: Store<Feature[]> = state.indexedFeatures.featuresById.map( | ||||
|             (featuresById) => { | ||||
|                 if (featuresById === undefined) { | ||||
|                     return [] | ||||
|                 } | ||||
|                 const properties = tagSource.data | ||||
|                 const features: Feature[] = [] | ||||
|                 for (const key of keys) { | ||||
|  |  | |||
|  | @ -1,124 +0,0 @@ | |||
| import Combine from "../Base/Combine" | ||||
| import { UIEventSource } from "../../Logic/UIEventSource" | ||||
| import { OsmConnection } from "../../Logic/Osm/OsmConnection" | ||||
| import Translations from "../i18n/Translations" | ||||
| import Title from "../Base/Title" | ||||
| import ValidatedTextField from "../Input/ValidatedTextField" | ||||
| import { SubtleButton } from "../Base/SubtleButton" | ||||
| import Svg from "../../Svg" | ||||
| import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource" | ||||
| import Toggle from "../Input/Toggle" | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||
| import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" | ||||
| import FilteredLayer from "../../Models/FilteredLayer" | ||||
| import Hash from "../../Logic/Web/Hash" | ||||
| 
 | ||||
| export default class NewNoteUi extends Toggle { | ||||
|     constructor( | ||||
|         noteLayer: FilteredLayer, | ||||
|         isShown: UIEventSource<boolean>, | ||||
|         state: { | ||||
|             LastClickLocation: UIEventSource<{ lat: number; lon: number }> | ||||
|             osmConnection: OsmConnection | ||||
|             layoutToUse: LayoutConfig | ||||
|             featurePipeline: FeaturePipeline | ||||
|             selectedElement: UIEventSource<any> | ||||
|         } | ||||
|     ) { | ||||
|         const t = Translations.t.notes | ||||
|         const isCreated = new UIEventSource(false) | ||||
|         state.LastClickLocation.addCallbackAndRun((_) => isCreated.setData(false)) // Reset 'isCreated' on every click
 | ||||
|         const text = ValidatedTextField.ForType("text").ConstructInputElement({ | ||||
|             value: LocalStorageSource.Get("note-text"), | ||||
|         }) | ||||
|         text.SetClass("border rounded-sm border-grey-500") | ||||
| 
 | ||||
|         const postNote = new SubtleButton(Svg.addSmall_svg().SetClass("max-h-7"), t.createNote) | ||||
|         postNote.OnClickWithLoading(t.creating, async () => { | ||||
|             let txt = text.GetValue().data | ||||
|             if (txt === undefined || txt === "") { | ||||
|                 return | ||||
|             } | ||||
|             txt += "\n\n #MapComplete #" + state?.layoutToUse?.id | ||||
|             const loc = state.LastClickLocation.data | ||||
|             const id = await state?.osmConnection?.openNote(loc.lat, loc.lon, txt) | ||||
|             const feature = { | ||||
|                 type: "Feature", | ||||
|                 geometry: { | ||||
|                     type: "Point", | ||||
|                     coordinates: [loc.lon, loc.lat], | ||||
|                 }, | ||||
|                 properties: { | ||||
|                     id: "" + id.id, | ||||
|                     date_created: new Date().toISOString(), | ||||
|                     _first_comment: txt, | ||||
|                     comments: JSON.stringify([ | ||||
|                         { | ||||
|                             text: txt, | ||||
|                             html: txt, | ||||
|                             user: state.osmConnection?.userDetails?.data?.name, | ||||
|                             uid: state.osmConnection?.userDetails?.data?.uid, | ||||
|                         }, | ||||
|                     ]), | ||||
|                 }, | ||||
|             } | ||||
|             state?.featurePipeline?.InjectNewPoint(feature) | ||||
|             state.selectedElement?.setData(feature) | ||||
|             Hash.hash.setData(feature.properties.id) | ||||
|             text.GetValue().setData("") | ||||
|             isCreated.setData(true) | ||||
|         }) | ||||
|         const createNoteDialog = new Combine([ | ||||
|             new Title(t.createNoteTitle), | ||||
|             t.createNoteIntro, | ||||
|             text, | ||||
|             new Combine([ | ||||
|                 new Toggle( | ||||
|                     undefined, | ||||
|                     t.warnAnonymous.SetClass("block alert"), | ||||
|                     state?.osmConnection?.isLoggedIn | ||||
|                 ), | ||||
|                 new Toggle( | ||||
|                     postNote, | ||||
|                     t.textNeeded.SetClass("block alert"), | ||||
|                     text.GetValue().map((txt) => txt?.length > 3) | ||||
|                 ), | ||||
|             ]).SetClass("flex justify-end items-center"), | ||||
|         ]).SetClass("flex flex-col border-2 border-black rounded-xl p-4") | ||||
| 
 | ||||
|         const newNoteUi = new Toggle( | ||||
|             new Toggle(t.isCreated.SetClass("thanks"), createNoteDialog, isCreated), | ||||
|             undefined, | ||||
|             new UIEventSource<boolean>(true) | ||||
|         ) | ||||
| 
 | ||||
|         super( | ||||
|             new Toggle( | ||||
|                 new Combine([ | ||||
|                     t.noteLayerHasFilters.SetClass("alert"), | ||||
|                     new SubtleButton(Svg.filter_svg(), t.disableAllNoteFilters).onClick(() => { | ||||
|                         const filters = noteLayer.appliedFilters.data | ||||
|                         for (const key of Array.from(filters.keys())) { | ||||
|                             filters.set(key, undefined) | ||||
|                         } | ||||
|                         noteLayer.appliedFilters.ping() | ||||
|                         isShown.setData(false) | ||||
|                     }), | ||||
|                 ]).SetClass("flex flex-col"), | ||||
|                 newNoteUi, | ||||
|                 noteLayer.appliedFilters.map((filters) => { | ||||
|                     console.log("Applied filters for notes are: ", filters) | ||||
|                     return Array.from(filters.values()).some((v) => v?.currentFilter !== undefined) | ||||
|                 }) | ||||
|             ), | ||||
|             new Combine([ | ||||
|                 t.noteLayerNotEnabled.SetClass("alert"), | ||||
|                 new SubtleButton(Svg.layers_svg(), t.noteLayerDoEnable).onClick(() => { | ||||
|                     noteLayer.isDisplayed.setData(true) | ||||
|                     isShown.setData(false) | ||||
|                 }), | ||||
|             ]).SetClass("flex flex-col"), | ||||
|             noteLayer.isDisplayed | ||||
|         ) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										35
									
								
								UI/Popup/TagHint.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								UI/Popup/TagHint.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | |||
| <script lang="ts"> | ||||
|   import { OsmConnection } from "../../Logic/Osm/OsmConnection"; | ||||
|   import { TagsFilter } from "../../Logic/Tags/TagsFilter"; | ||||
|   import FromHtml from "../Base/FromHtml.svelte"; | ||||
|   import Constants from "../../Models/Constants.js"; | ||||
|   import { Translation } from "../i18n/Translation"; | ||||
|   import Tr from "../Base/Tr.svelte"; | ||||
|   import { onDestroy } from "svelte"; | ||||
| 
 | ||||
|   /** | ||||
|    * A 'TagHint' will show the given tags in a human readable form. | ||||
|    * Depending on the options, it'll link through to the wiki or might be completely hidden | ||||
|    */ | ||||
|   export let osmConnection: OsmConnection; | ||||
|   /** | ||||
|    * If given, this function will be called to embed the given tags hint into this translation | ||||
|    */ | ||||
|   export let embedIn: (() => Translation) | undefined = undefined; | ||||
|   const userDetails = osmConnection.userDetails; | ||||
|   export let tags: TagsFilter; | ||||
|   let linkToWiki = false; | ||||
|   onDestroy(osmConnection.userDetails.addCallbackAndRunD(userdetails => { | ||||
|     linkToWiki = userdetails.csCount > Constants.userJourney.tagsVisibleAndWikiLinked; | ||||
|   })); | ||||
|   let tagsExplanation = ""; | ||||
|   $: tagsExplanation = tags?.asHumanString(linkToWiki, false, {}); | ||||
| </script> | ||||
| 
 | ||||
| {#if $userDetails.loggedIn} | ||||
|   {#if embedIn === undefined} | ||||
|     <FromHtml src={tagsExplanation} /> | ||||
|   {:else} | ||||
|     <Tr t={embedIn(tagsExplanation)} /> | ||||
|   {/if} | ||||
| {/if} | ||||
|  | @ -18,17 +18,23 @@ | |||
|   export let state: SpecialVisualizationState; | ||||
|   export let tags: UIEventSource<Record<string, string>>; | ||||
|   export let feature: Feature; | ||||
|   export let layer: LayerConfig | ||||
|   export let layer: LayerConfig; | ||||
| 
 | ||||
|   let txt: string; | ||||
|   onDestroy(Locale.language.addCallbackAndRunD(l => { | ||||
|   $: onDestroy(Locale.language.addCallbackAndRunD(l => { | ||||
|     txt = t.textFor(l); | ||||
|   })); | ||||
|   let specs: RenderingSpecification[] = SpecialVisualizations.constructSpecification(txt); | ||||
|   let specs: RenderingSpecification[] = []; | ||||
|   $: { | ||||
|     if (txt !== undefined) { | ||||
|       specs = SpecialVisualizations.constructSpecification(txt); | ||||
|     } | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
| {#each specs as specpart} | ||||
|   {#if typeof specpart === "string"} | ||||
|    <FromHtml src= {Utils.SubstituteKeys(specpart, $tags)}></FromHtml> | ||||
|     <FromHtml src={Utils.SubstituteKeys(specpart, $tags)}></FromHtml> | ||||
|   {:else if $tags !== undefined } | ||||
|     <ToSvelte construct={specpart.func.constr(state, tags, specpart.args, feature, layer)}></ToSvelte> | ||||
|   {/if} | ||||
|  |  | |||
|  | @ -17,6 +17,9 @@ | |||
|   export let state: SpecialVisualizationState; | ||||
|   export let selectedElement: Feature; | ||||
|   export let config: TagRenderingConfig; | ||||
|   if(config === undefined){ | ||||
|     throw "Config is undefined in tagRenderingAnswer" | ||||
|   } | ||||
|   export let layer: LayerConfig | ||||
|   let trs: { then: Translation; icon?: string; iconClass?: string }[]; | ||||
|   $: trs = Utils.NoNull(config?.GetRenderValues(_tags)); | ||||
|  |  | |||
|  | @ -16,6 +16,7 @@ | |||
|   import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; | ||||
|   import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||
|   import SpecialTranslation from "./SpecialTranslation.svelte"; | ||||
|   import TagHint from "../TagHint.svelte"; | ||||
| 
 | ||||
|   export let config: TagRenderingConfig; | ||||
|   export let tags: UIEventSource<Record<string, string>>; | ||||
|  | @ -87,7 +88,9 @@ | |||
|   <div class="border border-black subtle-background flex flex-col"> | ||||
|     <If condition={state.featureSwitchIsTesting}> | ||||
|       <div class="flex justify-between"> | ||||
|         <SpecialTranslation t={config.question} {tags} {state} {layer} feature={selectedElement}></SpecialTranslation> | ||||
|         <span> | ||||
|           <SpecialTranslation t={config.question} {tags} {state} {layer} feature={selectedElement}></SpecialTranslation> | ||||
|         </span> | ||||
|         <span class="alert">{config.id}</span> | ||||
|       </div> | ||||
|       <SpecialTranslation slot="else" t={config.question} {tags} {state} {layer} feature={selectedElement}></SpecialTranslation> | ||||
|  | @ -149,8 +152,7 @@ | |||
|       </div> | ||||
|     {/if} | ||||
| 
 | ||||
|     <FromHtml src={selectedTags?.asHumanString(true, true, {})} /> | ||||
| 
 | ||||
|   <TagHint osmConnection={state.osmConnection} tags={selectedTags}></TagHint> | ||||
|     <div> | ||||
|       <!-- TagRenderingQuestion-buttons --> | ||||
|       <slot name="cancel"></slot> | ||||
|  |  | |||
|  | @ -2,7 +2,11 @@ import { Store, UIEventSource } from "../Logic/UIEventSource" | |||
| import BaseUIElement from "./BaseUIElement" | ||||
| import { DefaultGuiState } from "./DefaultGuiState" | ||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" | ||||
| import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource" | ||||
| import { | ||||
|     FeatureSource, | ||||
|     IndexedFeatureSource, | ||||
|     WritableFeatureSource, | ||||
| } from "../Logic/FeatureSource/FeatureSource" | ||||
| import { OsmConnection } from "../Logic/Osm/OsmConnection" | ||||
| import { Changes } from "../Logic/Osm/Changes" | ||||
| import { MapProperties } from "../Models/MapProperties" | ||||
|  | @ -13,12 +17,14 @@ import { MangroveIdentity } from "../Logic/Web/MangroveReviews" | |||
| import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore" | ||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig" | ||||
| import FeatureSwitchState from "../Logic/State/FeatureSwitchState" | ||||
| import SimpleFeatureSource from "../Logic/FeatureSource/Sources/SimpleFeatureSource" | ||||
| import { MenuState } from "../Models/MenuState" | ||||
| 
 | ||||
| /** | ||||
|  * The state needed to render a special Visualisation. | ||||
|  */ | ||||
| export interface SpecialVisualizationState { | ||||
|     readonly guistate: DefaultGuiState | ||||
|     readonly guistate: MenuState | ||||
|     readonly layout: LayoutConfig | ||||
|     readonly featureSwitches: FeatureSwitchState | ||||
| 
 | ||||
|  | @ -27,6 +33,12 @@ export interface SpecialVisualizationState { | |||
| 
 | ||||
|     readonly indexedFeatures: IndexedFeatureSource | ||||
| 
 | ||||
|     /** | ||||
|      * Some features will create a new element that should be displayed. | ||||
|      * These can be injected by appending them to this featuresource (and pinging it) | ||||
|      */ | ||||
|     readonly newFeatures: WritableFeatureSource | ||||
| 
 | ||||
|     readonly historicalUserLocations: WritableFeatureSource | ||||
| 
 | ||||
|     readonly osmConnection: OsmConnection | ||||
|  | @ -39,6 +51,10 @@ export interface SpecialVisualizationState { | |||
|     readonly mapProperties: MapProperties | ||||
| 
 | ||||
|     readonly selectedElement: UIEventSource<Feature> | ||||
|     /** | ||||
|      * Works together with 'selectedElement' to indicate what properties should be displayed | ||||
|      */ | ||||
|     readonly selectedLayer: UIEventSource<LayerConfig> | ||||
| 
 | ||||
|     /** | ||||
|      * If data is currently being fetched from external sources | ||||
|  | @ -54,6 +70,7 @@ export interface SpecialVisualizationState { | |||
|         readonly mangroveIdentity: MangroveIdentity | ||||
|         readonly showAllQuestionsAtOnce: UIEventSource<boolean> | ||||
|     } | ||||
|     readonly lastClickObject: WritableFeatureSource | ||||
| } | ||||
| 
 | ||||
| export interface SpecialVisualization { | ||||
|  |  | |||
|  | @ -57,6 +57,11 @@ import SvelteUIElement from "./Base/SvelteUIElement" | |||
| import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" | ||||
| import QuestionViz from "./Popup/QuestionViz" | ||||
| import SimpleAddUI from "./BigComponents/SimpleAddUI" | ||||
| import { Feature } from "geojson" | ||||
| import { GeoOperations } from "../Logic/GeoOperations" | ||||
| import CreateNewNote from "./Popup/CreateNewNote.svelte" | ||||
| import { svelte } from "@sveltejs/vite-plugin-svelte" | ||||
| import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte" | ||||
| 
 | ||||
| export default class SpecialVisualizations { | ||||
|     public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList() | ||||
|  | @ -84,7 +89,10 @@ export default class SpecialVisualizations { | |||
|         } | ||||
| 
 | ||||
|         if (template["type"] !== undefined) { | ||||
|             console.trace("Got a non-expanded template while constructing the specification") | ||||
|             console.trace( | ||||
|                 "Got a non-expanded template while constructing the specification:", | ||||
|                 template | ||||
|             ) | ||||
|             throw "Got a non-expanded template while constructing the specification" | ||||
|         } | ||||
|         const allKnownSpecials = extraMappings.concat(SpecialVisualizations.specialVisualizations) | ||||
|  | @ -230,6 +238,26 @@ export default class SpecialVisualizations { | |||
|         ]).SetClass("flex flex-col") | ||||
|     } | ||||
| 
 | ||||
|     // noinspection JSUnusedGlobalSymbols
 | ||||
|     public static renderExampleOfSpecial( | ||||
|         state: SpecialVisualizationState, | ||||
|         s: SpecialVisualization | ||||
|     ): BaseUIElement { | ||||
|         const examples = | ||||
|             s.structuredExamples === undefined | ||||
|                 ? [] | ||||
|                 : s.structuredExamples().map((e) => { | ||||
|                       return s.constr( | ||||
|                           state, | ||||
|                           new UIEventSource<Record<string, string>>(e.feature.properties), | ||||
|                           e.args, | ||||
|                           e.feature, | ||||
|                           undefined | ||||
|                       ) | ||||
|                   }) | ||||
|         return new Combine([new Title(s.funcName), s.docs, ...examples]) | ||||
|     } | ||||
| 
 | ||||
|     private static initList(): SpecialVisualization[] { | ||||
|         const specialVisualizations: SpecialVisualization[] = [ | ||||
|             new QuestionViz(), | ||||
|  | @ -237,11 +265,14 @@ export default class SpecialVisualizations { | |||
|                 funcName: "add_new_point", | ||||
|                 docs: "An element which allows to add a new point on the 'last_click'-location. Only makes sense in the layer `last_click`", | ||||
|                 args: [], | ||||
|                 constr(state: SpecialVisualizationState): BaseUIElement { | ||||
|                     return new SimpleAddUI(state) | ||||
|                 constr(state: SpecialVisualizationState, _, __, feature): BaseUIElement { | ||||
|                     let [lon, lat] = GeoOperations.centerpointCoordinates(feature) | ||||
|                     return new SvelteUIElement(AddNewPoint, { | ||||
|                         state, | ||||
|                         coordinate: { lon, lat }, | ||||
|                     }) | ||||
|                 }, | ||||
|             }, | ||||
| 
 | ||||
|             new HistogramViz(), | ||||
|             new StealViz(), | ||||
|             new MinimapViz(), | ||||
|  | @ -250,6 +281,20 @@ export default class SpecialVisualizations { | |||
|             new MultiApplyViz(), | ||||
|             new ExportAsGpxViz(), | ||||
|             new AddNoteCommentViz(), | ||||
|             { | ||||
|                 funcName: "open_note", | ||||
|                 args: [], | ||||
|                 docs: "Creates a new map note on the given location. This options is placed in the 'last_click'-popup automatically if the 'notes'-layer is enabled", | ||||
|                 constr( | ||||
|                     state: SpecialVisualizationState, | ||||
|                     tagSource: UIEventSource<Record<string, string>>, | ||||
|                     argument: string[], | ||||
|                     feature: Feature | ||||
|                 ): BaseUIElement { | ||||
|                     const [lon, lat] = GeoOperations.centerpointCoordinates(feature) | ||||
|                     return new SvelteUIElement(CreateNewNote, { state, coordinate: { lon, lat } }) | ||||
|                 }, | ||||
|             }, | ||||
|             new CloseNoteButton(), | ||||
|             new PlantNetDetectionViz(), | ||||
| 
 | ||||
|  | @ -680,9 +725,7 @@ export default class SpecialVisualizations { | |||
|                             if (title === undefined) { | ||||
|                                 return undefined | ||||
|                             } | ||||
|                             return new SubstitutedTranslation(title, tagsSource, state).RemoveClass( | ||||
|                                 "w-full" | ||||
|                             ) | ||||
|                             return new SubstitutedTranslation(title, tagsSource, state) | ||||
|                         }) | ||||
|                     ), | ||||
|             }, | ||||
|  | @ -960,24 +1003,4 @@ export default class SpecialVisualizations { | |||
| 
 | ||||
|         return specialVisualizations | ||||
|     } | ||||
| 
 | ||||
|     // noinspection JSUnusedGlobalSymbols
 | ||||
|     public static renderExampleOfSpecial( | ||||
|         state: SpecialVisualizationState, | ||||
|         s: SpecialVisualization | ||||
|     ): BaseUIElement { | ||||
|         const examples = | ||||
|             s.structuredExamples === undefined | ||||
|                 ? [] | ||||
|                 : s.structuredExamples().map((e) => { | ||||
|                       return s.constr( | ||||
|                           state, | ||||
|                           new UIEventSource<Record<string, string>>(e.feature.properties), | ||||
|                           e.args, | ||||
|                           e.feature, | ||||
|                           undefined | ||||
|                       ) | ||||
|                   }) | ||||
|         return new Combine([new Title(s.funcName), s.docs, ...examples]) | ||||
|     } | ||||
| } | ||||
|  |  | |||
							
								
								
									
										18
									
								
								UI/Test.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								UI/Test.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | |||
| <script lang="ts"> | ||||
|   // Testing grounds | ||||
|   import { UIEventSource } from "../Logic/UIEventSource"; | ||||
|   import TabbedGroup from "./Base/TabbedGroup.svelte"; | ||||
| 
 | ||||
|   let tab = new UIEventSource(1) | ||||
|   console.log("Tab control", tab) | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <TabbedGroup {tab}> | ||||
|   <div slot="title0">Title 0</div> | ||||
|   <div slot="content0">Content 0 loaded</div> | ||||
| 
 | ||||
|   <div slot="title1">Title 1</div> | ||||
|   <div slot="content1">Content 1</div> | ||||
| 
 | ||||
| </TabbedGroup> | ||||
|  | @ -1,5 +1,5 @@ | |||
| <script lang="ts"> | ||||
|   import { ImmutableStore, Store, UIEventSource } from "../Logic/UIEventSource"; | ||||
|   import { Store, UIEventSource } from "../Logic/UIEventSource"; | ||||
|   import { Map as MlMap } from "maplibre-gl"; | ||||
|   import MaplibreMap from "./Map/MaplibreMap.svelte"; | ||||
|   import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; | ||||
|  | @ -19,10 +19,14 @@ | |||
|   import Geosearch from "./BigComponents/Geosearch.svelte"; | ||||
|   import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui"; | ||||
|   import Translations from "./i18n/Translations"; | ||||
|   import { MenuIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||
|   import { CogIcon, MenuIcon, EyeIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||
|   import Tr from "./Base/Tr.svelte"; | ||||
|   import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"; | ||||
|   import FloatOver from "./Base/FloatOver.svelte"; | ||||
|   import PrivacyPolicy from "./BigComponents/PrivacyPolicy.js"; | ||||
|   import { Utils } from "../Utils.js"; | ||||
|   import Constants from "../Models/Constants"; | ||||
|   import TabbedGroup from "./Base/TabbedGroup.svelte"; | ||||
| 
 | ||||
|   export let layout: LayoutConfig; | ||||
|   const state = new ThemeViewState(layout); | ||||
|  | @ -47,8 +51,8 @@ | |||
| </div> | ||||
| 
 | ||||
| <div class="absolute top-0 left-0 mt-2 ml-2"> | ||||
|   <MapControlButton on:click={() => state.guistate.welcomeMessageIsOpened.setData(true)}> | ||||
|     <div class="flex mr-2 items-center"> | ||||
|   <MapControlButton on:click={() => state.guistate.themeIsOpened.setData(true)}> | ||||
|     <div class="flex mr-2 items-center cursor-pointer"> | ||||
|       <img class="w-8 h-8 block mr-2" src={layout.icon}> | ||||
|       <b> | ||||
|         <Tr t={layout.title}></Tr> | ||||
|  | @ -56,7 +60,7 @@ | |||
|     </div> | ||||
|   </MapControlButton> | ||||
|   <MapControlButton on:click={() =>state.guistate.menuIsOpened.setData(true)}> | ||||
|     <MenuIcon class="w-8 h-8"></MenuIcon> | ||||
|     <MenuIcon class="w-8 h-8 cursor-pointer"></MenuIcon> | ||||
|   </MapControlButton> | ||||
|   <If condition={state.featureSwitchIsTesting}> | ||||
|     <span class="alert"> | ||||
|  | @ -86,107 +90,118 @@ | |||
| 
 | ||||
| <div class="absolute top-0 right-0 mt-4 mr-4"> | ||||
|   <If condition={state.featureSwitches.featureSwitchSearch}> | ||||
|     <Geosearch bounds={state.mapProperties.bounds} {selectedElement} {selectedLayer}></Geosearch> | ||||
|     <Geosearch bounds={state.mapProperties.bounds} {selectedElement} {selectedLayer} {state}></Geosearch> | ||||
|   </If> | ||||
| </div> | ||||
| 
 | ||||
| 
 | ||||
| <If condition={state.guistate.welcomeMessageIsOpened}> | ||||
|   <!-- Theme page --> | ||||
|   <FloatOver> | ||||
|       <div on:click={() => state.guistate.welcomeMessageIsOpened.setData(false)}>Close</div> | ||||
|       <TabGroup> | ||||
|         <TabList> | ||||
|           <Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}> | ||||
|             <Tr t={layout.title} /> | ||||
|           </Tab> | ||||
|           <Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}> | ||||
|             <Tr t={Translations.t.general.menu.filter} /> | ||||
|           </Tab> | ||||
|           <Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>Tab 3</Tab> | ||||
|         </TabList> | ||||
|         <TabPanels> | ||||
|           <TabPanel class="flex flex-col"> | ||||
|             <Tr t={layout.description}></Tr> | ||||
|             <Tr t={Translations.t.general.welcomeExplanation.general} /> | ||||
|             {#if layout.layers.some((l) => l.presets?.length > 0)} | ||||
|               <If condition={state.featureSwitches.featureSwitchAddNew}> | ||||
|                 <Tr t={Translations.t.general.welcomeExplanation.addNew} /> | ||||
|               </If> | ||||
|             {/if} | ||||
| 
 | ||||
|             <!--toTheMap, | ||||
|             loginStatus.SetClass("block mt-6 pt-2 md:border-t-2 border-dotted border-gray-400"), | ||||
|             --> | ||||
|             <Tr t={layout.descriptionTail}></Tr> | ||||
|             <div class="m-x-8"> | ||||
|               <button class="subtle-background rounded w-full p-4" | ||||
|                       on:click={() => state.guistate.welcomeMessageIsOpened.setData(false)}> | ||||
|                 <Tr t={Translations.t.general.openTheMap} /> | ||||
|               </button> | ||||
|             </div> | ||||
| 
 | ||||
| 
 | ||||
|           </TabPanel> | ||||
|           <TabPanel> | ||||
|             <div class="flex flex-col"> | ||||
|               <!-- Filter panel -- TODO move to actual location--> | ||||
|               {#each layout.layers as layer} | ||||
|                 <Filterview filteredLayer={state.layerState.filteredLayers.get(layer.id)}></Filterview> | ||||
|               {/each} | ||||
| 
 | ||||
|               <RasterLayerPicker {availableLayers} value={mapproperties.rasterLayer}></RasterLayerPicker> | ||||
|             </div> | ||||
|           </TabPanel> | ||||
|           <TabPanel>Content 3</TabPanel> | ||||
|         </TabPanels> | ||||
|       </TabGroup> | ||||
|    </FloatOver> | ||||
| </If> | ||||
| 
 | ||||
| 
 | ||||
| <If condition={state.guistate.menuIsOpened}> | ||||
|   <!-- Menu page --> | ||||
|   <FloatOver> | ||||
|       <div on:click={() => state.guistate.menuIsOpened.setData(false)}>Close</div> | ||||
|       <TabGroup> | ||||
|         <TabList> | ||||
|           <Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>About MapComplete</Tab> | ||||
|           <Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>Settings</Tab> | ||||
|           <Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}> | ||||
|             <div class="flex"> | ||||
|               <div class="w-6"> | ||||
|                 <ToSvelte construct={Svg.community_ui}></ToSvelte> | ||||
|               </div> | ||||
|               Get in touch with others | ||||
|             </div> | ||||
|           </Tab> | ||||
|           <Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>Privacy</Tab> | ||||
|         </TabList> | ||||
|         <TabPanels> | ||||
|           <TabPanel class="flex flex-col"> | ||||
|             About MC | ||||
| 
 | ||||
| 
 | ||||
|           </TabPanel> | ||||
|           <TabPanel>User settings</TabPanel> | ||||
|           <TabPanel> | ||||
|             <CommunityIndexView location={state.mapProperties.location}></CommunityIndexView> | ||||
| 
 | ||||
|           </TabPanel> | ||||
|           <TabPanel>Privacy</TabPanel> | ||||
|         </TabPanels> | ||||
|       </TabGroup> | ||||
|   </FloatOver> | ||||
| </If> | ||||
| 
 | ||||
| {#if $selectedElement !== undefined && $selectedLayer !== undefined} | ||||
|   <FloatOver> | ||||
|   <FloatOver on:close={() => {selectedElement.setData(undefined)}}> | ||||
|     <SelectedElementView layer={$selectedLayer} selectedElement={$selectedElement} | ||||
|                          tags={$selectedElementTags} state={state}></SelectedElementView> | ||||
|   </FloatOver> | ||||
| 
 | ||||
| {/if} | ||||
| 
 | ||||
| <If condition={state.guistate.themeIsOpened}> | ||||
|   <!-- Theme page --> | ||||
|   <FloatOver on:close={() => state.guistate.themeIsOpened.setData(false)}> | ||||
|     <TabbedGroup tab={state.guistate.themeViewTabIndex}> | ||||
|           <Tr slot="title0" t={layout.title} /> | ||||
| 
 | ||||
|       <div slot="content0"> | ||||
| 
 | ||||
|         <Tr t={layout.description}></Tr> | ||||
|         <Tr t={Translations.t.general.welcomeExplanation.general} /> | ||||
|         {#if layout.layers.some((l) => l.presets?.length > 0)} | ||||
|           <If condition={state.featureSwitches.featureSwitchAddNew}> | ||||
|             <Tr t={Translations.t.general.welcomeExplanation.addNew} /> | ||||
|           </If> | ||||
|         {/if} | ||||
| 
 | ||||
|         <!--toTheMap, | ||||
|         loginStatus.SetClass("block mt-6 pt-2 md:border-t-2 border-dotted border-gray-400"), | ||||
|         --> | ||||
|         <Tr t={layout.descriptionTail}></Tr> | ||||
|         <div class="m-x-8"> | ||||
|           <button class="subtle-background rounded w-full p-4" | ||||
|                   on:click={() => state.guistate.themeIsOpened.setData(false)}> | ||||
|             <Tr t={Translations.t.general.openTheMap} /> | ||||
|           </button> | ||||
|         </div> | ||||
| 
 | ||||
|       </div> | ||||
|        | ||||
|       <div slot="title1" class="flex"> | ||||
|         <If condition={state.featureSwitches.featureSwitchFilter}> | ||||
|         <img class="w-4 h-4" src="./assets/svg/filter.svg"> | ||||
|         <Tr t={Translations.t.general.menu.filter} /> | ||||
|         </If> | ||||
|       </div> | ||||
| 
 | ||||
|       <div slot="content1" class="flex flex-col"> | ||||
|         {#each layout.layers as layer} | ||||
|           <Filterview zoomlevel={state.mapProperties.zoom} filteredLayer={state.layerState.filteredLayers.get(layer.id)} highlightedLayer={state.guistate.highlightedLayerInFilters}></Filterview> | ||||
|         {/each} | ||||
|         <If condition={state.featureSwitches.featureSwitchBackgroundSelection}> | ||||
|           <RasterLayerPicker {availableLayers} value={mapproperties.rasterLayer}></RasterLayerPicker> | ||||
|         </If> | ||||
|       </div> | ||||
|     </TabbedGroup> | ||||
|   </FloatOver> | ||||
| </If> | ||||
| 
 | ||||
| 
 | ||||
| <If condition={state.guistate.menuIsOpened}> | ||||
|   <!-- Menu page --> | ||||
|   <FloatOver on:close={() => state.guistate.menuIsOpened.setData(false)}> | ||||
|     <TabGroup on:change={(e) => {state.guistate.menuViewTabIndex.setData(e.detail)} }> | ||||
|       <TabList> | ||||
|         <Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}> | ||||
|           <div class="flex"> | ||||
|             <Tr t={Translations.t.general.aboutMapcompleteTitle}></Tr> | ||||
|           </div> | ||||
|         </Tab> | ||||
|         <Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}> | ||||
|           <div class="flex"> | ||||
|             <CogIcon class="w-6 h-6"/> | ||||
|             Settings | ||||
|           </div> | ||||
|         </Tab> | ||||
|         <Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}> | ||||
|           <div class="flex"> | ||||
|             <img class="w-6" src="./assets/svg/community.svg"> | ||||
|             Get in touch with others | ||||
|           </div> | ||||
|         </Tab> | ||||
|         <Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}> | ||||
|           <div class="flex"> | ||||
|             <EyeIcon class="w-6"/> | ||||
|             <Tr t={Translations.t.privacy.title}></Tr> | ||||
|           </div> | ||||
|         </Tab> | ||||
|       </TabList> | ||||
|       <TabPanels > | ||||
|         <TabPanel class="flex flex-col"> | ||||
|           <Tr t={Translations.t.general.aboutMapcomplete.Subs({ | ||||
|                     osmcha_link: Utils.OsmChaLinkFor(7), | ||||
|                 })}></Tr> | ||||
| 
 | ||||
|           {Constants.vNumber} | ||||
|         </TabPanel> | ||||
|         <TabPanel>User settings</TabPanel> | ||||
|         <TabPanel> | ||||
|           <CommunityIndexView location={state.mapProperties.location}></CommunityIndexView> | ||||
| 
 | ||||
|         </TabPanel> | ||||
|         <TabPanel> | ||||
|           <ToSvelte construct={() => new PrivacyPolicy()}></ToSvelte> | ||||
|         </TabPanel> | ||||
|       </TabPanels> | ||||
|     </TabGroup> | ||||
|   </FloatOver> | ||||
| </If> | ||||
| 
 | ||||
| 
 | ||||
| <style> | ||||
|     /* WARNING: This is just for demonstration. | ||||
|         Using :global() in this way can be risky. */ | ||||
|  |  | |||
|  | @ -37,14 +37,17 @@ | |||
|   "tagRenderings": [ | ||||
|     { | ||||
|       "id": "add_new", | ||||
|       "mappings": [ | ||||
|         { | ||||
|           "if": "has_presets=yes", | ||||
|           "then": { | ||||
|             "*": "{add_new_point()}" | ||||
|           } | ||||
|         } | ||||
|       ] | ||||
|       "condition": "has_presets=yes", | ||||
|       "render": { | ||||
|         "*": "{add_new_point()}" | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "id": "add_note", | ||||
|       "condition": "has_note_layer=yes", | ||||
|       "render": { | ||||
|         "*": "{open_note()}" | ||||
|       } | ||||
|     }, | ||||
|     "all_tags" | ||||
|   ], | ||||
|  | @ -52,6 +55,15 @@ | |||
|     { | ||||
|       "icon": { | ||||
|         "mappings": [ | ||||
|           { | ||||
|             "if": { | ||||
|               "and": [ | ||||
|                 "has_note_layer=yes", | ||||
|                 "has_presets=no" | ||||
|               ] | ||||
|             }, | ||||
|             "then": "./assets/svg/note.svg" | ||||
|           }, | ||||
|           { | ||||
|             "if": "number_of_presets=1", | ||||
|             "then": "{first_preset}" | ||||
|  | @ -59,7 +71,8 @@ | |||
|         ], | ||||
|         "render": "<div class='relative'> <img src='./assets/svg/add_pin.svg' class='absolute' style='height: 50px'> <div class='absolute top-0 left-0 rounded-full overflow-hidden' style='width: 40px; height: 40px'><div class='flex slide min-w-min' style='animation: slide linear {number_of_presets}s infinite; width: calc( (1 + {number_of_presets}) * 40px ); height: 40px'>{renderings}{first_preset}</div></div></div>" | ||||
|       }, | ||||
|       "labelCssClasses": "text-sm min-w-min pl-1 pr-1 bg-gray-400 rounded-3xl text-white opacity-65 whitespace-nowrap", | ||||
|       "labelCssClasses": "text-sm min-w-min pl-1 pr-1 rounded-3xl text-white opacity-65 whitespace-nowrap block-ruby", | ||||
|       "labelCss": "background: #00000088", | ||||
|       "label": { | ||||
|         "render": { | ||||
|           "ca": "Afegir nou element", | ||||
|  | @ -77,7 +90,21 @@ | |||
|           "nl": "Klik hier om een item toe te voegen", | ||||
|           "pt": "Adicionar novo item", | ||||
|           "zh_Hant": "點這邊新增新項目" | ||||
|         } | ||||
|         }, | ||||
|         "mappings": [ | ||||
|           { | ||||
|             "if": { | ||||
|               "and": [ | ||||
|                 "has_note_layer=yes", | ||||
|                 "has_presets=yesno" | ||||
|               ] | ||||
|             }, | ||||
|             "then": { | ||||
|               "en": "Create a new map note", | ||||
|               "nl": "Maak een nieuwe kaartnotitie" | ||||
|             } | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       "iconBadges": [ | ||||
|         { | ||||
|  | @ -93,7 +120,20 @@ | |||
|       "location": [ | ||||
|         "point" | ||||
|       ], | ||||
|       "iconSize": "40,50,bottom" | ||||
|       "iconSize": { | ||||
|         "mappings": [ | ||||
|           { | ||||
|             "if": { | ||||
|               "and": [ | ||||
|                 "has_note_layer=yes", | ||||
|                 "has_presets=no" | ||||
|               ] | ||||
|             }, | ||||
|             "then": "40,40,bottom" | ||||
|           } | ||||
|         ], | ||||
|         "render": "40,50,bottom" | ||||
|       } | ||||
|     } | ||||
|   ], | ||||
|   "filter": [ | ||||
|  |  | |||
|  | @ -6,10 +6,7 @@ | |||
|     "de": "Hebt das aktuell ausgewählte Element hervor. Überschreiben Sie diese Ebene, um unterschiedliche Farben zu erhalten", | ||||
|     "fr": "Met en surbrillance l'élément actuellement sélectioné. Surcharger cette couche pour avoir d'autres couleurs." | ||||
|   }, | ||||
|   "source": { | ||||
|     "osmTags": "selected=yes", | ||||
|     "maxCacheAge": 0 | ||||
|   }, | ||||
|   "source": "special", | ||||
|   "mapRendering": [ | ||||
|     { | ||||
|       "icon": "circle:red", | ||||
|  |  | |||
|  | @ -750,6 +750,14 @@ video { | |||
|   right: 33.333333%; | ||||
| } | ||||
| 
 | ||||
| .right-10 { | ||||
|   right: 2.5rem; | ||||
| } | ||||
| 
 | ||||
| .top-10 { | ||||
|   top: 2.5rem; | ||||
| } | ||||
| 
 | ||||
| .top-4 { | ||||
|   top: 1rem; | ||||
| } | ||||
|  | @ -794,10 +802,6 @@ video { | |||
|   margin: 1.25rem; | ||||
| } | ||||
| 
 | ||||
| .m-2 { | ||||
|   margin: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| .m-0\.5 { | ||||
|   margin: 0.125rem; | ||||
| } | ||||
|  | @ -810,6 +814,10 @@ video { | |||
|   margin: 0.75rem; | ||||
| } | ||||
| 
 | ||||
| .m-2 { | ||||
|   margin: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| .m-4 { | ||||
|   margin: 1rem; | ||||
| } | ||||
|  | @ -903,18 +911,6 @@ video { | |||
|   margin-bottom: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| .mt-12 { | ||||
|   margin-top: 3rem; | ||||
| } | ||||
| 
 | ||||
| .ml-3 { | ||||
|   margin-left: 0.75rem; | ||||
| } | ||||
| 
 | ||||
| .ml-12 { | ||||
|   margin-left: 3rem; | ||||
| } | ||||
| 
 | ||||
| .mt-3 { | ||||
|   margin-top: 0.75rem; | ||||
| } | ||||
|  | @ -935,6 +931,10 @@ video { | |||
|   margin-top: 2rem; | ||||
| } | ||||
| 
 | ||||
| .ml-3 { | ||||
|   margin-left: 0.75rem; | ||||
| } | ||||
| 
 | ||||
| .mb-8 { | ||||
|   margin-bottom: 2rem; | ||||
| } | ||||
|  | @ -1047,6 +1047,10 @@ video { | |||
|   height: 1rem; | ||||
| } | ||||
| 
 | ||||
| .h-6 { | ||||
|   height: 1.5rem; | ||||
| } | ||||
| 
 | ||||
| .h-1\/2 { | ||||
|   height: 50%; | ||||
| } | ||||
|  | @ -1055,10 +1059,6 @@ video { | |||
|   height: 0.75rem; | ||||
| } | ||||
| 
 | ||||
| .h-6 { | ||||
|   height: 1.5rem; | ||||
| } | ||||
| 
 | ||||
| .h-11 { | ||||
|   height: 2.75rem; | ||||
| } | ||||
|  | @ -1095,6 +1095,10 @@ video { | |||
|   max-height: 2rem; | ||||
| } | ||||
| 
 | ||||
| .max-h-24 { | ||||
|   max-height: 6rem; | ||||
| } | ||||
| 
 | ||||
| .min-h-\[8rem\] { | ||||
|   min-height: 8rem; | ||||
| } | ||||
|  | @ -1175,17 +1179,12 @@ video { | |||
|   width: 6rem; | ||||
| } | ||||
| 
 | ||||
| .w-auto { | ||||
|   width: auto; | ||||
| } | ||||
| 
 | ||||
| .w-48 { | ||||
|   width: 12rem; | ||||
| } | ||||
| 
 | ||||
| .min-w-min { | ||||
|   min-width: -webkit-min-content; | ||||
|   min-width: min-content; | ||||
| .w-auto { | ||||
|   width: auto; | ||||
| } | ||||
| 
 | ||||
| .max-w-full { | ||||
|  | @ -1378,10 +1377,6 @@ video { | |||
|   text-overflow: ellipsis; | ||||
| } | ||||
| 
 | ||||
| .whitespace-nowrap { | ||||
|   white-space: nowrap; | ||||
| } | ||||
| 
 | ||||
| .break-normal { | ||||
|   overflow-wrap: normal; | ||||
|   word-break: normal; | ||||
|  | @ -1432,10 +1427,6 @@ video { | |||
|   border-bottom-left-radius: 0.25rem; | ||||
| } | ||||
| 
 | ||||
| .border-2 { | ||||
|   border-width: 2px; | ||||
| } | ||||
| 
 | ||||
| .border { | ||||
|   border-width: 1px; | ||||
| } | ||||
|  | @ -1444,6 +1435,10 @@ video { | |||
|   border-width: 4px; | ||||
| } | ||||
| 
 | ||||
| .border-2 { | ||||
|   border-width: 2px; | ||||
| } | ||||
| 
 | ||||
| .border-l-4 { | ||||
|   border-left-width: 4px; | ||||
| } | ||||
|  | @ -1533,11 +1528,6 @@ video { | |||
|   background-color: rgb(219 234 254 / var(--tw-bg-opacity)); | ||||
| } | ||||
| 
 | ||||
| .bg-gray-400 { | ||||
|   --tw-bg-opacity: 1; | ||||
|   background-color: rgb(156 163 175 / var(--tw-bg-opacity)); | ||||
| } | ||||
| 
 | ||||
| .bg-black { | ||||
|   --tw-bg-opacity: 1; | ||||
|   background-color: rgb(0 0 0 / var(--tw-bg-opacity)); | ||||
|  | @ -1611,11 +1601,6 @@ video { | |||
|   padding-right: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| .px-0 { | ||||
|   padding-left: 0px; | ||||
|   padding-right: 0px; | ||||
| } | ||||
| 
 | ||||
| .px-4 { | ||||
|   padding-left: 1rem; | ||||
|   padding-right: 1rem; | ||||
|  | @ -1653,22 +1638,6 @@ video { | |||
|   padding-bottom: 0.25rem; | ||||
| } | ||||
| 
 | ||||
| .pl-1 { | ||||
|   padding-left: 0.25rem; | ||||
| } | ||||
| 
 | ||||
| .pr-1 { | ||||
|   padding-right: 0.25rem; | ||||
| } | ||||
| 
 | ||||
| .pb-2 { | ||||
|   padding-bottom: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| .pt-0 { | ||||
|   padding-top: 0px; | ||||
| } | ||||
| 
 | ||||
| .pr-2 { | ||||
|   padding-right: 0.5rem; | ||||
| } | ||||
|  | @ -1677,6 +1646,10 @@ video { | |||
|   padding-top: 0.125rem; | ||||
| } | ||||
| 
 | ||||
| .pt-0 { | ||||
|   padding-top: 0px; | ||||
| } | ||||
| 
 | ||||
| .pb-8 { | ||||
|   padding-bottom: 2rem; | ||||
| } | ||||
|  | @ -1693,14 +1666,26 @@ video { | |||
|   padding-right: 1rem; | ||||
| } | ||||
| 
 | ||||
| .pl-1 { | ||||
|   padding-left: 0.25rem; | ||||
| } | ||||
| 
 | ||||
| .pr-0 { | ||||
|   padding-right: 0px; | ||||
| } | ||||
| 
 | ||||
| .pr-1 { | ||||
|   padding-right: 0.25rem; | ||||
| } | ||||
| 
 | ||||
| .pb-4 { | ||||
|   padding-bottom: 1rem; | ||||
| } | ||||
| 
 | ||||
| .pb-2 { | ||||
|   padding-bottom: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| .pl-6 { | ||||
|   padding-left: 1.5rem; | ||||
| } | ||||
|  | @ -1793,11 +1778,6 @@ video { | |||
|   letter-spacing: -0.025em; | ||||
| } | ||||
| 
 | ||||
| .text-white { | ||||
|   --tw-text-opacity: 1; | ||||
|   color: rgb(255 255 255 / var(--tw-text-opacity)); | ||||
| } | ||||
| 
 | ||||
| .text-gray-900 { | ||||
|   --tw-text-opacity: 1; | ||||
|   color: rgb(17 24 39 / var(--tw-text-opacity)); | ||||
|  | @ -1823,6 +1803,11 @@ video { | |||
|   color: rgb(153 153 153 / var(--tw-text-opacity)); | ||||
| } | ||||
| 
 | ||||
| .text-white { | ||||
|   --tw-text-opacity: 1; | ||||
|   color: rgb(255 255 255 / var(--tw-text-opacity)); | ||||
| } | ||||
| 
 | ||||
| .underline { | ||||
|   text-decoration-line: underline; | ||||
| } | ||||
|  | @ -1866,12 +1851,6 @@ video { | |||
|           filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); | ||||
| } | ||||
| 
 | ||||
| .drop-shadow { | ||||
|   --tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow(0 1px 1px rgb(0 0 0 / 0.06)); | ||||
|   -webkit-filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); | ||||
|           filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); | ||||
| } | ||||
| 
 | ||||
| .grayscale { | ||||
|   --tw-grayscale: grayscale(100%); | ||||
|   -webkit-filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); | ||||
|  |  | |||
|  | @ -89,6 +89,7 @@ | |||
|     "general": { | ||||
|         "about": "Easily edit and add OpenStreetMap for a certain theme", | ||||
|         "aboutMapcomplete": "<p>Use MapComplete to add OpenStreetMap info on a <b>single theme.</b> Answer questions, and within minutes your contributions are available everywhere. In most themes you can add pictures or even leave a review. The <b>theme maintainer</b> defines elements, questions and languages for it.</p><h3>Find out more</h3><p>MapComplete always <b>offers the next step</b> to learn more about OpenStreetMap.<ul><li>When embedded in a website, the iframe links to a full-screen MapComplete.</li><li>The fullscreen version offers info about OpenStreetMap.</li><li>Viewing works without login, but editing requires an OSM account.</li><li>If you are not logged in, you are asked to do so</li><li>Once you answered a single question, you can add new features to the map</li><li>After a while, actual OSM-tags are shown, later linking to the wiki</li></ul></p><br/><p>Did you notice <b>an issue</b>? Do you have a <b>feature request</b>? Want to <b>help translate</b>? Head over to <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>the source code</a> or <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker.</a> </p><p> Want to see <b>your progress</b>? Follow the edit count on <a href='{osmcha_link}' target='_blank' >OsmCha</a>.</p>", | ||||
|         "aboutMapcompleteTitle": "About MapComplete", | ||||
|         "add": { | ||||
|             "addNew": "Add {category}", | ||||
|             "backToSelect": "Select a different category", | ||||
|  | @ -96,6 +97,7 @@ | |||
|             "confirmIntro": "<h3>Add a {title}?</h3>The feature you create here will be <b>visible for everyone</b>. Please, only add things on to the map if they truly exist. A lot of applications use this data.", | ||||
|             "disableFilters": "Disable all filters", | ||||
|             "disableFiltersExplanation": "Some features might be hidden by a filter", | ||||
|             "enableLayer": "Enable layer {name}", | ||||
|             "hasBeenImported": "This feature has already been imported", | ||||
|             "import": { | ||||
|                 "hasBeenImported": "This object has been imported", | ||||
|  |  | |||
|  | @ -1994,6 +1994,15 @@ | |||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "last_click": { | ||||
|         "mapRendering": { | ||||
|             "0": { | ||||
|                 "label": { | ||||
|                     "render": "Afegir nou element" | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "map": { | ||||
|         "name": "Mapes", | ||||
|         "presets": { | ||||
|  |  | |||
|  | @ -946,6 +946,15 @@ | |||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "last_click": { | ||||
|         "mapRendering": { | ||||
|             "0": { | ||||
|                 "label": { | ||||
|                     "render": "Klikněte zde pro přidání nové položky" | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "usersettings": { | ||||
|         "tagRenderings": { | ||||
|             "picture-license": { | ||||
|  |  | |||
|  | @ -2065,6 +2065,15 @@ | |||
|     "gps_track": { | ||||
|         "name": "Dit tilbagelagte spor" | ||||
|     }, | ||||
|     "last_click": { | ||||
|         "mapRendering": { | ||||
|             "0": { | ||||
|                 "label": { | ||||
|                     "render": "Klik her for at tilføje et nyt punkt" | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "recycling": { | ||||
|         "filter": { | ||||
|             "2": { | ||||
|  |  | |||
|  | @ -5206,6 +5206,15 @@ | |||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "last_click": { | ||||
|         "mapRendering": { | ||||
|             "0": { | ||||
|                 "label": { | ||||
|                     "render": "Hier klicken, um ein neues Element hinzuzufügen" | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "map": { | ||||
|         "description": "Eine Karte, die für Touristen gedacht ist und dauerhaft im öffentlichen Raum aufgestellt ist", | ||||
|         "name": "Karten", | ||||
|  |  | |||
|  | @ -5209,6 +5209,33 @@ | |||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "last_click": { | ||||
|         "mapRendering": { | ||||
|             "0": { | ||||
|                 "label": { | ||||
|                     "mappings": { | ||||
|                         "0": { | ||||
|                             "then": "Create a new map note" | ||||
|                         } | ||||
|                     }, | ||||
|                     "render": "Click here to add a new item" | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         "title": { | ||||
|             "mappings": { | ||||
|                 "0": { | ||||
|                     "then": "Add a new point or add a note" | ||||
|                 }, | ||||
|                 "1": { | ||||
|                     "then": "Add a new note" | ||||
|                 }, | ||||
|                 "2": { | ||||
|                     "then": "Add a new point" | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "map": { | ||||
|         "description": "A map, meant for tourists which is permanently installed in the public space", | ||||
|         "name": "Maps", | ||||
|  |  | |||
|  | @ -2681,6 +2681,15 @@ | |||
|             "render": "Panel informativo" | ||||
|         } | ||||
|     }, | ||||
|     "last_click": { | ||||
|         "mapRendering": { | ||||
|             "0": { | ||||
|                 "label": { | ||||
|                     "render": "Haga clic aquí para añadir un nuevo ítem" | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "map": { | ||||
|         "description": "Un mapa, pensado para turistas y que está instalado de manera permanente en un espacio público", | ||||
|         "name": "Mapas", | ||||
|  |  | |||
|  | @ -1 +1,11 @@ | |||
| {} | ||||
| { | ||||
|     "last_click": { | ||||
|         "mapRendering": { | ||||
|             "0": { | ||||
|                 "label": { | ||||
|                     "render": "I-click ito para mag-dagdag ng bagong bagay" | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -3350,6 +3350,15 @@ | |||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "last_click": { | ||||
|         "mapRendering": { | ||||
|             "0": { | ||||
|                 "label": { | ||||
|                     "render": "Cliquez ici pour ajouter un élément" | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "map": { | ||||
|         "description": "Une carte, destinée aux touristes, installée en permanence dans l'espace public", | ||||
|         "name": "Cartes", | ||||
|  |  | |||
|  | @ -614,6 +614,15 @@ | |||
|             "render": "Hackerspace" | ||||
|         } | ||||
|     }, | ||||
|     "last_click": { | ||||
|         "mapRendering": { | ||||
|             "0": { | ||||
|                 "label": { | ||||
|                     "render": "Új elem hozzáadásához kattints ide" | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "postboxes": { | ||||
|         "description": "Postaládákat megjelenítő réteg.", | ||||
|         "name": "Postaládák", | ||||
|  |  | |||
|  | @ -356,6 +356,15 @@ | |||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "last_click": { | ||||
|         "mapRendering": { | ||||
|             "0": { | ||||
|                 "label": { | ||||
|                     "render": "Klik di sini untuk menambahkan item baru" | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "nature_reserve": { | ||||
|         "tagRenderings": { | ||||
|             "Email": { | ||||
|  |  | |||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue