forked from MapComplete/MapComplete
Merge branch 'develop' into feature/nsi
This commit is contained in:
commit
572d85a6b5
375 changed files with 34341 additions and 44682 deletions
|
@ -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)]
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 }))
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
17
src/UI/Base/Markdown.svelte
Normal file
17
src/UI/Base/Markdown.svelte
Normal 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}
|
||||
|
|
@ -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)
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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}
|
||||
|
|
|
@ -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()
|
||||
|
|
73
src/UI/InputElement/Validators/CurrencyValidator.ts
Normal file
73
src/UI/InputElement/Validators/CurrencyValidator.ts
Normal 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
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
11
src/UI/Studio/StudioHashSetter.ts
Normal file
11
src/UI/Studio/StudioHashSetter.ts
Normal 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])
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
23
src/Utils.ts
23
src/Utils.ts
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue