Chore: reformat all files with prettier

This commit is contained in:
Pieter Vander Vennet 2023-06-14 20:39:36 +02:00
parent 5757ae5dea
commit d008dcb54d
214 changed files with 8926 additions and 8196 deletions

View file

@ -1,6 +1,6 @@
import {Store, UIEventSource} from "../UIEventSource" import { Store, UIEventSource } from "../UIEventSource"
import {Utils} from "../../Utils" import { Utils } from "../../Utils"
import {RasterLayerPolygon, RasterLayerUtils,} from "../../Models/RasterLayers" import { RasterLayerPolygon, RasterLayerUtils } from "../../Models/RasterLayers"
/** /**
* When a user pans around on the map, they might pan out of the range of the current background raster layer. * When a user pans around on the map, they might pan out of the range of the current background raster layer.
@ -35,7 +35,7 @@ export default class BackgroundLayerResetter {
availableLayers, availableLayers,
currentBgPolygon?.properties?.category currentBgPolygon?.properties?.category
) )
if(!availableInSameCat){ if (!availableInSameCat) {
return return
} }
console.log("Selecting a different layer:", availableInSameCat.properties.id) console.log("Selecting a different layer:", availableInSameCat.properties.id)

View file

@ -1,15 +1,15 @@
import {QueryParameters} from "../Web/QueryParameters" import { QueryParameters } from "../Web/QueryParameters"
import {BBox} from "../BBox" import { BBox } from "../BBox"
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants"
import {GeoLocationState} from "../State/GeoLocationState" import { GeoLocationState } from "../State/GeoLocationState"
import {UIEventSource} from "../UIEventSource" import { UIEventSource } from "../UIEventSource"
import {Feature, LineString, Point} from "geojson" import { Feature, LineString, Point } from "geojson"
import {FeatureSource, WritableFeatureSource} from "../FeatureSource/FeatureSource" import { FeatureSource, WritableFeatureSource } from "../FeatureSource/FeatureSource"
import {LocalStorageSource} from "../Web/LocalStorageSource" import { LocalStorageSource } from "../Web/LocalStorageSource"
import {GeoOperations} from "../GeoOperations" import { GeoOperations } from "../GeoOperations"
import {OsmTags} from "../../Models/OsmFeature" import { OsmTags } from "../../Models/OsmFeature"
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource" import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
import {MapProperties} from "../../Models/MapProperties" import { MapProperties } from "../../Models/MapProperties"
/** /**
* The geolocation-handler takes a map-location and a geolocation state. * The geolocation-handler takes a map-location and a geolocation state.
@ -39,7 +39,9 @@ export default class GeoLocationHandler {
/** /**
* The last moment that the map has moved * The last moment that the map has moved
*/ */
public readonly mapHasMoved: UIEventSource<Date | undefined> = new UIEventSource<Date | undefined>(undefined) public readonly mapHasMoved: UIEventSource<Date | undefined> = new UIEventSource<
Date | undefined
>(undefined)
private readonly selectedElement: UIEventSource<any> private readonly selectedElement: UIEventSource<any>
private readonly mapProperties?: MapProperties private readonly mapProperties?: MapProperties
private readonly gpsLocationHistoryRetentionTime?: UIEventSource<number> private readonly gpsLocationHistoryRetentionTime?: UIEventSource<number>
@ -80,8 +82,11 @@ export default class GeoLocationHandler {
// The map hasn't moved yet; we received our first coordinates, so let's move there! // The map hasn't moved yet; we received our first coordinates, so let's move there!
self.MoveMapToCurrentLocation() self.MoveMapToCurrentLocation()
} }
if (timeSinceLastRequest < Constants.zoomToLocationTimeout && if (
(this.mapHasMoved.data === undefined || this.mapHasMoved.data.getTime() < geolocationState.requestMoment.data?.getTime() ) timeSinceLastRequest < Constants.zoomToLocationTimeout &&
(this.mapHasMoved.data === undefined ||
this.mapHasMoved.data.getTime() <
geolocationState.requestMoment.data?.getTime())
) { ) {
// still within request time and the map hasn't moved since requesting to jump to the current location // still within request time and the map hasn't moved since requesting to jump to the current location
self.MoveMapToCurrentLocation() self.MoveMapToCurrentLocation()
@ -154,8 +159,8 @@ export default class GeoLocationHandler {
return return
} }
const properties = { const properties = {
id: "gps-"+i, id: "gps-" + i,
"user:location": "yes", "user:location": "yes",
date: new Date().toISOString(), date: new Date().toISOString(),
} }
@ -164,7 +169,7 @@ export default class GeoLocationHandler {
for (const k in keysToCopy) { for (const k in keysToCopy) {
// For some weird reason, the 'Object.keys' method doesn't work for the 'location: GeolocationCoordinates'-object and will thus not copy all the properties when using {...location} // For some weird reason, the 'Object.keys' method doesn't work for the 'location: GeolocationCoordinates'-object and will thus not copy all the properties when using {...location}
// As such, they are copied here // As such, they are copied here
if(location[k]){ if (location[k]) {
properties[k] = location[k] properties[k] = location[k]
} }
} }

View file

@ -20,14 +20,15 @@ import { FixImages } from "../Models/ThemeConfig/Conversion/FixImages"
import Svg from "../Svg" import Svg from "../Svg"
import { import {
DoesImageExist, DoesImageExist,
PrevalidateTheme, ValidateTagRenderings, PrevalidateTheme,
ValidateTagRenderings,
ValidateThemeAndLayers, ValidateThemeAndLayers,
} from "../Models/ThemeConfig/Conversion/Validation" } from "../Models/ThemeConfig/Conversion/Validation"
import {DesugaringContext, Each, On} from "../Models/ThemeConfig/Conversion/Conversion"; import { DesugaringContext, Each, On } from "../Models/ThemeConfig/Conversion/Conversion"
import {PrepareLayer, RewriteSpecial} from "../Models/ThemeConfig/Conversion/PrepareLayer"; import { PrepareLayer, RewriteSpecial } from "../Models/ThemeConfig/Conversion/PrepareLayer"
import {AllSharedLayers} from "../Customizations/AllSharedLayers"; import { AllSharedLayers } from "../Customizations/AllSharedLayers"
import {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingConfigJson"; import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson"
import questions from "../assets/tagRenderings/questions.json"; import questions from "../assets/tagRenderings/questions.json"
export default class DetermineLayout { export default class DetermineLayout {
private static readonly _knownImages = new Set(Array.from(licenses).map((l) => l.path)) private static readonly _knownImages = new Set(Array.from(licenses).map((l) => l.path))

View file

@ -1,11 +1,11 @@
import {GeoOperations} from "./GeoOperations" import { GeoOperations } from "./GeoOperations"
import Combine from "../UI/Base/Combine" import Combine from "../UI/Base/Combine"
import BaseUIElement from "../UI/BaseUIElement" import BaseUIElement from "../UI/BaseUIElement"
import List from "../UI/Base/List" import List from "../UI/Base/List"
import Title from "../UI/Base/Title" import Title from "../UI/Base/Title"
import {BBox} from "./BBox" import { BBox } from "./BBox"
import {Feature, Geometry, MultiPolygon, Polygon} from "geojson" import { Feature, Geometry, MultiPolygon, Polygon } from "geojson"
import {GeoJSONFeature} from "maplibre-gl"; import { GeoJSONFeature } from "maplibre-gl"
export interface ExtraFuncParams { export interface ExtraFuncParams {
/** /**
@ -13,7 +13,10 @@ export interface ExtraFuncParams {
* Note that more features then requested can be given back. * Note that more features then requested can be given back.
* Format: [ [ geojson, geojson, geojson, ... ], [geojson, ...], ...] * Format: [ [ geojson, geojson, geojson, ... ], [geojson, ...], ...]
*/ */
getFeaturesWithin: (layerId: string, bbox: BBox) => Feature<Geometry, Record<string, string>>[][] getFeaturesWithin: (
layerId: string,
bbox: BBox
) => Feature<Geometry, Record<string, string>>[][]
getFeatureById: (id: string) => Feature<Geometry, Record<string, string>> getFeatureById: (id: string) => Feature<Geometry, Record<string, string>>
} }
@ -55,7 +58,6 @@ class EnclosingFunc implements ExtraFunction {
} }
for (const otherFeatures of otherFeaturess) { for (const otherFeatures of otherFeaturess) {
for (const otherFeature of otherFeatures) { for (const otherFeature of otherFeatures) {
if (seenIds.has(otherFeature.properties.id)) { if (seenIds.has(otherFeature.properties.id)) {
continue continue
} }
@ -72,7 +74,7 @@ class EnclosingFunc implements ExtraFunction {
<Feature<Polygon | MultiPolygon, any>>otherFeature <Feature<Polygon | MultiPolygon, any>>otherFeature
) )
) { ) {
result.push({feat: otherFeature}) result.push({ feat: otherFeature })
} }
} }
} }
@ -158,11 +160,14 @@ class IntersectionFunc implements ExtraFunction {
} }
for (const otherFeatures of otherLayers) { for (const otherFeatures of otherLayers) {
for (const otherFeature of otherFeatures) { for (const otherFeature of otherFeatures) {
const intersections = GeoOperations.LineIntersections(feat, <Feature<any, Record<string, string>>>otherFeature) const intersections = GeoOperations.LineIntersections(
feat,
<Feature<any, Record<string, string>>>otherFeature
)
if (intersections.length === 0) { if (intersections.length === 0) {
continue continue
} }
result.push({feat: otherFeature, intersections}) result.push({ feat: otherFeature, intersections })
} }
} }
} }
@ -254,7 +259,14 @@ class ClosestNObjectFunc implements ExtraFunction {
const maxDistance = options?.maxDistance ?? 500 const maxDistance = options?.maxDistance ?? 500
const uniqueTag: string | undefined = options?.uniqueTag const uniqueTag: string | undefined = options?.uniqueTag
let allFeatures: Feature[][] let allFeatures: Feature[][]
console.log("Calculating closest", options?.maxFeatures, "features around", feature, "in layer", features) console.log(
"Calculating closest",
options?.maxFeatures,
"features around",
feature,
"in layer",
features
)
if (typeof features === "string") { if (typeof features === "string") {
const name = features const name = features
const bbox = GeoOperations.bbox( const bbox = GeoOperations.bbox(
@ -272,7 +284,6 @@ class ClosestNObjectFunc implements ExtraFunction {
let closestFeatures: { feat: any; distance: number }[] = [] let closestFeatures: { feat: any; distance: number }[] = []
for (const feats of allFeatures) { for (const feats of allFeatures) {
for (const otherFeature of feats) { for (const otherFeature of feats) {
if ( if (
otherFeature === feature || otherFeature === feature ||
@ -333,7 +344,7 @@ class ClosestNObjectFunc implements ExtraFunction {
const uniqueTagsMatch = const uniqueTagsMatch =
otherFeature.properties[uniqueTag] !== undefined && otherFeature.properties[uniqueTag] !== undefined &&
closestFeature.feat.properties[uniqueTag] === closestFeature.feat.properties[uniqueTag] ===
otherFeature.properties[uniqueTag] otherFeature.properties[uniqueTag]
if (uniqueTagsMatch) { if (uniqueTagsMatch) {
targetIndex = -1 targetIndex = -1
if (closestFeature.distance > distance) { if (closestFeature.distance > distance) {
@ -341,7 +352,7 @@ class ClosestNObjectFunc implements ExtraFunction {
// We want to see the tag `uniquetag=some_value` only once in the entire list (e.g. to prevent road segements of identical names to fill up the list of 'names of nearby roads') // We want to see the tag `uniquetag=some_value` only once in the entire list (e.g. to prevent road segements of identical names to fill up the list of 'names of nearby roads')
// AT this point, we have found a closer segment with the same, identical tag // AT this point, we have found a closer segment with the same, identical tag
// so we replace directly // so we replace directly
closestFeatures[i] = {feat: otherFeature, distance: distance} closestFeatures[i] = { feat: otherFeature, distance: distance }
} }
break break
} }
@ -468,7 +479,15 @@ export class ExtraFunctions {
.SetClass("flex-col") .SetClass("flex-col")
.AsMarkdown() .AsMarkdown()
static readonly types = ["distanceTo", "overlapWith", "enclosingFeatures", "intersectionsWith", "closest", "closestn", "get"] as const static readonly types = [
"distanceTo",
"overlapWith",
"enclosingFeatures",
"intersectionsWith",
"closest",
"closestn",
"get",
] as const
private static readonly allFuncs = [ private static readonly allFuncs = [
new DistanceToFunc(), new DistanceToFunc(),
new OverlapFunc(), new OverlapFunc(),
@ -479,8 +498,9 @@ export class ExtraFunctions {
new GetParsed(), new GetParsed(),
] ]
public static constructHelpers(
public static constructHelpers(params: ExtraFuncParams): Record<ExtraFuncType, (feature: Feature) => Function> { params: ExtraFuncParams
): Record<ExtraFuncType, (feature: Feature) => Function> {
const record: Record<string, (feature: GeoJSONFeature) => Function> = {} const record: Record<string, (feature: GeoJSONFeature) => Function> = {}
for (const f of ExtraFunctions.allFuncs) { for (const f of ExtraFunctions.allFuncs) {
if (this.types.indexOf(<any>f._name) < 0) { if (this.types.indexOf(<any>f._name) < 0) {

View file

@ -1,5 +1,5 @@
import {FeatureSource} from "../FeatureSource" import { FeatureSource } from "../FeatureSource"
import {UIEventSource} from "../../UIEventSource" import { UIEventSource } from "../../UIEventSource"
/** /**
* Constructs a UIEventStore for the properties of every Feature, indexed by id * Constructs a UIEventStore for the properties of every Feature, indexed by id

View file

@ -1,5 +1,5 @@
import {IdbLocalStorage} from "../../Web/IdbLocalStorage" import { IdbLocalStorage } from "../../Web/IdbLocalStorage"
import {UIEventSource} from "../../UIEventSource" import { UIEventSource } from "../../UIEventSource"
/** /**
* A class which allows to read/write a tile to local storage. * A class which allows to read/write a tile to local storage.
@ -14,14 +14,18 @@ export default class TileLocalStorage<T> {
private readonly _layername: string private readonly _layername: string
private readonly inUse = new UIEventSource(false) private readonly inUse = new UIEventSource(false)
private readonly cachedSources: Record<number, UIEventSource<T> & { flush: () => void }> = {} private readonly cachedSources: Record<number, UIEventSource<T> & { flush: () => void }> = {}
private readonly _maxAgeSeconds: number; private readonly _maxAgeSeconds: number
private constructor(layername: string, maxAgeSeconds: number) { private constructor(layername: string, maxAgeSeconds: number) {
this._layername = layername this._layername = layername
this._maxAgeSeconds = maxAgeSeconds; this._maxAgeSeconds = maxAgeSeconds
} }
public static construct<T>(backend: string, layername: string, maxAgeS: number): TileLocalStorage<T> { public static construct<T>(
backend: string,
layername: string,
maxAgeS: number
): TileLocalStorage<T> {
const key = backend + "_" + layername const key = backend + "_" + layername
const cached = TileLocalStorage.perLayer[key] const cached = TileLocalStorage.perLayer[key]
if (cached) { if (cached) {
@ -59,7 +63,10 @@ export default class TileLocalStorage<T> {
await this.inUse.AsPromise((inUse) => !inUse) await this.inUse.AsPromise((inUse) => !inUse)
this.inUse.setData(true) this.inUse.setData(true)
await IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex, data) await IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex, data)
await IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex + "_date", Date.now()) await IdbLocalStorage.SetDirectly(
this._layername + "_" + tileIndex + "_date",
Date.now()
)
this.inUse.setData(false) this.inUse.setData(false)
} catch (e) { } catch (e) {
@ -80,7 +87,9 @@ export default class TileLocalStorage<T> {
if (!TileLocalStorage.useIndexedDb) { if (!TileLocalStorage.useIndexedDb) {
return undefined return undefined
} }
const date = <any>await IdbLocalStorage.GetDirectly(this._layername + "_" + tileIndex + "_date") const date = <any>(
await IdbLocalStorage.GetDirectly(this._layername + "_" + tileIndex + "_date")
)
const maxAge = this._maxAgeSeconds const maxAge = this._maxAgeSeconds
const timeDiff = Date.now() - date const timeDiff = Date.now() - date
if (timeDiff >= maxAge) { if (timeDiff >= maxAge) {

View file

@ -1,8 +1,8 @@
import {FeatureSource} from "./FeatureSource" import { FeatureSource } from "./FeatureSource"
import FilteredLayer from "../../Models/FilteredLayer" import FilteredLayer from "../../Models/FilteredLayer"
import SimpleFeatureSource from "./Sources/SimpleFeatureSource" import SimpleFeatureSource from "./Sources/SimpleFeatureSource"
import {Feature} from "geojson" import { Feature } from "geojson"
import {UIEventSource} from "../UIEventSource" import { UIEventSource } from "../UIEventSource"
/** /**
* In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled) * In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled)
@ -59,8 +59,11 @@ export default class PerLayerFeatureSourceSplitter<T extends FeatureSource = Fea
let foundALayer = false let foundALayer = false
for (let i = 0; i < layers.length; i++) { for (let i = 0; i < layers.length; i++) {
const layer = layers[i] const layer = layers[i]
if(!layer.layerDef?.source){ if (!layer.layerDef?.source) {
console.error("PerLayerFeatureSourceSplitter got a layer without a source:", layer.layerDef.id) console.error(
"PerLayerFeatureSourceSplitter got a layer without a source:",
layer.layerDef.id
)
continue continue
} }
if (layer.layerDef.source.osmTags.matchesProperties(f.properties)) { if (layer.layerDef.source.osmTags.matchesProperties(f.properties)) {

View file

@ -6,7 +6,7 @@ import { UIEventSource } from "../../UIEventSource"
import { FeatureSource, IndexedFeatureSource } from "../FeatureSource" import { FeatureSource, IndexedFeatureSource } from "../FeatureSource"
import { ChangeDescription, ChangeDescriptionTools } from "../../Osm/Actions/ChangeDescription" import { ChangeDescription, ChangeDescriptionTools } from "../../Osm/Actions/ChangeDescription"
import { Feature } from "geojson" import { Feature } from "geojson"
import {Utils} from "../../../Utils"; import { Utils } from "../../../Utils"
export default class ChangeGeometryApplicator implements FeatureSource { export default class ChangeGeometryApplicator implements FeatureSource {
public readonly features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([]) public readonly features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([])
@ -70,7 +70,7 @@ export default class ChangeGeometryApplicator implements FeatureSource {
// We only apply the last change as that one'll have the latest geometry // We only apply the last change as that one'll have the latest geometry
const change = changesForFeature[changesForFeature.length - 1] const change = changesForFeature[changesForFeature.length - 1]
copy.geometry = ChangeDescriptionTools.getGeojsonGeometry(change) copy.geometry = ChangeDescriptionTools.getGeojsonGeometry(change)
if(Utils.SameObject(copy.geometry, feature.geometry)){ if (Utils.SameObject(copy.geometry, feature.geometry)) {
// No actual changes: pass along the original // No actual changes: pass along the original
newFeatures.push(feature) newFeatures.push(feature)
continue continue

View file

@ -30,7 +30,7 @@ export default class FeatureSourceMerger implements IndexedFeatureSource {
} }
public addSource(source: FeatureSource) { public addSource(source: FeatureSource) {
if(!source){ if (!source) {
return return
} }
this._sources.push(source) this._sources.push(source)

View file

@ -1,14 +1,14 @@
/** /**
* Fetches a geojson file somewhere and passes it along * Fetches a geojson file somewhere and passes it along
*/ */
import {Store, UIEventSource} from "../../UIEventSource" import { Store, UIEventSource } from "../../UIEventSource"
import {Utils} from "../../../Utils" import { Utils } from "../../../Utils"
import {FeatureSource} from "../FeatureSource" import { FeatureSource } from "../FeatureSource"
import {BBox} from "../../BBox" import { BBox } from "../../BBox"
import {GeoOperations} from "../../GeoOperations" import { GeoOperations } from "../../GeoOperations"
import {Feature} from "geojson" import { Feature } from "geojson"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import {Tiles} from "../../../Models/TileRange" import { Tiles } from "../../../Models/TileRange"
export default class GeoJsonSource implements FeatureSource { export default class GeoJsonSource implements FeatureSource {
public readonly features: Store<Feature[]> public readonly features: Store<Feature[]>
@ -65,13 +65,13 @@ export default class GeoJsonSource implements FeatureSource {
return return
} }
this.LoadJSONFrom(url, eventsource, layer) this.LoadJSONFrom(url, eventsource, layer)
.then((fs) => console.debug("Loaded",fs.length, "features from", url)) .then((fs) => console.debug("Loaded", fs.length, "features from", url))
.catch((err) => console.warn("Could not load ", url, "due to", err)) .catch((err) => console.warn("Could not load ", url, "due to", err))
return true // data is loaded, we can safely unregister return true // data is loaded, we can safely unregister
}) })
} else { } else {
this.LoadJSONFrom(url, eventsource, layer) this.LoadJSONFrom(url, eventsource, layer)
.then((fs) => console.debug("Loaded",fs.length, "features from", url)) .then((fs) => console.debug("Loaded", fs.length, "features from", url))
.catch((err) => console.warn("Could not load ", url, "due to", err)) .catch((err) => console.warn("Could not load ", url, "due to", err))
} }
this.features = eventsource this.features = eventsource
@ -105,7 +105,7 @@ export default class GeoJsonSource implements FeatureSource {
let i = 0 let i = 0
let skipped = 0 let skipped = 0
for (const feature of json.features) { for (const feature of json.features) {
if(feature.geometry.type === "Point"){ if (feature.geometry.type === "Point") {
// See https://github.com/maproulette/maproulette-backend/issues/242 // See https://github.com/maproulette/maproulette-backend/issues/242
feature.geometry.coordinates = feature.geometry.coordinates.map(Number) feature.geometry.coordinates = feature.geometry.coordinates.map(Number)
} }

View file

@ -32,7 +32,9 @@ export class LastClickFeatureSource implements WritableFeatureSource {
} }
const renderings = Utils.Dedup( const renderings = Utils.Dedup(
allPresets.map((uiElem) => Utils.runningFromConsole ? "" : uiElem.ConstructElement().innerHTML) allPresets.map((uiElem) =>
Utils.runningFromConsole ? "" : uiElem.ConstructElement().innerHTML
)
) )
const properties = { const properties = {

View file

@ -1,16 +1,16 @@
import GeoJsonSource from "./GeoJsonSource" import GeoJsonSource from "./GeoJsonSource"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import {FeatureSource} from "../FeatureSource" import { FeatureSource } from "../FeatureSource"
import {Or} from "../../Tags/Or" import { Or } from "../../Tags/Or"
import FeatureSwitchState from "../../State/FeatureSwitchState" import FeatureSwitchState from "../../State/FeatureSwitchState"
import OverpassFeatureSource from "./OverpassFeatureSource" import OverpassFeatureSource from "./OverpassFeatureSource"
import {Store, UIEventSource} from "../../UIEventSource" import { Store, UIEventSource } from "../../UIEventSource"
import OsmFeatureSource from "./OsmFeatureSource" import OsmFeatureSource from "./OsmFeatureSource"
import FeatureSourceMerger from "./FeatureSourceMerger" import FeatureSourceMerger from "./FeatureSourceMerger"
import DynamicGeoJsonTileSource from "../TiledFeatureSource/DynamicGeoJsonTileSource" import DynamicGeoJsonTileSource from "../TiledFeatureSource/DynamicGeoJsonTileSource"
import {BBox} from "../../BBox" import { BBox } from "../../BBox"
import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeatureSource" import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeatureSource"
import FullNodeDatabaseSource from "../TiledFeatureSource/FullNodeDatabaseSource"; import FullNodeDatabaseSource from "../TiledFeatureSource/FullNodeDatabaseSource"
/** /**
* This source will fetch the needed data from various sources for the given layout. * This source will fetch the needed data from various sources for the given layout.
@ -41,7 +41,7 @@ export default class LayoutSource extends FeatureSourceMerger {
(l) => (l) =>
new LocalStorageFeatureSource(backend, l.id, 15, mapProperties, { new LocalStorageFeatureSource(backend, l.id, 15, mapProperties, {
isActive: isDisplayed(l.id), isActive: isDisplayed(l.id),
maxAge: l.maxAgeOfCache maxAge: l.maxAgeOfCache,
}) })
) )
@ -127,7 +127,7 @@ export default class LayoutSource extends FeatureSourceMerger {
backend, backend,
isActive, isActive,
patchRelations: true, patchRelations: true,
fullNodeDatabase fullNodeDatabase,
}) })
} }

View file

@ -7,7 +7,7 @@ import { TagsFilter } from "../../Tags/TagsFilter"
import { Feature } from "geojson" import { Feature } from "geojson"
import FeatureSourceMerger from "../Sources/FeatureSourceMerger" import FeatureSourceMerger from "../Sources/FeatureSourceMerger"
import OsmObjectDownloader from "../../Osm/OsmObjectDownloader" import OsmObjectDownloader from "../../Osm/OsmObjectDownloader"
import FullNodeDatabaseSource from "../TiledFeatureSource/FullNodeDatabaseSource"; import FullNodeDatabaseSource from "../TiledFeatureSource/FullNodeDatabaseSource"
/** /**
* If a tile is needed (requested via the UIEventSource in the constructor), will download the appropriate tile and pass it via 'handleTile' * If a tile is needed (requested via the UIEventSource in the constructor), will download the appropriate tile and pass it via 'handleTile'
@ -24,16 +24,16 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
/** /**
* If given: this featureSwitch will not update if the store contains 'false' * If given: this featureSwitch will not update if the store contains 'false'
*/ */
isActive?: Store<boolean>, isActive?: Store<boolean>
patchRelations?: true | boolean, patchRelations?: true | boolean
fullNodeDatabase?: FullNodeDatabaseSource fullNodeDatabase?: FullNodeDatabaseSource
}; }
public readonly isRunning: UIEventSource<boolean> = new UIEventSource<boolean>(false) public readonly isRunning: UIEventSource<boolean> = new UIEventSource<boolean>(false)
private readonly _downloadedTiles: Set<number> = new Set<number>() private readonly _downloadedTiles: Set<number> = new Set<number>()
private readonly _downloadedData: Feature[][] = [] private readonly _downloadedData: Feature[][] = []
private readonly _patchRelations: boolean; private readonly _patchRelations: boolean
/** /**
* Downloads data directly from the OSM-api within the given bounds. * Downloads data directly from the OSM-api within the given bounds.
* All features which match the TagsFilter 'allowedFeatures' are kept and converted into geojson * All features which match the TagsFilter 'allowedFeatures' are kept and converted into geojson
@ -45,12 +45,12 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
/** /**
* If given: this featureSwitch will not update if the store contains 'false' * If given: this featureSwitch will not update if the store contains 'false'
*/ */
isActive?: Store<boolean>, isActive?: Store<boolean>
patchRelations?: true | boolean, patchRelations?: true | boolean
fullNodeDatabase?: FullNodeDatabaseSource fullNodeDatabase?: FullNodeDatabaseSource
}) { }) {
super() super()
this.options = options; this.options = options
this._bounds = options.bounds this._bounds = options.bounds
this.allowedTags = options.allowedFeatures this.allowedTags = options.allowedFeatures
this.isActive = options.isActive ?? new ImmutableStore(true) this.isActive = options.isActive ?? new ImmutableStore(true)

View file

@ -1,6 +1,6 @@
import {FeatureSource} from "../FeatureSource" import { FeatureSource } from "../FeatureSource"
import {ImmutableStore, Store} from "../../UIEventSource" import { ImmutableStore, Store } from "../../UIEventSource"
import {Feature} from "geojson" import { Feature } from "geojson"
/** /**
* A simple, read only feature store. * A simple, read only feature store.
@ -8,13 +8,7 @@ import {Feature} from "geojson"
export default class StaticFeatureSource<T extends Feature = Feature> implements FeatureSource<T> { export default class StaticFeatureSource<T extends Feature = Feature> implements FeatureSource<T> {
public readonly features: Store<T[]> public readonly features: Store<T[]>
constructor( constructor(features: Store<T[]> | T[] | { features: T[] } | { features: Store<T[]> }) {
features:
| Store<T[]>
| T[]
| { features: T[] }
| { features: Store<T[]> }
) {
if (features === undefined) { if (features === undefined) {
throw "Static feature source received undefined as source" throw "Static feature source received undefined as source"
} }

View file

@ -1,9 +1,9 @@
import {FeatureSource, FeatureSourceForLayer} from "../FeatureSource" import { FeatureSource, FeatureSourceForLayer } from "../FeatureSource"
import StaticFeatureSource from "./StaticFeatureSource" import StaticFeatureSource from "./StaticFeatureSource"
import {BBox} from "../../BBox" import { BBox } from "../../BBox"
import FilteredLayer from "../../../Models/FilteredLayer" import FilteredLayer from "../../../Models/FilteredLayer"
import {Store} from "../../UIEventSource" import { Store } from "../../UIEventSource"
import {Feature} from "geojson"; import { Feature } from "geojson"
/** /**
* Results in a feature source which has all the elements that touch the given features * Results in a feature source which has all the elements that touch the given features
@ -30,7 +30,10 @@ export default class BBoxFeatureSource<T extends Feature = Feature> extends Stat
} }
} }
export class BBoxFeatureSourceForLayer<T extends Feature = Feature> extends BBoxFeatureSource<T> implements FeatureSourceForLayer { export class BBoxFeatureSourceForLayer<T extends Feature = Feature>
extends BBoxFeatureSource<T>
implements FeatureSourceForLayer
{
readonly layer: FilteredLayer readonly layer: FilteredLayer
constructor(features: FeatureSourceForLayer<T>, mustTouch: Store<BBox>) { constructor(features: FeatureSourceForLayer<T>, mustTouch: Store<BBox>) {

View file

@ -72,7 +72,9 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
if (!isWhiteListed) { if (!isWhiteListed) {
console.debug( console.debug(
"Not downloading tile", "Not downloading tile",
zxy,"for layer",layer.id, zxy,
"for layer",
layer.id,
"as it is not on the whitelist" "as it is not on the whitelist"
) )
return undefined return undefined

View file

@ -1,11 +1,10 @@
import {OsmNode, OsmObject, OsmWay} from "../../Osm/OsmObject" import { OsmNode, OsmObject, OsmWay } from "../../Osm/OsmObject"
import {UIEventSource} from "../../UIEventSource" import { UIEventSource } from "../../UIEventSource"
import {BBox} from "../../BBox"; import { BBox } from "../../BBox"
import StaticFeatureSource from "../Sources/StaticFeatureSource"; import StaticFeatureSource from "../Sources/StaticFeatureSource"
import {Tiles} from "../../../Models/TileRange"; import { Tiles } from "../../../Models/TileRange"
export default class FullNodeDatabaseSource { export default class FullNodeDatabaseSource {
private readonly loadedTiles = new Map<number, Map<number, OsmNode>>() private readonly loadedTiles = new Map<number, Map<number, OsmNode>>()
private readonly nodeByIds = new Map<number, OsmNode>() private readonly nodeByIds = new Map<number, OsmNode>()
private readonly parentWays = new Map<number, UIEventSource<OsmWay[]>>() private readonly parentWays = new Map<number, UIEventSource<OsmWay[]>>()
@ -13,7 +12,7 @@ export default class FullNodeDatabaseSource {
private smallestZoom = 99 private smallestZoom = 99
private largestZoom = 0 private largestZoom = 0
public handleOsmJson(osmJson: any, z: number, x: number, y: number) : void { public handleOsmJson(osmJson: any, z: number, x: number, y: number): void {
const allObjects = OsmObject.ParseObjects(osmJson.elements) const allObjects = OsmObject.ParseObjects(osmJson.elements)
const nodesById = new Map<number, OsmNode>() const nodesById = new Map<number, OsmNode>()
@ -81,14 +80,14 @@ export default class FullNodeDatabaseSource {
* Gets (at least) all nodes which are part of this BBOX; might also return some nodes that fall outside of the bbox but are closeby * Gets (at least) all nodes which are part of this BBOX; might also return some nodes that fall outside of the bbox but are closeby
* @param bbox * @param bbox
*/ */
getNodesWithin(bbox: BBox) : Map<number, OsmNode>{ getNodesWithin(bbox: BBox): Map<number, OsmNode> {
const allById = new Map<number, OsmNode>() const allById = new Map<number, OsmNode>()
for (let z = this.smallestZoom; z < this.largestZoom; z++) { for (let z = this.smallestZoom; z < this.largestZoom; z++) {
const range = Tiles.tileRangeFrom(bbox, z) const range = Tiles.tileRangeFrom(bbox, z)
Tiles.MapRange(range, (x, y ) => { Tiles.MapRange(range, (x, y) => {
const tileId = Tiles.tile_index(z, x, y) const tileId = Tiles.tile_index(z, x, y)
const nodesById = this.loadedTiles.get(tileId) const nodesById = this.loadedTiles.get(tileId)
nodesById?.forEach((v,k) => allById.set(k,v)) nodesById?.forEach((v, k) => allById.set(k, v))
}) })
} }
return allById return allById

View file

@ -1,8 +1,8 @@
import DynamicTileSource from "./DynamicTileSource" import DynamicTileSource from "./DynamicTileSource"
import {Store} from "../../UIEventSource" import { Store } from "../../UIEventSource"
import {BBox} from "../../BBox" import { BBox } from "../../BBox"
import TileLocalStorage from "../Actors/TileLocalStorage" import TileLocalStorage from "../Actors/TileLocalStorage"
import {Feature} from "geojson" import { Feature } from "geojson"
import StaticFeatureSource from "../Sources/StaticFeatureSource" import StaticFeatureSource from "../Sources/StaticFeatureSource"
export default class LocalStorageFeatureSource extends DynamicTileSource { export default class LocalStorageFeatureSource extends DynamicTileSource {
@ -15,26 +15,27 @@ export default class LocalStorageFeatureSource extends DynamicTileSource {
zoom: Store<number> zoom: Store<number>
}, },
options?: { options?: {
isActive?: Store<boolean>, isActive?: Store<boolean>
maxAge?: number // In seconds maxAge?: number // In seconds
} }
) { ) {
const storage = TileLocalStorage.construct<Feature[]>(backend, layername, options?.maxAge ?? 24 * 60 * 60) const storage = TileLocalStorage.construct<Feature[]>(
backend,
layername,
options?.maxAge ?? 24 * 60 * 60
)
super( super(
zoomlevel, zoomlevel,
(tileIndex) => (tileIndex) =>
new StaticFeatureSource( new StaticFeatureSource(
storage storage.getTileSource(tileIndex).mapD((features) => {
.getTileSource(tileIndex) if (features.length === undefined) {
.mapD((features) => { console.trace("These are not features:", features)
if (features.length === undefined) { storage.invalidate(zoomlevel, tileIndex)
console.trace("These are not features:", features) return []
storage.invalidate(zoomlevel, tileIndex) }
return [] return features.filter((f) => !f.properties.id.match(/(node|way)\/-[0-9]+/))
} })
return features.filter((f) => !f.properties.id.match(/(node|way)\/-[0-9]+/));
}
)
), ),
mapProperties, mapProperties,
options options

View file

@ -408,7 +408,10 @@ export class GeoOperations {
/** /**
* Calculates line intersection between two features. * Calculates line intersection between two features.
*/ */
public static LineIntersections(feature: Feature<LineString | MultiLineString | Polygon | MultiPolygon>, otherFeature: Feature<LineString | MultiLineString | Polygon | MultiPolygon>): [number, number][] { public static LineIntersections(
feature: Feature<LineString | MultiLineString | Polygon | MultiPolygon>,
otherFeature: Feature<LineString | MultiLineString | Polygon | MultiPolygon>
): [number, number][] {
return turf return turf
.lineIntersect(feature, otherFeature) .lineIntersect(feature, otherFeature)
.features.map((p) => <[number, number]>p.geometry.coordinates) .features.map((p) => <[number, number]>p.geometry.coordinates)
@ -420,8 +423,7 @@ export class GeoOperations {
* @param features * @param features
* @param zoomlevel * @param zoomlevel
*/ */
public static spreadIntoBboxes(features: Feature[], zoomlevel: number) : Map<number, Feature[]> { public static spreadIntoBboxes(features: Feature[], zoomlevel: number): Map<number, Feature[]> {
const perBbox = new Map<number, Feature[]>() const perBbox = new Map<number, Feature[]>()
for (const feature of features) { for (const feature of features) {
@ -430,7 +432,7 @@ export class GeoOperations {
Tiles.MapRange(tilerange, (x, y) => { Tiles.MapRange(tilerange, (x, y) => {
const tileNumber = Tiles.tile_index(zoomlevel, x, y) const tileNumber = Tiles.tile_index(zoomlevel, x, y)
let newFeatureList = perBbox.get(tileNumber) let newFeatureList = perBbox.get(tileNumber)
if(newFeatureList === undefined){ if (newFeatureList === undefined) {
newFeatureList = [] newFeatureList = []
perBbox.set(tileNumber, newFeatureList) perBbox.set(tileNumber, newFeatureList)
} }
@ -703,18 +705,18 @@ export class GeoOperations {
public static along(a: Coord, b: Coord, distanceMeter: number): Coord { public static along(a: Coord, b: Coord, distanceMeter: number): Coord {
return turf.along( return turf.along(
<any> { <any>{
type:"Feature", type: "Feature",
geometry:{ geometry: {
type:"LineString", type: "LineString",
coordinates: [a, b] coordinates: [a, b],
} },
}, distanceMeter, {units: "meters"} },
distanceMeter,
{ units: "meters" }
).geometry.coordinates ).geometry.coordinates
} }
/** /**
* Returns 'true' if one feature contains the other feature * Returns 'true' if one feature contains the other feature
* *

View file

@ -1,10 +1,10 @@
import { Mapillary } from "./Mapillary"; import { Mapillary } from "./Mapillary"
import { WikimediaImageProvider } from "./WikimediaImageProvider"; import { WikimediaImageProvider } from "./WikimediaImageProvider"
import { Imgur } from "./Imgur"; import { Imgur } from "./Imgur"
import GenericImageProvider from "./GenericImageProvider"; import GenericImageProvider from "./GenericImageProvider"
import { Store, UIEventSource } from "../UIEventSource"; import { Store, UIEventSource } from "../UIEventSource"
import ImageProvider, { ProvidedImage } from "./ImageProvider"; import ImageProvider, { ProvidedImage } from "./ImageProvider"
import { WikidataImageProvider } from "./WikidataImageProvider"; import { WikidataImageProvider } from "./WikidataImageProvider"
/** /**
* A generic 'from the interwebz' image picker, without attribution * A generic 'from the interwebz' image picker, without attribution
@ -44,7 +44,10 @@ export default class AllImageProviders {
UIEventSource<ProvidedImage[]> UIEventSource<ProvidedImage[]>
>() >()
public static LoadImagesFor(tags: Store<Record<string, string>>, tagKey?: string[]): Store<ProvidedImage[]> { public static LoadImagesFor(
tags: Store<Record<string, string>>,
tagKey?: string[]
): Store<ProvidedImage[]> {
if (tags.data.id === undefined) { if (tags.data.id === undefined) {
return undefined return undefined
} }

View file

@ -80,12 +80,12 @@ export default class Maproulette {
* Maproulette.codeToIndex("qdsf") // => undefined * Maproulette.codeToIndex("qdsf") // => undefined
* *
*/ */
public static codeToIndex(code: string) : number | undefined{ public static codeToIndex(code: string): number | undefined {
if(code === "Created"){ if (code === "Created") {
return Maproulette.STATUS_OPEN return Maproulette.STATUS_OPEN
} }
for (let i = 0; i < 9; i++) { for (let i = 0; i < 9; i++) {
if(Maproulette.STATUS_MEANING[""+i] === code){ if (Maproulette.STATUS_MEANING["" + i] === code) {
return i return i
} }
} }

View file

@ -1,14 +1,14 @@
import SimpleMetaTaggers, {MetataggingState, SimpleMetaTagger} from "./SimpleMetaTagger" import SimpleMetaTaggers, { MetataggingState, SimpleMetaTagger } from "./SimpleMetaTagger"
import {ExtraFuncParams, ExtraFunctions, ExtraFuncType} from "./ExtraFunctions" import { ExtraFuncParams, ExtraFunctions, ExtraFuncType } from "./ExtraFunctions"
import LayerConfig from "../Models/ThemeConfig/LayerConfig" import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import {Feature} from "geojson" import { Feature } from "geojson"
import FeaturePropertiesStore from "./FeatureSource/Actors/FeaturePropertiesStore" import FeaturePropertiesStore from "./FeatureSource/Actors/FeaturePropertiesStore"
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
import {GeoIndexedStoreForLayer} from "./FeatureSource/Actors/GeoIndexedStore" import { GeoIndexedStoreForLayer } from "./FeatureSource/Actors/GeoIndexedStore"
import {IndexedFeatureSource} from "./FeatureSource/FeatureSource" import { IndexedFeatureSource } from "./FeatureSource/FeatureSource"
import OsmObjectDownloader from "./Osm/OsmObjectDownloader" import OsmObjectDownloader from "./Osm/OsmObjectDownloader"
import {Utils} from "../Utils"; import { Utils } from "../Utils"
import {UIEventSource} from "./UIEventSource"; import { UIEventSource } from "./UIEventSource"
/** /**
* Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ... * Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ...
@ -18,7 +18,10 @@ import {UIEventSource} from "./UIEventSource";
export default class MetaTagging { export default class MetaTagging {
private static errorPrintCount = 0 private static errorPrintCount = 0
private static readonly stopErrorOutputAt = 10 private static readonly stopErrorOutputAt = 10
private static retaggingFuncCache = new Map<string, ((feature: Feature, propertiesStore: UIEventSource<any>) => void)[]>() private static retaggingFuncCache = new Map<
string,
((feature: Feature, propertiesStore: UIEventSource<any>) => void)[]
>()
constructor(state: { constructor(state: {
layout: LayoutConfig layout: LayoutConfig
@ -96,7 +99,7 @@ export default class MetaTagging {
// The calculated functions - per layer - which add the new keys // The calculated functions - per layer - which add the new keys
// Calculated functions are defined by the layer // Calculated functions are defined by the layer
const layerFuncs = this.createRetaggingFunc(layer, ExtraFunctions.constructHelpers(params)) const layerFuncs = this.createRetaggingFunc(layer, ExtraFunctions.constructHelpers(params))
const state: MetataggingState = {layout, osmObjectDownloader} const state: MetataggingState = { layout, osmObjectDownloader }
let atLeastOneFeatureChanged = false let atLeastOneFeatureChanged = false
let strictlyEvaluated = 0 let strictlyEvaluated = 0
@ -177,20 +180,20 @@ export default class MetaTagging {
} }
public static createExtraFuncParams(state: { public static createExtraFuncParams(state: {
indexedFeatures: IndexedFeatureSource, indexedFeatures: IndexedFeatureSource
perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
}) { }) {
return { return {
getFeatureById: (id) => state.indexedFeatures.featuresById.data.get(id), getFeatureById: (id) => state.indexedFeatures.featuresById.data.get(id),
getFeaturesWithin: (layerId, bbox) => { getFeaturesWithin: (layerId, bbox) => {
if (layerId === '*' || layerId === null || layerId === undefined) { if (layerId === "*" || layerId === null || layerId === undefined) {
const feats: Feature[][] = [] const feats: Feature[][] = []
state.perLayer.forEach((layer) => { state.perLayer.forEach((layer) => {
feats.push(layer.GetFeaturesWithin(bbox)) feats.push(layer.GetFeaturesWithin(bbox))
}) })
return feats return feats
} }
return [state.perLayer.get(layerId).GetFeaturesWithin(bbox)]; return [state.perLayer.get(layerId).GetFeaturesWithin(bbox)]
}, },
} }
} }
@ -202,23 +205,30 @@ export default class MetaTagging {
* @param layerId * @param layerId
* @private * @private
*/ */
private static createFunctionForFeature([key, code, isStrict]: [string, string, boolean], private static createFunctionForFeature(
helperFunctions: Record<ExtraFuncType, (feature: Feature) => Function>, [key, code, isStrict]: [string, string, boolean],
layerId: string = "unkown layer" helperFunctions: Record<ExtraFuncType, (feature: Feature) => Function>,
layerId: string = "unkown layer"
): ((feature: Feature, propertiesStore?: UIEventSource<any>) => void) | undefined { ): ((feature: Feature, propertiesStore?: UIEventSource<any>) => void) | undefined {
if (code === undefined) { if (code === undefined) {
return undefined return undefined
} }
const calculateAndAssign: (feat: Feature, store?: UIEventSource<any>) => string | any = (
const calculateAndAssign: ((feat: Feature, store?: UIEventSource<any>) => string | any) = (feat, store) => { feat,
store
) => {
try { try {
let result = new Function("feat", "{" + ExtraFunctions.types.join(", ") + "}", "return " + code + ";")(feat, helperFunctions) let result = new Function(
"feat",
"{" + ExtraFunctions.types.join(", ") + "}",
"return " + code + ";"
)(feat, helperFunctions)
if (result === "") { if (result === "") {
result = undefined result = undefined
} }
const oldValue= feat.properties[key] const oldValue = feat.properties[key]
if(oldValue == result){ if (oldValue == result) {
return oldValue return oldValue
} }
delete feat.properties[key] delete feat.properties[key]
@ -229,16 +239,16 @@ export default class MetaTagging {
if (MetaTagging.errorPrintCount < MetaTagging.stopErrorOutputAt) { if (MetaTagging.errorPrintCount < MetaTagging.stopErrorOutputAt) {
console.warn( console.warn(
"Could not calculate a " + "Could not calculate a " +
(isStrict ? "strict " : "") + (isStrict ? "strict " : "") +
" calculated tag for key " + " calculated tag for key " +
key + key +
" defined by " + " defined by " +
code + code +
" (in layer" + " (in layer" +
layerId + layerId +
") due to \n" + ") due to \n" +
e + e +
"\n. Are you the theme creator? Doublecheck your code. Note that the metatags might not be stable on new features", "\n. Are you the theme creator? Doublecheck your code. Note that the metatags might not be stable on new features",
e, e,
e.stack e.stack
) )
@ -276,9 +286,12 @@ export default class MetaTagging {
return undefined return undefined
} }
let functions: ((feature: Feature, propertiesStore?: UIEventSource<any>) => void)[] = MetaTagging.retaggingFuncCache.get(layer.id) let functions: ((feature: Feature, propertiesStore?: UIEventSource<any>) => void)[] =
MetaTagging.retaggingFuncCache.get(layer.id)
if (functions === undefined) { if (functions === undefined) {
functions = calculatedTags.map(spec => this.createFunctionForFeature(spec, helpers, layer.id)) functions = calculatedTags.map((spec) =>
this.createFunctionForFeature(spec, helpers, layer.id)
)
MetaTagging.retaggingFuncCache.set(layer.id, functions) MetaTagging.retaggingFuncCache.set(layer.id, functions)
} }

View file

@ -1,20 +1,23 @@
import {OsmCreateAction, PreviewableAction} from "./OsmChangeAction" import { OsmCreateAction, PreviewableAction } from "./OsmChangeAction"
import {Tag} from "../../Tags/Tag" import { Tag } from "../../Tags/Tag"
import {Changes} from "../Changes" import { Changes } from "../Changes"
import {ChangeDescription} from "./ChangeDescription" import { ChangeDescription } from "./ChangeDescription"
import CreateNewWayAction from "./CreateNewWayAction" import CreateNewWayAction from "./CreateNewWayAction"
import CreateWayWithPointReuseAction, {MergePointConfig} from "./CreateWayWithPointReuseAction" import CreateWayWithPointReuseAction, { MergePointConfig } from "./CreateWayWithPointReuseAction"
import {And} from "../../Tags/And" import { And } from "../../Tags/And"
import {TagUtils} from "../../Tags/TagUtils" import { TagUtils } from "../../Tags/TagUtils"
import {FeatureSource, IndexedFeatureSource} from "../../FeatureSource/FeatureSource" import { FeatureSource, IndexedFeatureSource } from "../../FeatureSource/FeatureSource"
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"; import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"
import {Position} from "geojson"; import { Position } from "geojson"
import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"; import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
/** /**
* More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points * More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points
*/ */
export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAction implements PreviewableAction { export default class CreateMultiPolygonWithPointReuseAction
extends OsmCreateAction
implements PreviewableAction
{
public newElementId: string = undefined public newElementId: string = undefined
public newElementIdNumber: number = undefined public newElementIdNumber: number = undefined
private readonly _tags: Tag[] private readonly _tags: Tag[]
@ -29,9 +32,9 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct
outerRingCoordinates: Position[], outerRingCoordinates: Position[],
innerRingsCoordinates: Position[][], innerRingsCoordinates: Position[][],
state: { state: {
layout: LayoutConfig; layout: LayoutConfig
changes: Changes; changes: Changes
indexedFeatures: IndexedFeatureSource, indexedFeatures: IndexedFeatureSource
fullNodeDatabase?: FullNodeDatabaseSource fullNodeDatabase?: FullNodeDatabaseSource
}, },
config: MergePointConfig[], config: MergePointConfig[],
@ -43,7 +46,7 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct
this.theme = state?.layout?.id ?? "" this.theme = state?.layout?.id ?? ""
this.createOuterWay = new CreateWayWithPointReuseAction( this.createOuterWay = new CreateWayWithPointReuseAction(
[], [],
<[number,number][]> outerRingCoordinates, <[number, number][]>outerRingCoordinates,
state, state,
config config
) )

View file

@ -1,9 +1,9 @@
import {ChangeDescription} from "./ChangeDescription" import { ChangeDescription } from "./ChangeDescription"
import {OsmCreateAction} from "./OsmChangeAction" import { OsmCreateAction } from "./OsmChangeAction"
import {Changes} from "../Changes" import { Changes } from "../Changes"
import {Tag} from "../../Tags/Tag" import { Tag } from "../../Tags/Tag"
import CreateNewNodeAction from "./CreateNewNodeAction" import CreateNewNodeAction from "./CreateNewNodeAction"
import {And} from "../../Tags/And" import { And } from "../../Tags/And"
export default class CreateNewWayAction extends OsmCreateAction { export default class CreateNewWayAction extends OsmCreateAction {
public newElementId: string = undefined public newElementId: string = undefined

View file

@ -1,17 +1,17 @@
import {OsmCreateAction, PreviewableAction} from "./OsmChangeAction" import { OsmCreateAction, PreviewableAction } from "./OsmChangeAction"
import {Tag} from "../../Tags/Tag" import { Tag } from "../../Tags/Tag"
import {Changes} from "../Changes" import { Changes } from "../Changes"
import {ChangeDescription} from "./ChangeDescription" import { ChangeDescription } from "./ChangeDescription"
import {BBox} from "../../BBox" import { BBox } from "../../BBox"
import {TagsFilter} from "../../Tags/TagsFilter" import { TagsFilter } from "../../Tags/TagsFilter"
import {GeoOperations} from "../../GeoOperations" import { GeoOperations } from "../../GeoOperations"
import {FeatureSource, IndexedFeatureSource} from "../../FeatureSource/FeatureSource" import { FeatureSource, IndexedFeatureSource } from "../../FeatureSource/FeatureSource"
import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource" import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource"
import CreateNewNodeAction from "./CreateNewNodeAction" import CreateNewNodeAction from "./CreateNewNodeAction"
import CreateNewWayAction from "./CreateNewWayAction" import CreateNewWayAction from "./CreateNewWayAction"
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"; import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"
import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"; import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
import {Position} from "geojson"; import { Position } from "geojson"
export interface MergePointConfig { export interface MergePointConfig {
withinRangeOfM: number withinRangeOfM: number
@ -56,7 +56,10 @@ interface CoordinateInfo {
/** /**
* More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points * More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points
*/ */
export default class CreateWayWithPointReuseAction extends OsmCreateAction implements PreviewableAction { export default class CreateWayWithPointReuseAction
extends OsmCreateAction
implements PreviewableAction
{
public newElementId: string = undefined public newElementId: string = undefined
public newElementIdNumber: number = undefined public newElementIdNumber: number = undefined
private readonly _tags: Tag[] private readonly _tags: Tag[]
@ -66,9 +69,9 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction imple
*/ */
private readonly _coordinateInfo: CoordinateInfo[] private readonly _coordinateInfo: CoordinateInfo[]
private readonly _state: { private readonly _state: {
layout: LayoutConfig; layout: LayoutConfig
changes: Changes; changes: Changes
indexedFeatures: IndexedFeatureSource, indexedFeatures: IndexedFeatureSource
fullNodeDatabase?: FullNodeDatabaseSource fullNodeDatabase?: FullNodeDatabaseSource
} }
private readonly _config: MergePointConfig[] private readonly _config: MergePointConfig[]
@ -77,9 +80,9 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction imple
tags: Tag[], tags: Tag[],
coordinates: Position[], coordinates: Position[],
state: { state: {
layout: LayoutConfig; layout: LayoutConfig
changes: Changes; changes: Changes
indexedFeatures: IndexedFeatureSource, indexedFeatures: IndexedFeatureSource
fullNodeDatabase?: FullNodeDatabaseSource fullNodeDatabase?: FullNodeDatabaseSource
}, },
config: MergePointConfig[] config: MergePointConfig[]
@ -90,7 +93,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction imple
this._config = config this._config = config
// The main logic of this class: the coordinateInfo contains all the changes // The main logic of this class: the coordinateInfo contains all the changes
this._coordinateInfo = this.CalculateClosebyNodes(<[number,number][]> coordinates) this._coordinateInfo = this.CalculateClosebyNodes(<[number, number][]>coordinates)
} }
public async getPreview(): Promise<FeatureSource> { public async getPreview(): Promise<FeatureSource> {
@ -245,7 +248,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction imple
}, },
}) })
} }
nodeIdsToUse.push({lat, lon, nodeId: id}) nodeIdsToUse.push({ lat, lon, nodeId: id })
} }
const newWay = new CreateNewWayAction(this._tags, nodeIdsToUse, { const newWay = new CreateNewWayAction(this._tags, nodeIdsToUse, {
@ -321,7 +324,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction imple
if (!config.ifMatches.matchesProperties(node.properties)) { if (!config.ifMatches.matchesProperties(node.properties)) {
continue continue
} }
closebyNodes.push({node, d, config}) closebyNodes.push({ node, d, config })
} }
} }

View file

@ -8,7 +8,7 @@ import { And } from "../../Tags/And"
import { Tag } from "../../Tags/Tag" import { Tag } from "../../Tags/Tag"
import { OsmId } from "../../../Models/OsmFeature" import { OsmId } from "../../../Models/OsmFeature"
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import OsmObjectDownloader from "../OsmObjectDownloader"; import OsmObjectDownloader from "../OsmObjectDownloader"
export default class DeleteAction extends OsmChangeAction { export default class DeleteAction extends OsmChangeAction {
private readonly _softDeletionTags: TagsFilter private readonly _softDeletionTags: TagsFilter
@ -72,9 +72,11 @@ export default class DeleteAction extends OsmChangeAction {
changes: Changes, changes: Changes,
object?: OsmObject object?: OsmObject
): Promise<ChangeDescription[]> { ): Promise<ChangeDescription[]> {
const osmObject = object ?? (await new OsmObjectDownloader(changes.backend, changes).DownloadObjectAsync(this._id)) const osmObject =
object ??
(await new OsmObjectDownloader(changes.backend, changes).DownloadObjectAsync(this._id))
if(osmObject === "deleted"){ if (osmObject === "deleted") {
// already deleted in the meantime - no more changes necessary // already deleted in the meantime - no more changes necessary
return [] return []
} }

View file

@ -4,7 +4,7 @@
*/ */
import { Changes } from "../Changes" import { Changes } from "../Changes"
import { ChangeDescription } from "./ChangeDescription" import { ChangeDescription } from "./ChangeDescription"
import {FeatureSource} from "../../FeatureSource/FeatureSource"; import { FeatureSource } from "../../FeatureSource/FeatureSource"
export default abstract class OsmChangeAction { export default abstract class OsmChangeAction {
public readonly trackStatistics: boolean public readonly trackStatistics: boolean

View file

@ -1,21 +1,21 @@
import OsmChangeAction, {PreviewableAction} from "./OsmChangeAction" import OsmChangeAction, { PreviewableAction } from "./OsmChangeAction"
import {Changes} from "../Changes" import { Changes } from "../Changes"
import {ChangeDescription} from "./ChangeDescription" import { ChangeDescription } from "./ChangeDescription"
import {Tag} from "../../Tags/Tag" import { Tag } from "../../Tags/Tag"
import {FeatureSource} from "../../FeatureSource/FeatureSource" import { FeatureSource } from "../../FeatureSource/FeatureSource"
import {OsmNode, OsmObject, OsmWay} from "../OsmObject" import { OsmNode, OsmObject, OsmWay } from "../OsmObject"
import {GeoOperations} from "../../GeoOperations" import { GeoOperations } from "../../GeoOperations"
import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource" import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource"
import CreateNewNodeAction from "./CreateNewNodeAction" import CreateNewNodeAction from "./CreateNewNodeAction"
import ChangeTagAction from "./ChangeTagAction" import ChangeTagAction from "./ChangeTagAction"
import {And} from "../../Tags/And" import { And } from "../../Tags/And"
import {Utils} from "../../../Utils" import { Utils } from "../../../Utils"
import {OsmConnection} from "../OsmConnection" import { OsmConnection } from "../OsmConnection"
import {Feature} from "@turf/turf" import { Feature } from "@turf/turf"
import {Geometry, LineString, Point} from "geojson" import { Geometry, LineString, Point } from "geojson"
import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
export default class ReplaceGeometryAction extends OsmChangeAction implements PreviewableAction{ export default class ReplaceGeometryAction extends OsmChangeAction implements PreviewableAction {
/** /**
* The target feature - mostly used for the metadata * The target feature - mostly used for the metadata
*/ */
@ -45,7 +45,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction implements Pr
public readonly newElementId: string public readonly newElementId: string
constructor( constructor(
state: { state: {
osmConnection: OsmConnection, osmConnection: OsmConnection
fullNodeDatabase?: FullNodeDatabaseSource fullNodeDatabase?: FullNodeDatabaseSource
}, },
feature: any, feature: any,
@ -460,7 +460,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction implements Pr
} }
} }
console.log("Adding tags", this.newTags,"to conflated way nr", this.wayToReplaceId) console.log("Adding tags", this.newTags, "to conflated way nr", this.wayToReplaceId)
if (this.newTags !== undefined && this.newTags.length > 0) { if (this.newTags !== undefined && this.newTags.length > 0) {
const addExtraTags = new ChangeTagAction( const addExtraTags = new ChangeTagAction(
this.wayToReplaceId, this.wayToReplaceId,

View file

@ -1,10 +1,10 @@
import {OsmWay} from "../OsmObject" import { OsmWay } from "../OsmObject"
import {Changes} from "../Changes" import { Changes } from "../Changes"
import {GeoOperations} from "../../GeoOperations" import { GeoOperations } from "../../GeoOperations"
import OsmChangeAction from "./OsmChangeAction" import OsmChangeAction from "./OsmChangeAction"
import {ChangeDescription} from "./ChangeDescription" import { ChangeDescription } from "./ChangeDescription"
import RelationSplitHandler from "./RelationSplitHandler" import RelationSplitHandler from "./RelationSplitHandler"
import {Feature, LineString} from "geojson" import { Feature, LineString } from "geojson"
import OsmObjectDownloader from "../OsmObjectDownloader" import OsmObjectDownloader from "../OsmObjectDownloader"
interface SplitInfo { interface SplitInfo {

View file

@ -1,16 +1,16 @@
import {OsmNode, OsmObject, OsmRelation, OsmWay} from "./OsmObject" import { OsmNode, OsmObject, OsmRelation, OsmWay } from "./OsmObject"
import {Store, UIEventSource} from "../UIEventSource" import { Store, UIEventSource } from "../UIEventSource"
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants"
import OsmChangeAction from "./Actions/OsmChangeAction" import OsmChangeAction from "./Actions/OsmChangeAction"
import {ChangeDescription, ChangeDescriptionTools} from "./Actions/ChangeDescription" import { ChangeDescription, ChangeDescriptionTools } from "./Actions/ChangeDescription"
import {Utils} from "../../Utils" import { Utils } from "../../Utils"
import {LocalStorageSource} from "../Web/LocalStorageSource" import { LocalStorageSource } from "../Web/LocalStorageSource"
import SimpleMetaTagger from "../SimpleMetaTagger" import SimpleMetaTagger from "../SimpleMetaTagger"
import {FeatureSource, IndexedFeatureSource} from "../FeatureSource/FeatureSource" import { FeatureSource, IndexedFeatureSource } from "../FeatureSource/FeatureSource"
import {GeoLocationPointProperties} from "../State/GeoLocationState" import { GeoLocationPointProperties } from "../State/GeoLocationState"
import {GeoOperations} from "../GeoOperations" import { GeoOperations } from "../GeoOperations"
import {ChangesetHandler, ChangesetTag} from "./ChangesetHandler" import { ChangesetHandler, ChangesetTag } from "./ChangesetHandler"
import {OsmConnection} from "./OsmConnection" import { OsmConnection } from "./OsmConnection"
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
import OsmObjectDownloader from "./OsmObjectDownloader" import OsmObjectDownloader from "./OsmObjectDownloader"
@ -408,7 +408,7 @@ export class Changes {
neededIds.map(async (id) => { neededIds.map(async (id) => {
try { try {
const osmObj = await downloader.DownloadObjectAsync(id) const osmObj = await downloader.DownloadObjectAsync(id)
return {id, osmObj} return { id, osmObj }
} catch (e) { } catch (e) {
console.error( console.error(
"Could not download OSM-object", "Could not download OSM-object",
@ -422,7 +422,7 @@ export class Changes {
osmObjects = Utils.NoNull(osmObjects) osmObjects = Utils.NoNull(osmObjects)
for (const {osmObj, id} of osmObjects) { for (const { osmObj, id } of osmObjects) {
if (osmObj === "deleted") { if (osmObj === "deleted") {
pending = pending.filter((ch) => ch.type + "/" + ch.id !== id) pending = pending.filter((ch) => ch.type + "/" + ch.id !== id)
} }
@ -573,9 +573,9 @@ export class Changes {
) )
console.log( console.log(
"Using current-open-changeset-" + "Using current-open-changeset-" +
theme + theme +
" from the preferences, got " + " from the preferences, got " +
openChangeset.data openChangeset.data
) )
return await self.flushSelectChanges(pendingChanges, openChangeset) return await self.flushSelectChanges(pendingChanges, openChangeset)

View file

@ -131,7 +131,8 @@ export class ChangesetHandler {
const changeset = generateChangeXML(csId, this._remappings) const changeset = generateChangeXML(csId, this._remappings)
console.log( console.log(
"Opened a new changeset (openChangeset.data is undefined):", "Opened a new changeset (openChangeset.data is undefined):",
changeset, extraMetaTags changeset,
extraMetaTags
) )
const changes = await this.UploadChange(csId, changeset) const changes = await this.UploadChange(csId, changeset)
const hasSpecialMotivationChanges = ChangesetHandler.rewriteMetaTags( const hasSpecialMotivationChanges = ChangesetHandler.rewriteMetaTags(

View file

@ -1,8 +1,8 @@
import {Utils} from "../../Utils" import { Utils } from "../../Utils"
import polygon_features from "../../assets/polygon-features.json" import polygon_features from "../../assets/polygon-features.json"
import OsmToGeoJson from "osmtogeojson" import OsmToGeoJson from "osmtogeojson"
import {OsmFeature, OsmId, OsmTags, WayId} from "../../Models/OsmFeature" import { OsmFeature, OsmId, OsmTags, WayId } from "../../Models/OsmFeature"
import {Feature, LineString, Polygon} from "geojson" import { Feature, LineString, Polygon } from "geojson"
export abstract class OsmObject { export abstract class OsmObject {
private static defaultBackend = "https://www.openstreetmap.org/" private static defaultBackend = "https://www.openstreetmap.org/"
@ -198,7 +198,6 @@ export class OsmNode extends OsmObject {
this.LoadData(extraData) this.LoadData(extraData)
} }
/** /**
* *
* const obj = new OsmNode(1234) * const obj = new OsmNode(1234)
@ -213,11 +212,11 @@ export class OsmNode extends OsmObject {
*/ */
ChangesetXML(changesetId: string, header?: string): string { ChangesetXML(changesetId: string, header?: string): string {
let tags = this.TagsXML() let tags = this.TagsXML()
return ( return ` <node id="${this.id}" ${header ?? ""} ${
` <node id="${this.id}" ${header ?? ""} ${changesetId ? (' changeset="' + changesetId+ '" ') : ""}${this.VersionXML()} lat="${this.lat}" lon="${this.lon}"> changesetId ? ' changeset="' + changesetId + '" ' : ""
}${this.VersionXML()} lat="${this.lat}" lon="${this.lon}">
${tags} </node> ${tags} </node>
` `
)
} }
SaveExtraData(element) { SaveExtraData(element) {
@ -269,11 +268,11 @@ export class OsmWay extends OsmObject {
nds += ' <nd ref="' + this.nodes[node] + '"/>\n' nds += ' <nd ref="' + this.nodes[node] + '"/>\n'
} }
return ( return ` <way id="${this.id}" ${header ?? ""} ${
` <way id="${this.id}" ${header ?? ""} ${changesetId ? ('changeset="' + changesetId + '" ') : ""} ${this.VersionXML()}> changesetId ? 'changeset="' + changesetId + '" ' : ""
} ${this.VersionXML()}>
${nds}${tags} </way> ${nds}${tags} </way>
` `
)
} }
SaveExtraData(element, allNodes: OsmNode[]) { SaveExtraData(element, allNodes: OsmNode[]) {

View file

@ -132,7 +132,7 @@ class CountryTagger extends SimpleMetaTagger {
CountryTagger.coder CountryTagger.coder
.GetCountryCodeAsync(lon, lat) .GetCountryCodeAsync(lon, lat)
.then((countries) => { .then((countries) => {
if(!countries){ if (!countries) {
console.warn("Country coder returned ", countries) console.warn("Country coder returned ", countries)
return return
} }
@ -315,8 +315,7 @@ export default class SimpleMetaTaggers {
}, },
(feature) => { (feature) => {
Utils.AddLazyProperty(feature.properties, "_surface", () => { Utils.AddLazyProperty(feature.properties, "_surface", () => {
return "" + GeoOperations.surfaceAreaInSqMeters(feature) return "" + GeoOperations.surfaceAreaInSqMeters(feature)
}) })
return true return true

View file

@ -2,38 +2,26 @@
* The part of the global state which initializes the feature switches, based on default values and on the layoutToUse * The part of the global state which initializes the feature switches, based on default values and on the layoutToUse
*/ */
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import {UIEventSource} from "../UIEventSource" import { UIEventSource } from "../UIEventSource"
import {QueryParameters} from "../Web/QueryParameters" import { QueryParameters } from "../Web/QueryParameters"
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants"
import {Utils} from "../../Utils" import { Utils } from "../../Utils"
class FeatureSwitchUtils { class FeatureSwitchUtils {
static initSwitch( static initSwitch(key: string, deflt: boolean, documentation: string): UIEventSource<boolean> {
key: string, const defaultValue = deflt
deflt:boolean, const queryParam = QueryParameters.GetQueryParameter(key, "" + defaultValue, documentation)
documentation: string
): UIEventSource<boolean> {
const defaultValue = deflt
const queryParam = QueryParameters.GetQueryParameter(
key,
"" + defaultValue,
documentation
)
// It takes the current layout, extracts the default value for this query parameter. A query parameter event source is then retrieved and flattened
return queryParam.sync(
(str) => (str === undefined ? defaultValue : str !== "false"),
[],
(b) => (b == defaultValue ? undefined : "" + b)
)
}
// It takes the current layout, extracts the default value for this query parameter. A query parameter event source is then retrieved and flattened
return queryParam.sync(
(str) => (str === undefined ? defaultValue : str !== "false"),
[],
(b) => (b == defaultValue ? undefined : "" + b)
)
}
} }
export class OsmConnectionFeatureSwitches { export class OsmConnectionFeatureSwitches {
public readonly featureSwitchFakeUser: UIEventSource<boolean> public readonly featureSwitchFakeUser: UIEventSource<boolean>
public readonly featureSwitchApiURL: UIEventSource<string> public readonly featureSwitchApiURL: UIEventSource<string>
@ -52,8 +40,7 @@ export class OsmConnectionFeatureSwitches {
} }
} }
export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
export default class FeatureSwitchState extends OsmConnectionFeatureSwitches{
/** /**
* The layout that is being used in this run * The layout that is being used in this run
*/ */
@ -154,7 +141,6 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches{
"Enable the export as GeoJSON and CSV button" "Enable the export as GeoJSON and CSV button"
) )
let testingDefaultValue = false let testingDefaultValue = false
if ( if (
this.featureSwitchApiURL.data !== "osm-test" && this.featureSwitchApiURL.data !== "osm-test" &&

View file

@ -21,7 +21,9 @@ export class GeoLocationState {
* 'granted' means that it is granted * 'granted' means that it is granted
* 'denied' means that we don't have access * 'denied' means that we don't have access
*/ */
public readonly permission: UIEventSource<GeolocationPermissionState> = new UIEventSource("prompt") public readonly permission: UIEventSource<GeolocationPermissionState> = new UIEventSource(
"prompt"
)
/** /**
* Important to determine e.g. if we move automatically on fix or not * Important to determine e.g. if we move automatically on fix or not

View file

@ -69,7 +69,7 @@ export default class LayerState {
new Tag("level", "" + level), new Tag("level", "" + level),
new RegexTag("level", new RegExp("(.*;)?" + level + "(;.*)?")), new RegexTag("level", new RegExp("(.*;)?" + level + "(;.*)?")),
] ]
if(level === "0") { if (level === "0") {
conditionsOrred.push(new Tag("level", "")) // No level tag is the same as level '0' conditionsOrred.push(new Tag("level", "")) // No level tag is the same as level '0'
} }
console.log("Setting levels filter to", conditionsOrred) console.log("Setting levels filter to", conditionsOrred)

View file

@ -1,20 +1,20 @@
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import {OsmConnection} from "../Osm/OsmConnection" import { OsmConnection } from "../Osm/OsmConnection"
import {MangroveIdentity} from "../Web/MangroveReviews" import { MangroveIdentity } from "../Web/MangroveReviews"
import {Store, Stores, UIEventSource} from "../UIEventSource" import { Store, Stores, UIEventSource } from "../UIEventSource"
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource" import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
import {FeatureSource} from "../FeatureSource/FeatureSource" import { FeatureSource } from "../FeatureSource/FeatureSource"
import {Feature} from "geojson" import { Feature } from "geojson"
import {Utils} from "../../Utils" import { Utils } from "../../Utils"
import translators from "../../assets/translators.json" import translators from "../../assets/translators.json"
import codeContributors from "../../assets/contributors.json" import codeContributors from "../../assets/contributors.json"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import {LayerConfigJson} from "../../Models/ThemeConfig/Json/LayerConfigJson" import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import usersettings from "../../assets/generated/layers/usersettings.json" import usersettings from "../../assets/generated/layers/usersettings.json"
import Locale from "../../UI/i18n/Locale" import Locale from "../../UI/i18n/Locale"
import LinkToWeblate from "../../UI/Base/LinkToWeblate" import LinkToWeblate from "../../UI/Base/LinkToWeblate"
import FeatureSwitchState from "./FeatureSwitchState" import FeatureSwitchState from "./FeatureSwitchState"
import Constants from "../../Models/Constants"; import Constants from "../../Models/Constants"
/** /**
* The part of the state which keeps track of user-related stuff, e.g. the OSM-connection, * The part of the state which keeps track of user-related stuff, e.g. the OSM-connection,
@ -34,8 +34,8 @@ export default class UserRelatedState {
public readonly mangroveIdentity: MangroveIdentity public readonly mangroveIdentity: MangroveIdentity
public readonly installedUserThemes: Store<string[]> public readonly installedUserThemes: Store<string[]>
public readonly showAllQuestionsAtOnce: UIEventSource<boolean> public readonly showAllQuestionsAtOnce: UIEventSource<boolean>
public static readonly SHOW_TAGS_VALUES = ["always","yes","full"] as const public static readonly SHOW_TAGS_VALUES = ["always", "yes", "full"] as const
public readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full">; public readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full">
public readonly homeLocation: FeatureSource public readonly homeLocation: FeatureSource
public readonly language: UIEventSource<string> public readonly language: UIEventSource<string>
/** /**
@ -102,26 +102,22 @@ export default class UserRelatedState {
} }
private static initUserRelatedState(): LayerConfig { private static initUserRelatedState(): LayerConfig {
try{ try {
return new LayerConfig(<LayerConfigJson>usersettings, "userinformationpanel")
return new LayerConfig( } catch (e) {
<LayerConfigJson>usersettings,
"userinformationpanel"
)
}catch(e){
return undefined return undefined
} }
} }
public GetUnofficialTheme(id: string): public GetUnofficialTheme(id: string):
| { | {
id: string id: string
icon: string icon: string
title: any title: any
shortDescription: any shortDescription: any
definition?: any definition?: any
isOfficial: boolean isOfficial: boolean
} }
| undefined { | undefined {
console.log("GETTING UNOFFICIAL THEME") console.log("GETTING UNOFFICIAL THEME")
const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id) const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id)
@ -146,8 +142,8 @@ export default class UserRelatedState {
} catch (e) { } catch (e) {
console.warn( console.warn(
"Removing theme " + "Removing theme " +
id + id +
" as it could not be parsed from the preferences; the content is:", " as it could not be parsed from the preferences; the content is:",
str str
) )
pref.setData(null) pref.setData(null)
@ -184,7 +180,7 @@ export default class UserRelatedState {
} }
private InitializeLanguage(availableLanguages?: string[]) { private InitializeLanguage(availableLanguages?: string[]) {
this.language.addCallbackAndRunD(language => Locale.language.setData(language)) this.language.addCallbackAndRunD((language) => Locale.language.setData(language))
Locale.language.addCallback((currentLanguage) => { Locale.language.addCallback((currentLanguage) => {
if (Locale.showLinkToWeblate.data) { if (Locale.showLinkToWeblate.data) {
return true // Disable auto switching as we are in translators mode return true // Disable auto switching as we are in translators mode
@ -262,7 +258,8 @@ export default class UserRelatedState {
_theme: layout?.id, _theme: layout?.id,
_backend: this.osmConnection.Backend(), _backend: this.osmConnection.Backend(),
_applicationOpened: new Date().toISOString(), _applicationOpened: new Date().toISOString(),
_supports_sharing: (typeof window === "undefined") ? "no" : (window.navigator.share ? "yes" : "no") _supports_sharing:
typeof window === "undefined" ? "no" : window.navigator.share ? "yes" : "no",
}) })
for (const key in Constants.userJourney) { for (const key in Constants.userJourney) {
@ -297,13 +294,13 @@ export default class UserRelatedState {
const zenLinks: { link: string; id: string }[] = Utils.NoNull([ const zenLinks: { link: string; id: string }[] = Utils.NoNull([
hasMissingTheme hasMissingTheme
? { ? {
id: "theme:" + layout.id, id: "theme:" + layout.id,
link: LinkToWeblate.hrefToWeblateZen( link: LinkToWeblate.hrefToWeblateZen(
language, language,
"themes", "themes",
layout.id layout.id
), ),
} }
: undefined, : undefined,
...missingLayers.map((id) => ({ ...missingLayers.map((id) => ({
id: "layer:" + id, id: "layer:" + id,
@ -375,7 +372,7 @@ export default class UserRelatedState {
// Language is managed seperately // Language is managed seperately
continue continue
} }
this.osmConnection.GetPreference(key, undefined, {prefix: ""}).setData(tags[key]) this.osmConnection.GetPreference(key, undefined, { prefix: "" }).setData(tags[key])
} }
}) })

View file

@ -1,5 +1,5 @@
import {Tag} from "./Tag" import { Tag } from "./Tag"
import {TagsFilter} from "./TagsFilter" import { TagsFilter } from "./TagsFilter"
export class RegexTag extends TagsFilter { export class RegexTag extends TagsFilter {
public readonly key: RegExp | string public readonly key: RegExp | string
@ -39,7 +39,6 @@ export class RegexTag extends TagsFilter {
return fromTag === possibleRegex return fromTag === possibleRegex
} }
return possibleRegex.test(fromTag) return possibleRegex.test(fromTag)
} }
private static source(r: string | RegExp) { private static source(r: string | RegExp) {
@ -155,7 +154,7 @@ export class RegexTag extends TagsFilter {
matchesProperties(tags: Record<string, string | number | boolean>): boolean { matchesProperties(tags: Record<string, string | number | boolean>): boolean {
if (typeof this.key === "string") { if (typeof this.key === "string") {
let value = tags[this.key] let value = tags[this.key]
if(!value || value === ""){ if (!value || value === "") {
// No tag is known, so we assume the empty string // No tag is known, so we assume the empty string
// If this regexTag matches the empty string, we return true, otherwise false // If this regexTag matches the empty string, we return true, otherwise false
// (Note: if inverted, we must reverse this) // (Note: if inverted, we must reverse this)
@ -176,7 +175,7 @@ export class RegexTag extends TagsFilter {
return !this.invert return !this.invert
} }
} }
if(typeof value !== "string"){ if (typeof value !== "string") {
value = JSON.stringify(value) value = JSON.stringify(value)
} }
return RegexTag.doesMatch(value, this.value) != this.invert return RegexTag.doesMatch(value, this.value) != this.invert
@ -188,7 +187,7 @@ export class RegexTag extends TagsFilter {
} }
if (RegexTag.doesMatch(key, this.key)) { if (RegexTag.doesMatch(key, this.key)) {
let value = tags[key] ?? "" let value = tags[key] ?? ""
if(typeof value !== "string"){ if (typeof value !== "string") {
value = JSON.stringify(value) value = JSON.stringify(value)
} }
return RegexTag.doesMatch(value, this.value) != this.invert return RegexTag.doesMatch(value, this.value) != this.invert

View file

@ -47,14 +47,14 @@ export class Tag extends TagsFilter {
// and it shouldn't be found! // and it shouldn't be found!
return true return true
} }
if(typeof foundValue !== "string"){ if (typeof foundValue !== "string") {
if(foundValue === true && (this.value === "true" || this.value === "yes" )){ if (foundValue === true && (this.value === "true" || this.value === "yes")) {
return true return true
} }
if(foundValue === false && (this.value === "false" || this.value === "no" )){ if (foundValue === false && (this.value === "false" || this.value === "no")) {
return true return true
} }
foundValue = ""+foundValue foundValue = "" + foundValue
} }
return foundValue === this.value return foundValue === this.value
} }

View file

@ -22,7 +22,7 @@ export class TagUtils {
] ]
static KVtoProperties(tags: Tag[]): Record<string, string> { static KVtoProperties(tags: Tag[]): Record<string, string> {
const properties : Record<string, string> = {} const properties: Record<string, string> = {}
for (const tag of tags) { for (const tag of tags) {
properties[tag.key] = tag.value properties[tag.key] = tag.value
} }

View file

@ -416,7 +416,7 @@ class MappedStore<TIn, T> extends Store<T> {
this._upstreamPingCount = upstreamListenerHandler?.pingCount this._upstreamPingCount = upstreamListenerHandler?.pingCount
this._extraStores = extraStores this._extraStores = extraStores
this.registerCallbacksToUpstream() this.registerCallbacksToUpstream()
if(onDestroy !== undefined){ if (onDestroy !== undefined) {
onDestroy(() => this.unregisterFromUpstream()) onDestroy(() => this.unregisterFromUpstream())
} }
} }
@ -698,7 +698,11 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
* srcSeen // => 21 * srcSeen // => 21
* lastSeen // => 42 * lastSeen // => 42
*/ */
public map<J>(f: (t: T) => J, extraSources: Store<any>[] = [], onDestroy?: (f : () => void ) => void): Store<J> { public map<J>(
f: (t: T) => J,
extraSources: Store<any>[] = [],
onDestroy?: (f: () => void) => void
): Store<J> {
return new MappedStore(this, f, extraSources, this._callbacks, f(this.data), onDestroy) return new MappedStore(this, f, extraSources, this._callbacks, f(this.data), onDestroy)
} }
/** /**

View file

@ -76,7 +76,8 @@ export default class FeatureReviews {
) { ) {
const centerLonLat = GeoOperations.centerpointCoordinates(feature) const centerLonLat = GeoOperations.centerpointCoordinates(feature)
;[this._lon, this._lat] = centerLonLat ;[this._lon, this._lat] = centerLonLat
this._identity = mangroveIdentity ?? new MangroveIdentity(new UIEventSource<string>(undefined)) this._identity =
mangroveIdentity ?? new MangroveIdentity(new UIEventSource<string>(undefined))
const nameKey = options?.nameKey ?? "name" const nameKey = options?.nameKey ?? "name"
if (feature.geometry.type === "Point") { if (feature.geometry.type === "Point") {
@ -103,7 +104,7 @@ export default class FeatureReviews {
this._uncertainty = options?.uncertaintyRadius ?? maxDistance this._uncertainty = options?.uncertaintyRadius ?? maxDistance
} }
this._name = tagsSource .map((tags) => tags[nameKey] ?? options?.fallbackName) this._name = tagsSource.map((tags) => tags[nameKey] ?? options?.fallbackName)
this.subjectUri = this.ConstructSubjectUri() this.subjectUri = this.ConstructSubjectUri()

View file

@ -1,8 +1,8 @@
import ThemeViewState from "../../Models/ThemeViewState"; import ThemeViewState from "../../Models/ThemeViewState"
import Hash from "./Hash"; import Hash from "./Hash"
export default class ThemeViewStateHashActor { export default class ThemeViewStateHashActor {
private readonly _state: ThemeViewState; private readonly _state: ThemeViewState
/** /**
* Converts the hash to the appropriate themeview state and, vice versa, sets the hash. * Converts the hash to the appropriate themeview state and, vice versa, sets the hash.
@ -15,27 +15,27 @@ export default class ThemeViewStateHashActor {
* @param state * @param state
*/ */
constructor(state: ThemeViewState) { constructor(state: ThemeViewState) {
this._state = state; this._state = state
// First of all, try to recover the selected element // First of all, try to recover the selected element
if (Hash.hash.data) { if (Hash.hash.data) {
const hash = Hash.hash.data const hash = Hash.hash.data
this.loadStateFromHash(hash) this.loadStateFromHash(hash)
Hash.hash.setData(hash) // reapply the previous hash Hash.hash.setData(hash) // reapply the previous hash
state.indexedFeatures.featuresById.addCallbackAndRunD(_ => { state.indexedFeatures.featuresById.addCallbackAndRunD((_) => {
let unregister = this.loadSelectedElementFromHash(hash); let unregister = this.loadSelectedElementFromHash(hash)
// once that we have found a matching element, we can be sure the indexedFeaturesource was popuplated and that the job is done // once that we have found a matching element, we can be sure the indexedFeaturesource was popuplated and that the job is done
return unregister return unregister
}) })
} }
// Register a hash change listener to correctly handle the back button // Register a hash change listener to correctly handle the back button
Hash.hash.addCallback(hash => { Hash.hash.addCallback((hash) => {
if (!!hash) { if (!!hash) {
// There is still a hash // There is still a hash
// We _only_ have to (at most) close the overlays in this case // We _only_ have to (at most) close the overlays in this case
const parts = hash.split(";") const parts = hash.split(";")
if(parts.indexOf("background") < 0){ if (parts.indexOf("background") < 0) {
state.guistate.backgroundLayerSelectionIsOpened.setData(false) state.guistate.backgroundLayerSelectionIsOpened.setData(false)
} }
this.loadSelectedElementFromHash(hash) this.loadSelectedElementFromHash(hash)
@ -46,16 +46,14 @@ export default class ThemeViewStateHashActor {
// At last, register callbacks on the state to update the hash when they change. // At last, register callbacks on the state to update the hash when they change.
// Note: these should use 'addCallback', not 'addCallbackAndRun' // Note: these should use 'addCallback', not 'addCallbackAndRun'
state.selectedElement.addCallback(_ => this.setHash()) state.selectedElement.addCallback((_) => this.setHash())
state.guistate.allToggles.forEach(({toggle, submenu}) => { state.guistate.allToggles.forEach(({ toggle, submenu }) => {
submenu?.addCallback(_ => this.setHash()) submenu?.addCallback((_) => this.setHash())
toggle.addCallback(_ => this.setHash()); toggle.addCallback((_) => this.setHash())
}) })
// When all is done, set the hash. This must happen last to give the code above correct info // When all is done, set the hash. This must happen last to give the code above correct info
this.setHash() this.setHash()
} }
/** /**
@ -103,8 +101,7 @@ export default class ThemeViewStateHashActor {
private loadStateFromHash(hash: string) { private loadStateFromHash(hash: string) {
const state = this._state const state = this._state
const parts = hash.split(";") const parts = hash.split(";")
outer: for (const {toggle, name, showOverOthers, submenu} of state.guistate.allToggles) { outer: for (const { toggle, name, showOverOthers, submenu } of state.guistate.allToggles) {
for (const part of parts) { for (const part of parts) {
if (part === name) { if (part === name) {
toggle.setData(true) toggle.setData(true)
@ -125,17 +122,15 @@ export default class ThemeViewStateHashActor {
// If we arrive here, the loop above has not found any match // If we arrive here, the loop above has not found any match
toggle.setData(false) toggle.setData(false)
} }
} }
private setHash() { private setHash() {
const s = this._state const s = this._state
let h = "" let h = ""
for (const {toggle, showOverOthers, name, submenu} of s.guistate.allToggles) { for (const { toggle, showOverOthers, name, submenu } of s.guistate.allToggles) {
if (showOverOthers || !toggle.data) { if (showOverOthers || !toggle.data) {
continue; continue
} }
h = name h = name
if (submenu?.data) { if (submenu?.data) {
@ -147,9 +142,9 @@ export default class ThemeViewStateHashActor {
h = s.selectedElement.data.properties.id h = s.selectedElement.data.properties.id
} }
for (const {toggle, showOverOthers, name, submenu} of s.guistate.allToggles) { for (const { toggle, showOverOthers, name, submenu } of s.guistate.allToggles) {
if (!showOverOthers || !toggle.data) { if (!showOverOthers || !toggle.data) {
continue; continue
} }
if (h) { if (h) {
h += ";" + name h += ";" + name
@ -161,7 +156,6 @@ export default class ThemeViewStateHashActor {
} }
} }
Hash.hash.setData(h) Hash.hash.setData(h)
} }
private back() { private back() {
@ -176,5 +170,4 @@ export default class ThemeViewStateHashActor {
return return
} }
} }
} }

View file

@ -1,4 +1,4 @@
import {Utils} from "../Utils" import { Utils } from "../Utils"
import * as meta from "../package.json" import * as meta from "../package.json"
export type PriviligedLayerType = typeof Constants.priviliged_layers[number] export type PriviligedLayerType = typeof Constants.priviliged_layers[number]
@ -119,7 +119,19 @@ export default class Constants {
/** /**
* These are the values that are allowed to use as 'backdrop' icon for a map pin * These are the values that are allowed to use as 'backdrop' icon for a map pin
*/ */
private static readonly _defaultPinIcons = ["square", "circle", "none", "pin", "person", "plus", "ring", "star", "teardrop", "triangle", "crosshair",] as const private static readonly _defaultPinIcons = [
"square",
"circle",
"none",
"pin",
"person",
"plus",
"ring",
"star",
"teardrop",
"triangle",
"crosshair",
] as const
public static readonly defaultPinIcons: string[] = <any>Constants._defaultPinIcons public static readonly defaultPinIcons: string[] = <any>Constants._defaultPinIcons
private static isRetina(): boolean { private static isRetina(): boolean {
@ -131,8 +143,8 @@ export default class Constants {
return ( return (
(window.matchMedia && (window.matchMedia &&
(window.matchMedia( (window.matchMedia(
"only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx), only screen and (min-resolution: 75.6dpcm)" "only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx), only screen and (min-resolution: 75.6dpcm)"
).matches || ).matches ||
window.matchMedia( window.matchMedia(
"only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min--moz-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2)" "only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min--moz-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2)"
).matches)) || ).matches)) ||

View file

@ -51,7 +51,6 @@ export class Denomination {
return (this._humanSingular ?? this._human).Clone() return (this._humanSingular ?? this._human).Clone()
} }
/** /**
* Create a representation of the given value * Create a representation of the given value
* @param value: the value from OSM * @param value: the value from OSM

View file

@ -1,8 +1,8 @@
import LayerConfig from "./ThemeConfig/LayerConfig" import LayerConfig from "./ThemeConfig/LayerConfig"
import {UIEventSource} from "../Logic/UIEventSource" import { UIEventSource } from "../Logic/UIEventSource"
import UserRelatedState from "../Logic/State/UserRelatedState" import UserRelatedState from "../Logic/State/UserRelatedState"
import {Utils} from "../Utils" import { Utils } from "../Utils"
import {LocalStorageSource} from "../Logic/Web/LocalStorageSource" import { LocalStorageSource } from "../Logic/Web/LocalStorageSource"
export type ThemeViewTabStates = typeof MenuState._themeviewTabs[number] export type ThemeViewTabStates = typeof MenuState._themeviewTabs[number]
export type MenuViewTabStates = typeof MenuState._menuviewTabs[number] export type MenuViewTabStates = typeof MenuState._menuviewTabs[number]
@ -14,8 +14,20 @@ export type MenuViewTabStates = typeof MenuState._menuviewTabs[number]
* Some convenience methods are provided for this as well * Some convenience methods are provided for this as well
*/ */
export class MenuState { export class MenuState {
public static readonly _themeviewTabs = ["intro", "filters", "download", "copyright","share"] as const public static readonly _themeviewTabs = [
public static readonly _menuviewTabs = ["about", "settings", "community", "privacy","advanced"] as const "intro",
"filters",
"download",
"copyright",
"share",
] as const
public static readonly _menuviewTabs = [
"about",
"settings",
"community",
"privacy",
"advanced",
] as const
public readonly themeIsOpened: UIEventSource<boolean> public readonly themeIsOpened: UIEventSource<boolean>
public readonly themeViewTabIndex: UIEventSource<number> public readonly themeViewTabIndex: UIEventSource<number>
public readonly themeViewTab: UIEventSource<ThemeViewTabStates> public readonly themeViewTab: UIEventSource<ThemeViewTabStates>
@ -23,11 +35,13 @@ export class MenuState {
public readonly menuViewTabIndex: UIEventSource<number> public readonly menuViewTabIndex: UIEventSource<number>
public readonly menuViewTab: UIEventSource<MenuViewTabStates> public readonly menuViewTab: UIEventSource<MenuViewTabStates>
public readonly backgroundLayerSelectionIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>(false) public readonly backgroundLayerSelectionIsOpened: UIEventSource<boolean> =
new UIEventSource<boolean>(false)
public readonly allToggles: { public readonly allToggles: {
toggle: UIEventSource<boolean>, name: string, toggle: UIEventSource<boolean>
submenu?: UIEventSource<string>, name: string
submenu?: UIEventSource<string>
showOverOthers?: boolean showOverOthers?: boolean
}[] }[]
@ -81,16 +95,19 @@ export class MenuState {
{ {
toggle: this.menuIsOpened, toggle: this.menuIsOpened,
name: "menu", name: "menu",
submenu: this.menuViewTab submenu: this.menuViewTab,
}, { },
{
toggle: this.themeIsOpened, toggle: this.themeIsOpened,
name: "theme-menu", name: "theme-menu",
submenu: this.themeViewTab submenu: this.themeViewTab,
}, { },
{
toggle: this.backgroundLayerSelectionIsOpened, toggle: this.backgroundLayerSelectionIsOpened,
name: "background", name: "background",
showOverOthers: true showOverOthers: true,
}] },
]
} }
public openFilterView(highlightLayer?: LayerConfig | string) { public openFilterView(highlightLayer?: LayerConfig | string) {
@ -128,10 +145,13 @@ export class MenuState {
* Returns 'true' if at least one menu was opened * Returns 'true' if at least one menu was opened
*/ */
public closeAll(): boolean { public closeAll(): boolean {
const toggles = [this.menuIsOpened, this.themeIsOpened, this.backgroundLayerSelectionIsOpened] const toggles = [
const somethingIsOpen = toggles.some(t => t.data) this.menuIsOpened,
toggles.forEach(t => t.setData(false)) this.themeIsOpened,
this.backgroundLayerSelectionIsOpened,
]
const somethingIsOpen = toggles.some((t) => t.data)
toggles.forEach((t) => t.setData(false))
return somethingIsOpen return somethingIsOpen
} }
} }

View file

@ -1,10 +1,10 @@
import {Feature, Polygon} from "geojson" import { Feature, Polygon } from "geojson"
import * as editorlayerindex from "../assets/editor-layer-index.json" import * as editorlayerindex from "../assets/editor-layer-index.json"
import * as globallayers from "../assets/global-raster-layers.json" import * as globallayers from "../assets/global-raster-layers.json"
import {BBox} from "../Logic/BBox" import { BBox } from "../Logic/BBox"
import {Store, Stores} from "../Logic/UIEventSource" import { Store, Stores } from "../Logic/UIEventSource"
import {GeoOperations} from "../Logic/GeoOperations" import { GeoOperations } from "../Logic/GeoOperations"
import {RasterLayerProperties} from "./RasterLayerProperties" import { RasterLayerProperties } from "./RasterLayerProperties"
export class AvailableRasterLayers { export class AvailableRasterLayers {
public static EditorLayerIndex: (Feature<Polygon, EditorLayerIndexProperties> & public static EditorLayerIndex: (Feature<Polygon, EditorLayerIndexProperties> &
@ -47,8 +47,8 @@ export class AvailableRasterLayers {
type: "vector", type: "vector",
attribution: { attribution: {
text: "Maptiler", text: "Maptiler",
url: "https://www.maptiler.com/copyright/" url: "https://www.maptiler.com/copyright/",
} },
}, },
geometry: BBox.global.asGeometry(), geometry: BBox.global.asGeometry(),
} }
@ -63,8 +63,8 @@ export class AvailableRasterLayers {
type: "vector", type: "vector",
attribution: { attribution: {
text: "Americana", text: "Americana",
url: "https://github.com/ZeLonewolf/openstreetmap-americana/" url: "https://github.com/ZeLonewolf/openstreetmap-americana/",
} },
}, },
geometry: BBox.global.asGeometry(), geometry: BBox.global.asGeometry(),
} }
@ -151,7 +151,14 @@ export interface EditorLayerIndexProperties extends RasterLayerProperties {
* Whether the imagery name should be translated * Whether the imagery name should be translated
*/ */
readonly i18n?: boolean readonly i18n?: boolean
readonly type: "tms" | "wms" | "bing" | "scanex" | "wms_endpoint" | "wmts" | "vector" /* Vector is not actually part of the ELI-spec, we add it for vector layers */ readonly type:
| "tms"
| "wms"
| "bing"
| "scanex"
| "wms_endpoint"
| "wmts"
| "vector" /* Vector is not actually part of the ELI-spec, we add it for vector layers */
/** /**
* A rough categorisation of different types of layers. See https://github.com/osmlab/editor-layer-index/blob/gh-pages/CONTRIBUTING.md#categories for a description of the individual categories. * A rough categorisation of different types of layers. See https://github.com/osmlab/editor-layer-index/blob/gh-pages/CONTRIBUTING.md#categories for a description of the individual categories.
*/ */

View file

@ -297,8 +297,8 @@ export class Fuse<T> extends DesugaringStep<T> {
const step = this.steps[i] const step = this.steps[i]
try { try {
let r = step.convert(json, "While running step " + step.name + ": " + context) let r = step.convert(json, "While running step " + step.name + ": " + context)
if(r.result["tagRenderings"]?.some(tr => tr === undefined)){ if (r.result["tagRenderings"]?.some((tr) => tr === undefined)) {
throw step.name+" introduced an undefined tagRendering" throw step.name + " introduced an undefined tagRendering"
} }
errors.push(...(r.errors ?? [])) errors.push(...(r.errors ?? []))
warnings.push(...(r.warnings ?? [])) warnings.push(...(r.warnings ?? []))

View file

@ -1,9 +1,9 @@
import {Conversion} from "./Conversion" import { Conversion } from "./Conversion"
import LayerConfig from "../LayerConfig" import LayerConfig from "../LayerConfig"
import {LayerConfigJson} from "../Json/LayerConfigJson" import { LayerConfigJson } from "../Json/LayerConfigJson"
import Translations from "../../../UI/i18n/Translations" import Translations from "../../../UI/i18n/Translations"
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson" import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"
import {Translation, TypedTranslation} from "../../../UI/i18n/Translation" import { Translation, TypedTranslation } from "../../../UI/i18n/Translation"
export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, LayerConfigJson> { export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, LayerConfigJson> {
/** /**
@ -101,7 +101,7 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, L
geoJsonZoomLevel: 10, geoJsonZoomLevel: 10,
maxCacheAge: 0, maxCacheAge: 0,
}, },
/* We need to set 'pass_all_features' /* We need to set 'pass_all_features'
There are probably many note_import-layers, and we don't want the first one to gobble up all notes and then discard them... There are probably many note_import-layers, and we don't want the first one to gobble up all notes and then discard them...
*/ */
passAllFeatures: true, passAllFeatures: true,
@ -175,7 +175,7 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, L
render: tr(t.nearbyImagesIntro), render: tr(t.nearbyImagesIntro),
}, },
{ {
id:"all_tags", id: "all_tags",
render: "{all_tags()}", render: "{all_tags()}",
metacondition: { metacondition: {
or: [ or: [
@ -184,7 +184,7 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, L
"mapcomplete-show_debug=yes", "mapcomplete-show_debug=yes",
], ],
}, },
} },
], ],
mapRendering: [ mapRendering: [
{ {

View file

@ -24,8 +24,8 @@ import { TagConfigJson } from "../Json/TagConfigJson"
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson" import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson" import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
import ValidationUtils from "./ValidationUtils" import ValidationUtils from "./ValidationUtils"
import {RenderingSpecification} from "../../../UI/SpecialVisualization" import { RenderingSpecification } from "../../../UI/SpecialVisualization"
import {QuestionableTagRenderingConfigJson} from "../Json/QuestionableTagRenderingConfigJson" import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
class ExpandFilter extends DesugaringStep<LayerConfigJson> { class ExpandFilter extends DesugaringStep<LayerConfigJson> {
private static readonly predefinedFilters = ExpandFilter.load_filters() private static readonly predefinedFilters = ExpandFilter.load_filters()
@ -446,11 +446,11 @@ class DetectInline extends DesugaringStep<QuestionableTagRenderingConfigJson> {
information?: string[] information?: string[]
} { } {
if (json.freeform === undefined) { if (json.freeform === undefined) {
return {result: json} return { result: json }
} }
let spec: Record<string, string> let spec: Record<string, string>
if (typeof json.render === "string") { if (typeof json.render === "string") {
spec = {"*": json.render} spec = { "*": json.render }
} else { } else {
spec = <Record<string, string>>json.render spec = <Record<string, string>>json.render
} }
@ -459,7 +459,7 @@ class DetectInline extends DesugaringStep<QuestionableTagRenderingConfigJson> {
if (spec[key].indexOf("<a ") >= 0) { if (spec[key].indexOf("<a ") >= 0) {
// We have a link element, it probably contains something that needs to be substituted... // We have a link element, it probably contains something that needs to be substituted...
// Let's play this safe and not inline it // Let's play this safe and not inline it
return {result: json} return { result: json }
} }
const fullSpecification = SpecialVisualizations.constructSpecification(spec[key]) const fullSpecification = SpecialVisualizations.constructSpecification(spec[key])
if (fullSpecification.length > 1) { if (fullSpecification.length > 1) {
@ -467,19 +467,19 @@ class DetectInline extends DesugaringStep<QuestionableTagRenderingConfigJson> {
if (json.freeform.inline === true) { if (json.freeform.inline === true) {
errors.push( errors.push(
"At " + "At " +
context + context +
": 'inline' is set, but the rendering contains a special visualisation...\n " + ": 'inline' is set, but the rendering contains a special visualisation...\n " +
spec[key] spec[key]
) )
} }
json = JSON.parse(JSON.stringify(json)) json = JSON.parse(JSON.stringify(json))
json.freeform.inline = false json.freeform.inline = false
return {result: json, errors} return { result: json, errors }
} }
} }
json = JSON.parse(JSON.stringify(json)) json = JSON.parse(JSON.stringify(json))
json.freeform.inline ??= true json.freeform.inline ??= true
return {result: json, errors} return { result: json, errors }
} }
} }
@ -500,7 +500,7 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
json.tagRenderings === undefined || json.tagRenderings === undefined ||
json.tagRenderings.some((tr) => tr["id"] === "leftover-questions") json.tagRenderings.some((tr) => tr["id"] === "leftover-questions")
) { ) {
return {result: json} return { result: json }
} }
json = JSON.parse(JSON.stringify(json)) json = JSON.parse(JSON.stringify(json))
const allSpecials: Exclude<RenderingSpecification, string>[] = [] const allSpecials: Exclude<RenderingSpecification, string>[] = []
@ -521,8 +521,8 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
if (noLabels.length > 1) { if (noLabels.length > 1) {
errors.push( errors.push(
"At " + "At " +
context + context +
": multiple 'questions'-visualisations found which would show _all_ questions. Don't do this" ": multiple 'questions'-visualisations found which would show _all_ questions. Don't do this"
) )
} }
@ -546,24 +546,24 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
if (blacklisted?.length > 0 && used?.length > 0) { if (blacklisted?.length > 0 && used?.length > 0) {
errors.push( errors.push(
"At " + "At " +
context + context +
": the {questions()}-special rendering only supports either a blacklist OR a whitelist, but not both." + ": the {questions()}-special rendering only supports either a blacklist OR a whitelist, but not both." +
"\n Whitelisted: " + "\n Whitelisted: " +
used.join(", ") + used.join(", ") +
"\n Blacklisted: " + "\n Blacklisted: " +
blacklisted.join(", ") blacklisted.join(", ")
) )
} }
for (const usedLabel of used) { for (const usedLabel of used) {
if (!allLabels.has(usedLabel)) { if (!allLabels.has(usedLabel)) {
errors.push( errors.push(
"At " + "At " +
context + context +
": this layers specifies a special question element for label `" + ": this layers specifies a special question element for label `" +
usedLabel + usedLabel +
"`, but this label doesn't exist.\n" + "`, but this label doesn't exist.\n" +
" Available labels are " + " Available labels are " +
Array.from(allLabels).join(", ") Array.from(allLabels).join(", ")
) )
} }
seen.add(usedLabel) seen.add(usedLabel)
@ -619,7 +619,7 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
if (json.allowSplit && !ValidationUtils.hasSpecialVisualisation(json, "split_button")) { if (json.allowSplit && !ValidationUtils.hasSpecialVisualisation(json, "split_button")) {
json.tagRenderings.push({ json.tagRenderings.push({
id: "split-button", id: "split-button",
render: {"*": "{split_button()}"}, render: { "*": "{split_button()}" },
}) })
delete json.allowSplit delete json.allowSplit
} }
@ -627,13 +627,13 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
if (json.allowMove && !ValidationUtils.hasSpecialVisualisation(json, "move_button")) { if (json.allowMove && !ValidationUtils.hasSpecialVisualisation(json, "move_button")) {
json.tagRenderings.push({ json.tagRenderings.push({
id: "move-button", id: "move-button",
render: {"*": "{move_button()}"}, render: { "*": "{move_button()}" },
}) })
} }
if (json.deletion && !ValidationUtils.hasSpecialVisualisation(json, "delete_button")) { if (json.deletion && !ValidationUtils.hasSpecialVisualisation(json, "delete_button")) {
json.tagRenderings.push({ json.tagRenderings.push({
id: "delete-button", id: "delete-button",
render: {"*": "{delete_button()}"}, render: { "*": "{delete_button()}" },
}) })
} }
@ -650,7 +650,7 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
if (!ValidationUtils.hasSpecialVisualisation(json, "all_tags")) { if (!ValidationUtils.hasSpecialVisualisation(json, "all_tags")) {
const trc: TagRenderingConfigJson = { const trc: TagRenderingConfigJson = {
id: "all-tags", id: "all-tags",
render: {"*": "{all_tags()}"}, render: { "*": "{all_tags()}" },
metacondition: { metacondition: {
or: [ or: [
@ -663,7 +663,7 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
json.tagRenderings?.push(trc) json.tagRenderings?.push(trc)
} }
return {result: json} return { result: json }
} }
} }
@ -1161,31 +1161,37 @@ class PreparePointRendering extends Fuse<PointRenderingConfigJson | LineRenderin
class SetFullNodeDatabase extends DesugaringStep<LayerConfigJson> { class SetFullNodeDatabase extends DesugaringStep<LayerConfigJson> {
constructor() { constructor() {
super("sets the fullNodeDatabase-bit if needed", super(
"sets the fullNodeDatabase-bit if needed",
["fullNodeDatabase"], ["fullNodeDatabase"],
"SetFullNodeDatabase") "SetFullNodeDatabase"
)
} }
convert(json: LayerConfigJson, context: string): { convert(
result: LayerConfigJson; json: LayerConfigJson,
errors?: string[]; context: string
warnings?: string[]; ): {
result: LayerConfigJson
errors?: string[]
warnings?: string[]
information?: string[] information?: string[]
} { } {
const needsSpecial = json.tagRenderings?.some(tr => { const needsSpecial =
if (typeof tr === "string") { json.tagRenderings?.some((tr) => {
return false if (typeof tr === "string") {
} return false
const specs = ValidationUtils.getSpecialVisualisations(<TagRenderingConfigJson>tr) }
return specs?.some(sp => sp.needsNodeDatabase) const specs = ValidationUtils.getSpecialVisualisations(<TagRenderingConfigJson>tr)
}) ?? false return specs?.some((sp) => sp.needsNodeDatabase)
}) ?? false
if (!needsSpecial) { if (!needsSpecial) {
return {result: json} return { result: json }
} }
return { return {
result: {...json, fullNodeDatabase: true}, result: { ...json, fullNodeDatabase: true },
information: ["Layer " + json.id + " needs the fullNodeDatabase"] information: ["Layer " + json.id + " needs the fullNodeDatabase"],
}; }
} }
} }
@ -1203,12 +1209,12 @@ export class AddMiniMap extends DesugaringStep<LayerConfigJson> {
convert(layerConfig: LayerConfigJson, context: string): { result: LayerConfigJson } { convert(layerConfig: LayerConfigJson, context: string): { result: LayerConfigJson } {
if (!layerConfig.tagRenderings || layerConfig.source === "special") { if (!layerConfig.tagRenderings || layerConfig.source === "special") {
return {result: layerConfig} return { result: layerConfig }
} }
const state = this._state const state = this._state
const hasMinimap = ValidationUtils.hasSpecialVisualisation(layerConfig, "minimap") const hasMinimap = ValidationUtils.hasSpecialVisualisation(layerConfig, "minimap")
if (!hasMinimap) { if (!hasMinimap) {
layerConfig = {...layerConfig} layerConfig = { ...layerConfig }
layerConfig.tagRenderings = [...layerConfig.tagRenderings] layerConfig.tagRenderings = [...layerConfig.tagRenderings]
const minimap = state.tagRenderings.get("minimap") const minimap = state.tagRenderings.get("minimap")
if (minimap === undefined) { if (minimap === undefined) {

View file

@ -1,8 +1,8 @@
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson" import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import {Utils} from "../../../Utils" import { Utils } from "../../../Utils"
import SpecialVisualizations from "../../../UI/SpecialVisualizations" import SpecialVisualizations from "../../../UI/SpecialVisualizations"
import {RenderingSpecification, SpecialVisualization} from "../../../UI/SpecialVisualization" import { RenderingSpecification, SpecialVisualization } from "../../../UI/SpecialVisualization"
import {LayerConfigJson} from "../Json/LayerConfigJson" import { LayerConfigJson } from "../Json/LayerConfigJson"
export default class ValidationUtils { export default class ValidationUtils {
public static hasSpecialVisualisation( public static hasSpecialVisualisation(
@ -11,14 +11,15 @@ export default class ValidationUtils {
): boolean { ): boolean {
return ( return (
layer.tagRenderings?.some((tagRendering) => { layer.tagRenderings?.some((tagRendering) => {
if(tagRendering === undefined){ if (tagRendering === undefined) {
return false return false
}
const spec = ValidationUtils.getSpecialVisualisations(<TagRenderingConfigJson>tagRendering)
return spec.some((vis) => vis.funcName === specialVisualisation);
} }
) ?? false
const spec = ValidationUtils.getSpecialVisualisations(
<TagRenderingConfigJson>tagRendering
)
return spec.some((vis) => vis.funcName === specialVisualisation)
}) ?? false
) )
} }
@ -44,7 +45,7 @@ export default class ValidationUtils {
const all: RenderingSpecification[] = [] const all: RenderingSpecification[] = []
for (let translation of translations) { for (let translation of translations) {
if (typeof translation == "string") { if (typeof translation == "string") {
translation = {"*": translation} translation = { "*": translation }
} }
for (const key in translation) { for (const key in translation) {

View file

@ -3,9 +3,9 @@ import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import { DeleteConfigJson } from "./Json/DeleteConfigJson" import { DeleteConfigJson } from "./Json/DeleteConfigJson"
import Translations from "../../UI/i18n/Translations" import Translations from "../../UI/i18n/Translations"
import { TagUtils } from "../../Logic/Tags/TagUtils" import { TagUtils } from "../../Logic/Tags/TagUtils"
import TagRenderingConfig from "./TagRenderingConfig"; import TagRenderingConfig from "./TagRenderingConfig"
import {QuestionableTagRenderingConfigJson} from "./Json/QuestionableTagRenderingConfigJson"; import { QuestionableTagRenderingConfigJson } from "./Json/QuestionableTagRenderingConfigJson"
import {TagConfigJson} from "./Json/TagConfigJson"; import { TagConfigJson } from "./Json/TagConfigJson"
export default class DeleteConfig { export default class DeleteConfig {
public static readonly deleteReasonKey = "_delete_reason" public static readonly deleteReasonKey = "_delete_reason"
@ -97,24 +97,24 @@ export default class DeleteConfig {
public constructTagRendering(): TagRenderingConfig { public constructTagRendering(): TagRenderingConfig {
const t = Translations.t.delete const t = Translations.t.delete
const mappings: {if: TagConfigJson, then: Record<string, string>} []= [] const mappings: { if: TagConfigJson; then: Record<string, string> }[] = []
for (const nonDeleteMapping of this.nonDeleteMappings) { for (const nonDeleteMapping of this.nonDeleteMappings) {
mappings.push({ mappings.push({
if: nonDeleteMapping.if, if: nonDeleteMapping.if,
then: nonDeleteMapping.then.translations then: nonDeleteMapping.then.translations,
}) })
} }
for (const deleteReason of this.deleteReasons) { for (const deleteReason of this.deleteReasons) {
mappings.push({ mappings.push({
if: DeleteConfig.deleteReasonKey+ "="+ deleteReason.changesetMessage, if: DeleteConfig.deleteReasonKey + "=" + deleteReason.changesetMessage,
then: deleteReason.explanation.translations then: deleteReason.explanation.translations,
}) })
} }
const config: QuestionableTagRenderingConfigJson = { const config: QuestionableTagRenderingConfigJson = {
question: t.whyDelete.translations, question: t.whyDelete.translations,
mappings mappings,
} }
return new TagRenderingConfig(config) return new TagRenderingConfig(config)
} }

View file

@ -103,7 +103,11 @@ export default class DependencyCalculator {
currentLine = i // Leak the state... currentLine = i // Leak the state...
currentKey = key currentKey = key
try { try {
const func = new Function("feat", "{"+ExtraFunctions.types.join(",")+"}", "return " + code + ";") const func = new Function(
"feat",
"{" + ExtraFunctions.types.join(",") + "}",
"return " + code + ";"
)
const result = func(obj, helpers) const result = func(obj, helpers)
obj.properties[key] = JSON.stringify(result) obj.properties[key] = JSON.stringify(result)
} catch (e) {} } catch (e) {}

View file

@ -31,7 +31,6 @@ export default interface LineRenderingConfigJson {
*/ */
lineCap?: "round" | "square" | "butt" | string | TagRenderingConfigJson lineCap?: "round" | "square" | "butt" | string | TagRenderingConfigJson
/** /**
* The color to fill a polygon with. * The color to fill a polygon with.
* If undefined, this will be slightly more opaque version of the stroke line. * If undefined, this will be slightly more opaque version of the stroke line.

View file

@ -417,7 +417,6 @@ export default class LayerConfig extends WithContextLoader {
) )
} }
this.popupInFloatover = json.popupInFloatover ?? false this.popupInFloatover = json.popupInFloatover ?? false
} }
public defaultIcon(): BaseUIElement | undefined { public defaultIcon(): BaseUIElement | undefined {

View file

@ -1,16 +1,16 @@
import PointRenderingConfigJson from "./Json/PointRenderingConfigJson" import PointRenderingConfigJson from "./Json/PointRenderingConfigJson"
import TagRenderingConfig from "./TagRenderingConfig" import TagRenderingConfig from "./TagRenderingConfig"
import {TagsFilter} from "../../Logic/Tags/TagsFilter" import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import {TagUtils} from "../../Logic/Tags/TagUtils" import { TagUtils } from "../../Logic/Tags/TagUtils"
import {Utils} from "../../Utils" import { Utils } from "../../Utils"
import Svg from "../../Svg" import Svg from "../../Svg"
import WithContextLoader from "./WithContextLoader" import WithContextLoader from "./WithContextLoader"
import {Store} from "../../Logic/UIEventSource" import { Store } from "../../Logic/UIEventSource"
import BaseUIElement from "../../UI/BaseUIElement" import BaseUIElement from "../../UI/BaseUIElement"
import {FixedUiElement} from "../../UI/Base/FixedUiElement" import { FixedUiElement } from "../../UI/Base/FixedUiElement"
import Img from "../../UI/Base/Img" import Img from "../../UI/Base/Img"
import Combine from "../../UI/Base/Combine" import Combine from "../../UI/Base/Combine"
import {VariableUiElement} from "../../UI/Base/VariableUIElement" import { VariableUiElement } from "../../UI/Base/VariableUIElement"
export default class PointRenderingConfig extends WithContextLoader { export default class PointRenderingConfig extends WithContextLoader {
static readonly allowed_location_codes: ReadonlySet<string> = new Set<string>([ static readonly allowed_location_codes: ReadonlySet<string> = new Set<string>([
@ -83,7 +83,7 @@ export default class PointRenderingConfig extends WithContextLoader {
} }
}) })
const iconPath = this.icon?.GetRenderValue({id: "node/-1"})?.txt const iconPath = this.icon?.GetRenderValue({ id: "node/-1" })?.txt
if (iconPath !== undefined && iconPath.startsWith(Utils.assets_path)) { if (iconPath !== undefined && iconPath.startsWith(Utils.assets_path)) {
const iconKey = iconPath.substr(Utils.assets_path.length) const iconKey = iconPath.substr(Utils.assets_path.length)
if (Svg.All[iconKey] === undefined) { if (Svg.All[iconKey] === undefined) {
@ -168,7 +168,7 @@ export default class PointRenderingConfig extends WithContextLoader {
noFullWidth?: boolean noFullWidth?: boolean
} }
): BaseUIElement { ): BaseUIElement {
tags = tags ?? {id: "node/-1"} tags = tags ?? { id: "node/-1" }
let defaultPin: BaseUIElement = undefined let defaultPin: BaseUIElement = undefined
if (this.label === undefined) { if (this.label === undefined) {
defaultPin = Svg.teardrop_with_hole_green_svg() defaultPin = Svg.teardrop_with_hole_green_svg()

View file

@ -1,20 +1,23 @@
import {Translation, TypedTranslation} from "../../UI/i18n/Translation" import { Translation, TypedTranslation } from "../../UI/i18n/Translation"
import {TagsFilter} from "../../Logic/Tags/TagsFilter" import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import Translations from "../../UI/i18n/Translations" import Translations from "../../UI/i18n/Translations"
import {TagUtils, UploadableTag} from "../../Logic/Tags/TagUtils" import { TagUtils, UploadableTag } from "../../Logic/Tags/TagUtils"
import {And} from "../../Logic/Tags/And" import { And } from "../../Logic/Tags/And"
import {Utils} from "../../Utils" import { Utils } from "../../Utils"
import {Tag} from "../../Logic/Tags/Tag" import { Tag } from "../../Logic/Tags/Tag"
import BaseUIElement from "../../UI/BaseUIElement" import BaseUIElement from "../../UI/BaseUIElement"
import Combine from "../../UI/Base/Combine" import Combine from "../../UI/Base/Combine"
import Title from "../../UI/Base/Title" import Title from "../../UI/Base/Title"
import Link from "../../UI/Base/Link" import Link from "../../UI/Base/Link"
import List from "../../UI/Base/List" import List from "../../UI/Base/List"
import {MappingConfigJson, QuestionableTagRenderingConfigJson,} from "./Json/QuestionableTagRenderingConfigJson" import {
import {FixedUiElement} from "../../UI/Base/FixedUiElement" MappingConfigJson,
import {Paragraph} from "../../UI/Base/Paragraph" QuestionableTagRenderingConfigJson,
} from "./Json/QuestionableTagRenderingConfigJson"
import { FixedUiElement } from "../../UI/Base/FixedUiElement"
import { Paragraph } from "../../UI/Base/Paragraph"
import Svg from "../../Svg" import Svg from "../../Svg"
import Validators, {ValidatorType} from "../../UI/InputElement/Validators"; import Validators, { ValidatorType } from "../../UI/InputElement/Validators"
export interface Mapping { export interface Mapping {
readonly if: UploadableTag readonly if: UploadableTag
@ -118,9 +121,9 @@ export default class TagRenderingConfig {
this.question = Translations.T(json.question, translationKey + ".question") this.question = Translations.T(json.question, translationKey + ".question")
this.questionhint = Translations.T(json.questionHint, translationKey + ".questionHint") this.questionhint = Translations.T(json.questionHint, translationKey + ".questionHint")
this.description = Translations.T(json.description, translationKey + ".description") this.description = Translations.T(json.description, translationKey + ".description")
this.condition = TagUtils.Tag(json.condition ?? {and: []}, `${context}.condition`) this.condition = TagUtils.Tag(json.condition ?? { and: [] }, `${context}.condition`)
this.metacondition = TagUtils.Tag( this.metacondition = TagUtils.Tag(
json.metacondition ?? {and: []}, json.metacondition ?? { and: [] },
`${context}.metacondition` `${context}.metacondition`
) )
if (json.freeform) { if (json.freeform) {
@ -537,11 +540,8 @@ export default class TagRenderingConfig {
} }
} }
if ( if (this.freeform?.key === undefined || tags[this.freeform.key] !== undefined) {
this.freeform?.key === undefined || return { then: this.render }
tags[this.freeform.key] !== undefined
) {
return {then: this.render}
} }
return undefined return undefined
@ -681,13 +681,13 @@ export default class TagRenderingConfig {
) )
} }
const and = TagUtils.FlattenMultiAnswer([...selectedMappings, ...unselectedMappings]) const and = TagUtils.FlattenMultiAnswer([...selectedMappings, ...unselectedMappings])
if(and.and.length === 0){ if (and.and.length === 0) {
return undefined return undefined
} }
return and return and
} else { } else {
// Is at least one mapping shown in the answer? // Is at least one mapping shown in the answer?
const someMappingIsShown = this.mappings.some(m => { const someMappingIsShown = this.mappings.some((m) => {
if (typeof m.hideInAnswer === "boolean") { if (typeof m.hideInAnswer === "boolean") {
return !m.hideInAnswer return !m.hideInAnswer
} }
@ -695,7 +695,9 @@ export default class TagRenderingConfig {
return !isHidden return !isHidden
}) })
// If all mappings are hidden for the current tags, we can safely assume that we should use the freeform key // If all mappings are hidden for the current tags, we can safely assume that we should use the freeform key
const useFreeform = freeformValue !== undefined && (singleSelectedMapping === this.mappings.length || !someMappingIsShown) const useFreeform =
freeformValue !== undefined &&
(singleSelectedMapping === this.mappings.length || !someMappingIsShown)
if (useFreeform) { if (useFreeform) {
return new And([ return new And([
new Tag(this.freeform.key, freeformValue), new Tag(this.freeform.key, freeformValue),
@ -711,7 +713,7 @@ export default class TagRenderingConfig {
freeformValue, freeformValue,
singleSelectedMapping, singleSelectedMapping,
multiSelectedMapping, multiSelectedMapping,
currentProperties currentProperties,
}) })
return undefined return undefined
} }
@ -757,7 +759,7 @@ export default class TagRenderingConfig {
if (m.ifnot !== undefined) { if (m.ifnot !== undefined) {
msgs.push( msgs.push(
"Unselecting this answer will add " + "Unselecting this answer will add " +
m.ifnot.asHumanString(true, false, {}) m.ifnot.asHumanString(true, false, {})
) )
} }
return msgs return msgs
@ -787,12 +789,12 @@ export default class TagRenderingConfig {
this.description, this.description,
this.question !== undefined this.question !== undefined
? new Combine([ ? new Combine([
"The question is ", "The question is ",
new FixedUiElement(this.question.txt).SetClass("font-bold bold"), new FixedUiElement(this.question.txt).SetClass("font-bold bold"),
]) ])
: new FixedUiElement( : new FixedUiElement(
"This tagrendering has no question and is thus read-only" "This tagrendering has no question and is thus read-only"
).SetClass("italic"), ).SetClass("italic"),
new Combine(withRender), new Combine(withRender),
mappings, mappings,
condition, condition,

View file

@ -21,7 +21,10 @@ export default class WithContextLoader {
if (deflt === undefined) { if (deflt === undefined) {
return undefined return undefined
} }
return new TagRenderingConfig(deflt, `${translationContext ?? this._context}.${key}.default value`) return new TagRenderingConfig(
deflt,
`${translationContext ?? this._context}.${key}.default value`
)
} }
if (typeof v === "string") { if (typeof v === "string") {
const shared = SharedTagRenderings.SharedTagRendering.get(v) const shared = SharedTagRenderings.SharedTagRendering.get(v)

View file

@ -1,23 +1,27 @@
import LayoutConfig from "./ThemeConfig/LayoutConfig" import LayoutConfig from "./ThemeConfig/LayoutConfig"
import {SpecialVisualizationState} from "../UI/SpecialVisualization" import { SpecialVisualizationState } from "../UI/SpecialVisualization"
import {Changes} from "../Logic/Osm/Changes" import { Changes } from "../Logic/Osm/Changes"
import {ImmutableStore, Store, UIEventSource} from "../Logic/UIEventSource" import { ImmutableStore, Store, UIEventSource } from "../Logic/UIEventSource"
import {FeatureSource, IndexedFeatureSource, WritableFeatureSource,} from "../Logic/FeatureSource/FeatureSource" import {
import {OsmConnection} from "../Logic/Osm/OsmConnection" FeatureSource,
import {ExportableMap, MapProperties} from "./MapProperties" IndexedFeatureSource,
WritableFeatureSource,
} from "../Logic/FeatureSource/FeatureSource"
import { OsmConnection } from "../Logic/Osm/OsmConnection"
import { ExportableMap, MapProperties } from "./MapProperties"
import LayerState from "../Logic/State/LayerState" import LayerState from "../Logic/State/LayerState"
import {Feature, Point, Polygon} from "geojson" import { Feature, Point, Polygon } from "geojson"
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
import {Map as MlMap} from "maplibre-gl" import { Map as MlMap } from "maplibre-gl"
import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning" import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning"
import {MapLibreAdaptor} from "../UI/Map/MapLibreAdaptor" import { MapLibreAdaptor } from "../UI/Map/MapLibreAdaptor"
import {GeoLocationState} from "../Logic/State/GeoLocationState" import { GeoLocationState } from "../Logic/State/GeoLocationState"
import FeatureSwitchState from "../Logic/State/FeatureSwitchState" import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
import {QueryParameters} from "../Logic/Web/QueryParameters" import { QueryParameters } from "../Logic/Web/QueryParameters"
import UserRelatedState from "../Logic/State/UserRelatedState" import UserRelatedState from "../Logic/State/UserRelatedState"
import LayerConfig from "./ThemeConfig/LayerConfig" import LayerConfig from "./ThemeConfig/LayerConfig"
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler" import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
import {AvailableRasterLayers, RasterLayerPolygon, RasterLayerUtils} from "./RasterLayers" import { AvailableRasterLayers, RasterLayerPolygon, RasterLayerUtils } from "./RasterLayers"
import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource" import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource"
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource" import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"
import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore" import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore"
@ -28,24 +32,24 @@ import TitleHandler from "../Logic/Actors/TitleHandler"
import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor" import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor"
import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader" import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader"
import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater" import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater"
import {BBox} from "../Logic/BBox" import { BBox } from "../Logic/BBox"
import Constants from "./Constants" import Constants from "./Constants"
import Hotkeys from "../UI/Base/Hotkeys" import Hotkeys from "../UI/Base/Hotkeys"
import Translations from "../UI/i18n/Translations" import Translations from "../UI/i18n/Translations"
import {GeoIndexedStoreForLayer} from "../Logic/FeatureSource/Actors/GeoIndexedStore" import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
import {LastClickFeatureSource} from "../Logic/FeatureSource/Sources/LastClickFeatureSource" import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource"
import {MenuState} from "./MenuState" import { MenuState } from "./MenuState"
import MetaTagging from "../Logic/MetaTagging" import MetaTagging from "../Logic/MetaTagging"
import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator" import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator"
import {NewGeometryFromChangesFeatureSource} from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource" import { NewGeometryFromChangesFeatureSource } from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource"
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader" import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"
import ShowOverlayRasterLayer from "../UI/Map/ShowOverlayRasterLayer" import ShowOverlayRasterLayer from "../UI/Map/ShowOverlayRasterLayer"
import {Utils} from "../Utils" import { Utils } from "../Utils"
import {EliCategory} from "./RasterLayerProperties" import { EliCategory } from "./RasterLayerProperties"
import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter" import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter"
import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage" import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage"
import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor"; import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor"
/** /**
* *
@ -146,7 +150,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
rasterInfo.defaultState ?? true, rasterInfo.defaultState ?? true,
"Wether or not overlayer layer " + rasterInfo.id + " is shown" "Wether or not overlayer layer " + rasterInfo.id + " is shown"
) )
const state = {isDisplayed} const state = { isDisplayed }
overlayLayerStates.set(rasterInfo.id, state) overlayLayerStates.set(rasterInfo.id, state)
new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state) new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state)
} }
@ -158,8 +162,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
* A bit tricky, as this is heavily intertwined with the 'changes'-element, which generate a stream of new and changed features too * A bit tricky, as this is heavily intertwined with the 'changes'-element, which generate a stream of new and changed features too
*/ */
if (this.layout.layers.some((l) => l._needsFullNodeDatabase)) {
if (this.layout.layers.some(l => l._needsFullNodeDatabase)) {
this.fullNodeDatabase = new FullNodeDatabaseSource() this.fullNodeDatabase = new FullNodeDatabaseSource()
} }
@ -176,18 +179,18 @@ export default class ThemeViewState implements SpecialVisualizationState {
let currentViewIndex = 0 let currentViewIndex = 0
this.currentView = new StaticFeatureSource( this.currentView = new StaticFeatureSource(
this.mapProperties.bounds.map((bbox) => { this.mapProperties.bounds.map((bbox) => {
if (!bbox) { if (!bbox) {
return empty return empty
}
currentViewIndex++
return <Feature[]>[bbox.asGeoJson({
zoom: this.mapProperties.zoom.data,
...this.mapProperties.location.data,
id: "current_view"
}
)];
} }
) currentViewIndex++
return <Feature[]>[
bbox.asGeoJson({
zoom: this.mapProperties.zoom.data,
...this.mapProperties.location.data,
id: "current_view",
}),
]
})
) )
this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds) this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds)
this.dataIsLoading = layoutSource.isLoading this.dataIsLoading = layoutSource.isLoading
@ -355,7 +358,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
private initHotkeys() { private initHotkeys() {
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{nomod: "Escape", onUp: true}, { nomod: "Escape", onUp: true },
Translations.t.hotkeyDocumentation.closeSidebar, Translations.t.hotkeyDocumentation.closeSidebar,
() => { () => {
this.selectedElement.setData(undefined) this.selectedElement.setData(undefined)
@ -376,7 +379,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
) )
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{shift: "O"}, { shift: "O" },
Translations.t.hotkeyDocumentation.selectMapnik, Translations.t.hotkeyDocumentation.selectMapnik,
() => { () => {
this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto) this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto)
@ -395,17 +398,17 @@ export default class ThemeViewState implements SpecialVisualizationState {
} }
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{nomod: "O"}, { nomod: "O" },
Translations.t.hotkeyDocumentation.selectOsmbasedmap, Translations.t.hotkeyDocumentation.selectOsmbasedmap,
() => setLayerCategory("osmbasedmap") () => setLayerCategory("osmbasedmap")
) )
Hotkeys.RegisterHotkey({nomod: "M"}, Translations.t.hotkeyDocumentation.selectMap, () => Hotkeys.RegisterHotkey({ nomod: "M" }, Translations.t.hotkeyDocumentation.selectMap, () =>
setLayerCategory("map") setLayerCategory("map")
) )
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{nomod: "P"}, { nomod: "P" },
Translations.t.hotkeyDocumentation.selectAerial, Translations.t.hotkeyDocumentation.selectAerial,
() => setLayerCategory("photo") () => setLayerCategory("photo")
) )
@ -473,10 +476,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
), ),
range: new StaticFeatureSource( range: new StaticFeatureSource(
this.mapProperties.maxbounds.map((bbox) => this.mapProperties.maxbounds.map((bbox) =>
bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({id: "range"})] bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({ id: "range" })]
) )
), ),
current_view: this.currentView current_view: this.currentView,
} }
if (this.layout?.lockLocation) { if (this.layout?.lockLocation) {
const bbox = new BBox(this.layout.lockLocation) const bbox = new BBox(this.layout.lockLocation)
@ -487,12 +490,19 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.featureSwitches.featureSwitchIsTesting this.featureSwitches.featureSwitchIsTesting
) )
} }
const currentViewLayer = this.layout.layers.find(l => l.id === "current_view") const currentViewLayer = this.layout.layers.find((l) => l.id === "current_view")
if (currentViewLayer?.tagRenderings?.length > 0) { if (currentViewLayer?.tagRenderings?.length > 0) {
const params = MetaTagging.createExtraFuncParams(this) const params = MetaTagging.createExtraFuncParams(this)
this.featureProperties.trackFeatureSource(specialLayers.current_view) this.featureProperties.trackFeatureSource(specialLayers.current_view)
specialLayers.current_view.features.addCallbackAndRunD(features => { specialLayers.current_view.features.addCallbackAndRunD((features) => {
MetaTagging.addMetatags(features, params, currentViewLayer, this.layout, this.osmObjectDownloader, this.featureProperties) MetaTagging.addMetatags(
features,
params,
currentViewLayer,
this.layout,
this.osmObjectDownloader,
this.featureProperties
)
}) })
} }
@ -537,7 +547,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
}) })
} }
{ {
this.selectedElement.addCallback(selected => { this.selectedElement.addCallback((selected) => {
if (selected === undefined) { if (selected === undefined) {
// We did _unselect_ an item - we always remove the lastclick-object // We did _unselect_ an item - we always remove the lastclick-object
this.lastClickObject.features.setData([]) this.lastClickObject.features.setData([])

View file

@ -123,8 +123,15 @@ export class Unit {
) )
) )
if(json.defaultInput && !applicable.some(denom => denom.canonical.trim() === json.defaultInput)){ if (
throw `${ctx}: no denomination has the specified default denomination. The default denomination is '${json.defaultInput}', but the available denominations are ${applicable.map(denom => denom.canonical).join(", ")}` json.defaultInput &&
!applicable.some((denom) => denom.canonical.trim() === json.defaultInput)
) {
throw `${ctx}: no denomination has the specified default denomination. The default denomination is '${
json.defaultInput
}', but the available denominations are ${applicable
.map((denom) => denom.canonical)
.join(", ")}`
} }
return new Unit(appliesTo, applicable, json.eraseInvalidValues ?? false) return new Unit(appliesTo, applicable, json.eraseInvalidValues ?? false)
} }

View file

@ -1,4 +1,4 @@
import SvelteUIElement from "./UI/Base/SvelteUIElement"; import SvelteUIElement from "./UI/Base/SvelteUIElement"
import StylesheetTestGui from "./UI/StylesheetTestGui.svelte"; import StylesheetTestGui from "./UI/StylesheetTestGui.svelte"
new SvelteUIElement(StylesheetTestGui, {}).AttachTo("main") new SvelteUIElement(StylesheetTestGui, {}).AttachTo("main")

View file

@ -9,8 +9,8 @@ import IndexText from "./BigComponents/IndexText"
import { LoginToggle } from "./Popup/LoginButton" import { LoginToggle } from "./Popup/LoginButton"
import { ImmutableStore } from "../Logic/UIEventSource" import { ImmutableStore } from "../Logic/UIEventSource"
import { OsmConnection } from "../Logic/Osm/OsmConnection" import { OsmConnection } from "../Logic/Osm/OsmConnection"
import {QueryParameters} from "../Logic/Web/QueryParameters"; import { QueryParameters } from "../Logic/Web/QueryParameters"
import {OsmConnectionFeatureSwitches} from "../Logic/State/FeatureSwitchState"; import { OsmConnectionFeatureSwitches } from "../Logic/State/FeatureSwitchState"
export default class AllThemesGui { export default class AllThemesGui {
setup() { setup() {
@ -27,9 +27,10 @@ export default class AllThemesGui {
}) })
const state = new UserRelatedState(osmConnection) const state = new UserRelatedState(osmConnection)
const intro = new Combine([ const intro = new Combine([
new LanguagePicker(Translations.t.index.title.SupportedLanguages(), state.language).SetClass( new LanguagePicker(
"flex absolute top-2 right-3" Translations.t.index.title.SupportedLanguages(),
), state.language
).SetClass("flex absolute top-2 right-3"),
new IndexText(), new IndexText(),
]) ])
new Combine([ new Combine([

View file

@ -1,17 +1,20 @@
<script lang="ts"> <script lang="ts">
/** /**
* Wrapper around 'subtleButton' with an arrow pointing to the right * Wrapper around 'subtleButton' with an arrow pointing to the right
* See also: NextButton * See also: NextButton
*/ */
import SubtleButton from "./SubtleButton.svelte"; import SubtleButton from "./SubtleButton.svelte"
import {ChevronLeftIcon} from "@rgossiaux/svelte-heroicons/solid"; import { ChevronLeftIcon } from "@rgossiaux/svelte-heroicons/solid"
import {createEventDispatcher} from "svelte"; import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher<{ click }>() const dispatch = createEventDispatcher<{ click }>()
export let clss = "" export let clss = ""
</script> </script>
<SubtleButton on:click={() => dispatch("click")} options={{extraClasses:clss+ " flex items-center"}}> <SubtleButton
<ChevronLeftIcon class="w-12 h-12" slot="image"/> on:click={() => dispatch("click")}
<slot slot="message"/> options={{ extraClasses: clss + " flex items-center" }}
>
<ChevronLeftIcon class="w-12 h-12" slot="image" />
<slot slot="message" />
</SubtleButton> </SubtleButton>

View file

@ -1,13 +1,12 @@
<script lang="ts"> <script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource.js"; import { UIEventSource } from "../../Logic/UIEventSource.js"
/** /**
* For some stupid reason, it is very hard to bind inputs * For some stupid reason, it is very hard to bind inputs
*/ */
export let selected: UIEventSource<boolean>; export let selected: UIEventSource<boolean>
let _c: boolean = selected.data ?? true; let _c: boolean = selected.data ?? true
$: selected.setData(_c) $: selected.setData(_c)
</script> </script>
<input type="checkbox" bind:checked={_c} /> <input type="checkbox" bind:checked={_c} />

View file

@ -3,84 +3,82 @@
* This overlay element will regularly show a hand that swipes over the underlying element. * This overlay element will regularly show a hand that swipes over the underlying element.
* This element will hide as soon as the Store 'hideSignal' receives a change (which is not undefined) * This element will hide as soon as the Store 'hideSignal' receives a change (which is not undefined)
*/ */
import { Store } from "../../Logic/UIEventSource"; import { Store } from "../../Logic/UIEventSource"
import { onDestroy } from "svelte"; import { onDestroy } from "svelte"
let mainElem: HTMLElement; let mainElem: HTMLElement
export let hideSignal: Store<any>; export let hideSignal: Store<any>
function hide(){ function hide() {
mainElem.style.visibility = "hidden"; mainElem.style.visibility = "hidden"
} }
if (hideSignal) { if (hideSignal) {
onDestroy(hideSignal.addCallbackD(() => { onDestroy(
console.log("Received hide signal") hideSignal.addCallbackD(() => {
hide() console.log("Received hide signal")
return true; hide()
})); return true
})
)
} }
$: {
mainElem?.addEventListener("click",_ => hide())
mainElem?.addEventListener("touchstart",_ => hide())
}
</script>
$: {
mainElem?.addEventListener("click", (_) => hide())
mainElem?.addEventListener("touchstart", (_) => hide())
}
</script>
<div bind:this={mainElem} class="absolute bottom-0 right-0 w-full h-full"> <div bind:this={mainElem} class="absolute bottom-0 right-0 w-full h-full">
<div id="hand-container" class="pointer-events-none"> <div id="hand-container" class="pointer-events-none">
<img src="./assets/svg/hand.svg"/> <img src="./assets/svg/hand.svg" />
</div> </div>
</div> </div>
<style> <style>
@keyframes hand-drag-animation {
@keyframes hand-drag-animation { /* This is the animation on the little extra hand on the location input. If fades in, invites the user to interact/drag the map */
/* This is the animation on the little extra hand on the location input. If fades in, invites the user to interact/drag the map */ 0% {
0% { opacity: 0;
opacity: 0; transform: rotate(-30deg);
transform: rotate(-30deg);
}
6% {
opacity: 1;
transform: rotate(-30deg);
}
12% {
opacity: 1;
transform: rotate(-45deg);
}
24% {
opacity: 1;
transform: rotate(-00deg);
}
30% {
opacity: 1;
transform: rotate(-30deg);
}
36% {
opacity: 0;
transform: rotate(-30deg);
}
100% {
opacity: 0;
transform: rotate(-30deg);
}
} }
6% {
opacity: 1;
transform: rotate(-30deg);
}
#hand-container { 12% {
position: absolute; opacity: 1;
width: 2rem; transform: rotate(-45deg);
left: calc(50% + 4rem); }
top: calc(50%);
opacity: 0.7; 24% {
animation: hand-drag-animation 4s ease-in-out infinite; opacity: 1;
transform-origin: 50% 125%; transform: rotate(-00deg);
}
30% {
opacity: 1;
transform: rotate(-30deg);
}
36% {
opacity: 0;
transform: rotate(-30deg);
}
100% {
opacity: 0;
transform: rotate(-30deg);
}
}
#hand-container {
position: absolute;
width: 2rem;
left: calc(50% + 4rem);
top: calc(50%);
opacity: 0.7;
animation: hand-drag-animation 4s ease-in-out infinite;
transform-origin: 50% 125%;
} }
</style> </style>

View file

@ -1,15 +1,14 @@
<script lang="ts"> <script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource.js"; import { UIEventSource } from "../../Logic/UIEventSource.js"
/** /**
* For some stupid reason, it is very hard to bind inputs * For some stupid reason, it is very hard to bind inputs
*/ */
export let value: UIEventSource<number>; export let value: UIEventSource<number>
let i: number = value.data; let i: number = value.data
$: value.setData(i) $: value.setData(i)
</script> </script>
<select bind:value={i} > <select bind:value={i}>
<slot></slot> <slot />
</select> </select>

View file

@ -1,31 +1,36 @@
<script lang="ts"> <script lang="ts">
import {createEventDispatcher} from "svelte"; import { createEventDispatcher } from "svelte"
import {XCircleIcon} from "@rgossiaux/svelte-heroicons/solid"; import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
/** /**
* The slotted element will be shown on top, with a lower-opacity border * The slotted element will be shown on top, with a lower-opacity border
*/ */
const dispatch = createEventDispatcher<{ close }>(); const dispatch = createEventDispatcher<{ close }>()
</script> </script>
<div class="absolute top-0 right-0 w-screen h-screen p-4 md:p-6" style="background-color: #00000088"> <div
<div class="content normal-background"> class="absolute top-0 right-0 w-screen h-screen p-4 md:p-6"
<div class="rounded-xl h-full"> style="background-color: #00000088"
<slot></slot> >
</div> <div class="content normal-background">
<slot name="close-button"> <div class="rounded-xl h-full">
<!-- The close button is placed _after_ the default slot in order to always paint it on top --> <slot />
<div class="w-8 h-8 absolute right-10 top-10 cursor-pointer" on:click={() => dispatch("close")}>
<XCircleIcon/>
</div>
</slot>
</div> </div>
<slot name="close-button">
<!-- The close button is placed _after_ the default slot in order to always paint it on top -->
<div
class="w-8 h-8 absolute right-10 top-10 cursor-pointer"
on:click={() => dispatch("close")}
>
<XCircleIcon />
</div>
</slot>
</div>
</div> </div>
<style> <style>
.content { .content {
height: calc( 100vh - 2rem ); height: calc(100vh - 2rem);
border-radius: 0.5rem; border-radius: 0.5rem;
overflow-x: auto; overflow-x: auto;
box-shadow: 0 0 1rem #00000088; box-shadow: 0 0 1rem #00000088;

View file

@ -3,11 +3,11 @@
* Given an HTML string, properly shows this * Given an HTML string, properly shows this
*/ */
export let src: string; export let src: string
let htmlElem: HTMLElement; let htmlElem: HTMLElement
$: { $: {
if (htmlElem) { if (htmlElem) {
htmlElem.innerHTML = src; htmlElem.innerHTML = src
} }
} }
@ -15,6 +15,5 @@
</script> </script>
{#if src !== undefined} {#if src !== undefined}
<span bind:this={htmlElem} class={clss}></span> <span bind:this={htmlElem} class={clss} />
{/if} {/if}

View file

@ -1,23 +1,24 @@
<script lang="ts"> <script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"; import { UIEventSource } from "../../Logic/UIEventSource"
import { onDestroy } from "svelte"; import { onDestroy } from "svelte"
/** /**
* For some stupid reason, it is very hard to let {#if} work together with UIEventSources, so we wrap then here * For some stupid reason, it is very hard to let {#if} work together with UIEventSources, so we wrap then here
*/ */
export let condition: UIEventSource<boolean>; export let condition: UIEventSource<boolean>
let _c = condition.data; let _c = condition.data
onDestroy(condition.addCallback(c => { onDestroy(
/* Do _not_ abbreviate this as `.addCallback(c => _c = c)`. This is the same as writing `.addCallback(c => {return _c = c})`, condition.addCallback((c) => {
/* Do _not_ abbreviate this as `.addCallback(c => _c = c)`. This is the same as writing `.addCallback(c => {return _c = c})`,
which will _unregister_ the callback if `c = true`! */ which will _unregister_ the callback if `c = true`! */
_c = c; _c = c
return false return false
})) })
)
</script> </script>
{#if _c} {#if _c}
<slot></slot> <slot />
{:else} {:else}
<slot name="else"></slot> <slot name="else" />
{/if} {/if}

View file

@ -1,33 +1,34 @@
<script lang="ts"> <script lang="ts">
import {UIEventSource} from "../../Logic/UIEventSource"; import { UIEventSource } from "../../Logic/UIEventSource"
import {onDestroy} from "svelte"; import { onDestroy } from "svelte"
/** /**
* Functions as 'If', but uses 'display:hidden' instead. * Functions as 'If', but uses 'display:hidden' instead.
*/ */
export let condition: UIEventSource<boolean>; export let condition: UIEventSource<boolean>
let _c = condition.data; let _c = condition.data
let hasBeenShownPositive = false let hasBeenShownPositive = false
let hasBeenShownNegative = false let hasBeenShownNegative = false
onDestroy(condition.addCallbackAndRun(c => { onDestroy(
/* Do _not_ abbreviate this as `.addCallback(c => _c = c)`. This is the same as writing `.addCallback(c => {return _c = c})`, condition.addCallbackAndRun((c) => {
/* Do _not_ abbreviate this as `.addCallback(c => _c = c)`. This is the same as writing `.addCallback(c => {return _c = c})`,
which will _unregister_ the callback if `c = true`! */ which will _unregister_ the callback if `c = true`! */
hasBeenShownPositive = hasBeenShownPositive || c hasBeenShownPositive = hasBeenShownPositive || c
hasBeenShownNegative = hasBeenShownNegative || !c hasBeenShownNegative = hasBeenShownNegative || !c
_c = c; _c = c
return false return false
})) })
)
</script> </script>
{#if hasBeenShownPositive} {#if hasBeenShownPositive}
<span class={_c ? "" : "hidden"}> <span class={_c ? "" : "hidden"}>
<slot/> <slot />
</span> </span>
{/if} {/if}
{#if hasBeenShownNegative} {#if hasBeenShownNegative}
<span class={_c ? "hidden" : ""}> <span class={_c ? "hidden" : ""}>
<slot name="else"/> <slot name="else" />
</span> </span>
{/if} {/if}

View file

@ -1,18 +1,20 @@
<script lang="ts"> <script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"; import { UIEventSource } from "../../Logic/UIEventSource"
import { onDestroy } from "svelte"; import { onDestroy } from "svelte"
/** /**
* For some stupid reason, it is very hard to let {#if} work together with UIEventSources, so we wrap then here * For some stupid reason, it is very hard to let {#if} work together with UIEventSources, so we wrap then here
*/ */
export let condition: UIEventSource<boolean>; export let condition: UIEventSource<boolean>
let _c = !condition.data; let _c = !condition.data
onDestroy(condition.addCallback(c => { onDestroy(
_c = !c; condition.addCallback((c) => {
return false _c = !c
})) return false
})
)
</script> </script>
{#if _c} {#if _c}
<slot></slot> <slot />
{/if} {/if}

View file

@ -1,13 +1,13 @@
<script> <script>
import ToSvelte from "./ToSvelte.svelte"; import ToSvelte from "./ToSvelte.svelte"
import Svg from "../../Svg"; import Svg from "../../Svg"
</script> </script>
<div class="pl-2 p-1 flex"> <div class="pl-2 p-1 flex">
<div class="animate-spin self-center w-6 h-6 min-w-6"> <div class="animate-spin self-center w-6 h-6 min-w-6">
<ToSvelte construct={Svg.loading_svg()}></ToSvelte> <ToSvelte construct={Svg.loading_svg()} />
</div> </div>
<div class="ml-2"> <div class="ml-2">
<slot></slot> <slot />
</div> </div>
</div> </div>

View file

@ -1,17 +1,17 @@
<script lang="ts"> <script lang="ts">
import {OsmConnection} from "../../Logic/Osm/OsmConnection"; import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Translations from "../i18n/Translations.js"; import Translations from "../i18n/Translations.js"
import Tr from "./Tr.svelte"; import Tr from "./Tr.svelte"
import ToSvelte from "./ToSvelte.svelte"; import ToSvelte from "./ToSvelte.svelte"
import Svg from "../../Svg"; import Svg from "../../Svg"
export let osmConnection: OsmConnection export let osmConnection: OsmConnection
export let clss = "" export let clss = ""
</script> </script>
<button class={clss} on:click={() => osmConnection.AttemptLogin()}> <button class={clss} on:click={() => osmConnection.AttemptLogin()}>
<ToSvelte construct={Svg.login_svg().SetClass("w-12 m-1")}/> <ToSvelte construct={Svg.login_svg().SetClass("w-12 m-1")} />
<slot name="message"> <slot name="message">
<Tr t={Translations.t.general.loginWithOpenStreetMap}/> <Tr t={Translations.t.general.loginWithOpenStreetMap} />
</slot> </slot>
</button> </button>

View file

@ -1,47 +1,45 @@
<script lang="ts"> <script lang="ts">
import Loading from "./Loading.svelte"; import Loading from "./Loading.svelte"
import type { OsmServiceState } from "../../Logic/Osm/OsmConnection"; import type { OsmServiceState } from "../../Logic/Osm/OsmConnection"
import { Translation } from "../i18n/Translation"; import { Translation } from "../i18n/Translation"
import Translations from "../i18n/Translations"; import Translations from "../i18n/Translations"
import Tr from "./Tr.svelte"; import Tr from "./Tr.svelte"
import {OsmConnection} from "../../Logic/Osm/OsmConnection"; import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import {ImmutableStore, UIEventSource} from "../../Logic/UIEventSource"; import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource"
export let state: {osmConnection: OsmConnection, featureSwitches?: { featureSwitchUserbadge?: UIEventSource<boolean>}}; export let state: {
osmConnection: OsmConnection
featureSwitches?: { featureSwitchUserbadge?: UIEventSource<boolean> }
}
/** /**
* If set, 'loading' will act as if we are already logged in. * If set, 'loading' will act as if we are already logged in.
*/ */
export let ignoreLoading: boolean = false export let ignoreLoading: boolean = false
let loadingStatus = state.osmConnection.loadingStatus; let loadingStatus = state.osmConnection.loadingStatus
let badge = state.featureSwitches?.featureSwitchUserbadge ?? new ImmutableStore(true); let badge = state.featureSwitches?.featureSwitchUserbadge ?? new ImmutableStore(true)
const t = Translations.t.general; const t = Translations.t.general
const offlineModes: Partial<Record<OsmServiceState, Translation>> = { const offlineModes: Partial<Record<OsmServiceState, Translation>> = {
offline: t.loginFailedOfflineMode, offline: t.loginFailedOfflineMode,
unreachable: t.loginFailedUnreachableMode, unreachable: t.loginFailedUnreachableMode,
unknown: t.loginFailedUnreachableMode, unknown: t.loginFailedUnreachableMode,
readonly: t.loginFailedReadonlyMode readonly: t.loginFailedReadonlyMode,
}; }
const apiState = state.osmConnection.apiIsOnline; const apiState = state.osmConnection.apiIsOnline
</script> </script>
{#if $badge} {#if $badge}
{#if !ignoreLoading && $loadingStatus === "loading"} {#if !ignoreLoading && $loadingStatus === "loading"}
<slot name="loading"> <slot name="loading">
<Loading></Loading> <Loading />
</slot> </slot>
{:else if $loadingStatus === "error"} {:else if $loadingStatus === "error"}
<div class="flex items-center alert max-w-64"> <div class="flex items-center alert max-w-64">
<img src="./assets/svg/invalid.svg" class="w-8 h-8 m-2 shrink-0"> <img src="./assets/svg/invalid.svg" class="w-8 h-8 m-2 shrink-0" />
<Tr t={offlineModes[$apiState]} /> <Tr t={offlineModes[$apiState]} />
</div> </div>
{:else if $loadingStatus === "logged-in"} {:else if $loadingStatus === "logged-in"}
<slot></slot> <slot />
{:else if $loadingStatus === "not-attempted"} {:else if $loadingStatus === "not-attempted"}
<slot name="not-logged-in"> <slot name="not-logged-in" />
</slot>
{/if} {/if}
{/if} {/if}

View file

@ -1,14 +1,16 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte"
/** /**
* A round button with an icon and possible a small text, which hovers above the map * A round button with an icon and possible a small text, which hovers above the map
*/ */
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let cls = "" export let cls = ""
</script> </script>
<button
<button on:click={e => dispatch("click", e)} class={"rounded-full h-fit w-fit m-0.5 md:m-1 p-0.5 sm:p-1 pointer-events-auto "+cls} > on:click={(e) => dispatch("click", e)}
<slot/> class={"rounded-full h-fit w-fit m-0.5 md:m-1 p-0.5 sm:p-1 pointer-events-auto " + cls}
>
<slot />
</button> </button>

View file

@ -1,20 +1,26 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte"
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"; import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
/** /**
* The slotted element will be shown on the right side * The slotted element will be shown on the right side
*/ */
const dispatch = createEventDispatcher<{ close }>(); const dispatch = createEventDispatcher<{ close }>()
</script> </script>
<div class="absolute top-0 right-0 h-screen overflow-auto w-full md:w-6/12 lg:w-5/12 xl:w-4/12 drop-shadow-2xl" style="max-width: 100vw; max-height: 100vh"> <div
class="absolute top-0 right-0 h-screen overflow-auto w-full md:w-6/12 lg:w-5/12 xl:w-4/12 drop-shadow-2xl"
style="max-width: 100vw; max-height: 100vh"
>
<div class="flex flex-col m-0 normal-background"> <div class="flex flex-col m-0 normal-background">
<slot name="close-button"> <slot name="close-button">
<div class="w-8 h-8 absolute right-10 top-10 cursor-pointer" on:click={() => dispatch("close")}> <div
class="w-8 h-8 absolute right-10 top-10 cursor-pointer"
on:click={() => dispatch("close")}
>
<XCircleIcon /> <XCircleIcon />
</div> </div>
</slot> </slot>
<slot></slot> <slot />
</div> </div>
</div> </div>

View file

@ -1,21 +1,24 @@
<script lang="ts"> <script lang="ts">
/** /**
* Wrapper around 'subtleButton' with an arrow pointing to the right * Wrapper around 'subtleButton' with an arrow pointing to the right
* See also: BackButton * See also: BackButton
*/ */
import SubtleButton from "./SubtleButton.svelte"; import SubtleButton from "./SubtleButton.svelte"
import {ChevronRightIcon} from "@rgossiaux/svelte-heroicons/solid"; import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid"
import {createEventDispatcher} from "svelte"; import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher<{ click }>() const dispatch = createEventDispatcher<{ click }>()
export let clss : string= "" export let clss: string = ""
</script> </script>
<SubtleButton on:click={() => dispatch("click")} options={{extraClasses: clss+" flex items-center"}}> <SubtleButton
<slot name="image" slot="image"/> on:click={() => dispatch("click")}
<div class="w-full flex justify-between items-center" slot="message"> options={{ extraClasses: clss + " flex items-center" }}
<slot/> >
<ChevronRightIcon class="w-12 h-12"/> <slot name="image" slot="image" />
</div> <div class="w-full flex justify-between items-center" slot="message">
<slot />
<ChevronRightIcon class="w-12 h-12" />
</div>
</SubtleButton> </SubtleButton>

View file

@ -1,33 +1,30 @@
<script lang="ts"> <script lang="ts">
import ToSvelte from "./ToSvelte.svelte"
import ToSvelte from "./ToSvelte.svelte"; import Svg from "../../Svg"
import Svg from "../../Svg";
export let generateShareData: () => { export let generateShareData: () => {
text: string text: string
title: string title: string
url: string url: string
} }
function share(){ function share() {
if (!navigator.share) { if (!navigator.share) {
console.log("web share not supported") console.log("web share not supported")
return; return
} }
navigator navigator
.share(generateShareData()) .share(generateShareData())
.then(() => { .then(() => {
console.log("Thanks for sharing!") console.log("Thanks for sharing!")
}) })
.catch((err) => { .catch((err) => {
console.log(`Couldn't share because of`, err.message) console.log(`Couldn't share because of`, err.message)
}) })
} }
</script> </script>
<button on:click={share} class="secondary w-8 h-8 m-0 p-0"> <button on:click={share} class="secondary w-8 h-8 m-0 p-0">
<slot name="content"> <slot name="content">
<ToSvelte construct={Svg.share_svg().SetClass("w-7 h-7 p-1")}/> <ToSvelte construct={Svg.share_svg().SetClass("w-7 h-7 p-1")} />
</slot> </slot>
</button> </button>

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import {createEventDispatcher} from "svelte"; import { createEventDispatcher } from "svelte"
import BaseUIElement from "../BaseUIElement"; import BaseUIElement from "../BaseUIElement"
import Img from "./Img"; import Img from "./Img"
export let imageUrl: string | BaseUIElement = undefined export let imageUrl: string | BaseUIElement = undefined
export let message: string | BaseUIElement = undefined export let message: string | BaseUIElement = undefined
@ -10,22 +10,22 @@
extraClasses?: string extraClasses?: string
} = {} } = {}
let imgClasses = "block justify-center shrink-0 mr-4 " + (options?.imgSize ?? "h-11 w-11"); let imgClasses = "block justify-center shrink-0 mr-4 " + (options?.imgSize ?? "h-11 w-11")
const dispatch = createEventDispatcher<{click}>() const dispatch = createEventDispatcher<{ click }>()
</script> </script>
<button <button
class={(options.extraClasses??"") + ' secondary no-image-background'} class={(options.extraClasses ?? "") + " secondary no-image-background"}
target={options?.newTab ? "_blank" : ""} target={options?.newTab ? "_blank" : ""}
on:click={(e) => dispatch("click", e)} on:click={(e) => dispatch("click", e)}
> >
<slot name="image"> <slot name="image">
{#if imageUrl !== undefined} {#if imageUrl !== undefined}
{#if typeof imageUrl === "string"} {#if typeof imageUrl === "string"}
<Img src={imageUrl} class={imgClasses}></Img> <Img src={imageUrl} class={imgClasses} />
{/if} {/if}
{/if} {/if}
</slot> </slot>
<slot name="message"/> <slot name="message" />
</button> </button>

View file

@ -5,10 +5,10 @@ import { VariableUiElement } from "./VariableUIElement"
import Lazy from "./Lazy" import Lazy from "./Lazy"
import Loading from "./Loading" import Loading from "./Loading"
import SvelteUIElement from "./SvelteUIElement" import SvelteUIElement from "./SvelteUIElement"
import SubtleLink from "./SubtleLink.svelte"; import SubtleLink from "./SubtleLink.svelte"
import Translations from "../i18n/Translations"; import Translations from "../i18n/Translations"
import Combine from "./Combine"; import Combine from "./Combine"
import Img from "./Img"; import Img from "./Img"
/** /**
* @deprecated * @deprecated
@ -40,26 +40,28 @@ export class SubtleButton extends UIElement {
} }
protected InnerRender(): string | BaseUIElement { protected InnerRender(): string | BaseUIElement {
if(this.options.url !== undefined){ if (this.options.url !== undefined) {
return new SvelteUIElement(SubtleLink, {href: this.options.url, newTab: this.options.newTab}) return new SvelteUIElement(SubtleLink, {
href: this.options.url,
newTab: this.options.newTab,
})
} }
const classes = "button"; const classes = "button"
const message = Translations.W(this.message)?.SetClass("block overflow-ellipsis no-images flex-shrink"); const message = Translations.W(this.message)?.SetClass(
let img; "block overflow-ellipsis no-images flex-shrink"
const imgClasses = "block justify-center flex-none mr-4 " + (this.options?.imgSize ?? "h-11 w-11") )
let img
const imgClasses =
"block justify-center flex-none mr-4 " + (this.options?.imgSize ?? "h-11 w-11")
if ((this.imageUrl ?? "") === "") { if ((this.imageUrl ?? "") === "") {
img = undefined; img = undefined
} else if (typeof (this.imageUrl) === "string") { } else if (typeof this.imageUrl === "string") {
img = new Img(this.imageUrl)?.SetClass(imgClasses) img = new Img(this.imageUrl)?.SetClass(imgClasses)
} else { } else {
img = this.imageUrl?.SetClass(imgClasses); img = this.imageUrl?.SetClass(imgClasses)
} }
const button = new Combine([ const button = new Combine([img, message]).SetClass("flex items-center group w-full")
img,
message
]).SetClass("flex items-center group w-full")
this.SetClass(classes) this.SetClass(classes)
return button return button

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte"; import { onMount } from "svelte"
import BaseUIElement from "../BaseUIElement"; import BaseUIElement from "../BaseUIElement"
import Img from "./Img"; import Img from "./Img"
export let imageUrl: string | BaseUIElement = undefined export let imageUrl: string | BaseUIElement = undefined
export let href: string export let href: string
@ -10,10 +10,9 @@
imgSize?: string imgSize?: string
// extraClasses?: string // extraClasses?: string
} = {} } = {}
let imgElem: HTMLElement; let imgElem: HTMLElement
let imgClasses = "block justify-center shrink-0 mr-4 " + (options?.imgSize ?? "h-11 w-11"); let imgClasses = "block justify-center shrink-0 mr-4 " + (options?.imgSize ?? "h-11 w-11")
onMount(() => { onMount(() => {
// Image // Image
@ -27,24 +26,23 @@
} }
if (img) imgElem.replaceWith(img.ConstructElement()) if (img) imgElem.replaceWith(img.ConstructElement())
} }
}) })
</script> </script>
<a <a
class={(options.extraClasses??"") + ' button text-ellipsis'} class={(options.extraClasses ?? "") + " button text-ellipsis"}
{href} {href}
target={(newTab ? "_blank" : undefined) } target={newTab ? "_blank" : undefined}
> >
<slot name="image"> <slot name="image">
{#if imageUrl !== undefined} {#if imageUrl !== undefined}
{#if typeof imageUrl === "string"} {#if typeof imageUrl === "string"}
<Img src={imageUrl} class={imgClasses}></Img> <Img src={imageUrl} class={imgClasses} />
{:else } {:else}
<template bind:this={imgElem} /> <template bind:this={imgElem} />
{/if} {/if}
{/if} {/if}
</slot> </slot>
<slot/> <slot />
</a> </a>

View file

@ -1,121 +1,126 @@
<script lang="ts"> <script lang="ts">
/** /**
* Thin wrapper around 'TabGroup' which binds the state * Thin wrapper around 'TabGroup' which binds the state
*/ */
import {Tab, TabGroup, TabList, TabPanel, TabPanels} from "@rgossiaux/svelte-headlessui"; import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui"
import {UIEventSource} from "../../Logic/UIEventSource"; import { UIEventSource } from "../../Logic/UIEventSource"
export let tab: UIEventSource<number>; export let tab: UIEventSource<number>
let tabElements: HTMLElement[] = []; let tabElements: HTMLElement[] = []
$: tabElements[$tab]?.click(); $: tabElements[$tab]?.click()
$: { $: {
if (tabElements[tab.data]) { if (tabElements[tab.data]) {
window.setTimeout(() => tabElements[tab.data].click(), 50) window.setTimeout(() => tabElements[tab.data].click(), 50)
}
} }
}
</script> </script>
<div class="tabbedgroup w-full h-full"> <div class="tabbedgroup w-full h-full">
<TabGroup class="h-full w-full flex flex-col" defaultIndex={1} <TabGroup
on:change={(e) =>{if(e.detail >= 0){tab.setData( e.detail); }} }> class="h-full w-full flex flex-col"
<div class="interactive flex items-center justify-between sticky top-0"> defaultIndex={1}
<TabList class="flex flex-wrap"> on:change={(e) => {
{#if $$slots.title1} if (e.detail >= 0) {
<Tab class={({selected}) => "tab "+(selected ? "primary" : "")}> tab.setData(e.detail)
<div bind:this={tabElements[0]} class="flex"> }
<slot name="title0"> }}
Tab 0 >
</slot> <div class="interactive flex items-center justify-between sticky top-0">
</div> <TabList class="flex flex-wrap">
</Tab> {#if $$slots.title1}
{/if} <Tab class={({ selected }) => "tab " + (selected ? "primary" : "")}>
{#if $$slots.title1} <div bind:this={tabElements[0]} class="flex">
<Tab class={({selected}) => "tab "+(selected ? "primary" : "")}> <slot name="title0">Tab 0</slot>
<div bind:this={tabElements[1]} class="flex"> </div>
<slot name="title1"/> </Tab>
</div> {/if}
</Tab> {#if $$slots.title1}
{/if} <Tab class={({ selected }) => "tab " + (selected ? "primary" : "")}>
{#if $$slots.title2} <div bind:this={tabElements[1]} class="flex">
<Tab class={({selected}) => "tab "+(selected ? "primary" : "")}> <slot name="title1" />
<div bind:this={tabElements[2]} class="flex"> </div>
<slot name="title2"/> </Tab>
</div> {/if}
</Tab> {#if $$slots.title2}
{/if} <Tab class={({ selected }) => "tab " + (selected ? "primary" : "")}>
{#if $$slots.title3} <div bind:this={tabElements[2]} class="flex">
<Tab class={({selected}) => "tab "+(selected ? "primary" : "")}> <slot name="title2" />
<div bind:this={tabElements[3]} class="flex"> </div>
<slot name="title3"/> </Tab>
</div> {/if}
</Tab> {#if $$slots.title3}
{/if} <Tab class={({ selected }) => "tab " + (selected ? "primary" : "")}>
{#if $$slots.title4} <div bind:this={tabElements[3]} class="flex">
<Tab class={({selected}) => "tab "+(selected ? "primary" : "")}> <slot name="title3" />
<div bind:this={tabElements[4]} class="flex"> </div>
<slot name="title4"/> </Tab>
</div> {/if}
</Tab> {#if $$slots.title4}
{/if} <Tab class={({ selected }) => "tab " + (selected ? "primary" : "")}>
</TabList> <div bind:this={tabElements[4]} class="flex">
<slot name="post-tablist"/> <slot name="title4" />
</div> </div>
<div class="overflow-y-auto normal-background"> </Tab>
{/if}
<TabPanels defaultIndex={$tab}> </TabList>
<TabPanel> <slot name="post-tablist" />
<slot name="content0"> </div>
<div>Empty</div> <div class="overflow-y-auto normal-background">
</slot> <TabPanels defaultIndex={$tab}>
</TabPanel> <TabPanel>
<TabPanel> <slot name="content0">
<slot name="content1"/> <div>Empty</div>
</TabPanel> </slot>
<TabPanel> </TabPanel>
<slot name="content2"/> <TabPanel>
</TabPanel> <slot name="content1" />
<TabPanel> </TabPanel>
<slot name="content3"/> <TabPanel>
</TabPanel> <slot name="content2" />
<TabPanel> </TabPanel>
<slot name="content4"/> <TabPanel>
</TabPanel> <slot name="content3" />
</TabPanels> </TabPanel>
</div> <TabPanel>
</TabGroup> <slot name="content4" />
</TabPanel>
</TabPanels>
</div>
</TabGroup>
</div> </div>
<style> <style>
.tabbedgroup { .tabbedgroup {
max-height: 100vh; max-height: 100vh;
height: 100%; height: 100%;
} }
:global(.tab) { :global(.tab) {
margin: 0.25rem; margin: 0.25rem;
padding: 0.25rem; padding: 0.25rem;
padding-left: 0.75rem; padding-left: 0.75rem;
padding-right: 0.75rem; padding-right: 0.75rem;
border-radius: 1rem; border-radius: 1rem;
} }
:global(.tab .flex) { :global(.tab .flex) {
align-items: center; align-items: center;
gap: 0.25rem; gap: 0.25rem;
} }
:global(.tab span|div) { :global(.tab span|div) {
align-items: center; align-items: center;
gap: 0.25rem; gap: 0.25rem;
display: flex; display: flex;
} }
:global(.tab-selected svg) { :global(.tab-selected svg) {
fill: var(--catch-detail-color-contrast); fill: var(--catch-detail-color-contrast);
} }
:global(.tab-unselected) { :global(.tab-unselected) {
background-color: var(--background-color) !important; background-color: var(--background-color) !important;
color: var(--foreground-color) !important; color: var(--foreground-color) !important;
} }
</style> </style>

View file

@ -78,7 +78,7 @@ export default class Table extends BaseUIElement {
for (let j = 0; j < row.length; j++) { for (let j = 0; j < row.length; j++) {
try { try {
let elem = row[j] let elem = row[j]
if(elem?.ConstructElement === undefined){ if (elem?.ConstructElement === undefined) {
continue continue
} }
const htmlElem = elem?.ConstructElement() const htmlElem = elem?.ConstructElement()

View file

@ -1,23 +1,21 @@
<script lang="ts"> <script lang="ts">
import BaseUIElement from "../BaseUIElement.js"; import BaseUIElement from "../BaseUIElement.js"
import { onDestroy, onMount } from "svelte"; import { onDestroy, onMount } from "svelte"
export let construct: BaseUIElement | (() => BaseUIElement); export let construct: BaseUIElement | (() => BaseUIElement)
let elem: HTMLElement; let elem: HTMLElement
let html: HTMLElement; let html: HTMLElement
onMount(() => { onMount(() => {
const uiElem = typeof construct === "function" const uiElem = typeof construct === "function" ? construct() : construct
? construct() : construct; html = uiElem?.ConstructElement()
html =uiElem?.ConstructElement();
if (html !== undefined) { if (html !== undefined) {
elem.replaceWith(html); elem.replaceWith(html)
} }
}); })
onDestroy(() => { onDestroy(() => {
html?.remove(); html?.remove()
}); })
</script> </script>
<span bind:this={elem} /> <span bind:this={elem} />

View file

@ -2,36 +2,37 @@
/** /**
* Properly renders a translation * Properly renders a translation
*/ */
import { Translation } from "../i18n/Translation"; import { Translation } from "../i18n/Translation"
import { onDestroy } from "svelte"; import { onDestroy } from "svelte"
import Locale from "../i18n/Locale"; import Locale from "../i18n/Locale"
import { Utils } from "../../Utils"; import { Utils } from "../../Utils"
import FromHtml from "./FromHtml.svelte"; import FromHtml from "./FromHtml.svelte"
import WeblateLink from "./WeblateLink.svelte"; import WeblateLink from "./WeblateLink.svelte"
export let t: Translation; export let t: Translation
export let cls: string = "" export let cls: string = ""
export let tags: Record<string, string> | undefined = undefined; export let tags: Record<string, string> | undefined = undefined
// Text for the current language // Text for the current language
let txt: string | undefined; let txt: string | undefined
$: onDestroy(Locale.language.addCallbackAndRunD(l => {
const translation = t?.textFor(l);
if (translation === undefined) {
return;
}
if (tags) {
txt = Utils.SubstituteKeys(txt, tags);
} else {
txt = translation;
}
}));
$: onDestroy(
Locale.language.addCallbackAndRunD((l) => {
const translation = t?.textFor(l)
if (translation === undefined) {
return
}
if (tags) {
txt = Utils.SubstituteKeys(txt, tags)
} else {
txt = translation
}
})
)
</script> </script>
{#if t} {#if t}
<span class={cls}> <span class={cls}>
<FromHtml src={txt}></FromHtml> <FromHtml src={txt} />
<WeblateLink context={t.context}></WeblateLink> <WeblateLink context={t.context} />
</span> </span>
{/if} {/if}

View file

@ -1,25 +1,32 @@
<script lang="ts"> <script lang="ts">
import Locale from "../i18n/Locale"; import Locale from "../i18n/Locale"
import LinkToWeblate from "./LinkToWeblate"; import LinkToWeblate from "./LinkToWeblate"
/** /**
* Shows a small icon which will open up weblate; a contributor can translate the item for 'context' there * Shows a small icon which will open up weblate; a contributor can translate the item for 'context' there
*/ */
export let context : string export let context: string
let linkToWeblate = Locale.showLinkToWeblate; let linkToWeblate = Locale.showLinkToWeblate
let linkOnMobile = Locale.showLinkOnMobile; let linkOnMobile = Locale.showLinkOnMobile
let language = Locale.language; let language = Locale.language
</script> </script>
{#if !!context && context.indexOf(":") > 0} {#if !!context && context.indexOf(":") > 0}
{#if $linkOnMobile} {#if $linkOnMobile}
<a href={LinkToWeblate.hrefToWeblate($language, context)} target="_blank" class="mx-1 weblate-link"> <a
href={LinkToWeblate.hrefToWeblate($language, context)}
target="_blank"
class="mx-1 weblate-link"
>
<img src="./assets/svg/translate.svg" class="font-gray" /> <img src="./assets/svg/translate.svg" class="font-gray" />
</a> </a>
{:else if $linkToWeblate} {:else if $linkToWeblate}
<a href={LinkToWeblate.hrefToWeblate($language, context)} class="weblate-link hidden-on-mobile mx-1" target="_blank"> <a
href={LinkToWeblate.hrefToWeblate($language, context)}
class="weblate-link hidden-on-mobile mx-1"
target="_blank"
>
<img src="./assets/svg/translate.svg" class="font-gray inline-block" /> <img src="./assets/svg/translate.svg" class="font-gray inline-block" />
</a> </a>
{/if} {/if}

View file

@ -1,80 +1,88 @@
<script lang="ts"> <script lang="ts">
import {Store, UIEventSource} from "../../Logic/UIEventSource"; import { Store, UIEventSource } from "../../Logic/UIEventSource"
import type {RasterLayerPolygon} from "../../Models/RasterLayers"; import type { RasterLayerPolygon } from "../../Models/RasterLayers"
import {AvailableRasterLayers} from "../../Models/RasterLayers"; import { AvailableRasterLayers } from "../../Models/RasterLayers"
import {createEventDispatcher, onDestroy} from "svelte"; import { createEventDispatcher, onDestroy } from "svelte"
import Svg from "../../Svg"; import Svg from "../../Svg"
import {Map as MlMap} from "maplibre-gl" import { Map as MlMap } from "maplibre-gl"
import type {MapProperties} from "../../Models/MapProperties"; import type { MapProperties } from "../../Models/MapProperties"
import OverlayMap from "../Map/OverlayMap.svelte"; import OverlayMap from "../Map/OverlayMap.svelte"
import RasterLayerPicker from "../Map/RasterLayerPicker.svelte"; import RasterLayerPicker from "../Map/RasterLayerPicker.svelte"
export let mapproperties: MapProperties export let mapproperties: MapProperties
export let normalMap: UIEventSource<MlMap> export let normalMap: UIEventSource<MlMap>
/** /**
* The current background (raster) layer of the polygon. * The current background (raster) layer of the polygon.
* This is undefined if a vector layer is used * This is undefined if a vector layer is used
*/ */
let rasterLayer: UIEventSource<RasterLayerPolygon | undefined> = mapproperties.rasterLayer let rasterLayer: UIEventSource<RasterLayerPolygon | undefined> = mapproperties.rasterLayer
let name = rasterLayer.data?.properties?.name let name = rasterLayer.data?.properties?.name
let icon = Svg.satellite_svg() let icon = Svg.satellite_svg()
onDestroy(rasterLayer.addCallback(polygon => { onDestroy(
name = polygon.properties?.name rasterLayer.addCallback((polygon) => {
})) name = polygon.properties?.name
/** })
* The layers that this component can offer as a choice. )
*/ /**
export let availableRasterLayers: Store<RasterLayerPolygon[]> * The layers that this component can offer as a choice.
*/
export let availableRasterLayers: Store<RasterLayerPolygon[]>
let raster0 = new UIEventSource<RasterLayerPolygon>(undefined) let raster0 = new UIEventSource<RasterLayerPolygon>(undefined)
let raster1 = new UIEventSource<RasterLayerPolygon>(undefined) let raster1 = new UIEventSource<RasterLayerPolygon>(undefined)
let currentLayer: RasterLayerPolygon let currentLayer: RasterLayerPolygon
function updatedAltLayer() { function updatedAltLayer() {
const available = availableRasterLayers.data const available = availableRasterLayers.data
const current = rasterLayer.data const current = rasterLayer.data
const defaultLayer = AvailableRasterLayers.maplibre const defaultLayer = AvailableRasterLayers.maplibre
const firstOther = available.find(l => l !== defaultLayer) const firstOther = available.find((l) => l !== defaultLayer)
const secondOther = available.find(l => l !== defaultLayer && l !== firstOther) const secondOther = available.find((l) => l !== defaultLayer && l !== firstOther)
raster0.setData(firstOther === current ? defaultLayer : firstOther) raster0.setData(firstOther === current ? defaultLayer : firstOther)
raster1.setData(secondOther === current ? defaultLayer : secondOther) raster1.setData(secondOther === current ? defaultLayer : secondOther)
}
updatedAltLayer()
onDestroy(mapproperties.rasterLayer.addCallbackAndRunD(updatedAltLayer))
onDestroy(availableRasterLayers.addCallbackAndRunD(updatedAltLayer))
function use(rasterLayer: UIEventSource<RasterLayerPolygon>): () => void {
return () => {
currentLayer = undefined
mapproperties.rasterLayer.setData(rasterLayer.data)
} }
}
updatedAltLayer() const dispatch = createEventDispatcher<{ copyright_clicked }>()
onDestroy(mapproperties.rasterLayer.addCallbackAndRunD(updatedAltLayer))
onDestroy(availableRasterLayers.addCallbackAndRunD(updatedAltLayer))
function use(rasterLayer: UIEventSource<RasterLayerPolygon>): (() => void) {
return () => {
currentLayer = undefined
mapproperties.rasterLayer.setData(rasterLayer.data)
}
}
const dispatch = createEventDispatcher<{ copyright_clicked }>()
</script> </script>
<div class="flex items-end opacity-50 hover:opacity-100"> <div class="flex items-end opacity-50 hover:opacity-100">
<div class="flex flex-col md:flex-row"> <div class="flex flex-col md:flex-row">
<button class="w-16 h-12 md:w-16 md:h-16 overflow-hidden m-0 p-0" <button class="w-16 h-12 md:w-16 md:h-16 overflow-hidden m-0 p-0" on:click={use(raster0)}>
on:click={use(raster0)}> <OverlayMap
<OverlayMap placedOverMap={normalMap} placedOverMapProperties={mapproperties} rasterLayer={raster0}/> placedOverMap={normalMap}
</button> placedOverMapProperties={mapproperties}
<button class="w-16 h-12 md:w-16 md:h-16 overflow-hidden m-0 p-0 " on:click={use(raster1)}> rasterLayer={raster0}
<OverlayMap placedOverMap={normalMap} placedOverMapProperties={mapproperties} rasterLayer={raster1}/> />
</button> </button>
</div> <button class="w-16 h-12 md:w-16 md:h-16 overflow-hidden m-0 p-0 " on:click={use(raster1)}>
<div class="text-sm flex flex-col gap-y-1 h-fit ml-1"> <OverlayMap
placedOverMap={normalMap}
<div class="low-interaction rounded p-1 w-64"> placedOverMapProperties={mapproperties}
<RasterLayerPicker availableLayers={availableRasterLayers} value={mapproperties.rasterLayer}></RasterLayerPicker> rasterLayer={raster1}
</div> />
</button>
<button class="small" on:click={() => dispatch("copyright_clicked")}> </div>
© OpenStreetMap <div class="text-sm flex flex-col gap-y-1 h-fit ml-1">
</button> <div class="low-interaction rounded p-1 w-64">
<RasterLayerPicker
availableLayers={availableRasterLayers}
value={mapproperties.rasterLayer}
/>
</div> </div>
<button class="small" on:click={() => dispatch("copyright_clicked")}>© OpenStreetMap</button>
</div>
</div> </div>

View file

@ -1,25 +1,25 @@
import Combine from "../Base/Combine" import Combine from "../Base/Combine"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import {Store} from "../../Logic/UIEventSource" import { Store } from "../../Logic/UIEventSource"
import {FixedUiElement} from "../Base/FixedUiElement" import { FixedUiElement } from "../Base/FixedUiElement"
import licenses from "../../assets/generated/license_info.json" import licenses from "../../assets/generated/license_info.json"
import SmallLicense from "../../Models/smallLicense" import SmallLicense from "../../Models/smallLicense"
import {Utils} from "../../Utils" import { Utils } from "../../Utils"
import Link from "../Base/Link" import Link from "../Base/Link"
import {VariableUiElement} from "../Base/VariableUIElement" import { VariableUiElement } from "../Base/VariableUIElement"
import contributors from "../../assets/contributors.json" import contributors from "../../assets/contributors.json"
import translators from "../../assets/translators.json" import translators from "../../assets/translators.json"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import Title from "../Base/Title" import Title from "../Base/Title"
import {BBox} from "../../Logic/BBox" import { BBox } from "../../Logic/BBox"
import {OsmConnection} from "../../Logic/Osm/OsmConnection" import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants"
import ContributorCount from "../../Logic/ContributorCount" import ContributorCount from "../../Logic/ContributorCount"
import Img from "../Base/Img" import Img from "../Base/Img"
import {TypedTranslation} from "../i18n/Translation" import { TypedTranslation } from "../i18n/Translation"
import GeoIndexedStore from "../../Logic/FeatureSource/Actors/GeoIndexedStore" import GeoIndexedStore from "../../Logic/FeatureSource/Actors/GeoIndexedStore"
import {RasterLayerPolygon} from "../../Models/RasterLayers"; import { RasterLayerPolygon } from "../../Models/RasterLayers"
/** /**
* The attribution panel in the theme menu. * The attribution panel in the theme menu.
@ -29,7 +29,10 @@ export default class CopyrightPanel extends Combine {
constructor(state: { constructor(state: {
layout: LayoutConfig layout: LayoutConfig
mapProperties: { readonly bounds: Store<BBox>, readonly rasterLayer: Store<RasterLayerPolygon> } mapProperties: {
readonly bounds: Store<BBox>
readonly rasterLayer: Store<RasterLayerPolygon>
}
osmConnection: OsmConnection osmConnection: OsmConnection
dataIsLoading: Store<boolean> dataIsLoading: Store<boolean>
perLayer: ReadonlyMap<string, GeoIndexedStore> perLayer: ReadonlyMap<string, GeoIndexedStore>
@ -90,27 +93,34 @@ export default class CopyrightPanel extends Combine {
new Title(t.attributionTitle), new Title(t.attributionTitle),
t.attributionContent, t.attributionContent,
new VariableUiElement(state.mapProperties.rasterLayer.mapD(layer => { new VariableUiElement(
const props = layer.properties state.mapProperties.rasterLayer.mapD((layer) => {
const attrUrl = props.attribution?.url const props = layer.properties
const attrText = props.attribution?.text const attrUrl = props.attribution?.url
const attrText = props.attribution?.text
let bgAttr: BaseUIElement | string = undefined let bgAttr: BaseUIElement | string = undefined
if(attrText && attrUrl){ if (attrText && attrUrl) {
bgAttr = "<a href='"+attrUrl+"' target='_blank'>"+attrText+"</a>" bgAttr =
}else if(attrUrl){ "<a href='" + attrUrl + "' target='_blank'>" + attrText + "</a>"
bgAttr = attrUrl } else if (attrUrl) {
}else{ bgAttr = attrUrl
bgAttr = attrText } else {
} bgAttr = attrText
if(bgAttr){ }
return Translations.t.general.attribution.attributionBackgroundLayerWithCopyright.Subs({ if (bgAttr) {
name: props.name, return Translations.t.general.attribution.attributionBackgroundLayerWithCopyright.Subs(
copyright: bgAttr {
}) name: props.name,
} copyright: bgAttr,
return Translations.t.general.attribution.attributionBackgroundLayer.Subs(props) }
})), )
}
return Translations.t.general.attribution.attributionBackgroundLayer.Subs(
props
)
})
),
maintainer, maintainer,
dataContributors, dataContributors,

View file

@ -1,21 +1,21 @@
import {UIElement} from "../UIElement" import { UIElement } from "../UIElement"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import {Store} from "../../Logic/UIEventSource" import { Store } from "../../Logic/UIEventSource"
import ExtraLinkConfig from "../../Models/ThemeConfig/ExtraLinkConfig" import ExtraLinkConfig from "../../Models/ThemeConfig/ExtraLinkConfig"
import Img from "../Base/Img" import Img from "../Base/Img"
import {SubtleButton} from "../Base/SubtleButton" import { SubtleButton } from "../Base/SubtleButton"
import Toggle from "../Input/Toggle" import Toggle from "../Input/Toggle"
import Locale from "../i18n/Locale" import Locale from "../i18n/Locale"
import {Utils} from "../../Utils" import { Utils } from "../../Utils"
import Svg from "../../Svg" import Svg from "../../Svg"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import {Translation} from "../i18n/Translation" import { Translation } from "../i18n/Translation"
interface ExtraLinkButtonState { interface ExtraLinkButtonState {
layout: { id: string; title: Translation } layout: { id: string; title: Translation }
featureSwitches: { featureSwitchWelcomeMessage: Store<boolean> }, featureSwitches: { featureSwitchWelcomeMessage: Store<boolean> }
mapProperties: { mapProperties: {
location: Store<{ lon: number, lat: number }>; location: Store<{ lon: number; lat: number }>
zoom: Store<number> zoom: Store<number>
} }
} }
@ -23,10 +23,7 @@ export default class ExtraLinkButton extends UIElement {
private readonly _config: ExtraLinkConfig private readonly _config: ExtraLinkConfig
private readonly state: ExtraLinkButtonState private readonly state: ExtraLinkButtonState
constructor( constructor(state: ExtraLinkButtonState, config: ExtraLinkConfig) {
state: ExtraLinkButtonState,
config: ExtraLinkConfig
) {
super() super()
this.state = state this.state = state
this._config = config this._config = config
@ -45,21 +42,24 @@ export default class ExtraLinkButton extends UIElement {
} }
if (c.requirements?.has("no-iframe") && isIframe) { if (c.requirements?.has("no-iframe") && isIframe) {
return undefined return undefined
} }
let link: BaseUIElement let link: BaseUIElement
const theme = this.state.layout?.id ?? "" const theme = this.state.layout?.id ?? ""
const basepath = window.location.host const basepath = window.location.host
const href = this.state.mapProperties.location.map((loc) => { const href = this.state.mapProperties.location.map(
const subs = { (loc) => {
...loc, const subs = {
theme: theme, ...loc,
basepath, theme: theme,
language: Locale.language.data, basepath,
} language: Locale.language.data,
return Utils.SubstituteKeys(c.href, subs) }
}, [this.state.mapProperties.zoom]) return Utils.SubstituteKeys(c.href, subs)
},
[this.state.mapProperties.zoom]
)
let img: BaseUIElement = Svg.pop_out_svg() let img: BaseUIElement = Svg.pop_out_svg()
if (c.icon !== undefined) { if (c.icon !== undefined) {
@ -81,11 +81,19 @@ export default class ExtraLinkButton extends UIElement {
}) })
if (c.requirements?.has("no-welcome-message")) { if (c.requirements?.has("no-welcome-message")) {
link = new Toggle(undefined, link, this.state.featureSwitches.featureSwitchWelcomeMessage) link = new Toggle(
undefined,
link,
this.state.featureSwitches.featureSwitchWelcomeMessage
)
} }
if (c.requirements?.has("welcome-message")) { if (c.requirements?.has("welcome-message")) {
link = new Toggle(link, undefined, this.state.featureSwitches.featureSwitchWelcomeMessage) link = new Toggle(
link,
undefined,
this.state.featureSwitches.featureSwitchWelcomeMessage
)
} }
return link return link

View file

@ -1,107 +1,110 @@
<script lang="ts">/** <script lang="ts">
* The FilterView shows the various options to enable/disable a single layer or to only show a subset of the data. /**
*/ * 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"; import type FilteredLayer from "../../Models/FilteredLayer"
import ToSvelte from "../Base/ToSvelte.svelte"; import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import Checkbox from "../Base/Checkbox.svelte"; import ToSvelte from "../Base/ToSvelte.svelte"
import FilterConfig from "../../Models/ThemeConfig/FilterConfig"; import Checkbox from "../Base/Checkbox.svelte"
import type {Writable} from "svelte/store"; import FilterConfig from "../../Models/ThemeConfig/FilterConfig"
import If from "../Base/If.svelte"; import type { Writable } from "svelte/store"
import Dropdown from "../Base/Dropdown.svelte"; import If from "../Base/If.svelte"
import {onDestroy} from "svelte"; import Dropdown from "../Base/Dropdown.svelte"
import {ImmutableStore, Store} from "../../Logic/UIEventSource"; import { onDestroy } from "svelte"
import FilterviewWithFields from "./FilterviewWithFields.svelte"; import { ImmutableStore, Store } from "../../Logic/UIEventSource"
import Tr from "../Base/Tr.svelte"; import FilterviewWithFields from "./FilterviewWithFields.svelte"
import Translations from "../i18n/Translations"; import Tr from "../Base/Tr.svelte"
import Translations from "../i18n/Translations"
export let filteredLayer: FilteredLayer; export let filteredLayer: FilteredLayer
export let highlightedLayer: Store<string | undefined> = new ImmutableStore(undefined); export let highlightedLayer: Store<string | undefined> = new ImmutableStore(undefined)
export let zoomlevel: Store<number> = new ImmutableStore(22); export let zoomlevel: Store<number> = new ImmutableStore(22)
let layer: LayerConfig = filteredLayer.layerDef; let layer: LayerConfig = filteredLayer.layerDef
let isDisplayed: Store<boolean> = filteredLayer.isDisplayed; let isDisplayed: Store<boolean> = filteredLayer.isDisplayed
/** /**
* Gets a UIEventSource as boolean for the given option, to be used with a checkbox * Gets a UIEventSource as boolean for the given option, to be used with a checkbox
*/ */
function getBooleanStateFor(option: FilterConfig): Writable<boolean> { function getBooleanStateFor(option: FilterConfig): Writable<boolean> {
const state = filteredLayer.appliedFilters.get(option.id); const state = filteredLayer.appliedFilters.get(option.id)
return state.sync(f => f === 0, [], (b) => b ? 0 : undefined); return state.sync(
} (f) => f === 0,
[],
(b) => (b ? 0 : undefined)
)
}
/** /**
* Gets a UIEventSource as number for the given option, to be used with a dropdown or radiobutton * Gets a UIEventSource as number for the given option, to be used with a dropdown or radiobutton
*/ */
function getStateFor(option: FilterConfig): Writable<number> { function getStateFor(option: FilterConfig): Writable<number> {
return filteredLayer.appliedFilters.get(option.id); return filteredLayer.appliedFilters.get(option.id)
} }
let mainElem: HTMLElement; let mainElem: HTMLElement
$: onDestroy( $: onDestroy(
highlightedLayer.addCallbackAndRun(highlightedLayer => { highlightedLayer.addCallbackAndRun((highlightedLayer) => {
if (highlightedLayer === filteredLayer.layerDef.id) { if (highlightedLayer === filteredLayer.layerDef.id) {
mainElem?.classList?.add("glowing-shadow"); mainElem?.classList?.add("glowing-shadow")
} else { } else {
mainElem?.classList?.remove("glowing-shadow"); mainElem?.classList?.remove("glowing-shadow")
} }
}) })
); )
</script> </script>
{#if filteredLayer.layerDef.name} {#if filteredLayer.layerDef.name}
<div bind:this={mainElem} class="mb-1.5"> <div bind:this={mainElem} class="mb-1.5">
<label class="flex gap-1 no-image-background"> <label class="flex gap-1 no-image-background">
<Checkbox selected={isDisplayed}/> <Checkbox selected={isDisplayed} />
<If condition={filteredLayer.isDisplayed}> <If condition={filteredLayer.isDisplayed}>
<ToSvelte <ToSvelte
construct={() => layer.defaultIcon()?.SetClass("block h-6 w-6 no-image-background")}></ToSvelte> construct={() => layer.defaultIcon()?.SetClass("block h-6 w-6 no-image-background")}
<ToSvelte slot="else" />
construct={() => layer.defaultIcon()?.SetClass("block h-6 w-6 no-image-background opacity-50")}></ToSvelte> <ToSvelte
</If> slot="else"
construct={() =>
layer.defaultIcon()?.SetClass("block h-6 w-6 no-image-background opacity-50")}
/>
</If>
{filteredLayer.layerDef.name} {filteredLayer.layerDef.name}
{#if $zoomlevel < layer.minzoom} {#if $zoomlevel < layer.minzoom}
<span class="alert"> <span class="alert">
<Tr t={Translations.t.general.layerSelection.zoomInToSeeThisLayer}/> <Tr t={Translations.t.general.layerSelection.zoomInToSeeThisLayer} />
</span> </span>
{/if}
</label>
{#if $isDisplayed && filteredLayer.layerDef.filters?.length > 0}
<div id="subfilters" class="flex flex-col gap-y-1 ml-4">
{#each filteredLayer.layerDef.filters as filter}
<div>
<!-- There are three (and a half) modes of filters: a single checkbox, a radio button/dropdown or with searchable fields -->
{#if filter.options.length === 1 && filter.options[0].fields.length === 0}
<label>
<Checkbox selected={getBooleanStateFor(filter)} />
{filter.options[0].question}
</label>
{/if} {/if}
</label> {#if filter.options.length === 1 && filter.options[0].fields.length > 0}
<FilterviewWithFields id={filter.id} {filteredLayer} option={filter.options[0]} />
{/if}
{#if $isDisplayed && filteredLayer.layerDef.filters?.length > 0} {#if filter.options.length > 1}
<div id="subfilters" class="flex flex-col gap-y-1 ml-4"> <Dropdown value={getStateFor(filter)}>
{#each filteredLayer.layerDef.filters as filter} {#each filter.options as option, i}
<div> <option value={i}>
{option.question}
<!-- There are three (and a half) modes of filters: a single checkbox, a radio button/dropdown or with searchable fields --> </option>
{#if filter.options.length === 1 && filter.options[0].fields.length === 0}
<label>
<Checkbox selected={getBooleanStateFor(filter)}/>
{filter.options[0].question}
</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}
<option value={i}>
{ option.question}
</option>
{/each}
</Dropdown>
{/if}
</div>
{/each} {/each}
</div> </Dropdown>
{/if} {/if}
</div> </div>
{/each}
</div>
{/if}
</div>
{/if} {/if}

View file

@ -1,60 +1,61 @@
<script lang="ts"> <script lang="ts">
import FilteredLayer from "../../Models/FilteredLayer"; import FilteredLayer from "../../Models/FilteredLayer"
import type {FilterConfigOption} from "../../Models/ThemeConfig/FilterConfig"; import type { FilterConfigOption } from "../../Models/ThemeConfig/FilterConfig"
import Locale from "../i18n/Locale"; import Locale from "../i18n/Locale"
import ValidatedInput from "../InputElement/ValidatedInput.svelte"; import ValidatedInput from "../InputElement/ValidatedInput.svelte"
import {UIEventSource} from "../../Logic/UIEventSource"; import { UIEventSource } from "../../Logic/UIEventSource"
import {onDestroy} from "svelte"; import { onDestroy } from "svelte"
import {Utils} from "../../Utils"; import { Utils } from "../../Utils"
export let filteredLayer: FilteredLayer; export let filteredLayer: FilteredLayer
export let option: FilterConfigOption; export let option: FilterConfigOption
export let id: string; export let id: string
let parts: ({ message: string } | { subs: string })[]; let parts: ({ message: string } | { subs: string })[]
let language = Locale.language; let language = Locale.language
$: { $: {
const template = option.question.textFor($language) const template = option.question.textFor($language)
parts = Utils.splitIntoSubstitutionParts(template) parts = Utils.splitIntoSubstitutionParts(template)
} }
let fieldValues: Record<string, UIEventSource<string>> = {}; let fieldValues: Record<string, UIEventSource<string>> = {}
let fieldTypes: Record<string, string> = {}; let fieldTypes: Record<string, string> = {}
let appliedFilter = <UIEventSource<string>>filteredLayer.appliedFilters.get(id); let appliedFilter = <UIEventSource<string>>filteredLayer.appliedFilters.get(id)
let initialState: Record<string, string> = JSON.parse(appliedFilter?.data ?? "{}"); let initialState: Record<string, string> = JSON.parse(appliedFilter?.data ?? "{}")
function setFields() { function setFields() {
const properties: Record<string, string> = {}; const properties: Record<string, string> = {}
for (const key in fieldValues) { for (const key in fieldValues) {
const v = fieldValues[key].data; const v = fieldValues[key].data
if (v === undefined) { if (v === undefined) {
properties[key] = undefined; properties[key] = undefined
} else { } else {
properties[key] = v; properties[key] = v
} }
}
appliedFilter?.setData(FilteredLayer.fieldsToString(properties));
} }
appliedFilter?.setData(FilteredLayer.fieldsToString(properties))
}
for (const field of option.fields) { for (const field of option.fields) {
// A bit of cheating: the 'parts' will have '}' suffixed for fields // A bit of cheating: the 'parts' will have '}' suffixed for fields
const src = new UIEventSource<string>(initialState[field.name] ?? ""); const src = new UIEventSource<string>(initialState[field.name] ?? "")
fieldTypes[field.name] = field.type; fieldTypes[field.name] = field.type
fieldValues[field.name] = src; fieldValues[field.name] = src
onDestroy(src.stabilized(200).addCallback(() => { onDestroy(
setFields(); src.stabilized(200).addCallback(() => {
})); setFields()
} })
)
}
</script> </script>
<div> <div>
{#each parts as part, i} {#each parts as part, i}
{#if part.subs} {#if part.subs}
<!-- This is a field! --> <!-- This is a field! -->
<span class="mx-1"> <span class="mx-1">
<ValidatedInput value={fieldValues[part.subs]} type={fieldTypes[part.subs]}/> <ValidatedInput value={fieldValues[part.subs]} type={fieldTypes[part.subs]} />
</span> </span>
{:else} {:else}
{part.message} {part.message}
{/if} {/if}
{/each} {/each}
</div> </div>

View file

@ -1,119 +1,116 @@
<script lang="ts"> <script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"
import type { Feature } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import ToSvelte from "../Base/ToSvelte.svelte"
import Svg from "../../Svg.js"
import Translations from "../i18n/Translations"
import Loading from "../Base/Loading.svelte"
import Hotkeys from "../Base/Hotkeys"
import { Geocoding } from "../../Logic/Osm/Geocoding"
import { BBox } from "../../Logic/BBox"
import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore"
import { createEventDispatcher, onDestroy } from "svelte"
import {UIEventSource} from "../../Logic/UIEventSource"; export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined
import type {Feature} from "geojson"; export let bounds: UIEventSource<BBox>
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; export let selectedElement: UIEventSource<Feature> | undefined = undefined
import ToSvelte from "../Base/ToSvelte.svelte"; export let selectedLayer: UIEventSource<LayerConfig> | undefined = undefined
import Svg from "../../Svg.js";
import Translations from "../i18n/Translations";
import Loading from "../Base/Loading.svelte";
import Hotkeys from "../Base/Hotkeys";
import {Geocoding} from "../../Logic/Osm/Geocoding";
import {BBox} from "../../Logic/BBox";
import {GeoIndexedStoreForLayer} from "../../Logic/FeatureSource/Actors/GeoIndexedStore";
import {createEventDispatcher, onDestroy} from "svelte";
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined; export let clearAfterView: boolean = true
export let bounds: UIEventSource<BBox>;
export let selectedElement: UIEventSource<Feature> | undefined = undefined;
export let selectedLayer: UIEventSource<LayerConfig> | undefined = undefined;
export let clearAfterView: boolean = true
let searchContents: string = "" let searchContents: string = ""
export let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined) export let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
onDestroy(triggerSearch.addCallback(_ => { onDestroy(
performSearch() triggerSearch.addCallback((_) => {
})) performSearch()
})
)
let isRunning: boolean = false; let isRunning: boolean = false
let inputElement: HTMLInputElement; let inputElement: HTMLInputElement
let feedback: string = undefined; let feedback: string = undefined
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey({ ctrl: "F" }, Translations.t.hotkeyDocumentation.selectSearch, () => {
{ctrl: "F"}, inputElement?.focus()
Translations.t.hotkeyDocumentation.selectSearch, inputElement?.select()
() => { })
inputElement?.focus();
inputElement?.select(); const dispatch = createEventDispatcher<{ searchCompleted; searchIsValid: boolean }>()
} $: {
); if (!searchContents?.trim()) {
dispatch("searchIsValid", false)
const dispatch = createEventDispatcher<{ searchCompleted, searchIsValid: boolean }>() } else {
$: { dispatch("searchIsValid", true)
if (!searchContents?.trim()) {
dispatch("searchIsValid", false)
}else{
dispatch("searchIsValid", true)
}
} }
}
async function performSearch() {
try {
isRunning = true
searchContents = searchContents?.trim() ?? ""
async function performSearch() { if (searchContents === "") {
try { return
isRunning = true; }
searchContents = searchContents?.trim() ?? ""; const result = await Geocoding.Search(searchContents, bounds.data)
if (result.length == 0) {
if (searchContents === "") { feedback = Translations.t.general.search.nothing.txt
return; return
} }
const result = await Geocoding.Search(searchContents, bounds.data); const poi = result[0]
if (result.length == 0) { const [lat0, lat1, lon0, lon1] = poi.boundingbox
feedback = Translations.t.general.search.nothing.txt; bounds.set(
return; new BBox([
} [lon0, lat0],
const poi = result[0]; [lon1, lat1],
const [lat0, lat1, lon0, lon1] = poi.boundingbox; ]).pad(0.01)
bounds.set(new BBox([[lon0, lat0], [lon1, lat1]]).pad(0.01)); )
if (perLayer !== undefined) { if (perLayer !== undefined) {
const id = poi.osm_type + "/" + poi.osm_id; const id = poi.osm_type + "/" + poi.osm_id
const layers = Array.from(perLayer?.values() ?? []); const layers = Array.from(perLayer?.values() ?? [])
for (const layer of layers) { for (const layer of layers) {
const found = layer.features.data.find(f => f.properties.id === id); const found = layer.features.data.find((f) => f.properties.id === id)
selectedElement?.setData(found); selectedElement?.setData(found)
selectedLayer?.setData(layer.layer.layerDef); selectedLayer?.setData(layer.layer.layerDef)
}
}
if(clearAfterView){
searchContents = ""
}
dispatch("searchIsValid", false)
dispatch("searchCompleted")
} catch (e) {
console.error(e);
feedback = Translations.t.general.search.error.txt;
} finally {
isRunning = false;
} }
}
if (clearAfterView) {
searchContents = ""
}
dispatch("searchIsValid", false)
dispatch("searchCompleted")
} catch (e) {
console.error(e)
feedback = Translations.t.general.search.error.txt
} finally {
isRunning = false
} }
}
</script> </script>
<div class="flex normal-background rounded-full pl-2 justify-between"> <div class="flex normal-background rounded-full pl-2 justify-between">
<form class="w-full"> <form class="w-full">
{#if isRunning}
{#if isRunning} <Loading>{Translations.t.general.search.searching}</Loading>
<Loading>{Translations.t.general.search.searching}</Loading> {:else if feedback !== undefined}
{:else if feedback !== undefined} <div class="alert" on:click={() => (feedback = undefined)}>
<div class="alert" on:click={() => feedback = undefined}> {feedback}
{feedback} </div>
</div> {:else}
{:else } <input
<input type="search"
type="search" class="w-full"
class="w-full" bind:this={inputElement}
bind:this={inputElement} on:keypress={(keypr) => (keypr.key === "Enter" ? performSearch() : undefined)}
on:keypress={keypr => keypr.key === "Enter" ? performSearch() : undefined} bind:value={searchContents}
placeholder={Translations.t.general.search.search}
bind:value={searchContents} />
placeholder={Translations.t.general.search.search}> {/if}
{/if} </form>
<div class="w-6 h-6 self-end" on:click={performSearch}>
</form> <ToSvelte construct={Svg.search_svg} />
<div class="w-6 h-6 self-end" on:click={performSearch}> </div>
<ToSvelte construct={Svg.search_svg}></ToSvelte>
</div>
</div> </div>

View file

@ -1,53 +1,50 @@
<script lang="ts"> <script lang="ts">
import {OsmConnection} from "../../Logic/Osm/OsmConnection" import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import {UIEventSource} from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
import * as themeOverview from "../../assets/generated/theme_overview.json" import * as themeOverview from "../../assets/generated/theme_overview.json"
import {Utils} from "../../Utils" import { Utils } from "../../Utils"
import ThemesList from "./ThemesList.svelte" import ThemesList from "./ThemesList.svelte"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import {LayoutInformation} from "../../Models/ThemeConfig/LayoutConfig" import { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
import LoginToggle from "../Base/LoginToggle.svelte"; import LoginToggle from "../Base/LoginToggle.svelte"
export let search: UIEventSource<string> export let search: UIEventSource<string>
export let state: { osmConnection: OsmConnection } export let state: { osmConnection: OsmConnection }
export let onMainScreen: boolean = true export let onMainScreen: boolean = true
const prefix = "mapcomplete-hidden-theme-" const prefix = "mapcomplete-hidden-theme-"
const hiddenThemes: LayoutInformation[] = (themeOverview["default"] ?? themeOverview)?.filter( const hiddenThemes: LayoutInformation[] =
(layout) => layout.hideFromOverview (themeOverview["default"] ?? themeOverview)?.filter((layout) => layout.hideFromOverview) ?? []
) ?? [] const userPreferences = state.osmConnection.preferencesHandler.preferences
const userPreferences = state.osmConnection.preferencesHandler.preferences const t = Translations.t.general.morescreen
const t = Translations.t.general.morescreen
let knownThemesId: string[] let knownThemesId: string[]
$: knownThemesId = Utils.NoNull( $: knownThemesId = Utils.NoNull(
Object.keys($userPreferences) Object.keys($userPreferences)
.filter((key) => key.startsWith(prefix)) .filter((key) => key.startsWith(prefix))
.map((key) => key.substring(prefix.length, key.length - "-enabled".length)) .map((key) => key.substring(prefix.length, key.length - "-enabled".length))
) )
$: console.log("Known theme ids:", knownThemesId) $: console.log("Known theme ids:", knownThemesId)
$: knownThemes = hiddenThemes.filter((theme) => knownThemesId.includes(theme.id)) $: knownThemes = hiddenThemes.filter((theme) => knownThemesId.includes(theme.id))
</script> </script>
<LoginToggle {state}> <LoginToggle {state}>
<ThemesList
<ThemesList hideThemes={false}
hideThemes={false} isCustom={false}
isCustom={false} {onMainScreen}
{onMainScreen} {search}
{search} {state}
{state} themes={knownThemes}
themes={knownThemes} >
> <svelte:fragment slot="title">
<svelte:fragment slot="title"> <h3>{t.previouslyHiddenTitle.toString()}</h3>
<h3>{t.previouslyHiddenTitle.toString()}</h3> <p>
<p> {t.hiddenExplanation.Subs({
{t.hiddenExplanation.Subs({ hidden_discovered: knownThemes.length.toString(),
hidden_discovered: knownThemes.length.toString(), total_hidden: hiddenThemes.length.toString(),
total_hidden: hiddenThemes.length.toString(), })}
})} </p>
</p> </svelte:fragment>
</svelte:fragment> </ThemesList>
</ThemesList>
</LoginToggle> </LoginToggle>

View file

@ -2,28 +2,31 @@
/** /**
* Shows a 'floorSelector' and maps the selected floor onto a global filter * Shows a 'floorSelector' and maps the selected floor onto a global filter
*/ */
import LayerState from "../../Logic/State/LayerState"; import LayerState from "../../Logic/State/LayerState"
import FloorSelector from "../InputElement/Helpers/FloorSelector.svelte"; import FloorSelector from "../InputElement/Helpers/FloorSelector.svelte"
import { Store, UIEventSource } from "../../Logic/UIEventSource"; import { Store, UIEventSource } from "../../Logic/UIEventSource"
export let layerState: LayerState; export let layerState: LayerState
export let floors: Store<string[]>; export let floors: Store<string[]>
export let zoom: Store<number>; export let zoom: Store<number>
const maxZoom = 16 const maxZoom = 16
let selectedFloor: UIEventSource<string> = new UIEventSource<string>(undefined); let selectedFloor: UIEventSource<string> = new UIEventSource<string>(undefined)
selectedFloor.stabilized(5).map(floor => { selectedFloor.stabilized(5).map(
if(floors.data === undefined || floors.data.length <= 1 || zoom.data < maxZoom){ (floor) => {
// Only a single floor is visible -> disable the 'level' global filter if (floors.data === undefined || floors.data.length <= 1 || zoom.data < maxZoom) {
// OR we might have zoomed out to much ant want to show all // Only a single floor is visible -> disable the 'level' global filter
layerState.setLevelFilter(undefined) // OR we might have zoomed out to much ant want to show all
}else{ layerState.setLevelFilter(undefined)
layerState.setLevelFilter(floor) } else {
} layerState.setLevelFilter(floor)
}, [floors, zoom]) }
},
[floors, zoom]
)
</script> </script>
{#if $zoom >= maxZoom} {#if $zoom >= maxZoom}
<FloorSelector {floors} value={selectedFloor} /> <FloorSelector {floors} value={selectedFloor} />
{/if} {/if}

View file

@ -1,31 +1,29 @@
<script lang="ts"> <script lang="ts">
import Translations from "../i18n/Translations"
import Svg from "../../Svg"
import { Store } from "../../Logic/UIEventSource"
import Tr from "../Base/Tr.svelte"
import ToSvelte from "../Base/ToSvelte.svelte"
import Translations from "../i18n/Translations" /*
import Svg from "../../Svg"
import {Store} from "../../Logic/UIEventSource";
import Tr from "../Base/Tr.svelte";
import ToSvelte from "../Base/ToSvelte.svelte";
/*
A subtleButton which opens mapillary in a new tab at the current location A subtleButton which opens mapillary in a new tab at the current location
*/ */
export let mapProperties: { export let mapProperties: {
readonly zoom: Store<number>, readonly zoom: Store<number>
readonly location: Store<{ lon: number, lat: number }> readonly location: Store<{ lon: number; lat: number }>
} }
let location = mapProperties.location let location = mapProperties.location
let zoom = mapProperties.zoom let zoom = mapProperties.zoom
let mapillaryLink = `https://www.mapillary.com/app/?focus=map&lat=${ let mapillaryLink = `https://www.mapillary.com/app/?focus=map&lat=${$location?.lat ?? 0}&lng=${
$location?.lat ?? 0 $location?.lon ?? 0
}&lng=${$location?.lon ?? 0}&z=${Math.max(($zoom ?? 2) - 1, 1)}` }&z=${Math.max(($zoom ?? 2) - 1, 1)}`
</script> </script>
<a class="flex button items-center" href={mapillaryLink} target="_blank"> <a class="flex button items-center" href={mapillaryLink} target="_blank">
<ToSvelte construct={() =>Svg.mapillary_black_svg().SetClass("w-12 h-12 m-2 mr-4 shrink-0")}/> <ToSvelte construct={() => Svg.mapillary_black_svg().SetClass("w-12 h-12 m-2 mr-4 shrink-0")} />
<div class="flex flex-col"> <div class="flex flex-col">
<Tr t={Translations.t.general.attribution.openMapillary}/> <Tr t={Translations.t.general.attribution.openMapillary} />
<Tr cls="subtle" t={ Translations.t.general.attribution.mapillaryHelp}/> <Tr cls="subtle" t={Translations.t.general.attribution.mapillaryHelp} />
</div> </div>
</a> </a>

View file

@ -1,12 +1,12 @@
import Svg from "../../Svg" import Svg from "../../Svg"
import Combine from "../Base/Combine" import Combine from "../Base/Combine"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import LayoutConfig, {LayoutInformation} from "../../Models/ThemeConfig/LayoutConfig" import LayoutConfig, { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
import {ImmutableStore, Store} from "../../Logic/UIEventSource" import { ImmutableStore, Store } from "../../Logic/UIEventSource"
import UserRelatedState from "../../Logic/State/UserRelatedState" import UserRelatedState from "../../Logic/State/UserRelatedState"
import {Utils} from "../../Utils" import { Utils } from "../../Utils"
import themeOverview from "../../assets/generated/theme_overview.json" import themeOverview from "../../assets/generated/theme_overview.json"
import {TextField} from "../Input/TextField" import { TextField } from "../Input/TextField"
import Locale from "../i18n/Locale" import Locale from "../i18n/Locale"
import SvelteUIElement from "../Base/SvelteUIElement" import SvelteUIElement from "../Base/SvelteUIElement"
import ThemesList from "./ThemesList.svelte" import ThemesList from "./ThemesList.svelte"
@ -29,7 +29,7 @@ export default class MoreScreen extends Combine {
}) })
search.enterPressed.addCallbackD((searchTerm) => { search.enterPressed.addCallbackD((searchTerm) => {
searchTerm = searchTerm.toLowerCase() searchTerm = searchTerm.toLowerCase()
if(!searchTerm){ if (!searchTerm) {
return return
} }
if (searchTerm === "personal") { if (searchTerm === "personal") {

View file

@ -1,108 +1,113 @@
<script lang="ts"> <script lang="ts">
import type {SpecialVisualizationState} from "../SpecialVisualization"; import type { SpecialVisualizationState } from "../SpecialVisualization"
import LocationInput from "../InputElement/Helpers/LocationInput.svelte"; import LocationInput from "../InputElement/Helpers/LocationInput.svelte"
import {UIEventSource} from "../../Logic/UIEventSource"; import { UIEventSource } from "../../Logic/UIEventSource"
import {Tiles} from "../../Models/TileRange"; import { Tiles } from "../../Models/TileRange"
import {Map as MlMap} from "maplibre-gl"; import { Map as MlMap } from "maplibre-gl"
import {BBox} from "../../Logic/BBox"; import { BBox } from "../../Logic/BBox"
import type {MapProperties} from "../../Models/MapProperties"; import type { MapProperties } from "../../Models/MapProperties"
import ShowDataLayer from "../Map/ShowDataLayer"; import ShowDataLayer from "../Map/ShowDataLayer"
import type {FeatureSource, FeatureSourceForLayer} from "../../Logic/FeatureSource/FeatureSource"; import type {
import SnappingFeatureSource from "../../Logic/FeatureSource/Sources/SnappingFeatureSource"; FeatureSource,
import FeatureSourceMerger from "../../Logic/FeatureSource/Sources/FeatureSourceMerger"; FeatureSourceForLayer,
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; } from "../../Logic/FeatureSource/FeatureSource"
import {Utils} from "../../Utils"; 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: * An advanced location input, which has support to:
* - Show more layers * - Show more layers
* - Snap to layers * - Snap to layers
* *
* This one is mostly used to insert new points, including when importing * This one is mostly used to insert new points, including when importing
*/ */
export let state: SpecialVisualizationState; export let state: SpecialVisualizationState
/** /**
* The start coordinate * The start coordinate
*/ */
export let coordinate: { lon: number, lat: number }; export let coordinate: { lon: number; lat: number }
export let snapToLayers: string[] | undefined; export let snapToLayers: string[] | undefined
export let targetLayer: LayerConfig; export let targetLayer: LayerConfig
export let maxSnapDistance: number = undefined; export let maxSnapDistance: number = undefined
export let snappedTo: UIEventSource<string | undefined>; export let snappedTo: UIEventSource<string | undefined>
export let value: UIEventSource<{ lon: number, lat: number }>; export let value: UIEventSource<{ lon: number; lat: number }>
if (value.data === undefined) { if (value.data === undefined) {
value.setData(coordinate); value.setData(coordinate)
}
let preciseLocation: UIEventSource<{ lon: number; lat: number }> = new UIEventSource<{
lon: number
lat: number
}>(undefined)
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
? new UIEventSource<{ lon: number; lat: number }>(coordinate)
: value,
bounds: new UIEventSource<BBox>(undefined),
allowMoving: new UIEventSource<boolean>(true),
allowZooming: new UIEventSource<boolean>(true),
minzoom: new UIEventSource<number>(18),
rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer),
}
const featuresForLayer = state.perLayer.get(targetLayer.id)
if (featuresForLayer) {
new ShowDataLayer(map, {
layer: targetLayer,
features: featuresForLayer,
})
}
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,
}
)
let preciseLocation: UIEventSource<{ lon: number, lat: number }> = new UIEventSource<{ new ShowDataLayer(map, {
lon: number; layer: targetLayer,
lat: number features: snappedLocation,
}>(undefined); })
}
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 ? new UIEventSource<{ lon: number; lat: number }>(coordinate) : value,
bounds: new UIEventSource<BBox>(undefined),
allowMoving: new UIEventSource<boolean>(true),
allowZooming: new UIEventSource<boolean>(true),
minzoom: new UIEventSource<number>(18),
rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer)
};
const featuresForLayer = state.perLayer.get(targetLayer.id)
if(featuresForLayer){
new ShowDataLayer(map, {
layer: targetLayer,
features: featuresForLayer
})
}
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> </script>
<LocationInput
<LocationInput {map} mapProperties={initialMapProperties} {map}
value={preciseLocation} initialCoordinate={coordinate} maxDistanceInMeters=50 /> mapProperties={initialMapProperties}
value={preciseLocation}
initialCoordinate={coordinate}
maxDistanceInMeters="50"
/>

View file

@ -1,35 +1,34 @@
<script context="module" lang="ts"> <script context="module" lang="ts">
export interface Theme { export interface Theme {
id: string id: string
icon: string icon: string
title: any title: any
shortDescription: any shortDescription: any
definition?: any definition?: any
mustHaveLanguage?: boolean mustHaveLanguage?: boolean
hideFromOverview: boolean hideFromOverview: boolean
keywords?: any[] keywords?: any[]
} }
</script> </script>
<script lang="ts"> <script lang="ts">
import {UIEventSource} from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
import Svg from "../../Svg" import Svg from "../../Svg"
import ToSvelte from "../Base/ToSvelte.svelte" import ToSvelte from "../Base/ToSvelte.svelte"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"; import Tr from "../Base/Tr.svelte"
export let search: UIEventSource<string> export let search: UIEventSource<string>
const t = Translations.t.general.morescreen const t = Translations.t.general.morescreen
</script> </script>
<div class="w-full"> <div class="w-full">
<h5>{t.noMatchingThemes.toString()}</h5> <h5>{t.noMatchingThemes.toString()}</h5>
<div class="flex justify-center"> <div class="flex justify-center">
<button on:click={() => search.setData("")}>
<button on:click={() => search.setData("")}> <ToSvelte construct={Svg.search_disable_svg().SetClass("w-6 mr-2")} />
<ToSvelte construct={Svg.search_disable_svg().SetClass("w-6 mr-2")}/> <Tr slot="message" t={t.noSearch} />
<Tr slot="message" t={t.noSearch}/> </button>
</button> </div>
</div>
</div> </div>

View file

@ -1,12 +1,11 @@
<script lang="ts"> <script lang="ts">
import { Square3Stack3dIcon } from "@babeard/svelte-heroicons/solid"
import MapControlButton from "../Base/MapControlButton.svelte"
import ThemeViewState from "../../Models/ThemeViewState"
import {Square3Stack3dIcon} from "@babeard/svelte-heroicons/solid"; export let state: ThemeViewState
import MapControlButton from "../Base/MapControlButton.svelte";
import ThemeViewState from "../../Models/ThemeViewState";
export let state: ThemeViewState
</script> </script>
<MapControlButton on:click={() => state.guistate.backgroundLayerSelectionIsOpened.setData(true)}> <MapControlButton on:click={() => state.guistate.backgroundLayerSelectionIsOpened.setData(true)}>
<Square3Stack3dIcon class="w-6 h-6"/> <Square3Stack3dIcon class="w-6 h-6" />
</MapControlButton> </MapControlButton>

View file

@ -1,33 +1,32 @@
<script lang="ts"> <script lang="ts">
import {Store} from "../../Logic/UIEventSource"; import { Store } from "../../Logic/UIEventSource"
import {PencilIcon} from "@babeard/svelte-heroicons/solid"; import { PencilIcon } from "@babeard/svelte-heroicons/solid"
import Translations from "../i18n/Translations"; import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"; import Tr from "../Base/Tr.svelte"
export let mapProperties: { location: Store<{ lon: number; lat: number }>; zoom: Store<number> } export let mapProperties: { location: Store<{ lon: number; lat: number }>; zoom: Store<number> }
let location = mapProperties.location let location = mapProperties.location
let zoom = mapProperties.zoom let zoom = mapProperties.zoom
export let objectId: undefined | string = undefined export let objectId: undefined | string = undefined
let elementSelect = "" let elementSelect = ""
if (objectId !== undefined) { if (objectId !== undefined) {
const parts = objectId?.split("/") const parts = objectId?.split("/")
const tp = parts[0] const tp = parts[0]
if ( if (
parts.length === 2 && parts.length === 2 &&
!isNaN(Number(parts[1])) && !isNaN(Number(parts[1])) &&
(tp === "node" || tp === "way" || tp === "relation") (tp === "node" || tp === "way" || tp === "relation")
) { ) {
elementSelect = "&" + tp + "=" + parts[1] elementSelect = "&" + tp + "=" + parts[1]
}
} }
const idLink = `https://www.openstreetmap.org/edit?editor=id${elementSelect}#map=${ }
$zoom ?? 0 const idLink = `https://www.openstreetmap.org/edit?editor=id${elementSelect}#map=${$zoom ?? 0}/${
}/${$location?.lat ?? 0}/${$location?.lon ?? 0}` $location?.lat ?? 0
}/${$location?.lon ?? 0}`
</script> </script>
<a class="flex button items-center" target="_blank" href={idLink}> <a class="flex button items-center" target="_blank" href={idLink}>
<PencilIcon class="w-12 h-12 p-2 pr-4"/> <PencilIcon class="w-12 h-12 p-2 pr-4" />
<Tr t={ Translations.t.general.attribution.editId}/> <Tr t={Translations.t.general.attribution.editId} />
</a> </a>

Some files were not shown because too many files have changed in this diff Show more