Huge refactorings of JSON-parsing and Tagsfilter, other cleanups, warning cleanups and lots of small subtle bugfixes

This commit is contained in:
Pieter Vander Vennet 2020-08-30 01:13:18 +02:00
parent 9a5b35b9f3
commit a57b7d93fa
113 changed files with 1565 additions and 2594 deletions

View file

@ -1,6 +1,5 @@
import {LayerDefinition} from "./LayerDefinition";
import {Layout} from "./Layout";
import {All} from "./Layouts/All";
import {Groen} from "./Layouts/Groen";
import Cyclofix from "./Layouts/Cyclofix";
import {StreetWidth} from "./Layouts/StreetWidth";
@ -10,12 +9,14 @@ import {Smoothness} from "./Layouts/Smoothness";
import {MetaMap} from "./Layouts/MetaMap";
import {Natuurpunt} from "./Layouts/Natuurpunt";
import {GhostBikes} from "./Layouts/GhostBikes";
import {CustomLayoutFromJSON} from "./JSON/CustomLayoutFromJSON";
import {FromJSON} from "./JSON/FromJSON";
import * as bookcases from "../assets/themes/bookcases/Bookcases.json";
import * as aed from "../assets/themes/aed/aed.json";
import * as toilets from "../assets/themes/toilets/toilets.json";
import * as artworks from "../assets/themes/artwork/artwork.json";
import * as cyclestreets from "../assets/themes/cyclestreets/cyclestreets.json";
import {PersonalLayout} from "../Logic/PersonalLayout";
export class AllKnownLayouts {
@ -28,11 +29,11 @@ export class AllKnownLayouts {
new GRB(),
new Cyclofix(),
new GhostBikes(),
CustomLayoutFromJSON.LayoutFromJSON(bookcases),
CustomLayoutFromJSON.LayoutFromJSON(aed),
CustomLayoutFromJSON.LayoutFromJSON(toilets),
CustomLayoutFromJSON.LayoutFromJSON(artworks),
CustomLayoutFromJSON.LayoutFromJSON(cyclestreets),
FromJSON.LayoutFromJSON(bookcases),
// FromJSON.LayoutFromJSON(aed),
// FromJSON.LayoutFromJSON(toilets),
// FromJSON.LayoutFromJSON(artworks),
// FromJSON.LayoutFromJSON(cyclestreets),
new MetaMap(),
new StreetWidth(),
@ -48,26 +49,22 @@ export class AllKnownLayouts {
private static AllLayouts(): Map<string, Layout> {
const all = new All();
this.allLayers = new Map<string, LayerDefinition>();
for (const layout of this.layoutsList) {
for (const layer of layout.layers) {
const key = layer.id;
if (this.allLayers[layer.id] !== undefined) {
continue;
}
this.allLayers[layer.id] = layer;
this.allLayers[layer.id.toLowerCase()] = layer;
all.layers.push(layer);
}
}
const allSets: Map<string, Layout> = new Map();
for (const layout of this.layoutsList) {
allSets[layout.name] = layout;
allSets[layout.name.toLowerCase()] = layout;
allSets[layout.id] = layout;
allSets[layout.id.toLowerCase()] = layout;
}
allSets[all.name] = all;
return allSets;
}

View file

@ -1,306 +0,0 @@
import {TagRenderingOptions} from "../TagRenderingOptions";
import {LayerDefinition, Preset} from "../LayerDefinition";
import {Layout} from "../Layout";
import Translation from "../../UI/i18n/Translation";
import Combine from "../../UI/Base/Combine";
import {And, Tag} from "../../Logic/TagsFilter";
import FixedText from "../Questions/FixedText";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
import {UIEventSource} from "../../Logic/UIEventSource";
import {TagDependantUIElementConstructor} from "../UIElementConstructor";
import {Map} from "../Layers/Map";
import {UIElement} from "../../UI/UIElement";
import Translations from "../../UI/i18n/Translations";
export interface TagRenderingConfigJson {
// If this key is present, then...
key?: string,
// Use this string to render
render?: string | any,
// One of string, int, nat, float, pfloat, email, phone. Default: string
type?: string,
// If it is not known (and no mapping below matches), this question is asked; a textfield is inserted in the rendering above
question?: string | any,
// If a value is added with the textfield, this extra tag is addded. Optional field
addExtraTags?: string | { k: string, v: string }[];
// Extra tags: rendering is only shown/asked if these tags are present
condition?: string;
// Alternatively, these tags are shown if they match - even if the key above is not there
// If unknown, these become a radio button
mappings?:
{
if: string,
then: string | any
}[]
}
export interface LayerConfigJson {
name: string;
title: string | any | TagRenderingConfigJson;
description: string | any;
minzoom: number | string,
icon?: TagRenderingConfigJson;
color?: TagRenderingConfigJson;
width?: TagRenderingConfigJson;
overpassTags: string | { k: string, v: string }[];
wayHandling?: number,
widenFactor?: number,
presets: {
tags: string,
title: string | any,
description?: string | any,
icon?: string
}[],
tagRenderings: TagRenderingConfigJson []
}
export interface LayoutConfigJson {
widenFactor?: number;
name: string;
title: string | any;
description: string | any;
maintainer: string;
language: string | string[];
layers: LayerConfigJson[],
startZoom: string | number;
startLat: string | number;
startLon: string | number;
/**
* Either a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64,'
*/
icon: string;
}
export class CustomLayoutFromJSON {
public static FromQueryParam(layoutFromBase64: string): Layout {
return CustomLayoutFromJSON.LayoutFromJSON(JSON.parse(atob(layoutFromBase64)));
}
public static TagRenderingFromJson(json: TagRenderingConfigJson): TagDependantUIElementConstructor {
if(json === undefined){
return undefined;
}
if (typeof (json) === "string") {
return new FixedText(json);
}
let freeform = undefined;
if (json.render !== undefined) {
const type = json.type ?? "text";
let renderTemplate = CustomLayoutFromJSON.MaybeTranslation(json.render);;
const template = renderTemplate.replace("{" + json.key + "}", "$" + type + "$");
if(type === "url"){
renderTemplate = json.render.replace("{" + json.key + "}",
`<a href='{${json.key}}' target='_blank'>{${json.key}}</a>`
);
}
freeform = {
key: json.key,
template: template,
renderTemplate: renderTemplate,
extraTags: CustomLayoutFromJSON.TagsFromJson(json.addExtraTags),
}
if (freeform.key === "*") {
freeform.key = "id"; // Id is always there -> always take the rendering. Used for 'icon' and 'stroke'
}
}
let mappings = undefined;
if (json.mappings !== undefined) {
mappings = [];
for (const mapping of json.mappings) {
mappings.push({
k: new And(CustomLayoutFromJSON.TagsFromJson(mapping.if)),
txt: CustomLayoutFromJSON.MaybeTranslation(mapping.then)
})
}
}
const rendering = new TagRenderingOptions({
question: CustomLayoutFromJSON.MaybeTranslation(json.question),
freeform: freeform,
mappings: mappings
});
if (json.condition) {
const conditionTags: Tag[] = CustomLayoutFromJSON.TagsFromJson(json.condition);
return rendering.OnlyShowIf(new And(conditionTags));
}
return rendering;
}
private static PresetFromJson(layout: any, preset: any): Preset {
const t = CustomLayoutFromJSON.MaybeTranslation;
const tags = CustomLayoutFromJSON.TagsFromJson;
return {
icon: preset.icon ?? CustomLayoutFromJSON.TagRenderingFromJson(layout.icon),
tags: tags(preset.tags) ?? tags(layout.overpassTags),
title: t(preset.title) ?? t(layout.title),
description: t(preset.description) ?? t(layout.description)
}
}
private static StyleFromJson(layout: LayerConfigJson): ((tags: any) => {
color: string,
weight?: number,
icon: {
iconUrl: string,
iconSize: number[],
},
}) {
const iconRendering: TagDependantUIElementConstructor = CustomLayoutFromJSON.TagRenderingFromJson(layout.icon);
const colourRendering = CustomLayoutFromJSON.TagRenderingFromJson(layout.color);
let thickness = CustomLayoutFromJSON.TagRenderingFromJson(layout.width);
return (tags) => {
const iconUrl = iconRendering.GetContent(tags);
const stroke = colourRendering.GetContent(tags) ?? "#00f";
let weight = parseInt(thickness?.GetContent(tags)) ?? 10;
if(isNaN(weight)){
weight = 10;
}
return {
color: stroke,
weight: weight,
icon: {
iconUrl: iconUrl,
iconSize: [40, 40],
},
}
};
}
private static TagFromJson(json: string | { k: string, v: string }): Tag {
if (json === undefined) {
return undefined;
}
if (typeof (json) !== "string") {
return new Tag(json.k.trim(), json.v.trim())
}
let kv: string[] = undefined;
let invert = false;
let regex = false;
if (json.indexOf("!=") >= 0) {
kv = json.split("!=");
invert = true;
} else if (json.indexOf("~=") >= 0) {
kv = json.split("~=");
regex = true;
} else {
kv = json.split("=");
}
if (kv.length !== 2) {
return undefined;
}
if (kv[0].trim() === "") {
return undefined;
}
let v = kv[1].trim();
if(v.startsWith("/") && v.endsWith("/")){
v = v.substr(1, v.length - 2);
regex = true;
}
return new Tag(kv[0].trim(), regex ? new RegExp(v): v, invert);
}
public static TagsFromJson(json: string | { k: string, v: string }[]): Tag[] {
if (json === undefined) {
return undefined;
}
if (json === "") {
return [];
}
let tags = [];
if (typeof (json) === "string") {
tags = json.split("&").map(CustomLayoutFromJSON.TagFromJson);
} else {
tags = json.map(x => {CustomLayoutFromJSON.TagFromJson(x)});
}
for (const tag of tags) {
if (tag === undefined) {
return undefined;
}
}
return tags;
}
private static LayerFromJson(json: LayerConfigJson): LayerDefinition {
const t = CustomLayoutFromJSON.MaybeTranslation;
const tr = CustomLayoutFromJSON.TagRenderingFromJson;
const tags = CustomLayoutFromJSON.TagsFromJson(json.overpassTags);
// We run the icon rendering with the bare minimum of tags (the overpass tags) to get the actual icon
const icon = CustomLayoutFromJSON.TagRenderingFromJson(json.icon).GetContent({id:"node/-1"});
// @ts-ignore
const id = json.name?.replace(/[^a-zA-Z0-9_-]/g,'') ?? json.id;
return new LayerDefinition(
id,
{
description: t(json.description),
name: Translations.WT(t(json.name)),
icon: icon,
minzoom: parseInt(""+json.minzoom),
title: tr(json.title),
presets: json.presets.map((preset) => {
return CustomLayoutFromJSON.PresetFromJson(json, preset)
}),
elementsToShow:
[new ImageCarouselWithUploadConstructor()].concat(json.tagRenderings.map(tr)),
overpassFilter: new And(tags),
wayHandling: parseInt(""+json.wayHandling) ?? LayerDefinition.WAYHANDLING_CENTER_AND_WAY,
maxAllowedOverlapPercentage: 0,
style: CustomLayoutFromJSON.StyleFromJson(json)
}
)
}
private static MaybeTranslation(json: any): Translation | string {
if (json === undefined) {
return undefined;
}
if (typeof (json) === "string") {
return json;
}
return new Translation(json);
}
public static LayoutFromJSON(json: LayoutConfigJson) {
const t = CustomLayoutFromJSON.MaybeTranslation;
let languages : string[] ;
if(typeof (json.language) === "string"){
languages = [json.language];
}else{
languages = json.language
}
const layout = new Layout(json.name,
languages,
t(json.title),
json.layers.map(CustomLayoutFromJSON.LayerFromJson),
parseInt(""+json.startZoom),
parseFloat(""+json.startLat),
parseFloat(""+json.startLon),
new Combine(['<h3>', t(json.title), '</h3><br/>', t(json.description)])
);
layout.icon = json.icon;
layout.maintainer = json.maintainer;
layout.widenFactor = parseFloat(""+json.widenFactor) ?? 0.03;
if(isNaN(layout.widenFactor)){
layout.widenFactor = 0.03;
}
if (layout.widenFactor > 0.1) {
layout.widenFactor = 0.1;
}
return layout;
}
}

View file

@ -0,0 +1,259 @@
import {Layout} from "../Layout";
import {LayoutConfigJson} from "./LayoutConfigJson";
import {AndOrTagConfigJson} from "./TagConfigJson";
import {And, RegexTag, Tag, TagsFilter} from "../../Logic/Tags";
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
import {TagRenderingOptions} from "../TagRenderingOptions";
import Translation from "../../UI/i18n/Translation";
import {LayerConfigJson} from "./LayerConfigJson";
import {LayerDefinition, Preset} from "../LayerDefinition";
import {TagDependantUIElementConstructor} from "../UIElementConstructor";
import FixedText from "../Questions/FixedText";
import Translations from "../../UI/i18n/Translations";
import Combine from "../../UI/Base/Combine";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
import {ImageCarouselConstructor} from "../../UI/Image/ImageCarousel";
export class FromJSON {
public static FromBase64(layoutFromBase64: string): Layout {
return FromJSON.LayoutFromJSON(JSON.parse(atob(layoutFromBase64)));
}
public static LayoutFromJSON(json: LayoutConfigJson): Layout {
console.log("Parsing ", json.id)
const tr = FromJSON.Translation;
const layers = json.layers.map(FromJSON.Layer);
const roaming: TagDependantUIElementConstructor[] = json.roamingRenderings?.map(FromJSON.TagRendering) ?? [];
for (const layer of layers) {
layer.elementsToShow.push(...roaming);
}
const layout = new Layout(
json.id,
typeof (json.language) === "string" ? [json.language] : json.language,
tr(json.title),
layers,
json.startZoom,
json.startLat,
json.startLon,
new Combine(["<h3>", tr(json.title), "</h3>", tr(json.description)]),
);
layout.widenFactor = json.widenFactor ?? 0.07;
layout.icon = json.icon;
layout.maintainer = json.maintainer;
layout.version = json.version;
layout.socialImage = json.socialImage;
layout.changesetMessage = json.changesetmessage;
return layout;
}
public static Translation(json: string | any): string | Translation {
if (json === undefined) {
return undefined;
}
if (typeof (json) === "string") {
return json;
}
const tr = {};
for (let key in json) {
tr[key] = json[key]; // I'm doing this wrong, I know
}
return new Translation(tr);
}
public static TagRendering(json: TagRenderingConfigJson | string): TagDependantUIElementConstructor {
return FromJSON.TagRenderingWithDefault(json, "", undefined);
}
public static TagRenderingWithDefault(json: TagRenderingConfigJson | string, propertyName, defaultValue: string): TagDependantUIElementConstructor {
if (json === undefined) {
if(defaultValue !== undefined){
console.warn(`Using default value ${defaultValue} for ${propertyName}`)
return FromJSON.TagRendering(defaultValue);
}
throw `Tagrendering ${propertyName} is undefined...`
}
if (typeof json === "string") {
switch (json) {
case "picture": {
return new ImageCarouselWithUploadConstructor()
}
case "pictures": {
return new ImageCarouselWithUploadConstructor()
}
case "image": {
return new ImageCarouselWithUploadConstructor()
}
case "images": {
return new ImageCarouselWithUploadConstructor()
}
case "picturesNoUpload": {
return new ImageCarouselConstructor()
}
}
return new TagRenderingOptions({
freeform: {
key: "id",
renderTemplate: json,
template: "$$$"
}
});
}
let template = FromJSON.Translation(json.render);
let freeform = undefined;
if (json.freeform) {
if(json.render === undefined){
console.error("Freeform is defined, but render is not. This is not allowed.", json)
throw "Freeform is defined, but render is not. This is not allowed."
}
freeform = {
template: `$${json.freeform.type ?? "string"}$`,
renderTemplate: template,
key: json.freeform.key
};
if (json.freeform.addExtraTags) {
freeform["extraTags"] = FromJSON.Tag(json.freeform.addExtraTags);
}
} else if (json.render) {
freeform = {
template: `$string$`,
renderTemplate: template,
key: "id"
}
}
const mappings = json.mappings?.map(mapping => (
{
k: FromJSON.Tag(mapping.if),
txt: FromJSON.Translation(mapping.then),
hideInAnswer: mapping.hideInAnswer
})
);
return new TagRenderingOptions({
question: FromJSON.Translation(json.question),
freeform: freeform,
mappings: mappings
});
}
public static SimpleTag(json: string): Tag {
const tag = json.split("=");
return new Tag(tag[0], tag[1]);
}
public static Tag(json: AndOrTagConfigJson | string): TagsFilter {
if (typeof (json) == "string") {
const tag = json as string;
if (tag.indexOf("!~") >= 0) {
const split = tag.split("!~");
if(split[1] == "*"){
split[1] = ".*"
}
return new RegexTag(
new RegExp(split[0]),
new RegExp(split[1]),
true
);
}
if (tag.indexOf("!=") >= 0) {
const split = tag.split("!=");
return new RegexTag(
new RegExp(split[0]),
new RegExp(split[1]),
true
);
}
if (tag.indexOf("~") >= 0) {
const split = tag.split("~");
if(split[1] == "*"){
split[1] = ".*"
}
return new RegexTag(
new RegExp("^"+split[0]+"$"),
new RegExp("^"+split[1]+"$")
);
}
const split = tag.split("=");
return new Tag(split[0], split[1])
}
if (json.and !== undefined) {
return new And(json.and.map(FromJSON.Tag));
}
if (json.or !== undefined) {
return new And(json.or.map(FromJSON.Tag));
}
}
private static Title(json: string | Map<string, string> | TagRenderingConfigJson): TagDependantUIElementConstructor {
if ((json as TagRenderingConfigJson).render !== undefined) {
return FromJSON.TagRendering((json as TagRenderingConfigJson));
} else if (typeof (json) === "string") {
return new FixedText(Translations.WT(json));
} else {
return new FixedText(FromJSON.Translation(json as Map<string, string>));
}
}
public static Layer(json: LayerConfigJson): LayerDefinition {
console.log("Parsing ",json.name);
const tr = FromJSON.Translation;
const overpassTags = FromJSON.Tag(json.overpassTags);
const icon = FromJSON.TagRenderingWithDefault(json.icon, "layericon", "./assets/bug.svg");
const color = FromJSON.TagRenderingWithDefault(json.color, "layercolor", "#0000ff");
const width = FromJSON.TagRenderingWithDefault(json.width, "layerwidth", "10");
const renderTags = {"id": "node/-1"}
const presets: Preset[] = json?.presets?.map(preset => {
return ({
title: tr(preset.title),
description: tr(preset.description),
tags: preset.tags.map(FromJSON.SimpleTag)
});
}) ?? [];
function style(tags) {
return {
color: color.GetContent(tags).txt,
weight: width.GetContent(tags).txt,
icon: {
iconUrl: icon.GetContent(tags).txt
},
}
}
const layer = new LayerDefinition(
json.id,
{
name: tr(json.name),
description: tr(json.description),
icon: icon.GetContent(renderTags).txt,
overpassFilter: overpassTags,
title: FromJSON.Title(json.title),
minzoom: json.minzoom,
presets: presets,
elementsToShow: json.tagRenderings?.map(FromJSON.TagRendering) ?? [],
style: style,
wayHandling: json.wayHandling
}
);
return layer;
}
}

View file

@ -0,0 +1,79 @@
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
import {AndOrTagConfigJson} from "./TagConfigJson";
/**
* Configuration for a single layer
*/
export interface LayerConfigJson {
/**
* The id of this layer.
* This should be a simple, lowercase, human readable string that is used to identify the layer.
*/
id: string;
/**
* The name of this layer
* Used in the layer control panel and the 'Personal theme'
*/
name: string | any
/**
* A description for this layer.
* Shown in the layer selections and in the personal theme
*/
description?: string | any;
/**
* The tags to load from overpass. Either a simple 'key=value'-string, otherwise an advanced configuration
*/
overpassTags: AndOrTagConfigJson | string;
/**
* The zoomlevel at which point the data is shown and loaded.
*/
minzoom: number;
/**
* The title shown in a popup for elements of this layer
*/
title: string | any | TagRenderingConfigJson;
/**
* The icon for an element.
* Note that this also doubles as the icon for this layer (rendered with the overpass-tags) ánd the icon in the presets.
*/
icon?: string | TagRenderingConfigJson;
/**
* The color for way-elements
*/
color?: string | TagRenderingConfigJson;
/**
* The stroke-width for way-elements
*/
width?: string | TagRenderingConfigJson;
/**
* Wayhandling: should a way/area be displayed as:
* 0) The way itself
* 1) The centerpoint and the way
* 2) Only the centerpoint?
*/
wayHandling?: number;
/**
* Presets for this layer
*/
presets?: {
tags: string[],
title: string | any,
description?: string | any,
}[],
/**
* All the tag renderings.
*/
tagRenderings?: (string | TagRenderingConfigJson) []
}

View file

@ -0,0 +1,98 @@
import {LayerConfigJson} from "./LayerConfigJson";
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
/**
* Defines what a JSON-segment defining a layout should look like.
*
* General remark: a type (string | any) indicates either a fixed or a translatable string
*/
export interface LayoutConfigJson {
/**
* The id of this layout.
* This should be a simple, lowercase string which is used to create the html-page, e.g.
* 'cyclestreets' which become 'cyclestreets.html'
*/
id: string;
/**
* Who does maintian this preset?
*/
maintainer: string;
/**
* Extra piece of text that can be added to the changeset
*/
changesetmessage?: string;
/**
* A version number, either semantically or by date.
* Should be sortable, where the higher value is the later version
*/
version: string;
/**
* The supported language(s).
* This should be a two-letter, lowercase code which identifies the language, e.g. "en", "nl", ...
* If the theme supports multiple languages, use a list: `["en","nl","fr"]` to allow the user to pick any of them
*/
language: string | string[];
/**
* The title, as shown in the welcome message and the more-screen
*/
title: string | any;
/**
* The description, as shown in the welcome message and the more-screen
*/
description: string | any;
/**
* The icon representing this theme.
* Used as logo in the more-screen and (for official themes) as favicon, webmanifest logo, ...
* Either a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64)
*/
icon: string;
/**
* Link to a 'social image' which is included as og:image-tag on official themes.
* Usefull to share the theme on social media
*/
socialImage?: string;
/**
* Default location and zoom to start.
* Note that this is barely used. Once the user has visited mapcomplete at least once, the previous location of the user will be used
*/
startZoom: number;
startLat: number;
startLon: number;
/**
* When a query is run, the data within bounds of the visible map is loaded.
* However, users tend to pan and zoom a lot. It is pretty annoying if every single pan means a reloading of the data.
* For this, the bounds are widened in order to make a small pan still within bounds of the loaded data.
*
* IF widenfactor is 0, this feature is disabled. A recommended value is between 0.5 and 0.01 (the latter for very dense queries)
*/
widenFactor?: number;
/**
* A tagrendering depicts how to show some tags or how to show a question for it.
*
* These tagrenderings are applied to _all_ the loaded layers and are a way to reuse tagrenderings.
* Note that if multiple themes are loaded (e.g. via the personal theme)
* that these roamingRenderings are applied to the layers of the OTHER themes too!
*
* In order to prevent them to do too much damage, all the overpass-tags of the layers are taken and combined as OR.
* These tag renderings will only show up if the object matches this filter.
*/
roamingRenderings?: TagRenderingConfigJson[],
/**
* The layers to display
*/
layers: LayerConfigJson[],
}

View file

@ -0,0 +1,14 @@
/**
* Read a tagconfig and converts it into a TagsFilter value
*/
import {AndOrTagConfigJson} from "./TagConfigJson";
export default class TagConfig {
public static fromJson(json: any): TagConfig {
const config: AndOrTagConfigJson = json;
return config;
}
}

View file

@ -0,0 +1,8 @@
export interface AndOrTagConfigJson {
and?: (string | AndOrTagConfigJson)[]
or?: (string | AndOrTagConfigJson)[]
}

View file

@ -0,0 +1,51 @@
import {AndOrTagConfigJson} from "./TagConfigJson";
export interface TagRenderingConfigJson {
/**
* Renders this value. Note that "{key}"-parts are substituted by the corresponding values of the element.
* If neither 'textFieldQuestion' nor 'mappings' are defined, this text is simply shown as default value.
*/
render?: string | any,
/**
* If it turns out that this tagRendering doesn't match _any_ value, then we show this question.
* If undefined, the question is never asked and this tagrendering is read-only
*/
question?: string | any,
/**
* Only show this question if the object also matches the following tags.
*
* This is useful to ask a follow-up question. E.g. if there is a diaper table, then ask a follow-up question on diaper tables...
* */
condition?: AndOrTagConfigJson | string;
/**
* Allow freeform text input from the user
*/
freeform?: {
/**
* If this key is present, then 'render' is used to display the value.
* If this is undefined, the rendering is _always_ shown
*/
key: string,
/**
* The type of the text-field, e.g. 'string', 'nat', 'float', 'date',...
*/
type?: string,
/**
* If a value is added with the textfield, these extra tag is addded.
* Usefull to add a 'fixme=freeform textfield used - to be checked'
**/
addExtraTags?: AndOrTagConfigJson | string;
}
/**
* Allows fixed-tag inputs, shown either as radiobuttons or as checkboxes
*/
mappings?: {
if: AndOrTagConfigJson | string,
then: string | any
hideInAnswer?: boolean
}[]
}

View file

@ -1,9 +1,8 @@
import {Tag, TagsFilter} from "../Logic/TagsFilter";
import {Tag, TagsFilter} from "../Logic/Tags";
import {UIElement} from "../UI/UIElement";
import {TagDependantUIElementConstructor} from "./UIElementConstructor";
import {TagRenderingOptions} from "./TagRenderingOptions";
import Translation from "../UI/i18n/Translation";
import {LayerConfigJson, TagRenderingConfigJson} from "./JSON/CustomLayoutFromJSON";
export interface Preset {
tags: Tag[],
@ -75,9 +74,8 @@ export class LayerDefinition {
style: (tags: any) => {
color: string,
weight?: number,
icon: {
iconUrl: string,
iconSize: number[],
icon: {
iconUrl: string, iconSize?: number[], popupAnchor?: number[], iconAnchor?: number[]
},
};

View file

@ -3,8 +3,8 @@ import FixedText from "../Questions/FixedText";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
import Translations from "../../UI/i18n/Translations";
import CafeName from "../Questions/bike/CafeName";
import { Or, And, Tag, anyValueExcept, Regex } from "../../Logic/TagsFilter";
import { PhoneNumberQuestion } from "../Questions/PhoneNumberQuestion";
import {And, Or, RegexTag, Tag} from "../../Logic/Tags";
import {PhoneNumberQuestion} from "../Questions/PhoneNumberQuestion";
import Website from "../Questions/Website";
import CafeRepair from "../Questions/bike/CafeRepair";
import CafeDiy from "../Questions/bike/CafeDiy";
@ -20,10 +20,11 @@ export default class BikeCafes extends LayerDefinition {
this.name = this.to.name
this.icon = "./assets/bike/cafe.svg"
this.overpassFilter = new And([
new Tag("amenity", /^pub|bar|cafe$/),
new RegexTag(/^amenity$/, /^pub|bar|cafe$/),
new Or([
new Tag(/^service:bicycle:/, "*"),
new Tag("pub", "cycling")
new RegexTag(/^service:bicycle:/, /.*/),
new RegexTag(/^pub$/, /^cycling|bicycle$/),
new RegexTag(/^theme$/, /^cycling|bicycle$/),
])
])
@ -40,7 +41,14 @@ export default class BikeCafes extends LayerDefinition {
this.maxAllowedOverlapPercentage = 10;
this.minzoom = 13
this.style = this.generateStyleFunction()
this.style = () => ({
color: "#00bb00",
icon: {
iconUrl: "./assets/bike/cafe.svg",
iconSize: [50, 50],
iconAnchor: [25, 50]
}
});
this.title = new FixedText(this.to.title)
this.elementsToShow = [
new ImageCarouselWithUploadConstructor(),
@ -54,18 +62,4 @@ export default class BikeCafes extends LayerDefinition {
]
this.wayHandling = LayerDefinition.WAYHANDLING_CENTER_AND_WAY
}
private generateStyleFunction() {
const self = this
return function (properties: any) {
return {
color: "#00bb00",
icon: {
iconUrl: "./assets/bike/cafe.svg",
iconSize: [50, 50],
iconAnchor: [25,50]
}
}
}
}
}

View file

@ -1,7 +1,7 @@
import { LayerDefinition } from "../LayerDefinition";
import {LayerDefinition} from "../LayerDefinition";
import Translations from "../../UI/i18n/Translations";
import {And, Tag, Or, anyValueExcept} from "../../Logic/TagsFilter";
import { ImageCarouselWithUploadConstructor } from "../../UI/Image/ImageCarouselWithUpload";
import {And, RegexTag, Tag} from "../../Logic/Tags";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
import ShopRetail from "../Questions/bike/ShopRetail";
import ShopPump from "../Questions/bike/ShopPump";
import ShopRental from "../Questions/bike/ShopRental";
@ -9,7 +9,7 @@ import ShopRepair from "../Questions/bike/ShopRepair";
import ShopDiy from "../Questions/bike/ShopDiy";
import ShopName from "../Questions/bike/ShopName";
import ShopSecondHand from "../Questions/bike/ShopSecondHand";
import { PhoneNumberQuestion } from "../Questions/PhoneNumberQuestion";
import {PhoneNumberQuestion} from "../Questions/PhoneNumberQuestion";
import Website from "../Questions/Website";
import {TagRenderingOptions} from "../TagRenderingOptions";
@ -24,8 +24,8 @@ export default class BikeOtherShops extends LayerDefinition {
this.name = this.to.name
this.icon = "./assets/bike/non_bike_repair_shop.svg"
this.overpassFilter = new And([
anyValueExcept("shop", "bicycle"),
new Tag(/^service:bicycle:/, "*"),
new RegexTag(/^shop$/, /^bicycle$/, true),
new RegexTag(/^service:bicycle:/, /.*/),
])
this.presets = []
this.maxAllowedOverlapPercentage = 10

View file

@ -1,12 +1,10 @@
import {LayerDefinition} from "../LayerDefinition";
import {And, Or, Tag, TagsFilter} from "../../Logic/TagsFilter";
import {OperatorTag} from "../Questions/OperatorTag";
import {Tag} from "../../Logic/Tags";
import FixedText from "../Questions/FixedText";
import ParkingType from "../Questions/bike/ParkingType";
import ParkingCapacity from "../Questions/bike/ParkingCapacity";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
import Translations from "../../UI/i18n/Translations";
import ParkingOperator from "../Questions/bike/ParkingOperator";
import ParkingAccessCargo from "../Questions/bike/ParkingAccessCargo";
import ParkingCapacityCargo from "../Questions/bike/ParkingCapacityCargo";
@ -28,8 +26,17 @@ export default class BikeParkings extends LayerDefinition {
this.maxAllowedOverlapPercentage = 10;
this.minzoom = 13;
this.style = this.generateStyleFunction();
this.minzoom = 17;
this.style = function () {
return {
color: "#00bb00",
icon: {
iconUrl: "./assets/bike/parking.svg",
iconSize: [50, 50],
iconAnchor: [25, 50]
}
};
};
this.title = new FixedText(Translations.t.cyclofix.parking.title)
this.elementsToShow = [
new ImageCarouselWithUploadConstructor(),
@ -42,18 +49,4 @@ export default class BikeParkings extends LayerDefinition {
this.wayHandling = LayerDefinition.WAYHANDLING_CENTER_AND_WAY;
}
private generateStyleFunction() {
const self = this;
return function (properties: any) {
return {
color: "#00bb00",
icon: {
iconUrl: "./assets/bike/parking.svg",
iconSize: [50, 50],
iconAnchor: [25,50]
}
};
};
}
}

View file

@ -1,8 +1,7 @@
import { LayerDefinition } from "../LayerDefinition";
import {LayerDefinition} from "../LayerDefinition";
import Translations from "../../UI/i18n/Translations";
import {And, Tag, Or} from "../../Logic/TagsFilter";
import FixedText from "../Questions/FixedText";
import { ImageCarouselWithUploadConstructor } from "../../UI/Image/ImageCarouselWithUpload";
import {And, Tag} from "../../Logic/Tags";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
import ShopRetail from "../Questions/bike/ShopRetail";
import ShopPump from "../Questions/bike/ShopPump";
import ShopRental from "../Questions/bike/ShopRental";
@ -18,7 +17,6 @@ import {TagRenderingOptions} from "../TagRenderingOptions";
export default class BikeShops extends LayerDefinition {
private readonly sellsBikes = new Tag("service:bicycle:retail", "yes")
private readonly repairsBikes = new Tag("service:bicycle:repair", "yes")
constructor() {
super("bikeshop");

View file

@ -1,12 +1,9 @@
import {LayerDefinition} from "../LayerDefinition";
import {And, Tag, TagsFilter, Or, Not} from "../../Logic/TagsFilter";
import {And, Or, Tag} from "../../Logic/Tags";
import BikeStationChain from "../Questions/bike/StationChain";
import BikeStationPumpTools from "../Questions/bike/StationPumpTools";
import BikeStationStand from "../Questions/bike/StationStand";
import PumpManual from "../Questions/bike/PumpManual";
import BikeStationOperator from "../Questions/bike/StationOperator";
import BikeStationBrand from "../Questions/bike/StationBrand";
import FixedText from "../Questions/FixedText";
import PumpManometer from "../Questions/bike/PumpManometer";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
import PumpOperational from "../Questions/bike/PumpOperational";
@ -19,7 +16,6 @@ export default class BikeStations extends LayerDefinition {
private readonly repairStation = new Tag("amenity", "bicycle_repair_station");
private readonly pump = new Tag("service:bicycle:pump", "yes");
private readonly nopump = new Tag("service:bicycle:pump", "no");
private readonly pumpOperationalAny = new Tag("service:bicycle:pump:operational_status", "yes");
private readonly pumpOperationalOk = new Or([new Tag("service:bicycle:pump:operational_status", "yes"), new Tag("service:bicycle:pump:operational_status", "operational"), new Tag("service:bicycle:pump:operational_status", "ok"), new Tag("service:bicycle:pump:operational_status", "")]);
private readonly tools = new Tag("service:bicycle:tools", "yes");
private readonly notools = new Tag("service:bicycle:tools", "no");

View file

@ -1,6 +1,5 @@
import {LayerDefinition} from "../LayerDefinition";
import {And, Or, Tag} from "../../Logic/TagsFilter";
import FixedText from "../Questions/FixedText";
import {And, Or, Tag} from "../../Logic/Tags";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
import {TagRenderingOptions} from "../TagRenderingOptions";
@ -24,7 +23,7 @@ export class Birdhide extends LayerDefinition {
tags: [Birdhide.birdhide]
}
],
style(tags: any): { color: string; icon: any } {
style(): { color: string; icon: any } {
return {color: "", icon: undefined};
},
});

View file

@ -1,5 +1,5 @@
import {LayerDefinition} from "../LayerDefinition";
import {Or, Tag} from "../../Logic/TagsFilter";
import {Or, Tag} from "../../Logic/Tags";
import {AccessTag} from "../Questions/AccessTag";
import {OperatorTag} from "../Questions/OperatorTag";
import {NameQuestion} from "../Questions/NameQuestion";

View file

@ -1,7 +1,7 @@
import {LayerDefinition} from "../LayerDefinition";
import Translations from "../../UI/i18n/Translations";
import FixedText from "../Questions/FixedText";
import {And, Tag} from "../../Logic/TagsFilter";
import {And, Tag} from "../../Logic/Tags";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
export class ClimbingTree extends LayerDefinition {

View file

@ -1,7 +1,6 @@
import {LayerDefinition} from "../LayerDefinition";
import {And, Or, Tag} from "../../Logic/TagsFilter";
import {And, Or, Tag} from "../../Logic/Tags";
import {OperatorTag} from "../Questions/OperatorTag";
import * as L from "leaflet";
import FixedText from "../Questions/FixedText";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
import Translations from "../../UI/i18n/Translations";
@ -29,7 +28,7 @@ export class DrinkingWater extends LayerDefinition {
this.wayHandling = LayerDefinition.WAYHANDLING_CENTER_AND_WAY
this.minzoom = 13;
this.style = this.generateStyleFunction();
this.style = DrinkingWater.generateStyleFunction();
this.title = new FixedText("Drinking water");
this.elementsToShow = [
new OperatorTag(),
@ -47,10 +46,8 @@ export class DrinkingWater extends LayerDefinition {
}
private generateStyleFunction() {
const self = this;
return function (properties: any) {
private static generateStyleFunction() {
return function () {
return {
color: "#00bb00",
icon: {

View file

@ -1,5 +1,5 @@
import {LayerDefinition} from "../LayerDefinition";
import {Tag} from "../../Logic/TagsFilter";
import {Tag} from "../../Logic/Tags";
import FixedText from "../Questions/FixedText";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
import {TagRenderingOptions} from "../TagRenderingOptions";

View file

@ -1,5 +1,5 @@
import {LayerDefinition} from "../LayerDefinition";
import {And, Regex, Tag} from "../../Logic/TagsFilter";
import {And, RegexTag, Tag} from "../../Logic/Tags";
import {TagRenderingOptions} from "../TagRenderingOptions";
export class GrbToFix extends LayerDefinition {
@ -10,12 +10,12 @@ export class GrbToFix extends LayerDefinition {
this.name = "grb";
this.presets = [];
this.icon = "./assets/star.svg";
this.overpassFilter = new Regex("fixme", "GRB");
this.overpassFilter = new RegexTag(/fixme/, /.*GRB.*/);
this.minzoom = 13;
this.style = function (tags) {
this.style = function () {
return {
icon: {
iconUrl: "assets/star.svg",

View file

@ -1,7 +1,7 @@
import {LayerDefinition} from "../LayerDefinition";
import FixedText from "../Questions/FixedText";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
import {And, Tag} from "../../Logic/TagsFilter";
import {And, Tag} from "../../Logic/Tags";
import {TagRenderingOptions} from "../TagRenderingOptions";
export class InformationBoard extends LayerDefinition {

View file

@ -1,7 +1,7 @@
import {LayerDefinition} from "../LayerDefinition";
import FixedText from "../Questions/FixedText";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
import {And, Tag} from "../../Logic/TagsFilter";
import {Tag} from "../../Logic/Tags";
import {TagRenderingOptions} from "../TagRenderingOptions";
export class Map extends LayerDefinition {

View file

@ -1,5 +1,5 @@
import {LayerDefinition} from "../LayerDefinition";
import {Or, Tag} from "../../Logic/TagsFilter";
import {Or, Tag} from "../../Logic/Tags";
import {AccessTag} from "../Questions/AccessTag";
import {OperatorTag} from "../Questions/OperatorTag";
import {NameQuestion} from "../Questions/NameQuestion";

View file

@ -1,7 +1,5 @@
import {LayerDefinition} from "../LayerDefinition";
import {And, Or, Tag} from "../../Logic/TagsFilter";
import {AccessTag} from "../Questions/AccessTag";
import {OperatorTag} from "../Questions/OperatorTag";
import {Or, Tag} from "../../Logic/Tags";
import {NameQuestion} from "../Questions/NameQuestion";
import {NameInline} from "../Questions/NameInline";
import {DescriptionQuestion} from "../Questions/DescriptionQuestion";

View file

@ -1,7 +1,6 @@
import {LayerDefinition} from "../LayerDefinition";
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
import FixedText from "../Questions/FixedText";
import {Tag} from "../../Logic/TagsFilter";
import {Tag} from "../../Logic/Tags";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
import {TagRenderingOptions} from "../TagRenderingOptions";
@ -19,7 +18,7 @@ export class Viewpoint extends LayerDefinition {
}],
icon: "assets/viewpoint.svg",
wayHandling: LayerDefinition.WAYHANDLING_CENTER_ONLY,
style: tags => {
style: _ => {
return {
color: undefined, icon: {
iconUrl: "assets/viewpoint.svg",

View file

@ -1,13 +1,12 @@
import {LayerDefinition} from "../LayerDefinition";
import {And, Not, Or, Tag} from "../../Logic/TagsFilter";
import {Park} from "./Park";
import {And, Or, Tag} from "../../Logic/Tags";
import {TagRenderingOptions} from "../TagRenderingOptions";
export class Widths extends LayerDefinition {
private cyclistWidth: number;
private carWidth: number;
private pedestrianWidth: number;
private readonly cyclistWidth: number;
private readonly carWidth: number;
private readonly pedestrianWidth: number;
private readonly _bothSideParking = new Tag("parking:lane:both", "parallel");
private readonly _noSideParking = new Tag("parking:lane:both", "no_parking");
@ -36,10 +35,9 @@ export class Widths extends LayerDefinition {
private readonly _oneSideParking = new Or([this._leftSideParking, this._rightSideParking]);
private readonly _carfree = new Or(
private readonly _carfree = new And(
[new Tag("highway", "pedestrian"), new Tag("highway", "living_street"),
new Tag("access","destination"), new Tag("motor_vehicle", "destination")])
private readonly _notCarFree = new Not(this._carfree);
private calcProps(properties) {
let parkingStateKnown = true;
@ -59,8 +57,7 @@ export class Widths extends LayerDefinition {
}
let pedestrianFlowNeeded = 0;
let pedestrianFlowNeeded;
if (this._sidewalkBoth.matchesProperties(properties)) {
pedestrianFlowNeeded = 0;
} else if (this._sidewalkNone.matchesProperties(properties)) {
@ -198,7 +195,7 @@ export class Widths extends LayerDefinition {
renderTemplate: "{note:width:carriageway}",
template: "$$$",
}
}).OnlyShowIf(this._notCarFree),
}).OnlyShowIf(this._carfree, true),
new TagRenderingOptions({
@ -218,7 +215,7 @@ export class Widths extends LayerDefinition {
renderTemplate: "{note:width:carriageway}",
template: "$$$",
}
}).OnlyShowIf(this._notCarFree),
}).OnlyShowIf(this._carfree, true),
new TagRenderingOptions({
@ -248,7 +245,7 @@ export class Widths extends LayerDefinition {
txt: "Tweerichtingsverkeer voor iedereen. Dit gebruikt <b>" + r(2 * this.carWidth + 2 * this.cyclistWidth) + "m</b>"
}
]
}).OnlyShowIf(this._notCarFree),
}).OnlyShowIf(this._carfree, true),
new TagRenderingOptions(
{
@ -266,7 +263,7 @@ export class Widths extends LayerDefinition {
{k: new Tag("short",""), txt: "De totale nodige ruimte voor vlot en veilig verkeer is dus <span class='thanks'>{targetWidth}m</span>"}
]
}
).OnlyShowIf(this._notCarFree),
).OnlyShowIf(this._carfree, true),
new TagRenderingOptions({

View file

@ -2,7 +2,6 @@ import {LayerDefinition} from "./LayerDefinition";
import {UIElement} from "../UI/UIElement";
import Translations from "../UI/i18n/Translations";
import Combine from "../UI/Base/Combine";
import {FixedUiElement} from "../UI/Base/FixedUiElement";
import {State} from "../State";
/**
@ -10,7 +9,7 @@ import {State} from "../State";
*/
export class Layout {
public name: string;
public id: string;
public icon: string = "./assets/logo.svg";
public title: UIElement;
public maintainer: string;
@ -25,20 +24,19 @@ export class Layout {
public welcomeBackMessage: UIElement;
public welcomeTail: UIElement;
public startzoom: number;
public supportedLanguages: string[];
public startzoom: number;
public startLon: number;
public startLat: number;
public locationContains: string[];
public enableAdd: boolean = true;
public enableUserBadge: boolean = true;
public enableSearch: boolean = true;
public enableLayers: boolean = true;
public enableMoreQuests: boolean = true;
public enableShareScreen: boolean = true;
public enableGeolocation: boolean = true;
public hideFromOverview: boolean = false;
/**
@ -47,11 +45,10 @@ export class Layout {
*/
public widenFactor: number = 0.07;
public defaultBackground: string = "osm";
public enableGeolocation: boolean = true;
/**
*
* @param name: The name used in the query string. If in the query "quests=<name>" is defined, it will select this layout
* @param id: The name used in the query string. If in the query "quests=<name>" is defined, it will select this layout
* @param title: Will be used in the <title> of the page
* @param layers: The layers to show, a list of LayerDefinitions
* @param startzoom: The initial starting zoom of the map
@ -63,7 +60,7 @@ export class Layout {
* @param welcomeTail: This text is shown below the login message. It is ideal for extra help
*/
constructor(
name: string,
id: string,
supportedLanguages: string[],
title: UIElement | string,
layers: LayerDefinition[],
@ -85,7 +82,7 @@ export class Layout {
this.startLon = startLon;
this.startLat = startLat;
this.startzoom = startzoom;
this.name = name;
this.id = id;
this.layers = layers;
this.welcomeMessage = Translations.W(welcomeMessage)
this.gettingStartedPlzLogin = Translations.W(gettingStartedPlzLogin);

View file

@ -1,20 +0,0 @@
import {Layout} from "../Layout";
export class All extends Layout{
constructor() {
super(
"all",
["en"],
"All quest layers",
[],
15,
51.2,
3.2,
"<h3>All quests of MapComplete</h3>" +
"This is a mixed bag. Some quests might be hard or for experts to answer only",
"Please log in",
""
);
this.hideFromOverview = true;
}
}

View file

@ -1,4 +1,3 @@
import {LayerDefinition} from "../LayerDefinition";
import Translations from "../../UI/i18n/Translations";
import {Layout} from "../Layout";
import {ClimbingTree} from "../Layers/ClimbingTree";

View file

@ -29,6 +29,7 @@ export default class Cyclofix extends Layout {
);
this.icon = "./assets/bike/logo.svg"
this.description = "Easily search and contribute bicycle data nearby";
this.socialImage = "./assets/bike/cyclofix.jpeg"
this.socialImage = "./assets/bike/cyclofix.jpeg";
this.widenFactor = 0.5;
}
}

View file

@ -1,7 +1,6 @@
import {Layout} from "../Layout";
import {GhostBike} from "../Layers/GhostBike";
import Combine from "../../UI/Base/Combine";
import Translations from "../../UI/i18n/Translations";
export class GhostBikes extends Layout {
constructor() {

View file

@ -1,6 +1,6 @@
import {Layout} from "../Layout";
import {LayerDefinition} from "../LayerDefinition";
import {Or, Tag} from "../../Logic/TagsFilter";
import {Or, Tag} from "../../Logic/Tags";
import {TagRenderingOptions} from "../TagRenderingOptions";

View file

@ -1,26 +1,28 @@
import {TagDependantUIElement, TagDependantUIElementConstructor} from "./UIElementConstructor";
import {TagsFilter, TagUtils} from "../Logic/Tags";
import {UIElement} from "../UI/UIElement";
import {UIEventSource} from "../Logic/UIEventSource";
import Translation from "../UI/i18n/Translation";
/**
* Wrapper around another TagDependandElement, which only shows if the filters match
*/
import {TagDependantUIElement, TagDependantUIElementConstructor} from "./UIElementConstructor";
import {TagsFilter, TagUtils} from "../Logic/TagsFilter";
import {UIElement} from "../UI/UIElement";
import {UIEventSource} from "../Logic/UIEventSource";
import {Changes} from "../Logic/Osm/Changes";
export class OnlyShowIfConstructor implements TagDependantUIElementConstructor{
private _tagsFilter: TagsFilter;
private _embedded: TagDependantUIElementConstructor;
constructor(tagsFilter : TagsFilter, embedded: TagDependantUIElementConstructor) {
private readonly _tagsFilter: TagsFilter;
private readonly _embedded: TagDependantUIElementConstructor;
private readonly _invert: boolean;
constructor(tagsFilter: TagsFilter, embedded: TagDependantUIElementConstructor, invert: boolean = false) {
this._tagsFilter = tagsFilter;
this._embedded = embedded;
this._invert = invert;
}
construct(dependencies): TagDependantUIElement {
return new OnlyShowIf(dependencies.tags,
this._embedded.construct(dependencies),
this._tagsFilter);
this._tagsFilter,
this._invert);
}
IsKnown(properties: any): boolean {
@ -41,34 +43,38 @@ export class OnlyShowIfConstructor implements TagDependantUIElementConstructor{
return this._embedded.Priority();
}
GetContent(tags: any): string {
if(!this.IsKnown(tags)){
GetContent(tags: any): Translation {
if(this.IsKnown(tags)){
return undefined;
}
return this._embedded.GetContent(tags);
}
private Matches(properties: any) : boolean{
return this._tagsFilter.matches(TagUtils.proprtiesToKV(properties));
return this._tagsFilter.matches(TagUtils.proprtiesToKV(properties)) != this._invert;
}
}
class OnlyShowIf extends UIElement implements TagDependantUIElement {
private _embedded: TagDependantUIElement;
private _filter: TagsFilter;
private readonly _embedded: TagDependantUIElement;
private readonly _filter: TagsFilter;
private readonly _invert: boolean;
constructor(
tags: UIEventSource<any>,
embedded: TagDependantUIElement, filter: TagsFilter) {
embedded: TagDependantUIElement,
filter: TagsFilter,
invert: boolean) {
super(tags);
this._filter = filter;
this._embedded = embedded;
this._invert = invert;
}
private Matches() : boolean{
return this._filter.matches(TagUtils.proprtiesToKV(this._source.data));
return this._filter.matches(TagUtils.proprtiesToKV(this._source.data)) != this._invert;
}
InnerRender(): string {

View file

@ -1,5 +1,4 @@
import {Changes} from "../../Logic/Osm/Changes";
import {And, Tag} from "../../Logic/TagsFilter";
import {And, Tag} from "../../Logic/Tags";
import {TagRenderingOptions} from "../TagRenderingOptions";
export class AccessTag extends TagRenderingOptions {

View file

@ -1,4 +1,3 @@
import {UIElement} from "../../UI/UIElement";
import {TagRenderingOptions} from "../TagRenderingOptions";
import Translation from "../../UI/i18n/Translation";

View file

@ -1,5 +1,4 @@
import {And, Tag} from "../../Logic/TagsFilter";
import {UIElement} from "../../UI/UIElement";
import {Tag} from "../../Logic/Tags";
import Translations from "../../UI/i18n/Translations";
import {TagRenderingOptions} from "../TagRenderingOptions";
import Translation from "../../UI/i18n/Translation";

View file

@ -3,7 +3,7 @@
* One is a big 'name-question', the other is the 'edit name' in the title.
* THis one is the big question
*/
import {Tag} from "../../Logic/TagsFilter";
import {Tag} from "../../Logic/Tags";
import {TagRenderingOptions} from "../TagRenderingOptions";
export class NameQuestion extends TagRenderingOptions{

View file

@ -1,5 +1,4 @@
import {Changes} from "../../Logic/Osm/Changes";
import {Tag} from "../../Logic/TagsFilter";
import {Tag} from "../../Logic/Tags";
import {TagRenderingOptions} from "../TagRenderingOptions";

View file

@ -1,5 +1,5 @@
import {Img} from "../../UI/Img";
import {Tag} from "../../Logic/TagsFilter";
import {Tag} from "../../Logic/Tags";
import {TagRenderingOptions} from "../TagRenderingOptions";

View file

@ -1,4 +1,4 @@
import {Tag} from "../../../Logic/TagsFilter";
import {Tag} from "../../../Logic/Tags";
import Translations from "../../../UI/i18n/Translations";
import {TagRenderingOptions} from "../../TagRenderingOptions";

View file

@ -1,4 +1,4 @@
import {Tag} from "../../../Logic/TagsFilter";
import {Tag} from "../../../Logic/Tags";
import Translations from "../../../UI/i18n/Translations";
import {TagRenderingOptions} from "../../TagRenderingOptions";

View file

@ -1,4 +1,4 @@
import {Tag} from "../../../Logic/TagsFilter";
import {Tag} from "../../../Logic/Tags";
import Translations from "../../../UI/i18n/Translations";
import {TagRenderingOptions} from "../../TagRenderingOptions";

View file

@ -1,4 +1,4 @@
import { Tag } from "../../../Logic/TagsFilter";
import { Tag } from "../../../Logic/Tags";
import Translations from "../../../UI/i18n/Translations";
import {TagRenderingOptions} from "../../TagRenderingOptions";

View file

@ -1,5 +1,4 @@
import Translations from "../../../UI/i18n/Translations";
import Combine from "../../../UI/Base/Combine";
import {TagRenderingOptions} from "../../TagRenderingOptions";

View file

@ -1,4 +1,4 @@
import { Tag } from "../../../Logic/TagsFilter";
import { Tag } from "../../../Logic/Tags";
import Translations from "../../../UI/i18n/Translations";
import {TagRenderingOptions} from "../../TagRenderingOptions";

View file

@ -1,4 +1,4 @@
import {Tag, And} from "../../../Logic/TagsFilter";
import {Tag, And} from "../../../Logic/Tags";
import Translations from "../../../UI/i18n/Translations";
import {TagRenderingOptions} from "../../TagRenderingOptions";

View file

@ -1,4 +1,4 @@
import {Tag} from "../../../Logic/TagsFilter";
import {Tag} from "../../../Logic/Tags";
import Translations from "../../../UI/i18n/Translations";
import Combine from "../../../UI/Base/Combine";
import {TagRenderingOptions} from "../../TagRenderingOptions";

View file

@ -1,4 +1,4 @@
import {Tag} from "../../../Logic/TagsFilter";
import {Tag} from "../../../Logic/Tags";
import Translations from "../../../UI/i18n/Translations";
import {TagRenderingOptions} from "../../TagRenderingOptions";

View file

@ -1,4 +1,4 @@
import {Tag} from "../../../Logic/TagsFilter";
import {Tag} from "../../../Logic/Tags";
import Translations from "../../../UI/i18n/Translations";
import {TagRenderingOptions} from "../../TagRenderingOptions";

View file

@ -1,4 +1,4 @@
import {Tag} from "../../../Logic/TagsFilter";
import {Tag} from "../../../Logic/Tags";
import Translations from "../../../UI/i18n/Translations";
import {TagRenderingOptions} from "../../TagRenderingOptions";

View file

@ -1,4 +1,4 @@
import {Tag} from "../../../Logic/TagsFilter";
import {Tag} from "../../../Logic/Tags";
import Translations from "../../../UI/i18n/Translations";
import {TagRenderingOptions} from "../../TagRenderingOptions";

View file

@ -1,4 +1,4 @@
import {Tag} from "../../../Logic/TagsFilter";
import {Tag} from "../../../Logic/Tags";
import Translations from "../../../UI/i18n/Translations";
import {TagRenderingOptions} from "../../TagRenderingOptions";

View file

@ -1,4 +1,4 @@
import {Tag} from "../../../Logic/TagsFilter";
import {Tag} from "../../../Logic/Tags";
import Translations from "../../../UI/i18n/Translations";
import {TagRenderingOptions} from "../../TagRenderingOptions";

View file

@ -1,4 +1,4 @@
import {Tag} from "../../../Logic/TagsFilter";
import {Tag} from "../../../Logic/Tags";
import Translations from "../../../UI/i18n/Translations";
import {TagRenderingOptions} from "../../TagRenderingOptions";

View file

@ -1,4 +1,4 @@
import {Tag} from "../../../Logic/TagsFilter";
import {Tag} from "../../../Logic/Tags";
import Translations from "../../../UI/i18n/Translations";
import {TagRenderingOptions} from "../../TagRenderingOptions";

View file

@ -1,4 +1,4 @@
import {Tag} from "../../../Logic/TagsFilter";
import {Tag} from "../../../Logic/Tags";
import Translations from "../../../UI/i18n/Translations";
import {TagRenderingOptions} from "../../TagRenderingOptions";

View file

@ -1,4 +1,4 @@
import {Tag} from "../../../Logic/TagsFilter";
import {Tag} from "../../../Logic/Tags";
import Translations from "../../../UI/i18n/Translations";
import {TagRenderingOptions} from "../../TagRenderingOptions";

View file

@ -1,29 +0,0 @@
import {Tag} from "../../../Logic/TagsFilter";
import {TagRenderingOptions} from "../../TagRenderingOptions";
/**
* Currently not used in Cyclofix because it's a little vague
*
* TODO: Translations
*/
export default class BikeStationBrand extends TagRenderingOptions {
private static options = {
priority: 15,
question: "What is the brand of this bike station (name of university, shop, city...)?",
freeform: {
key: "brand",
template: "The brand of this bike station is $$$",
renderTemplate: "The brand of this bike station is {operator}",
placeholder: "brand"
},
mappings: [
{k: new Tag("brand", "Velo Fix Station"), txt: "Velo Fix Station"}
]
}
constructor() {
throw Error('BikeStationBrand disabled')
super(BikeStationBrand.options);
}
}

View file

@ -1,4 +1,4 @@
import {Tag} from "../../../Logic/TagsFilter";
import {Tag} from "../../../Logic/Tags";
import Translations from "../../../UI/i18n/Translations";
import {TagRenderingOptions} from "../../TagRenderingOptions";

View file

@ -1,4 +1,4 @@
import {Tag} from "../../../Logic/TagsFilter";
import {Tag} from "../../../Logic/Tags";
import Translations from "../../../UI/i18n/Translations";
import {TagRenderingOptions} from "../../TagRenderingOptions";

View file

@ -1,4 +1,4 @@
import {Tag, And} from "../../../Logic/TagsFilter";
import {Tag, And} from "../../../Logic/Tags";
import Translations from "../../../UI/i18n/Translations";
import {TagRenderingOptions} from "../../TagRenderingOptions";

View file

@ -1,4 +1,4 @@
import {Tag} from "../../../Logic/TagsFilter";
import {Tag} from "../../../Logic/Tags";
import Translations from "../../../UI/i18n/Translations";
import {TagRenderingOptions} from "../../TagRenderingOptions";

View file

@ -1,6 +1,6 @@
import {UIElement} from "../UI/UIElement";
import {UIEventSource} from "../Logic/UIEventSource";
import {And, Tag, TagsFilter, TagUtils} from "../Logic/TagsFilter";
import {And, Tag, TagsFilter, TagUtils} from "../Logic/Tags";
import {FixedUiElement} from "../UI/Base/FixedUiElement";
import {SaveButton} from "../UI/SaveButton";
import {VariableUiElement} from "../UI/Base/VariableUIElement";
@ -15,23 +15,21 @@ import Locale from "../UI/i18n/Locale";
import {State} from "../State";
import {TagRenderingOptions} from "./TagRenderingOptions";
import Translation from "../UI/i18n/Translation";
import {SubtleButton} from "../UI/Base/SubtleButton";
import Combine from "../UI/Base/Combine";
export class TagRendering extends UIElement implements TagDependantUIElement {
export class
TagRendering extends UIElement implements TagDependantUIElement {
private _priority: number;
private _question: string | Translation;
private _mapping: { k: TagsFilter, txt: string | UIElement, priority?: number }[];
private readonly _priority: number;
private readonly _question: string | Translation;
private readonly _mapping: { k: TagsFilter, txt: string | UIElement, priority?: number }[];
private currentTags : UIEventSource<any> ;
private _freeform: {
private readonly _freeform: {
key: string,
template: string | UIElement,
renderTemplate: string | Translation,
@ -53,10 +51,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
private readonly _questionSkipped: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly _editMode: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private static injected = TagRendering.injectFunction();
static injectFunction() {
// This is a workaround as not to import tagrendering into TagREnderingOptions
TagRenderingOptions.tagRendering = (tags, options) => new TagRendering(tags, options);
@ -76,7 +71,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
extraTags?: TagsFilter,
},
tagsPreprocessor?: ((tags: any) => any),
mappings?: { k: TagsFilter, txt: string | Translation, priority?: number, substitute?: boolean }[]
mappings?: { k: TagsFilter, txt: string | Translation, priority?: number, substitute?: boolean, hideInAnswer?: boolean }[]
}) {
super(tags);
this.ListenTo(Locale.language);
@ -204,7 +199,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
placeholder?: string | Translation,
extraTags?: TagsFilter,
},
mappings?: { k: TagsFilter, txt: string | Translation, priority?: number, substitute?: boolean }[]
mappings?: { k: TagsFilter, txt: string | Translation, priority?: number, substitute?: boolean, hideInAnswer?: boolean }[]
}):
InputElement<TagsFilter> {
@ -217,7 +212,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
if(mapping.k === null){
continue;
}
if(previousTexts.indexOf(mapping.txt) >= 0){
if(mapping.hideInAnswer){
continue;
}
previousTexts.push(this.ApplyTemplate(mapping.txt));
@ -262,7 +257,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
let isValid = ValidatedTextField.inputValidation[type];
if (isValid === undefined) {
isValid = (str) => true;
isValid = () => true;
}
let formatter = ValidatedTextField.formatting[type] ?? ((str) => str);
@ -297,7 +292,6 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
}
let inputElement: InputElement<TagsFilter>;
const textField = new TextField({
placeholder: this._freeform.placeholder,
fromString: pickString,
@ -455,9 +449,9 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
private ApplyTemplate(template: string | Translation): UIElement {
if (template === undefined || template === null) {
throw "Trying to apply a template, but the template is null/undefined"
console.warn("Applying template which is undefined by ",this); // TODO THis error msg can probably be removed
return undefined;
}
const self = this;
return new VariableUiElement(this.currentTags.map(tags => {
const tr = Translations.WT(template);
if (tr.Subs === undefined) {

View file

@ -1,21 +1,15 @@
import {Dependencies, TagDependantUIElement, TagDependantUIElementConstructor} from "./UIElementConstructor";
import * as EmailValidator from "email-validator";
import {parsePhoneNumberFromString} from "libphonenumber-js";
import {UIElement} from "../UI/UIElement";
import {TagsFilter, TagUtils} from "../Logic/TagsFilter";
import {TagsFilter, TagUtils} from "../Logic/Tags";
import {OnlyShowIfConstructor} from "./OnlyShowIf";
import {UIEventSource} from "../Logic/UIEventSource";
import Translation from "../UI/i18n/Translation";
import Translations from "../UI/i18n/Translations";
export class TagRenderingOptions implements TagDependantUIElementConstructor {
/**
* Notes: by not giving a 'question', one disables the question form alltogether
*/
public options: {
priority?: number;
question?: string | Translation;
@ -27,10 +21,9 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor {
placeholder?: string | Translation;
extraTags?: TagsFilter
};
mappings?: { k: TagsFilter; txt: string | Translation; priority?: number, substitute?: boolean }[]
mappings?: { k: TagsFilter; txt: string | Translation; priority?: number, substitute?: boolean, hideInAnwser?: boolean }[]
};
constructor(options: {
@ -61,7 +54,7 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor {
*
*
*/
mappings?: { k: TagsFilter, txt: Translation | string, priority?: number, substitute?: boolean }[],
mappings?: { k: TagsFilter, txt: Translation | string, priority?: number, substitute?: boolean , hideInAnswer?:boolean}[],
/**
@ -85,12 +78,11 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor {
*/
tagsPreprocessor?: ((tags: any) => void)
}) {
this.options = options;
}
OnlyShowIf(tagsFilter: TagsFilter): TagDependantUIElementConstructor {
return new OnlyShowIfConstructor(tagsFilter, this);
OnlyShowIf(tagsFilter: TagsFilter, invert: boolean = false): TagDependantUIElementConstructor {
return new OnlyShowIfConstructor(tagsFilter, this, invert);
}
@ -105,39 +97,28 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor {
if (this.options.freeform !== undefined && tags[this.options.freeform.key] !== undefined) {
return false;
}
if (this.options.question === undefined) {
return false;
}
return true;
return this.options.question !== undefined;
}
GetContent(tags: any): string {
GetContent(tags: any): Translation {
const tagsKV = TagUtils.proprtiesToKV(tags);
for (const oneOnOneElement of this.options.mappings ?? []) {
if (oneOnOneElement.k === null || oneOnOneElement.k.matches(tagsKV)) {
const mapping = oneOnOneElement.txt;
if (typeof (mapping) === "string") {
return mapping;
} else {
return mapping.InnerRender();
}
return Translations.WT(oneOnOneElement.txt);
}
}
if (this.options.freeform !== undefined) {
let template = this.options.freeform.renderTemplate;
if (typeof (template) !== "string") {
template = template.InnerRender();
}
return TagUtils.ApplyTemplate(template, tags);
let template = Translations.WT(this.options.freeform.renderTemplate);
return template.Subs(tags);
}
console.warn("No content defined for",tags," with mapping",this);
return undefined;
}
public static tagRendering: (tags: UIEventSource<any>, options: { priority?: number; question?: string | Translation; freeform?: { key: string; tagsPreprocessor?: (tags: any) => any; template: string | Translation; renderTemplate: string | Translation; placeholder?: string | Translation; extraTags?: TagsFilter }; mappings?: { k: TagsFilter; txt: string | Translation; priority?: number; substitute?: boolean }[] }) => TagDependantUIElement;
public static tagRendering: (tags: UIEventSource<any>, options: { priority?: number; question?: string | Translation; freeform?: { key: string; tagsPreprocessor?: (tags: any) => any; template: string | Translation; renderTemplate: string | Translation; placeholder?: string | Translation; extraTags?: TagsFilter }; mappings?: { k: TagsFilter; txt: string | Translation; priority?: number; substitute?: boolean, hideInAnswer?: boolean }[] }) => TagDependantUIElement;
construct(dependencies: Dependencies): TagDependantUIElement {
return TagRenderingOptions.tagRendering(dependencies.tags, this.options);

View file

@ -1,5 +1,6 @@
import {UIElement} from "../UI/UIElement";
import {UIEventSource} from "../Logic/UIEventSource";
import Translation from "../UI/i18n/Translation";
export interface Dependencies {
@ -12,7 +13,7 @@ export interface TagDependantUIElementConstructor {
IsKnown(properties: any): boolean;
IsQuestioning(properties: any): boolean;
Priority(): number;
GetContent(tags: any): string;
GetContent(tags: any): Translation;
}