forked from MapComplete/MapComplete
Studio: improvements after user test
This commit is contained in:
parent
449c1adb00
commit
e79a0fc81d
59 changed files with 1312 additions and 2920 deletions
|
@ -118,13 +118,10 @@ export class OsmConnection {
|
|||
if (options.oauth_token?.data !== undefined) {
|
||||
console.log(options.oauth_token.data)
|
||||
const self = this
|
||||
this.auth.bootstrapToken(
|
||||
options.oauth_token.data,
|
||||
(err, result) => {
|
||||
console.log("Bootstrap token called back", err, result)
|
||||
self.AttemptLogin()
|
||||
}
|
||||
)
|
||||
this.auth.bootstrapToken(options.oauth_token.data, (err, result) => {
|
||||
console.log("Bootstrap token called back", err, result)
|
||||
self.AttemptLogin()
|
||||
})
|
||||
|
||||
options.oauth_token.setData(undefined)
|
||||
}
|
||||
|
@ -281,20 +278,24 @@ export class OsmConnection {
|
|||
content?: string,
|
||||
allowAnonymous: boolean = false
|
||||
): Promise<string> {
|
||||
|
||||
let connection: OSMAuthInstance = this.auth
|
||||
if(allowAnonymous && !this.auth.authenticated()) {
|
||||
const possibleResult = await Utils.downloadAdvanced(`${this.Backend()}/api/0.6/${path}`,header, method, content)
|
||||
if(possibleResult["content"]) {
|
||||
if (allowAnonymous && !this.auth.authenticated()) {
|
||||
const possibleResult = await Utils.downloadAdvanced(
|
||||
`${this.Backend()}/api/0.6/${path}`,
|
||||
header,
|
||||
method,
|
||||
content
|
||||
)
|
||||
if (possibleResult["content"]) {
|
||||
return possibleResult["content"]
|
||||
}
|
||||
console.error(possibleResult)
|
||||
throw "Could not interact with OSM:"+possibleResult["error"]
|
||||
throw "Could not interact with OSM:" + possibleResult["error"]
|
||||
}
|
||||
|
||||
return new Promise((ok, error) => {
|
||||
connection.xhr(
|
||||
<any> {
|
||||
<any>{
|
||||
method,
|
||||
options: {
|
||||
header,
|
||||
|
@ -330,8 +331,12 @@ export class OsmConnection {
|
|||
return await this.interact(path, "PUT", header, content)
|
||||
}
|
||||
|
||||
public async get(path: string, header?: Record<string, string | number>): Promise<any> {
|
||||
return await this.interact(path, "GET", header)
|
||||
public async get(
|
||||
path: string,
|
||||
header?: Record<string, string | number>,
|
||||
allowAnonymous: boolean = false
|
||||
): Promise<string> {
|
||||
return await this.interact(path, "GET", header, undefined, allowAnonymous)
|
||||
}
|
||||
|
||||
public closeNote(id: number | string, text?: string): Promise<void> {
|
||||
|
@ -374,9 +379,14 @@ export class OsmConnection {
|
|||
}
|
||||
// Lat and lon must be strings for the API to accept it
|
||||
const content = `lat=${lat}&lon=${lon}&text=${encodeURIComponent(text)}`
|
||||
const response = await this.post("notes.json", content, {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
|
||||
}, true)
|
||||
const response = await this.post(
|
||||
"notes.json",
|
||||
content,
|
||||
{
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
},
|
||||
true
|
||||
)
|
||||
const parsed = JSON.parse(response)
|
||||
console.log("Got result:", parsed)
|
||||
const id = parsed.properties
|
||||
|
@ -519,7 +529,6 @@ export class OsmConnection {
|
|||
singlepage: !standalone,
|
||||
auto: true,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private CheckForMessagesContinuously() {
|
||||
|
@ -543,6 +552,29 @@ export class OsmConnection {
|
|||
})
|
||||
}
|
||||
|
||||
private readonly _userInfoCache: Record<number, any> = {}
|
||||
public async getInformationAboutUser(id: number): Promise<{
|
||||
id: number
|
||||
display_name: string
|
||||
account_created: string
|
||||
description: string
|
||||
contributor_terms: { agreed: boolean }
|
||||
roles: []
|
||||
changesets: { count: number }
|
||||
traces: { count: number }
|
||||
blocks: { received: { count: number; active: number } }
|
||||
}> {
|
||||
if (id === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (this._userInfoCache[id]) {
|
||||
return this._userInfoCache[id]
|
||||
}
|
||||
const info = await this.get("user/" + id + ".json", { accepts: "application/json" }, true)
|
||||
const parsed = JSON.parse(info)["user"]
|
||||
this._userInfoCache[id] = parsed
|
||||
return parsed
|
||||
}
|
||||
private async FetchCapabilities(): Promise<{ api: OsmServiceState; gpx: OsmServiceState }> {
|
||||
if (Utils.runningFromConsole) {
|
||||
return { api: "online", gpx: "online" }
|
||||
|
|
|
@ -79,6 +79,9 @@ export class Tag extends TagsFilter {
|
|||
currentProperties?: Record<string, string>
|
||||
) {
|
||||
let v = this.value
|
||||
if (typeof v !== "string") {
|
||||
v = JSON.stringify(v)
|
||||
}
|
||||
if (shorten) {
|
||||
v = Utils.EllipsesAfter(v, 25)
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ import { Or } from "./Or"
|
|||
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
|
||||
import key_counts from "../../assets/key_totals.json"
|
||||
|
||||
import { ConversionContext } from "../../Models/ThemeConfig/Conversion/ConversionContext"
|
||||
|
||||
type Tags = Record<string, string>
|
||||
export type UploadableTag = Tag | SubstitutingTag | And
|
||||
|
||||
|
@ -475,12 +477,18 @@ export class TagUtils {
|
|||
* regex.matchesProperties({maxspeed: "50 mph"}) // => true
|
||||
*/
|
||||
|
||||
public static Tag(json: TagConfigJson, context: string = ""): TagsFilter {
|
||||
public static Tag(json: TagConfigJson, context: string | ConversionContext = ""): TagsFilter {
|
||||
try {
|
||||
return this.ParseTagUnsafe(json, context)
|
||||
let ctx = typeof context === "string" ? context : context.path.join(".")
|
||||
return this.ParseTagUnsafe(json, ctx)
|
||||
} catch (e) {
|
||||
console.error("Could not parse tag", json, "in context", context, "due to ", e)
|
||||
throw e
|
||||
if (typeof context === "string") {
|
||||
console.error("Could not parse tag", json, "in context", context, "due to ", e)
|
||||
throw e
|
||||
} else {
|
||||
context.err(e)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { ConversionContext, DesugaringStep } from "./Conversion"
|
||||
import { DesugaringStep } from "./Conversion"
|
||||
import { Utils } from "../../../Utils"
|
||||
import Translations from "../../../UI/i18n/Translations"
|
||||
import { ConversionContext } from "./ConversionContext"
|
||||
|
||||
export class AddContextToTranslations<T> extends DesugaringStep<T> {
|
||||
private readonly _prefix: string
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import { Utils } from "../../../Utils"
|
||||
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
|
||||
import { ConversionContext } from "./ConversionContext"
|
||||
|
||||
export interface DesugaringContext {
|
||||
tagRenderings: Map<string, QuestionableTagRenderingConfigJson>
|
||||
|
@ -8,112 +9,6 @@ export interface DesugaringContext {
|
|||
publicLayers?: Set<string>
|
||||
}
|
||||
|
||||
export class ConversionContext {
|
||||
/**
|
||||
* The path within the data structure where we are currently operating
|
||||
*/
|
||||
readonly path: ReadonlyArray<string | number>
|
||||
/**
|
||||
* Some information about the current operation
|
||||
*/
|
||||
readonly operation: ReadonlyArray<string>
|
||||
readonly messages: ConversionMessage[]
|
||||
|
||||
private constructor(
|
||||
messages: ConversionMessage[],
|
||||
path: ReadonlyArray<string | number>,
|
||||
operation?: ReadonlyArray<string>
|
||||
) {
|
||||
this.path = path
|
||||
this.operation = operation ?? []
|
||||
// Messages is shared by reference amonst all 'context'-objects for performance
|
||||
this.messages = messages
|
||||
if (this.path.some((p) => typeof p === "object" || p === "[object Object]")) {
|
||||
throw "ConversionMessage: got an object as path entry:" + JSON.stringify(path)
|
||||
}
|
||||
}
|
||||
|
||||
public static construct(path: (string | number)[], operation: string[]) {
|
||||
return new ConversionContext([], [...path], [...operation])
|
||||
}
|
||||
|
||||
public static test(msg?: string) {
|
||||
return new ConversionContext([], msg ? [msg] : [], ["test"])
|
||||
}
|
||||
|
||||
static print(msg: ConversionMessage) {
|
||||
const noString = msg.context.path.filter(
|
||||
(p) => typeof p !== "string" && typeof p !== "number"
|
||||
)
|
||||
if (noString.length > 0) {
|
||||
console.warn("Non-string value in path:", ...noString)
|
||||
}
|
||||
if (msg.level === "error") {
|
||||
console.error(
|
||||
ConversionContext.red("ERR "),
|
||||
msg.context.path.join("."),
|
||||
ConversionContext.red(msg.message),
|
||||
msg.context.operation.join(".")
|
||||
)
|
||||
} else if (msg.level === "warning") {
|
||||
console.warn(
|
||||
ConversionContext.red("<!> "),
|
||||
msg.context.path.join("."),
|
||||
ConversionContext.yellow(msg.message),
|
||||
msg.context.operation.join(".")
|
||||
)
|
||||
} else {
|
||||
console.log(" ", msg.context.path.join("."), msg.message)
|
||||
}
|
||||
}
|
||||
|
||||
private static yellow(s) {
|
||||
return "\x1b[33m" + s + "\x1b[0m"
|
||||
}
|
||||
|
||||
private static red(s) {
|
||||
return "\x1b[31m" + s + "\x1b[0m"
|
||||
}
|
||||
|
||||
public enter(key: string | number | (string | number)[]) {
|
||||
if (!Array.isArray(key)) {
|
||||
return new ConversionContext(this.messages, [...this.path, key], this.operation)
|
||||
}
|
||||
return new ConversionContext(this.messages, [...this.path, ...key], this.operation)
|
||||
}
|
||||
|
||||
public enters(...key: (string | number)[]) {
|
||||
return this.enter(key)
|
||||
}
|
||||
|
||||
public inOperation(key: string) {
|
||||
return new ConversionContext(this.messages, this.path, [...this.operation, key])
|
||||
}
|
||||
|
||||
warn(message: string) {
|
||||
this.messages.push({ context: this, level: "warning", message })
|
||||
}
|
||||
|
||||
err(message: string) {
|
||||
this.messages.push({ context: this, level: "error", message })
|
||||
}
|
||||
|
||||
info(message: string) {
|
||||
this.messages.push({ context: this, level: "information", message })
|
||||
}
|
||||
|
||||
getAll(mode: ConversionMsgLevel): ConversionMessage[] {
|
||||
return this.messages.filter((m) => m.level === mode)
|
||||
}
|
||||
public hasErrors() {
|
||||
return this.messages?.find((m) => m.level === "error") !== undefined
|
||||
}
|
||||
|
||||
debug(message: string) {
|
||||
this.messages.push({ context: this, level: "debug", message })
|
||||
}
|
||||
}
|
||||
|
||||
export type ConversionMsgLevel = "debug" | "information" | "warning" | "error"
|
||||
export interface ConversionMessage {
|
||||
context: ConversionContext
|
||||
|
|
116
src/Models/ThemeConfig/Conversion/ConversionContext.ts
Normal file
116
src/Models/ThemeConfig/Conversion/ConversionContext.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
import { ConversionMessage, ConversionMsgLevel } from "./Conversion"
|
||||
|
||||
export class ConversionContext {
|
||||
/**
|
||||
* The path within the data structure where we are currently operating
|
||||
*/
|
||||
readonly path: ReadonlyArray<string | number>
|
||||
/**
|
||||
* Some information about the current operation
|
||||
*/
|
||||
readonly operation: ReadonlyArray<string>
|
||||
readonly messages: ConversionMessage[]
|
||||
|
||||
private _hasErrors: boolean = false
|
||||
|
||||
private constructor(
|
||||
messages: ConversionMessage[],
|
||||
path: ReadonlyArray<string | number>,
|
||||
operation?: ReadonlyArray<string>
|
||||
) {
|
||||
this.path = path
|
||||
this.operation = operation ?? []
|
||||
// Messages is shared by reference amonst all 'context'-objects for performance
|
||||
this.messages = messages
|
||||
if (this.path.some((p) => typeof p === "object" || p === "[object Object]")) {
|
||||
throw "ConversionMessage: got an object as path entry:" + JSON.stringify(path)
|
||||
}
|
||||
}
|
||||
|
||||
public static construct(path: (string | number)[], operation: string[]) {
|
||||
return new ConversionContext([], [...path], [...operation])
|
||||
}
|
||||
|
||||
public static test(msg?: string) {
|
||||
return new ConversionContext([], msg ? [msg] : [], ["test"])
|
||||
}
|
||||
|
||||
static print(msg: ConversionMessage) {
|
||||
const noString = msg.context.path.filter(
|
||||
(p) => typeof p !== "string" && typeof p !== "number"
|
||||
)
|
||||
if (noString.length > 0) {
|
||||
console.warn("Non-string value in path:", ...noString)
|
||||
}
|
||||
if (msg.level === "error") {
|
||||
console.error(
|
||||
ConversionContext.red("ERR "),
|
||||
msg.context.path.join("."),
|
||||
ConversionContext.red(msg.message),
|
||||
msg.context.operation.join(".")
|
||||
)
|
||||
} else if (msg.level === "warning") {
|
||||
console.warn(
|
||||
ConversionContext.red("<!> "),
|
||||
msg.context.path.join("."),
|
||||
ConversionContext.yellow(msg.message),
|
||||
msg.context.operation.join(".")
|
||||
)
|
||||
} else {
|
||||
console.log(" ", msg.context.path.join("."), msg.message)
|
||||
}
|
||||
}
|
||||
|
||||
private static yellow(s) {
|
||||
return "\x1b[33m" + s + "\x1b[0m"
|
||||
}
|
||||
|
||||
private static red(s) {
|
||||
return "\x1b[31m" + s + "\x1b[0m"
|
||||
}
|
||||
|
||||
public enter(key: string | number | (string | number)[]) {
|
||||
if (!Array.isArray(key)) {
|
||||
return new ConversionContext(this.messages, [...this.path, key], this.operation)
|
||||
}
|
||||
return new ConversionContext(this.messages, [...this.path, ...key], this.operation)
|
||||
}
|
||||
|
||||
public enters(...key: (string | number)[]) {
|
||||
return this.enter(key)
|
||||
}
|
||||
|
||||
public inOperation(key: string) {
|
||||
return new ConversionContext(this.messages, this.path, [...this.operation, key])
|
||||
}
|
||||
|
||||
warn(message: string) {
|
||||
this.messages.push({ context: this, level: "warning", message })
|
||||
}
|
||||
|
||||
err(message: string) {
|
||||
this._hasErrors = true
|
||||
this.messages.push({ context: this, level: "error", message })
|
||||
}
|
||||
|
||||
info(message: string) {
|
||||
this.messages.push({ context: this, level: "information", message })
|
||||
}
|
||||
|
||||
getAll(mode: ConversionMsgLevel): ConversionMessage[] {
|
||||
return this.messages.filter((m) => m.level === mode)
|
||||
}
|
||||
|
||||
public hasErrors() {
|
||||
if (this._hasErrors) {
|
||||
return true
|
||||
}
|
||||
const foundErr = this.messages?.find((m) => m.level === "error") !== undefined
|
||||
this._hasErrors = foundErr
|
||||
return foundErr
|
||||
}
|
||||
|
||||
debug(message: string) {
|
||||
this.messages.push({ context: this, level: "debug", message })
|
||||
}
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
import { Conversion, ConversionContext } from "./Conversion"
|
||||
import { Conversion } from "./Conversion"
|
||||
import LayerConfig from "../LayerConfig"
|
||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import Translations from "../../../UI/i18n/Translations"
|
||||
import { Translation, TypedTranslation } from "../../../UI/i18n/Translation"
|
||||
import { ConversionContext } from "./ConversionContext"
|
||||
|
||||
export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, LayerConfigJson> {
|
||||
/**
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Conversion, ConversionContext, DesugaringStep } from "./Conversion"
|
||||
import { Conversion, DesugaringStep } from "./Conversion"
|
||||
import { LayoutConfigJson } from "../Json/LayoutConfigJson"
|
||||
import { Utils } from "../../../Utils"
|
||||
import metapaths from "../../../assets/schemas/layoutconfigmeta.json"
|
||||
|
@ -6,6 +6,7 @@ import tagrenderingmetapaths from "../../../assets/schemas/questionabletagrender
|
|||
import Translations from "../../../UI/i18n/Translations"
|
||||
|
||||
import { parse as parse_html } from "node-html-parser"
|
||||
import { ConversionContext } from "./ConversionContext"
|
||||
|
||||
export class ExtractImages extends Conversion<
|
||||
LayoutConfigJson,
|
||||
|
|
|
@ -2,8 +2,9 @@ import { LayoutConfigJson } from "../Json/LayoutConfigJson"
|
|||
import { Utils } from "../../../Utils"
|
||||
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
|
||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import { ConversionContext, DesugaringStep, Each, Fuse, On } from "./Conversion"
|
||||
import { DesugaringStep, Each, Fuse, On } from "./Conversion"
|
||||
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"
|
||||
import { ConversionContext } from "./ConversionContext"
|
||||
|
||||
export class UpdateLegacyLayer extends DesugaringStep<
|
||||
LayerConfigJson | string | { builtin; override }
|
||||
|
@ -57,6 +58,9 @@ export class UpdateLegacyLayer extends DesugaringStep<
|
|||
if (config.tagRenderings !== undefined) {
|
||||
let i = 0
|
||||
for (const tagRendering of config.tagRenderings) {
|
||||
if (!tagRendering) {
|
||||
continue
|
||||
}
|
||||
i++
|
||||
if (
|
||||
typeof tagRendering === "string" ||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import {
|
||||
Cached,
|
||||
Concat,
|
||||
Conversion,
|
||||
ConversionContext,
|
||||
DesugaringContext,
|
||||
DesugaringStep,
|
||||
Each,
|
||||
|
@ -32,7 +30,7 @@ import { RenderingSpecification } from "../../../UI/SpecialVisualization"
|
|||
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
|
||||
import { ConfigMeta } from "../../../UI/Studio/configMeta"
|
||||
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
|
||||
import { j } from "vite-node/types-63205a44"
|
||||
import { ConversionContext } from "./ConversionContext"
|
||||
|
||||
class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
||||
private static readonly predefinedFilters = ExpandFilter.load_filters()
|
||||
|
@ -1192,9 +1190,9 @@ class ExpandMarkerRenderings extends DesugaringStep<IconConfigJson> {
|
|||
}
|
||||
}
|
||||
|
||||
export class PrepareLayer extends Cached<LayerConfigJson, LayerConfigJson> {
|
||||
export class PrepareLayer extends Fuse<LayerConfigJson> {
|
||||
constructor(state: DesugaringContext) {
|
||||
const steps = new Fuse<LayerConfigJson>(
|
||||
super(
|
||||
"Fully prepares and expands a layer for the LayerConfig.",
|
||||
new On("tagRenderings", new Each(new RewriteSpecial())),
|
||||
new On("tagRenderings", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)),
|
||||
|
@ -1224,6 +1222,5 @@ export class PrepareLayer extends Cached<LayerConfigJson, LayerConfigJson> {
|
|||
),
|
||||
new ExpandFilter(state)
|
||||
)
|
||||
super(steps)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import {
|
||||
Concat,
|
||||
Conversion,
|
||||
ConversionContext,
|
||||
DesugaringContext,
|
||||
DesugaringStep,
|
||||
Each,
|
||||
|
@ -21,6 +20,7 @@ import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
|
|||
import DependencyCalculator from "../DependencyCalculator"
|
||||
import { AddContextToTranslations } from "./AddContextToTranslations"
|
||||
import ValidationUtils from "./ValidationUtils"
|
||||
import { ConversionContext } from "./ConversionContext"
|
||||
|
||||
class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJson[]> {
|
||||
private readonly _state: DesugaringContext
|
||||
|
|
|
@ -1,13 +1,4 @@
|
|||
import {
|
||||
Conversion,
|
||||
ConversionContext,
|
||||
DesugaringStep,
|
||||
Each,
|
||||
Fuse,
|
||||
On,
|
||||
Pipe,
|
||||
Pure,
|
||||
} from "./Conversion"
|
||||
import { Conversion, DesugaringStep, Each, Fuse, On, Pipe, Pure } from "./Conversion"
|
||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import LayerConfig from "../LayerConfig"
|
||||
import { Utils } from "../../../Utils"
|
||||
|
@ -29,6 +20,8 @@ import TagRenderingConfig from "../TagRenderingConfig"
|
|||
import { parse as parse_html } from "node-html-parser"
|
||||
import PresetConfig from "../PresetConfig"
|
||||
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
|
||||
import { Translatable } from "../Json/Translatable"
|
||||
import { ConversionContext } from "./ConversionContext"
|
||||
|
||||
class ValidateLanguageCompleteness extends DesugaringStep<any> {
|
||||
private readonly _languages: string[]
|
||||
|
@ -285,7 +278,7 @@ export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
|
|||
new Each(
|
||||
new Pipe(
|
||||
new ValidateLayer(undefined, isBuiltin, doesImageExist, false, true),
|
||||
new Pure((x) => x.raw)
|
||||
new Pure((x) => x?.raw)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -375,32 +368,34 @@ export class DetectConflictingAddExtraTags extends DesugaringStep<TagRenderingCo
|
|||
return json
|
||||
}
|
||||
|
||||
const tagRendering = new TagRenderingConfig(json)
|
||||
try {
|
||||
const tagRendering = new TagRenderingConfig(json)
|
||||
|
||||
const errors = []
|
||||
for (let i = 0; i < tagRendering.mappings.length; i++) {
|
||||
const mapping = tagRendering.mappings[i]
|
||||
if (!mapping.addExtraTags) {
|
||||
continue
|
||||
for (let i = 0; i < tagRendering.mappings.length; i++) {
|
||||
const mapping = tagRendering.mappings[i]
|
||||
if (!mapping.addExtraTags) {
|
||||
continue
|
||||
}
|
||||
const keysInMapping = new Set(mapping.if.usedKeys())
|
||||
|
||||
const keysInAddExtraTags = mapping.addExtraTags.map((t) => t.key)
|
||||
|
||||
const duplicateKeys = keysInAddExtraTags.filter((k) => keysInMapping.has(k))
|
||||
if (duplicateKeys.length > 0) {
|
||||
context
|
||||
.enters("mappings", i)
|
||||
.err(
|
||||
"AddExtraTags overrides a key that is set in the `if`-clause of this mapping. Selecting this answer might thus first set one value (needed to match as answer) and then override it with a different value, resulting in an unsaveable question. The offending `addExtraTags` is " +
|
||||
duplicateKeys.join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
const keysInMapping = new Set(mapping.if.usedKeys())
|
||||
|
||||
const keysInAddExtraTags = mapping.addExtraTags.map((t) => t.key)
|
||||
|
||||
const duplicateKeys = keysInAddExtraTags.filter((k) => keysInMapping.has(k))
|
||||
if (duplicateKeys.length > 0) {
|
||||
errors.push(
|
||||
"At " +
|
||||
context +
|
||||
".mappings[" +
|
||||
i +
|
||||
"]: AddExtraTags overrides a key that is set in the `if`-clause of this mapping. Selecting this answer might thus first set one value (needed to match as answer) and then override it with a different value, resulting in an unsaveable question. The offending `addExtraTags` is " +
|
||||
duplicateKeys.join(", ")
|
||||
)
|
||||
}
|
||||
return json
|
||||
} catch (e) {
|
||||
context.err(e)
|
||||
return undefined
|
||||
}
|
||||
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -475,8 +470,8 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
|
|||
"some_calculated_tag_value_for_" + calculatedTagName
|
||||
}
|
||||
const parsedConditions = json.mappings.map((m, i) => {
|
||||
const ctx = `${context}.mappings[${i}]`
|
||||
const ifTags = TagUtils.Tag(m.if, ctx)
|
||||
const c = context.enters("mappings", i)
|
||||
const ifTags = TagUtils.Tag(m.if, c.enter("if"))
|
||||
const hideInAnswer = m["hideInAnswer"]
|
||||
if (hideInAnswer !== undefined && hideInAnswer !== false && hideInAnswer !== true) {
|
||||
let conditionTags = TagUtils.Tag(hideInAnswer)
|
||||
|
@ -486,7 +481,7 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
|
|||
return ifTags
|
||||
})
|
||||
for (let i = 0; i < json.mappings.length; i++) {
|
||||
if (!parsedConditions[i].isUsableAsAnswer()) {
|
||||
if (!parsedConditions[i]?.isUsableAsAnswer()) {
|
||||
// There is no straightforward way to convert this mapping.if into a properties-object, so we simply skip this one
|
||||
// Yes, it might be shadowed, but running this check is to difficult right now
|
||||
continue
|
||||
|
@ -661,12 +656,57 @@ class ValidatePossibleLinks extends DesugaringStep<string | Record<string, strin
|
|||
}
|
||||
}
|
||||
|
||||
class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
||||
private _options: { noQuestionHintCheck: boolean }
|
||||
class CheckTranslation extends DesugaringStep<Translatable> {
|
||||
public static readonly allowUndefined: CheckTranslation = new CheckTranslation(true)
|
||||
public static readonly noUndefined: CheckTranslation = new CheckTranslation()
|
||||
private readonly _allowUndefined: boolean
|
||||
|
||||
constructor(options: { noQuestionHintCheck: boolean }) {
|
||||
constructor(allowUndefined: boolean = false) {
|
||||
super(
|
||||
"Checks that a translation is valid and internally consistent",
|
||||
["*"],
|
||||
"CheckTranslation"
|
||||
)
|
||||
this._allowUndefined = allowUndefined
|
||||
}
|
||||
|
||||
convert(json: Translatable, context: ConversionContext): Translatable {
|
||||
if (json === undefined || json === null) {
|
||||
if (!this._allowUndefined) {
|
||||
context.err("Expected a translation, but got " + json)
|
||||
}
|
||||
return json
|
||||
}
|
||||
if (typeof json === "string") {
|
||||
return json
|
||||
}
|
||||
const keys = Object.keys(json)
|
||||
if (keys.length === 0) {
|
||||
context.err("No actual values are given in this translation, it is completely empty")
|
||||
return json
|
||||
}
|
||||
const en = json["en"]
|
||||
if (!en && json["*"] === undefined) {
|
||||
const msg = "Received a translation without english version"
|
||||
context.warn(msg)
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
const lng = json[key]
|
||||
if (lng === "") {
|
||||
context.enter(lng).err("Got an empty string in translation for language " + lng)
|
||||
}
|
||||
|
||||
// TODO validate that all subparts are here
|
||||
}
|
||||
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
||||
constructor() {
|
||||
super("Miscellaneous checks on the tagrendering", ["special"], "MiscTagRenderingChecks")
|
||||
this._options = options
|
||||
}
|
||||
|
||||
convert(
|
||||
|
@ -678,10 +718,42 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
'Detected `special` on the top level. Did you mean `{"render":{ "special": ... }}`'
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
for (const key of ["question", "questionHint", "render"]) {
|
||||
CheckTranslation.allowUndefined.convert(json[key], context.enter(key))
|
||||
}
|
||||
for (let i = 0; i < json.mappings?.length ?? 0; i++) {
|
||||
const mapping = json.mappings[i]
|
||||
CheckTranslation.noUndefined.convert(
|
||||
mapping.then,
|
||||
context.enters("mappings", i, "then")
|
||||
)
|
||||
if (!mapping.if) {
|
||||
context.enters("mappings", i).err("No `if` is defined")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (json["group"]) {
|
||||
context.err('Groups are deprecated, use `"label": ["' + json["group"] + '"]` instead')
|
||||
}
|
||||
|
||||
if (json["question"] && json.freeform?.key === undefined && json.mappings === undefined) {
|
||||
context.err(
|
||||
"A question is defined, but no mappings nor freeform (key) are. Add at least one of them"
|
||||
)
|
||||
}
|
||||
if (json["question"] && !json.freeform && (json.mappings?.length ?? 0) == 1) {
|
||||
context.err("A question is defined, but there is only one option to choose from.")
|
||||
}
|
||||
if (json["questionHint"] && !json["question"]) {
|
||||
context
|
||||
.enter("questionHint")
|
||||
.err(
|
||||
"A questionHint is defined, but no question is given. As such, the questionHint will never be shown"
|
||||
)
|
||||
}
|
||||
|
||||
if (json.freeform) {
|
||||
if (json.render === undefined) {
|
||||
context
|
||||
|
@ -771,16 +843,13 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> {
|
||||
constructor(
|
||||
layerConfig?: LayerConfigJson,
|
||||
doesImageExist?: DoesImageExist,
|
||||
options?: { noQuestionHintCheck: boolean }
|
||||
) {
|
||||
constructor(layerConfig?: LayerConfigJson, doesImageExist?: DoesImageExist) {
|
||||
super(
|
||||
"Various validation on tagRenderingConfigs",
|
||||
new DetectShadowedMappings(layerConfig),
|
||||
|
@ -790,67 +859,46 @@ export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> {
|
|||
new On("question", new ValidatePossibleLinks()),
|
||||
new On("questionHint", new ValidatePossibleLinks()),
|
||||
new On("mappings", new Each(new On("then", new ValidatePossibleLinks()))),
|
||||
new MiscTagRenderingChecks(options)
|
||||
new MiscTagRenderingChecks()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidateLayer extends Conversion<
|
||||
LayerConfigJson,
|
||||
{ parsed: LayerConfig; raw: LayerConfigJson }
|
||||
> {
|
||||
/**
|
||||
* The paths where this layer is originally saved. Triggers some extra checks
|
||||
* @private
|
||||
*/
|
||||
private readonly _path?: string
|
||||
export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
||||
private readonly _isBuiltin: boolean
|
||||
private readonly _doesImageExist: DoesImageExist
|
||||
/**
|
||||
* The paths where this layer is originally saved. Triggers some extra checks
|
||||
*/
|
||||
private readonly _path: string
|
||||
private readonly _studioValidations: boolean
|
||||
private _skipDefaultLayers: boolean
|
||||
|
||||
constructor(
|
||||
path: string,
|
||||
isBuiltin: boolean,
|
||||
doesImageExist: DoesImageExist,
|
||||
studioValidations: boolean = false,
|
||||
skipDefaultLayers: boolean = false
|
||||
) {
|
||||
super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer")
|
||||
constructor(path: string, isBuiltin, doesImageExist, studioValidations) {
|
||||
super("Runs various checks against common mistakes for a layer", [], "PrevalidateLayer")
|
||||
this._path = path
|
||||
this._isBuiltin = isBuiltin
|
||||
this._doesImageExist = doesImageExist
|
||||
this._studioValidations = studioValidations
|
||||
this._skipDefaultLayers = skipDefaultLayers
|
||||
}
|
||||
|
||||
convert(
|
||||
json: LayerConfigJson,
|
||||
context: ConversionContext
|
||||
): { parsed: LayerConfig; raw: LayerConfigJson } {
|
||||
context = context.inOperation(this.name)
|
||||
if (typeof json === "string") {
|
||||
context.err("This layer hasn't been expanded: " + json)
|
||||
return null
|
||||
}
|
||||
|
||||
if (this._skipDefaultLayers && Constants.added_by_default.indexOf(<any>json.id) >= 0) {
|
||||
return { parsed: undefined, raw: json }
|
||||
}
|
||||
|
||||
if (typeof json === "string") {
|
||||
context.err(
|
||||
`Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed`
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
|
||||
if (json.id === undefined) {
|
||||
context.enter("id").err(`Not a valid layer: id is undefined`)
|
||||
} else {
|
||||
if (json.id?.toLowerCase() !== json.id) {
|
||||
context.enter("id").err(`The id of a layer should be lowercase: ${json.id}`)
|
||||
}
|
||||
if (json.id?.match(/[a-z0-9-_]/) == null) {
|
||||
context.enter("id").err(`The id of a layer should match [a-z0-9-_]*: ${json.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (json.source === undefined) {
|
||||
context.enter("source").err("No source section is defined")
|
||||
context
|
||||
.enter("source")
|
||||
.err(
|
||||
"No source section is defined; please define one as data is not loaded otherwise"
|
||||
)
|
||||
} else {
|
||||
if (json.source === "special" || json.source === "special:library") {
|
||||
} else if (json.source && json.source["osmTags"] === undefined) {
|
||||
|
@ -884,13 +932,6 @@ export class ValidateLayer extends Conversion<
|
|||
}
|
||||
}
|
||||
|
||||
if (json.id?.toLowerCase() !== json.id) {
|
||||
context.enter("id").err(`The id of a layer should be lowercase: ${json.id}`)
|
||||
}
|
||||
if (json.id?.match(/[a-z0-9-_]/) == null) {
|
||||
context.enter("id").err(`The id of a layer should match [a-z0-9-_]*: ${json.id}`)
|
||||
}
|
||||
|
||||
if (
|
||||
json.syncSelection !== undefined &&
|
||||
LayerConfig.syncSelectionAllowed.indexOf(json.syncSelection) < 0
|
||||
|
@ -906,27 +947,6 @@ export class ValidateLayer extends Conversion<
|
|||
)
|
||||
}
|
||||
|
||||
let layerConfig: LayerConfig
|
||||
try {
|
||||
layerConfig = new LayerConfig(json, "validation", true)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
context.err("Could not parse layer due to:" + e)
|
||||
return undefined
|
||||
}
|
||||
for (let i = 0; i < (layerConfig.calculatedTags ?? []).length; i++) {
|
||||
const [_, code, __] = layerConfig.calculatedTags[i]
|
||||
try {
|
||||
new Function("feat", "return " + code + ";")
|
||||
} catch (e) {
|
||||
context
|
||||
.enters("calculatedTags", i)
|
||||
.err(
|
||||
`Invalid function definition: the custom javascript is invalid:${e}. The offending javascript code is:\n ${code}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (json.source === "special") {
|
||||
if (!Constants.priviliged_layers.find((x) => x == json.id)) {
|
||||
context.err(
|
||||
|
@ -937,6 +957,10 @@ export class ValidateLayer extends Conversion<
|
|||
}
|
||||
}
|
||||
|
||||
if (context.hasErrors()) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (json.tagRenderings !== undefined && json.tagRenderings.length > 0) {
|
||||
new On("tagRendering", new Each(new ValidateTagRenderings(json)))
|
||||
if (json.title === undefined && json.source !== "special:library") {
|
||||
|
@ -1001,172 +1025,6 @@ export class ValidateLayer extends Conversion<
|
|||
}
|
||||
|
||||
try {
|
||||
if (this._isBuiltin) {
|
||||
// Some checks for legacy elements
|
||||
|
||||
if (json["overpassTags"] !== undefined) {
|
||||
context.err(
|
||||
"Layer " +
|
||||
json.id +
|
||||
'still uses the old \'overpassTags\'-format. Please use "source": {"osmTags": <tags>}\' instead of "overpassTags": <tags> (note: this isn\'t your fault, the custom theme generator still spits out the old format)'
|
||||
)
|
||||
}
|
||||
const forbiddenTopLevel = [
|
||||
"icon",
|
||||
"wayHandling",
|
||||
"roamingRenderings",
|
||||
"roamingRendering",
|
||||
"label",
|
||||
"width",
|
||||
"color",
|
||||
"colour",
|
||||
"iconOverlays",
|
||||
]
|
||||
for (const forbiddenKey of forbiddenTopLevel) {
|
||||
if (json[forbiddenKey] !== undefined)
|
||||
context.err(
|
||||
"Layer " + json.id + " still has a forbidden key " + forbiddenKey
|
||||
)
|
||||
}
|
||||
if (json["hideUnderlayingFeaturesMinPercentage"] !== undefined) {
|
||||
context.err(
|
||||
"Layer " +
|
||||
json.id +
|
||||
" contains an old 'hideUnderlayingFeaturesMinPercentage'"
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
json.isShown !== undefined &&
|
||||
(json.isShown["render"] !== undefined || json.isShown["mappings"] !== undefined)
|
||||
) {
|
||||
context.warn("Has a tagRendering as `isShown`")
|
||||
}
|
||||
}
|
||||
if (this._isBuiltin) {
|
||||
// Check location of layer file
|
||||
const expected: string = `assets/layers/${json.id}/${json.id}.json`
|
||||
if (this._path != undefined && this._path.indexOf(expected) < 0) {
|
||||
context.err(
|
||||
"Layer is in an incorrect place. The path is " +
|
||||
this._path +
|
||||
", but expected " +
|
||||
expected
|
||||
)
|
||||
}
|
||||
}
|
||||
if (this._isBuiltin) {
|
||||
// Check for correct IDs
|
||||
if (json.tagRenderings?.some((tr) => tr["id"] === "")) {
|
||||
const emptyIndexes: number[] = []
|
||||
for (let i = 0; i < json.tagRenderings.length; i++) {
|
||||
const tagRendering = json.tagRenderings[i]
|
||||
if (tagRendering["id"] === "") {
|
||||
emptyIndexes.push(i)
|
||||
}
|
||||
}
|
||||
context
|
||||
.enter(["tagRenderings", ...emptyIndexes])
|
||||
.err(
|
||||
`Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${emptyIndexes.join(
|
||||
","
|
||||
)}])`
|
||||
)
|
||||
}
|
||||
|
||||
const duplicateIds = Utils.Duplicates(
|
||||
(json.tagRenderings ?? [])
|
||||
?.map((f) => f["id"])
|
||||
.filter((id) => id !== "questions")
|
||||
)
|
||||
if (duplicateIds.length > 0 && !Utils.runningFromConsole) {
|
||||
context
|
||||
.enter("tagRenderings")
|
||||
.err(`Some tagRenderings have a duplicate id: ${duplicateIds}`)
|
||||
}
|
||||
|
||||
if (json.description === undefined) {
|
||||
if (typeof json.source === null) {
|
||||
context.err("A priviliged layer must have a description")
|
||||
} else {
|
||||
context.warn("A builtin layer should have a description")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (json.filter) {
|
||||
new On("filter", new Each(new ValidateFilter())).convert(json, context)
|
||||
}
|
||||
|
||||
if (json.tagRenderings !== undefined) {
|
||||
new On(
|
||||
"tagRenderings",
|
||||
new Each(
|
||||
new ValidateTagRenderings(json, this._doesImageExist, {
|
||||
noQuestionHintCheck: json["#"]?.indexOf("no-question-hint-check") >= 0,
|
||||
})
|
||||
)
|
||||
).convert(json, context)
|
||||
}
|
||||
|
||||
if (json.pointRendering !== null && json.pointRendering !== undefined) {
|
||||
if (!Array.isArray(json.pointRendering)) {
|
||||
throw (
|
||||
"pointRendering in " +
|
||||
json.id +
|
||||
" is not iterable, it is: " +
|
||||
typeof json.pointRendering
|
||||
)
|
||||
}
|
||||
for (let i = 0; i < json.pointRendering.length; i++) {
|
||||
const pointRendering = json.pointRendering[i]
|
||||
if (pointRendering.marker === undefined) {
|
||||
continue
|
||||
}
|
||||
for (const icon of pointRendering?.marker) {
|
||||
const indexM = pointRendering?.marker.indexOf(icon)
|
||||
if (!icon.icon) {
|
||||
continue
|
||||
}
|
||||
if (icon.icon["condition"]) {
|
||||
context
|
||||
.enters("pointRendering", i, "marker", indexM, "icon", "condition")
|
||||
.err(
|
||||
"Don't set a condition in a marker as this will result in an invisible but clickable element. Use extra filters in the source instead."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (json.presets !== undefined) {
|
||||
if (typeof json.source === "string") {
|
||||
context.err("A special layer cannot have presets")
|
||||
}
|
||||
// Check that a preset will be picked up by the layer itself
|
||||
const baseTags = TagUtils.Tag(json.source["osmTags"])
|
||||
for (let i = 0; i < json.presets.length; i++) {
|
||||
const preset = json.presets[i]
|
||||
const tags: { k: string; v: string }[] = new And(
|
||||
preset.tags.map((t) => TagUtils.Tag(t))
|
||||
).asChange({ id: "node/-1" })
|
||||
const properties = {}
|
||||
for (const tag of tags) {
|
||||
properties[tag.k] = tag.v
|
||||
}
|
||||
const doMatch = baseTags.matchesProperties(properties)
|
||||
if (!doMatch) {
|
||||
context
|
||||
.enters("presets", i, "tags")
|
||||
.err(
|
||||
"This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n A newly created point will have properties: " +
|
||||
JSON.stringify(properties) +
|
||||
"\n The required tags are: " +
|
||||
baseTags.asHumanString(false, false, {})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
context.err("Could not validate layer due to: " + e + e.stack)
|
||||
}
|
||||
|
@ -1180,6 +1038,232 @@ export class ValidateLayer extends Conversion<
|
|||
}
|
||||
}
|
||||
|
||||
if (this._isBuiltin) {
|
||||
// Some checks for legacy elements
|
||||
|
||||
if (json["overpassTags"] !== undefined) {
|
||||
context.err(
|
||||
"Layer " +
|
||||
json.id +
|
||||
'still uses the old \'overpassTags\'-format. Please use "source": {"osmTags": <tags>}\' instead of "overpassTags": <tags> (note: this isn\'t your fault, the custom theme generator still spits out the old format)'
|
||||
)
|
||||
}
|
||||
const forbiddenTopLevel = [
|
||||
"icon",
|
||||
"wayHandling",
|
||||
"roamingRenderings",
|
||||
"roamingRendering",
|
||||
"label",
|
||||
"width",
|
||||
"color",
|
||||
"colour",
|
||||
"iconOverlays",
|
||||
]
|
||||
for (const forbiddenKey of forbiddenTopLevel) {
|
||||
if (json[forbiddenKey] !== undefined)
|
||||
context.err("Layer " + json.id + " still has a forbidden key " + forbiddenKey)
|
||||
}
|
||||
if (json["hideUnderlayingFeaturesMinPercentage"] !== undefined) {
|
||||
context.err(
|
||||
"Layer " + json.id + " contains an old 'hideUnderlayingFeaturesMinPercentage'"
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
json.isShown !== undefined &&
|
||||
(json.isShown["render"] !== undefined || json.isShown["mappings"] !== undefined)
|
||||
) {
|
||||
context.warn("Has a tagRendering as `isShown`")
|
||||
}
|
||||
}
|
||||
if (this._isBuiltin) {
|
||||
// Check location of layer file
|
||||
const expected: string = `assets/layers/${json.id}/${json.id}.json`
|
||||
if (this._path != undefined && this._path.indexOf(expected) < 0) {
|
||||
context.err(
|
||||
"Layer is in an incorrect place. The path is " +
|
||||
this._path +
|
||||
", but expected " +
|
||||
expected
|
||||
)
|
||||
}
|
||||
}
|
||||
if (this._isBuiltin) {
|
||||
// Check for correct IDs
|
||||
if (json.tagRenderings?.some((tr) => tr["id"] === "")) {
|
||||
const emptyIndexes: number[] = []
|
||||
for (let i = 0; i < json.tagRenderings.length; i++) {
|
||||
const tagRendering = json.tagRenderings[i]
|
||||
if (tagRendering["id"] === "") {
|
||||
emptyIndexes.push(i)
|
||||
}
|
||||
}
|
||||
context
|
||||
.enter(["tagRenderings", ...emptyIndexes])
|
||||
.err(
|
||||
`Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${emptyIndexes.join(
|
||||
","
|
||||
)}])`
|
||||
)
|
||||
}
|
||||
|
||||
const duplicateIds = Utils.Duplicates(
|
||||
(json.tagRenderings ?? [])?.map((f) => f["id"]).filter((id) => id !== "questions")
|
||||
)
|
||||
if (duplicateIds.length > 0 && !Utils.runningFromConsole) {
|
||||
context
|
||||
.enter("tagRenderings")
|
||||
.err(`Some tagRenderings have a duplicate id: ${duplicateIds}`)
|
||||
}
|
||||
|
||||
if (json.description === undefined) {
|
||||
if (typeof json.source === null) {
|
||||
context.err("A priviliged layer must have a description")
|
||||
} else {
|
||||
context.warn("A builtin layer should have a description")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (json.filter) {
|
||||
new On("filter", new Each(new ValidateFilter())).convert(json, context)
|
||||
}
|
||||
|
||||
if (json.tagRenderings !== undefined) {
|
||||
new On(
|
||||
"tagRenderings",
|
||||
new Each(new ValidateTagRenderings(json, this._doesImageExist))
|
||||
).convert(json, context)
|
||||
}
|
||||
|
||||
if (json.pointRendering !== null && json.pointRendering !== undefined) {
|
||||
if (!Array.isArray(json.pointRendering)) {
|
||||
throw (
|
||||
"pointRendering in " +
|
||||
json.id +
|
||||
" is not iterable, it is: " +
|
||||
typeof json.pointRendering
|
||||
)
|
||||
}
|
||||
for (let i = 0; i < json.pointRendering.length; i++) {
|
||||
const pointRendering = json.pointRendering[i]
|
||||
if (pointRendering.marker === undefined) {
|
||||
continue
|
||||
}
|
||||
for (const icon of pointRendering?.marker) {
|
||||
const indexM = pointRendering?.marker.indexOf(icon)
|
||||
if (!icon.icon) {
|
||||
continue
|
||||
}
|
||||
if (icon.icon["condition"]) {
|
||||
context
|
||||
.enters("pointRendering", i, "marker", indexM, "icon", "condition")
|
||||
.err(
|
||||
"Don't set a condition in a marker as this will result in an invisible but clickable element. Use extra filters in the source instead."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (json.presets !== undefined) {
|
||||
if (typeof json.source === "string") {
|
||||
context.err("A special layer cannot have presets")
|
||||
}
|
||||
// Check that a preset will be picked up by the layer itself
|
||||
const baseTags = TagUtils.Tag(json.source["osmTags"])
|
||||
for (let i = 0; i < json.presets.length; i++) {
|
||||
const preset = json.presets[i]
|
||||
const tags: { k: string; v: string }[] = new And(
|
||||
preset.tags.map((t) => TagUtils.Tag(t))
|
||||
).asChange({ id: "node/-1" })
|
||||
const properties = {}
|
||||
for (const tag of tags) {
|
||||
properties[tag.k] = tag.v
|
||||
}
|
||||
const doMatch = baseTags.matchesProperties(properties)
|
||||
if (!doMatch) {
|
||||
context
|
||||
.enters("presets", i, "tags")
|
||||
.err(
|
||||
"This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n A newly created point will have properties: " +
|
||||
JSON.stringify(properties) +
|
||||
"\n The required tags are: " +
|
||||
baseTags.asHumanString(false, false, {})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidateLayer extends Conversion<
|
||||
LayerConfigJson,
|
||||
{ parsed: LayerConfig; raw: LayerConfigJson }
|
||||
> {
|
||||
private readonly _skipDefaultLayers: boolean
|
||||
private readonly _prevalidation: PrevalidateLayer
|
||||
|
||||
constructor(
|
||||
path: string,
|
||||
isBuiltin: boolean,
|
||||
doesImageExist: DoesImageExist,
|
||||
studioValidations: boolean = false,
|
||||
skipDefaultLayers: boolean = false
|
||||
) {
|
||||
super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer")
|
||||
this._prevalidation = new PrevalidateLayer(
|
||||
path,
|
||||
isBuiltin,
|
||||
doesImageExist,
|
||||
studioValidations
|
||||
)
|
||||
this._skipDefaultLayers = skipDefaultLayers
|
||||
}
|
||||
|
||||
convert(
|
||||
json: LayerConfigJson,
|
||||
context: ConversionContext
|
||||
): { parsed: LayerConfig; raw: LayerConfigJson } {
|
||||
context = context.inOperation(this.name)
|
||||
if (typeof json === "string") {
|
||||
context.err(
|
||||
`Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed`
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (this._skipDefaultLayers && Constants.added_by_default.indexOf(<any>json.id) >= 0) {
|
||||
return { parsed: undefined, raw: json }
|
||||
}
|
||||
|
||||
this._prevalidation.convert(json, context.inOperation(this._prevalidation.name))
|
||||
|
||||
if (context.hasErrors()) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
let layerConfig: LayerConfig
|
||||
try {
|
||||
layerConfig = new LayerConfig(json, "validation", true)
|
||||
} catch (e) {
|
||||
context.err("Could not parse layer due to:" + e)
|
||||
return undefined
|
||||
}
|
||||
for (let i = 0; i < (layerConfig.calculatedTags ?? []).length; i++) {
|
||||
const [_, code, __] = layerConfig.calculatedTags[i]
|
||||
try {
|
||||
new Function("feat", "return " + code + ";")
|
||||
} catch (e) {
|
||||
context
|
||||
.enters("calculatedTags", i)
|
||||
.err(
|
||||
`Invalid function definition: the custom javascript is invalid:${e}. The offending javascript code is:\n ${code}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return { raw: json, parsed: layerConfig }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -198,6 +198,8 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
|
|||
freeform?: {
|
||||
/**
|
||||
* question: What is the name of the attribute that should be written to?
|
||||
* This is the OpenStreetMap-key that that value will be written to
|
||||
*
|
||||
* ifunset: do not offer a freeform textfield as answer option
|
||||
*/
|
||||
key: string
|
||||
|
@ -215,7 +217,7 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
|
|||
* A (translated) text that is shown (as gray text) within the textfield
|
||||
* type: translation
|
||||
*/
|
||||
placeholder?: string | any
|
||||
placeholder?: Translatable
|
||||
|
||||
/**
|
||||
* Extra parameters to initialize the input helper arguments.
|
||||
|
@ -259,7 +261,7 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
|
|||
*
|
||||
* ifunset: This tagrendering will be shown if it is known, but cannot be edited by the contributor, effectively resutling in a read-only rendering
|
||||
*/
|
||||
question?: string | Translatable
|
||||
question?: Translatable
|
||||
|
||||
/**
|
||||
* question: Should some extra information be shown to the contributor, alongside the question?
|
||||
|
@ -267,7 +269,7 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
|
|||
* This can give some extra information on what the answer should ook like
|
||||
* ifunset: No extra hint is given
|
||||
*/
|
||||
questionHint?: string | Translatable
|
||||
questionHint?: Translatable
|
||||
|
||||
/**
|
||||
* A list of labels. These are strings that are used for various purposes, e.g. to only include a subset of the tagRenderings when reusing a layer
|
||||
|
|
|
@ -9,7 +9,8 @@ import { Utils } from "../../Utils"
|
|||
import LanguageUtils from "../../Utils/LanguageUtils"
|
||||
|
||||
import { RasterLayerProperties } from "../RasterLayerProperties"
|
||||
import { ConversionContext } from "./Conversion/Conversion"
|
||||
|
||||
import { ConversionContext } from "./Conversion/ConversionContext"
|
||||
|
||||
/**
|
||||
* Minimal information about a theme
|
||||
|
|
|
@ -240,10 +240,6 @@ export default class TagRenderingConfig {
|
|||
)
|
||||
}
|
||||
|
||||
if (this.question && this.freeform?.key === undefined && this.mappings === undefined) {
|
||||
throw `${context}: A question is defined, but no mappings nor freeform (key) are. The question is ${this.question.txt} at ${context}`
|
||||
}
|
||||
|
||||
if (!json.multiAnswer && this.mappings !== undefined && this.question !== undefined) {
|
||||
let keys = []
|
||||
for (let i = 0; i < this.mappings.length; i++) {
|
||||
|
@ -315,7 +311,7 @@ export default class TagRenderingConfig {
|
|||
) {
|
||||
const ctx = `${translationKey}.mappings.${i}`
|
||||
if (mapping.if === undefined) {
|
||||
throw `${ctx}: Invalid mapping: "if" is not defined in ${JSON.stringify(mapping)}`
|
||||
throw `Invalid mapping: "if" is not defined`
|
||||
}
|
||||
if (mapping.then === undefined) {
|
||||
if (mapping["render"] !== undefined) {
|
||||
|
|
|
@ -22,11 +22,14 @@
|
|||
|
||||
export let highlightedRendering: UIEventSource<string> = undefined;
|
||||
export let showQuestionIfUnknown: boolean = false;
|
||||
let editMode = false;
|
||||
/**
|
||||
* Indicates if this tagRendering currently shows the attribute or asks the question to _change_ the property
|
||||
*/
|
||||
export let editMode = !config.IsKnown(tags) || showQuestionIfUnknown;
|
||||
if (tags) {
|
||||
onDestroy(
|
||||
tags.addCallbackAndRunD((tags) => {
|
||||
editMode = showQuestionIfUnknown && !config.IsKnown(tags);
|
||||
tags.addCallbackD((tags) => {
|
||||
editMode = !config.IsKnown(tags)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -132,6 +132,7 @@
|
|||
|
||||
function onSave() {
|
||||
if (selectedTags === undefined) {
|
||||
console.log("SelectedTags is undefined, ignoring 'onSave'-event")
|
||||
return;
|
||||
}
|
||||
if (layer === undefined || layer?.source === null) {
|
||||
|
@ -197,20 +198,20 @@
|
|||
</span>
|
||||
<slot name="upper-right" />
|
||||
</div>
|
||||
|
||||
{#if config.questionhint}
|
||||
<div>
|
||||
<SpecialTranslation
|
||||
t={config.questionhint}
|
||||
{tags}
|
||||
{state}
|
||||
{layer}
|
||||
feature={selectedElement}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if config.questionhint}
|
||||
<div class="max-h-60 overflow-y-auto">
|
||||
<SpecialTranslation
|
||||
t={config.questionhint}
|
||||
{tags}
|
||||
{state}
|
||||
{layer}
|
||||
feature={selectedElement}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if config.mappings?.length >= 8}
|
||||
<div class="sticky flex w-full">
|
||||
<img src="./assets/svg/search.svg" class="h-6 w-6" />
|
||||
|
|
|
@ -1,20 +1,10 @@
|
|||
<script lang="ts">
|
||||
import Marker from "../Map/Marker.svelte";
|
||||
import NextButton from "../Base/NextButton.svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { AllSharedLayers } from "../../Customizations/AllSharedLayers";
|
||||
import { AllKnownLayouts, AllKnownLayoutsLazy } from "../../Customizations/AllKnownLayouts";
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection";
|
||||
import EditItemButton from "./EditItemButton.svelte";
|
||||
|
||||
export let layerIds: { id: string }[];
|
||||
export let layerIds: { id: string, owner: number }[];
|
||||
export let category: "layers" | "themes" = "layers";
|
||||
const dispatch = createEventDispatcher<{ layerSelected: string }>();
|
||||
|
||||
function fetchIconDescription(layerId): any {
|
||||
if(category === "themes"){
|
||||
return AllKnownLayouts.allKnownLayouts.get(layerId).icon
|
||||
}
|
||||
return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon;
|
||||
}
|
||||
export let osmConnection: OsmConnection;
|
||||
|
||||
</script>
|
||||
|
||||
|
@ -22,12 +12,7 @@
|
|||
<slot name="title" />
|
||||
<div class="flex flex-wrap">
|
||||
{#each Array.from(layerIds) as layer}
|
||||
<NextButton clss="small" on:click={() => dispatch("layerSelected", layer)}>
|
||||
<div class="w-4 h-4 mr-1">
|
||||
<Marker icons={fetchIconDescription(layer.id)} />
|
||||
</div>
|
||||
{layer.id}
|
||||
</NextButton>
|
||||
<EditItemButton info={layer} {category} {osmConnection} on:layerSelected/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
36
src/UI/Studio/EditItemButton.svelte
Normal file
36
src/UI/Studio/EditItemButton.svelte
Normal file
|
@ -0,0 +1,36 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection";
|
||||
import Marker from "../Map/Marker.svelte";
|
||||
import NextButton from "../Base/NextButton.svelte";
|
||||
import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts";
|
||||
import { AllSharedLayers } from "../../Customizations/AllSharedLayers";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let info: { id: string, owner: number };
|
||||
export let category: "layers" | "themes";
|
||||
export let osmConnection: OsmConnection;
|
||||
|
||||
let displayName = UIEventSource.FromPromise(osmConnection.getInformationAboutUser(info.owner)).mapD(response => response.display_name);
|
||||
|
||||
let selfId = osmConnection.userDetails.mapD(ud => ud.uid)
|
||||
function fetchIconDescription(layerId): any {
|
||||
if (category === "themes") {
|
||||
return AllKnownLayouts.allKnownLayouts.get(layerId).icon;
|
||||
}
|
||||
return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon;
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher<{ layerSelected: string }>();
|
||||
|
||||
</script>
|
||||
|
||||
<NextButton clss="small" on:click={() => dispatch("layerSelected", info)}>
|
||||
<div class="w-4 h-4 mr-1">
|
||||
<Marker icons={fetchIconDescription(info.id)} />
|
||||
</div>
|
||||
<b class="px-1"> {info.id}</b>
|
||||
{#if info.owner && info.owner !== $selfId}
|
||||
(made by {$displayName ?? info.owner})
|
||||
{/if}
|
||||
</NextButton>
|
|
@ -21,8 +21,8 @@
|
|||
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw;
|
||||
|
||||
export let state: EditLayerState;
|
||||
const messages = state.messages;
|
||||
const hasErrors = messages.map((m: ConversionMessage[]) => m.filter(m => m.level === "error").length);
|
||||
let messages = state.messages;
|
||||
let hasErrors = messages.mapD((m: ConversionMessage[]) => m.filter(m => m.level === "error").length);
|
||||
const configuration = state.configuration;
|
||||
|
||||
const allNames = Utils.Dedup(layerSchema.map(meta => meta.hints.group));
|
||||
|
@ -33,7 +33,7 @@
|
|||
}
|
||||
|
||||
|
||||
const title: Store<string> = state.getStoreFor(["id"]);
|
||||
let title: Store<string> = state.getStoreFor(["id"]);
|
||||
const wl = window.location;
|
||||
const baseUrl = wl.protocol + "//" + wl.host + "/theme.html?userlayout=";
|
||||
|
||||
|
@ -53,13 +53,15 @@
|
|||
let config = layerSchema.find(config => config.path.length === 1 && config.path[0] === id);
|
||||
config = Utils.Clone(config);
|
||||
config.required = true;
|
||||
console.log(">>>", config);
|
||||
config.hints.ifunset = undefined;
|
||||
return config;
|
||||
}
|
||||
|
||||
let requiredFields = ["id", "name", "description"];
|
||||
let currentlyMissing = state.configuration.map(config => {
|
||||
if(!config){
|
||||
return []
|
||||
}
|
||||
const missing = [];
|
||||
for (const requiredField of requiredFields) {
|
||||
if (!config[requiredField]) {
|
||||
|
@ -160,7 +162,9 @@
|
|||
</div>
|
||||
{#if $highlightedItem !== undefined}
|
||||
<FloatOver on:close={() => highlightedItem.setData(undefined)}>
|
||||
<TagRenderingInput path={$highlightedItem.path} {state} schema={$highlightedItem.schema} />
|
||||
<div class="mt-16">
|
||||
<TagRenderingInput path={$highlightedItem.path} {state} schema={$highlightedItem.schema} />
|
||||
</div>
|
||||
</FloatOver>
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
|||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
import {
|
||||
Conversion,
|
||||
ConversionContext,
|
||||
ConversionMessage,
|
||||
DesugaringContext,
|
||||
Pipe,
|
||||
|
@ -21,6 +20,7 @@ import { Feature, Point } from "geojson"
|
|||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { LayoutConfigJson } from "../../Models/ThemeConfig/Json/LayoutConfigJson"
|
||||
import { PrepareTheme } from "../../Models/ThemeConfig/Conversion/PrepareTheme"
|
||||
import { ConversionContext } from "../../Models/ThemeConfig/Conversion/ConversionContext";
|
||||
|
||||
export interface HighlightedTagRendering {
|
||||
path: ReadonlyArray<string | number>
|
||||
|
@ -41,7 +41,9 @@ export abstract class EditJsonState<T> {
|
|||
public readonly highlightedItem: UIEventSource<HighlightedTagRendering> = new UIEventSource(
|
||||
undefined
|
||||
)
|
||||
sendingUpdates = false
|
||||
private readonly _stores = new Map<string, UIEventSource<any>>()
|
||||
private boolean
|
||||
|
||||
constructor(schema: ConfigMeta[], server: StudioServer, category: "layers" | "themes") {
|
||||
this.schema = schema
|
||||
|
@ -52,7 +54,13 @@ export abstract class EditJsonState<T> {
|
|||
|
||||
const layerId = this.getId()
|
||||
this.configuration
|
||||
.mapD((config) => JSON.stringify(config, null, " "))
|
||||
.mapD((config) => {
|
||||
if (!this.sendingUpdates) {
|
||||
console.log("Not sending updates yet! Trigger 'startSendingUpdates' first")
|
||||
return undefined
|
||||
}
|
||||
return JSON.stringify(config, null, " ")
|
||||
})
|
||||
.stabilized(100)
|
||||
.addCallbackD(async (config) => {
|
||||
const id = layerId.data
|
||||
|
@ -60,10 +68,17 @@ export abstract class EditJsonState<T> {
|
|||
console.warn("No id found in layer, not updating")
|
||||
return
|
||||
}
|
||||
await server.update(id, config, category)
|
||||
await this.server.update(id, config, this.category)
|
||||
})
|
||||
}
|
||||
|
||||
public startSavingUpdates(enabled = true) {
|
||||
this.sendingUpdates = enabled
|
||||
if (enabled) {
|
||||
this.configuration.ping()
|
||||
}
|
||||
}
|
||||
|
||||
public getCurrentValueFor(path: ReadonlyArray<string | number>): any | undefined {
|
||||
// Walk the path down to see if we find something
|
||||
let entry = this.configuration.data
|
||||
|
@ -96,7 +111,7 @@ export abstract class EditJsonState<T> {
|
|||
public register(
|
||||
path: ReadonlyArray<string | number>,
|
||||
value: Store<any>,
|
||||
noInitialSync: boolean = false
|
||||
noInitialSync: boolean = true
|
||||
): () => void {
|
||||
const unsync = value.addCallback((v) => {
|
||||
this.setValueAt(path, v)
|
||||
|
@ -260,6 +275,18 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
|
|||
}
|
||||
|
||||
this.addMissingTagRenderingIds()
|
||||
|
||||
this.configuration.addCallbackAndRunD((layer) => {
|
||||
if (layer.tagRenderings) {
|
||||
// A bit of cleanup
|
||||
const lBefore = layer.tagRenderings.length
|
||||
const cleaned = Utils.NoNull(layer.tagRenderings)
|
||||
if (cleaned.length != lBefore) {
|
||||
layer.tagRenderings = cleaned
|
||||
this.configuration.ping()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
protected buildValidation(state: DesugaringContext) {
|
||||
|
@ -300,6 +327,10 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
|
|||
}
|
||||
|
||||
export class EditThemeState extends EditJsonState<LayoutConfigJson> {
|
||||
constructor(schema: ConfigMeta[], server: StudioServer) {
|
||||
super(schema, server, "themes")
|
||||
}
|
||||
|
||||
protected buildValidation(state: DesugaringContext): Conversion<LayoutConfigJson, any> {
|
||||
return new Pipe(
|
||||
new PrepareTheme(state),
|
||||
|
@ -307,10 +338,6 @@ export class EditThemeState extends EditJsonState<LayoutConfigJson> {
|
|||
)
|
||||
}
|
||||
|
||||
constructor(schema: ConfigMeta[], server: StudioServer) {
|
||||
super(schema, server, "themes")
|
||||
}
|
||||
|
||||
protected getId(): Store<string> {
|
||||
return this.configuration.mapD((config) => config.id)
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
export let state: EditThemeState;
|
||||
let schema: ConfigMeta[] = state.schema.filter(schema => schema.path.length > 0);
|
||||
let config = state.configuration;
|
||||
const messages = state.messages;
|
||||
const hasErrors = messages.map((m: ConversionMessage[]) => m.filter(m => m.level === "error").length);
|
||||
let messages = state.messages;
|
||||
let hasErrors = messages.map((m: ConversionMessage[]) => m.filter(m => m.level === "error").length);
|
||||
let title = state.getStoreFor(["id"]);
|
||||
const wl = window.location;
|
||||
const baseUrl = wl.protocol + "//" + wl.host + "/theme.html?userlayout=";
|
||||
|
|
|
@ -5,12 +5,13 @@
|
|||
import { ImmutableStore, Store } from "../../Logic/UIEventSource";
|
||||
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte";
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
|
||||
import * as nmd from "nano-markdown";
|
||||
import nmd from "nano-markdown";
|
||||
import type {
|
||||
QuestionableTagRenderingConfigJson
|
||||
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.js";
|
||||
import type { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson";
|
||||
import FromHtml from "../Base/FromHtml.svelte";
|
||||
import { Utils } from "../../Utils";
|
||||
|
||||
export let state: EditLayerState;
|
||||
export let path: ReadonlyArray<string | number>;
|
||||
|
@ -34,9 +35,15 @@
|
|||
return [x];
|
||||
}
|
||||
});
|
||||
let configs: Store<TagRenderingConfig[]> = configJson.mapD(configs => configs.map(config => new TagRenderingConfig(config)));
|
||||
let configs: Store<TagRenderingConfig[]> =configJson.mapD(configs => Utils.NoNull( configs.map(config => {
|
||||
try{
|
||||
return new TagRenderingConfig(config);
|
||||
}catch (e) {
|
||||
return undefined
|
||||
}
|
||||
})));
|
||||
let id: Store<string> = value.mapD(c => {
|
||||
if (c.id) {
|
||||
if (c?.id) {
|
||||
return c.id;
|
||||
}
|
||||
if (typeof c === "string") {
|
||||
|
@ -49,6 +56,14 @@
|
|||
|
||||
let messages = state.messagesFor(path);
|
||||
|
||||
let description = schema.description
|
||||
if(description){
|
||||
try{
|
||||
description = nmd(description)
|
||||
}catch (e) {
|
||||
console.error("Could not convert description to markdown", {description})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex">
|
||||
|
@ -63,8 +78,8 @@
|
|||
{schema.hints.question}
|
||||
{/if}
|
||||
</button>
|
||||
{#if schema.description}
|
||||
<FromHtml src={nmd(schema.description)} />
|
||||
{#if description}
|
||||
<FromHtml src={description} />
|
||||
{/if}
|
||||
{#each $messages as message}
|
||||
<div class="alert">
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
export let state: EditLayerState;
|
||||
export let path: (string | number)[] = [];
|
||||
export let schema: ConfigMeta;
|
||||
export let startInEditModeIfUnset: boolean = false
|
||||
let value = new UIEventSource<string | any>(undefined);
|
||||
|
||||
const isTranslation = schema.hints.typehint === "translation" || schema.hints.typehint === "rendered" || ConfigMetaUtils.isTranslation(schema);
|
||||
|
@ -118,6 +119,7 @@
|
|||
}
|
||||
let startValue = state.getCurrentValueFor(path);
|
||||
const tags = new UIEventSource<Record<string, string>>({ value: startValue });
|
||||
let startInEditMode = !startValue && startInEditModeIfUnset
|
||||
try {
|
||||
onDestroy(state.register(path, tags.map(tgs => {
|
||||
const v = tgs["value"];
|
||||
|
@ -157,7 +159,7 @@
|
|||
<span class="alert">{err}</span>
|
||||
{:else}
|
||||
<div class="w-full flex flex-col">
|
||||
<TagRenderingEditable {config} selectedElement={undefined} showQuestionIfUnknown={true} {state} {tags} />
|
||||
<TagRenderingEditable editMode={startInEditMode} {config} selectedElement={undefined} showQuestionIfUnknown={true} {state} {tags} />
|
||||
{#if $messages.length > 0}
|
||||
{#each $messages as msg}
|
||||
<div class="alert">{msg.message}</div>
|
||||
|
|
|
@ -149,7 +149,7 @@
|
|||
}
|
||||
return tags["value"] === "true";
|
||||
});
|
||||
onDestroy(state.register(path, directValue, true));
|
||||
onDestroy(state.register(path, directValue));
|
||||
}
|
||||
|
||||
let subSchemas: ConfigMeta[] = [];
|
||||
|
|
|
@ -72,6 +72,7 @@ const configBuiltin = new TagRenderingConfig(<QuestionableTagRenderingConfigJson
|
|||
|
||||
|
||||
const tags = new UIEventSource({ value });
|
||||
|
||||
const store = state.getStoreFor(path);
|
||||
tags.addCallbackAndRunD(tgs => {
|
||||
store.setData(tgs["value"]);
|
||||
|
@ -112,7 +113,7 @@ const missing: string[] = questionableTagRenderingSchemaRaw.filter(schema => sch
|
|||
<slot name="upper-right" />
|
||||
</div>
|
||||
{#if $allowQuestions}
|
||||
<SchemaBasedField {state} path={[...path,"question"]} schema={topLevelItems["question"]} />
|
||||
<SchemaBasedField startInEditModeIfUnset={true} {state} path={[...path,"question"]} schema={topLevelItems["question"]} />
|
||||
<SchemaBasedField {state} path={[...path,"questionHint"]} schema={topLevelItems["questionHint"]} />
|
||||
{/if}
|
||||
{#each ($mappings ?? []) as mapping, i (mapping)}
|
||||
|
|
|
@ -24,8 +24,9 @@
|
|||
import { QuestionMarkCircleIcon } from "@babeard/svelte-heroicons/mini";
|
||||
import type { ConfigMeta } from "./Studio/configMeta";
|
||||
import EditTheme from "./Studio/EditTheme.svelte";
|
||||
|
||||
export let studioUrl = window.location.hostname === "127.0.0.1" ? "http://127.0.0.1:1235" : "https://studio.mapcomplete.org";
|
||||
import * as meta from "../../package.json"
|
||||
|
||||
export let studioUrl = window.location.hostname === "127.0.0.2" ? "http://127.0.0.1:1235" : "https://studio.mapcomplete.org";
|
||||
|
||||
let osmConnection = new OsmConnection(new OsmConnection({
|
||||
oauth_token: QueryParameters.GetQueryParameter(
|
||||
|
@ -61,18 +62,22 @@
|
|||
let layerId = editLayerState.configuration.map(layerConfig => layerConfig.id);
|
||||
|
||||
let showIntro = UIEventSource.asBoolean(LocalStorageSource.Get("studio-show-intro", "true"));
|
||||
|
||||
const version = meta.version
|
||||
async function editLayer(event: Event) {
|
||||
const layerId: {owner: number, id: string} = event.detail;
|
||||
state = "loading";
|
||||
editLayerState.startSavingUpdates(false)
|
||||
editLayerState.configuration.setData(await studio.fetch(layerId.id, "layers", layerId.owner));
|
||||
editLayerState.startSavingUpdates()
|
||||
state = "editing_layer";
|
||||
}
|
||||
|
||||
async function editTheme(event: Event) {
|
||||
const id : {id: string, owner: number} = event.detail;
|
||||
state = "loading";
|
||||
editThemeState.startSavingUpdates(false)
|
||||
editThemeState.configuration.setData(await studio.fetch(id.id, "themes", id.owner));
|
||||
editThemeState.startSavingUpdates()
|
||||
state = "editing_theme";
|
||||
}
|
||||
|
||||
|
@ -153,6 +158,7 @@
|
|||
Show the introduction again
|
||||
</NextButton>
|
||||
</div>
|
||||
<span class="subtle">MapComplete version {version}</span>
|
||||
</div>
|
||||
{:else if state === "edit_layer"}
|
||||
|
||||
|
@ -160,14 +166,14 @@
|
|||
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio
|
||||
</BackButton>
|
||||
<h2>Choose a layer to edit</h2>
|
||||
<ChooseLayerToEdit layerIds={$selfLayers} on:layerSelected={editLayer}>
|
||||
<ChooseLayerToEdit {osmConnection} layerIds={$selfLayers} on:layerSelected={editLayer}>
|
||||
<h3 slot="title">Your layers</h3>
|
||||
</ChooseLayerToEdit>
|
||||
<h3>Layers by other contributors</h3>
|
||||
<ChooseLayerToEdit layerIds={$otherLayers} on:layerSelected={editLayer} />
|
||||
<ChooseLayerToEdit {osmConnection} layerIds={$otherLayers} on:layerSelected={editLayer} />
|
||||
|
||||
<h3>Official layers by MapComplete</h3>
|
||||
<ChooseLayerToEdit layerIds={$officialLayers} on:layerSelected={editLayer} />
|
||||
<ChooseLayerToEdit {osmConnection} layerIds={$officialLayers} on:layerSelected={editLayer} />
|
||||
</div>
|
||||
{:else if state === "edit_theme"}
|
||||
|
||||
|
@ -175,13 +181,13 @@
|
|||
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio
|
||||
</BackButton>
|
||||
<h2>Choose a theme to edit</h2>
|
||||
<ChooseLayerToEdit layerIds={$selfThemes} on:layerSelected={editTheme}>
|
||||
<ChooseLayerToEdit {osmConnection} layerIds={$selfThemes} on:layerSelected={editTheme}>
|
||||
<h3 slot="title">Your themes</h3>
|
||||
</ChooseLayerToEdit>
|
||||
<h3>Themes by other contributors</h3>
|
||||
<ChooseLayerToEdit layerIds={$otherThemes} on:layerSelected={editTheme} />
|
||||
<ChooseLayerToEdit {osmConnection} layerIds={$otherThemes} on:layerSelected={editTheme} />
|
||||
<h3>Official themes by MapComplete</h3>
|
||||
<ChooseLayerToEdit layerIds={$officialThemes} on:layerSelected={editTheme} />
|
||||
<ChooseLayerToEdit {osmConnection} layerIds={$officialThemes} on:layerSelected={editTheme} />
|
||||
|
||||
</div>
|
||||
{:else if state === "loading"}
|
||||
|
|
|
@ -59,7 +59,6 @@ export class Translation extends BaseUIElement {
|
|||
"Constructing a translation, but the object containing translations is empty " +
|
||||
(context ?? "No context given")
|
||||
)
|
||||
throw `Constructing a translation, but the object containing translations is empty (${context})`
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9816,556 +9816,7 @@
|
|||
"required": false,
|
||||
"hints": {
|
||||
"question": "Should the created point be snapped to a line layer?",
|
||||
"suggestions": [
|
||||
{
|
||||
"if": "value=address",
|
||||
"then": "address - Addresses"
|
||||
},
|
||||
{
|
||||
"if": "value=advertising",
|
||||
"then": "advertising - We will complete data from advertising features with reference, operator and lit"
|
||||
},
|
||||
{
|
||||
"if": "value=ambulancestation",
|
||||
"then": "ambulancestation - An ambulance station is an area for storage of ambulance vehicles, medical equipment, personal protective equipment, and other medical supplies."
|
||||
},
|
||||
{
|
||||
"if": "value=animal_shelter",
|
||||
"then": "animal_shelter - An animal shelter is a facility where animals in trouble are brought and facility's staff (volunteers or not) feeds them and cares of them, rehabilitating and healing them if necessary. This definition includes kennels for abandoned dogs, catteries for abandoned cats, shelters for other abandoned pets and wildlife recovery centres. "
|
||||
},
|
||||
{
|
||||
"if": "value=artwork",
|
||||
"then": "artwork - An open map of statues, busts, graffitis and other artwork all over the world"
|
||||
},
|
||||
{
|
||||
"if": "value=atm",
|
||||
"then": "atm - ATMs to withdraw money"
|
||||
},
|
||||
{
|
||||
"if": "value=bank",
|
||||
"then": "bank - A financial institution to deposit money"
|
||||
},
|
||||
{
|
||||
"if": "value=barrier",
|
||||
"then": "barrier - Obstacles while cycling, such as bollards and cycle barriers"
|
||||
},
|
||||
{
|
||||
"if": "value=bench",
|
||||
"then": "bench - A bench is a wooden, metal, stone, … surface where a human can sit. This layers visualises them and asks a few questions about them."
|
||||
},
|
||||
{
|
||||
"if": "value=bench_at_pt",
|
||||
"then": "bench_at_pt - A layer showing all public-transport-stops which do have a bench"
|
||||
},
|
||||
{
|
||||
"if": "value=bicycle_library",
|
||||
"then": "bicycle_library - A facility where bicycles can be lent for longer period of times"
|
||||
},
|
||||
{
|
||||
"if": "value=bicycle_rental",
|
||||
"then": "bicycle_rental - Bicycle rental stations"
|
||||
},
|
||||
{
|
||||
"if": "value=bicycle_tube_vending_machine",
|
||||
"then": "bicycle_tube_vending_machine - A layer showing vending machines for bicycle tubes (either purpose-built bicycle tube vending machines or classical vending machines with bicycle tubes and optionally additional bicycle related objects such as lights, gloves, locks, …)"
|
||||
},
|
||||
{
|
||||
"if": "value=bike_cafe",
|
||||
"then": "bike_cafe - A bike café is a café geared towards cyclists, for example with services such as a pump, with lots of bicycle-related decoration, …"
|
||||
},
|
||||
{
|
||||
"if": "value=bike_cleaning",
|
||||
"then": "bike_cleaning - A layer showing facilities where one can clean their bike"
|
||||
},
|
||||
{
|
||||
"if": "value=bike_parking",
|
||||
"then": "bike_parking - A layer showing where you can park your bike"
|
||||
},
|
||||
{
|
||||
"if": "value=bike_repair_station",
|
||||
"then": "bike_repair_station - A layer showing bicycle pumps and bicycle repair tool stands"
|
||||
},
|
||||
{
|
||||
"if": "value=bike_shop",
|
||||
"then": "bike_shop - A shop specifically selling bicycles or related items"
|
||||
},
|
||||
{
|
||||
"if": "value=bike_themed_object",
|
||||
"then": "bike_themed_object - A layer with bike-themed objects but who don't match any other layer"
|
||||
},
|
||||
{
|
||||
"if": "value=binocular",
|
||||
"then": "binocular - Binoculars"
|
||||
},
|
||||
{
|
||||
"if": "value=birdhide",
|
||||
"then": "birdhide - A birdhide"
|
||||
},
|
||||
{
|
||||
"if": "value=cafe_pub",
|
||||
"then": "cafe_pub - A layer showing cafés and pubs where one can gather around a drink. The layer asks for some relevant questions"
|
||||
},
|
||||
{
|
||||
"if": "value=car_rental",
|
||||
"then": "car_rental - Places where you can rent a car"
|
||||
},
|
||||
{
|
||||
"if": "value=charging_station",
|
||||
"then": "charging_station - A charging station"
|
||||
},
|
||||
{
|
||||
"if": "value=climbing",
|
||||
"then": "climbing - A dummy layer which contains tagrenderings, shared among the climbing layers"
|
||||
},
|
||||
{
|
||||
"if": "value=climbing_area",
|
||||
"then": "climbing_area - An area where climbing is possible, e.g. a crag, site, boulder, … Contains aggregation of routes"
|
||||
},
|
||||
{
|
||||
"if": "value=climbing_club",
|
||||
"then": "climbing_club - A climbing club or organisation"
|
||||
},
|
||||
{
|
||||
"if": "value=climbing_gym",
|
||||
"then": "climbing_gym - A climbing gym"
|
||||
},
|
||||
{
|
||||
"if": "value=climbing_opportunity",
|
||||
"then": "climbing_opportunity - Fallback layer with items on which climbing _might_ be possible. It is loaded when zoomed in a lot, to prevent duplicate items to be added"
|
||||
},
|
||||
{
|
||||
"if": "value=climbing_route",
|
||||
"then": "climbing_route - A single climbing route and its properties. Some properties are derived from the containing features"
|
||||
},
|
||||
{
|
||||
"if": "value=clock",
|
||||
"then": "clock - Layer with public clocks"
|
||||
},
|
||||
{
|
||||
"if": "value=conflation",
|
||||
"then": "conflation - If the import-button moves OSM points, the imported way points or conflates, a preview is shown. This layer defines how this preview is rendered. This layer cannot be included in a theme."
|
||||
},
|
||||
{
|
||||
"if": "value=crab_address",
|
||||
"then": "crab_address - Address data for Flanders by the governement, suited for import into OpenStreetMap. Datadump from 2021-10-26. This layer contains only visualisation logic. Import buttons should be added via an override. Note that HNRLABEL contains the original value, whereas _HNRLABEL contains a slightly cleaned version"
|
||||
},
|
||||
{
|
||||
"if": "value=crossings",
|
||||
"then": "crossings - Crossings for pedestrians and cyclists"
|
||||
},
|
||||
{
|
||||
"if": "value=current_view",
|
||||
"then": "current_view - A meta-layer which contains one single feature, namely the bounding box of the current map view. This can be used to trigger special actions. If a popup is defined for this layer, this popup will be accessible via an extra button on screen.\n\nThe icon on the button is the default icon of the layer, but can be customized by detecting 'button=yes'."
|
||||
},
|
||||
{
|
||||
"if": "value=cycleways_and_roads",
|
||||
"then": "cycleways_and_roads - All infrastructure that someone can cycle over, accompanied with questions about this infrastructure"
|
||||
},
|
||||
{
|
||||
"if": "value=defibrillator",
|
||||
"then": "defibrillator - A layer showing defibrillators which can be used in case of emergency. This contains public defibrillators, but also defibrillators which might need staff to fetch the actual device"
|
||||
},
|
||||
{
|
||||
"if": "value=dentist",
|
||||
"then": "dentist - This layer shows dentist offices"
|
||||
},
|
||||
{
|
||||
"if": "value=direction",
|
||||
"then": "direction - This layer visualizes directions"
|
||||
},
|
||||
{
|
||||
"if": "value=doctors",
|
||||
"then": "doctors - This layer shows doctor offices"
|
||||
},
|
||||
{
|
||||
"if": "value=dogpark",
|
||||
"then": "dogpark - A layer showing dogparks, which are areas where dog are allowed to run without a leash"
|
||||
},
|
||||
{
|
||||
"if": "value=drinking_water",
|
||||
"then": "drinking_water - A layer showing drinking water fountains"
|
||||
},
|
||||
{
|
||||
"if": "value=elevator",
|
||||
"then": "elevator - This layer show elevators and asks for operational status and elevator dimensions. Useful for wheelchair accessibility information"
|
||||
},
|
||||
{
|
||||
"if": "value=elongated_coin",
|
||||
"then": "elongated_coin - Layer showing penny presses."
|
||||
},
|
||||
{
|
||||
"if": "value=entrance",
|
||||
"then": "entrance - A layer showing entrances and offering capabilities to survey some advanced data which is important for e.g. wheelchair users (but also bicycle users, people who want to deliver, …)"
|
||||
},
|
||||
{
|
||||
"if": "value=etymology",
|
||||
"then": "etymology - All objects which have an etymology known"
|
||||
},
|
||||
{
|
||||
"if": "value=extinguisher",
|
||||
"then": "extinguisher - Map layer to show fire extinguishers."
|
||||
},
|
||||
{
|
||||
"if": "value=filters",
|
||||
"then": "filters - This layer acts as library for common filters"
|
||||
},
|
||||
{
|
||||
"if": "value=fire_station",
|
||||
"then": "fire_station - Map layer to show fire stations."
|
||||
},
|
||||
{
|
||||
"if": "value=fitness_centre",
|
||||
"then": "fitness_centre - Layer showing fitness centres"
|
||||
},
|
||||
{
|
||||
"if": "value=fitness_station",
|
||||
"then": "fitness_station - Find a fitness station near you, and add missing ones."
|
||||
},
|
||||
{
|
||||
"if": "value=fixme",
|
||||
"then": "fixme - OSM objects that likely need to be fixed, based on a FIXME tag."
|
||||
},
|
||||
{
|
||||
"if": "value=food",
|
||||
"then": "food - A layer showing restaurants and fast-food amenities (with a special rendering for friteries)"
|
||||
},
|
||||
{
|
||||
"if": "value=ghost_bike",
|
||||
"then": "ghost_bike - A layer showing memorials for cyclists, killed in road accidents"
|
||||
},
|
||||
{
|
||||
"if": "value=governments",
|
||||
"then": "governments - This layer show governmental buildings. It was setup as commissioned layer for the client of OSOC '22"
|
||||
},
|
||||
{
|
||||
"if": "value=gps_location",
|
||||
"then": "gps_location - Meta layer showing the current location of the user. Add this to your theme and override the icon to change the appearance of the current location. The object will always have `id=gps` and will have _all_ the properties included in the [`Coordinates`-object](https://developer.mozilla.org/en-US/docs/Web/API/GeolocationCoordinates) (except latitude and longitude) returned by the browser, such as `speed`, `altitude`, `heading`, ...."
|
||||
},
|
||||
{
|
||||
"if": "value=gps_location_history",
|
||||
"then": "gps_location_history - Meta layer which contains the previous locations of the user as single points. This is mainly for technical reasons, e.g. to keep match the distance to the modified object"
|
||||
},
|
||||
{
|
||||
"if": "value=gps_track",
|
||||
"then": "gps_track - Meta layer showing the previous locations of the user as single line with controls, e.g. to erase, upload or download this track. Add this to your theme and override the maprendering to change the appearance of the travelled track."
|
||||
},
|
||||
{
|
||||
"if": "value=guidepost",
|
||||
"then": "guidepost - Guideposts (also known as fingerposts or finger posts) are often found along official hiking/cycling/riding/skiing routes to indicate the directions to different destinations"
|
||||
},
|
||||
{
|
||||
"if": "value=hackerspace",
|
||||
"then": "hackerspace - Hackerspace"
|
||||
},
|
||||
{
|
||||
"if": "value=home_location",
|
||||
"then": "home_location - Meta layer showing the home location of the user. The home location can be set in the [profile settings](https://www.openstreetmap.org/profile/edit) of OpenStreetMap."
|
||||
},
|
||||
{
|
||||
"if": "value=hospital",
|
||||
"then": "hospital - A layer showing hospital grounds"
|
||||
},
|
||||
{
|
||||
"if": "value=hotel",
|
||||
"then": "hotel - Layer showing all hotels"
|
||||
},
|
||||
{
|
||||
"if": "value=hydrant",
|
||||
"then": "hydrant - Map layer to show fire hydrants."
|
||||
},
|
||||
{
|
||||
"if": "value=ice_cream",
|
||||
"then": "ice_cream - A place where ice cream is sold over the counter"
|
||||
},
|
||||
{
|
||||
"if": "value=icons",
|
||||
"then": "icons - A layer acting as library for icon-tagrenderings, especially to show as badge next to a POI"
|
||||
},
|
||||
{
|
||||
"if": "value=id_presets",
|
||||
"then": "id_presets - Layer containing various presets and questions generated by ID. These are meant to be reused in other layers by importing the tagRenderings with `id_preset.<tagrendering>"
|
||||
},
|
||||
{
|
||||
"if": "value=import_candidate",
|
||||
"then": "import_candidate - Layer used as template in the importHelper"
|
||||
},
|
||||
{
|
||||
"if": "value=indoors",
|
||||
"then": "indoors - Basic indoor mapping: shows room outlines"
|
||||
},
|
||||
{
|
||||
"if": "value=information_board",
|
||||
"then": "information_board - A layer showing touristical, road side information boards (e.g. giving information about the landscape, a building, a feature, a map, …)"
|
||||
},
|
||||
{
|
||||
"if": "value=kerbs",
|
||||
"then": "kerbs - A layer showing kerbs."
|
||||
},
|
||||
{
|
||||
"if": "value=kindergarten_childcare",
|
||||
"then": "kindergarten_childcare - Shows kindergartens and preschools. Both are grouped in one layer, as they are regularly confused with each other"
|
||||
},
|
||||
{
|
||||
"if": "value=last_click",
|
||||
"then": "last_click - This layer defines how to render the 'last click'-location. By default, it will show a marker with the possibility to add a new point (if there are some presets) and/or to add a new note (if the 'note' layer attribute is set). If none are possible, this layer won't show up"
|
||||
},
|
||||
{
|
||||
"if": "value=map",
|
||||
"then": "map - A map, meant for tourists which is permanently installed in the public space"
|
||||
},
|
||||
{
|
||||
"if": "value=maproulette",
|
||||
"then": "maproulette - Layer showing all tasks in MapRoulette"
|
||||
},
|
||||
{
|
||||
"if": "value=maproulette_challenge",
|
||||
"then": "maproulette_challenge - Layer showing tasks of a single MapRoulette challenge. This layer is intended to be reused and extended in themes; refer to [the documentation](https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Integrating_Maproulette.md) on how to do this."
|
||||
},
|
||||
{
|
||||
"if": "value=maxspeed",
|
||||
"then": "maxspeed - Shows the allowed speed for every road"
|
||||
},
|
||||
{
|
||||
"if": "value=memorial",
|
||||
"then": "memorial - Layer showing memorial plaques, based upon a unofficial theme. Can be expanded to have multiple types of memorials later on"
|
||||
},
|
||||
{
|
||||
"if": "value=named_streets",
|
||||
"then": "named_streets - Hidden layer with all streets which have a name. Useful to detect addresses"
|
||||
},
|
||||
{
|
||||
"if": "value=nature_reserve",
|
||||
"then": "nature_reserve - A nature reserve is an area where nature can take its course"
|
||||
},
|
||||
{
|
||||
"if": "value=note",
|
||||
"then": "note - This layer shows notes on OpenStreetMap. Having this layer in your theme will trigger the 'add new note' functionality in the 'addNewPoint'-popup (or if your theme has no presets, it'll enable adding notes)"
|
||||
},
|
||||
{
|
||||
"if": "value=observation_tower",
|
||||
"then": "observation_tower - Towers with a panoramic view"
|
||||
},
|
||||
{
|
||||
"if": "value=osm_community_index",
|
||||
"then": "osm_community_index - A layer showing the OpenStreetMap Communities"
|
||||
},
|
||||
{
|
||||
"if": "value=parcel_lockers",
|
||||
"then": "parcel_lockers - Layer showing parcel lockers for collecting and sending parcels."
|
||||
},
|
||||
{
|
||||
"if": "value=parking",
|
||||
"then": "parking - A layer showing car parkings"
|
||||
},
|
||||
{
|
||||
"if": "value=parking_spaces",
|
||||
"then": "parking_spaces - Layer showing individual parking spaces."
|
||||
},
|
||||
{
|
||||
"if": "value=parking_ticket_machine",
|
||||
"then": "parking_ticket_machine - Layer with parking ticket machines to pay for parking."
|
||||
},
|
||||
{
|
||||
"if": "value=pedestrian_path",
|
||||
"then": "pedestrian_path - Pedestrian footpaths, especially used for indoor navigation and snapping entrances to this layer"
|
||||
},
|
||||
{
|
||||
"if": "value=pharmacy",
|
||||
"then": "pharmacy - A layer showing pharmacies, which (probably) dispense prescription drugs"
|
||||
},
|
||||
{
|
||||
"if": "value=physiotherapist",
|
||||
"then": "physiotherapist - This layer shows physiotherapists"
|
||||
},
|
||||
{
|
||||
"if": "value=picnic_table",
|
||||
"then": "picnic_table - The layer showing picnic tables"
|
||||
},
|
||||
{
|
||||
"if": "value=play_forest",
|
||||
"then": "play_forest - Een speelbos is een vrij toegankelijke zone in een bos"
|
||||
},
|
||||
{
|
||||
"if": "value=playground",
|
||||
"then": "playground - Playgrounds"
|
||||
},
|
||||
{
|
||||
"if": "value=postboxes",
|
||||
"then": "postboxes - The layer showing postboxes."
|
||||
},
|
||||
{
|
||||
"if": "value=postoffices",
|
||||
"then": "postoffices - A layer showing post offices."
|
||||
},
|
||||
{
|
||||
"if": "value=public_bookcase",
|
||||
"then": "public_bookcase - A streetside cabinet with books, accessible to anyone"
|
||||
},
|
||||
{
|
||||
"if": "value=questions",
|
||||
"then": "questions - Special library layer which does not need a '.questions'-prefix before being imported"
|
||||
},
|
||||
{
|
||||
"if": "value=railway_platforms",
|
||||
"then": "railway_platforms - Find every platform in the station, and the train routes that use them."
|
||||
},
|
||||
{
|
||||
"if": "value=rainbow_crossings",
|
||||
"then": "rainbow_crossings - A layer showing pedestrian crossings with rainbow paintings"
|
||||
},
|
||||
{
|
||||
"if": "value=range",
|
||||
"then": "range - Meta-layer, simply showing a bbox in red"
|
||||
},
|
||||
{
|
||||
"if": "value=reception_desk",
|
||||
"then": "reception_desk - A layer showing where the reception desks are and which asks some accessibility information"
|
||||
},
|
||||
{
|
||||
"if": "value=recycling",
|
||||
"then": "recycling - A layer with recycling containers and centres"
|
||||
},
|
||||
{
|
||||
"if": "value=route_marker",
|
||||
"then": "route_marker - Route markers are small markers often found along official hiking/cycling/riding/skiing routes to indicate the direction of the route."
|
||||
},
|
||||
{
|
||||
"if": "value=school",
|
||||
"then": "school - Schools giving primary and secondary education and post-secondary, non-tertiary education. Note that this level of education does not imply an age of the pupiles"
|
||||
},
|
||||
{
|
||||
"if": "value=selected_element",
|
||||
"then": "selected_element - Highlights the currently selected element. Override this layer to have different colors"
|
||||
},
|
||||
{
|
||||
"if": "value=shelter",
|
||||
"then": "shelter - Layer showing shelter structures"
|
||||
},
|
||||
{
|
||||
"if": "value=shops",
|
||||
"then": "shops - A shop"
|
||||
},
|
||||
{
|
||||
"if": "value=shower",
|
||||
"then": "shower - A layer showing (public) showers"
|
||||
},
|
||||
{
|
||||
"if": "value=slow_roads",
|
||||
"then": "slow_roads - All carfree roads"
|
||||
},
|
||||
{
|
||||
"if": "value=speed_camera",
|
||||
"then": "speed_camera - Layer showing speed cameras"
|
||||
},
|
||||
{
|
||||
"if": "value=speed_display",
|
||||
"then": "speed_display - Layer showing speed displays that alert drivers of their speed."
|
||||
},
|
||||
{
|
||||
"if": "value=split_point",
|
||||
"then": "split_point - Layer rendering the little scissors for the minimap in the 'splitRoadWizard'"
|
||||
},
|
||||
{
|
||||
"if": "value=split_road",
|
||||
"then": "split_road - Layer rendering the way to split in the 'splitRoadWizard'. This one is used instead of the variable rendering by the themes themselves, as they might not always be very visible"
|
||||
},
|
||||
{
|
||||
"if": "value=sport_pitch",
|
||||
"then": "sport_pitch - A sport pitch"
|
||||
},
|
||||
{
|
||||
"if": "value=sports_centre",
|
||||
"then": "sports_centre - Indoor and outdoor sports centres can be found on this layer"
|
||||
},
|
||||
{
|
||||
"if": "value=stairs",
|
||||
"then": "stairs - Layer showing stairs and escalators"
|
||||
},
|
||||
{
|
||||
"if": "value=street_lamps",
|
||||
"then": "street_lamps - A layer showing street lights"
|
||||
},
|
||||
{
|
||||
"if": "value=surveillance_camera",
|
||||
"then": "surveillance_camera - This layer shows surveillance cameras and allows a contributor to update information and add new cameras"
|
||||
},
|
||||
{
|
||||
"if": "value=tertiary_education",
|
||||
"then": "tertiary_education - Layer with all tertiary education institutes (ISCED:2011 levels 6,7 and 8)"
|
||||
},
|
||||
{
|
||||
"if": "value=ticket_machine",
|
||||
"then": "ticket_machine - Find ticket machines for public transport tickets"
|
||||
},
|
||||
{
|
||||
"if": "value=ticket_validator",
|
||||
"then": "ticket_validator - Find ticket validators to validate public transport tickets"
|
||||
},
|
||||
{
|
||||
"if": "value=toilet",
|
||||
"then": "toilet - A layer showing (public) toilets"
|
||||
},
|
||||
{
|
||||
"if": "value=toilet_at_amenity",
|
||||
"then": "toilet_at_amenity - A layer showing (public) toilets located at different places."
|
||||
},
|
||||
{
|
||||
"if": "value=trail",
|
||||
"then": "trail - Aangeduide wandeltochten"
|
||||
},
|
||||
{
|
||||
"if": "value=transit_routes",
|
||||
"then": "transit_routes - Layer showing bus lines"
|
||||
},
|
||||
{
|
||||
"if": "value=transit_stops",
|
||||
"then": "transit_stops - Layer showing different types of transit stops."
|
||||
},
|
||||
{
|
||||
"if": "value=tree_node",
|
||||
"then": "tree_node - A layer showing trees"
|
||||
},
|
||||
{
|
||||
"if": "value=usersettings",
|
||||
"then": "usersettings - A special layer which is not meant to be shown on a map, but which is used to set user settings"
|
||||
},
|
||||
{
|
||||
"if": "value=vending_machine",
|
||||
"then": "vending_machine - Layer showing vending machines"
|
||||
},
|
||||
{
|
||||
"if": "value=veterinary",
|
||||
"then": "veterinary - A layer showing veterinarians"
|
||||
},
|
||||
{
|
||||
"if": "value=viewpoint",
|
||||
"then": "viewpoint - A nice viewpoint or nice view. Ideal to add an image if no other category fits"
|
||||
},
|
||||
{
|
||||
"if": "value=village_green",
|
||||
"then": "village_green - A layer showing village-green (which are communal green areas, but not quite parks)"
|
||||
},
|
||||
{
|
||||
"if": "value=visitor_information_centre",
|
||||
"then": "visitor_information_centre - A visitor center offers information about a specific attraction or place of interest where it is located."
|
||||
},
|
||||
{
|
||||
"if": "value=walls_and_buildings",
|
||||
"then": "walls_and_buildings - Special builtin layer providing all walls and buildings. This layer is useful in presets for objects which can be placed against walls (e.g. AEDs, postboxes, entrances, addresses, surveillance cameras, …). This layer is invisible by default and not toggleable by the user."
|
||||
},
|
||||
{
|
||||
"if": "value=waste_basket",
|
||||
"then": "waste_basket - This is a public waste basket, thrash can, where you can throw away your thrash."
|
||||
},
|
||||
{
|
||||
"if": "value=waste_disposal",
|
||||
"then": "waste_disposal - Waste Disposal Bin, medium to large bin for disposal of (household) waste"
|
||||
},
|
||||
{
|
||||
"if": "value=windturbine",
|
||||
"then": "windturbine - Modern windmills generating electricity"
|
||||
}
|
||||
]
|
||||
"suggestions": []
|
||||
},
|
||||
"type": "array",
|
||||
"description": "If specified, these layers will be shown in the precise location picker and the new point will be snapped towards it.\nFor example, this can be used to snap against `walls_and_buildings` (e.g. to attach a defibrillator, an entrance, an artwork, ... to the wall)\nor to snap an obstacle (such as a bollard) to the `cycleways_and_roads`."
|
||||
|
@ -10418,7 +9869,7 @@
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
|
||||
"description": "question: What is the name of the attribute that should be written to?\nThis is the OpenStreetMap-key that that value will be written to\n\nifunset: do not offer a freeform textfield as answer option",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
|
@ -10426,7 +9877,15 @@
|
|||
"type": "string"
|
||||
},
|
||||
"placeholder": {
|
||||
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
|
||||
"description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Record<string,string>"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"helperArgs": {
|
||||
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
|
||||
|
@ -11150,7 +10609,7 @@
|
|||
"ifunset": "do not offer a freeform textfield as answer option"
|
||||
},
|
||||
"type": "string",
|
||||
"description": ""
|
||||
"description": "This is the OpenStreetMap-key that that value will be written to"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
|
@ -11255,6 +10714,14 @@
|
|||
"typehint": "translation",
|
||||
"question": "What placeholder text should be shown in the input-element if there is no input?"
|
||||
},
|
||||
"type": [
|
||||
{
|
||||
"$ref": "#/definitions/Record<string,string>"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "A (translated) text that is shown (as gray text) within the textfield"
|
||||
},
|
||||
{
|
||||
|
@ -12277,7 +11744,7 @@
|
|||
"ifunset": "do not offer a freeform textfield as answer option"
|
||||
},
|
||||
"type": "string",
|
||||
"description": ""
|
||||
"description": "This is the OpenStreetMap-key that that value will be written to"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
|
@ -12384,6 +11851,14 @@
|
|||
"typehint": "translation",
|
||||
"question": "What placeholder text should be shown in the input-element if there is no input?"
|
||||
},
|
||||
"type": [
|
||||
{
|
||||
"$ref": "#/definitions/Record<string,string>"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "A (translated) text that is shown (as gray text) within the textfield"
|
||||
},
|
||||
{
|
||||
|
@ -13437,7 +12912,7 @@
|
|||
"ifunset": "do not offer a freeform textfield as answer option"
|
||||
},
|
||||
"type": "string",
|
||||
"description": ""
|
||||
"description": "This is the OpenStreetMap-key that that value will be written to"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
|
@ -13544,6 +13019,14 @@
|
|||
"typehint": "translation",
|
||||
"question": "What placeholder text should be shown in the input-element if there is no input?"
|
||||
},
|
||||
"type": [
|
||||
{
|
||||
"$ref": "#/definitions/Record<string,string>"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "A (translated) text that is shown (as gray text) within the textfield"
|
||||
},
|
||||
{
|
||||
|
@ -14609,7 +14092,7 @@
|
|||
"ifunset": "do not offer a freeform textfield as answer option"
|
||||
},
|
||||
"type": "string",
|
||||
"description": ""
|
||||
"description": "This is the OpenStreetMap-key that that value will be written to"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
|
@ -14718,6 +14201,14 @@
|
|||
"typehint": "translation",
|
||||
"question": "What placeholder text should be shown in the input-element if there is no input?"
|
||||
},
|
||||
"type": [
|
||||
{
|
||||
"$ref": "#/definitions/Record<string,string>"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "A (translated) text that is shown (as gray text) within the textfield"
|
||||
},
|
||||
{
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -477,7 +477,7 @@
|
|||
"ifunset": "do not offer a freeform textfield as answer option"
|
||||
},
|
||||
"type": "string",
|
||||
"description": ""
|
||||
"description": "This is the OpenStreetMap-key that that value will be written to"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
|
@ -580,6 +580,14 @@
|
|||
"typehint": "translation",
|
||||
"question": "What placeholder text should be shown in the input-element if there is no input?"
|
||||
},
|
||||
"type": [
|
||||
{
|
||||
"$ref": "#/definitions/Record<string,string>"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "A (translated) text that is shown (as gray text) within the textfield"
|
||||
},
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue