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