refactoring: fix basic flow to add a new point

This commit is contained in:
Pieter Vander Vennet 2023-04-06 01:33:08 +02:00
parent 52a0810ea9
commit 0241f89d3d
109 changed files with 1931 additions and 1446 deletions

View file

@ -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"

View file

@ -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()

View file

@ -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,
})

View file

@ -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)

View file

@ -1,4 +1,4 @@
import FeatureSource, { IndexedFeatureSource } from "../FeatureSource"
import { FeatureSource, IndexedFeatureSource } from "../FeatureSource"
import { UIEventSource } from "../../UIEventSource"
/**

View file

@ -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"

View file

@ -1,4 +1,4 @@
import FeatureSource from "../FeatureSource"
import { FeatureSource } from "../FeatureSource"
import { Feature } from "geojson"
import TileLocalStorage from "./TileLocalStorage"
import { GeoOperations } from "../../GeoOperations"

View file

@ -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 {

View file

@ -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) {

View file

@ -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"

View file

@ -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"

View file

@ -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 ?? []) {

View file

@ -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"

View file

@ -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])
})
}
}

View file

@ -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) {

View file

@ -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"

View file

@ -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"

View file

@ -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]
)

View file

@ -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"

View file

@ -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"

View file

@ -81,6 +81,7 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
return new GeoJsonSource(layer, {
zxy,
featureIdBlacklist: blackList,
isActive: options?.isActive,
})
},
mapProperties,

View file

@ -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(),

View file

@ -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"

View file

@ -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],
},
}
}
}

View file

@ -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

View file

@ -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][]

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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
}

View file

@ -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(

View file

@ -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)
}
}

View file

@ -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"

View file

@ -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"
/**

View file

@ -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

View file

@ -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 =

View file

@ -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))
}
/**

View file

@ -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
View 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)
}
}

View file

@ -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) {

View file

@ -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",

View file

@ -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) {

View file

@ -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>

View file

@ -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>

View 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>

View 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}

View file

@ -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>

View file

@ -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}

View file

@ -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
}

View 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>

View file

@ -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")
}
}

View file

@ -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")
}
}

View file

@ -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
}
}

View file

@ -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}

View 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>

View file

@ -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
}

View file

@ -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)

View file

@ -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")
}

View 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>

View file

@ -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
}

View file

@ -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")
}
}

View file

@ -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")
}
}

View file

@ -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),

View file

@ -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,
])

View file

@ -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")

View file

@ -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>

View file

@ -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}

View file

@ -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) {

View file

@ -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) => {

View file

@ -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"

View file

@ -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")

View 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>

View 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>

View 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}

View file

@ -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

View file

@ -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"

View file

@ -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) {

View file

@ -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
View 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}

View file

@ -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}

View file

@ -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));

View file

@ -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>

View file

@ -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 {

View file

@ -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
View 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>

View file

@ -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. */

View file

@ -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 @@
]
}
]
}
}

View file

@ -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"
}
]
}
}

View file

@ -2016,4 +2016,4 @@
"pl": "Nazwa sieci to <b>{internet_access:ssid}</b>"
}
}
}
}

View file

@ -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);

View file

@ -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",

View file

@ -1994,6 +1994,15 @@
}
}
},
"last_click": {
"mapRendering": {
"0": {
"label": {
"render": "Afegir nou element"
}
}
}
},
"map": {
"name": "Mapes",
"presets": {

View file

@ -946,6 +946,15 @@
}
}
},
"last_click": {
"mapRendering": {
"0": {
"label": {
"render": "Klikněte zde pro přidání nové položky"
}
}
}
},
"usersettings": {
"tagRenderings": {
"picture-license": {

View file

@ -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": {

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -1 +1,11 @@
{}
{
"last_click": {
"mapRendering": {
"0": {
"label": {
"render": "I-click ito para mag-dagdag ng bagong bagay"
}
}
}
}
}

View file

@ -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",

View file

@ -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