Add rewrite of 'special' clauses, various QOLimprovements on import viewer

This commit is contained in:
pietervdvn 2022-03-29 00:20:10 +02:00
parent 8df0324572
commit c47a6d5ea7
22 changed files with 597 additions and 155 deletions

View file

@ -181,6 +181,9 @@ class ChangesetDataTools {
cs.properties.metadata.host = new URL(cs.properties.metadata.host).host cs.properties.metadata.host = new URL(cs.properties.metadata.host).host
} catch (e) { } catch (e) {
}
if(cs.properties.metadata["answer"] > 100){
console.log("Lots of answers for https://osm.org/changeset/"+cs.id)
} }
return cs return cs
} }

View file

@ -42,7 +42,7 @@ export abstract class Conversion<TIn, TOut> {
public convertAll(jsons: TIn[], context: string): { result: TOut[], errors: string[], warnings: string[], information?: string[] } { public convertAll(jsons: TIn[], context: string): { result: TOut[], errors: string[], warnings: string[], information?: string[] } {
if(jsons === undefined || jsons === null){ if(jsons === undefined || jsons === null){
throw "convertAll received undefined or null - don't do this (at "+context+")" throw `Detected a bug in the preprocessor pipeline: ${this.name}.convertAll received undefined or null - don't do this (at ${context})`
} }
const result = [] const result = []
const errors = [] const errors = []
@ -72,23 +72,34 @@ export abstract class DesugaringStep<T> extends Conversion<T, T> {
export class OnEvery<X, T> extends DesugaringStep<T> { export class OnEvery<X, T> extends DesugaringStep<T> {
private readonly key: string; private readonly key: string;
private readonly step: DesugaringStep<X>; private readonly step: DesugaringStep<X>;
private _options: { ignoreIfUndefined: boolean };
constructor(key: string, step: DesugaringStep<X>) { constructor(key: string, step: DesugaringStep<X>, options?: {
ignoreIfUndefined: false | boolean
}) {
super("Applies " + step.name + " onto every object of the list `key`", [key], "OnEvery("+step.name+")"); super("Applies " + step.name + " onto every object of the list `key`", [key], "OnEvery("+step.name+")");
this.step = step; this.step = step;
this.key = key; this.key = key;
this._options = options;
} }
convert(json: T, context: string): { result: T; errors?: string[]; warnings?: string[], information?: string[] } { convert(json: T, context: string): { result: T; errors?: string[]; warnings?: string[], information?: string[] } {
json = {...json} json = {...json}
const step = this.step const step = this.step
const key = this.key; const key = this.key;
const r = step.convertAll((<X[]>json[key]), context + "." + key) if( this._options?.ignoreIfUndefined && json[key] === undefined){
json[key] = r.result return {
return { result: json,
...r, };
result: json, }else{
}; const r = step.convertAll((<X[]>json[key]), context + "." + key)
json[key] = r.result
return {
...r,
result: json,
};
}
} }
} }

View file

@ -1,10 +1,12 @@
import {Conversion, DesugaringContext, Fuse, OnEvery, OnEveryConcat, SetDefault} from "./Conversion"; import {Conversion, DesugaringContext, DesugaringStep, Fuse, OnEvery, OnEveryConcat, SetDefault} from "./Conversion";
import {LayerConfigJson} from "../Json/LayerConfigJson"; import {LayerConfigJson} from "../Json/LayerConfigJson";
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson"; import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson";
import {Utils} from "../../../Utils"; import {Utils} from "../../../Utils";
import RewritableConfigJson from "../Json/RewritableConfigJson";
import SpecialVisualizations from "../../../UI/SpecialVisualizations";
import Translations from "../../../UI/i18n/Translations"; import Translations from "../../../UI/i18n/Translations";
import {Translation} from "../../../UI/i18n/Translation"; import {Translation} from "../../../UI/i18n/Translation";
import RewritableConfigJson from "../Json/RewritableConfigJson"; import * as tagrenderingconfigmeta from "../../../assets/tagrenderingconfigmeta.json"
class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | { builtin: string | string[], override: any }, TagRenderingConfigJson[]> { class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | { builtin: string | string[], override: any }, TagRenderingConfigJson[]> {
private readonly _state: DesugaringContext; private readonly _state: DesugaringContext;
@ -349,28 +351,168 @@ class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[]> {
} }
/**
class ExpandRewriteWithFlatten<T> extends Conversion<T | RewritableConfigJson<T | T[]>, T[]> { * Converts a 'special' translation into a regular translation which uses parameters
* E.g.
private _rewrite = new ExpandRewrite<T>() *
* const tr = <TagRenderingJson> {
* "special":
* }
*/
export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
constructor() { constructor() {
super("Applies a rewrite, the result is flattened if it is an array", [], "ExpandRewriteWithFlatten"); super("Converts a 'special' translation into a regular translation which uses parameters", ["special"],"RewriteSpecial");
} }
convert(json: RewritableConfigJson<T[] | T> | T, context: string): { result: T[]; errors?: string[]; warnings?: string[]; information?: string[] } { /**
return undefined; * Does the heavy lifting and conversion
*
* // should not do anything if no 'special'-key is present
* RewriteSpecial.convertIfNeeded({"en": "xyz", "nl": "abc"}, [], "test") // => {"en": "xyz", "nl": "abc"}
*
* // should handle a simple special case
* RewriteSpecial.convertIfNeeded({"special": {"type":"image_carousel"}}, [], "test") // => {'*': "{image_carousel()}"}
*
* // should handle special case with a parameter
* RewriteSpecial.convertIfNeeded({"special": {"type":"image_carousel", "image_key": "some_image_key"}}, [], "test") // => {'*': "{image_carousel(some_image_key)}"}
*
* // should handle special case with a translated parameter
* const spec = {"special": {"type":"image_upload", "label": {"en": "Add a picture to this object", "nl": "Voeg een afbeelding toe"}}}
* const r = RewriteSpecial.convertIfNeeded(spec, [], "test")
* r // => {"en": "{image_upload(,Add a picture to this object)}", "nl": "{image_upload(,Voeg een afbeelding toe)}" }
*
* // should warn for unexpected keys
* const errors = []
* RewriteSpecial.convertIfNeeded({"special": {type: "image_carousel"}, "en": "xyz"}, errors, "test") // => {'*': "{image_carousel()}"}
* errors // => ["At test: Unexpected key in a special block: en"]
*
* // should give an error on unknown visualisations
* const errors = []
* RewriteSpecial.convertIfNeeded({"special": {type: "qsdf"}}, errors, "test") // => undefined
* errors.length // => 1
* errors[0].indexOf("Special visualisation 'qsdf' not found") >= 0 // => true
*
* // should give an error is 'type' is missing
* const errors = []
* RewriteSpecial.convertIfNeeded({"special": {}}, errors, "test") // => undefined
* errors // => ["A 'special'-block should define 'type' to indicate which visualisation should be used"]
*/
private static convertIfNeeded(input: (object & {special : {type: string}}) | any, errors: string[], context: string): any {
const special = input["special"]
if(special === undefined){
return input
}
for (const wrongKey of Object.keys(input).filter(k => k !== "special")) {
errors.push(`At ${context}: Unexpected key in a special block: ${wrongKey}`)
}
const type = special["type"]
if(type === undefined){
errors.push("A 'special'-block should define 'type' to indicate which visualisation should be used")
return undefined
}
const vis = SpecialVisualizations.specialVisualizations.find(sp => sp.funcName === type)
if(vis === undefined){
const options = Utils.sortedByLevenshteinDistance(type, SpecialVisualizations.specialVisualizations, sp => sp.funcName)
errors.push(`Special visualisation '${type}' not found. Did you perhaps mean ${options[0].funcName}, ${options[1].funcName} or ${options[2].funcName}?\n\tFor all known special visualisations, please see https://github.com/pietervdvn/MapComplete/blob/develop/Docs/SpecialRenderings.md`)
return undefined
}
const argNamesList = vis.args.map(a => a.name)
const argNames = new Set<string>(argNamesList)
// Check for obsolete and misspelled arguments
errors.push(...Object.keys(special)
.filter(k => !argNames.has(k))
.filter(k => k !== "type")
.map(wrongArg => {
const byDistance = Utils.sortedByLevenshteinDistance(wrongArg, argNamesList, x => x)
return `Unexpected argument with name '${wrongArg}'. Did you mean ${byDistance[0]}?\n\tAll known arguments are ${ argNamesList.join(", ")}` ;
}))
// Check that all obligated arguments are present. They are obligated if they don't have a preset value
for (const arg of vis.args) {
if (arg.required !== true) {
continue;
}
const param = special[arg.name]
if(param === undefined){
errors.push(`Obligated parameter '${arg.name}' not found`)
}
}
const foundLanguages = new Set<string>()
const translatedArgs = argNamesList.map(nm => special[nm])
.filter(v => v !== undefined)
.filter(v => Translations.isProbablyATranslation(v))
for (const translatedArg of translatedArgs) {
for (const ln of Object.keys(translatedArg)) {
foundLanguages.add(ln)
}
}
if(foundLanguages.size === 0){
const args= argNamesList.map(nm => special[nm] ?? "").join(",")
return {'*': `{${type}(${args})}`
}
}
const result = {}
const languages = Array.from(foundLanguages)
languages.sort()
for (const ln of languages) {
const args = []
for (const argName of argNamesList) {
const v = special[argName] ?? ""
if(Translations.isProbablyATranslation(v)){
args.push(new Translation(v).textFor(ln))
}else{
args.push(v)
}
}
result[ln] = `{${type}(${args.join(",")})}`
}
return result
} }
/**
* const tr = {
* render: {special: {type: "image_carousel", image_key: "image" }},
* mappings: [
* {
* if: "other_image_key",
* then: {special: {type: "image_carousel", image_key: "other_image_key"}}
* }
* ]
* }
* const result = new RewriteSpecial().convert(tr,"test").result
* const expected = {render: {'*': "{image_carousel(image)}"}, mappings: [{if: "other_image_key", then: {'*': "{image_carousel(other_image_key)}"}} ]}
* result // => expected
*/
convert(json: TagRenderingConfigJson, context: string): { result: TagRenderingConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
const errors = []
json = Utils.Clone(json)
const paths : {path: string[], type?: any, typeHint?: string}[] = tagrenderingconfigmeta["default"] ?? tagrenderingconfigmeta
for (const path of paths) {
if(path.typeHint !== "rendered"){
continue
}
Utils.WalkPath(path.path, json, ((leaf, travelled) => RewriteSpecial.convertIfNeeded(leaf, errors, travelled.join("."))))
}
return {
result:json,
errors
};
}
} }
export class PrepareLayer extends Fuse<LayerConfigJson> { export class PrepareLayer extends Fuse<LayerConfigJson> {
constructor(state: DesugaringContext) { constructor(state: DesugaringContext) {
super( super(
"Fully prepares and expands a layer for the LayerConfig.", "Fully prepares and expands a layer for the LayerConfig.",
new OnEvery("tagRenderings", new RewriteSpecial(), {ignoreIfUndefined: true}),
new OnEveryConcat("tagRenderings", new ExpandGroupRewrite(state)), new OnEveryConcat("tagRenderings", new ExpandGroupRewrite(state)),
new OnEveryConcat("tagRenderings", new ExpandTagRendering(state)), new OnEveryConcat("tagRenderings", new ExpandTagRendering(state)),
new OnEveryConcat("mapRendering", new ExpandRewrite()), new OnEveryConcat("mapRendering", new ExpandRewrite()),

View file

@ -234,7 +234,7 @@ export interface LayerConfigJson {
/** /**
* The type of background picture * The type of background picture
*/ */
preferredBackground: "osmbasedmap" | "photo" | "historicphoto" | "map" | string | string [], preferredBackground: "osmbasedmap" | "photo" | "historicphoto" | "map" | string | string[],
/** /**
* If specified, these layers will be shown to and the new point will be snapped towards it * If specified, these layers will be shown to and the new point will be snapped towards it
*/ */

View file

@ -197,14 +197,14 @@ export default class LayerConfig extends WithContextLoader {
snapToLayers = pr.preciseInput.snapToLayer snapToLayers = pr.preciseInput.snapToLayer
} }
let preferredBackground: string[] let preferredBackground: ("map" | "photo" | "osmbasedmap" | "historicphoto" | string)[]
if (typeof pr.preciseInput.preferredBackground === "string") { if (typeof pr.preciseInput.preferredBackground === "string") {
preferredBackground = [pr.preciseInput.preferredBackground] preferredBackground = [pr.preciseInput.preferredBackground]
} else { } else {
preferredBackground = pr.preciseInput.preferredBackground preferredBackground = pr.preciseInput.preferredBackground
} }
preciseInput = { preciseInput = {
preferredBackground: preferredBackground, preferredBackground,
snapToLayers, snapToLayers,
maxSnapDistance: pr.preciseInput.maxSnapDistance ?? 10 maxSnapDistance: pr.preciseInput.maxSnapDistance ?? 10
} }

View file

@ -2,7 +2,7 @@ import {Translation} from "../../UI/i18n/Translation";
import {Tag} from "../../Logic/Tags/Tag"; import {Tag} from "../../Logic/Tags/Tag";
export interface PreciseInput { export interface PreciseInput {
preferredBackground?: string[], preferredBackground?: ("map" | "photo" | "osmbasedmap" | "historicphoto" | string)[],
snapToLayers?: string[], snapToLayers?: string[],
maxSnapDistance?: number maxSnapDistance?: number
} }

View file

@ -1,20 +1,26 @@
import BaseUIElement from "../BaseUIElement"; import BaseUIElement from "../BaseUIElement";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import Translations from "../i18n/Translations"; import Translations from "../i18n/Translations";
import {UIEventSource} from "../../Logic/UIEventSource";
export default class Table extends BaseUIElement { export default class Table extends BaseUIElement {
private readonly _header: BaseUIElement[]; private readonly _header: BaseUIElement[];
private readonly _contents: BaseUIElement[][]; private readonly _contents: BaseUIElement[][];
private readonly _contentStyle: string[][]; private readonly _contentStyle: string[][];
private readonly _sortable: boolean;
constructor(header: (BaseUIElement | string)[], constructor(header: (BaseUIElement | string)[],
contents: (BaseUIElement | string)[][], contents: (BaseUIElement | string)[][],
contentStyle?: string[][]) { options?: {
contentStyle?: string[][],
sortable?: false | boolean
}) {
super(); super();
this._contentStyle = contentStyle ?? [["min-width: 9rem"]]; this._contentStyle = options?.contentStyle ?? [["min-width: 9rem"]];
this._header = header?.map(Translations.W); this._header = header?.map(Translations.W);
this._contents = contents.map(row => row.map(Translations.W)); this._contents = contents.map(row => row.map(Translations.W));
this._sortable = options?.sortable ?? false
} }
AsMarkdown(): string { AsMarkdown(): string {
@ -30,7 +36,25 @@ export default class Table extends BaseUIElement {
protected InnerConstructElement(): HTMLElement { protected InnerConstructElement(): HTMLElement {
const table = document.createElement("table") const table = document.createElement("table")
const headerElems = Utils.NoNull((this._header ?? []).map(elems => elems.ConstructElement())) /**
* Sortmode: i: sort column i ascending;
* if i is negative : sort column (-i - 1) descending
*/
const sortmode = new UIEventSource<number>(undefined);
const self = this;
const headerElems = Utils.NoNull((this._header ?? []).map((elem, i) => {
if(self._sortable){
elem.onClick(() => {
const current = sortmode.data
if(current == i){
sortmode.setData(- 1 - i )
}else{
sortmode.setData(i)
}
})
}
return elem.ConstructElement();
}))
if (headerElems.length > 0) { if (headerElems.length > 0) {
const thead = document.createElement("thead") const thead = document.createElement("thead")
@ -73,6 +97,31 @@ export default class Table extends BaseUIElement {
} }
table.appendChild(tr) table.appendChild(tr)
} }
sortmode.addCallback(sortCol => {
if(sortCol === undefined){
return
}
const descending = sortCol < 0
const col = descending ? - sortCol - 1: sortCol;
let rows: HTMLTableRowElement[] = Array.from(table.rows)
rows.splice(0,1) // remove header row
rows = rows.sort((a, b) => {
const ac = a.cells[col]?.innerText?.toLowerCase()
const bc = b.cells[col]?.innerText?.toLowerCase()
if(ac === bc){
return 0
}
return( ac < bc !== descending) ? -1 : 1;
})
for (let j = rows.length ; j > 1; j--) {
table.deleteRow(j)
}
for (const row of rows) {
table.appendChild(row)
}
})
return table; return table;
} }

View file

@ -128,8 +128,9 @@ export default class Histogram<T> extends VariableUiElement {
.SetStyle(`background: ${actualAssignColor(key)}; width: ${100 * counts.get(key) / max}%`) .SetStyle(`background: ${actualAssignColor(key)}; width: ${100 * counts.get(key) / max}%`)
]).SetClass("block w-full") ]).SetClass("block w-full")
]), ]),{
keys.map(_ => ["width: 20%"]) contentStyle:keys.map(_ => ["width: 20%"])
}
).SetClass("w-full zebra-table"); ).SetClass("w-full zebra-table");
}, [sortMode])); }, [sortMode]));
} }

View file

@ -139,16 +139,59 @@ class MassAction extends Combine {
} }
class NoteTable extends Combine {
constructor(noteStates: NoteState[], state?: UserRelatedState) {
const typicalComment = noteStates[0].props.comments[0].html
const table = new Table(
["id", "status", "last comment", "last modified by"],
noteStates.map(ns => {
const link = new Link(
"" + ns.props.id,
"https://openstreetmap.org/note/" + ns.props.id, true
)
let last_comment = "";
const last_comment_props = ns.props.comments[ns.props.comments.length - 1]
const before_last_comment = ns.props.comments[ns.props.comments.length - 2]
if (ns.props.comments.length > 1) {
last_comment = last_comment_props.text
if (last_comment === undefined && before_last_comment?.uid === last_comment_props.uid) {
last_comment = before_last_comment.text
}
}
const statusIcon = BatchView.icons[ns.status]().SetClass("h-4 w-4 shrink-0")
return [link, new Combine([statusIcon, ns.status]).SetClass("flex"), last_comment,
new Link(last_comment_props.user, "https://www.openstreetmap.org/user/" + last_comment_props.user, true)
]
}),
{sortable: true}
).SetClass("zebra-table link-underline");
super([
new Title("Mass apply an action on " + noteStates.length + " notes below"),
state !== undefined ? new MassAction(state, noteStates.map(ns => ns.props)).SetClass("block") : undefined,
table,
new Title("Example note", 4),
new FixedUiElement(typicalComment).SetClass("literal-code link-underline"),
])
this.SetClass("flex flex-col")
}
}
class BatchView extends Toggleable { class BatchView extends Toggleable {
private static icons = { public static icons = {
open: Svg.compass_svg, open: Svg.compass_svg,
has_comments: Svg.speech_bubble_svg, has_comments: Svg.speech_bubble_svg,
imported: Svg.addSmall_svg, imported: Svg.addSmall_svg,
already_mapped: Svg.checkmark_svg, already_mapped: Svg.checkmark_svg,
invalid: Svg.invalid_svg,
closed: Svg.close_svg,
not_found: Svg.not_found_svg, not_found: Svg.not_found_svg,
closed: Svg.close_svg,
invalid: Svg.invalid_svg,
} }
constructor(noteStates: NoteState[], state?: UserRelatedState) { constructor(noteStates: NoteState[], state?: UserRelatedState) {
@ -164,14 +207,44 @@ class BatchView extends Toggleable {
statusHist.set(st, c + 1) statusHist.set(st, c + 1)
} }
const badges: (BaseUIElement)[] = [new FixedUiElement(dateStr).SetClass("literal-code rounded-full")] const unresolvedTotal = (statusHist.get("open") ?? 0) + (statusHist.get("has_comments") ?? 0)
statusHist.forEach((count, status) => { const badges: (BaseUIElement)[] = [
const icon = BatchView.icons[status]().SetClass("h-6 m-1") new FixedUiElement(dateStr).SetClass("literal-code rounded-full"),
badges.push(new Combine([icon, count + " " + status]) new FixedUiElement(noteStates.length + " total").SetClass("literal-code rounded-full ml-1 border-4 border-gray")
.SetClass("flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border border-black")) .onClick(() => filterOn.setData(undefined)),
unresolvedTotal === 0 ?
new Combine([Svg.party_svg().SetClass("h-6 m-1"), "All done!"])
.SetClass("flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border border-black") :
new FixedUiElement(Math.round(100 - 100 * unresolvedTotal / noteStates.length) + "%").SetClass("literal-code rounded-full ml-1")
]
const filterOn = new UIEventSource<string>(undefined)
Object.keys(BatchView.icons).forEach(status => {
const count = statusHist.get(status)
if (count === undefined) {
return undefined
}
const normal = new Combine([BatchView.icons[status]().SetClass("h-6 m-1"), count + " " + status])
.SetClass("flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border border-black")
const selected = new Combine([BatchView.icons[status]().SetClass("h-6 m-1"), count + " " + status])
.SetClass("flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border-4 border-black animate-pulse")
const toggle = new Toggle(selected, normal, filterOn.map(f => f === status, [], (selected, previous) => {
if (selected) {
return status;
}
if (previous === status) {
return undefined
}
return previous
})).ToggleOnClick()
badges.push(toggle)
}) })
const typicalComment = noteStates[0].props.comments[0].html
const fullTable = new NoteTable(noteStates, state);
super( super(
@ -179,27 +252,12 @@ class BatchView extends Toggleable {
new Title(theme + ": " + intro, 2), new Title(theme + ": " + intro, 2),
new Combine(badges).SetClass("flex flex-wrap"), new Combine(badges).SetClass("flex flex-wrap"),
]), ]),
new Combine([ new VariableUiElement(filterOn.map(filter => {
new Title("Example note", 4), if (filter === undefined) {
new FixedUiElement(typicalComment).SetClass("literal-code link-underline"), return fullTable
new Title("Mass apply an action"), }
state !== undefined ? new MassAction(state, noteStates.map(ns => ns.props)).SetClass("block") : undefined, return new NoteTable(noteStates.filter(ns => ns.status === filter), state)
new Table( })),
["id", "status", "last comment"],
noteStates.map(ns => {
const link = new Link(
"" + ns.props.id,
"https://openstreetmap.org/note/" + ns.props.id, true
)
let last_comment = "";
if (ns.props.comments.length > 1) {
last_comment = ns.props.comments[ns.props.comments.length - 1].text
}
const statusIcon = BatchView.icons[ns.status]().SetClass("h-4 w-4 shrink-0")
return [link, new Combine([statusIcon, ns.status]).SetClass("flex"), last_comment]
})
).SetClass("zebra-table link-underline")
]).SetClass("flex flex-col"),
{ {
closeOnClick: false closeOnClick: false
}) })

View file

@ -168,7 +168,7 @@ export default class OpeningHoursVisualization extends Toggle {
} }
return new Table(undefined, return new Table(undefined,
[["&nbsp", header], ...weekdays], [["&nbsp", header], ...weekdays],
[["width: 5%", `position: relative; height: ${headerHeight}`], ...weekdayStyles] {contentStyle: [["width: 5%", `position: relative; height: ${headerHeight}`], ...weekdayStyles]}
).SetClass("w-full") ).SetClass("w-full")
.SetStyle("border-collapse: collapse; word-break; word-break: normal; word-wrap: normal") .SetStyle("border-collapse: collapse; word-break; word-break: normal; word-wrap: normal")

View file

@ -156,22 +156,26 @@ class ApplyButton extends UIElement {
export default class AutoApplyButton implements SpecialVisualization { export default class AutoApplyButton implements SpecialVisualization {
public readonly docs: string; public readonly docs: string;
public readonly funcName: string = "auto_apply"; public readonly funcName: string = "auto_apply";
public readonly args: { name: string; defaultValue?: string; doc: string }[] = [ public readonly args: { name: string; defaultValue?: string; doc: string, required?: boolean }[] = [
{ {
name: "target_layer", name: "target_layer",
doc: "The layer that the target features will reside in" doc: "The layer that the target features will reside in",
required: true
}, },
{ {
name: "target_feature_ids", name: "target_feature_ids",
doc: "The key, of which the value contains a list of ids" doc: "The key, of which the value contains a list of ids",
required: true
}, },
{ {
name: "tag_rendering_id", name: "tag_rendering_id",
doc: "The ID of the tagRendering containing the autoAction. This tagrendering will be calculated. The embedded actions will be executed" doc: "The ID of the tagRendering containing the autoAction. This tagrendering will be calculated. The embedded actions will be executed",
required: true
}, },
{ {
name: "text", name: "text",
doc: "The text to show on the button" doc: "The text to show on the button",
required: true
}, },
{ {
name: "icon", name: "icon",

View file

@ -42,6 +42,7 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {Changes} from "../../Logic/Osm/Changes"; import {Changes} from "../../Logic/Osm/Changes";
import {ElementStorage} from "../../Logic/ElementStorage"; import {ElementStorage} from "../../Logic/ElementStorage";
import Hash from "../../Logic/Web/Hash"; import Hash from "../../Logic/Web/Hash";
import {PreciseInput} from "../../Models/ThemeConfig/PresetConfig";
/** /**
* A helper class for the various import-flows. * A helper class for the various import-flows.
@ -54,7 +55,7 @@ abstract class AbstractImportButton implements SpecialVisualizations {
public readonly args: { name: string, defaultValue?: string, doc: string }[] public readonly args: { name: string, defaultValue?: string, doc: string }[]
private readonly showRemovedTags: boolean; private readonly showRemovedTags: boolean;
constructor(funcName: string, docsIntro: string, extraArgs: { name: string, doc: string, defaultValue?: string }[], showRemovedTags = true) { constructor(funcName: string, docsIntro: string, extraArgs: { name: string, doc: string, defaultValue?: string, required?: boolean }[], showRemovedTags = true) {
this.funcName = funcName this.funcName = funcName
this.showRemovedTags = showRemovedTags; this.showRemovedTags = showRemovedTags;
@ -73,11 +74,13 @@ ${Utils.special_visualizations_importRequirementDocs}
this.args = [ this.args = [
{ {
name: "targetLayer", name: "targetLayer",
doc: "The id of the layer where this point should end up. This is not very strict, it will simply result in checking that this layer is shown preventing possible duplicate elements" doc: "The id of the layer where this point should end up. This is not very strict, it will simply result in checking that this layer is shown preventing possible duplicate elements",
required: true
}, },
{ {
name: "tags", name: "tags",
doc: "The tags to add onto the new object - see specification above. If this is a key (a single word occuring in the properties of the object), the corresponding value is taken and expanded instead" doc: "The tags to add onto the new object - see specification above. If this is a key (a single word occuring in the properties of the object), the corresponding value is taken and expanded instead",
required: true
}, },
{ {
name: "text", name: "text",
@ -394,6 +397,43 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
) )
} }
private static CreateAction(feature,
args: { max_snap_distance: string; snap_onto_layers: string; icon: string; text: string; tags: string; newTags: UIEventSource<any>; targetLayer: string },
state: FeaturePipelineState,
mergeConfigs: any[]) {
const coors = feature.geometry.coordinates
if ((feature.geometry.type === "Polygon") && coors.length > 1) {
const outer = coors[0]
const inner = [...coors]
inner.splice(0, 1)
return new CreateMultiPolygonWithPointReuseAction(
args.newTags.data,
outer,
inner,
state,
mergeConfigs,
"import"
)
} else if (feature.geometry.type === "Polygon") {
const outer = coors[0]
return new CreateWayWithPointReuseAction(
args.newTags.data,
outer,
state,
mergeConfigs
)
} else if (feature.geometry.type === "LineString") {
return new CreateWayWithPointReuseAction(
args.newTags.data,
coors,
state,
mergeConfigs
)
} else {
throw "Unsupported type"
}
}
async applyActionOn(state: { layoutToUse: LayoutConfig; changes: Changes, allElements: ElementStorage }, async applyActionOn(state: { layoutToUse: LayoutConfig; changes: Changes, allElements: ElementStorage },
originalFeatureTags: UIEventSource<any>, originalFeatureTags: UIEventSource<any>,
argument: string[]): Promise<void> { argument: string[]): Promise<void> {
@ -484,43 +524,6 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
return mergeConfigs; return mergeConfigs;
} }
private static CreateAction(feature,
args: { max_snap_distance: string; snap_onto_layers: string; icon: string; text: string; tags: string; newTags: UIEventSource<any>; targetLayer: string },
state: FeaturePipelineState,
mergeConfigs: any[]) {
const coors = feature.geometry.coordinates
if ((feature.geometry.type === "Polygon" ) && coors.length > 1) {
const outer = coors[0]
const inner = [...coors]
inner.splice(0, 1)
return new CreateMultiPolygonWithPointReuseAction(
args.newTags.data,
outer,
inner,
state,
mergeConfigs,
"import"
)
} else if(feature.geometry.type === "Polygon"){
const outer = coors[0]
return new CreateWayWithPointReuseAction(
args.newTags.data,
outer,
state,
mergeConfigs
)
}else if(feature.geometry.type === "LineString"){
return new CreateWayWithPointReuseAction(
args.newTags.data,
coors,
state,
mergeConfigs
)
}else{
throw "Unsupported type"
}
}
} }
export class ImportPointButton extends AbstractImportButton { export class ImportPointButton extends AbstractImportButton {
@ -528,18 +531,23 @@ export class ImportPointButton extends AbstractImportButton {
constructor() { constructor() {
super("import_button", super("import_button",
"This button will copy the point from an external dataset into OpenStreetMap", "This button will copy the point from an external dataset into OpenStreetMap",
[{ [
name: "snap_onto_layers", {
doc: "If a way of the given layer(s) is closeby, will snap the new point onto this way (similar as preset might snap). To show multiple layers to snap onto, use a `;`-seperated list" name: "snap_onto_layers",
}, doc: "If a way of the given layer(s) is closeby, will snap the new point onto this way (similar as preset might snap). To show multiple layers to snap onto, use a `;`-seperated list"
},
{ {
name: "max_snap_distance", name: "max_snap_distance",
doc: "The maximum distance that the imported point will be moved to snap onto a way in an already existing layer (in meters). This is previewed to the contributor, similar to the 'add new point'-action of MapComplete", doc: "The maximum distance that the imported point will be moved to snap onto a way in an already existing layer (in meters). This is previewed to the contributor, similar to the 'add new point'-action of MapComplete",
defaultValue: "5" defaultValue: "5"
}, { },
name: "note_id", {
doc: "If given, this key will be read. The corresponding note on OSM will be closed, stating 'imported'" name: "note_id",
}], doc: "If given, this key will be read. The corresponding note on OSM will be closed, stating 'imported'"
},
{name:"location_picker",
defaultValue: "photo",
doc: "Chooses the background for the precise location picker, options are 'map', 'photo' or 'osmbasedmap' or 'none' if the precise input picker should be disabled"}],
false false
) )
} }
@ -581,7 +589,7 @@ export class ImportPointButton extends AbstractImportButton {
newElementAction.newElementId newElementAction.newElementId
)) ))
Hash.hash.setData(newElementAction.newElementId) Hash.hash.setData(newElementAction.newElementId)
if (note_id !== undefined) { if (note_id !== undefined) {
state.osmConnection.closeNote(note_id, "imported") state.osmConnection.closeNote(note_id, "imported")
originalFeatureTags.data["closed_at"] = new Date().toISOString() originalFeatureTags.data["closed_at"] = new Date().toISOString()
@ -589,16 +597,24 @@ export class ImportPointButton extends AbstractImportButton {
} }
} }
let preciseInputOption = args["location_picker"]
let preciseInputSpec: PreciseInput = undefined
console.log("Precise input location is ", preciseInputOption)
if(preciseInputOption !== "none") {
preciseInputSpec = {
snapToLayers: args.snap_onto_layers?.split(";"),
maxSnapDistance: Number(args.max_snap_distance),
preferredBackground: args["location_picker"] ?? ["photo", "map"]
}
}
const presetInfo = <PresetInfo>{ const presetInfo = <PresetInfo>{
tags: args.newTags.data, tags: args.newTags.data,
icon: () => new Img(args.icon), icon: () => new Img(args.icon),
layerToAddTo: state.filteredLayers.data.filter(l => l.layerDef.id === args.targetLayer)[0], layerToAddTo: state.filteredLayers.data.filter(l => l.layerDef.id === args.targetLayer)[0],
name: args.text, name: args.text,
title: Translations.WT(args.text), title: Translations.WT(args.text),
preciseInput: { preciseInput: preciseInputSpec, // must be explicitely assigned, if 'undefined' won't work otherwise
snapToLayers: args.snap_onto_layers?.split(";"),
maxSnapDistance: Number(args.max_snap_distance)
},
boundsFactor: 3 boundsFactor: 3
} }

View file

@ -52,7 +52,7 @@ export interface SpecialVisualization {
constr: ((state: FeaturePipelineState, tagSource: UIEventSource<any>, argument: string[], guistate: DefaultGuiState,) => BaseUIElement), constr: ((state: FeaturePipelineState, tagSource: UIEventSource<any>, argument: string[], guistate: DefaultGuiState,) => BaseUIElement),
docs: string, docs: string,
example?: string, example?: string,
args: { name: string, defaultValue?: string, doc: string }[], args: { name: string, defaultValue?: string, doc: string, required?: false | boolean }[],
getLayerDependencies?: (argument: string[]) => string[] getLayerDependencies?: (argument: string[]) => string[]
} }
@ -102,6 +102,7 @@ class CloseNoteButton implements SpecialVisualization {
{ {
name: "text", name: "text",
doc: "Text to show on this button", doc: "Text to show on this button",
required: true
}, },
{ {
name: "icon", name: "icon",
@ -179,7 +180,7 @@ class CloseNoteButton implements SpecialVisualization {
export default class SpecialVisualizations { export default class SpecialVisualizations {
public static specialVisualizations = SpecialVisualizations.init() public static specialVisualizations : SpecialVisualization[] = SpecialVisualizations.init()
public static HelpMessage() { public static HelpMessage() {
@ -206,9 +207,28 @@ export default class SpecialVisualizations {
)); ));
return new Combine([ return new Combine([
new Combine([
new Title("Special tag renderings", 1), new Title("Special tag renderings", 1),
"In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.", "In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.",
"General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args", "General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args",
new Title("Using expanded syntax",4),
`Instead of using \`{"render": {"en": "{some_special_visualisation(some_arg, some other really long message, more args)} , "nl": "{some_special_visualisation(some_arg, een boodschap in een andere taal, more args)}}, one can also write`,
new FixedUiElement(JSON.stringify({
render: {
special:{
type: "some_special_visualisation",
"argname": "some_arg",
"message":{
en:"some other really long message",
nl: "een boodschap in een andere taal"
},
"other_arg_name":"more args"
}
}
})).SetClass("code")
]).SetClass("flex flex-col"),
...helpTexts ...helpTexts
] ]
).SetClass("flex flex-col"); ).SetClass("flex flex-col");
@ -227,9 +247,9 @@ export default class SpecialVisualizations {
funcName: "image_carousel", funcName: "image_carousel",
docs: "Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)", docs: "Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)",
args: [{ args: [{
name: "image key/prefix (multiple values allowed if comma-seperated)", name: "image_key",
defaultValue: AllImageProviders.defaultKeys.join(","), defaultValue: AllImageProviders.defaultKeys.join(","),
doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... " doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... Multiple values are allowed if ';'-separated "
}], }],
constr: (state, tags, args) => { constr: (state, tags, args) => {
let imagePrefixes: string[] = undefined; let imagePrefixes: string[] = undefined;
@ -368,6 +388,7 @@ export default class SpecialVisualizations {
{ {
doc: "The side to show, either `left` or `right`", doc: "The side to show, either `left` or `right`",
name: "side", name: "side",
required: true
} }
], ],
example: "`{sided_minimap(left)}`", example: "`{sided_minimap(left)}`",
@ -461,12 +482,15 @@ export default class SpecialVisualizations {
docs: "Downloads a JSON from the given URL, e.g. '{live(example.org/data.json, shorthand:x.y.z, other:a.b.c, shorthand)}' will download the given file, will create an object {shorthand: json[x][y][z], other: json[a][b][c] out of it and will return 'other' or 'json[a][b][c]. This is made to use in combination with tags, e.g. {live({url}, {url:format}, needed_value)}", docs: "Downloads a JSON from the given URL, e.g. '{live(example.org/data.json, shorthand:x.y.z, other:a.b.c, shorthand)}' will download the given file, will create an object {shorthand: json[x][y][z], other: json[a][b][c] out of it and will return 'other' or 'json[a][b][c]. This is made to use in combination with tags, e.g. {live({url}, {url:format}, needed_value)}",
example: "{live({url},{url:format},hour)} {live(https://data.mobility.brussels/bike/api/counts/?request=live&featureID=CB2105,hour:data.hour_cnt;day:data.day_cnt;year:data.year_cnt,hour)}", example: "{live({url},{url:format},hour)} {live(https://data.mobility.brussels/bike/api/counts/?request=live&featureID=CB2105,hour:data.hour_cnt;day:data.day_cnt;year:data.year_cnt,hour)}",
args: [{ args: [{
name: "Url", doc: "The URL to load" name: "Url",
doc: "The URL to load",
required: true
}, { }, {
name: "Shorthands", name: "Shorthands",
doc: "A list of shorthands, of the format 'shorthandname:path.path.path'. separated by ;" doc: "A list of shorthands, of the format 'shorthandname:path.path.path'. separated by ;"
}, { }, {
name: "path", doc: "The path (or shorthand) that should be returned" name: "path",
doc: "The path (or shorthand) that should be returned"
}], }],
constr: (state, tagSource: UIEventSource<any>, args) => { constr: (state, tagSource: UIEventSource<any>, args) => {
const url = args[0]; const url = args[0];
@ -483,7 +507,8 @@ export default class SpecialVisualizations {
args: [ args: [
{ {
name: "key", name: "key",
doc: "The key to be read and to generate a histogram from" doc: "The key to be read and to generate a histogram from",
required: true
}, },
{ {
name: "title", name: "title",
@ -588,7 +613,8 @@ export default class SpecialVisualizations {
example: "{canonical(length)} will give 42 metre (in french)", example: "{canonical(length)} will give 42 metre (in french)",
args: [{ args: [{
name: "key", name: "key",
doc: "The key of the tag to give the canonical text for" doc: "The key of the tag to give the canonical text for",
required: true
}], }],
constr: (state, tagSource, args) => { constr: (state, tagSource, args) => {
const key = args [0] const key = args [0]
@ -618,16 +644,19 @@ export default class SpecialVisualizations {
{name: "feature_ids", doc: "A JSON-serialized list of IDs of features to apply the tagging on"}, {name: "feature_ids", doc: "A JSON-serialized list of IDs of features to apply the tagging on"},
{ {
name: "keys", name: "keys",
doc: "One key (or multiple keys, seperated by ';') of the attribute that should be copied onto the other features." doc: "One key (or multiple keys, seperated by ';') of the attribute that should be copied onto the other features.",
required: true
}, },
{name: "text", doc: "The text to show on the button"}, {name: "text", doc: "The text to show on the button"},
{ {
name: "autoapply", name: "autoapply",
doc: "A boolean indicating wether this tagging should be applied automatically if the relevant tags on this object are changed. A visual element indicating the multi_apply is still shown" doc: "A boolean indicating wether this tagging should be applied automatically if the relevant tags on this object are changed. A visual element indicating the multi_apply is still shown",
required: true
}, },
{ {
name: "overwrite", name: "overwrite",
doc: "If set to 'true', the tags on the other objects will always be overwritten. The default behaviour will be to only change the tags on other objects if they are either undefined or had the same value before the change" doc: "If set to 'true', the tags on the other objects will always be overwritten. The default behaviour will be to only change the tags on other objects if they are either undefined or had the same value before the change",
required: true
} }
], ],
example: "{multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology information on all nearby objects with the same name)}", example: "{multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology information on all nearby objects with the same name)}",
@ -918,7 +947,7 @@ export default class SpecialVisualizations {
] ]
specialVisualizations.push(new AutoApplyButton(specialVisualizations)) specialVisualizations.push(new AutoApplyButton(specialVisualizations))
return specialVisualizations; return specialVisualizations;
} }

View file

@ -407,7 +407,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
* *
* If a list is encountered, this is tranparently walked recursively on every object. * If a list is encountered, this is tranparently walked recursively on every object.
* *
* The leaf objects are replaced by the function * The leaf objects are replaced in the object itself by the specified function
*/ */
public static WalkPath(path: string[], object: any, replaceLeaf: ((leaf: any, travelledPath: string[]) => any), travelledPath: string[] = []) { public static WalkPath(path: string[], object: any, replaceLeaf: ((leaf: any, travelledPath: string[]) => any), travelledPath: string[] = []) {
const head = path[0] const head = path[0]
@ -793,6 +793,12 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
} }
return new Date(str) return new Date(str)
} }
public static sortedByLevenshteinDistance<T>(reference: string, ts: T[], getName: (t:T) => string): T[]{
const withDistance: [T, number][] = ts.map(t => [t, Utils.levenshteinDistance(getName(t), reference)])
withDistance.sort(([_, a], [__, b]) => a - b)
return withDistance.map(n => n[0])
}
public static levenshteinDistance(str1: string, str2: string) { public static levenshteinDistance(str1: string, str2: string) {
const track = Array(str2.length + 1).fill(null).map(() => const track = Array(str2.length + 1).fill(null).map(() =>

View file

@ -85,7 +85,7 @@
"iconBadges": [ "iconBadges": [
{ {
"if": "_total_comments>1", "if": "_total_comments>1",
"then": "speech_bubble" "then": "circle:white;speech_bubble"
} }
] ]
} }

View file

@ -915,6 +915,16 @@
"https://www.OpenStreetMap.org" "https://www.OpenStreetMap.org"
] ]
}, },
{
"path": "party.svg",
"license": "CC-BY 4.0",
"authors": [
"Twemoji"
],
"sources": [
"https://github.com/twitter/twemoji"
]
},
{ {
"path": "payment_card.svg", "path": "payment_card.svg",
"license": "CC0", "license": "CC0",

1
assets/svg/party.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#DD2E44" d="M11.626 7.488c-.112.112-.197.247-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269 1.562-1.562-.971-6.627-5.656-11.313-4.687-4.686-9.752-7.218-11.315-5.656z"/><path fill="#EA596E" d="M13 12L.416 32.506l-.282.635.011.011c-.208.403.14 1.223.853 1.937.232.232.473.408.709.557L17 17l-4-5z"/><path fill="#A0041E" d="M23.012 13.066c4.67 4.672 7.263 9.652 5.789 11.124-1.473 1.474-6.453-1.118-11.126-5.788-4.671-4.672-7.263-9.654-5.79-11.127 1.474-1.473 6.454 1.119 11.127 5.791z"/><path fill="#AA8DD8" d="M18.59 13.609c-.199.161-.459.245-.734.215-.868-.094-1.598-.396-2.109-.873-.541-.505-.808-1.183-.735-1.862.128-1.192 1.324-2.286 3.363-2.066.793.085 1.147-.17 1.159-.292.014-.121-.277-.446-1.07-.532-.868-.094-1.598-.396-2.11-.873-.541-.505-.809-1.183-.735-1.862.13-1.192 1.325-2.286 3.362-2.065.578.062.883-.057 1.012-.134.103-.063.144-.123.148-.158.012-.121-.275-.446-1.07-.532-.549-.06-.947-.552-.886-1.102.059-.549.55-.946 1.101-.886 2.037.219 2.973 1.542 2.844 2.735-.13 1.194-1.325 2.286-3.364 2.067-.578-.063-.88.057-1.01.134-.103.062-.145.123-.149.157-.013.122.276.446 1.071.532 2.037.22 2.973 1.542 2.844 2.735-.129 1.192-1.324 2.286-3.362 2.065-.578-.062-.882.058-1.012.134-.104.064-.144.124-.148.158-.013.121.276.446 1.07.532.548.06.947.553.886 1.102-.028.274-.167.511-.366.671z"/><path fill="#77B255" d="M30.661 22.857c1.973-.557 3.334.323 3.658 1.478.324 1.154-.378 2.615-2.35 3.17-.77.216-1.001.584-.97.701.034.118.425.312 1.193.095 1.972-.555 3.333.325 3.657 1.479.326 1.155-.378 2.614-2.351 3.17-.769.216-1.001.585-.967.702.033.117.423.311 1.192.095.53-.149 1.084.16 1.233.691.148.532-.161 1.084-.693 1.234-1.971.555-3.333-.323-3.659-1.479-.324-1.154.379-2.613 2.353-3.169.77-.217 1.001-.584.967-.702-.032-.117-.422-.312-1.19-.096-1.974.556-3.334-.322-3.659-1.479-.325-1.154.378-2.613 2.351-3.17.768-.215.999-.585.967-.701-.034-.118-.423-.312-1.192-.096-.532.15-1.083-.16-1.233-.691-.149-.53.161-1.082.693-1.232z"/><path fill="#AA8DD8" d="M23.001 20.16c-.294 0-.584-.129-.782-.375-.345-.432-.274-1.061.156-1.406.218-.175 5.418-4.259 12.767-3.208.547.078.927.584.849 1.131-.078.546-.58.93-1.132.848-6.493-.922-11.187 2.754-11.233 2.791-.186.148-.406.219-.625.219z"/><path fill="#77B255" d="M5.754 16c-.095 0-.192-.014-.288-.042-.529-.159-.829-.716-.67-1.245 1.133-3.773 2.16-9.794.898-11.364-.141-.178-.354-.353-.842-.316-.938.072-.849 2.051-.848 2.071.042.551-.372 1.031-.922 1.072-.559.034-1.031-.372-1.072-.923-.103-1.379.326-4.035 2.692-4.214 1.056-.08 1.933.287 2.552 1.057 2.371 2.951-.036 11.506-.542 13.192-.13.433-.528.712-.958.712z"/><circle fill="#5C913B" cx="25.5" cy="9.5" r="1.5"/><circle fill="#9266CC" cx="2" cy="18" r="2"/><circle fill="#5C913B" cx="32.5" cy="19.5" r="1.5"/><circle fill="#5C913B" cx="23.5" cy="31.5" r="1.5"/><circle fill="#FFCC4D" cx="28" cy="4" r="2"/><circle fill="#FFCC4D" cx="32.5" cy="8.5" r="1.5"/><circle fill="#FFCC4D" cx="29.5" cy="12.5" r="1.5"/><circle fill="#FFCC4D" cx="7.5" cy="23.5" r="1.5"/></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -144,6 +144,72 @@
"https://mapcomplete.osm.be/toerisme_vlaanderen" "https://mapcomplete.osm.be/toerisme_vlaanderen"
] ]
}, },
{
"path": "pin je punt1.jpg",
"license": "CC0",
"authors": [
"Toerisme Vlaanderen"
],
"sources": [
"https://toerismevlaanderen.be/pinjepunt",
"https://mapcomplete.osm.be/toerisme_vlaanderenn"
]
},
{
"path": "pin je punt2.jpg",
"license": "CC0",
"authors": [
"Toerisme Vlaanderen"
],
"sources": [
"https://toerismevlaanderen.be/pinjepunt",
"https://mapcomplete.osm.be/toerisme_vlaanderenn"
]
},
{
"path": "pin je punt3.jpg",
"license": "CC0",
"authors": [
"Toerisme Vlaanderen"
],
"sources": [
"https://toerismevlaanderen.be/pinjepunt",
"https://mapcomplete.osm.be/toerisme_vlaanderenn"
]
},
{
"path": "pin je punt4.jpg",
"license": "CC0",
"authors": [
"Toerisme Vlaanderen"
],
"sources": [
"https://toerismevlaanderen.be/pinjepunt",
"https://mapcomplete.osm.be/toerisme_vlaanderenn"
]
},
{
"path": "pin je punt5.jpg",
"license": "CC0",
"authors": [
"Toerisme Vlaanderen"
],
"sources": [
"https://toerismevlaanderen.be/pinjepunt",
"https://mapcomplete.osm.be/toerisme_vlaanderenn"
]
},
{
"path": "pin je punt6.jpg",
"license": "CC0",
"authors": [
"Toerisme Vlaanderen"
],
"sources": [
"https://toerismevlaanderen.be/pinjepunt",
"https://mapcomplete.osm.be/toerisme_vlaanderenn"
]
},
{ {
"path": "playground.svg", "path": "playground.svg",
"license": "CC0", "license": "CC0",

View file

@ -123,7 +123,16 @@
}, },
{ {
"id": "uk_addresses_import_button", "id": "uk_addresses_import_button",
"render": "{import_button(address,urpn_count=$urpn_count;ref:GB:uprn=$ref:GB:uprn$, Add this address, ./assets/themes/uk_addresses/housenumber_add.svg)}" "render":{
"special": {
"type": "import_button",
"targetLayer": "address",
"tags": "urpn_count=$urpn_count;ref:GB:uprn=$ref:GB:uprn$",
"text": "Add this address",
"icon": "./assets/themes/uk_addresses/housenumber_add.svg",
"location_picker": "none"
}
}
} }
], ],
"calculatedTags": [ "calculatedTags": [

View file

@ -706,6 +706,7 @@ video {
} }
.sticky { .sticky {
position: -webkit-sticky;
position: sticky; position: sticky;
} }
@ -1504,10 +1505,6 @@ video {
padding: 0.125rem; padding: 0.125rem;
} }
.p-8 {
padding: 2rem;
}
.px-0 { .px-0 {
padding-left: 0px; padding-left: 0px;
padding-right: 0px; padding-right: 0px;

View file

@ -1,21 +1,15 @@
import {describe} from 'mocha' import {describe} from 'mocha'
import {expect} from 'chai' import {expect} from 'chai'
import {LayoutConfigJson} from "../../../../Models/ThemeConfig/Json/LayoutConfigJson";
import {LayerConfigJson} from "../../../../Models/ThemeConfig/Json/LayerConfigJson"; import {LayerConfigJson} from "../../../../Models/ThemeConfig/Json/LayerConfigJson";
import {PrepareTheme} from "../../../../Models/ThemeConfig/Conversion/PrepareTheme";
import {TagRenderingConfigJson} from "../../../../Models/ThemeConfig/Json/TagRenderingConfigJson"; import {TagRenderingConfigJson} from "../../../../Models/ThemeConfig/Json/TagRenderingConfigJson";
import LayoutConfig from "../../../../Models/ThemeConfig/LayoutConfig";
import * as bookcaseLayer from "../../../../assets/generated/layers/public_bookcase.json"
import LayerConfig from "../../../../Models/ThemeConfig/LayerConfig";
import {ExtractImages} from "../../../../Models/ThemeConfig/Conversion/FixImages";
import * as cyclofix from "../../../../assets/generated/themes/cyclofix.json"
import LineRenderingConfigJson from "../../../../Models/ThemeConfig/Json/LineRenderingConfigJson"; import LineRenderingConfigJson from "../../../../Models/ThemeConfig/Json/LineRenderingConfigJson";
import {PrepareLayer} from "../../../../Models/ThemeConfig/Conversion/PrepareLayer"; import {PrepareLayer, RewriteSpecial} from "../../../../Models/ThemeConfig/Conversion/PrepareLayer";
import {
QuestionableTagRenderingConfigJson
} from "../../../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson";
describe("PrepareLayer", () => { describe("PrepareLayer", () => {
it("should expand mappings in map renderings", () => { it("should expand mappings in map renderings", () => {
const exampleLayer: LayerConfigJson = { const exampleLayer: LayerConfigJson = {
id: "testlayer", id: "testlayer",
@ -66,10 +60,12 @@ describe("PrepareLayer", () => {
"if": "parking:condition:left=free", "if": "parking:condition:left=free",
"then": "#299921" "then": "#299921"
}, },
{"if": "parking:condition:left=disc", {
"then": "#219991"}] "if": "parking:condition:left=disc",
"then": "#219991"
}]
}, },
"offset": -6 "offset": -6
}, { }, {
"color": { "color": {
"render": "#888", "render": "#888",
@ -77,8 +73,10 @@ describe("PrepareLayer", () => {
"if": "parking:condition:right=free", "if": "parking:condition:right=free",
"then": "#299921" "then": "#299921"
}, },
{"if": "parking:condition:right=disc", {
"then": "#219991"}] "if": "parking:condition:right=disc",
"then": "#219991"
}]
}, },
"offset": 6 "offset": 6
}], }],
@ -91,3 +89,26 @@ describe("PrepareLayer", () => {
) )
}) })
describe('RewriteSpecial', function () {
it("should rewrite the UK import button", () => {
const tr = <QuestionableTagRenderingConfigJson>{
"id": "uk_addresses_import_button",
"render": {
"special": {
"type": "import_button",
"targetLayer": "address",
"tags": "urpn_count=$urpn_count;ref:GB:uprn=$ref:GB:uprn$",
"text": "Add this address",
"icon": "./assets/themes/uk_addresses/housenumber_add.svg",
"location_picker": "none"
}
}
}
const r = new RewriteSpecial().convert(tr, "test").result
expect(r).to.deep.eq({
"id": "uk_addresses_import_button",
"render": {'*': "{import_button(address,urpn_count=$urpn_count;ref:GB:uprn=$ref:GB:uprn$,Add this address,./assets/themes/uk_addresses/housenumber_add.svg,,,,none)}"}
})
})
});

View file

@ -0,0 +1,19 @@
import {describe} from 'mocha'
import SpecialVisualizations from "../../UI/SpecialVisualizations";
import {expect} from "chai";
describe("SpecialVisualisations", () => {
describe("predifined special visualisations", () => {
it("should not have an argument called 'type'", () => {
const specials = SpecialVisualizations.specialVisualizations
for (const special of specials) {
expect(special.funcName).not.eq('type', "A special visualisation is not allowed to be named 'type', as this will conflict with the 'special'-blocks")
for (const arg of special.args) {
expect(arg.name).not.eq('type', "An argument is not allowed to be called 'type', as this will conflict with the 'special'-blocks")
}
}
})
})
})