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 { GeoLocationPointProperties, GeoLocationState } from "../State/GeoLocationState"
|
||||||
import { UIEventSource } from "../UIEventSource"
|
import { UIEventSource } from "../UIEventSource"
|
||||||
import { Feature, LineString, Point } from "geojson"
|
import { Feature, LineString, Point } from "geojson"
|
||||||
import FeatureSource from "../FeatureSource/FeatureSource"
|
import { FeatureSource } from "../FeatureSource/FeatureSource"
|
||||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||||
import { GeoOperations } from "../GeoOperations"
|
import { GeoOperations } from "../GeoOperations"
|
||||||
import { OsmTags } from "../../Models/OsmFeature"
|
import { OsmTags } from "../../Models/OsmFeature"
|
||||||
|
|
|
@ -9,6 +9,7 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||||
import SimpleMetaTagger from "../SimpleMetaTagger"
|
import SimpleMetaTagger from "../SimpleMetaTagger"
|
||||||
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
|
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
|
import { OsmTags } from "../../Models/OsmFeature"
|
||||||
|
|
||||||
export default class SelectedElementTagsUpdater {
|
export default class SelectedElementTagsUpdater {
|
||||||
private static readonly metatags = new Set([
|
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
|
const state = this.state
|
||||||
try {
|
try {
|
||||||
const leftRightSensitive = state.layoutToUse.isLeftRightSensitive()
|
const leftRightSensitive = state.layoutToUse.isLeftRightSensitive()
|
||||||
|
|
|
@ -26,11 +26,15 @@ export default class TitleHandler {
|
||||||
|
|
||||||
const tags = selected.properties
|
const tags = selected.properties
|
||||||
const layer = selectedLayer.data
|
const layer = selectedLayer.data
|
||||||
|
if (layer.title === undefined) {
|
||||||
|
return defaultTitle
|
||||||
|
}
|
||||||
const tagsSource =
|
const tagsSource =
|
||||||
allElements.getStore(tags.id) ?? new UIEventSource<Record<string, string>>(tags)
|
allElements.getStore(tags.id) ?? new UIEventSource<Record<string, string>>(tags)
|
||||||
const title = new SvelteUIElement(TagRenderingAnswer, {
|
const title = new SvelteUIElement(TagRenderingAnswer, {
|
||||||
tags: tagsSource,
|
tags: tagsSource,
|
||||||
state,
|
state,
|
||||||
|
config: layer.title,
|
||||||
selectedElement: selectedElement.data,
|
selectedElement: selectedElement.data,
|
||||||
layer,
|
layer,
|
||||||
})
|
})
|
||||||
|
|
|
@ -138,6 +138,45 @@ export class BBox {
|
||||||
return true
|
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() {
|
getEast() {
|
||||||
return this.maxLon
|
return this.maxLon
|
||||||
}
|
}
|
||||||
|
@ -214,7 +253,7 @@ export class BBox {
|
||||||
* @param zoomlevel
|
* @param zoomlevel
|
||||||
*/
|
*/
|
||||||
expandToTileBounds(zoomlevel: number): BBox {
|
expandToTileBounds(zoomlevel: number): BBox {
|
||||||
if(zoomlevel === undefined){
|
if (zoomlevel === undefined) {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
const ul = Tiles.embedded_tile(this.minLat, this.minLon, zoomlevel)
|
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"
|
import { UIEventSource } from "../../UIEventSource"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import FeatureSource, { FeatureSourceForLayer } from "../FeatureSource"
|
import { FeatureSource , FeatureSourceForLayer } from "../FeatureSource"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
import { BBox } from "../../BBox"
|
import { BBox } from "../../BBox"
|
||||||
import { GeoOperations } from "../../GeoOperations"
|
import { GeoOperations } from "../../GeoOperations"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import FeatureSource from "../FeatureSource"
|
import { FeatureSource } from "../FeatureSource"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
import TileLocalStorage from "./TileLocalStorage"
|
import TileLocalStorage from "./TileLocalStorage"
|
||||||
import { GeoOperations } from "../../GeoOperations"
|
import { GeoOperations } from "../../GeoOperations"
|
||||||
|
|
|
@ -3,7 +3,7 @@ import FilteredLayer from "../../Models/FilteredLayer"
|
||||||
import { BBox } from "../BBox"
|
import { BBox } from "../BBox"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
|
|
||||||
export default interface FeatureSource {
|
export interface FeatureSource {
|
||||||
features: Store<Feature[]>
|
features: Store<Feature[]>
|
||||||
}
|
}
|
||||||
export interface WritableFeatureSource extends FeatureSource {
|
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 FilteredLayer from "../../Models/FilteredLayer"
|
||||||
import SimpleFeatureSource from "./Sources/SimpleFeatureSource"
|
import SimpleFeatureSource from "./Sources/SimpleFeatureSource"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
import { UIEventSource } from "../UIEventSource"
|
import { UIEventSource } from "../UIEventSource"
|
||||||
import { feature } from "@turf/turf"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled)
|
* 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>()
|
const knownLayers = new Map<string, T>()
|
||||||
this.perLayer = knownLayers
|
this.perLayer = knownLayers
|
||||||
const layerSources = new Map<string, UIEventSource<Feature[]>>()
|
const layerSources = new Map<string, UIEventSource<Feature[]>>()
|
||||||
|
console.log("PerLayerFeatureSourceSplitter got layers", layers)
|
||||||
const constructStore =
|
const constructStore =
|
||||||
options?.constructStore ?? ((store, layer) => new SimpleFeatureSource(layer, store))
|
options?.constructStore ?? ((store, layer) => new SimpleFeatureSource(layer, store))
|
||||||
for (const layer of layers) {
|
for (const layer of layers) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import FeatureSource from "../FeatureSource"
|
import { FeatureSource } from "../FeatureSource"
|
||||||
import { Feature, Polygon } from "geojson"
|
import { Feature, Polygon } from "geojson"
|
||||||
import StaticFeatureSource from "./StaticFeatureSource"
|
import StaticFeatureSource from "./StaticFeatureSource"
|
||||||
import { GeoOperations } from "../../GeoOperations"
|
import { GeoOperations } from "../../GeoOperations"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Store, UIEventSource } from "../../UIEventSource"
|
import { Store, UIEventSource } from "../../UIEventSource"
|
||||||
import FeatureSource, { IndexedFeatureSource } from "../FeatureSource"
|
import { FeatureSource , IndexedFeatureSource } from "../FeatureSource"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
import { Utils } from "../../../Utils"
|
import { Utils } from "../../../Utils"
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Store, UIEventSource } from "../../UIEventSource"
|
import { Store, UIEventSource } from "../../UIEventSource"
|
||||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||||
import FeatureSource from "../FeatureSource"
|
import { FeatureSource } from "../FeatureSource"
|
||||||
import { TagsFilter } from "../../Tags/TagsFilter"
|
import { TagsFilter } from "../../Tags/TagsFilter"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
import { GlobalFilter } from "../../../Models/GlobalFilter"
|
import { GlobalFilter } from "../../../Models/GlobalFilter"
|
||||||
|
@ -73,21 +73,9 @@ export default class FilteringFeatureSource implements FeatureSource {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const filter of layer.layerDef.filters) {
|
let neededTags: TagsFilter = layer.currentFilter.data
|
||||||
const state = layer.appliedFilters.get(filter.id).data
|
if (neededTags !== undefined && !neededTags.matchesProperties(f.properties)) {
|
||||||
if (state === undefined) {
|
return false
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const globalFilter of globalFilters ?? []) {
|
for (const globalFilter of globalFilters ?? []) {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
*/
|
*/
|
||||||
import { Store, UIEventSource } from "../../UIEventSource"
|
import { Store, UIEventSource } from "../../UIEventSource"
|
||||||
import { Utils } from "../../../Utils"
|
import { Utils } from "../../../Utils"
|
||||||
import FeatureSource from "../FeatureSource"
|
import { FeatureSource } from "../FeatureSource"
|
||||||
import { BBox } from "../../BBox"
|
import { BBox } from "../../BBox"
|
||||||
import { GeoOperations } from "../../GeoOperations"
|
import { GeoOperations } from "../../GeoOperations"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
|
|
|
@ -1,21 +1,23 @@
|
||||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"
|
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"
|
||||||
import FeatureSource from "../FeatureSource"
|
import { WritableFeatureSource } from "../FeatureSource"
|
||||||
import { ImmutableStore, Store } from "../../UIEventSource"
|
import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource"
|
||||||
import { Feature, Point } from "geojson"
|
import { Feature, Point } from "geojson"
|
||||||
import { TagUtils } from "../../Tags/TagUtils"
|
import { TagUtils } from "../../Tags/TagUtils"
|
||||||
import BaseUIElement from "../../../UI/BaseUIElement"
|
import BaseUIElement from "../../../UI/BaseUIElement"
|
||||||
import { Utils } from "../../../Utils"
|
import { Utils } from "../../../Utils"
|
||||||
import { regex_not_newline_characters } from "svelte/types/compiler/utils/patterns"
|
|
||||||
import { render } from "sass"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Highly specialized feature source.
|
* Highly specialized feature source.
|
||||||
* Based on a lon/lat UIEVentSource, will generate the corresponding feature with the correct properties
|
* Based on a lon/lat UIEVentSource, will generate the corresponding feature with the correct properties
|
||||||
*/
|
*/
|
||||||
export class LastClickFeatureSource implements FeatureSource {
|
export class LastClickFeatureSource implements WritableFeatureSource {
|
||||||
features: Store<Feature[]>
|
public readonly features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Must be public: passed as tags into the selected view
|
||||||
|
*/
|
||||||
public properties: Record<string, string>
|
public properties: Record<string, string>
|
||||||
|
|
||||||
constructor(location: Store<{ lon: number; lat: number }>, layout: LayoutConfig) {
|
constructor(location: Store<{ lon: number; lat: number }>, layout: LayoutConfig) {
|
||||||
const allPresets: BaseUIElement[] = []
|
const allPresets: BaseUIElement[] = []
|
||||||
for (const layer of layout.layers)
|
for (const layer of layout.layers)
|
||||||
|
@ -43,15 +45,16 @@ export class LastClickFeatureSource implements FeatureSource {
|
||||||
first_preset: renderings[0],
|
first_preset: renderings[0],
|
||||||
}
|
}
|
||||||
this.properties = properties
|
this.properties = properties
|
||||||
this.features = location.mapD(({ lon, lat }) => [
|
location.addCallbackAndRunD(({ lon, lat }) => {
|
||||||
<Feature<Point>>{
|
const point = <Feature<Point>>{
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
properties,
|
properties,
|
||||||
geometry: {
|
geometry: {
|
||||||
type: "Point",
|
type: "Point",
|
||||||
coordinates: [lon, lat],
|
coordinates: [lon, lat],
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
])
|
this.features.setData([point])
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import GeoJsonSource from "./GeoJsonSource"
|
import GeoJsonSource from "./GeoJsonSource"
|
||||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||||
import FeatureSource from "../FeatureSource"
|
import { FeatureSource } from "../FeatureSource"
|
||||||
import { Or } from "../../Tags/Or"
|
import { Or } from "../../Tags/Or"
|
||||||
import FeatureSwitchState from "../../State/FeatureSwitchState"
|
import FeatureSwitchState from "../../State/FeatureSwitchState"
|
||||||
import OverpassFeatureSource from "./OverpassFeatureSource"
|
import OverpassFeatureSource from "./OverpassFeatureSource"
|
||||||
import { Store } from "../../UIEventSource"
|
import { ImmutableStore, Store } from "../../UIEventSource"
|
||||||
import OsmFeatureSource from "./OsmFeatureSource"
|
import OsmFeatureSource from "./OsmFeatureSource"
|
||||||
import FeatureSourceMerger from "./FeatureSourceMerger"
|
import FeatureSourceMerger from "./FeatureSourceMerger"
|
||||||
import DynamicGeoJsonTileSource from "../TiledFeatureSource/DynamicGeoJsonTileSource"
|
import DynamicGeoJsonTileSource from "../TiledFeatureSource/DynamicGeoJsonTileSource"
|
||||||
import { BBox } from "../../BBox"
|
import { BBox } from "../../BBox"
|
||||||
import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeatureSource"
|
import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeatureSource"
|
||||||
|
import StaticFeatureSource from "./StaticFeatureSource"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This source will fetch the needed data from various sources for the given layout.
|
* 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,
|
backend: string,
|
||||||
featureSwitches: FeatureSwitchState
|
featureSwitches: FeatureSwitchState
|
||||||
): FeatureSource {
|
): FeatureSource {
|
||||||
|
if (osmLayers.length == 0) {
|
||||||
|
return new StaticFeatureSource(new ImmutableStore([]))
|
||||||
|
}
|
||||||
const minzoom = Math.min(...osmLayers.map((layer) => layer.minzoom))
|
const minzoom = Math.min(...osmLayers.map((layer) => layer.minzoom))
|
||||||
const isActive = zoom.mapD((z) => {
|
const isActive = zoom.mapD((z) => {
|
||||||
if (z < minzoom) {
|
if (z < minzoom) {
|
||||||
|
@ -107,6 +111,9 @@ export default class LayoutSource extends FeatureSourceMerger {
|
||||||
zoom: Store<number>,
|
zoom: Store<number>,
|
||||||
featureSwitches: FeatureSwitchState
|
featureSwitches: FeatureSwitchState
|
||||||
): FeatureSource {
|
): FeatureSource {
|
||||||
|
if (osmLayers.length == 0) {
|
||||||
|
return new StaticFeatureSource(new ImmutableStore([]))
|
||||||
|
}
|
||||||
const minzoom = Math.min(...osmLayers.map((layer) => layer.minzoom))
|
const minzoom = Math.min(...osmLayers.map((layer) => layer.minzoom))
|
||||||
const isActive = zoom.mapD((z) => {
|
const isActive = zoom.mapD((z) => {
|
||||||
if (z < minzoom) {
|
if (z < minzoom) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Changes } from "../../Osm/Changes"
|
import { Changes } from "../../Osm/Changes"
|
||||||
import { OsmNode, OsmObject, OsmRelation, OsmWay } from "../../Osm/OsmObject"
|
import { OsmNode, OsmObject, OsmRelation, OsmWay } from "../../Osm/OsmObject"
|
||||||
import FeatureSource from "../FeatureSource"
|
import { FeatureSource } from "../FeatureSource"
|
||||||
import { UIEventSource } from "../../UIEventSource"
|
import { UIEventSource } from "../../UIEventSource"
|
||||||
import { ChangeDescription } from "../../Osm/Actions/ChangeDescription"
|
import { ChangeDescription } from "../../Osm/Actions/ChangeDescription"
|
||||||
import { ElementStorage } from "../../ElementStorage"
|
import { ElementStorage } from "../../ElementStorage"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
import FeatureSource from "../FeatureSource"
|
import { FeatureSource } from "../FeatureSource"
|
||||||
import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource"
|
import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource"
|
||||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||||
import { Or } from "../../Tags/Or"
|
import { Or } from "../../Tags/Or"
|
||||||
|
|
|
@ -1,37 +1,69 @@
|
||||||
import FeatureSource from "../FeatureSource"
|
import { FeatureSource } from "../FeatureSource"
|
||||||
import { Store } from "../../UIEventSource"
|
import { Store, UIEventSource } from "../../UIEventSource"
|
||||||
import { Feature, Point } from "geojson"
|
import { Feature, Point } from "geojson"
|
||||||
import { GeoOperations } from "../../GeoOperations"
|
import { GeoOperations } from "../../GeoOperations"
|
||||||
|
import { BBox } from "../../BBox"
|
||||||
|
|
||||||
export interface SnappingOptions {
|
export interface SnappingOptions {
|
||||||
/**
|
/**
|
||||||
* If the distance is bigger then this amount, don't snap.
|
* If the distance is bigger then this amount, don't snap.
|
||||||
* In meter
|
* 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 {
|
export default class SnappingFeatureSource implements FeatureSource {
|
||||||
public readonly features: Store<Feature<Point>[]>
|
public readonly features: Store<Feature<Point>[]>
|
||||||
|
|
||||||
|
private readonly _snappedTo: UIEventSource<string>
|
||||||
|
public readonly snappedTo: Store<string>
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
snapTo: FeatureSource,
|
snapTo: FeatureSource,
|
||||||
location: Store<{ lon: number; lat: number }>,
|
location: Store<{ lon: number; lat: number }>,
|
||||||
options?: SnappingOptions
|
options: SnappingOptions
|
||||||
) {
|
) {
|
||||||
const simplifiedFeatures = snapTo.features.mapD((features) =>
|
const maxDistance = options?.maxDistance
|
||||||
features
|
this._snappedTo = options.snappedTo ?? new UIEventSource<string>(undefined)
|
||||||
.filter((feature) => feature.geometry.type !== "Point")
|
this.snappedTo = this._snappedTo
|
||||||
.map((f) => GeoOperations.forceLineString(<any>f))
|
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 }) => {
|
({ lon, lat }) => {
|
||||||
const features = snapTo.features.data
|
const features = simplifiedFeatures.data
|
||||||
const loc: [number, number] = [lon, lat]
|
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
|
let bestSnap: Feature<Point, { "snapped-to": string; dist: number }> = undefined
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
|
if (feature.geometry.type !== "LineString") {
|
||||||
|
// TODO handle Polygons with holes
|
||||||
|
continue
|
||||||
|
}
|
||||||
const snapped = GeoOperations.nearestPoint(<any>feature, loc)
|
const snapped = GeoOperations.nearestPoint(<any>feature, loc)
|
||||||
if (snapped.properties.dist > maxDistance) {
|
if (snapped.properties.dist > maxDistance) {
|
||||||
continue
|
continue
|
||||||
|
@ -44,7 +76,23 @@ export default class SnappingFeatureSource implements FeatureSource {
|
||||||
bestSnap = <any>snapped
|
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]
|
[snapTo.features]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import FeatureSource, { FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
import { FeatureSource , FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
||||||
import { ImmutableStore, Store } from "../../UIEventSource"
|
import { ImmutableStore, Store } from "../../UIEventSource"
|
||||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||||
import { BBox } from "../../BBox"
|
import { BBox } from "../../BBox"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import FeatureSource, { FeatureSourceForLayer } from "../FeatureSource"
|
import {FeatureSource, FeatureSourceForLayer } from "../FeatureSource"
|
||||||
import StaticFeatureSource from "./StaticFeatureSource"
|
import StaticFeatureSource from "./StaticFeatureSource"
|
||||||
import { GeoOperations } from "../../GeoOperations"
|
import { GeoOperations } from "../../GeoOperations"
|
||||||
import { BBox } from "../../BBox"
|
import { BBox } from "../../BBox"
|
||||||
|
|
|
@ -81,6 +81,7 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
|
||||||
return new GeoJsonSource(layer, {
|
return new GeoJsonSource(layer, {
|
||||||
zxy,
|
zxy,
|
||||||
featureIdBlacklist: blackList,
|
featureIdBlacklist: blackList,
|
||||||
|
isActive: options?.isActive,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
mapProperties,
|
mapProperties,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Store, Stores } from "../../UIEventSource"
|
import { Store, Stores } from "../../UIEventSource"
|
||||||
import { Tiles } from "../../../Models/TileRange"
|
import { Tiles } from "../../../Models/TileRange"
|
||||||
import { BBox } from "../../BBox"
|
import { BBox } from "../../BBox"
|
||||||
import FeatureSource from "../FeatureSource"
|
import { FeatureSource } from "../FeatureSource"
|
||||||
import FeatureSourceMerger from "../Sources/FeatureSourceMerger"
|
import FeatureSourceMerger from "../Sources/FeatureSourceMerger"
|
||||||
|
|
||||||
/***
|
/***
|
||||||
|
@ -26,10 +26,6 @@ export default class DynamicTileSource extends FeatureSourceMerger {
|
||||||
mapProperties.bounds
|
mapProperties.bounds
|
||||||
.mapD(
|
.mapD(
|
||||||
(bounds) => {
|
(bounds) => {
|
||||||
if (options?.isActive?.data === false) {
|
|
||||||
// No need to download! - the layer is disabled
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
const tileRange = Tiles.TileRangeBetween(
|
const tileRange = Tiles.TileRangeBetween(
|
||||||
zoomlevel,
|
zoomlevel,
|
||||||
bounds.getNorth(),
|
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 { OsmNode, OsmObject, OsmWay } from "../../Osm/OsmObject"
|
||||||
import SimpleFeatureSource from "../Sources/SimpleFeatureSource"
|
import SimpleFeatureSource from "../Sources/SimpleFeatureSource"
|
||||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { BBox } from "./BBox"
|
import { BBox } from "./BBox"
|
||||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||||
import * as turf from "@turf/turf"
|
import * as turf from "@turf/turf"
|
||||||
import { AllGeoJSON, booleanWithin, Coord } from "@turf/turf"
|
import { AllGeoJSON, booleanWithin, Coord, Lines } from "@turf/turf"
|
||||||
import {
|
import {
|
||||||
Feature,
|
Feature,
|
||||||
GeoJSON,
|
GeoJSON,
|
||||||
|
@ -273,7 +273,7 @@ export class GeoOperations {
|
||||||
* @param point Point defined as [lon, lat]
|
* @param point Point defined as [lon, lat]
|
||||||
*/
|
*/
|
||||||
public static nearestPoint(
|
public static nearestPoint(
|
||||||
way: Feature<LineString | MultiLineString | Polygon | MultiPolygon>,
|
way: Feature<LineString>,
|
||||||
point: [number, number]
|
point: [number, number]
|
||||||
): Feature<
|
): Feature<
|
||||||
Point,
|
Point,
|
||||||
|
@ -951,4 +951,24 @@ export class GeoOperations {
|
||||||
}
|
}
|
||||||
throw "CalculateIntersection fallthrough: can not calculate an intersection between features"
|
throw "CalculateIntersection fallthrough: can not calculate an intersection between features"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a linestring object based on the outer ring of the given polygon
|
||||||
|
*
|
||||||
|
* Returns the argument if not a polygon
|
||||||
|
* @param p
|
||||||
|
*/
|
||||||
|
public static outerRing<P>(p: Feature<Polygon | LineString, P>): Feature<LineString, P> {
|
||||||
|
if (p.geometry.type !== "Polygon") {
|
||||||
|
return <Feature<LineString, P>>p
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: "Feature",
|
||||||
|
properties: p.properties,
|
||||||
|
geometry: {
|
||||||
|
type: "LineString",
|
||||||
|
coordinates: p.geometry.coordinates[0],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import CreateWayWithPointReuseAction, { MergePointConfig } from "./CreateWayWith
|
||||||
import { And } from "../../Tags/And"
|
import { And } from "../../Tags/And"
|
||||||
import { TagUtils } from "../../Tags/TagUtils"
|
import { TagUtils } from "../../Tags/TagUtils"
|
||||||
import { SpecialVisualizationState } from "../../../UI/SpecialVisualization"
|
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
|
* 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
|
// Project the point onto the way
|
||||||
console.log("Snapping a node onto an existing way...")
|
console.log("Snapping a node onto an existing way...")
|
||||||
const geojson = this._snapOnto.asGeoJson()
|
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 projectedCoor = <[number, number]>projected.geometry.coordinates
|
||||||
const index = projected.properties.index
|
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
|
// We check that it isn't close to an already existing point
|
||||||
let reusedPointId = undefined
|
let reusedPointId = undefined
|
||||||
let outerring: [number, number][]
|
let outerring: [number, number][]
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { ChangeDescription } from "./ChangeDescription"
|
||||||
import { BBox } from "../../BBox"
|
import { BBox } from "../../BBox"
|
||||||
import { TagsFilter } from "../../Tags/TagsFilter"
|
import { TagsFilter } from "../../Tags/TagsFilter"
|
||||||
import { GeoOperations } from "../../GeoOperations"
|
import { GeoOperations } from "../../GeoOperations"
|
||||||
import FeatureSource from "../../FeatureSource/FeatureSource"
|
import { FeatureSource } from "../../FeatureSource/FeatureSource"
|
||||||
import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource"
|
import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource"
|
||||||
import CreateNewNodeAction from "./CreateNewNodeAction"
|
import CreateNewNodeAction from "./CreateNewNodeAction"
|
||||||
import CreateNewWayAction from "./CreateNewWayAction"
|
import CreateNewWayAction from "./CreateNewWayAction"
|
||||||
|
|
|
@ -2,7 +2,7 @@ import OsmChangeAction from "./OsmChangeAction"
|
||||||
import { Changes } from "../Changes"
|
import { Changes } from "../Changes"
|
||||||
import { ChangeDescription } from "./ChangeDescription"
|
import { ChangeDescription } from "./ChangeDescription"
|
||||||
import { Tag } from "../../Tags/Tag"
|
import { Tag } from "../../Tags/Tag"
|
||||||
import FeatureSource from "../../FeatureSource/FeatureSource"
|
import { FeatureSource } from "../../FeatureSource/FeatureSource"
|
||||||
import { OsmNode, OsmObject, OsmWay } from "../OsmObject"
|
import { OsmNode, OsmObject, OsmWay } from "../OsmObject"
|
||||||
import { GeoOperations } from "../../GeoOperations"
|
import { GeoOperations } from "../../GeoOperations"
|
||||||
import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource"
|
import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource"
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { ChangeDescription, ChangeDescriptionTools } from "./Actions/ChangeDescr
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||||
import SimpleMetaTagger from "../SimpleMetaTagger"
|
import SimpleMetaTagger from "../SimpleMetaTagger"
|
||||||
import FeatureSource, { IndexedFeatureSource } from "../FeatureSource/FeatureSource"
|
import {FeatureSource, IndexedFeatureSource } from "../FeatureSource/FeatureSource"
|
||||||
import { GeoLocationPointProperties } from "../State/GeoLocationState"
|
import { GeoLocationPointProperties } from "../State/GeoLocationState"
|
||||||
import { GeoOperations } from "../GeoOperations"
|
import { GeoOperations } from "../GeoOperations"
|
||||||
import { ChangesetHandler, ChangesetTag } from "./ChangesetHandler"
|
import { ChangesetHandler, ChangesetTag } from "./ChangesetHandler"
|
||||||
|
|
|
@ -368,7 +368,7 @@ export class OsmConnection {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
})
|
})
|
||||||
const parsed = JSON.parse(response)
|
const parsed = JSON.parse(response)
|
||||||
const id = parsed.properties.id
|
const id = parsed.properties
|
||||||
console.log("OPENED NOTE", id)
|
console.log("OPENED NOTE", id)
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,8 @@ export abstract class OsmObject {
|
||||||
if (rawData["error"] !== undefined && rawData["statuscode"] === 410) {
|
if (rawData["error"] !== undefined && rawData["statuscode"] === 410) {
|
||||||
return "deleted"
|
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(
|
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
|
* 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
|
private readonly osmConnection: OsmConnection
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -32,14 +32,15 @@ export default class LayerState {
|
||||||
*/
|
*/
|
||||||
constructor(osmConnection: OsmConnection, layers: LayerConfig[], context: string) {
|
constructor(osmConnection: OsmConnection, layers: LayerConfig[], context: string) {
|
||||||
this.osmConnection = osmConnection
|
this.osmConnection = osmConnection
|
||||||
this.filteredLayers = new Map()
|
const filteredLayers = new Map()
|
||||||
for (const layer of layers) {
|
for (const layer of layers) {
|
||||||
this.filteredLayers.set(
|
filteredLayers.set(
|
||||||
layer.id,
|
layer.id,
|
||||||
FilteredLayer.initLinkedState(layer, context, this.osmConnection)
|
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
|
* 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) {
|
if (layer.filterIsSameAs === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const toReuse = this.filteredLayers.get(layer.filterIsSameAs)
|
const toReuse = filteredLayers.get(layer.filterIsSameAs)
|
||||||
if (toReuse === undefined) {
|
if (toReuse === undefined) {
|
||||||
throw (
|
throw (
|
||||||
"Error in layer " +
|
"Error in layer " +
|
||||||
|
@ -65,6 +69,6 @@ export default class LayerState {
|
||||||
console.warn(
|
console.warn(
|
||||||
"Linking filter and isDisplayed-states of " + layer.id + " and " + layer.filterIsSameAs
|
"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 TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"
|
||||||
import { QueryParameters } from "../Web/QueryParameters"
|
import { QueryParameters } from "../Web/QueryParameters"
|
||||||
import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer"
|
import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer"
|
||||||
import FeatureSource, { FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource"
|
import { FeatureSource, FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource"
|
||||||
import StaticFeatureSource, {
|
import StaticFeatureSource, {
|
||||||
TiledStaticFeatureSource,
|
TiledStaticFeatureSource,
|
||||||
} from "../FeatureSource/Sources/StaticFeatureSource"
|
} from "../FeatureSource/Sources/StaticFeatureSource"
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { MangroveIdentity } from "../Web/MangroveReviews"
|
||||||
import { Store, Stores, UIEventSource } from "../UIEventSource"
|
import { Store, Stores, UIEventSource } from "../UIEventSource"
|
||||||
import Locale from "../../UI/i18n/Locale"
|
import Locale from "../../UI/i18n/Locale"
|
||||||
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
|
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
|
||||||
import FeatureSource from "../FeatureSource/FeatureSource"
|
import { FeatureSource } from "../FeatureSource/FeatureSource"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { TagsFilter } from "./TagsFilter"
|
||||||
export class Tag extends TagsFilter {
|
export class Tag extends TagsFilter {
|
||||||
public key: string
|
public key: string
|
||||||
public value: string
|
public value: string
|
||||||
public static newlyCreated = new Tag("_newly_created", "yes")
|
|
||||||
constructor(key: string, value: string) {
|
constructor(key: string, value: string) {
|
||||||
super()
|
super()
|
||||||
this.key = key
|
this.key = key
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Utils } from "../Utils"
|
import { Utils } from "../Utils"
|
||||||
|
|
||||||
export default class Constants {
|
export default class Constants {
|
||||||
public static vNumber = "0.27.0"
|
public static vNumber = "0.30.0"
|
||||||
|
|
||||||
public static ImgurApiKey = "7070e7167f0a25a"
|
public static ImgurApiKey = "7070e7167f0a25a"
|
||||||
public static readonly mapillary_client_token_v4 =
|
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 LayerConfig from "./ThemeConfig/LayerConfig"
|
||||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||||
import { LocalStorageSource } from "../Logic/Web/LocalStorageSource"
|
import { LocalStorageSource } from "../Logic/Web/LocalStorageSource"
|
||||||
import { QueryParameters } from "../Logic/Web/QueryParameters"
|
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 {
|
export default class FilteredLayer {
|
||||||
/**
|
/**
|
||||||
|
@ -10,11 +15,22 @@ export default class FilteredLayer {
|
||||||
*/
|
*/
|
||||||
readonly isDisplayed: UIEventSource<boolean>
|
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
|
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(
|
constructor(
|
||||||
layer: LayerConfig,
|
layer: LayerConfig,
|
||||||
appliedFilters?: Map<string, UIEventSource<undefined | number | string>>,
|
appliedFilters?: Map<string, UIEventSource<undefined | number | string>>,
|
||||||
|
@ -24,6 +40,105 @@ export default class FilteredLayer {
|
||||||
this.isDisplayed = isDisplayed ?? new UIEventSource(true)
|
this.isDisplayed = isDisplayed ?? new UIEventSource(true)
|
||||||
this.appliedFilters =
|
this.appliedFilters =
|
||||||
appliedFilters ?? new Map<string, UIEventSource<number | string | undefined>>()
|
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 {
|
export interface MapProperties {
|
||||||
readonly location: UIEventSource<{ lon: number; lat: number }>
|
readonly location: UIEventSource<{ lon: number; lat: number }>
|
||||||
readonly zoom: UIEventSource<number>
|
readonly zoom: UIEventSource<number>
|
||||||
|
readonly minzoom: UIEventSource<number>
|
||||||
readonly bounds: UIEventSource<BBox>
|
readonly bounds: UIEventSource<BBox>
|
||||||
readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined>
|
readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined>
|
||||||
readonly maxbounds: UIEventSource<undefined | BBox>
|
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 BaseUIElement from "../../UI/BaseUIElement"
|
||||||
import Table from "../../UI/Base/Table"
|
import Table from "../../UI/Base/Table"
|
||||||
import Combine from "../../UI/Base/Combine"
|
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 {
|
export default class FilterConfig {
|
||||||
public readonly id: string
|
public readonly id: string
|
||||||
public readonly options: {
|
public readonly options: FilterConfigOption[]
|
||||||
question: Translation
|
|
||||||
osmTags: TagsFilter | undefined
|
|
||||||
originalTagsSpec: TagConfigJson
|
|
||||||
fields: { name: string; type: string }[]
|
|
||||||
}[]
|
|
||||||
public readonly defaultSelection?: number
|
public readonly defaultSelection?: number
|
||||||
|
|
||||||
constructor(json: FilterConfigJson, context: string) {
|
constructor(json: FilterConfigJson, context: string) {
|
||||||
|
|
|
@ -2,12 +2,12 @@ import LayoutConfig from "./ThemeConfig/LayoutConfig"
|
||||||
import { SpecialVisualizationState } from "../UI/SpecialVisualization"
|
import { SpecialVisualizationState } from "../UI/SpecialVisualization"
|
||||||
import { Changes } from "../Logic/Osm/Changes"
|
import { Changes } from "../Logic/Osm/Changes"
|
||||||
import { ImmutableStore, Store, UIEventSource } from "../Logic/UIEventSource"
|
import { ImmutableStore, Store, UIEventSource } from "../Logic/UIEventSource"
|
||||||
import FeatureSource, {
|
import {
|
||||||
|
FeatureSource,
|
||||||
IndexedFeatureSource,
|
IndexedFeatureSource,
|
||||||
WritableFeatureSource,
|
WritableFeatureSource,
|
||||||
} from "../Logic/FeatureSource/FeatureSource"
|
} from "../Logic/FeatureSource/FeatureSource"
|
||||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||||
import { DefaultGuiState } from "../UI/DefaultGuiState"
|
|
||||||
import { MapProperties } from "./MapProperties"
|
import { MapProperties } from "./MapProperties"
|
||||||
import LayerState from "../Logic/State/LayerState"
|
import LayerState from "../Logic/State/LayerState"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
|
@ -39,6 +39,8 @@ import Hotkeys from "../UI/Base/Hotkeys"
|
||||||
import Translations from "../UI/i18n/Translations"
|
import Translations from "../UI/i18n/Translations"
|
||||||
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
|
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
|
||||||
import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource"
|
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 mapProperties: MapProperties
|
||||||
|
|
||||||
readonly dataIsLoading: Store<boolean> // TODO
|
readonly dataIsLoading: Store<boolean> // TODO
|
||||||
readonly guistate: DefaultGuiState
|
readonly guistate: MenuState
|
||||||
readonly fullNodeDatabase?: FullNodeDatabaseSource // TODO
|
readonly fullNodeDatabase?: FullNodeDatabaseSource // TODO
|
||||||
|
|
||||||
readonly historicalUserLocations: WritableFeatureSource
|
readonly historicalUserLocations: WritableFeatureSource
|
||||||
readonly indexedFeatures: IndexedFeatureSource
|
readonly indexedFeatures: IndexedFeatureSource
|
||||||
|
readonly newFeatures: WritableFeatureSource
|
||||||
readonly layerState: LayerState
|
readonly layerState: LayerState
|
||||||
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
|
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
|
||||||
readonly availableLayers: Store<RasterLayerPolygon[]>
|
readonly availableLayers: Store<RasterLayerPolygon[]>
|
||||||
|
@ -75,9 +78,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
readonly userRelatedState: UserRelatedState
|
readonly userRelatedState: UserRelatedState
|
||||||
readonly geolocation: GeoLocationHandler
|
readonly geolocation: GeoLocationHandler
|
||||||
|
|
||||||
|
readonly lastClickObject: WritableFeatureSource
|
||||||
constructor(layout: LayoutConfig) {
|
constructor(layout: LayoutConfig) {
|
||||||
this.layout = layout
|
this.layout = layout
|
||||||
this.guistate = new DefaultGuiState()
|
this.guistate = new MenuState()
|
||||||
this.map = new UIEventSource<MlMap>(undefined)
|
this.map = new UIEventSource<MlMap>(undefined)
|
||||||
const initial = new InitialMapPositioning(layout)
|
const initial = new InitialMapPositioning(layout)
|
||||||
this.mapProperties = new MapLibreAdaptor(this.map, initial)
|
this.mapProperties = new MapLibreAdaptor(this.map, initial)
|
||||||
|
@ -109,20 +113,26 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
|
|
||||||
this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location)
|
this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location)
|
||||||
|
|
||||||
|
const self = this
|
||||||
this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id)
|
this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id)
|
||||||
|
this.newFeatures = new SimpleFeatureSource(undefined)
|
||||||
this.indexedFeatures = new LayoutSource(
|
this.indexedFeatures = new LayoutSource(
|
||||||
layout.layers,
|
layout.layers,
|
||||||
this.featureSwitches,
|
this.featureSwitches,
|
||||||
new StaticFeatureSource([]),
|
this.newFeatures,
|
||||||
this.mapProperties,
|
this.mapProperties,
|
||||||
this.osmConnection.Backend(),
|
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
|
const indexedElements = this.indexedFeatures
|
||||||
this.featureProperties = new FeaturePropertiesStore(indexedElements)
|
this.featureProperties = new FeaturePropertiesStore(indexedElements)
|
||||||
const perLayer = new PerLayerFeatureSourceSplitter(
|
const perLayer = new PerLayerFeatureSourceSplitter(
|
||||||
Array.from(this.layerState.filteredLayers.values()).filter(
|
Array.from(this.layerState.filteredLayers.values()).filter(
|
||||||
(l) => l.layerDef.source !== null
|
(l) => l.layerDef?.source !== null
|
||||||
),
|
),
|
||||||
indexedElements,
|
indexedElements,
|
||||||
{
|
{
|
||||||
|
@ -176,9 +186,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
)
|
)
|
||||||
|
|
||||||
this.initActors()
|
this.initActors()
|
||||||
this.drawSpecialLayers()
|
this.drawSpecialLayers(lastClick)
|
||||||
this.initHotkeys()
|
this.initHotkeys()
|
||||||
this.miscSetup()
|
this.miscSetup()
|
||||||
|
console.log("State setup completed", this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -197,21 +208,30 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
this.guistate.closeAll()
|
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
|
* Add the special layers to the map
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private drawSpecialLayers() {
|
private drawSpecialLayers(last_click: LastClickFeatureSource) {
|
||||||
type AddedByDefaultTypes = typeof Constants.added_by_default[number]
|
type AddedByDefaultTypes = typeof Constants.added_by_default[number]
|
||||||
const empty = []
|
const empty = []
|
||||||
{
|
{
|
||||||
// The last_click gets a _very_ special treatment
|
// 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")
|
const last_click_layer = this.layerState.filteredLayers.get("last_click")
|
||||||
this.featureProperties.addSpecial(
|
this.featureProperties.addSpecial(
|
||||||
"last_click",
|
"last_click",
|
||||||
|
|
|
@ -84,7 +84,7 @@ export class Tiles {
|
||||||
* Return x, y of the tile containing (lat, lon) on the given zoom level
|
* 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 } {
|
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) {
|
static tileRangeFrom(bbox: BBox, zoomlevel: number) {
|
||||||
|
|
|
@ -11,18 +11,17 @@
|
||||||
let mainElem: HTMLElement;
|
let mainElem: HTMLElement;
|
||||||
export let hideSignal: Store<any>;
|
export let hideSignal: Store<any>;
|
||||||
function hide(){
|
function hide(){
|
||||||
console.trace("Hiding...")
|
|
||||||
mainElem.style.visibility = "hidden";
|
mainElem.style.visibility = "hidden";
|
||||||
}
|
}
|
||||||
if (hideSignal) {
|
if (hideSignal) {
|
||||||
onDestroy(hideSignal.addCallbackD(() => {
|
onDestroy(hideSignal.addCallbackD(() => {
|
||||||
console.trace("Hiding invitation")
|
console.log("Received hide signal")
|
||||||
|
hide()
|
||||||
return true;
|
return true;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
console.log("Binding listeners on", mainElem)
|
|
||||||
mainElem?.addEventListener("click",_ => hide())
|
mainElem?.addEventListener("click",_ => hide())
|
||||||
mainElem?.addEventListener("touchstart",_ => hide())
|
mainElem?.addEventListener("touchstart",_ => hide())
|
||||||
}
|
}
|
||||||
|
@ -30,8 +29,8 @@ $: {
|
||||||
|
|
||||||
|
|
||||||
<div bind:this={mainElem} class="absolute bottom-0 right-0 w-full h-full">
|
<div bind:this={mainElem} class="absolute bottom-0 right-0 w-full h-full">
|
||||||
<div id="hand-container">
|
<div id="hand-container" class="pointer-events-none">
|
||||||
<ToSvelte construct={Svg.hand_ui}></ToSvelte>
|
<img src="./assets/svg/hand.svg"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,20 @@
|
||||||
<script lang="ts">
|
<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
|
* The slotted element will be shown on top, with a lower-opacity border
|
||||||
*/
|
*/
|
||||||
|
const dispatch = createEventDispatcher<{ close }>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="absolute top-0 right-0 w-screen h-screen overflow-auto" style="background-color: #00000088">
|
<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">
|
<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>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</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>
|
<slot class="m-4"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { createEventDispatcher, onMount } from "svelte";
|
||||||
import { Store } from "../../Logic/UIEventSource";
|
import { Store } from "../../Logic/UIEventSource";
|
||||||
import BaseUIElement from "../BaseUIElement";
|
import BaseUIElement from "../BaseUIElement";
|
||||||
import Img from "./Img";
|
import Img from "./Img";
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
let imgElem: HTMLElement;
|
let imgElem: HTMLElement;
|
||||||
let msgElem: HTMLElement;
|
let msgElem: HTMLElement;
|
||||||
let imgClasses = "block justify-center shrink-0 mr-4 " + (options?.imgSize ?? "h-11 w-11");
|
let imgClasses = "block justify-center shrink-0 mr-4 " + (options?.imgSize ?? "h-11 w-11");
|
||||||
|
const dispatch = createEventDispatcher<{click}>()
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Image
|
// Image
|
||||||
if (imgElem && imageUrl) {
|
if (imgElem && imageUrl) {
|
||||||
|
@ -47,15 +47,16 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:element
|
<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}
|
href={$href}
|
||||||
target={options?.newTab ? "_blank" : ""}
|
target={options?.newTab ? "_blank" : ""}
|
||||||
this={href === undefined ? "span" : "a"}
|
this={href === undefined ? "span" : "a"}
|
||||||
|
on:click={(e) => dispatch("click", e)}
|
||||||
>
|
>
|
||||||
<slot name="image">
|
<slot name="image">
|
||||||
{#if imageUrl !== undefined}
|
{#if imageUrl !== undefined}
|
||||||
{#if typeof imageUrl === "string"}
|
{#if typeof imageUrl === "string"}
|
||||||
<Img src={imageUrl} class={imgClasses+ " bg-red border border-black"}></Img>
|
<Img src={imageUrl} class={imgClasses}></Img>
|
||||||
{:else }
|
{:else }
|
||||||
<template bind:this={imgElem} />
|
<template bind:this={imgElem} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -20,11 +20,15 @@ export default class SvelteUIElement<
|
||||||
}): SvelteComponentTyped<Props, Events, Slots>
|
}): SvelteComponentTyped<Props, Events, Slots>
|
||||||
}
|
}
|
||||||
private readonly _props: Props
|
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()
|
super()
|
||||||
this._svelteComponent = svelteElement
|
this._svelteComponent = svelteElement
|
||||||
this._props = props
|
this._props = props
|
||||||
|
this._events = events
|
||||||
|
this._slots = slots
|
||||||
}
|
}
|
||||||
|
|
||||||
protected InnerConstructElement(): HTMLElement {
|
protected InnerConstructElement(): HTMLElement {
|
||||||
|
@ -32,6 +36,8 @@ export default class SvelteUIElement<
|
||||||
new this._svelteComponent({
|
new this._svelteComponent({
|
||||||
target: el,
|
target: el,
|
||||||
props: this._props,
|
props: this._props,
|
||||||
|
events: this._events,
|
||||||
|
slots: this._slots,
|
||||||
})
|
})
|
||||||
return el
|
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 { VariableUiElement } from "../Base/VariableUIElement"
|
||||||
import Toggle from "../Input/Toggle"
|
import Toggle from "../Input/Toggle"
|
||||||
import Combine from "../Base/Combine"
|
import Combine from "../Base/Combine"
|
||||||
|
@ -6,18 +5,9 @@ import Translations from "../i18n/Translations"
|
||||||
import { Translation } from "../i18n/Translation"
|
import { Translation } from "../i18n/Translation"
|
||||||
import Svg from "../../Svg"
|
import Svg from "../../Svg"
|
||||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import BaseUIElement from "../BaseUIElement"
|
|
||||||
import FilteredLayer from "../../Models/FilteredLayer"
|
import FilteredLayer from "../../Models/FilteredLayer"
|
||||||
import FilterConfig from "../../Models/ThemeConfig/FilterConfig"
|
|
||||||
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"
|
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 Loc from "../../Models/Loc"
|
||||||
import { BackToThemeOverview } from "./ActionButtons"
|
|
||||||
|
|
||||||
export default class FilterView extends VariableUiElement {
|
export default class FilterView extends VariableUiElement {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -31,11 +21,6 @@ export default class FilterView extends VariableUiElement {
|
||||||
readonly featureSwitchMoreQuests: Store<boolean>
|
readonly featureSwitchMoreQuests: Store<boolean>
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const backgroundSelector = new Toggle(
|
|
||||||
new BackgroundSelector(state),
|
|
||||||
undefined,
|
|
||||||
state.featureSwitchBackgroundSelection ?? new ImmutableStore(false)
|
|
||||||
)
|
|
||||||
super(
|
super(
|
||||||
filteredLayer.map((filteredLayers) => {
|
filteredLayer.map((filteredLayers) => {
|
||||||
// Create the views which toggle layers (and filters them) ...
|
// 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))
|
tileLayers.map((tl) => FilterView.createOverlayToggle(state, tl))
|
||||||
)
|
)
|
||||||
|
|
||||||
elements.push(
|
|
||||||
backgroundSelector,
|
|
||||||
new BackToThemeOverview(state, { imgSize: "h-6 w-6" }).SetClass("block mt-12")
|
|
||||||
)
|
|
||||||
return elements
|
return elements
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -73,17 +54,8 @@ export default class FilterView extends VariableUiElement {
|
||||||
const styledNameChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-2")
|
const styledNameChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-2")
|
||||||
const styledNameUnChecked = 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 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)
|
.SetStyle(style)
|
||||||
.onClick(() => config.isDisplayed.setData(false))
|
.onClick(() => config.isDisplayed.setData(false))
|
||||||
|
|
||||||
|
@ -93,188 +65,4 @@ export default class FilterView extends VariableUiElement {
|
||||||
|
|
||||||
return new Toggle(layerChecked, layerNotChecked, config.isDisplayed)
|
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">/**
|
<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 type FilteredLayer from "../../Models/FilteredLayer";
|
||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||||
|
@ -10,14 +10,19 @@ import type { Writable } from "svelte/store";
|
||||||
import If from "../Base/If.svelte";
|
import If from "../Base/If.svelte";
|
||||||
import Dropdown from "../Base/Dropdown.svelte";
|
import Dropdown from "../Base/Dropdown.svelte";
|
||||||
import { onDestroy } from "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 filteredLayer: FilteredLayer;
|
||||||
export let zoomlevel: number;
|
export let highlightedLayer: UIEventSource<string> | undefined;
|
||||||
|
export let zoomlevel: UIEventSource<number>;
|
||||||
let layer: LayerConfig = filteredLayer.layerDef;
|
let layer: LayerConfig = filteredLayer.layerDef;
|
||||||
let isDisplayed: boolean = filteredLayer.isDisplayed.data;
|
let isDisplayed: boolean = filteredLayer.isDisplayed.data;
|
||||||
onDestroy(filteredLayer.isDisplayed.addCallbackAndRunD(d => {
|
onDestroy(filteredLayer.isDisplayed.addCallbackAndRunD(d => {
|
||||||
isDisplayed = d;
|
isDisplayed = d;
|
||||||
return false
|
return false;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,9 +39,20 @@ function getBooleanStateFor(option: FilterConfig): Writable<boolean> {
|
||||||
function getStateFor(option: FilterConfig): Writable<number> {
|
function getStateFor(option: FilterConfig): Writable<number> {
|
||||||
return filteredLayer.appliedFilters.get(option.id);
|
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>
|
</script>
|
||||||
{#if filteredLayer.layerDef.name}
|
{#if filteredLayer.layerDef.name}
|
||||||
<div>
|
<div bind:this={mainElem}>
|
||||||
<label class="flex gap-1">
|
<label class="flex gap-1">
|
||||||
<Checkbox selected={filteredLayer.isDisplayed} />
|
<Checkbox selected={filteredLayer.isDisplayed} />
|
||||||
<If condition={filteredLayer.isDisplayed}>
|
<If condition={filteredLayer.isDisplayed}>
|
||||||
|
@ -45,6 +61,13 @@ function getStateFor(option: FilterConfig): Writable<number> {
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
{filteredLayer.layerDef.name}
|
{filteredLayer.layerDef.name}
|
||||||
|
|
||||||
|
{#if $zoomlevel < layer.minzoom}
|
||||||
|
<span class="alert">
|
||||||
|
<Tr t={Translations.t.general.layerSelection.zoomInToSeeThisLayer} />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
</label>
|
</label>
|
||||||
<If condition={filteredLayer.isDisplayed}>
|
<If condition={filteredLayer.isDisplayed}>
|
||||||
<div id="subfilters" class="flex flex-col gap-y-1 mb-4 ml-4">
|
<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>
|
</label>
|
||||||
{/if}
|
{/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}
|
{#if filter.options.length > 1}
|
||||||
<Dropdown value={getStateFor(filter)}>
|
<Dropdown value={getStateFor(filter)}>
|
||||||
{#each filter.options as option, i}
|
{#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 { Utils } from "../../Utils"
|
||||||
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
||||||
import Loc from "../../Models/Loc"
|
import Loc from "../../Models/Loc"
|
||||||
import BaseLayer from "../../Models/BaseLayer"
|
|
||||||
import FilteredLayer from "../../Models/FilteredLayer"
|
import FilteredLayer from "../../Models/FilteredLayer"
|
||||||
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
|
|
||||||
import PrivacyPolicy from "./PrivacyPolicy"
|
|
||||||
import Hotkeys from "../Base/Hotkeys"
|
import Hotkeys from "../Base/Hotkeys"
|
||||||
|
|
||||||
export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
|
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) })
|
tabs.push({ header: Svg.share_img, content: new ShareScreen(state) })
|
||||||
}
|
}
|
||||||
|
|
||||||
const privacy = {
|
|
||||||
header: Svg.eye_svg(),
|
|
||||||
content: new PrivacyPolicy(),
|
|
||||||
}
|
|
||||||
tabs.push(privacy)
|
|
||||||
|
|
||||||
return tabs
|
return tabs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
|
||||||
import type { Feature } from "geojson";
|
import type { Feature } from "geojson";
|
||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||||
|
@ -11,9 +10,9 @@
|
||||||
import Hotkeys from "../Base/Hotkeys";
|
import Hotkeys from "../Base/Hotkeys";
|
||||||
import { Geocoding } from "../../Logic/Osm/Geocoding";
|
import { Geocoding } from "../../Logic/Osm/Geocoding";
|
||||||
import { BBox } from "../../Logic/BBox";
|
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 bounds: UIEventSource<BBox>
|
||||||
export let selectedElement: UIEventSource<Feature>;
|
export let selectedElement: UIEventSource<Feature>;
|
||||||
export let selectedLayer: UIEventSource<LayerConfig>;
|
export let selectedLayer: UIEventSource<LayerConfig>;
|
||||||
|
@ -50,6 +49,7 @@
|
||||||
const [lat0, lat1, lon0, lon1] = poi.boundingbox
|
const [lat0, lat1, lon0, lon1] = poi.boundingbox
|
||||||
bounds.set(new BBox([[lon0, lat0], [lon1, lat1]]).pad(0.01))
|
bounds.set(new BBox([[lon0, lat0], [lon1, lat1]]).pad(0.01))
|
||||||
const id = poi.osm_type + "/" + poi.osm_id
|
const id = poi.osm_type + "/" + poi.osm_id
|
||||||
|
const perLayer = state.perLayer
|
||||||
const layers = Array.from(perLayer.values())
|
const layers = Array.from(perLayer.values())
|
||||||
for (const layer of layers) {
|
for (const layer of layers) {
|
||||||
const found = layer.features.data.find(f => f.properties.id === id)
|
const found = layer.features.data.find(f => f.properties.id === id)
|
||||||
|
|
|
@ -1,18 +1,13 @@
|
||||||
import Combine from "../Base/Combine"
|
import Combine from "../Base/Combine"
|
||||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
|
||||||
import Translations from "../i18n/Translations"
|
|
||||||
import Toggle from "../Input/Toggle"
|
import Toggle from "../Input/Toggle"
|
||||||
import MapControlButton from "../MapControlButton"
|
import MapControlButton from "../MapControlButton"
|
||||||
import Svg from "../../Svg"
|
import Svg from "../../Svg"
|
||||||
import AllDownloads from "./AllDownloads"
|
import AllDownloads from "./AllDownloads"
|
||||||
import FilterView from "./FilterView"
|
|
||||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import BackgroundMapSwitch from "./BackgroundMapSwitch"
|
|
||||||
import Lazy from "../Base/Lazy"
|
import Lazy from "../Base/Lazy"
|
||||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||||
import FeatureInfoBox from "../Popup/FeatureInfoBox"
|
import FeatureInfoBox from "../Popup/FeatureInfoBox"
|
||||||
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
|
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
|
||||||
import Hotkeys from "../Base/Hotkeys"
|
|
||||||
import { DefaultGuiState } from "../DefaultGuiState"
|
import { DefaultGuiState } from "../DefaultGuiState"
|
||||||
|
|
||||||
export default class LeftControls extends Combine {
|
export default class LeftControls extends Combine {
|
||||||
|
@ -74,32 +69,7 @@ export default class LeftControls extends Combine {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
new ScrollableFullScreen(
|
super([currentViewAction, downloadButton])
|
||||||
() => 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])
|
|
||||||
|
|
||||||
this.SetClass("flex flex-col")
|
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 LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||||
import Loc from "../../Models/Loc"
|
import Loc from "../../Models/Loc"
|
||||||
import BaseLayer from "../../Models/BaseLayer"
|
|
||||||
import FilteredLayer from "../../Models/FilteredLayer"
|
import FilteredLayer from "../../Models/FilteredLayer"
|
||||||
import { InputElement } from "../Input/InputElement"
|
import { InputElement } from "../Input/InputElement"
|
||||||
import { CheckBox } from "../Input/Checkboxes"
|
import { CheckBox } from "../Input/Checkboxes"
|
||||||
import { SubtleButton } from "../Base/SubtleButton"
|
import { SubtleButton } from "../Base/SubtleButton"
|
||||||
import LZString from "lz-string"
|
import LZString from "lz-string"
|
||||||
|
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||||
|
|
||||||
export default class ShareScreen extends Combine {
|
export default class ShareScreen extends Combine {
|
||||||
constructor(state: {
|
constructor(state: SpecialVisualizationState) {
|
||||||
layoutToUse: LayoutConfig
|
const layout = state?.layout
|
||||||
locationControl: UIEventSource<Loc>
|
|
||||||
backgroundLayer: UIEventSource<BaseLayer>
|
|
||||||
filteredLayers: UIEventSource<FilteredLayer[]>
|
|
||||||
}) {
|
|
||||||
const layout = state?.layoutToUse
|
|
||||||
const tr = Translations.t.general.sharescreen
|
const tr = Translations.t.general.sharescreen
|
||||||
|
|
||||||
const optionCheckboxes: InputElement<boolean>[] = []
|
const optionCheckboxes: InputElement<boolean>[] = []
|
||||||
|
@ -32,7 +27,8 @@ export default class ShareScreen extends Combine {
|
||||||
const includeLocation = new CheckBox(tr.fsIncludeCurrentLocation, true)
|
const includeLocation = new CheckBox(tr.fsIncludeCurrentLocation, true)
|
||||||
optionCheckboxes.push(includeLocation)
|
optionCheckboxes.push(includeLocation)
|
||||||
|
|
||||||
const currentLocation = state.locationControl
|
const currentLocation = state.mapProperties.location
|
||||||
|
const zoom = state.mapProperties.zoom
|
||||||
|
|
||||||
optionParts.push(
|
optionParts.push(
|
||||||
includeLocation.GetValue().map(
|
includeLocation.GetValue().map(
|
||||||
|
@ -42,7 +38,7 @@ export default class ShareScreen extends Combine {
|
||||||
}
|
}
|
||||||
if (includeL) {
|
if (includeL) {
|
||||||
return [
|
return [
|
||||||
["z", currentLocation.data?.zoom],
|
["z", zoom.data],
|
||||||
["lat", currentLocation.data?.lat],
|
["lat", currentLocation.data?.lat],
|
||||||
["lon", currentLocation.data?.lon],
|
["lon", currentLocation.data?.lon],
|
||||||
]
|
]
|
||||||
|
@ -53,7 +49,7 @@ export default class ShareScreen extends Combine {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[currentLocation]
|
[currentLocation, zoom]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -67,8 +63,8 @@ export default class ShareScreen extends Combine {
|
||||||
return "layer-" + flayer.layerDef.id + "=" + flayer.isDisplayed.data
|
return "layer-" + flayer.layerDef.id + "=" + flayer.isDisplayed.data
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentLayer: UIEventSource<{ id: string; name: string; layer: any }> =
|
const currentLayer: Store<{ id: string; name: string } | undefined> =
|
||||||
state.backgroundLayer
|
state.mapProperties.rasterLayer.map((l) => l?.properties)
|
||||||
const currentBackground = new VariableUiElement(
|
const currentBackground = new VariableUiElement(
|
||||||
currentLayer.map((layer) => {
|
currentLayer.map((layer) => {
|
||||||
return tr.fsIncludeCurrentBackgroundMap.Subs({ name: layer?.name ?? "" })
|
return tr.fsIncludeCurrentBackgroundMap.Subs({ name: layer?.name ?? "" })
|
||||||
|
@ -96,7 +92,9 @@ export default class ShareScreen extends Combine {
|
||||||
includeLayerChoices.GetValue().map(
|
includeLayerChoices.GetValue().map(
|
||||||
(includeLayerSelection) => {
|
(includeLayerSelection) => {
|
||||||
if (includeLayerSelection) {
|
if (includeLayerSelection) {
|
||||||
return Utils.NoNull(state.filteredLayers.data.map(fLayerToParam)).join("&")
|
return Utils.NoNull(
|
||||||
|
state.layerState.filteredLayers.map(fLayerToParam)
|
||||||
|
).join("&")
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,22 @@
|
||||||
/**
|
/**
|
||||||
* Asks to add a feature at the last clicked location, at least if zoom is sufficient
|
* Asks to add a feature at the last clicked location, at least if zoom is sufficient
|
||||||
*/
|
*/
|
||||||
import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource"
|
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import Svg from "../../Svg"
|
|
||||||
import { SubtleButton } from "../Base/SubtleButton"
|
|
||||||
import Combine from "../Base/Combine"
|
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../i18n/Translations"
|
||||||
import Constants from "../../Models/Constants"
|
|
||||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
|
||||||
import BaseUIElement from "../BaseUIElement"
|
import BaseUIElement from "../BaseUIElement"
|
||||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||||
import Toggle from "../Input/Toggle"
|
import Toggle from "../Input/Toggle"
|
||||||
import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
|
||||||
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"
|
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"
|
||||||
import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject"
|
import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject"
|
||||||
import PresetConfig from "../../Models/ThemeConfig/PresetConfig"
|
import PresetConfig from "../../Models/ThemeConfig/PresetConfig"
|
||||||
import FilteredLayer from "../../Models/FilteredLayer"
|
import FilteredLayer from "../../Models/FilteredLayer"
|
||||||
import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint"
|
|
||||||
import Loading from "../Base/Loading"
|
import Loading from "../Base/Loading"
|
||||||
import Hash from "../../Logic/Web/Hash"
|
import Hash from "../../Logic/Web/Hash"
|
||||||
import { WayId } from "../../Models/OsmFeature"
|
import { WayId } from "../../Models/OsmFeature"
|
||||||
import { Tag } from "../../Logic/Tags/Tag"
|
import { Tag } from "../../Logic/Tags/Tag"
|
||||||
import { LoginToggle } from "../Popup/LoginButton"
|
|
||||||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
|
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The SimpleAddUI is a single panel, which can have multiple states:
|
* The SimpleAddUI is a single panel, which can have multiple states:
|
||||||
|
@ -40,33 +33,18 @@ export interface PresetInfo extends PresetConfig {
|
||||||
boundsFactor?: 0.25 | number
|
boundsFactor?: 0.25 | number
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class SimpleAddUI extends LoginToggle {
|
export default class SimpleAddUI extends Toggle {
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
constructor(state: SpecialVisualizationState) {
|
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 takeLocationFrom = state.mapProperties.lastClickLocation
|
||||||
const selectedPreset = new UIEventSource<PresetInfo>(undefined)
|
const selectedPreset = new UIEventSource<PresetInfo>(undefined)
|
||||||
|
|
||||||
takeLocationFrom.addCallback((_) => selectedPreset.setData(undefined))
|
takeLocationFrom.addCallback((_) => selectedPreset.setData(undefined))
|
||||||
|
|
||||||
const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset, state)
|
|
||||||
|
|
||||||
async function createNewPoint(
|
async function createNewPoint(
|
||||||
tags: Tag[],
|
tags: Tag[],
|
||||||
location: { lat: number; lon: number },
|
location: { lat: number; lon: number },
|
||||||
snapOntoWay?: OsmWay
|
snapOntoWay?: OsmWay
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
tags.push(new Tag(Tag.newlyCreated.key, new Date().toISOString()))
|
|
||||||
if (snapOntoWay) {
|
if (snapOntoWay) {
|
||||||
tags.push(new Tag("_referencing_ways", "way/" + snapOntoWay.id))
|
tags.push(new Tag("_referencing_ways", "way/" + snapOntoWay.id))
|
||||||
}
|
}
|
||||||
|
@ -86,10 +64,6 @@ export default class SimpleAddUI extends LoginToggle {
|
||||||
|
|
||||||
const addUi = new VariableUiElement(
|
const addUi = new VariableUiElement(
|
||||||
selectedPreset.map((preset) => {
|
selectedPreset.map((preset) => {
|
||||||
if (preset === undefined) {
|
|
||||||
return presetsOverview
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirm(
|
function confirm(
|
||||||
tags: any[],
|
tags: any[],
|
||||||
location: { lat: number; lon: number },
|
location: { lat: number; lon: number },
|
||||||
|
@ -113,7 +87,7 @@ export default class SimpleAddUI extends LoginToggle {
|
||||||
{ category: preset.name },
|
{ category: preset.name },
|
||||||
preset.name["context"]
|
preset.name["context"]
|
||||||
)
|
)
|
||||||
return new ConfirmLocationOfPoint(
|
return new FixedUiElement("ConfirmLocationOfPoint...") /*ConfirmLocationOfPoint(
|
||||||
state,
|
state,
|
||||||
filterViewIsOpened,
|
filterViewIsOpened,
|
||||||
preset,
|
preset,
|
||||||
|
@ -128,140 +102,14 @@ export default class SimpleAddUI extends LoginToggle {
|
||||||
cancelIcon: Svg.back_svg(),
|
cancelIcon: Svg.back_svg(),
|
||||||
cancelText: Translations.t.general.add.backToSelect,
|
cancelText: Translations.t.general.add.backToSelect,
|
||||||
}
|
}
|
||||||
)
|
)*/
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
super(
|
super(
|
||||||
new Toggle(
|
new Loading(Translations.t.general.add.stillLoading).SetClass("alert"),
|
||||||
new Toggle(
|
addUi,
|
||||||
new Toggle(
|
state.dataIsLoading
|
||||||
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
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 { DefaultGuiState } from "./DefaultGuiState"
|
||||||
import NewNoteUi from "./Popup/NewNoteUi"
|
import NewNoteUi from "./Popup/NewNoteUi"
|
||||||
import Combine from "./Base/Combine"
|
import Combine from "./Base/Combine"
|
||||||
import AddNewMarker from "./BigComponents/AddNewMarker"
|
|
||||||
import FilteredLayer from "../Models/FilteredLayer"
|
import FilteredLayer from "../Models/FilteredLayer"
|
||||||
import ExtraLinkButton from "./BigComponents/ExtraLinkButton"
|
import ExtraLinkButton from "./BigComponents/ExtraLinkButton"
|
||||||
import { VariableUiElement } from "./Base/VariableUIElement"
|
import { VariableUiElement } from "./Base/VariableUIElement"
|
||||||
|
@ -108,13 +107,6 @@ export default class DefaultGUI {
|
||||||
newPointDialogIsShown
|
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
|
let noteMarker = undefined
|
||||||
if (!hasPresets && addNewNoteDialog !== undefined) {
|
if (!hasPresets && addNewNoteDialog !== undefined) {
|
||||||
noteMarker = new Combine([
|
noteMarker = new Combine([
|
||||||
|
@ -126,15 +118,6 @@ export default class DefaultGUI {
|
||||||
.SetClass("block relative h-full")
|
.SetClass("block relative h-full")
|
||||||
.SetStyle("left: calc( 50% - 15px )") // This is a bit hacky, yes I know!
|
.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) {
|
if (noteLayer !== undefined) {
|
||||||
|
@ -208,22 +191,6 @@ export default class DefaultGUI {
|
||||||
self.InitWelcomeMessage()
|
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(
|
new ScrollableFullScreen(
|
||||||
() => Translations.t.general.attribution.attributionTitle,
|
() => Translations.t.general.attribution.attributionTitle,
|
||||||
() => new CopyrightPanel(state),
|
() => new CopyrightPanel(state),
|
||||||
|
@ -233,14 +200,7 @@ export default class DefaultGUI {
|
||||||
const copyright = new MapControlButton(Svg.copyright_svg()).onClick(() =>
|
const copyright = new MapControlButton(Svg.copyright_svg()).onClick(() =>
|
||||||
guiState.copyrightViewIsOpened.setData(true)
|
guiState.copyrightViewIsOpened.setData(true)
|
||||||
)
|
)
|
||||||
new Combine([
|
new Combine([welcomeMessageMapControl, userInfoMapControl, copyright, extraLink])
|
||||||
welcomeMessageMapControl,
|
|
||||||
userInfoMapControl,
|
|
||||||
copyright,
|
|
||||||
communityIndex,
|
|
||||||
extraLink,
|
|
||||||
testingBadge,
|
|
||||||
])
|
|
||||||
.SetClass("flex flex-col")
|
.SetClass("flex flex-col")
|
||||||
.AttachTo("top-left")
|
.AttachTo("top-left")
|
||||||
|
|
||||||
|
@ -264,32 +224,11 @@ export default class DefaultGUI {
|
||||||
}
|
}
|
||||||
|
|
||||||
private InitWelcomeMessage(): BaseUIElement {
|
private InitWelcomeMessage(): BaseUIElement {
|
||||||
const isOpened = this.guiState.welcomeMessageIsOpened
|
return new FullWelcomePaneWithTabs(
|
||||||
new FullWelcomePaneWithTabs(
|
new UIEventSource<boolean>(false),
|
||||||
isOpened,
|
|
||||||
this.guiState.welcomeMessageOpenedTab,
|
this.guiState.welcomeMessageOpenedTab,
|
||||||
this.state,
|
this.state,
|
||||||
this.guiState
|
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 { BBox } from "../../Logic/BBox";
|
||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||||
import Combine from "../Base/Combine"
|
import Combine from "../Base/Combine";
|
||||||
import Title from "../Base/Title"
|
import Title from "../Base/Title";
|
||||||
import { Overpass } from "../../Logic/Osm/Overpass"
|
import { Overpass } from "../../Logic/Osm/Overpass";
|
||||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../../Logic/UIEventSource";
|
||||||
import Constants from "../../Models/Constants"
|
import Constants from "../../Models/Constants";
|
||||||
import RelationsTracker from "../../Logic/Osm/RelationsTracker"
|
import RelationsTracker from "../../Logic/Osm/RelationsTracker";
|
||||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
import { VariableUiElement } from "../Base/VariableUIElement";
|
||||||
import { FlowStep } from "./FlowStep"
|
import { FlowStep } from "./FlowStep";
|
||||||
import Loading from "../Base/Loading"
|
import Loading from "../Base/Loading";
|
||||||
import { SubtleButton } from "../Base/SubtleButton"
|
import { SubtleButton } from "../Base/SubtleButton";
|
||||||
import Svg from "../../Svg"
|
import Svg from "../../Svg";
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils";
|
||||||
import { IdbLocalStorage } from "../../Logic/Web/IdbLocalStorage"
|
import { IdbLocalStorage } from "../../Logic/Web/IdbLocalStorage";
|
||||||
import Minimap from "../Base/Minimap"
|
import Minimap from "../Base/Minimap";
|
||||||
import BaseLayer from "../../Models/BaseLayer"
|
import BaseLayer from "../../Models/BaseLayer";
|
||||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"
|
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
||||||
import Loc from "../../Models/Loc"
|
import Loc from "../../Models/Loc";
|
||||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
|
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
|
||||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||||
import ValidatedTextField from "../Input/ValidatedTextField"
|
import ValidatedTextField from "../Input/ValidatedTextField";
|
||||||
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
|
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource";
|
||||||
import import_candidate from "../../assets/layers/import_candidate/import_candidate.json"
|
import import_candidate from "../../assets/layers/import_candidate/import_candidate.json";
|
||||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
import { GeoOperations } from "../../Logic/GeoOperations";
|
||||||
import FeatureInfoBox from "../Popup/FeatureInfoBox"
|
import FeatureInfoBox from "../Popup/FeatureInfoBox";
|
||||||
import { ImportUtils } from "./ImportUtils"
|
import { ImportUtils } from "./ImportUtils";
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../i18n/Translations";
|
||||||
import currentview from "../../assets/layers/current_view/current_view.json"
|
import currentview from "../../assets/layers/current_view/current_view.json";
|
||||||
import { CheckBox } from "../Input/Checkboxes"
|
import { CheckBox } from "../Input/Checkboxes";
|
||||||
import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"
|
import { Feature, FeatureCollection, Point } from "geojson";
|
||||||
import { Feature, FeatureCollection, Point } from "geojson"
|
import DivContainer from "../Base/DivContainer";
|
||||||
import DivContainer from "../Base/DivContainer"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given the data to import, the bbox and the layer, will query overpass for similar items
|
* 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,
|
t.setRangeToZero,
|
||||||
matchedFeaturesMap,
|
matchedFeaturesMap,
|
||||||
new Combine([
|
showOsmLayer,
|
||||||
new BackgroundMapSwitch(
|
|
||||||
{ backgroundLayer: background, locationControl: matchedFeaturesMap.location },
|
|
||||||
background
|
|
||||||
),
|
|
||||||
showOsmLayer,
|
|
||||||
]).SetClass("flex"),
|
|
||||||
]).SetClass("flex flex-col")
|
]).SetClass("flex flex-col")
|
||||||
super([
|
super([
|
||||||
new Title(t.title),
|
new Title(t.title),
|
||||||
|
|
|
@ -17,7 +17,6 @@ import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||||
import Title from "../Base/Title"
|
import Title from "../Base/Title"
|
||||||
import CheckBoxes from "../Input/Checkboxes"
|
import CheckBoxes from "../Input/Checkboxes"
|
||||||
import AllTagsPanel from "../Popup/AllTagsPanel.svelte"
|
import AllTagsPanel from "../Popup/AllTagsPanel.svelte"
|
||||||
import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"
|
|
||||||
import { Feature, Point } from "geojson"
|
import { Feature, Point } from "geojson"
|
||||||
import DivContainer from "../Base/DivContainer"
|
import DivContainer from "../Base/DivContainer"
|
||||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||||
|
@ -112,13 +111,7 @@ export class MapPreview
|
||||||
const currentBounds = new UIEventSource<BBox>(undefined)
|
const currentBounds = new UIEventSource<BBox>(undefined)
|
||||||
const { ui, mapproperties, map } = MapLibreAdaptor.construct()
|
const { ui, mapproperties, map } = MapLibreAdaptor.construct()
|
||||||
|
|
||||||
const layerControl = new BackgroundMapSwitch(
|
|
||||||
{
|
|
||||||
backgroundLayer: background,
|
|
||||||
locationControl: location,
|
|
||||||
},
|
|
||||||
background
|
|
||||||
)
|
|
||||||
ui.SetClass("w-full").SetStyle("height: 500px")
|
ui.SetClass("w-full").SetStyle("height: 500px")
|
||||||
|
|
||||||
layerPicker.GetValue().addCallbackAndRunD((layerToShow) => {
|
layerPicker.GetValue().addCallbackAndRunD((layerToShow) => {
|
||||||
|
@ -160,7 +153,6 @@ export class MapPreview
|
||||||
mismatchIndicator,
|
mismatchIndicator,
|
||||||
ui,
|
ui,
|
||||||
new DivContainer("fullscreen"),
|
new DivContainer("fullscreen"),
|
||||||
layerControl,
|
|
||||||
confirm,
|
confirm,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { InputElement } from "./InputElement"
|
import { InputElement } from "./InputElement";
|
||||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource";
|
||||||
import Combine from "../Base/Combine"
|
import Combine from "../Base/Combine";
|
||||||
import Svg from "../../Svg"
|
import Svg from "../../Svg";
|
||||||
import Loc from "../../Models/Loc"
|
import Loc from "../../Models/Loc";
|
||||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
import { GeoOperations } from "../../Logic/GeoOperations";
|
||||||
import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"
|
import BaseUIElement from "../BaseUIElement";
|
||||||
import BaseUIElement from "../BaseUIElement"
|
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers";
|
||||||
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selects a length after clicking on the minimap, in meters
|
* Selects a length after clicking on the minimap, in meters
|
||||||
|
@ -38,7 +37,7 @@ export default class LengthInput extends InputElement<string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected InnerConstructElement(): HTMLElement {
|
protected InnerConstructElement(): HTMLElement {
|
||||||
let map: BaseUIElement & MinimapObj = undefined
|
let map: BaseUIElement = undefined
|
||||||
let layerControl: BaseUIElement = undefined
|
let layerControl: BaseUIElement = undefined
|
||||||
map = Minimap.createMiniMap({
|
map = Minimap.createMiniMap({
|
||||||
background: this.background,
|
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([
|
const crosshair = new Combine([
|
||||||
Svg.length_crosshair_svg().SetStyle(
|
Svg.length_crosshair_svg().SetStyle(
|
||||||
`position: absolute;top: 0;left: 0;transform:rotate(${this.value.data ?? 0}deg);`
|
`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([
|
const element = new Combine([
|
||||||
crosshair,
|
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"),
|
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")
|
.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
|
* A visualisation to pick a direction on a map background
|
||||||
*/
|
*/
|
||||||
export let value: UIEventSource<{lon: number, lat: number}>;
|
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
|
* 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
|
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);
|
let mla = new MapLibreAdaptor(map, mapProperties);
|
||||||
mla.allowMoving.setData(true)
|
|
||||||
mla.allowZooming.setData(true)
|
|
||||||
|
|
||||||
if(onCreated){
|
if(onCreated){
|
||||||
onCreated(value, map, mla)
|
onCreated(value, map, mla)
|
||||||
}
|
}
|
||||||
</script>
|
</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">
|
<div class="w-full h-full absolute top-0 left-0 cursor-pointer">
|
||||||
<MaplibreMap {map} attribution={false}></MaplibreMap>
|
<MaplibreMap {map} attribution={false}></MaplibreMap>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full h-full absolute top-0 left-0 p-8 pointer-events-none opacity-50">
|
<div class="w-full h-full absolute top-0 left-0 p-8 pointer-events-none opacity-50 flex items-center">
|
||||||
<ToSvelte construct={() => Svg.move_arrows_svg().SetClass("h-full")}></ToSvelte>
|
<img src="./assets/svg/move-arrows.svg" class="h-full max-h-24"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DragInvitation></DragInvitation>
|
<DragInvitation hideSignal={mla.location.stabilized(3000)}></DragInvitation>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -33,10 +33,10 @@
|
||||||
|
|
||||||
let dispatch = createEventDispatcher<{ selected }>();
|
let dispatch = createEventDispatcher<{ selected }>();
|
||||||
$: {
|
$: {
|
||||||
console.log(htmlElem)
|
console.log(htmlElem);
|
||||||
if (htmlElem !== undefined) {
|
if (htmlElem !== undefined) {
|
||||||
htmlElem.onfocus = () => {
|
htmlElem.onfocus = () => {
|
||||||
console.log("Dispatching selected event")
|
console.log("Dispatching selected event");
|
||||||
return dispatch("selected");
|
return dispatch("selected");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -44,12 +44,12 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if validator.textArea}
|
{#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 }
|
{:else }
|
||||||
<div class="flex">
|
<span class="flex">
|
||||||
<input bind:this={htmlElem} bind:value={$_value} inputmode={validator.inputmode ?? "text"}>
|
<input bind:this={htmlElem} bind:value={$_value} inputmode={validator.inputmode ?? "text"}>
|
||||||
{#if !$isValid}
|
{#if !$isValid}
|
||||||
<ExclamationIcon class="h-6 w-6 -ml-6"></ExclamationIcon>
|
<ExclamationIcon class="h-6 w-6 -ml-6"></ExclamationIcon>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -35,8 +35,8 @@ export class MapLibreAdaptor implements MapProperties {
|
||||||
readonly allowMoving: UIEventSource<true | boolean | undefined>
|
readonly allowMoving: UIEventSource<true | boolean | undefined>
|
||||||
readonly allowZooming: UIEventSource<true | boolean | undefined>
|
readonly allowZooming: UIEventSource<true | boolean | undefined>
|
||||||
readonly lastClickLocation: Store<undefined | { lon: number; lat: number }>
|
readonly lastClickLocation: Store<undefined | { lon: number; lat: number }>
|
||||||
|
readonly minzoom: UIEventSource<number>
|
||||||
private readonly _maplibreMap: Store<MLMap>
|
private readonly _maplibreMap: Store<MLMap>
|
||||||
private readonly _bounds: UIEventSource<BBox>
|
|
||||||
/**
|
/**
|
||||||
* Used for internal bookkeeping (to remove a rasterLayer when done loading)
|
* Used for internal bookkeeping (to remove a rasterLayer when done loading)
|
||||||
* @private
|
* @private
|
||||||
|
@ -48,9 +48,10 @@ export class MapLibreAdaptor implements MapProperties {
|
||||||
|
|
||||||
this.location = state?.location ?? new UIEventSource({ lon: 0, lat: 0 })
|
this.location = state?.location ?? new UIEventSource({ lon: 0, lat: 0 })
|
||||||
this.zoom = state?.zoom ?? new UIEventSource(1)
|
this.zoom = state?.zoom ?? new UIEventSource(1)
|
||||||
|
this.minzoom = state?.minzoom ?? new UIEventSource(0)
|
||||||
this.zoom.addCallbackAndRunD((z) => {
|
this.zoom.addCallbackAndRunD((z) => {
|
||||||
if (z < 0) {
|
if (z < this.minzoom.data) {
|
||||||
this.zoom.setData(0)
|
this.zoom.setData(this.minzoom.data)
|
||||||
}
|
}
|
||||||
if (z > 24) {
|
if (z > 24) {
|
||||||
this.zoom.setData(24)
|
this.zoom.setData(24)
|
||||||
|
@ -59,8 +60,7 @@ export class MapLibreAdaptor implements MapProperties {
|
||||||
this.maxbounds = state?.maxbounds ?? new UIEventSource(undefined)
|
this.maxbounds = state?.maxbounds ?? new UIEventSource(undefined)
|
||||||
this.allowMoving = state?.allowMoving ?? new UIEventSource(true)
|
this.allowMoving = state?.allowMoving ?? new UIEventSource(true)
|
||||||
this.allowZooming = state?.allowZooming ?? new UIEventSource(true)
|
this.allowZooming = state?.allowZooming ?? new UIEventSource(true)
|
||||||
this._bounds = new UIEventSource(undefined)
|
this.bounds = state?.bounds ?? new UIEventSource(undefined)
|
||||||
this.bounds = this._bounds
|
|
||||||
this.rasterLayer =
|
this.rasterLayer =
|
||||||
state?.rasterLayer ?? new UIEventSource<RasterLayerPolygon | undefined>(undefined)
|
state?.rasterLayer ?? new UIEventSource<RasterLayerPolygon | undefined>(undefined)
|
||||||
|
|
||||||
|
@ -69,32 +69,28 @@ export class MapLibreAdaptor implements MapProperties {
|
||||||
const self = this
|
const self = this
|
||||||
maplibreMap.addCallbackAndRunD((map) => {
|
maplibreMap.addCallbackAndRunD((map) => {
|
||||||
map.on("load", () => {
|
map.on("load", () => {
|
||||||
|
this.updateStores()
|
||||||
self.setBackground()
|
self.setBackground()
|
||||||
self.MoveMapToCurrentLoc(self.location.data)
|
self.MoveMapToCurrentLoc(self.location.data)
|
||||||
self.SetZoom(self.zoom.data)
|
self.SetZoom(self.zoom.data)
|
||||||
self.setMaxBounds(self.maxbounds.data)
|
self.setMaxBounds(self.maxbounds.data)
|
||||||
self.setAllowMoving(self.allowMoving.data)
|
self.setAllowMoving(self.allowMoving.data)
|
||||||
self.setAllowZooming(self.allowZooming.data)
|
self.setAllowZooming(self.allowZooming.data)
|
||||||
|
self.setMinzoom(self.minzoom.data)
|
||||||
})
|
})
|
||||||
self.MoveMapToCurrentLoc(self.location.data)
|
self.MoveMapToCurrentLoc(self.location.data)
|
||||||
self.SetZoom(self.zoom.data)
|
self.SetZoom(self.zoom.data)
|
||||||
self.setMaxBounds(self.maxbounds.data)
|
self.setMaxBounds(self.maxbounds.data)
|
||||||
self.setAllowMoving(self.allowMoving.data)
|
self.setAllowMoving(self.allowMoving.data)
|
||||||
self.setAllowZooming(self.allowZooming.data)
|
self.setAllowZooming(self.allowZooming.data)
|
||||||
map.on("moveend", () => {
|
self.setMinzoom(self.minzoom.data)
|
||||||
const dt = this.location.data
|
this.updateStores()
|
||||||
dt.lon = map.getCenter().lng
|
map.on("moveend", () => this.updateStores())
|
||||||
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)
|
|
||||||
})
|
|
||||||
map.on("click", (e) => {
|
map.on("click", (e) => {
|
||||||
|
if (e.originalEvent["consumed"]) {
|
||||||
|
// Workaround, 'ShowPointLayer' sets this flag
|
||||||
|
return
|
||||||
|
}
|
||||||
const lon = e.lngLat.lng
|
const lon = e.lngLat.lng
|
||||||
const lat = e.lngLat.lat
|
const lat = e.lngLat.lat
|
||||||
lastClickLocation.setData({ lon, lat })
|
lastClickLocation.setData({ lon, lat })
|
||||||
|
@ -117,6 +113,23 @@ export class MapLibreAdaptor implements MapProperties {
|
||||||
this.bounds.addCallbackAndRunD((bounds) => self.setBounds(bounds))
|
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
|
* Convenience constructor
|
||||||
*/
|
*/
|
||||||
|
@ -191,7 +204,7 @@ export class MapLibreAdaptor implements MapProperties {
|
||||||
if (map === undefined) {
|
if (map === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
while (!map.isStyleLoaded()) {
|
while (!map?.isStyleLoaded()) {
|
||||||
await Utils.waitFor(250)
|
await Utils.waitFor(250)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -265,9 +278,9 @@ export class MapLibreAdaptor implements MapProperties {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (bbox) {
|
if (bbox) {
|
||||||
map.setMaxBounds(bbox.toLngLat())
|
map?.setMaxBounds(bbox.toLngLat())
|
||||||
} else {
|
} 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) {
|
private setAllowZooming(allow: true | boolean | undefined) {
|
||||||
const map = this._maplibreMap.data
|
const map = this._maplibreMap.data
|
||||||
if (map === undefined) {
|
if (map === undefined) {
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { GeoOperations } from "../../Logic/GeoOperations"
|
||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||||
import PointRenderingConfig from "../../Models/ThemeConfig/PointRenderingConfig"
|
import PointRenderingConfig from "../../Models/ThemeConfig/PointRenderingConfig"
|
||||||
import { OsmTags } from "../../Models/OsmFeature"
|
import { OsmTags } from "../../Models/OsmFeature"
|
||||||
import FeatureSource from "../../Logic/FeatureSource/FeatureSource"
|
import { FeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
||||||
import { BBox } from "../../Logic/BBox"
|
import { BBox } from "../../Logic/BBox"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||||
|
@ -124,8 +124,11 @@ class PointRenderingLayer {
|
||||||
|
|
||||||
if (this._onClick) {
|
if (this._onClick) {
|
||||||
const self = this
|
const self = this
|
||||||
el.addEventListener("click", function () {
|
el.addEventListener("click", function (ev) {
|
||||||
self._onClick(feature)
|
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 _layername: string
|
||||||
private readonly _listenerInstalledOn: Set<string> = new Set<string>()
|
private readonly _listenerInstalledOn: Set<string> = new Set<string>()
|
||||||
|
|
||||||
|
private static missingIdTriggered = false
|
||||||
constructor(
|
constructor(
|
||||||
map: MlMap,
|
map: MlMap,
|
||||||
features: FeatureSource,
|
features: FeatureSource,
|
||||||
|
@ -281,11 +285,14 @@ class LineRenderingLayer {
|
||||||
const feature = features[i]
|
const feature = features[i]
|
||||||
const id = feature.properties.id ?? feature.id
|
const id = feature.properties.id ?? feature.id
|
||||||
if (id === undefined) {
|
if (id === undefined) {
|
||||||
console.trace(
|
if (!LineRenderingLayer.missingIdTriggered) {
|
||||||
"Got a feature without ID; this causes rendering bugs:",
|
console.trace(
|
||||||
feature,
|
"Got a feature without ID; this causes rendering bugs:",
|
||||||
"from"
|
feature,
|
||||||
)
|
"from"
|
||||||
|
)
|
||||||
|
LineRenderingLayer.missingIdTriggered = true
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (this._listenerInstalledOn.has(id)) {
|
if (this._listenerInstalledOn.has(id)) {
|
||||||
|
@ -334,7 +341,7 @@ export default class ShowDataLayer {
|
||||||
options?: Partial<ShowDataLayerOptions>
|
options?: Partial<ShowDataLayerOptions>
|
||||||
) {
|
) {
|
||||||
const perLayer = new PerLayerFeatureSourceSplitter(
|
const perLayer = new PerLayerFeatureSourceSplitter(
|
||||||
layers.map((l) => new FilteredLayer(l)),
|
layers.filter((l) => l.source !== null).map((l) => new FilteredLayer(l)),
|
||||||
new StaticFeatureSource(features)
|
new StaticFeatureSource(features)
|
||||||
)
|
)
|
||||||
perLayer.forEach((fs) => {
|
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 { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
|
|
|
@ -8,8 +8,7 @@ import Combine from "../Base/Combine"
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../i18n/Translations"
|
||||||
import Svg from "../../Svg"
|
import Svg from "../../Svg"
|
||||||
import Toggle from "../Input/Toggle"
|
import Toggle from "../Input/Toggle"
|
||||||
import SimpleAddUI, { PresetInfo } from "../BigComponents/SimpleAddUI"
|
import { PresetInfo } from "../BigComponents/SimpleAddUI"
|
||||||
import Img from "../Base/Img"
|
|
||||||
import Title from "../Base/Title"
|
import Title from "../Base/Title"
|
||||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||||
import { Tag } from "../../Logic/Tags/Tag"
|
import { Tag } from "../../Logic/Tags/Tag"
|
||||||
|
@ -115,10 +114,6 @@ export default class ConfirmLocationOfPoint extends Combine {
|
||||||
)
|
)
|
||||||
.SetClass("font-bold break-words")
|
.SetClass("font-bold break-words")
|
||||||
.onClick(() => {
|
.onClick(() => {
|
||||||
console.log(
|
|
||||||
"The confirmLocationPanel - precise input yielded ",
|
|
||||||
preciseInput?.GetValue()?.data
|
|
||||||
)
|
|
||||||
const globalFilterTagsToAdd: Tag[][] = state.globalFilters.data
|
const globalFilterTagsToAdd: Tag[][] = state.globalFilters.data
|
||||||
.filter((gf) => gf.onNewPoint !== undefined)
|
.filter((gf) => gf.onNewPoint !== undefined)
|
||||||
.map((gf) => gf.onNewPoint.tags)
|
.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) {
|
if (preciseInput !== undefined) {
|
||||||
confirmButton = new Combine([preciseInput, warn, confirmButton])
|
confirmButton = new Combine([preciseInput, confirmButton])
|
||||||
} else {
|
} else {
|
||||||
confirmButton = new Combine([warn, confirmButton])
|
confirmButton = new Combine([confirmButton])
|
||||||
}
|
}
|
||||||
|
|
||||||
const openLayerControl = new SubtleButton(
|
let openLayerOrConfirm = confirmButton
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
const disableFilter = new SubtleButton(
|
const disableFilter = new SubtleButton(
|
||||||
new Combine([
|
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
|
// 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(
|
const disableFiltersOrConfirm = new Toggle(openLayerOrConfirm, disableFilter)
|
||||||
openLayerOrConfirm,
|
|
||||||
disableFilter,
|
|
||||||
hasActiveFilter
|
|
||||||
)
|
|
||||||
|
|
||||||
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, state.osmConnection)
|
|
||||||
|
|
||||||
const cancelButton = new SubtleButton(
|
const cancelButton = new SubtleButton(
|
||||||
options?.cancelIcon ?? Svg.close_ui(),
|
options?.cancelIcon ?? Svg.close_ui(),
|
||||||
|
@ -223,18 +188,7 @@ export default class ConfirmLocationOfPoint extends Combine {
|
||||||
|
|
||||||
let examples: BaseUIElement = undefined
|
let examples: BaseUIElement = undefined
|
||||||
if (preset.exampleImages !== undefined && preset.exampleImages.length > 0) {
|
if (preset.exampleImages !== undefined && preset.exampleImages.length > 0) {
|
||||||
examples = new Combine([
|
examples = new Combine([new Title()])
|
||||||
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"),
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
super([
|
super([
|
||||||
|
@ -247,7 +201,6 @@ export default class ConfirmLocationOfPoint extends Combine {
|
||||||
cancelButton,
|
cancelButton,
|
||||||
preset.description,
|
preset.description,
|
||||||
examples,
|
examples,
|
||||||
tagInfo,
|
|
||||||
])
|
])
|
||||||
|
|
||||||
this.SetClass("flex flex-col")
|
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[] = [
|
const allRenderings: BaseUIElement[] = [
|
||||||
new VariableUiElement(
|
new VariableUiElement(
|
||||||
tags
|
tags
|
||||||
.map((data) => data[Tag.newlyCreated.key])
|
.map((data) => data["_newly_created"])
|
||||||
.map((isCreated) => {
|
.map((isCreated) => {
|
||||||
if (isCreated === undefined) {
|
if (isCreated === undefined) {
|
||||||
return undefined
|
return undefined
|
||||||
|
|
|
@ -20,7 +20,7 @@ import CreateWayWithPointReuseAction, {
|
||||||
MergePointConfig,
|
MergePointConfig,
|
||||||
} from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction"
|
} from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction"
|
||||||
import OsmChangeAction, { OsmCreateAction } from "../../Logic/Osm/Actions/OsmChangeAction"
|
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 { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject"
|
||||||
import { PresetInfo } from "../BigComponents/SimpleAddUI"
|
import { PresetInfo } from "../BigComponents/SimpleAddUI"
|
||||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||||
|
|
|
@ -36,6 +36,9 @@ export class MinimapViz implements SpecialVisualization {
|
||||||
keys.splice(0, 1)
|
keys.splice(0, 1)
|
||||||
const featuresToShow: Store<Feature[]> = state.indexedFeatures.featuresById.map(
|
const featuresToShow: Store<Feature[]> = state.indexedFeatures.featuresById.map(
|
||||||
(featuresById) => {
|
(featuresById) => {
|
||||||
|
if (featuresById === undefined) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
const properties = tagSource.data
|
const properties = tagSource.data
|
||||||
const features: Feature[] = []
|
const features: Feature[] = []
|
||||||
for (const key of keys) {
|
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 state: SpecialVisualizationState;
|
||||||
export let tags: UIEventSource<Record<string, string>>;
|
export let tags: UIEventSource<Record<string, string>>;
|
||||||
export let feature: Feature;
|
export let feature: Feature;
|
||||||
export let layer: LayerConfig
|
export let layer: LayerConfig;
|
||||||
|
|
||||||
let txt: string;
|
let txt: string;
|
||||||
onDestroy(Locale.language.addCallbackAndRunD(l => {
|
$: onDestroy(Locale.language.addCallbackAndRunD(l => {
|
||||||
txt = t.textFor(l);
|
txt = t.textFor(l);
|
||||||
}));
|
}));
|
||||||
let specs: RenderingSpecification[] = SpecialVisualizations.constructSpecification(txt);
|
let specs: RenderingSpecification[] = [];
|
||||||
|
$: {
|
||||||
|
if (txt !== undefined) {
|
||||||
|
specs = SpecialVisualizations.constructSpecification(txt);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each specs as specpart}
|
{#each specs as specpart}
|
||||||
{#if typeof specpart === "string"}
|
{#if typeof specpart === "string"}
|
||||||
<FromHtml src= {Utils.SubstituteKeys(specpart, $tags)}></FromHtml>
|
<FromHtml src={Utils.SubstituteKeys(specpart, $tags)}></FromHtml>
|
||||||
{:else if $tags !== undefined }
|
{:else if $tags !== undefined }
|
||||||
<ToSvelte construct={specpart.func.constr(state, tags, specpart.args, feature, layer)}></ToSvelte>
|
<ToSvelte construct={specpart.func.constr(state, tags, specpart.args, feature, layer)}></ToSvelte>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -17,6 +17,9 @@
|
||||||
export let state: SpecialVisualizationState;
|
export let state: SpecialVisualizationState;
|
||||||
export let selectedElement: Feature;
|
export let selectedElement: Feature;
|
||||||
export let config: TagRenderingConfig;
|
export let config: TagRenderingConfig;
|
||||||
|
if(config === undefined){
|
||||||
|
throw "Config is undefined in tagRenderingAnswer"
|
||||||
|
}
|
||||||
export let layer: LayerConfig
|
export let layer: LayerConfig
|
||||||
let trs: { then: Translation; icon?: string; iconClass?: string }[];
|
let trs: { then: Translation; icon?: string; iconClass?: string }[];
|
||||||
$: trs = Utils.NoNull(config?.GetRenderValues(_tags));
|
$: trs = Utils.NoNull(config?.GetRenderValues(_tags));
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||||
import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid";
|
import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||||
import SpecialTranslation from "./SpecialTranslation.svelte";
|
import SpecialTranslation from "./SpecialTranslation.svelte";
|
||||||
|
import TagHint from "../TagHint.svelte";
|
||||||
|
|
||||||
export let config: TagRenderingConfig;
|
export let config: TagRenderingConfig;
|
||||||
export let tags: UIEventSource<Record<string, string>>;
|
export let tags: UIEventSource<Record<string, string>>;
|
||||||
|
@ -87,7 +88,9 @@
|
||||||
<div class="border border-black subtle-background flex flex-col">
|
<div class="border border-black subtle-background flex flex-col">
|
||||||
<If condition={state.featureSwitchIsTesting}>
|
<If condition={state.featureSwitchIsTesting}>
|
||||||
<div class="flex justify-between">
|
<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>
|
<span class="alert">{config.id}</span>
|
||||||
</div>
|
</div>
|
||||||
<SpecialTranslation slot="else" t={config.question} {tags} {state} {layer} feature={selectedElement}></SpecialTranslation>
|
<SpecialTranslation slot="else" t={config.question} {tags} {state} {layer} feature={selectedElement}></SpecialTranslation>
|
||||||
|
@ -149,8 +152,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<FromHtml src={selectedTags?.asHumanString(true, true, {})} />
|
<TagHint osmConnection={state.osmConnection} tags={selectedTags}></TagHint>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<!-- TagRenderingQuestion-buttons -->
|
<!-- TagRenderingQuestion-buttons -->
|
||||||
<slot name="cancel"></slot>
|
<slot name="cancel"></slot>
|
||||||
|
|
|
@ -2,7 +2,11 @@ import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||||
import BaseUIElement from "./BaseUIElement"
|
import BaseUIElement from "./BaseUIElement"
|
||||||
import { DefaultGuiState } from "./DefaultGuiState"
|
import { DefaultGuiState } from "./DefaultGuiState"
|
||||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
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 { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||||
import { Changes } from "../Logic/Osm/Changes"
|
import { Changes } from "../Logic/Osm/Changes"
|
||||||
import { MapProperties } from "../Models/MapProperties"
|
import { MapProperties } from "../Models/MapProperties"
|
||||||
|
@ -13,12 +17,14 @@ import { MangroveIdentity } from "../Logic/Web/MangroveReviews"
|
||||||
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
|
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
|
||||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
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.
|
* The state needed to render a special Visualisation.
|
||||||
*/
|
*/
|
||||||
export interface SpecialVisualizationState {
|
export interface SpecialVisualizationState {
|
||||||
readonly guistate: DefaultGuiState
|
readonly guistate: MenuState
|
||||||
readonly layout: LayoutConfig
|
readonly layout: LayoutConfig
|
||||||
readonly featureSwitches: FeatureSwitchState
|
readonly featureSwitches: FeatureSwitchState
|
||||||
|
|
||||||
|
@ -27,6 +33,12 @@ export interface SpecialVisualizationState {
|
||||||
|
|
||||||
readonly indexedFeatures: IndexedFeatureSource
|
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 historicalUserLocations: WritableFeatureSource
|
||||||
|
|
||||||
readonly osmConnection: OsmConnection
|
readonly osmConnection: OsmConnection
|
||||||
|
@ -39,6 +51,10 @@ export interface SpecialVisualizationState {
|
||||||
readonly mapProperties: MapProperties
|
readonly mapProperties: MapProperties
|
||||||
|
|
||||||
readonly selectedElement: UIEventSource<Feature>
|
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
|
* If data is currently being fetched from external sources
|
||||||
|
@ -54,6 +70,7 @@ export interface SpecialVisualizationState {
|
||||||
readonly mangroveIdentity: MangroveIdentity
|
readonly mangroveIdentity: MangroveIdentity
|
||||||
readonly showAllQuestionsAtOnce: UIEventSource<boolean>
|
readonly showAllQuestionsAtOnce: UIEventSource<boolean>
|
||||||
}
|
}
|
||||||
|
readonly lastClickObject: WritableFeatureSource
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpecialVisualization {
|
export interface SpecialVisualization {
|
||||||
|
|
|
@ -57,6 +57,11 @@ import SvelteUIElement from "./Base/SvelteUIElement"
|
||||||
import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
|
import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
|
||||||
import QuestionViz from "./Popup/QuestionViz"
|
import QuestionViz from "./Popup/QuestionViz"
|
||||||
import SimpleAddUI from "./BigComponents/SimpleAddUI"
|
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 {
|
export default class SpecialVisualizations {
|
||||||
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
|
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
|
||||||
|
@ -84,7 +89,10 @@ export default class SpecialVisualizations {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (template["type"] !== undefined) {
|
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"
|
throw "Got a non-expanded template while constructing the specification"
|
||||||
}
|
}
|
||||||
const allKnownSpecials = extraMappings.concat(SpecialVisualizations.specialVisualizations)
|
const allKnownSpecials = extraMappings.concat(SpecialVisualizations.specialVisualizations)
|
||||||
|
@ -230,6 +238,26 @@ export default class SpecialVisualizations {
|
||||||
]).SetClass("flex flex-col")
|
]).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[] {
|
private static initList(): SpecialVisualization[] {
|
||||||
const specialVisualizations: SpecialVisualization[] = [
|
const specialVisualizations: SpecialVisualization[] = [
|
||||||
new QuestionViz(),
|
new QuestionViz(),
|
||||||
|
@ -237,11 +265,14 @@ export default class SpecialVisualizations {
|
||||||
funcName: "add_new_point",
|
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`",
|
docs: "An element which allows to add a new point on the 'last_click'-location. Only makes sense in the layer `last_click`",
|
||||||
args: [],
|
args: [],
|
||||||
constr(state: SpecialVisualizationState): BaseUIElement {
|
constr(state: SpecialVisualizationState, _, __, feature): BaseUIElement {
|
||||||
return new SimpleAddUI(state)
|
let [lon, lat] = GeoOperations.centerpointCoordinates(feature)
|
||||||
|
return new SvelteUIElement(AddNewPoint, {
|
||||||
|
state,
|
||||||
|
coordinate: { lon, lat },
|
||||||
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
new HistogramViz(),
|
new HistogramViz(),
|
||||||
new StealViz(),
|
new StealViz(),
|
||||||
new MinimapViz(),
|
new MinimapViz(),
|
||||||
|
@ -250,6 +281,20 @@ export default class SpecialVisualizations {
|
||||||
new MultiApplyViz(),
|
new MultiApplyViz(),
|
||||||
new ExportAsGpxViz(),
|
new ExportAsGpxViz(),
|
||||||
new AddNoteCommentViz(),
|
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 CloseNoteButton(),
|
||||||
new PlantNetDetectionViz(),
|
new PlantNetDetectionViz(),
|
||||||
|
|
||||||
|
@ -680,9 +725,7 @@ export default class SpecialVisualizations {
|
||||||
if (title === undefined) {
|
if (title === undefined) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return new SubstitutedTranslation(title, tagsSource, state).RemoveClass(
|
return new SubstitutedTranslation(title, tagsSource, state)
|
||||||
"w-full"
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -960,24 +1003,4 @@ export default class SpecialVisualizations {
|
||||||
|
|
||||||
return 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">
|
<script lang="ts">
|
||||||
import { ImmutableStore, Store, UIEventSource } from "../Logic/UIEventSource";
|
import { Store, UIEventSource } from "../Logic/UIEventSource";
|
||||||
import { Map as MlMap } from "maplibre-gl";
|
import { Map as MlMap } from "maplibre-gl";
|
||||||
import MaplibreMap from "./Map/MaplibreMap.svelte";
|
import MaplibreMap from "./Map/MaplibreMap.svelte";
|
||||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
|
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
|
||||||
|
@ -19,10 +19,14 @@
|
||||||
import Geosearch from "./BigComponents/Geosearch.svelte";
|
import Geosearch from "./BigComponents/Geosearch.svelte";
|
||||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui";
|
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui";
|
||||||
import Translations from "./i18n/Translations";
|
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 Tr from "./Base/Tr.svelte";
|
||||||
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte";
|
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte";
|
||||||
import FloatOver from "./Base/FloatOver.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;
|
export let layout: LayoutConfig;
|
||||||
const state = new ThemeViewState(layout);
|
const state = new ThemeViewState(layout);
|
||||||
|
@ -47,8 +51,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute top-0 left-0 mt-2 ml-2">
|
<div class="absolute top-0 left-0 mt-2 ml-2">
|
||||||
<MapControlButton on:click={() => state.guistate.welcomeMessageIsOpened.setData(true)}>
|
<MapControlButton on:click={() => state.guistate.themeIsOpened.setData(true)}>
|
||||||
<div class="flex mr-2 items-center">
|
<div class="flex mr-2 items-center cursor-pointer">
|
||||||
<img class="w-8 h-8 block mr-2" src={layout.icon}>
|
<img class="w-8 h-8 block mr-2" src={layout.icon}>
|
||||||
<b>
|
<b>
|
||||||
<Tr t={layout.title}></Tr>
|
<Tr t={layout.title}></Tr>
|
||||||
|
@ -56,7 +60,7 @@
|
||||||
</div>
|
</div>
|
||||||
</MapControlButton>
|
</MapControlButton>
|
||||||
<MapControlButton on:click={() =>state.guistate.menuIsOpened.setData(true)}>
|
<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>
|
</MapControlButton>
|
||||||
<If condition={state.featureSwitchIsTesting}>
|
<If condition={state.featureSwitchIsTesting}>
|
||||||
<span class="alert">
|
<span class="alert">
|
||||||
|
@ -86,107 +90,118 @@
|
||||||
|
|
||||||
<div class="absolute top-0 right-0 mt-4 mr-4">
|
<div class="absolute top-0 right-0 mt-4 mr-4">
|
||||||
<If condition={state.featureSwitches.featureSwitchSearch}>
|
<If condition={state.featureSwitches.featureSwitchSearch}>
|
||||||
<Geosearch bounds={state.mapProperties.bounds} {selectedElement} {selectedLayer}></Geosearch>
|
<Geosearch bounds={state.mapProperties.bounds} {selectedElement} {selectedLayer} {state}></Geosearch>
|
||||||
</If>
|
</If>
|
||||||
</div>
|
</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}
|
{#if $selectedElement !== undefined && $selectedLayer !== undefined}
|
||||||
<FloatOver>
|
<FloatOver on:close={() => {selectedElement.setData(undefined)}}>
|
||||||
<SelectedElementView layer={$selectedLayer} selectedElement={$selectedElement}
|
<SelectedElementView layer={$selectedLayer} selectedElement={$selectedElement}
|
||||||
tags={$selectedElementTags} state={state}></SelectedElementView>
|
tags={$selectedElementTags} state={state}></SelectedElementView>
|
||||||
</FloatOver>
|
</FloatOver>
|
||||||
|
|
||||||
{/if}
|
{/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>
|
<style>
|
||||||
/* WARNING: This is just for demonstration.
|
/* WARNING: This is just for demonstration.
|
||||||
Using :global() in this way can be risky. */
|
Using :global() in this way can be risky. */
|
||||||
|
|
|
@ -37,14 +37,17 @@
|
||||||
"tagRenderings": [
|
"tagRenderings": [
|
||||||
{
|
{
|
||||||
"id": "add_new",
|
"id": "add_new",
|
||||||
"mappings": [
|
"condition": "has_presets=yes",
|
||||||
{
|
"render": {
|
||||||
"if": "has_presets=yes",
|
"*": "{add_new_point()}"
|
||||||
"then": {
|
}
|
||||||
"*": "{add_new_point()}"
|
},
|
||||||
}
|
{
|
||||||
}
|
"id": "add_note",
|
||||||
]
|
"condition": "has_note_layer=yes",
|
||||||
|
"render": {
|
||||||
|
"*": "{open_note()}"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"all_tags"
|
"all_tags"
|
||||||
],
|
],
|
||||||
|
@ -52,6 +55,15 @@
|
||||||
{
|
{
|
||||||
"icon": {
|
"icon": {
|
||||||
"mappings": [
|
"mappings": [
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"and": [
|
||||||
|
"has_note_layer=yes",
|
||||||
|
"has_presets=no"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"then": "./assets/svg/note.svg"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"if": "number_of_presets=1",
|
"if": "number_of_presets=1",
|
||||||
"then": "{first_preset}"
|
"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>"
|
"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": {
|
"label": {
|
||||||
"render": {
|
"render": {
|
||||||
"ca": "Afegir nou element",
|
"ca": "Afegir nou element",
|
||||||
|
@ -77,7 +90,21 @@
|
||||||
"nl": "Klik hier om een item toe te voegen",
|
"nl": "Klik hier om een item toe te voegen",
|
||||||
"pt": "Adicionar novo item",
|
"pt": "Adicionar novo item",
|
||||||
"zh_Hant": "點這邊新增新項目"
|
"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": [
|
"iconBadges": [
|
||||||
{
|
{
|
||||||
|
@ -93,7 +120,20 @@
|
||||||
"location": [
|
"location": [
|
||||||
"point"
|
"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": [
|
"filter": [
|
||||||
|
@ -113,4 +153,4 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -6,10 +6,7 @@
|
||||||
"de": "Hebt das aktuell ausgewählte Element hervor. Überschreiben Sie diese Ebene, um unterschiedliche Farben zu erhalten",
|
"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."
|
"fr": "Met en surbrillance l'élément actuellement sélectioné. Surcharger cette couche pour avoir d'autres couleurs."
|
||||||
},
|
},
|
||||||
"source": {
|
"source": "special",
|
||||||
"osmTags": "selected=yes",
|
|
||||||
"maxCacheAge": 0
|
|
||||||
},
|
|
||||||
"mapRendering": [
|
"mapRendering": [
|
||||||
{
|
{
|
||||||
"icon": "circle:red",
|
"icon": "circle:red",
|
||||||
|
@ -22,4 +19,4 @@
|
||||||
"cssClasses": "block relative rounded-full"
|
"cssClasses": "block relative rounded-full"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -2016,4 +2016,4 @@
|
||||||
"pl": "Nazwa sieci to <b>{internet_access:ssid}</b>"
|
"pl": "Nazwa sieci to <b>{internet_access:ssid}</b>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -750,6 +750,14 @@ video {
|
||||||
right: 33.333333%;
|
right: 33.333333%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.right-10 {
|
||||||
|
right: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-10 {
|
||||||
|
top: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.top-4 {
|
.top-4 {
|
||||||
top: 1rem;
|
top: 1rem;
|
||||||
}
|
}
|
||||||
|
@ -794,10 +802,6 @@ video {
|
||||||
margin: 1.25rem;
|
margin: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.m-2 {
|
|
||||||
margin: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.m-0\.5 {
|
.m-0\.5 {
|
||||||
margin: 0.125rem;
|
margin: 0.125rem;
|
||||||
}
|
}
|
||||||
|
@ -810,6 +814,10 @@ video {
|
||||||
margin: 0.75rem;
|
margin: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.m-2 {
|
||||||
|
margin: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.m-4 {
|
.m-4 {
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
}
|
}
|
||||||
|
@ -903,18 +911,6 @@ video {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mt-12 {
|
|
||||||
margin-top: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ml-3 {
|
|
||||||
margin-left: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ml-12 {
|
|
||||||
margin-left: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-3 {
|
.mt-3 {
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
}
|
}
|
||||||
|
@ -935,6 +931,10 @@ video {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ml-3 {
|
||||||
|
margin-left: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.mb-8 {
|
.mb-8 {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
@ -1047,6 +1047,10 @@ video {
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h-6 {
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.h-1\/2 {
|
.h-1\/2 {
|
||||||
height: 50%;
|
height: 50%;
|
||||||
}
|
}
|
||||||
|
@ -1055,10 +1059,6 @@ video {
|
||||||
height: 0.75rem;
|
height: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-6 {
|
|
||||||
height: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-11 {
|
.h-11 {
|
||||||
height: 2.75rem;
|
height: 2.75rem;
|
||||||
}
|
}
|
||||||
|
@ -1095,6 +1095,10 @@ video {
|
||||||
max-height: 2rem;
|
max-height: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.max-h-24 {
|
||||||
|
max-height: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
.min-h-\[8rem\] {
|
.min-h-\[8rem\] {
|
||||||
min-height: 8rem;
|
min-height: 8rem;
|
||||||
}
|
}
|
||||||
|
@ -1175,17 +1179,12 @@ video {
|
||||||
width: 6rem;
|
width: 6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-auto {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-48 {
|
.w-48 {
|
||||||
width: 12rem;
|
width: 12rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.min-w-min {
|
.w-auto {
|
||||||
min-width: -webkit-min-content;
|
width: auto;
|
||||||
min-width: min-content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.max-w-full {
|
.max-w-full {
|
||||||
|
@ -1378,10 +1377,6 @@ video {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.whitespace-nowrap {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.break-normal {
|
.break-normal {
|
||||||
overflow-wrap: normal;
|
overflow-wrap: normal;
|
||||||
word-break: normal;
|
word-break: normal;
|
||||||
|
@ -1432,10 +1427,6 @@ video {
|
||||||
border-bottom-left-radius: 0.25rem;
|
border-bottom-left-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-2 {
|
|
||||||
border-width: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.border {
|
.border {
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
}
|
}
|
||||||
|
@ -1444,6 +1435,10 @@ video {
|
||||||
border-width: 4px;
|
border-width: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.border-2 {
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.border-l-4 {
|
.border-l-4 {
|
||||||
border-left-width: 4px;
|
border-left-width: 4px;
|
||||||
}
|
}
|
||||||
|
@ -1533,11 +1528,6 @@ video {
|
||||||
background-color: rgb(219 234 254 / var(--tw-bg-opacity));
|
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 {
|
.bg-black {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||||
|
@ -1611,11 +1601,6 @@ video {
|
||||||
padding-right: 0.5rem;
|
padding-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.px-0 {
|
|
||||||
padding-left: 0px;
|
|
||||||
padding-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.px-4 {
|
.px-4 {
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
|
@ -1653,22 +1638,6 @@ video {
|
||||||
padding-bottom: 0.25rem;
|
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 {
|
.pr-2 {
|
||||||
padding-right: 0.5rem;
|
padding-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
@ -1677,6 +1646,10 @@ video {
|
||||||
padding-top: 0.125rem;
|
padding-top: 0.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pt-0 {
|
||||||
|
padding-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.pb-8 {
|
.pb-8 {
|
||||||
padding-bottom: 2rem;
|
padding-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
@ -1693,14 +1666,26 @@ video {
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pl-1 {
|
||||||
|
padding-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.pr-0 {
|
.pr-0 {
|
||||||
padding-right: 0px;
|
padding-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pr-1 {
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.pb-4 {
|
.pb-4 {
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pb-2 {
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.pl-6 {
|
.pl-6 {
|
||||||
padding-left: 1.5rem;
|
padding-left: 1.5rem;
|
||||||
}
|
}
|
||||||
|
@ -1793,11 +1778,6 @@ video {
|
||||||
letter-spacing: -0.025em;
|
letter-spacing: -0.025em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-white {
|
|
||||||
--tw-text-opacity: 1;
|
|
||||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-gray-900 {
|
.text-gray-900 {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(17 24 39 / var(--tw-text-opacity));
|
color: rgb(17 24 39 / var(--tw-text-opacity));
|
||||||
|
@ -1823,6 +1803,11 @@ video {
|
||||||
color: rgb(153 153 153 / var(--tw-text-opacity));
|
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 {
|
.underline {
|
||||||
text-decoration-line: 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);
|
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 {
|
.grayscale {
|
||||||
--tw-grayscale: grayscale(100%);
|
--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);
|
-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": {
|
"general": {
|
||||||
"about": "Easily edit and add OpenStreetMap for a certain theme",
|
"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>",
|
"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": {
|
"add": {
|
||||||
"addNew": "Add {category}",
|
"addNew": "Add {category}",
|
||||||
"backToSelect": "Select a different 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.",
|
"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",
|
"disableFilters": "Disable all filters",
|
||||||
"disableFiltersExplanation": "Some features might be hidden by a filter",
|
"disableFiltersExplanation": "Some features might be hidden by a filter",
|
||||||
|
"enableLayer": "Enable layer {name}",
|
||||||
"hasBeenImported": "This feature has already been imported",
|
"hasBeenImported": "This feature has already been imported",
|
||||||
"import": {
|
"import": {
|
||||||
"hasBeenImported": "This object has been imported",
|
"hasBeenImported": "This object has been imported",
|
||||||
|
|
|
@ -1994,6 +1994,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"last_click": {
|
||||||
|
"mapRendering": {
|
||||||
|
"0": {
|
||||||
|
"label": {
|
||||||
|
"render": "Afegir nou element"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"map": {
|
"map": {
|
||||||
"name": "Mapes",
|
"name": "Mapes",
|
||||||
"presets": {
|
"presets": {
|
||||||
|
|
|
@ -946,6 +946,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"last_click": {
|
||||||
|
"mapRendering": {
|
||||||
|
"0": {
|
||||||
|
"label": {
|
||||||
|
"render": "Klikněte zde pro přidání nové položky"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"usersettings": {
|
"usersettings": {
|
||||||
"tagRenderings": {
|
"tagRenderings": {
|
||||||
"picture-license": {
|
"picture-license": {
|
||||||
|
|
|
@ -2065,6 +2065,15 @@
|
||||||
"gps_track": {
|
"gps_track": {
|
||||||
"name": "Dit tilbagelagte spor"
|
"name": "Dit tilbagelagte spor"
|
||||||
},
|
},
|
||||||
|
"last_click": {
|
||||||
|
"mapRendering": {
|
||||||
|
"0": {
|
||||||
|
"label": {
|
||||||
|
"render": "Klik her for at tilføje et nyt punkt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"recycling": {
|
"recycling": {
|
||||||
"filter": {
|
"filter": {
|
||||||
"2": {
|
"2": {
|
||||||
|
|
|
@ -5206,6 +5206,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"last_click": {
|
||||||
|
"mapRendering": {
|
||||||
|
"0": {
|
||||||
|
"label": {
|
||||||
|
"render": "Hier klicken, um ein neues Element hinzuzufügen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"map": {
|
"map": {
|
||||||
"description": "Eine Karte, die für Touristen gedacht ist und dauerhaft im öffentlichen Raum aufgestellt ist",
|
"description": "Eine Karte, die für Touristen gedacht ist und dauerhaft im öffentlichen Raum aufgestellt ist",
|
||||||
"name": "Karten",
|
"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": {
|
"map": {
|
||||||
"description": "A map, meant for tourists which is permanently installed in the public space",
|
"description": "A map, meant for tourists which is permanently installed in the public space",
|
||||||
"name": "Maps",
|
"name": "Maps",
|
||||||
|
|
|
@ -2681,6 +2681,15 @@
|
||||||
"render": "Panel informativo"
|
"render": "Panel informativo"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"last_click": {
|
||||||
|
"mapRendering": {
|
||||||
|
"0": {
|
||||||
|
"label": {
|
||||||
|
"render": "Haga clic aquí para añadir un nuevo ítem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"map": {
|
"map": {
|
||||||
"description": "Un mapa, pensado para turistas y que está instalado de manera permanente en un espacio público",
|
"description": "Un mapa, pensado para turistas y que está instalado de manera permanente en un espacio público",
|
||||||
"name": "Mapas",
|
"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": {
|
"map": {
|
||||||
"description": "Une carte, destinée aux touristes, installée en permanence dans l'espace public",
|
"description": "Une carte, destinée aux touristes, installée en permanence dans l'espace public",
|
||||||
"name": "Cartes",
|
"name": "Cartes",
|
||||||
|
|
|
@ -614,6 +614,15 @@
|
||||||
"render": "Hackerspace"
|
"render": "Hackerspace"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"last_click": {
|
||||||
|
"mapRendering": {
|
||||||
|
"0": {
|
||||||
|
"label": {
|
||||||
|
"render": "Új elem hozzáadásához kattints ide"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"postboxes": {
|
"postboxes": {
|
||||||
"description": "Postaládákat megjelenítő réteg.",
|
"description": "Postaládákat megjelenítő réteg.",
|
||||||
"name": "Postaládák",
|
"name": "Postaládák",
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue