Merge develop

This commit is contained in:
Pieter Vander Vennet 2024-09-05 17:34:13 +02:00
commit 423618847b
334 changed files with 9307 additions and 6025 deletions

View file

@ -14,18 +14,26 @@ export type PageType = (typeof MenuState.pageNames)[number]
* Some convenience methods are provided for this as well
*/
export class MenuState {
public static readonly pageNames = [
"copyright", "copyright_icons", "community_index", "hotkeys",
"privacy", "filter", "background", "about_theme", "download", "favourites",
"usersettings", "share", "menu",
"copyright",
"copyright_icons",
"community_index",
"hotkeys",
"privacy",
"filter",
"background",
"about_theme",
"download",
"favourites",
"usersettings",
"share",
"menu",
] as const
public readonly pageStates: Record<PageType, UIEventSource<boolean>>
public readonly highlightedLayerInFilters: UIEventSource<string> = new UIEventSource<string>(
undefined,
undefined
)
public highlightedUserSetting: UIEventSource<string> = new UIEventSource<string>(undefined)
@ -39,10 +47,10 @@ export class MenuState {
this.pageStates = <Record<PageType, UIEventSource<boolean>>>states
for (const pageName of MenuState.pageNames) {
if(pageName === "menu"){
if (pageName === "menu") {
continue
}
this.pageStates[pageName].addCallback(enabled => {
this.pageStates[pageName].addCallback((enabled) => {
if (enabled) {
this.pageStates.menu.set(false)
}
@ -50,7 +58,8 @@ export class MenuState {
}
const visitedBefore = LocalStorageSource.GetParsed<boolean>(
themeid + "thememenuisopened", false,
themeid + "thememenuisopened",
false
)
if (!visitedBefore.data && shouldShowWelcomeMessage) {
this.pageStates.about_theme.set(true)
@ -85,11 +94,12 @@ export class MenuState {
Utils.sortedByLevenshteinDistance(
highlightTagRendering,
UserRelatedState.availableUserSettingsIds,
(x) => x,
),
(x) => x
)
)
}
this.highlightedUserSetting.setData(highlightTagRendering)
this.pageStates.usersettings.set(true)
}
public isSomethingOpen(): boolean {
@ -115,5 +125,4 @@ export class MenuState {
return true
}
}
}

View file

@ -1,11 +1,13 @@
export const eliCategory = ["photo"
, "map"
, "historicmap"
, "osmbasedmap"
, "historicphoto"
, "qa"
, "elevation"
, "other"] as const
export const eliCategory = [
"photo",
"map",
"historicmap",
"osmbasedmap",
"historicphoto",
"qa",
"elevation",
"other",
] as const
export type EliCategory = (typeof eliCategory)[number]
/**

View file

@ -38,7 +38,7 @@ export class AvailableRasterLayers {
<RasterLayerPolygon>{
type: "Feature",
properties,
geometry: BBox.global.asGeometry()
geometry: BBox.global.asGeometry(),
}
)
public static bing = <RasterLayerPolygon>bingJson
@ -48,18 +48,18 @@ export class AvailableRasterLayers {
url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
attribution: {
text: "OpenStreetMap",
url: "https://openStreetMap.org/copyright"
url: "https://openStreetMap.org/copyright",
},
best: true,
max_zoom: 19,
min_zoom: 0,
category: "osmbasedmap"
category: "osmbasedmap",
}
public static readonly osmCarto: RasterLayerPolygon = {
type: "Feature",
properties: AvailableRasterLayers.osmCartoProperties,
geometry: BBox.global.asGeometry()
geometry: BBox.global.asGeometry(),
}
/**
@ -197,7 +197,7 @@ export interface EditorLayerIndexProperties extends RasterLayerProperties {
/**
* A URL template for imagery tiles
*/
readonly url: string
readonly url: string
readonly min_zoom?: number
readonly max_zoom?: number
/**

View file

@ -203,7 +203,7 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, L
anchor: "center",
},
],
allowMove: false
allowMove: false,
}
}
}

View file

@ -571,6 +571,13 @@ class DetectInline extends DesugaringStep<QuestionableTagRenderingConfigJson> {
context.err("'freeform' is a string, but should be an object")
return json
}
if(!Object.values(json.render).some(render => render !== "{"+json.freeform.key+"}")){
// We only render the current value, without anything more. Not worth inlining
return json
}
json.freeform.inline ??= true
return json
}

View file

@ -30,7 +30,7 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
super(
"Converts the identifier of a builtin layer into the actual layer, or converts a 'builtin' syntax with override in the fully expanded form. Note that 'tagRenderings+' will be inserted before 'leftover-questions'",
[],
"SubstituteLayer"
"SubstituteLayer",
)
this._state = state
}
@ -86,14 +86,14 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
(found["tagRenderings"] ?? []).length > 0
) {
context.err(
`When overriding a layer, an override is not allowed to override into tagRenderings. Use "+tagRenderings" or "tagRenderings+" instead to prepend or append some questions.`
`When overriding a layer, an override is not allowed to override into tagRenderings. Use "+tagRenderings" or "tagRenderings+" instead to prepend or append some questions.`,
)
}
try {
const trPlus = json["override"]["tagRenderings+"]
if (trPlus) {
let index = found.tagRenderings.findIndex(
(tr) => tr["id"] === "leftover-questions"
(tr) => tr["id"] === "leftover-questions",
)
if (index < 0) {
index = found.tagRenderings.length
@ -107,8 +107,8 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
} catch (e) {
context.err(
`Could not apply an override due to: ${e}.\nThe override is: ${JSON.stringify(
json["override"]
)}`
json["override"],
)}`,
)
}
@ -132,9 +132,9 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
usedLabels.add(labels[forbiddenLabel])
context.info(
"Dropping tagRendering " +
tr["id"] +
" as it has a forbidden label: " +
labels[forbiddenLabel]
tr["id"] +
" as it has a forbidden label: " +
labels[forbiddenLabel],
)
continue
}
@ -143,7 +143,7 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
if (hideLabels.has(tr["id"])) {
usedLabels.add(tr["id"])
context.info(
"Dropping tagRendering " + tr["id"] + " as its id is a forbidden label"
"Dropping tagRendering " + tr["id"] + " as its id is a forbidden label",
)
continue
}
@ -152,10 +152,10 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
usedLabels.add(tr["group"])
context.info(
"Dropping tagRendering " +
tr["id"] +
" as its group `" +
tr["group"] +
"` is a forbidden label"
tr["id"] +
" as its group `" +
tr["group"] +
"` is a forbidden label",
)
continue
}
@ -166,8 +166,8 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
if (unused.length > 0) {
context.err(
"This theme specifies that certain tagrenderings have to be removed based on forbidden layers. One or more of these layers did not match any tagRenderings and caused no deletions: " +
unused.join(", ") +
"\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore"
unused.join(", ") +
"\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore",
)
}
found.tagRenderings = filtered
@ -184,7 +184,7 @@ class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
super(
"Adds the default layers, namely: " + Constants.added_by_default.join(", "),
["layers"],
"AddDefaultLayers"
"AddDefaultLayers",
)
this._state = state
}
@ -207,10 +207,10 @@ class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
if (alreadyLoaded.has(v.id)) {
context.warn(
"Layout " +
context +
" already has a layer with name " +
v.id +
"; skipping inclusion of this builtin layer"
context +
" already has a layer with name " +
v.id +
"; skipping inclusion of this builtin layer",
)
continue
}
@ -226,14 +226,14 @@ class AddImportLayers extends DesugaringStep<LayoutConfigJson> {
super(
"For every layer in the 'layers'-list, create a new layer which'll import notes. (Note that priviliged layers and layers which have a geojson-source set are ignored)",
["layers"],
"AddImportLayers"
"AddImportLayers",
)
}
convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
if (!(json.enableNoteImports ?? true)) {
context.info(
"Not creating a note import layers for theme " + json.id + " as they are disabled"
"Not creating a note import layers for theme " + json.id + " as they are disabled",
)
return json
}
@ -268,7 +268,7 @@ class AddImportLayers extends DesugaringStep<LayoutConfigJson> {
try {
const importLayerResult = creator.convert(
layer,
context.inOperation(this.name).enter(i1)
context.inOperation(this.name).enter(i1),
)
if (importLayerResult !== undefined) {
json.layers.push(importLayerResult)
@ -288,7 +288,7 @@ class AddContextToTranslationsInLayout extends DesugaringStep<LayoutConfigJson>
super(
"Adds context to translations, including the prefix 'themes:json.id'; this is to make sure terms in an 'overrides' or inline layer are linkable too",
["_context"],
"AddContextToTranlationsInLayout"
"AddContextToTranlationsInLayout",
)
}
@ -297,7 +297,7 @@ class AddContextToTranslationsInLayout extends DesugaringStep<LayoutConfigJson>
// The context is used to generate the 'context' in the translation .It _must_ be `json.id` to correctly link into weblate
return conversion.convert(
json,
ConversionContext.construct([json.id], ["AddContextToTranslation"])
ConversionContext.construct([json.id], ["AddContextToTranslation"]),
)
}
}
@ -307,7 +307,7 @@ class ApplyOverrideAll extends DesugaringStep<LayoutConfigJson> {
super(
"Applies 'overrideAll' onto every 'layer'. The 'overrideAll'-field is removed afterwards",
["overrideAll", "layers"],
"ApplyOverrideAll"
"ApplyOverrideAll",
)
}
@ -336,7 +336,7 @@ class ApplyOverrideAll extends DesugaringStep<LayoutConfigJson> {
layer.tagRenderings = tagRenderingsPlus
} else {
let index = layer.tagRenderings.findIndex(
(tr) => tr["id"] === "leftover-questions"
(tr) => tr["id"] === "leftover-questions",
)
if (index < 0) {
index = layer.tagRenderings.length - 1
@ -357,14 +357,9 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
constructor(state: DesugaringContext) {
super(
`If a layer has a dependency on another layer, these layers are added automatically on the theme. (For example: defibrillator depends on 'walls_and_buildings' to snap onto. This layer is added automatically)
Note that these layers are added _at the start_ of the layer list, meaning that they will see _every_ feature.
Furthermore, \`passAllFeatures\` will be set, so that they won't steal away features from further layers.
Some layers (e.g. \`all_buildings_and_walls\' or \'streets_with_a_name\') are invisible, so by default, \'force_load\' is set too.
`,
`If a layer has a dependency on another layer, these layers are added automatically on the theme. (For example: defibrillator depends on 'walls_and_buildings' to snap onto. This layer is added automatically)`,
["layers"],
"AddDependencyLayersToTheme"
"AddDependencyLayersToTheme",
)
this._state = state
}
@ -373,7 +368,7 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
alreadyLoaded: LayerConfigJson[],
allKnownLayers: Map<string, LayerConfigJson>,
themeId: string,
context: ConversionContext
context: ConversionContext,
): { config: LayerConfigJson; reason: string }[] {
const dependenciesToAdd: { config: LayerConfigJson; reason: string }[] = []
const loadedLayerIds: Set<string> = new Set<string>(alreadyLoaded.map((l) => l?.id))
@ -391,12 +386,13 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
reason: string
context?: string
neededBy: string
checkHasSnapName: boolean
}[] = []
for (const layerConfig of alreadyLoaded) {
try {
const layerDeps = DependencyCalculator.getLayerDependencies(
new LayerConfig(layerConfig, themeId + "(dependencies)")
new LayerConfig(layerConfig, themeId + "(dependencies)"),
)
dependencies.push(...layerDeps)
} catch (e) {
@ -413,7 +409,11 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
for (const dependency of dependencies) {
if (loadedLayerIds.has(dependency.neededLayer)) {
// We mark the needed layer as 'mustLoad'
alreadyLoaded.find((l) => l.id === dependency.neededLayer).forceLoad = true
const loadedLayer = alreadyLoaded.find((l) => l.id === dependency.neededLayer)
loadedLayer.forceLoad = true
if(dependency.checkHasSnapName && !loadedLayer.snapName){
context.enters("layer dependency").err("Layer "+dependency.neededLayer+" is loaded because "+dependency.reason+"; so it must specify a `snapName`. This is used in the sentence `move this point to snap it to {snapName}`")
}
}
}
@ -436,10 +436,10 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
if (dep === undefined) {
const message = [
"Loading a dependency failed: layer " +
unmetDependency.neededLayer +
" is not found, neither as layer of " +
themeId +
" nor as builtin layer.",
unmetDependency.neededLayer +
" is not found, neither as layer of " +
themeId +
" nor as builtin layer.",
reason,
"Loaded layers are: " + alreadyLoaded.map((l) => l.id).join(","),
]
@ -455,11 +455,12 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
})
loadedLayerIds.add(dep.id)
unmetDependencies = unmetDependencies.filter(
(d) => d.neededLayer !== unmetDependency.neededLayer
(d) => d.neededLayer !== unmetDependency.neededLayer,
)
}
} while (unmetDependencies.length > 0)
return dependenciesToAdd
}
@ -477,12 +478,12 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
layers,
allKnownLayers,
theme.id,
context
context,
)
if (dependencies.length > 0) {
for (const dependency of dependencies) {
context.info(
"Added " + dependency.config.id + " to the theme. " + dependency.reason
"Added " + dependency.config.id + " to the theme. " + dependency.reason,
)
}
}
@ -530,7 +531,7 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<LayoutConfigJson>
super(
"Generates a warning if a theme uses an unsubstituted layer",
["layers"],
"WarnForUnsubstitutedLayersInTheme"
"WarnForUnsubstitutedLayersInTheme",
)
}
@ -542,7 +543,7 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<LayoutConfigJson>
context
.enter("layers")
.err(
"No layers are defined. You must define at least one layer to have a valid theme"
"No layers are defined. You must define at least one layer to have a valid theme",
)
return json
}
@ -566,10 +567,10 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<LayoutConfigJson>
context.warn(
"The theme " +
json.id +
" has an inline layer: " +
layer["id"] +
". This is discouraged."
json.id +
" has an inline layer: " +
layer["id"] +
". This is discouraged.",
)
}
return json
@ -578,6 +579,7 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<LayoutConfigJson>
class PostvalidateTheme extends DesugaringStep<LayoutConfigJson> {
private readonly _state: DesugaringContext
constructor(state: DesugaringContext) {
super("Various validation steps when everything is done", [], "PostvalidateTheme")
this._state = state
@ -596,13 +598,13 @@ class PostvalidateTheme extends DesugaringStep<LayoutConfigJson> {
}
const sameBasedOn = <LayerConfigJson[]>(
json.layers.filter(
(l) => l["_basedOn"] === layer["_basedOn"] && l["id"] !== layer.id
(l) => l["_basedOn"] === layer["_basedOn"] && l["id"] !== layer.id,
)
)
const minZoomAll = Math.min(...sameBasedOn.map((sbo) => sbo.minzoom))
const sameNameDetected = sameBasedOn.some(
(same) => JSON.stringify(layer["name"]) === JSON.stringify(same["name"])
(same) => JSON.stringify(layer["name"]) === JSON.stringify(same["name"]),
)
if (!sameNameDetected) {
// The name is unique, so it'll won't be confusing
@ -611,12 +613,12 @@ class PostvalidateTheme extends DesugaringStep<LayoutConfigJson> {
if (minZoomAll < layer.minzoom) {
context.err(
"There are multiple layers based on " +
basedOn +
". The layer with id " +
layer.id +
" has a minzoom of " +
layer.minzoom +
", and has a name set. Another similar layer has a lower minzoom. As such, the layer selection might show 'zoom in to see features' even though some of the features are already visible. Set `\"name\": null` for this layer and eventually remove the 'name':null for the other layer."
basedOn +
". The layer with id " +
layer.id +
" has a minzoom of " +
layer.minzoom +
", and has a name set. Another similar layer has a lower minzoom. As such, the layer selection might show 'zoom in to see features' even though some of the features are already visible. Set `\"name\": null` for this layer and eventually remove the 'name':null for the other layer.",
)
}
}
@ -636,17 +638,17 @@ class PostvalidateTheme extends DesugaringStep<LayoutConfigJson> {
const closeLayers = Utils.sortedByLevenshteinDistance(
sameAs,
json.layers,
(l) => l["id"]
(l) => l["id"],
).map((l) => l["id"])
context
.enters("layers", config.id, "filter", "sameAs")
.err(
"The layer " +
config.id +
" follows the filter state of layer " +
sameAs +
", but no layer with this name was found.\n\tDid you perhaps mean one of: " +
closeLayers.slice(0, 3).join(", ")
config.id +
" follows the filter state of layer " +
sameAs +
", but no layer with this name was found.\n\tDid you perhaps mean one of: " +
closeLayers.slice(0, 3).join(", "),
)
}
}
@ -654,6 +656,7 @@ class PostvalidateTheme extends DesugaringStep<LayoutConfigJson> {
return json
}
}
export class PrepareTheme extends Fuse<LayoutConfigJson> {
private state: DesugaringContext
@ -661,7 +664,7 @@ export class PrepareTheme extends Fuse<LayoutConfigJson> {
state: DesugaringContext,
options?: {
skipDefaultLayers: false | boolean
}
},
) {
super(
"Fully prepares and expands a theme",
@ -683,7 +686,7 @@ export class PrepareTheme extends Fuse<LayoutConfigJson> {
: new AddDefaultLayers(state),
new AddDependencyLayersToTheme(state),
new AddImportLayers(),
new PostvalidateTheme(state)
new PostvalidateTheme(state),
)
this.state = state
}
@ -698,13 +701,13 @@ export class PrepareTheme extends Fuse<LayoutConfigJson> {
const needsNodeDatabase = result.layers?.some((l: LayerConfigJson) =>
l.tagRenderings?.some((tr) =>
ValidationUtils.getSpecialVisualisations(<any>tr)?.some(
(special) => special.needsNodeDatabase
)
)
(special) => special.needsNodeDatabase,
),
),
)
if (needsNodeDatabase) {
context.info(
"Setting 'enableNodeDatabase' as this theme uses a special visualisation which needs to keep track of _all_ nodes"
"Setting 'enableNodeDatabase' as this theme uses a special visualisation which needs to keep track of _all_ nodes",
)
result.enableNodeDatabase = true
}

View file

@ -141,13 +141,9 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
}
}
if(json.allowMove === undefined && json.source["geoJson"] === undefined){
if(this._isBuiltin && json.allowMove === undefined && json.source["geoJson"] === undefined) {
if (!Constants.priviliged_layers.find((x) => x == json.id)) {
context.err(
"Layer " +
json.id +
" does not have an explicit 'allowMove'"
)
context.err("Layer " + json.id + " does not have an explicit 'allowMove'")
}
}

View file

@ -33,8 +33,8 @@ export default class DependencyCalculator {
*/
public static getLayerDependencies(
layer: LayerConfig
): { neededLayer: string; reason: string; context?: string; neededBy: string }[] {
const deps: { neededLayer: string; reason: string; context?: string; neededBy: string }[] =
): { neededLayer: string; reason: string; context?: string; neededBy: string, checkHasSnapName: boolean }[] {
const deps: { neededLayer: string; reason: string; context?: string; neededBy: string, checkHasSnapName: boolean }[] =
[]
for (let i = 0; layer.presets !== undefined && i < layer.presets.length; i++) {
@ -51,6 +51,7 @@ export default class DependencyCalculator {
reason: `preset \`${preset.title.textFor("en")}\` snaps to this layer`,
context: `${layer.id}.presets[${i}]`,
neededBy: layer.id,
checkHasSnapName: true
})
})
}
@ -62,6 +63,7 @@ export default class DependencyCalculator {
reason: "a tagrendering needs this layer",
context: tr.id,
neededBy: layer.id,
checkHasSnapName: false
})
}
}
@ -97,6 +99,7 @@ export default class DependencyCalculator {
"] which calculates the value for " +
currentKey,
neededBy: layer.id,
checkHasSnapName: false
})
return []

View file

@ -587,4 +587,13 @@ export interface LayerConfigJson {
* iftrue: Do not write 'change_within_x_m' and do not indicate that this was done by survey
*/
enableMorePrivacy?: boolean
/**
* question: When a feature is snapped to this name, how should this item be called?
*
* In the move wizard, the option `snap object onto {snapName}` is shown
*
* group: hidden
*/
snapName?: Translatable
}

View file

@ -22,6 +22,7 @@ import { Overpass } from "../../Logic/Osm/Overpass"
import Constants from "../Constants"
import { QuestionableTagRenderingConfigJson } from "./Json/QuestionableTagRenderingConfigJson"
import MarkdownUtils from "../../Utils/MarkdownUtils"
import { And } from "../../Logic/Tags/And"
import Combine from "../../UI/Base/Combine"
export default class LayerConfig extends WithContextLoader {
@ -50,6 +51,7 @@ export default class LayerConfig extends WithContextLoader {
public readonly allowSplit: boolean
public readonly shownByDefault: boolean
public readonly doCount: boolean
public readonly snapName?: Translation
/**
* In seconds
*/
@ -102,12 +104,13 @@ export default class LayerConfig extends WithContextLoader {
mercatorCrs: json.source["mercatorCrs"],
idKey: json.source["idKey"],
},
json.id
json.id,
)
}
this.allowSplit = json.allowSplit ?? false
this.name = Translations.T(json.name, translationContext + ".name")
this.snapName = Translations.T(json.snapName, translationContext + ".snapName")
if (json.description !== undefined) {
if (Object.keys(json.description).length === 0) {
@ -457,6 +460,22 @@ export default class LayerConfig extends WithContextLoader {
)
}
let presets: string[] = []
if (this.presets.length > 0) {
presets = [
"## Presets",
"The following options to create new points are included:",
MarkdownUtils.list(this.presets.map(preset => {
let snaps = ""
if (preset.preciseInput?.snapToLayers) {
snaps = " (snaps to layers " + preset.preciseInput.snapToLayers.map(id => `\`${id}\``).join(", ") + ")"
}
return "**" + preset.title.txt + "** which has the following tags:" + new And(preset.tags).asHumanString(true) + snaps
})),
]
}
for (const revDep of Utils.Dedup(layerIsNeededBy?.get(this.id) ?? [])) {
extraProps.push(
["This layer is needed as dependency for layer", `[${revDep}](#${revDep})`].join(
@ -566,6 +585,7 @@ export default class LayerConfig extends WithContextLoader {
].join("\n\n"),
MarkdownUtils.list(extraProps),
...usingLayer,
...presets,
...tagsDescription,
"## Supported attributes",
quickOverview,
@ -590,4 +610,35 @@ export default class LayerConfig extends WithContextLoader {
public isLeftRightSensitive(): boolean {
return this.lineRendering.some((lr) => lr.leftRightSensitive)
}
public getMostMatchingPreset(tags: Record<string, string>): PresetConfig {
const presets = this.presets
if (!presets) {
return undefined
}
const matchingPresets = presets
.filter((pr) => new And(pr.tags).matchesProperties(tags))
let mostShadowed = matchingPresets[0]
let mostShadowedTags = new And(mostShadowed.tags)
for (let i = 1; i < matchingPresets.length; i++) {
const pr = matchingPresets[i]
const prTags = new And(pr.tags)
if (mostShadowedTags.shadows(prTags)) {
if (!prTags.shadows(mostShadowedTags)) {
// We have a new most shadowed item
mostShadowed = pr
mostShadowedTags = prTags
} else {
// Both shadow each other: abort
mostShadowed = undefined
break
}
} else if (!prTags.shadows(mostShadowedTags)) {
// The new contender does not win, but it might defeat the current contender
mostShadowed = undefined
break
}
}
return mostShadowed ?? matchingPresets[0]
}
}

View file

@ -18,6 +18,7 @@ import { GeoOperations } from "../../Logic/GeoOperations"
import { Feature } from "geojson"
import MarkdownUtils from "../../Utils/MarkdownUtils"
import { UploadableTag } from "../../Logic/Tags/TagTypes"
import LayerConfig from "./LayerConfig"
export interface Mapping {
readonly if: UploadableTag
@ -84,7 +85,7 @@ export default class TagRenderingConfig {
| string
| TagRenderingConfigJson
| (QuestionableTagRenderingConfigJson & { questionHintIsMd?: boolean }),
context?: string
context?: string,
) {
let json = <string | QuestionableTagRenderingConfigJson>config
if (json === undefined) {
@ -143,7 +144,7 @@ export default class TagRenderingConfig {
this.description = Translations.T(json.description, translationKey + ".description")
this.editButtonAriaLabel = Translations.T(
json.editButtonAriaLabel,
translationKey + ".editButtonAriaLabel"
translationKey + ".editButtonAriaLabel",
)
this.condition = TagUtils.Tag(json.condition ?? { and: [] }, `${context}.condition`)
@ -159,7 +160,7 @@ export default class TagRenderingConfig {
}
this.metacondition = TagUtils.Tag(
json.metacondition ?? { and: [] },
`${context}.metacondition`
`${context}.metacondition`,
)
if (json.freeform) {
if (
@ -177,7 +178,7 @@ export default class TagRenderingConfig {
}, perhaps you meant ${Utils.sortedByLevenshteinDistance(
json.freeform.key,
<any>Validators.availableTypes,
(s) => <any>s
(s) => <any>s,
)}`
}
const type: ValidatorType = <any>json.freeform.type ?? "string"
@ -199,7 +200,7 @@ export default class TagRenderingConfig {
placeholder,
addExtraTags:
json.freeform.addExtraTags?.map((tg, i) =>
TagUtils.ParseUploadableTag(tg, `${context}.extratag[${i}]`)
TagUtils.ParseUploadableTag(tg, `${context}.extratag[${i}]`),
) ?? [],
inline: json.freeform.inline ?? false,
default: json.freeform.default,
@ -265,8 +266,8 @@ export default class TagRenderingConfig {
context,
this.multiAnswer,
this.question !== undefined,
commonIconSize
)
commonIconSize,
),
)
} else {
this.mappings = []
@ -292,7 +293,7 @@ export default class TagRenderingConfig {
for (const expectedKey of keys) {
if (usedKeys.indexOf(expectedKey) < 0) {
const msg = `${context}.mappings[${i}]: This mapping only defines values for ${usedKeys.join(
", "
", ",
)}, but it should also give a value for ${expectedKey}`
this.configuration_warnings.push(msg)
}
@ -339,7 +340,7 @@ export default class TagRenderingConfig {
context: string,
multiAnswer?: boolean,
isQuestionable?: boolean,
commonSize: string = "small"
commonSize: string = "small",
): Mapping {
const ctx = `${translationKey}.mappings.${i}`
if (mapping.if === undefined) {
@ -348,7 +349,7 @@ export default class TagRenderingConfig {
if (mapping.then === undefined) {
if (mapping["render"] !== undefined) {
throw `${ctx}: Invalid mapping: no 'then'-clause found. You might have typed 'render' instead of 'then', change it in ${JSON.stringify(
mapping
mapping,
)}`
}
throw `${ctx}: Invalid mapping: no 'then'-clause found in ${JSON.stringify(mapping)}`
@ -359,7 +360,7 @@ export default class TagRenderingConfig {
if (mapping["render"] !== undefined) {
throw `${ctx}: Invalid mapping: a 'render'-key is present, this is probably a bug: ${JSON.stringify(
mapping
mapping,
)}`
}
if (typeof mapping.if !== "string" && mapping.if["length"] !== undefined) {
@ -382,11 +383,11 @@ export default class TagRenderingConfig {
} else if (mapping.hideInAnswer !== undefined) {
hideInAnswer = TagUtils.Tag(
mapping.hideInAnswer,
`${context}.mapping[${i}].hideInAnswer`
`${context}.mapping[${i}].hideInAnswer`,
)
}
const addExtraTags = (mapping.addExtraTags ?? []).map((str, j) =>
TagUtils.SimpleTag(str, `${ctx}.addExtraTags[${j}]`)
TagUtils.SimpleTag(str, `${ctx}.addExtraTags[${j}]`),
)
if (hideInAnswer === true && addExtraTags.length > 0) {
throw `${ctx}: Invalid mapping: 'hideInAnswer' is set to 'true', but 'addExtraTags' is enabled as well. This means that extra tags will be applied if this mapping is chosen as answer, but it cannot be chosen as answer. This either indicates a thought error or obsolete code that must be removed.`
@ -482,7 +483,7 @@ export default class TagRenderingConfig {
* @constructor
*/
public GetRenderValues(
tags: Record<string, string>
tags: Record<string, string>,
): { then: Translation; icon?: string; iconClass?: string }[] {
if (!this.multiAnswer) {
return [this.GetRenderValueWithImage(tags)]
@ -505,7 +506,7 @@ export default class TagRenderingConfig {
return mapping
}
return undefined
})
}),
)
if (freeformKeyDefined && tags[this.freeform.key] !== undefined) {
@ -513,7 +514,7 @@ export default class TagRenderingConfig {
applicableMappings
?.flatMap((m) => m.if?.usedTags() ?? [])
?.filter((kv) => kv.key === this.freeform.key)
?.map((kv) => kv.value)
?.map((kv) => kv.value),
)
const freeformValues = tags[this.freeform.key].split(";")
@ -522,7 +523,7 @@ export default class TagRenderingConfig {
applicableMappings.push({
then: new TypedTranslation<object>(
this.render.replace("{" + this.freeform.key + "}", leftover).translations,
this.render.context
this.render.context,
),
})
}
@ -540,7 +541,7 @@ export default class TagRenderingConfig {
* @constructor
*/
public GetRenderValueWithImage(
tags: Record<string, string>
tags: Record<string, string>,
): { then: TypedTranslation<any>; icon?: string; iconClass?: string } | undefined {
if (this.condition !== undefined) {
if (!this.condition.matchesProperties(tags)) {
@ -609,7 +610,7 @@ export default class TagRenderingConfig {
const answerMappings = this.mappings?.filter((m) => m.hideInAnswer !== true)
if (key === undefined) {
const values: { k: string; v: string }[][] = Utils.NoNull(
answerMappings?.map((m) => m.if.asChange({})) ?? []
answerMappings?.map((m) => m.if.asChange({})) ?? [],
)
if (values.length === 0) {
return
@ -627,15 +628,15 @@ export default class TagRenderingConfig {
return {
key: commonKey,
values: Utils.NoNull(
values.map((arr) => arr.filter((item) => item.k === commonKey)[0]?.v)
values.map((arr) => arr.filter((item) => item.k === commonKey)[0]?.v),
),
}
}
let values = Utils.NoNull(
answerMappings?.map(
(m) => m.if.asChange({}).filter((item) => item.k === key)[0]?.v
) ?? []
(m) => m.if.asChange({}).filter((item) => item.k === key)[0]?.v,
) ?? [],
)
if (values.length === undefined) {
values = undefined
@ -699,7 +700,7 @@ export default class TagRenderingConfig {
freeformValue: string | undefined,
singleSelectedMapping: number,
multiSelectedMapping: boolean[] | undefined,
currentProperties: Record<string, string>
currentProperties: Record<string, string>,
): UploadableTag {
if (typeof freeformValue === "string") {
freeformValue = freeformValue?.trim()
@ -774,7 +775,7 @@ export default class TagRenderingConfig {
new And([
new Tag(this.freeform.key, freeformValue),
...(this.freeform.addExtraTags ?? []),
])
]),
)
}
const and = TagUtils.FlattenMultiAnswer([...selectedMappings, ...unselectedMappings])
@ -844,11 +845,11 @@ export default class TagRenderingConfig {
}
const msgs: string[] = [
icon +
" " +
"*" +
m.then.textFor(lang) +
"* is shown if with " +
m.if.asHumanString(true, false, {}),
" " +
"*" +
m.then.textFor(lang) +
"* is shown if with " +
m.if.asHumanString(true, false, {}),
]
if (m.hideInAnswer === true) {
@ -857,11 +858,11 @@ export default class TagRenderingConfig {
if (m.ifnot !== undefined) {
msgs.push(
"Unselecting this answer will add " +
m.ifnot.asHumanString(true, false, {})
m.ifnot.asHumanString(true, false, {}),
)
}
return msgs.join(". ")
})
}),
)
}
@ -870,7 +871,7 @@ export default class TagRenderingConfig {
const conditionAsLink = (<TagsFilter>this.condition.optimize()).asHumanString(
true,
false,
{}
{},
)
condition =
"This tagrendering is only visible in the popup if the following condition is met: " +
@ -904,7 +905,7 @@ export default class TagRenderingConfig {
this.metacondition,
this.condition,
this.freeform?.key ? new RegexTag(this.freeform?.key, /.*/) : undefined,
this.invalidValues
this.invalidValues,
)
for (const m of this.mappings ?? []) {
tags.push(m.if)
@ -922,18 +923,42 @@ export default class TagRenderingConfig {
/**
* The keys that should be erased if one has to revert to 'unknown'.
* Might give undefined
* Might give undefined if setting to unknown is not possible
*/
public settableKeys(): string[] | undefined {
public removeToSetUnknown(partOfLayer: LayerConfig, currentTags: Record<string, string>): string[] | undefined {
const toDelete = new Set<string>()
if (this.freeform) {
toDelete.add(this.freeform.key)
}
for (const mapping of this.mappings) {
for (const usedKey of mapping.if.usedKeys()) {
toDelete.add(usedKey)
const extraTags = new And(this.freeform.addExtraTags ?? []).usedKeys().filter(k => k !== "fixme")
if (extraTags.length > 0) {
return undefined
}
}
if (this.mappings?.length > 0) {
const mainkey = this.mappings[0].if.usedKeys()
mainkey.forEach(k => toDelete.add(k))
for (const mapping of this.mappings) {
if (mapping.addExtraTags?.length > 0) {
return undefined
}
for (const usedKey of mapping.if.usedKeys()) {
if (mainkey.indexOf(usedKey) < 0) {
// This is a complicated case, we ignore this for now
return undefined
}
}
}
}
currentTags = { ...currentTags }
for (const key of toDelete) {
delete currentTags[key]
}
const required = partOfLayer.source.osmTags
if (!required.matchesProperties(currentTags)) {
return undefined
}
return Array.from(toDelete)
}
@ -943,7 +968,7 @@ export class TagRenderingConfigUtils {
public static withNameSuggestionIndex(
config: TagRenderingConfig,
tags: UIEventSource<Record<string, string>>,
feature?: Feature
feature?: Feature,
): Store<TagRenderingConfig> {
const isNSI = NameSuggestionIndex.supportedTypes().indexOf(config.freeform?.key) >= 0
if (!isNSI) {
@ -961,8 +986,8 @@ export class TagRenderingConfigUtils {
tags,
country.split(";"),
center,
{ sortByFrequency: true }
)
{ sortByFrequency: true },
),
)
})
return extraMappings.map((extraMappings) => {
@ -978,7 +1003,7 @@ export class TagRenderingConfigUtils {
...m,
addExtraTags: [new Tag("nobrand", "")],
priorityIf: m.priorityIf ?? TagUtils.Tag("id~*"),
}
},
) ?? []
clone.mappings = [...oldMappingsCloned, ...extraMappings]
return clone

View file

@ -58,7 +58,7 @@ import { GeolocationControlState } from "../UI/BigComponents/GeolocationControl"
import Zoomcontrol from "../UI/Zoomcontrol"
import {
SummaryTileSource,
SummaryTileSourceRewriter
SummaryTileSourceRewriter,
} from "../Logic/FeatureSource/TiledFeatureSource/SummaryTileSource"
import summaryLayer from "../assets/generated/layers/summary.json"
import last_click_layerconfig from "../assets/generated/layers/last_click.json"
@ -175,7 +175,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
"oauth_token",
undefined,
"Used to complete the login"
)
),
})
this.userRelatedState = new UserRelatedState(
this.osmConnection,
@ -254,8 +254,8 @@ export default class ThemeViewState implements SpecialVisualizationState {
bbox.asGeoJson({
zoom: this.mapProperties.zoom.data,
...this.mapProperties.location.data,
id: "current_view_" + currentViewIndex
})
id: "current_view_" + currentViewIndex,
}),
]
})
)
@ -272,7 +272,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
featurePropertiesStore: this.featureProperties,
osmConnection: this.osmConnection,
historicalUserLocations: this.geolocation.historicalUserLocations,
featureSwitches: this.featureSwitches
featureSwitches: this.featureSwitches,
},
layout?.isLeftRightSensitive() ?? false,
(e, extraMsg) => this.reportError(e, extraMsg)
@ -300,7 +300,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
"leftover features, such as",
features[0].properties
)
}
},
}
)
this.perLayer = perLayer.perLayer
@ -356,7 +356,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
{
currentZoom: this.mapProperties.zoom,
layerState: this.layerState,
bounds: this.visualFeedbackViewportBounds
bounds: this.visualFeedbackViewportBounds,
}
)
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView
@ -453,7 +453,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
doShowLayer,
metaTags: this.userRelatedState.preferencesAsTags,
selectedElement: this.selectedElement,
fetchStore: (id) => this.featureProperties.getStore(id)
fetchStore: (id) => this.featureProperties.getStore(id),
})
})
return filteringFeatureSource
@ -480,7 +480,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
doShowLayer: flayerGps.isDisplayed,
layer: flayerGps.layerDef,
metaTags: this.userRelatedState.preferencesAsTags,
selectedElement: this.selectedElement
selectedElement: this.selectedElement,
})
}
@ -554,11 +554,11 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.previewedImage.setData(undefined)
return
}
if(this.searchState.showSearchDrawer.data){
if (this.searchState.showSearchDrawer.data){
this.searchState.showSearchDrawer.set(false)
return
}
if(this.guistate.closeAll()){
if (this.guistate.closeAll()){
return
}
this.selectedElement.setData(undefined)
@ -573,17 +573,14 @@ export default class ThemeViewState implements SpecialVisualizationState {
Hotkeys.RegisterHotkey(
{
nomod: " ",
onUp: true
onUp: true,
},
docs.selectItem,
() => {
if (this.selectedElement.data !== undefined) {
return false
}
if (
this.guistate.isSomethingOpen() ||
this.previewedImage.data !== undefined
) {
if (this.guistate.isSomethingOpen() || this.previewedImage.data !== undefined) {
return
}
if(document.activeElement.tagName === "button" || document.activeElement.tagName === "input"){
@ -605,7 +602,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
Hotkeys.RegisterHotkey(
{
nomod: "" + i,
onUp: true
onUp: true,
},
doc,
() => this.selectClosestAtCenter(i - 1)
@ -624,7 +621,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
}
Hotkeys.RegisterHotkey(
{
nomod: "b"
nomod: "b",
},
docs.openLayersPanel,
() => {
@ -635,7 +632,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
)
Hotkeys.RegisterHotkey(
{
nomod: "s"
nomod: "s",
},
Translations.t.hotkeyDocumentation.openFilterPanel,
() => {
@ -713,12 +710,12 @@ export default class ThemeViewState implements SpecialVisualizationState {
Hotkeys.RegisterHotkey(
{
shift: "T"
shift: "T",
},
Translations.t.hotkeyDocumentation.translationMode,
() => {
const tm = this.userRelatedState.translationMode
if(tm.data === "false"){
if (tm.data === "false") {
tm.setData("true")
} else {
tm.setData("false")
@ -750,7 +747,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.mapProperties.zoom.map((z) => Math.max(Math.floor(z), 0)),
this.mapProperties,
{
isActive: this.mapProperties.zoom.map((z) => z < maxzoom)
isActive: this.mapProperties.zoom.map((z) => z < maxzoom),
}
)
@ -837,7 +834,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
doShowLayer: flayer.isDisplayed,
layer: flayer.layerDef,
metaTags: this.userRelatedState.preferencesAsTags,
selectedElement: this.selectedElement
selectedElement: this.selectedElement,
})
})
const summaryLayerConfig = new LayerConfig(<LayerConfigJson>summaryLayer, "summaryLayer")
@ -845,7 +842,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
features: specialLayers.summary,
layer: summaryLayerConfig,
// doShowLayer: this.mapProperties.zoom.map((z) => z < maxzoom),
selectedElement: this.selectedElement
selectedElement: this.selectedElement,
})
const lastClickLayerConfig = new LayerConfig(
@ -856,14 +853,14 @@ export default class ThemeViewState implements SpecialVisualizationState {
lastClickLayerConfig.isShown === undefined
? specialLayers.last_click
: specialLayers.last_click.features.mapD((fs) =>
fs.filter((f) => {
const matches = lastClickLayerConfig.isShown.matchesProperties(
f.properties
)
console.debug("LastClick ", f, "matches", matches)
return matches
})
)
fs.filter((f) => {
const matches = lastClickLayerConfig.isShown.matchesProperties(
f.properties
)
console.debug("LastClick ", f, "matches", matches)
return matches
})
)
new ShowDataLayer(this.map, {
features: new StaticFeatureSource(lastClickFiltered),
layer: lastClickLayerConfig,
@ -874,9 +871,9 @@ export default class ThemeViewState implements SpecialVisualizationState {
}
this.map.data.flyTo({
zoom: Constants.minZoomLevelToAddNewPoint,
center: GeoOperations.centerpointCoordinates(feature)
center: GeoOperations.centerpointCoordinates(feature),
})
}
},
})
}
@ -968,7 +965,13 @@ export default class ThemeViewState implements SpecialVisualizationState {
} catch (e) {
// pass
}
message = "XMLHttpRequest with status code " + req.status + ", " + req.statusText + ", received: " + body
message =
"XMLHttpRequest with status code " +
req.status +
", " +
req.statusText +
", received: " +
body
}
if (extramessage) {
@ -990,8 +993,8 @@ export default class ThemeViewState implements SpecialVisualizationState {
userid: this.osmConnection.userDetails.data?.uid,
pendingChanges: this.changes.pendingChanges.data,
previousChanges: this.changes.allChanges.data,
changeRewrites: Utils.MapToObj(this.changes._changesetHandler._remappings)
})
changeRewrites: Utils.MapToObj(this.changes._changesetHandler._remappings),
}),
})
} catch (e) {
console.error("Could not upload an error report")