forked from MapComplete/MapComplete
Add rewrite of 'special' clauses, various QOLimprovements on import viewer
This commit is contained in:
parent
8df0324572
commit
c47a6d5ea7
22 changed files with 597 additions and 155 deletions
|
@ -181,6 +181,9 @@ class ChangesetDataTools {
|
|||
cs.properties.metadata.host = new URL(cs.properties.metadata.host).host
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
if(cs.properties.metadata["answer"] > 100){
|
||||
console.log("Lots of answers for https://osm.org/changeset/"+cs.id)
|
||||
}
|
||||
return cs
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ export abstract class Conversion<TIn, TOut> {
|
|||
|
||||
public convertAll(jsons: TIn[], context: string): { result: TOut[], errors: string[], warnings: string[], information?: string[] } {
|
||||
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 errors = []
|
||||
|
@ -72,17 +72,26 @@ export abstract class DesugaringStep<T> extends Conversion<T, T> {
|
|||
export class OnEvery<X, T> extends DesugaringStep<T> {
|
||||
private readonly key: string;
|
||||
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+")");
|
||||
this.step = step;
|
||||
this.key = key;
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
convert(json: T, context: string): { result: T; errors?: string[]; warnings?: string[], information?: string[] } {
|
||||
json = {...json}
|
||||
const step = this.step
|
||||
const key = this.key;
|
||||
if( this._options?.ignoreIfUndefined && json[key] === undefined){
|
||||
return {
|
||||
result: json,
|
||||
};
|
||||
}else{
|
||||
const r = step.convertAll((<X[]>json[key]), context + "." + key)
|
||||
json[key] = r.result
|
||||
return {
|
||||
|
@ -90,6 +99,8 @@ export class OnEvery<X, T> extends DesugaringStep<T> {
|
|||
result: json,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export class OnEveryConcat<X, T> extends DesugaringStep<T> {
|
||||
|
|
|
@ -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 {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson";
|
||||
import {Utils} from "../../../Utils";
|
||||
import RewritableConfigJson from "../Json/RewritableConfigJson";
|
||||
import SpecialVisualizations from "../../../UI/SpecialVisualizations";
|
||||
import Translations from "../../../UI/i18n/Translations";
|
||||
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[]> {
|
||||
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[]> {
|
||||
|
||||
private _rewrite = new ExpandRewrite<T>()
|
||||
|
||||
/**
|
||||
* Converts a 'special' translation into a regular translation which uses parameters
|
||||
* E.g.
|
||||
*
|
||||
* const tr = <TagRenderingJson> {
|
||||
* "special":
|
||||
* }
|
||||
*/
|
||||
export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
||||
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> {
|
||||
|
||||
|
||||
constructor(state: DesugaringContext) {
|
||||
super(
|
||||
"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 ExpandTagRendering(state)),
|
||||
new OnEveryConcat("mapRendering", new ExpandRewrite()),
|
||||
|
|
|
@ -234,7 +234,7 @@ export interface LayerConfigJson {
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
|
|
@ -197,14 +197,14 @@ export default class LayerConfig extends WithContextLoader {
|
|||
snapToLayers = pr.preciseInput.snapToLayer
|
||||
}
|
||||
|
||||
let preferredBackground: string[]
|
||||
let preferredBackground: ("map" | "photo" | "osmbasedmap" | "historicphoto" | string)[]
|
||||
if (typeof pr.preciseInput.preferredBackground === "string") {
|
||||
preferredBackground = [pr.preciseInput.preferredBackground]
|
||||
} else {
|
||||
preferredBackground = pr.preciseInput.preferredBackground
|
||||
}
|
||||
preciseInput = {
|
||||
preferredBackground: preferredBackground,
|
||||
preferredBackground,
|
||||
snapToLayers,
|
||||
maxSnapDistance: pr.preciseInput.maxSnapDistance ?? 10
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import {Translation} from "../../UI/i18n/Translation";
|
|||
import {Tag} from "../../Logic/Tags/Tag";
|
||||
|
||||
export interface PreciseInput {
|
||||
preferredBackground?: string[],
|
||||
preferredBackground?: ("map" | "photo" | "osmbasedmap" | "historicphoto" | string)[],
|
||||
snapToLayers?: string[],
|
||||
maxSnapDistance?: number
|
||||
}
|
||||
|
|
|
@ -1,20 +1,26 @@
|
|||
import BaseUIElement from "../BaseUIElement";
|
||||
import {Utils} from "../../Utils";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
|
||||
export default class Table extends BaseUIElement {
|
||||
|
||||
private readonly _header: BaseUIElement[];
|
||||
private readonly _contents: BaseUIElement[][];
|
||||
private readonly _contentStyle: string[][];
|
||||
private readonly _sortable: boolean;
|
||||
|
||||
constructor(header: (BaseUIElement | string)[],
|
||||
contents: (BaseUIElement | string)[][],
|
||||
contentStyle?: string[][]) {
|
||||
options?: {
|
||||
contentStyle?: string[][],
|
||||
sortable?: false | boolean
|
||||
}) {
|
||||
super();
|
||||
this._contentStyle = contentStyle ?? [["min-width: 9rem"]];
|
||||
this._contentStyle = options?.contentStyle ?? [["min-width: 9rem"]];
|
||||
this._header = header?.map(Translations.W);
|
||||
this._contents = contents.map(row => row.map(Translations.W));
|
||||
this._sortable = options?.sortable ?? false
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
|
@ -30,7 +36,25 @@ export default class Table extends BaseUIElement {
|
|||
protected InnerConstructElement(): HTMLElement {
|
||||
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) {
|
||||
|
||||
const thead = document.createElement("thead")
|
||||
|
@ -74,6 +98,31 @@ export default class Table extends BaseUIElement {
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -128,8 +128,9 @@ export default class Histogram<T> extends VariableUiElement {
|
|||
.SetStyle(`background: ${actualAssignColor(key)}; width: ${100 * counts.get(key) / max}%`)
|
||||
]).SetClass("block w-full")
|
||||
|
||||
]),
|
||||
keys.map(_ => ["width: 20%"])
|
||||
]),{
|
||||
contentStyle:keys.map(_ => ["width: 20%"])
|
||||
}
|
||||
).SetClass("w-full zebra-table");
|
||||
}, [sortMode]));
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
||||
private static icons = {
|
||||
public static icons = {
|
||||
open: Svg.compass_svg,
|
||||
has_comments: Svg.speech_bubble_svg,
|
||||
imported: Svg.addSmall_svg,
|
||||
already_mapped: Svg.checkmark_svg,
|
||||
invalid: Svg.invalid_svg,
|
||||
closed: Svg.close_svg,
|
||||
not_found: Svg.not_found_svg,
|
||||
closed: Svg.close_svg,
|
||||
invalid: Svg.invalid_svg,
|
||||
}
|
||||
|
||||
constructor(noteStates: NoteState[], state?: UserRelatedState) {
|
||||
|
@ -164,14 +207,44 @@ class BatchView extends Toggleable {
|
|||
statusHist.set(st, c + 1)
|
||||
}
|
||||
|
||||
const badges: (BaseUIElement)[] = [new FixedUiElement(dateStr).SetClass("literal-code rounded-full")]
|
||||
statusHist.forEach((count, status) => {
|
||||
const icon = BatchView.icons[status]().SetClass("h-6 m-1")
|
||||
badges.push(new Combine([icon, count + " " + status])
|
||||
.SetClass("flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border border-black"))
|
||||
const unresolvedTotal = (statusHist.get("open") ?? 0) + (statusHist.get("has_comments") ?? 0)
|
||||
const badges: (BaseUIElement)[] = [
|
||||
new FixedUiElement(dateStr).SetClass("literal-code rounded-full"),
|
||||
new FixedUiElement(noteStates.length + " total").SetClass("literal-code rounded-full ml-1 border-4 border-gray")
|
||||
.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(
|
||||
|
@ -179,27 +252,12 @@ class BatchView extends Toggleable {
|
|||
new Title(theme + ": " + intro, 2),
|
||||
new Combine(badges).SetClass("flex flex-wrap"),
|
||||
]),
|
||||
new Combine([
|
||||
new Title("Example note", 4),
|
||||
new FixedUiElement(typicalComment).SetClass("literal-code link-underline"),
|
||||
new Title("Mass apply an action"),
|
||||
state !== undefined ? new MassAction(state, noteStates.map(ns => ns.props)).SetClass("block") : undefined,
|
||||
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
|
||||
new VariableUiElement(filterOn.map(filter => {
|
||||
if (filter === undefined) {
|
||||
return fullTable
|
||||
}
|
||||
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"),
|
||||
return new NoteTable(noteStates.filter(ns => ns.status === filter), state)
|
||||
})),
|
||||
{
|
||||
closeOnClick: false
|
||||
})
|
||||
|
|
|
@ -168,7 +168,7 @@ export default class OpeningHoursVisualization extends Toggle {
|
|||
}
|
||||
return new Table(undefined,
|
||||
[[" ", header], ...weekdays],
|
||||
[["width: 5%", `position: relative; height: ${headerHeight}`], ...weekdayStyles]
|
||||
{contentStyle: [["width: 5%", `position: relative; height: ${headerHeight}`], ...weekdayStyles]}
|
||||
).SetClass("w-full")
|
||||
.SetStyle("border-collapse: collapse; word-break; word-break: normal; word-wrap: normal")
|
||||
|
||||
|
|
|
@ -156,22 +156,26 @@ class ApplyButton extends UIElement {
|
|||
export default class AutoApplyButton implements SpecialVisualization {
|
||||
public readonly docs: string;
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
doc: "The text to show on the button"
|
||||
doc: "The text to show on the button",
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: "icon",
|
||||
|
|
|
@ -42,6 +42,7 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
|||
import {Changes} from "../../Logic/Osm/Changes";
|
||||
import {ElementStorage} from "../../Logic/ElementStorage";
|
||||
import Hash from "../../Logic/Web/Hash";
|
||||
import {PreciseInput} from "../../Models/ThemeConfig/PresetConfig";
|
||||
|
||||
/**
|
||||
* 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 }[]
|
||||
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.showRemovedTags = showRemovedTags;
|
||||
|
||||
|
@ -73,11 +74,13 @@ ${Utils.special_visualizations_importRequirementDocs}
|
|||
this.args = [
|
||||
{
|
||||
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",
|
||||
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",
|
||||
|
@ -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 },
|
||||
originalFeatureTags: UIEventSource<any>,
|
||||
argument: string[]): Promise<void> {
|
||||
|
@ -484,43 +524,6 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
|
|||
|
||||
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 {
|
||||
|
@ -528,7 +531,8 @@ export class ImportPointButton extends AbstractImportButton {
|
|||
constructor() {
|
||||
super("import_button",
|
||||
"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"
|
||||
},
|
||||
|
@ -536,10 +540,14 @@ export class ImportPointButton extends AbstractImportButton {
|
|||
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",
|
||||
defaultValue: "5"
|
||||
}, {
|
||||
},
|
||||
{
|
||||
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
|
||||
)
|
||||
}
|
||||
|
@ -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>{
|
||||
tags: args.newTags.data,
|
||||
icon: () => new Img(args.icon),
|
||||
layerToAddTo: state.filteredLayers.data.filter(l => l.layerDef.id === args.targetLayer)[0],
|
||||
name: args.text,
|
||||
title: Translations.WT(args.text),
|
||||
preciseInput: {
|
||||
snapToLayers: args.snap_onto_layers?.split(";"),
|
||||
maxSnapDistance: Number(args.max_snap_distance)
|
||||
},
|
||||
preciseInput: preciseInputSpec, // must be explicitely assigned, if 'undefined' won't work otherwise
|
||||
boundsFactor: 3
|
||||
}
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ export interface SpecialVisualization {
|
|||
constr: ((state: FeaturePipelineState, tagSource: UIEventSource<any>, argument: string[], guistate: DefaultGuiState,) => BaseUIElement),
|
||||
docs: string,
|
||||
example?: string,
|
||||
args: { name: string, defaultValue?: string, doc: string }[],
|
||||
args: { name: string, defaultValue?: string, doc: string, required?: false | boolean }[],
|
||||
getLayerDependencies?: (argument: string[]) => string[]
|
||||
}
|
||||
|
||||
|
@ -102,6 +102,7 @@ class CloseNoteButton implements SpecialVisualization {
|
|||
{
|
||||
name: "text",
|
||||
doc: "Text to show on this button",
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: "icon",
|
||||
|
@ -179,7 +180,7 @@ class CloseNoteButton implements SpecialVisualization {
|
|||
|
||||
export default class SpecialVisualizations {
|
||||
|
||||
public static specialVisualizations = SpecialVisualizations.init()
|
||||
public static specialVisualizations : SpecialVisualization[] = SpecialVisualizations.init()
|
||||
|
||||
public static HelpMessage() {
|
||||
|
||||
|
@ -206,9 +207,28 @@ export default class SpecialVisualizations {
|
|||
));
|
||||
|
||||
return new Combine([
|
||||
new Combine([
|
||||
|
||||
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.",
|
||||
"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
|
||||
]
|
||||
).SetClass("flex flex-col");
|
||||
|
@ -227,9 +247,9 @@ export default class SpecialVisualizations {
|
|||
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)",
|
||||
args: [{
|
||||
name: "image key/prefix (multiple values allowed if comma-seperated)",
|
||||
name: "image_key",
|
||||
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) => {
|
||||
let imagePrefixes: string[] = undefined;
|
||||
|
@ -368,6 +388,7 @@ export default class SpecialVisualizations {
|
|||
{
|
||||
doc: "The side to show, either `left` or `right`",
|
||||
name: "side",
|
||||
required: true
|
||||
}
|
||||
],
|
||||
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)}",
|
||||
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: [{
|
||||
name: "Url", doc: "The URL to load"
|
||||
name: "Url",
|
||||
doc: "The URL to load",
|
||||
required: true
|
||||
}, {
|
||||
name: "Shorthands",
|
||||
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) => {
|
||||
const url = args[0];
|
||||
|
@ -483,7 +507,8 @@ export default class SpecialVisualizations {
|
|||
args: [
|
||||
{
|
||||
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",
|
||||
|
@ -588,7 +613,8 @@ export default class SpecialVisualizations {
|
|||
example: "{canonical(length)} will give 42 metre (in french)",
|
||||
args: [{
|
||||
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) => {
|
||||
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: "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: "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",
|
||||
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)}",
|
||||
|
|
8
Utils.ts
8
Utils.ts
|
@ -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.
|
||||
*
|
||||
* 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[] = []) {
|
||||
const head = path[0]
|
||||
|
@ -794,6 +794,12 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
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) {
|
||||
const track = Array(str2.length + 1).fill(null).map(() =>
|
||||
Array(str1.length + 1).fill(null));
|
||||
|
|
|
@ -85,7 +85,7 @@
|
|||
"iconBadges": [
|
||||
{
|
||||
"if": "_total_comments>1",
|
||||
"then": "speech_bubble"
|
||||
"then": "circle:white;speech_bubble"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -915,6 +915,16 @@
|
|||
"https://www.OpenStreetMap.org"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "party.svg",
|
||||
"license": "CC-BY 4.0",
|
||||
"authors": [
|
||||
"Twemoji"
|
||||
],
|
||||
"sources": [
|
||||
"https://github.com/twitter/twemoji"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "payment_card.svg",
|
||||
"license": "CC0",
|
||||
|
|
1
assets/svg/party.svg
Normal file
1
assets/svg/party.svg
Normal 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 |
|
@ -144,6 +144,72 @@
|
|||
"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",
|
||||
"license": "CC0",
|
||||
|
|
|
@ -123,7 +123,16 @@
|
|||
},
|
||||
{
|
||||
"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": [
|
||||
|
|
|
@ -706,6 +706,7 @@ video {
|
|||
}
|
||||
|
||||
.sticky {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
}
|
||||
|
||||
|
@ -1504,10 +1505,6 @@ video {
|
|||
padding: 0.125rem;
|
||||
}
|
||||
|
||||
.p-8 {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.px-0 {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
|
|
|
@ -1,18 +1,12 @@
|
|||
import {describe} from 'mocha'
|
||||
import {expect} from 'chai'
|
||||
import {LayoutConfigJson} from "../../../../Models/ThemeConfig/Json/LayoutConfigJson";
|
||||
import {LayerConfigJson} from "../../../../Models/ThemeConfig/Json/LayerConfigJson";
|
||||
import {PrepareTheme} from "../../../../Models/ThemeConfig/Conversion/PrepareTheme";
|
||||
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 {PrepareLayer} from "../../../../Models/ThemeConfig/Conversion/PrepareLayer";
|
||||
|
||||
|
||||
import {PrepareLayer, RewriteSpecial} from "../../../../Models/ThemeConfig/Conversion/PrepareLayer";
|
||||
import {
|
||||
QuestionableTagRenderingConfigJson
|
||||
} from "../../../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson";
|
||||
|
||||
describe("PrepareLayer", () => {
|
||||
|
||||
|
@ -66,8 +60,10 @@ describe("PrepareLayer", () => {
|
|||
"if": "parking:condition:left=free",
|
||||
"then": "#299921"
|
||||
},
|
||||
{"if": "parking:condition:left=disc",
|
||||
"then": "#219991"}]
|
||||
{
|
||||
"if": "parking:condition:left=disc",
|
||||
"then": "#219991"
|
||||
}]
|
||||
},
|
||||
"offset": -6
|
||||
}, {
|
||||
|
@ -77,8 +73,10 @@ describe("PrepareLayer", () => {
|
|||
"if": "parking:condition:right=free",
|
||||
"then": "#299921"
|
||||
},
|
||||
{"if": "parking:condition:right=disc",
|
||||
"then": "#219991"}]
|
||||
{
|
||||
"if": "parking:condition:right=disc",
|
||||
"then": "#219991"
|
||||
}]
|
||||
},
|
||||
"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)}"}
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
|
|
19
tests/UI/SpecialVisualisations.spec.ts
Normal file
19
tests/UI/SpecialVisualisations.spec.ts
Normal 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")
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue