forked from MapComplete/MapComplete
Feature: allow to move and snap to a layer, fix #2120
This commit is contained in:
parent
eb89427bfc
commit
fdedb75954
34 changed files with 824 additions and 301 deletions
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue