Merge branch 'develop' into feature/nsi

This commit is contained in:
Robin van der Linde 2024-04-29 00:24:51 +02:00
commit 572d85a6b5
Signed by untrusted user: Robin-van-der-Linde
GPG key ID: 53956B3252478F0D
375 changed files with 34341 additions and 44682 deletions

View file

@ -228,6 +228,10 @@ export default class MetaTagging {
})
return feats
}
if(!state.perLayer.get(layerId)){
// This layer is not loaded
return []
}
return [state.perLayer.get(layerId).GetFeaturesWithin(bbox)]
},
}

View file

@ -33,6 +33,12 @@ export class UpdateLegacyLayer extends DesugaringStep<
delete config["overpassTags"]
}
if(config.allowMove?.["enableImproveAccuraccy"]){
// Fix common misspelling: 'accuracy' is often typo'ed as 'accuraCCy'
config.allowMove["enableImproveAccuracy"] = config.allowMove["enableImproveAccuraccy"]
delete config.allowMove["enableImproveAccuraccy"]
}
for (const preset of config.presets ?? []) {
const preciseInput = preset["preciseInput"]
if (typeof preciseInput === "boolean") {

View file

@ -230,7 +230,7 @@ class ExpandTagRendering extends Conversion<
}
for (let foundTr of indirect) {
foundTr = Utils.Clone<any>(foundTr)
ctx.Merge(tagRenderingConfigJson["override"] ?? {}, foundTr)
ctx.MergeObjectsForOverride(tagRenderingConfigJson["override"] ?? {}, foundTr)
foundTr["id"] = tagRenderingConfigJson["id"] ?? foundTr["id"]
result.push(foundTr)
}

View file

@ -475,7 +475,13 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
)
}
}
layers.unshift(...dependencies.map((l) => l.config))
/**
* Must be added to the _end_ of the layer list:
* - Imagine that 'walls_and_buildings' is added...
* - but there is a layer about a specific type of building already
* Adding it up front would cause 'walls_and_buildings' to be triggered
*/
layers.push(...dependencies.map((l) => l.config))
return {
...theme,

View file

@ -1647,6 +1647,10 @@ export class ValidateLayer extends Conversion<
}
}
if(json.allowMove?.["enableAccuraccy"] !== undefined){
context.enters("allowMove", "enableAccuracy").err("`enableAccuracy` is written with two C in the first occurrence and only one in the last")
}
return { raw: json, parsed: layerConfig }
}
}

View file

@ -291,17 +291,24 @@ export interface LayerConfigJson {
forceLoad?: false | boolean
/**
* Presets for this layer.
* A preset shows up when clicking the map on a without data (or when right-clicking/long-pressing);
* it will prompt the user to add a new point.
* <div class='flex'>
* <div>
* Presets for this layer.
*
* The most important aspect are the tags, which define which tags the new point will have;
* The title is shown in the dialog, along with the first sentence of the description.
* A preset consists of one or more attributes (tags), a title and optionally a description and optionally example images.
*
* Upon confirmation, the full description is shown beneath the buttons - perfect to add pictures and examples.
* When the contributor wishes to add a point to OpenStreetMap, they'll:
*
* Note: the icon of the preset is determined automatically based on the tags and the icon above. Don't worry about that!
* NB: if no presets are defined, the popup to add new points doesn't show up at all
* 1. Press the 'add new point'-button
* 2. Choose a preset from the list of all presets
* 3. Confirm the choice. In this step, the `description` (if set) and `exampleImages` (if given) will be shown
* 4. Confirm the location
* 5. A new point will be created with the attributes that were defined in the preset
*
* If no presets are defined, the button which invites to add a new preset will not be shown.
*</div>
* <video controls autoplay muted src='./Docs/Screenshots/AddNewItemScreencast.webm' class='w-64'/>
*</div>
*
* group: presets
*/

View file

@ -157,17 +157,16 @@ export interface LayoutConfigJson {
* types: hidden | layer | hidden
* group: layers
* suggestions: return Array.from(layers.keys()).map(key => ({if: "value="+key, then: "<b>"+key+"</b> (builtin) - "+layers.get(key).description}))
* Every layer contains a description of which feature to display - the overpassTags which are queried.
* Instead of running one query for every layer, the query is fused.
*
* Afterwards, every layer is given the list of features.
* Every layer takes away the features that match with them*, and give the leftovers to the next layers.
* A theme must contain at least one layer.
*
* This implies that the _order_ of the layers is important in the case of features with the same tags;
* as the later layers might never receive their feature.
* A layer contains all features of a single type, for example "shops", "bicycle pumps", "benches".
* Note that every layer contains a specification of attributes that it should match. MapComplete will fetch the relevant data from either overpass, the OSM-API or the cache server.
* If a feature can match multiple layers, the first matching layer in the list will be used.
* This implies that the _order_ of the layers is important.
*
* *layers can also remove 'leftover'-features if the leftovers overlap with a feature in the layer itself
*
* <div class='hidden-in-studio'>
* Note that builtin layers can be reused. Either put in the name of the layer to reuse, or use {builtin: "layername", override: ...}
*
* The 'override'-object will be copied over the original values of the layer, which allows to change certain aspects of the layer
@ -194,6 +193,7 @@ export interface LayoutConfigJson {
* "override": {"minzoom": 12}
* }
*```
* </div>
*/
layers: (
| LayerConfigJson
@ -362,7 +362,7 @@ export interface LayoutConfigJson {
/**
* question: Should the 'download as CSV'- and 'download as Geojson'-buttons be enabled?
* iftrue: Enable the option to download the map as CSV and GeoJson
* iffalse: Enable the option to download the map as CSV and GeoJson
* iffalse: Disable the option to download the map as CSV and GeoJson
* ifunset: MapComplete default: Enable the option to download the map as CSV and GeoJson
* group: feature_switches
*/

View file

@ -368,7 +368,6 @@ export default class LayerConfig extends WithContextLoader {
canBeIncluded = true
): BaseUIElement {
const extraProps: (string | BaseUIElement)[] = []
extraProps.push("This layer is shown at zoomlevel **" + this.minzoom + "** and higher")
if (canBeIncluded) {
@ -424,7 +423,7 @@ export default class LayerConfig extends WithContextLoader {
if (!addedByDefault) {
if (usedInThemes?.length > 0) {
usingLayer = [
new Title("Themes using this layer", 4),
new Title("Themes using this layer", 2),
new List(
(usedInThemes ?? []).map(
(id) => new Link(id, "https://mapcomplete.org/" + id)

View file

@ -12,6 +12,8 @@ import { VariableUiElement } from "../../UI/Base/VariableUIElement"
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
import SvelteUIElement from "../../UI/Base/SvelteUIElement"
import DynamicMarker from "../../UI/Map/DynamicMarker.svelte"
import { UIElement } from "../../UI/UIElement"
import Img from "../../UI/Base/Img"
export class IconConfig extends WithContextLoader {
public static readonly defaultIcon = new IconConfig({ icon: "pin", color: "#ff9939" })
@ -121,9 +123,14 @@ export default class PointRenderingConfig extends WithContextLoader {
context + ".rotationAlignment"
)
}
private static FromHtmlMulti(multiSpec: string, tags: Store<Record<string, string>>) {
private static FromHtmlMulti(multiSpec: string, tags: Store<Record<string, string>>): BaseUIElement {
const icons: IconConfig[] = []
for (const subspec of multiSpec.split(";")) {
if(subspec.startsWith("http://") || subspec.startsWith("https://")){
icons.push(new IconConfig({icon: subspec}))
continue
}
const [icon, color] = subspec.split(":")
icons.push(new IconConfig({ icon, color }))
}

View file

@ -52,6 +52,7 @@ export default class TagRenderingConfig {
public readonly renderIconClass?: string
public readonly question?: TypedTranslation<object>
public readonly questionhint?: TypedTranslation<object>
public readonly questionHintIsMd?: boolean
public readonly condition?: TagsFilter
public readonly invalidValues?: TagsFilter
/**
@ -80,7 +81,7 @@ export default class TagRenderingConfig {
public readonly classes: string[] | undefined
constructor(
config: string | TagRenderingConfigJson | QuestionableTagRenderingConfigJson,
config: string | TagRenderingConfigJson | (QuestionableTagRenderingConfigJson & {questionHintIsMd?: boolean}),
context?: string
) {
let json = <string | QuestionableTagRenderingConfigJson>config
@ -136,6 +137,7 @@ export default class TagRenderingConfig {
this.render = Translations.T(<any>json.render, translationKey + ".render")
this.question = Translations.T(json.question, translationKey + ".question")
this.questionhint = Translations.T(json.questionHint, translationKey + ".questionHint")
this.questionHintIsMd = json["questionHintIsMd"] ?? false
this.description = Translations.T(json.description, translationKey + ".description")
this.editButtonAriaLabel = Translations.T(
json.editButtonAriaLabel,

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"
import { marked } from "marked"
export let src: string
export let srcWritable: UIEventSource<string> = undefined
srcWritable?.addCallbackAndRunD(t => {
src = t
})
if(src !== undefined && typeof src !== "string") {
console.trace("Got a non-string object in Markdown", src)
throw "Markdown.svelte got a non-string object"
}
</script>
{#if src?.length > 0}
{@html marked.parse(src)}
{/if}

View file

@ -1,73 +1,21 @@
import Combine from "./Combine"
import BaseUIElement from "../BaseUIElement"
import { Translation } from "../i18n/Translation"
import { FixedUiElement } from "./FixedUiElement"
import Title from "./Title"
import List from "./List"
import Link from "./Link"
import { marked } from "marked"
import { parse as parse_html } from "node-html-parser"
import {default as turndown} from "turndown"
import { Utils } from "../../Utils"
export default class TableOfContents extends Combine {
private readonly titles: Title[]
export default class TableOfContents {
constructor(
elements: Combine | Title[],
options?: {
noTopLevel: false | boolean
maxDepth?: number
}
) {
let titles: Title[]
if (elements instanceof Combine) {
titles = TableOfContents.getTitles(elements.getElements()) ?? []
} else {
titles = elements ?? []
}
let els: { level: number; content: BaseUIElement }[] = []
for (const title of titles) {
let content: BaseUIElement
if (title.title instanceof Translation) {
content = title.title.Clone()
} else if (title.title instanceof FixedUiElement) {
content = new FixedUiElement(title.title.content)
} else if (Utils.runningFromConsole) {
content = new FixedUiElement(title.AsMarkdown())
} else if (title["title"] !== undefined) {
content = new FixedUiElement(title.title.ConstructElement().textContent)
} else {
console.log("Not generating a title for ", title)
continue
}
const vis = new Link(content, "#" + title.id)
els.push({ level: title.level, content: vis })
}
const minLevel = Math.min(...els.map((e) => e.level))
if (options?.noTopLevel) {
els = els.filter((e) => e.level !== minLevel)
}
if (options?.maxDepth) {
els = els.filter((e) => e.level <= options.maxDepth + minLevel)
}
super(TableOfContents.mergeLevel(els).map((el) => el.SetClass("mt-2")))
this.SetClass("flex flex-col")
this.titles = titles
}
private static getTitles(elements: BaseUIElement[]): Title[] {
const titles = []
for (const uiElement of elements) {
if (uiElement instanceof Combine) {
titles.push(...TableOfContents.getTitles(uiElement.getElements()))
} else if (uiElement instanceof Title) {
titles.push(uiElement)
}
}
return titles
private static asLinkableId(text: string): string {
return text
?.replace(/ /g, "-")
?.replace(/[?#.;:/]/, "")
?.toLowerCase() ?? ""
}
private static mergeLevel(
@ -88,7 +36,7 @@ export default class TableOfContents extends Combine {
if (running.length !== undefined) {
result.push({
content: new List(running),
level: maxLevel - 1,
level: maxLevel - 1
})
running = []
}
@ -97,24 +45,81 @@ export default class TableOfContents extends Combine {
if (running.length !== undefined) {
result.push({
content: new List(running),
level: maxLevel - 1,
level: maxLevel - 1
})
}
return TableOfContents.mergeLevel(result)
}
AsMarkdown(): string {
const depthIcons = ["1.", " -", " +", " *"]
const lines = ["## Table of contents\n"]
const minLevel = Math.min(...this.titles.map((t) => t.level))
for (const title of this.titles) {
const prefix = depthIcons[title.level - minLevel] ?? " ~"
const text = title.title.AsMarkdown().replace("\n", "")
const link = title.id
lines.push(prefix + " [" + text + "](#" + link + ")")
public static insertTocIntoMd(md: string): string {
const htmlSource = <string>marked.parse(md)
const el = parse_html(htmlSource)
const structure = TableOfContents.generateStructure(<any>el)
let firstTitle = structure[1]
let minDepth = undefined
do {
minDepth = Math.min(...structure.map(s => s.depth))
const minDepthCount = structure.filter(s => s.depth === minDepth)
if (minDepthCount.length > 1) {
break
}
// Erase a single top level heading
structure.splice(structure.findIndex(s => s.depth === minDepth), 1)
} while (structure.length > 0)
if (structure.length <= 1) {
return md
}
const separators = {
1: " -",
2: " +",
3: " *"
}
return lines.join("\n") + "\n\n"
let toc = ""
let topLevelCount = 0
for (const el of structure) {
const depthDiff = el.depth - minDepth
let link = `[${el.title}](#${TableOfContents.asLinkableId(el.title)})`
if (depthDiff === 0) {
topLevelCount++
toc += `${topLevelCount}. ${link}\n`
} else if (depthDiff <= 3) {
toc += `${separators[depthDiff]} ${link}\n`
}
}
const heading = Utils.Times(() => "#", firstTitle.depth)
toc = heading +" Table of contents\n\n"+toc
const original = el.outerHTML
const firstTitleIndex = original.indexOf(firstTitle.el.outerHTML)
const tocHtml = (<string>marked.parse(toc))
const withToc = original.substring(0, firstTitleIndex) + tocHtml + original.substring(firstTitleIndex)
const htmlToMd = new turndown()
return htmlToMd.turndown(withToc)
}
public static generateStructure(html: Element): { depth: number, title: string, el: Element }[] {
if (html === undefined) {
return []
}
return [].concat(...Array.from(html.childNodes ?? []).map(
child => {
const tag: string = child["tagName"]?.toLowerCase()
if (!tag) {
return []
}
if (tag.match(/h[0-9]/)) {
const depth = Number(tag.substring(1))
return [{ depth, title: child.textContent, el: child }]
}
return TableOfContents.generateStructure(<Element>child)
}
))
}
}

View file

@ -119,7 +119,7 @@
<Tr t={t.conflicting.intro} />
{/if}
{#if $different.length > 0}
{#each $different as key}
{#each $different as key (key)}
<div class="mx-2 rounded-2xl">
<ComparisonAction
{key}
@ -136,7 +136,7 @@
{#if $missing.length > 0}
{#if currentStep === "init"}
{#each $missing as key}
{#each $missing as key (key)}
<div class:glowing-shadow={applyAllHovered} class="mx-2 rounded-2xl">
<ComparisonAction
{key}
@ -174,7 +174,7 @@
{#if readonly}
<div class="w-full overflow-x-auto">
<div class="flex h-32 w-max gap-x-2">
{#each $unknownImages as image}
{#each $unknownImages as image (image)}
<AttributedImage
imgClass="h-32 w-max shrink-0"
image={{ url: image }}
@ -184,7 +184,7 @@
</div>
</div>
{:else}
{#each $unknownImages as image}
{#each $unknownImages as image (image)}
<LinkableImage
{tags}
{state}

View file

@ -1,46 +0,0 @@
import { InputElement } from "./InputElement"
import { UIEventSource } from "../../Logic/UIEventSource"
import Translations from "../i18n/Translations"
import BaseUIElement from "../BaseUIElement"
/**
* @deprecated
*/
export class FixedInputElement<T> extends InputElement<T> {
private readonly value: UIEventSource<T>
private readonly _comparator: (t0: T, t1: T) => boolean
private readonly _el: HTMLElement
constructor(
rendering: BaseUIElement | string,
value: T | UIEventSource<T>,
comparator: (t0: T, t1: T) => boolean = undefined
) {
super()
this._comparator = comparator ?? ((t0, t1) => t0 == t1)
if (value instanceof UIEventSource) {
this.value = value
} else {
this.value = new UIEventSource<T>(value)
}
this._el = document.createElement("span")
const e = Translations.W(rendering)?.ConstructElement()
if (e) {
this._el.appendChild(e)
}
}
GetValue(): UIEventSource<T> {
return this.value
}
IsValid(t: T): boolean {
return this._comparator(t, this.value.data)
}
protected InnerConstructElement(): HTMLElement {
return this._el
}
}

View file

@ -1,161 +0,0 @@
import { InputElement } from "./InputElement"
import { UIEventSource } from "../../Logic/UIEventSource"
import { Utils } from "../../Utils"
/**
* @deprecated
*/
export class RadioButton<T> extends InputElement<T> {
private static _nextId = 0
private readonly value: UIEventSource<T>
private _elements: InputElement<T>[]
private _selectFirstAsDefault: boolean
private _dontStyle: boolean
constructor(
elements: InputElement<T>[],
options?: {
selectFirstAsDefault?: true | boolean
dontStyle?: boolean
value?: UIEventSource<T>
}
) {
super()
options = options ?? {}
this._selectFirstAsDefault = options.selectFirstAsDefault ?? true
this._elements = Utils.NoNull(elements)
this.value = options?.value ?? new UIEventSource<T>(undefined)
this._dontStyle = options.dontStyle ?? false
}
IsValid(t: T): boolean {
for (const inputElement of this._elements) {
if (inputElement.IsValid(t)) {
return true
}
}
return false
}
GetValue(): UIEventSource<T> {
return this.value
}
protected InnerConstructElement(): HTMLElement {
const elements = this._elements
const selectFirstAsDefault = this._selectFirstAsDefault
const selectedElementIndex: UIEventSource<number> = new UIEventSource<number>(null)
const value = UIEventSource.flatten(
selectedElementIndex.map((selectedIndex) => {
if (selectedIndex !== undefined && selectedIndex !== null) {
return elements[selectedIndex].GetValue()
}
}),
elements.map((e) => e?.GetValue())
)
value.syncWith(this.value)
if (selectFirstAsDefault) {
value.addCallbackAndRun((selected) => {
if (selected === undefined) {
for (const element of elements) {
const v = element.GetValue().data
if (v !== undefined) {
value.setData(v)
break
}
}
}
})
}
for (let i = 0; i < elements.length; i++) {
// If an element is clicked, the radio button corresponding with it should be selected as well
elements[i]?.onClick(() => {
selectedElementIndex.setData(i)
})
elements[i].GetValue().addCallback(() => {
selectedElementIndex.setData(i)
})
}
const groupId = "radiogroup" + RadioButton._nextId
RadioButton._nextId++
const form = document.createElement("form")
const inputs = []
const wrappers: HTMLElement[] = []
for (let i1 = 0; i1 < elements.length; i1++) {
let element = elements[i1]
const labelHtml = element.ConstructElement()
if (labelHtml === undefined) {
continue
}
const input = document.createElement("input")
input.id = "radio" + groupId + "-" + i1
input.name = groupId
input.type = "radio"
input.classList.add("cursor-pointer", "p-1", "mr-2")
if (!this._dontStyle) {
input.classList.add("p-1", "ml-2", "pl-2", "pr-0", "m-3", "mr-0")
}
input.onchange = () => {
if (input.checked) {
selectedElementIndex.setData(i1)
}
}
inputs.push(input)
const label = document.createElement("label")
label.appendChild(labelHtml)
label.htmlFor = input.id
label.classList.add("flex", "w-full", "cursor-pointer")
if (!this._dontStyle) {
labelHtml.classList.add("p-2")
}
const block = document.createElement("div")
block.appendChild(input)
block.appendChild(label)
block.classList.add("flex", "w-full")
if (!this._dontStyle) {
block.classList.add("m-1", "border", "border-gray-400")
}
block.style.borderRadius = "1.5rem"
wrappers.push(block)
form.appendChild(block)
}
value.addCallbackAndRun((selected: T) => {
let somethingChecked = false
for (let i = 0; i < inputs.length; i++) {
let input = inputs[i]
input.checked = !somethingChecked && elements[i].IsValid(selected)
somethingChecked = somethingChecked || input.checked
if (input.checked) {
wrappers[i].classList.remove("border-gray-400")
wrappers[i].classList.add("border-attention")
} else {
wrappers[i].classList.add("border-gray-400")
wrappers[i].classList.remove("border-attention")
}
}
})
this.SetClass("flex flex-col")
return form
}
}

View file

@ -5,8 +5,8 @@
import { UIEventSource } from "../../../Logic/UIEventSource"
import BasicTagInput from "../../Studio/TagInput/BasicTagInput.svelte"
import { TagUtils } from "../../../Logic/Tags/TagUtils"
import nmd from "nano-markdown"
import FromHtml from "../../Base/FromHtml.svelte"
import Markdown from "../../Base/Markdown.svelte"
export let value: UIEventSource<undefined | string>
export let args: string[] = []
let uploadableOnly: boolean = args[0] === "uploadableOnly"
@ -34,6 +34,6 @@
{#if $dropdownFocussed}
<div class="m-2 border border-dashed border-black p-2">
<b>{documentation.name}</b>
<FromHtml src={nmd(documentation.docs)} />
<Markdown src={documentation.docs} />
</div>
{/if}

View file

@ -29,6 +29,7 @@ import IdValidator from "./Validators/IdValidator"
import SlopeValidator from "./Validators/SlopeValidator"
import VeloparkValidator from "./Validators/VeloparkValidator"
import NameSuggestionIndexValidator from "./Validators/NameSuggestionIndexValidator"
import CurrencyValidator from "./Validators/CurrencyValidator"
export type ValidatorType = (typeof Validators.availableTypes)[number]
@ -62,6 +63,7 @@ export default class Validators {
"slope",
"velopark",
"nsi",
"currency"
] as const
public static readonly AllValidators: ReadonlyArray<Validator> = [
@ -92,6 +94,7 @@ export default class Validators {
new SlopeValidator(),
new VeloparkValidator(),
new NameSuggestionIndexValidator(),
new CurrencyValidator()
]
private static _byType = Validators._byTypeConstructor()

View file

@ -0,0 +1,73 @@
import { Validator } from "../Validator"
import { Utils } from "../../../Utils"
export default class CurrencyValidator extends Validator {
private readonly segmenter: Intl.Segmenter
private readonly symbolToCurrencyMapping: Map<string, string>
private readonly supportedCurrencies: Set<string>
constructor() {
super("currency", "Validates monetary amounts")
if (Intl.Segmenter === undefined || Utils.runningFromConsole) {
// Librewolf doesn't support this
return
}
let locale = "en-US"
if(!Utils.runningFromConsole){
locale??= navigator.language
}
this.segmenter = new Intl.Segmenter(locale, {
granularity: "word"
})
const mapping: Map<string, string> = new Map<string, string>()
const supportedCurrencies: Set<string> = new Set(Intl.supportedValuesOf("currency"))
this.supportedCurrencies = supportedCurrencies
for (const currency of supportedCurrencies) {
const symbol = (0).toLocaleString(
locale,
{
style: "currency",
currency: currency,
minimumFractionDigits: 0,
maximumFractionDigits: 0
}
).replace(/\d/g, "").trim()
mapping.set(symbol.toLowerCase(), currency)
}
this.symbolToCurrencyMapping = mapping
}
reformat(s: string): string {
if (!this.segmenter) {
return s
}
const parts = Array.from(this.segmenter.segment(s)).map(i => i.segment).filter(part => part.trim().length > 0)
if(parts.length !== 2){
return s
}
const mapping = this.symbolToCurrencyMapping
let currency: string = undefined
let amount = undefined
for (const part of parts) {
const lc = part.toLowerCase()
if (this.supportedCurrencies.has(part.toUpperCase())) {
currency = part.toUpperCase()
continue
}
if (mapping.has(lc)) {
currency = mapping.get(lc)
continue
}
amount = part
}
if(amount === undefined || currency === undefined){
return s
}
return amount+" "+currency
}
}

View file

@ -3,7 +3,7 @@ import UrlValidator from "./UrlValidator"
export default class VeloparkValidator extends UrlValidator {
constructor() {
super("velopark", "A custom element to allow copy-pasting velopark-pages")
super("velopark", "A special URL-validator that checks the domain name and rewrites to the correct velopark format.")
}
getFeedback(s: string): Translation {

View file

@ -38,11 +38,13 @@
{#if !allCalculatedTags.has(key)}
<tr>
<td>{key}</td>
<td>
<td style="width: 75%">
{#if $tags[key] === undefined}
<i>undefined</i>
{:else if $tags[key] === ""}
<i>Empty string</i>
{:else if typeof $tags[key] === "object"}
<div class="literal-code" >{JSON.stringify($tags[key])}</div>
{:else}
{$tags[key]}
{/if}

View file

@ -30,8 +30,9 @@
import { placeholder } from "../../../Utils/placeholder"
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
import { Tag } from "../../../Logic/Tags/Tag"
import { get, writable } from "svelte/store"
import { And } from "../../../Logic/Tags/And"
import { get } from "svelte/store"
import Markdown from "../../Base/Markdown.svelte"
export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>>
@ -70,13 +71,19 @@
/**
* Prepares and fills the checkedMappings
*/
function initialize(tgs: Record<string, string>, confg: TagRenderingConfig) {
function initialize(tgs: Record<string, string>, confg: TagRenderingConfig): void {
mappings = confg.mappings?.filter((m) => {
if (typeof m.hideInAnswer === "boolean") {
return !m.hideInAnswer
}
return !m.hideInAnswer.matchesProperties(tgs)
})
selectedMapping = mappings?.findIndex(
(mapping) => mapping.if.matchesProperties(tgs) || mapping.alsoShowIf?.matchesProperties(tgs)
)
if (selectedMapping < 0) {
selectedMapping = undefined
}
// We received a new config -> reinit
unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key))
@ -87,7 +94,7 @@
checkedMappings?.length < confg.mappings.length + (confg.freeform ? 1 : 0))
) {
const seenFreeforms = []
// Initial setup of the mappings
// Initial setup of the mappings; detect checked mappings
checkedMappings = [
...confg.mappings.map((mapping) => {
if (mapping.hideInAnswer === true) {
@ -275,15 +282,19 @@
</div>
{#if config.questionhint}
<div class="max-h-60 overflow-y-auto">
<SpecialTranslation
t={config.questionhint}
{tags}
{state}
{layer}
feature={selectedElement}
/>
</div>
{#if config.questionHintIsMd}
<Markdown srcWritable={config.questionhint.current} />
{:else}
<div class="max-h-60 overflow-y-auto">
<SpecialTranslation
t={config.questionhint}
{tags}
{state}
{layer}
feature={selectedElement}
/>
</div>
{/if}
{/if}
</legend>

View file

@ -20,12 +20,16 @@
import NextButton from "../Base/NextButton.svelte"
import BackButton from "../Base/BackButton.svelte"
import DeleteButton from "./DeleteButton.svelte"
import StudioHashSetter from "./StudioHashSetter"
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw
export let state: EditLayerState
export let backToStudio: () => void
new StudioHashSetter("layer", state.selectedTab, state.getStoreFor(["id"]))
let messages = state.messages
let hasErrors = messages.mapD(
(m: ConversionMessage[]) => m.filter((m) => m.level === "error").length
@ -127,14 +131,14 @@
{/each}
{:else}
<div class="m4 h-full overflow-y-auto">
<TabbedGroup>
<TabbedGroup tab={state.selectedTab}>
<div slot="title0" class="flex">
General properties
<ErrorIndicatorForRegion firstPaths={firstPathsFor("Basic")} {state} />
</div>
<div class="flex flex-col mb-8" slot="content0">
<Region {state} configs={perRegion["Basic"]} />
<DeleteButton {state} {backToStudio} objectType="layer"/>
<DeleteButton {state} {backToStudio} objectType="layer" />
</div>
<div slot="title1" class="flex">
@ -185,19 +189,18 @@
Below, you'll find the raw configuration file in `.json`-format. This is mostly for
debugging purposes, but you can also edit the file directly if you want.
</div>
<div class="literal-code overflow-y-auto h-full" style="min-height: 75%">
<RawEditor {state} />
</div>
<ShowConversionMessages messages={$messages} />
<div class="flex h-full w-full flex-row justify-between overflow-y-auto">
<div class="literal-code h-full w-5/6 overflow-y-auto">
<RawEditor {state} />
</div>
<div class="h-full w-1/6">
<div>
The testobject (which is used to render the questions in the 'information panel'
item has the following tags:
</div>
<AllTagsPanel tags={state.testTags} />
<div class="flex w-full flex-col">
<div>
The testobject (which is used to render the questions in the 'information panel'
item has the following tags:
</div>
<AllTagsPanel tags={state.testTags} />
</div>
</div>
</TabbedGroup>

View file

@ -42,6 +42,11 @@ export abstract class EditJsonState<T> {
public readonly configuration: UIEventSource<Partial<T>> = new UIEventSource<Partial<T>>({})
public readonly messages: Store<ConversionMessage[]>
/**
* The tab in the UI that is selected, used for deeplinks
*/
public readonly selectedTab: UIEventSource<number> = new UIEventSource<number>(0)
/**
* The EditLayerUI shows a 'schemaBasedInput' for this path to pop advanced questions out
*/
@ -508,6 +513,9 @@ export class EditThemeState extends EditJsonState<LayoutConfigJson> {
}
const prepare = this.buildValidation(state)
const context = ConversionContext.construct([], ["prepare"])
if(configuration.layers){
Utils.NoNullInplace(configuration.layers)
}
try {
prepare.convert(<LayoutConfigJson>configuration, context)
} catch (e) {

View file

@ -9,12 +9,15 @@
import RawEditor from "./RawEditor.svelte"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import DeleteButton from "./DeleteButton.svelte"
import { UIEventSource } from "../../Logic/UIEventSource"
import StudioHashSetter from "./StudioHashSetter"
export let state: EditThemeState
export let osmConnection: OsmConnection
export let backToStudio: () => void
let schema: ConfigMeta[] = state.schema.filter((schema) => schema.path.length > 0)
new StudioHashSetter("theme", state.selectedTab, state.getStoreFor(["id"]))
export let selfLayers: { owner: number; id: string }[]
export let otherLayers: { owner: number; id: string }[]
@ -94,7 +97,7 @@
<div class="m4 h-full overflow-y-auto">
<!-- {Object.keys(perRegion).join(";")} -->
<TabbedGroup>
<TabbedGroup tab={state.selectedTab}>
<div slot="title0">Basic properties</div>
<div slot="content0" class="mb-8">
<Region configs={perRegion["basic"]} path={[]} {state} title="Basic properties" />
@ -123,11 +126,10 @@
Below, you'll find the raw configuration file in `.json`-format. This is mostly for
debugging purposes, but you can also edit the file directly if you want.
</div>
<ShowConversionMessages messages={$messages} />
<div class="literal-code h-full w-full">
<div class="literal-code overflow-y-auto h-full" style="min-height: 75%">
<RawEditor {state} />
</div>
</div>
<ShowConversionMessages messages={$messages} />
</TabbedGroup>
</div>
</div>

View file

@ -5,12 +5,12 @@
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
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 ShowConversionMessage from "./ShowConversionMessage.svelte"
import NextButton from "../Base/NextButton.svelte"
import Markdown from "../Base/Markdown.svelte"
export let state: EditLayerState
export let path: ReadonlyArray<string | number>
@ -62,13 +62,6 @@
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">
@ -82,7 +75,7 @@
{/if}
</NextButton>
{#if description}
<FromHtml src={description} />
<Markdown src={description}/>
{/if}
{#each $messages as message}
<ShowConversionMessage {message} />

View file

@ -8,6 +8,7 @@
import QuestionPreview from "./QuestionPreview.svelte"
import SchemaBasedMultiType from "./SchemaBasedMultiType.svelte"
import ShowConversionMessage from "./ShowConversionMessage.svelte"
import Markdown from "../Base/Markdown.svelte"
export let state: EditLayerState
export let schema: ConfigMeta
@ -30,10 +31,9 @@
schema.description = undefined
}
const subparts: ConfigMeta = state
const subparts: ConfigMeta[] = state
.getSchemaStartingWith(schema.path)
.filter((part) => part.path.length - 1 === schema.path.length)
let messages = state.messagesFor(path)
const currentValue: UIEventSource<any[]> = state.getStoreFor(path)
@ -97,9 +97,7 @@
<h3>{schema.path.at(-1)}</h3>
{#if subparts.length > 0}
<span class="subtle">
{schema.description}
</span>
<Markdown src={schema.description}/>
{/if}
{#if $currentValue === undefined}
No array defined

View file

@ -3,7 +3,6 @@
import type { ConfigMeta } from "./configMeta"
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import nmd from "nano-markdown"
import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import EditLayerState from "./EditLayerState"
import { onDestroy } from "svelte"
@ -68,11 +67,12 @@
type = type.substring(0, type.length - 2)
}
const configJson: QuestionableTagRenderingConfigJson = {
const configJson: QuestionableTagRenderingConfigJson & {questionHintIsMd: boolean} = {
id: path.join("_"),
render: rendervalue,
question: schema.hints.question,
questionHint: nmd(schema.description),
questionHint: schema.description,
questionHintIsMd: true,
freeform:
schema.type === "boolean"
? undefined

View file

@ -8,9 +8,8 @@
import { onDestroy } from "svelte"
import SchemaBasedInput from "./SchemaBasedInput.svelte"
import type { JsonSchemaType } from "./jsonSchema"
// @ts-ignore
import nmd from "nano-markdown"
import ShowConversionMessage from "./ShowConversionMessage.svelte"
import type { Translatable } from "../../Models/ThemeConfig/Json/Translatable"
/**
* If 'types' is defined: allow the user to pick one of the types to input.
@ -41,10 +40,11 @@
if (lastIsString) {
types.splice(types.length - 1, 1)
}
const configJson: QuestionableTagRenderingConfigJson = {
const configJson: QuestionableTagRenderingConfigJson & {questionHintIsMd: boolean}= {
id: "TYPE_OF:" + path.join("_"),
question: "Which subcategory is needed for " + schema.path.at(-1) + "?",
questionHint: nmd(schema.description),
question: schema.hints.question ?? "Which subcategory is needed for " + schema.path.at(-1) + "?",
questionHint: schema.description,
questionHintIsMd: true,
mappings: types
.map((opt) => opt.trim())
.filter((opt) => opt.length > 0)

View file

@ -0,0 +1,11 @@
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import Hash from "../../Logic/Web/Hash"
export default class StudioHashSetter {
constructor(mode: "layer" | "theme", tab: Store<number>, name: Store<string>) {
tab.mapD(tab => {
Hash.hash.setData(mode + "/" + name.data + "/" + tab)
}
, [name])
}
}

View file

@ -1,7 +1,7 @@
<script lang="ts">
import NextButton from "./Base/NextButton.svelte"
import { Store, UIEventSource } from "../Logic/UIEventSource"
import EditLayerState, { EditThemeState } from "./Studio/EditLayerState"
import EditLayerState, { EditJsonState, EditThemeState } from "./Studio/EditLayerState"
import EditLayer from "./Studio/EditLayer.svelte"
import Loading from "../assets/svg/Loading.svelte"
import StudioServer from "./Studio/StudioServer"
@ -30,6 +30,7 @@
import Tr from "./Base/Tr.svelte"
import Add from "../assets/svg/Add.svelte"
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
import Hash from "../Logic/Web/Hash"
export let studioUrl =
window.location.hostname === "127.0.0.2"
@ -43,11 +44,11 @@
)
let osmConnection = new OsmConnection({
oauth_token,
checkOnlineRegularly: true,
checkOnlineRegularly: true
})
const expertMode = UIEventSource.asBoolean(
osmConnection.GetPreference("studio-expert-mode", "false", {
documentation: "Indicates if more options are shown in mapcomplete studio",
documentation: "Indicates if more options are shown in mapcomplete studio"
})
)
expertMode.addCallbackAndRunD((expert) => console.log("Expert mode is", expert))
@ -61,12 +62,12 @@
l["success"]?.filter((l) => l.category === "layers")
)
$: selfLayers = layers.mapD(
(ls) =>
ls.filter(
(l) => l.owner === uid.data && l.id.toLowerCase().includes(layerFilterTerm.toLowerCase())
),
[uid]
)
(ls) =>
ls.filter(
(l) => l.owner === uid.data && l.id.toLowerCase().includes(layerFilterTerm.toLowerCase())
),
[uid]
)
$: otherLayers = layers.mapD(
(ls) =>
ls.filter(
@ -132,16 +133,17 @@
const version = meta.version
async function editLayer(event: Event) {
async function editLayer(event: { detail }): Promise<EditLayerState> {
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"
return editLayerState
}
async function editTheme(event: Event) {
async function editTheme(event: { detail }): Promise<EditThemeState> {
const id: { id: string; owner: number } = event["detail"]
state = "loading"
editThemeState.startSavingUpdates(false)
@ -149,6 +151,7 @@
editThemeState.configuration.setData(layout)
editThemeState.startSavingUpdates()
state = "editing_theme"
return editThemeState
}
async function createNewLayer() {
@ -162,23 +165,50 @@
marker: [
{
icon: "circle",
color: "white",
},
],
},
color: "white"
}
]
}
],
tagRenderings: ["images"],
lineRendering: [
{
width: 1,
color: "blue",
},
],
color: "blue"
}
]
}
editLayerState.configuration.setData(initialLayerConfig)
editLayerState.startSavingUpdates()
state = "editing_layer"
}
async function selectStateBasedOnHash() {
const hash = Hash.hash.data
if (!hash) {
return
}
console.log("Selecting state based on ", hash)
const [mode, id, tab] = hash.split("/")
// Not really an event, we just set the 'detail'
const event = {
detail: {
id,
owner: uid.data
}
}
const statePromise: Promise<EditJsonState<any>> = mode === "layer" ? editLayer(event) : editTheme(event)
const state = await statePromise
state.selectedTab.setData(Number(tab))
}
selectStateBasedOnHash()
function backToStudio() {
console.log("Back to studio")
state = undefined
Hash.hash.setData(undefined)
}
</script>
<If condition={layersWithErr.map((d) => d?.["error"] !== undefined)}>
@ -191,8 +221,8 @@
<li>Try again in a few minutes</li>
<li>
Contact <a href="https://app.element.io/#/room/#MapComplete:matrix.org">
the MapComplete community via the chat.
</a>
the MapComplete community via the chat.
</a>
Someone might be able to help you
</li>
<li>
@ -257,9 +287,7 @@
<BackButton
clss="small p-1"
imageClass="w-8 h-8"
on:click={() => {
state = undefined
}}
on:click={() => backToStudio()}
>
MapComplete Studio
</BackButton>
@ -306,9 +334,7 @@
<BackButton
clss="small p-1"
imageClass="w-8 h-8"
on:click={() => {
state = undefined
}}
on:click={() => backToStudio()}
>
MapComplete Studio
</BackButton>
@ -348,30 +374,23 @@
{:else if state === "editing_layer"}
<EditLayer
state={editLayerState}
backToStudio={() => {
state = undefined
}}
{backToStudio}
>
<BackButton
clss="small p-1"
imageClass="w-8 h-8"
on:click={() => {
state = undefined
}}
on:click={() => backToStudio()}
>
MapComplete Studio
</BackButton>
</EditLayer>
{:else if state === "editing_theme"}
<EditTheme state={editThemeState} selfLayers={$selfLayers} otherLayers={$otherLayers} {osmConnection} backToStudio={() => {
state = undefined
}}>
<EditTheme state={editThemeState} selfLayers={$selfLayers} otherLayers={$otherLayers} {osmConnection}
{backToStudio}>
<BackButton
clss="small p-1"
imageClass="w-8 h-8"
on:click={() => {
state = undefined
}}
on:click={() => backToStudio()}
>
MapComplete Studio
</BackButton>

View file

@ -285,9 +285,9 @@
<div class="flex w-full items-end justify-between px-4">
<div class="flex flex-col">
<If condition={featureSwitches.featureSwitchEnableLogin}>
{#if state.layout.hasPresets() || state.layout.hasNoteLayer()}
{#if (state.layout.hasPresets() && state.layout.enableAddNewPoints) || state.layout.hasNoteLayer()}
<button
class="pointer-events-auto w-fit"
class="pointer-events-auto w-fit low-interaction"
class:disabled={$currentZoom < Constants.minZoomLevelToAddNewPoint}
on:click={() => {
state.openNewDialog()

View file

@ -1,8 +1,8 @@
<script lang="ts">
import nmd from "nano-markdown"
import { createEventDispatcher } from "svelte"
import WalkthroughStep from "./WalkthroughStep.svelte"
import FromHtml from "../Base/FromHtml.svelte"
import Markdown from "../Base/Markdown.svelte"
/**
* Markdown
@ -31,5 +31,5 @@
totalPages={pages.length}
pageNumber={currentPage}
>
<FromHtml src={nmd(pages[currentPage])} />
<Markdown src={pages[currentPage]} />
</WalkthroughStep>

View file

@ -11,7 +11,7 @@
</script>
<div class="link-underline flex h-full w-full flex-col justify-between">
<div class="overflow-y-auto">
<div class="overflow-y-auto h-full">
<slot />
</div>

View file

@ -331,11 +331,16 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
configurable: true,
get: () => {
delete object[name]
object[name] = init()
if (whenDone) {
whenDone()
try{
object[name] = init()
if (whenDone) {
whenDone()
}
return object[name]
}catch (e) {
console.error("Error while calculating a lazy property", e)
return undefined
}
return object[name]
},
})
}
@ -502,7 +507,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
let result = ""
while (match) {
const [_, normal, key, leftover] = match
let v = tags === undefined ? undefined : tags[key]
let v = tags?.[key]
if (v !== undefined && v !== null) {
if (v["toISOString"] != undefined) {
// This is a date, probably the timestamp of the object
@ -1644,4 +1649,12 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
}
return n + Utils._metrixPrefixes[index]
}
static NoNullInplace(layers: any[]):void {
for (let i = layers.length - 1; i >= 0; i--) {
if(layers[i] === null || layers[i] === undefined){
layers.splice(i, 1)
}
}
}
}

View file

@ -9965,7 +9965,7 @@
"group": "presets"
},
"type": "array",
"description": "Presets for this layer.\nA preset shows up when clicking the map on a without data (or when right-clicking/long-pressing);\nit will prompt the user to add a new point.\nThe most important aspect are the tags, which define which tags the new point will have;\nThe title is shown in the dialog, along with the first sentence of the description.\nUpon confirmation, the full description is shown beneath the buttons - perfect to add pictures and examples.\nNote: the icon of the preset is determined automatically based on the tags and the icon above. Don't worry about that!\nNB: if no presets are defined, the popup to add new points doesn't show up at all"
"description": "<div class='flex'>\n <div>\nPresets for this layer.\nA preset consists of one or more attributes (tags), a title and optionally a description and optionally example images.\nWhen the contributor wishes to add a point to OpenStreetMap, they'll:\n1. Press the 'add new point'-button\n2. Choose a preset from the list of all presets\n3. Confirm the choice. In this step, the `description` (if set) and `exampleImages` (if given) will be shown\n4. Confirm the location\n5. A new point will be created with the attributes that were defined in the preset\nIf no presets are defined, the button which invites to add a new preset will not be shown.\n</div>\n<a class='block m-2 min-w-64' href='./Docs/Screenshots/AddNewItemScreencast.webm' target='_blank'> <video controls autoplay muted src='./Docs/Screenshots/AddNewItemScreencast.webm' class='w-64'/></a>\n</div>"
},
{
"path": [
@ -10275,6 +10275,10 @@
"if": "value=food",
"then": "food - A layer showing restaurants and fast-food amenities (with a special rendering for friteries)"
},
{
"if": "value=food_courts",
"then": "food_courts - Food courts with a variety of food options."
},
{
"if": "value=ghost_bike",
"then": "ghost_bike - A layer showing memorials for cyclists, killed in road accidents"
@ -10407,6 +10411,10 @@
"if": "value=osm_community_index",
"then": "osm_community_index - A layer showing the OpenStreetMap Communities"
},
{
"if": "value=outdoor_seating",
"then": "outdoor_seating - Outdoor seating areas, usually located near cafes and restaurants."
},
{
"if": "value=parcel_lockers",
"then": "parcel_lockers - Layer showing parcel lockers for collecting and sending parcels."
@ -11646,7 +11654,11 @@
},
{
"if": "value=velopark",
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
},
{
"if": "value=currency",
"then": "<b>currency</b> Validates monetary amounts"
}
]
},
@ -12918,7 +12930,11 @@
},
{
"if": "value=velopark",
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
},
{
"if": "value=currency",
"then": "<b>currency</b> Validates monetary amounts"
}
]
},
@ -14223,7 +14239,11 @@
},
{
"if": "value=velopark",
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
},
{
"if": "value=currency",
"then": "<b>currency</b> Validates monetary amounts"
}
]
},
@ -15544,7 +15564,11 @@
},
{
"if": "value=velopark",
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
},
{
"if": "value=currency",
"then": "<b>currency</b> Validates monetary amounts"
}
]
},
@ -16864,7 +16888,11 @@
},
{
"if": "value=velopark",
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
},
{
"if": "value=currency",
"then": "<b>currency</b> Validates monetary amounts"
}
]
},
@ -18185,7 +18213,11 @@
},
{
"if": "value=velopark",
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
},
{
"if": "value=currency",
"then": "<b>currency</b> Validates monetary amounts"
}
]
},

View file

@ -556,6 +556,10 @@
"if": "value=food",
"then": "<b>food</b> (builtin) - A layer showing restaurants and fast-food amenities (with a special rendering for friteries)"
},
{
"if": "value=food_courts",
"then": "<b>food_courts</b> (builtin) - Food courts with a variety of food options."
},
{
"if": "value=ghost_bike",
"then": "<b>ghost_bike</b> (builtin) - A layer showing memorials for cyclists, killed in road accidents"
@ -688,6 +692,10 @@
"if": "value=osm_community_index",
"then": "<b>osm_community_index</b> (builtin) - A layer showing the OpenStreetMap Communities"
},
{
"if": "value=outdoor_seating",
"then": "<b>outdoor_seating</b> (builtin) - Outdoor seating areas, usually located near cafes and restaurants."
},
{
"if": "value=parcel_lockers",
"then": "<b>parcel_lockers</b> (builtin) - Layer showing parcel lockers for collecting and sending parcels."
@ -1315,7 +1323,7 @@
"type": "boolean"
},
"presets": {
"description": "Presets for this layer.\nA preset shows up when clicking the map on a without data (or when right-clicking/long-pressing);\nit will prompt the user to add a new point.\n\nThe most important aspect are the tags, which define which tags the new point will have;\nThe title is shown in the dialog, along with the first sentence of the description.\n\nUpon confirmation, the full description is shown beneath the buttons - perfect to add pictures and examples.\n\nNote: the icon of the preset is determined automatically based on the tags and the icon above. Don't worry about that!\nNB: if no presets are defined, the popup to add new points doesn't show up at all\n\ngroup: presets",
"description": "<div class='flex'>\n <div>\nPresets for this layer.\n\nA preset consists of one or more attributes (tags), a title and optionally a description and optionally example images.\n\nWhen the contributor wishes to add a point to OpenStreetMap, they'll:\n\n1. Press the 'add new point'-button\n2. Choose a preset from the list of all presets\n3. Confirm the choice. In this step, the `description` (if set) and `exampleImages` (if given) will be shown\n4. Confirm the location\n5. A new point will be created with the attributes that were defined in the preset\n\nIf no presets are defined, the button which invites to add a new preset will not be shown.\n</div>\n<a class='block m-2 min-w-64' href='./Docs/Screenshots/AddNewItemScreencast.webm' target='_blank'> <video controls autoplay muted src='./Docs/Screenshots/AddNewItemScreencast.webm' class='w-64'/></a>\n</div>\n\ngroup: presets",
"type": "array",
"items": {
"type": "object",
@ -1908,7 +1916,7 @@
"type": "string"
}
],
"description": "Every layer contains a description of which feature to display - the overpassTags which are queried.\nInstead of running one query for every layer, the query is fused.\nAfterwards, every layer is given the list of features.\nEvery layer takes away the features that match with them*, and give the leftovers to the next layers.\nThis implies that the _order_ of the layers is important in the case of features with the same tags;\nas the later layers might never receive their feature.\n*layers can also remove 'leftover'-features if the leftovers overlap with a feature in the layer itself\nNote that builtin layers can be reused. Either put in the name of the layer to reuse, or use {builtin: \"layername\", override: ...}\nThe 'override'-object will be copied over the original values of the layer, which allows to change certain aspects of the layer\nFor example: If you would like to use layer nature reserves, but only from a specific operator (eg. Natuurpunt) you would use the following in your theme:\n```\n\"layer\": {\n \"builtin\": \"nature_reserve\",\n \"override\": {\"source\":\n {\"osmTags\": {\n \"+and\":[\"operator=Natuurpunt\"]\n }\n }\n }\n}\n```\nIt's also possible to load multiple layers at once, for example, if you would like for both drinking water and benches to start at the zoomlevel at 12, you would use the following:\n```\n\"layer\": {\n \"builtin\": [\"benches\", \"drinking_water\"],\n \"override\": {\"minzoom\": 12}\n}\n```"
"description": "A theme must contain at least one layer.\nA layer contains all features of a single type, for example \"shops\", \"bicycle pumps\", \"benches\".\nNote that every layer contains a specification of attributes that it should match. MapComplete will fetch the relevant data from either overpass, the OSM-API or the cache server.\nIf a feature can match multiple layers, the first matching layer in the list will be used.\nThis implies that the _order_ of the layers is important.\n<div class='hidden-in-studio'>\nNote that builtin layers can be reused. Either put in the name of the layer to reuse, or use {builtin: \"layername\", override: ...}\nThe 'override'-object will be copied over the original values of the layer, which allows to change certain aspects of the layer\nFor example: If you would like to use layer nature reserves, but only from a specific operator (eg. Natuurpunt) you would use the following in your theme:\n```\n\"layer\": {\n \"builtin\": \"nature_reserve\",\n \"override\": {\"source\":\n {\"osmTags\": {\n \"+and\":[\"operator=Natuurpunt\"]\n }\n }\n }\n}\n```\nIt's also possible to load multiple layers at once, for example, if you would like for both drinking water and benches to start at the zoomlevel at 12, you would use the following:\n```\n\"layer\": {\n \"builtin\": [\"benches\", \"drinking_water\"],\n \"override\": {\"minzoom\": 12}\n}\n```\n</div>"
},
{
"path": [
@ -12195,7 +12203,7 @@
"group": "presets"
},
"type": "array",
"description": "Presets for this layer.\nA preset shows up when clicking the map on a without data (or when right-clicking/long-pressing);\nit will prompt the user to add a new point.\nThe most important aspect are the tags, which define which tags the new point will have;\nThe title is shown in the dialog, along with the first sentence of the description.\nUpon confirmation, the full description is shown beneath the buttons - perfect to add pictures and examples.\nNote: the icon of the preset is determined automatically based on the tags and the icon above. Don't worry about that!\nNB: if no presets are defined, the popup to add new points doesn't show up at all"
"description": "<div class='flex'>\n <div>\nPresets for this layer.\nA preset consists of one or more attributes (tags), a title and optionally a description and optionally example images.\nWhen the contributor wishes to add a point to OpenStreetMap, they'll:\n1. Press the 'add new point'-button\n2. Choose a preset from the list of all presets\n3. Confirm the choice. In this step, the `description` (if set) and `exampleImages` (if given) will be shown\n4. Confirm the location\n5. A new point will be created with the attributes that were defined in the preset\nIf no presets are defined, the button which invites to add a new preset will not be shown.\n</div>\n<a class='block m-2 min-w-64' href='./Docs/Screenshots/AddNewItemScreencast.webm' target='_blank'> <video controls autoplay muted src='./Docs/Screenshots/AddNewItemScreencast.webm' class='w-64'/></a>\n</div>"
},
{
"path": [
@ -12510,6 +12518,10 @@
"if": "value=food",
"then": "food - A layer showing restaurants and fast-food amenities (with a special rendering for friteries)"
},
{
"if": "value=food_courts",
"then": "food_courts - Food courts with a variety of food options."
},
{
"if": "value=ghost_bike",
"then": "ghost_bike - A layer showing memorials for cyclists, killed in road accidents"
@ -12642,6 +12654,10 @@
"if": "value=osm_community_index",
"then": "osm_community_index - A layer showing the OpenStreetMap Communities"
},
{
"if": "value=outdoor_seating",
"then": "outdoor_seating - Outdoor seating areas, usually located near cafes and restaurants."
},
{
"if": "value=parcel_lockers",
"then": "parcel_lockers - Layer showing parcel lockers for collecting and sending parcels."
@ -13908,7 +13924,11 @@
},
{
"if": "value=velopark",
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
},
{
"if": "value=currency",
"then": "<b>currency</b> Validates monetary amounts"
}
]
},
@ -15229,7 +15249,11 @@
},
{
"if": "value=velopark",
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
},
{
"if": "value=currency",
"then": "<b>currency</b> Validates monetary amounts"
}
]
},
@ -16584,7 +16608,11 @@
},
{
"if": "value=velopark",
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
},
{
"if": "value=currency",
"then": "<b>currency</b> Validates monetary amounts"
}
]
},
@ -17954,7 +17982,11 @@
},
{
"if": "value=velopark",
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
},
{
"if": "value=currency",
"then": "<b>currency</b> Validates monetary amounts"
}
]
},
@ -19323,7 +19355,11 @@
},
{
"if": "value=velopark",
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
},
{
"if": "value=currency",
"then": "<b>currency</b> Validates monetary amounts"
}
]
},
@ -20693,7 +20729,11 @@
},
{
"if": "value=velopark",
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
},
{
"if": "value=currency",
"then": "<b>currency</b> Validates monetary amounts"
}
]
},
@ -32813,7 +32853,7 @@
"group": "presets"
},
"type": "array",
"description": "Presets for this layer.\nA preset shows up when clicking the map on a without data (or when right-clicking/long-pressing);\nit will prompt the user to add a new point.\nThe most important aspect are the tags, which define which tags the new point will have;\nThe title is shown in the dialog, along with the first sentence of the description.\nUpon confirmation, the full description is shown beneath the buttons - perfect to add pictures and examples.\nNote: the icon of the preset is determined automatically based on the tags and the icon above. Don't worry about that!\nNB: if no presets are defined, the popup to add new points doesn't show up at all"
"description": "<div class='flex'>\n <div>\nPresets for this layer.\nA preset consists of one or more attributes (tags), a title and optionally a description and optionally example images.\nWhen the contributor wishes to add a point to OpenStreetMap, they'll:\n1. Press the 'add new point'-button\n2. Choose a preset from the list of all presets\n3. Confirm the choice. In this step, the `description` (if set) and `exampleImages` (if given) will be shown\n4. Confirm the location\n5. A new point will be created with the attributes that were defined in the preset\nIf no presets are defined, the button which invites to add a new preset will not be shown.\n</div>\n<a class='block m-2 min-w-64' href='./Docs/Screenshots/AddNewItemScreencast.webm' target='_blank'> <video controls autoplay muted src='./Docs/Screenshots/AddNewItemScreencast.webm' class='w-64'/></a>\n</div>"
},
{
"path": [
@ -33133,6 +33173,10 @@
"if": "value=food",
"then": "food - A layer showing restaurants and fast-food amenities (with a special rendering for friteries)"
},
{
"if": "value=food_courts",
"then": "food_courts - Food courts with a variety of food options."
},
{
"if": "value=ghost_bike",
"then": "ghost_bike - A layer showing memorials for cyclists, killed in road accidents"
@ -33265,6 +33309,10 @@
"if": "value=osm_community_index",
"then": "osm_community_index - A layer showing the OpenStreetMap Communities"
},
{
"if": "value=outdoor_seating",
"then": "outdoor_seating - Outdoor seating areas, usually located near cafes and restaurants."
},
{
"if": "value=parcel_lockers",
"then": "parcel_lockers - Layer showing parcel lockers for collecting and sending parcels."
@ -34558,7 +34606,11 @@
},
{
"if": "value=velopark",
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
},
{
"if": "value=currency",
"then": "<b>currency</b> Validates monetary amounts"
}
]
},
@ -35928,7 +35980,11 @@
},
{
"if": "value=velopark",
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
},
{
"if": "value=currency",
"then": "<b>currency</b> Validates monetary amounts"
}
]
},
@ -37333,7 +37389,11 @@
},
{
"if": "value=velopark",
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
},
{
"if": "value=currency",
"then": "<b>currency</b> Validates monetary amounts"
}
]
},
@ -38752,7 +38812,11 @@
},
{
"if": "value=velopark",
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
},
{
"if": "value=currency",
"then": "<b>currency</b> Validates monetary amounts"
}
]
},
@ -40170,7 +40234,11 @@
},
{
"if": "value=velopark",
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
},
{
"if": "value=currency",
"then": "<b>currency</b> Validates monetary amounts"
}
]
},
@ -41589,7 +41657,11 @@
},
{
"if": "value=velopark",
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
},
{
"if": "value=currency",
"then": "<b>currency</b> Validates monetary amounts"
}
]
},
@ -43424,7 +43496,7 @@
"group": "feature_switches",
"question": "Should the 'download as CSV'- and 'download as Geojson'-buttons be enabled?",
"iftrue": "Enable the option to download the map as CSV and GeoJson",
"iffalse": "Enable the option to download the map as CSV and GeoJson",
"iffalse": "Disable the option to download the map as CSV and GeoJson",
"ifunset": "MapComplete default: Enable the option to download the map as CSV and GeoJson"
},
"type": "boolean",

View file

@ -663,7 +663,11 @@
},
{
"if": "value=velopark",
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
},
{
"if": "value=currency",
"then": "<b>currency</b> Validates monetary amounts"
}
]
},

View file

@ -27,6 +27,7 @@
--alert-foreground-color: var(--foreground-color);
--low-interaction-background: #eeeeee;
--low-interaction-background-50: #eeeeee90;
--low-interaction-foreground: black;
--low-interaction-contrast: #ff00ff;
@ -264,6 +265,10 @@ button.primary:not(.no-image-background) svg path, .button.primary:not(.no-image
transition: all 250ms;
}
button.disabled.low-interaction, .button.disabled.low-interaction {
background-color: var(--low-interaction-background-50);
}
button.disabled, .button.disabled {
cursor: default;
@ -274,6 +279,7 @@ button.disabled, .button.disabled {
}
button.disabled:hover, .button.disabled:hover {
cursor: default;
border: 2px dashed var(--button-background);
@ -494,7 +500,7 @@ textarea {
}
.literal-code {
.literal-code, code {
/* A codeblock */
display: inline-block;
background-color: lightgray;