forked from MapComplete/MapComplete
Merge branch 'master' into master
This commit is contained in:
commit
3bfa3dbfc6
99 changed files with 4967 additions and 542 deletions
|
@ -22,6 +22,10 @@ import * as tree_nodes from "../assets/layers/trees/tree_nodes.json"
|
|||
import * as benches from "../assets/layers/benches/benches.json"
|
||||
import * as benches_at_pt from "../assets/layers/benches/benches_at_pt.json"
|
||||
import * as picnic_tables from "../assets/layers/benches/picnic_tables.json"
|
||||
import * as play_forest from "../assets/layers/play_forest/play_forest.json"
|
||||
import * as playground from "../assets/layers/playground/playground.json"
|
||||
import * as sport_pitch from "../assets/layers/sport_pitch/sport_pitch.json"
|
||||
import * as slow_roads from "../assets/layers/slow_roads/slow_roads.json"
|
||||
import LayerConfig from "./JSON/LayerConfig";
|
||||
import {LayerConfigJson} from "./JSON/LayerConfigJson";
|
||||
|
||||
|
@ -52,7 +56,11 @@ export default class AllKnownLayers {
|
|||
tree_nodes,
|
||||
benches,
|
||||
benches_at_pt,
|
||||
picnic_tables
|
||||
picnic_tables,
|
||||
play_forest,
|
||||
playground,
|
||||
sport_pitch,
|
||||
slow_roads
|
||||
];
|
||||
|
||||
// Must be below the list...
|
||||
|
|
|
@ -22,6 +22,9 @@ import * as personal from "../assets/themes/personalLayout/personalLayout.json"
|
|||
import * as playgrounds from "../assets/themes/playgrounds/playgrounds.json"
|
||||
import * as bicycle_lib from "../assets/themes/bicycle_library/bicycle_library.json"
|
||||
import * as campersites from "../assets/themes/campersites/campersites.json"
|
||||
import * as play_forests from "../assets/themes/play_forests/play_forests.json"
|
||||
import * as speelplekken from "../assets/themes/speelplekken/speelplekken.json"
|
||||
import * as sport_pitches from "../assets/themes/sport_pitches/sport_pitches.json"
|
||||
import LayerConfig from "./JSON/LayerConfig";
|
||||
import LayoutConfig from "./JSON/LayoutConfig";
|
||||
import AllKnownLayers from "./AllKnownLayers";
|
||||
|
@ -71,7 +74,10 @@ export class AllKnownLayouts {
|
|||
new LayoutConfig(climbing),
|
||||
new LayoutConfig(playgrounds),
|
||||
new LayoutConfig(trees),
|
||||
new LayoutConfig(campersites)
|
||||
new LayoutConfig(campersites),
|
||||
new LayoutConfig(play_forests) ,
|
||||
new LayoutConfig(speelplekken),
|
||||
new LayoutConfig(sport_pitches)
|
||||
];
|
||||
|
||||
|
||||
|
|
|
@ -48,25 +48,27 @@ export default class LayerConfig {
|
|||
}[];
|
||||
|
||||
tagRenderings: TagRenderingConfig [];
|
||||
|
||||
private readonly configuration_warnings : string[] = []
|
||||
|
||||
constructor(json: LayerConfigJson,
|
||||
context?: string) {
|
||||
context = context + "." + json.id;
|
||||
const self = this;
|
||||
this.id = json.id;
|
||||
this.name = Translations.T(json.name);
|
||||
this.description = Translations.T(json.description);
|
||||
this.name = Translations.T(json.name, context+".name");
|
||||
this.description = Translations.T(json.description, context+".description");
|
||||
this.overpassTags = FromJSON.Tag(json.overpassTags, context + ".overpasstags");
|
||||
this.doNotDownload = json.doNotDownload ?? false,
|
||||
this.passAllFeatures = json.passAllFeatures ?? false;
|
||||
this.minzoom = json.minzoom;
|
||||
this.wayHandling = json.wayHandling ?? 0;
|
||||
this.hideUnderlayingFeaturesMinPercentage = json.hideUnderlayingFeaturesMinPercentage ?? 0;
|
||||
this.presets = (json.presets ?? []).map(pr =>
|
||||
this.presets = (json.presets ?? []).map((pr, i) =>
|
||||
({
|
||||
title: Translations.T(pr.title),
|
||||
title: Translations.T(pr.title, `${context}.presets[${i}].title`),
|
||||
tags: pr.tags.map(t => FromJSON.SimpleTag(t)),
|
||||
description: Translations.T(pr.description)
|
||||
description: Translations.T(pr.description, `${context}.presets[${i}].description`)
|
||||
}))
|
||||
|
||||
|
||||
|
|
|
@ -140,8 +140,20 @@ export interface LayerConfigJson {
|
|||
* NB: if no presets are defined, the popup to add new points doesn't show up at all
|
||||
*/
|
||||
presets?: {
|
||||
/**
|
||||
* The title - shown on the 'add-new'-button.
|
||||
*/
|
||||
title: string | any,
|
||||
/**
|
||||
* The tags to add. It determines the icon too
|
||||
*/
|
||||
tags: string[],
|
||||
/**
|
||||
* The _first sentence_ of the description is shown on the button of the `add` menu.
|
||||
* The full description is shown in the confirmation dialog.
|
||||
*
|
||||
* (The first sentence is until the first '.'-character in the description)
|
||||
*/
|
||||
description?: string | any,
|
||||
}[],
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ export default class LayoutConfig {
|
|||
};
|
||||
|
||||
public readonly hideFromOverview: boolean;
|
||||
public readonly lockLocation: boolean | [[number,number],[number, number]];
|
||||
public readonly enableUserBadge: boolean;
|
||||
public readonly enableShareScreen: boolean;
|
||||
public readonly enableMoreQuests: boolean;
|
||||
|
@ -53,6 +54,9 @@ export default class LayoutConfig {
|
|||
} else {
|
||||
this.language = json.language;
|
||||
}
|
||||
if(this.language.length == 0){
|
||||
throw "No languages defined. Define at least one language"
|
||||
}
|
||||
if (json.title === undefined) {
|
||||
throw "Title not defined in " + this.id;
|
||||
}
|
||||
|
@ -62,7 +66,7 @@ export default class LayoutConfig {
|
|||
this.title = new Translation(json.title, context + ".title");
|
||||
this.description = new Translation(json.description, context + ".description");
|
||||
this.shortDescription = json.shortDescription === undefined ? this.description.FirstSentence() : new Translation(json.shortDescription, context + ".shortdescription");
|
||||
this.descriptionTail = json.descriptionTail === undefined ? new Translation({"*": ""}, context) : new Translation(json.descriptionTail, context + ".descriptionTail");
|
||||
this.descriptionTail = json.descriptionTail === undefined ? new Translation({"*": ""}, context+".descriptionTail") : new Translation(json.descriptionTail, context + ".descriptionTail");
|
||||
this.icon = json.icon;
|
||||
this.socialImage = json.socialImage;
|
||||
this.startZoom = json.startZoom;
|
||||
|
@ -103,7 +107,7 @@ export default class LayoutConfig {
|
|||
return new LayerConfig(layer, `${this.id}.layers[${i}]`)
|
||||
});
|
||||
|
||||
// ALl the layers are constructed, let them share tags in piece now!
|
||||
// ALl the layers are constructed, let them share tags in now!
|
||||
const roaming : {r, source: LayerConfig}[] = []
|
||||
for (const layer of this.layers) {
|
||||
roaming.push({r: layer.GetRoamingRenderings(), source:layer});
|
||||
|
@ -117,6 +121,17 @@ export default class LayoutConfig {
|
|||
layer.AddRoamingRenderings(r.r);
|
||||
}
|
||||
}
|
||||
|
||||
for(const layer of this.layers) {
|
||||
layer.AddRoamingRenderings(
|
||||
{
|
||||
titleIcons:[],
|
||||
iconOverlays: [],
|
||||
tagRenderings: this.roamingRenderings
|
||||
}
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
this.clustering = {
|
||||
maxZoom: 16,
|
||||
|
@ -135,7 +150,7 @@ export default class LayoutConfig {
|
|||
}
|
||||
|
||||
this.hideFromOverview = json.hideFromOverview ?? false;
|
||||
|
||||
this.lockLocation = json.lockLocation ?? false;
|
||||
this.enableUserBadge = json.enableUserBadge ?? true;
|
||||
this.enableShareScreen = json.enableShareScreen ?? true;
|
||||
this.enableMoreQuests = json.enableMoreQuests ?? true;
|
||||
|
|
|
@ -158,6 +158,13 @@ export interface LayoutConfigJson {
|
|||
*/
|
||||
hideFromOverview?: boolean;
|
||||
|
||||
/**
|
||||
* If set to true, the basemap will not scroll outside of the area visible on initial zoom.
|
||||
* If set to [[lat0, lon0], [lat1, lon1]], the map will not scroll outside of those bounds.
|
||||
* Off by default, which will enable panning to the entire world
|
||||
*/
|
||||
lockLocation?: boolean | [[number, number], [number, number]];
|
||||
|
||||
enableUserBadge?: boolean;
|
||||
enableShareScreen?: boolean;
|
||||
enableMoreQuests?: boolean;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {And, TagsFilter} from "../../Logic/Tags";
|
||||
import {And, TagsFilter, TagUtils} from "../../Logic/Tags";
|
||||
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
|
||||
import Translations from "../../UI/i18n/Translations";
|
||||
import {FromJSON} from "./FromJSON";
|
||||
|
@ -15,6 +15,8 @@ export default class TagRenderingConfig {
|
|||
readonly render?: Translation;
|
||||
readonly question?: Translation;
|
||||
readonly condition?: TagsFilter;
|
||||
|
||||
readonly configuration_warnings : string[] = []
|
||||
|
||||
readonly freeform?: {
|
||||
readonly key: string,
|
||||
|
@ -45,13 +47,13 @@ export default class TagRenderingConfig {
|
|||
throw "Initing a TagRenderingConfig with undefined in " + context;
|
||||
}
|
||||
if (typeof json === "string") {
|
||||
this.render = Translations.T(json);
|
||||
this.render = Translations.T(json, context+".render");
|
||||
this.multiAnswer = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.render = Translations.T(json.render);
|
||||
this.question = Translations.T(json.question);
|
||||
this.render = Translations.T(json.render, context+".render");
|
||||
this.question = Translations.T(json.question, context+".question");
|
||||
this.roaming = json.roaming ?? false;
|
||||
const condition = FromJSON.Tag(json.condition ?? {"and": []}, `${context}.condition`);
|
||||
if (this.roaming && conditionIfRoaming !== undefined) {
|
||||
|
@ -66,15 +68,27 @@ export default class TagRenderingConfig {
|
|||
addExtraTags: json.freeform.addExtraTags?.map((tg, i) =>
|
||||
FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? []
|
||||
}
|
||||
if(this.freeform.key === undefined || this.freeform.key === ""){
|
||||
throw `Freeform.key is undefined or the empty string - this is not allowed; either fill out something or remove the freeform block alltogether. Error in ${context}`
|
||||
}
|
||||
if (ValidatedTextField.AllTypes[this.freeform.type] === undefined) {
|
||||
throw `Freeform.key ${this.freeform.key} is an invalid type`
|
||||
}
|
||||
if(this.freeform.addExtraTags){
|
||||
const usedKeys = new And(this.freeform.addExtraTags).usedKeys();
|
||||
if(usedKeys.indexOf(this.freeform.key) >= 0){
|
||||
throw `The freeform key ${this.freeform.key} will be overwritten by one of the extra tags, as they use the same key too. This is in ${context}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.multiAnswer = json.multiAnswer ?? false
|
||||
if (json.mappings) {
|
||||
|
||||
|
||||
this.mappings = json.mappings.map((mapping, i) => {
|
||||
|
||||
|
||||
if (mapping.then === undefined) {
|
||||
throw `${context}.mapping[${i}]: Invalid mapping: if without body`
|
||||
}
|
||||
|
@ -87,14 +101,15 @@ export default class TagRenderingConfig {
|
|||
} else if (mapping.hideInAnswer !== undefined) {
|
||||
hideInAnswer = FromJSON.Tag(mapping.hideInAnswer, `${context}.mapping[${i}].hideInAnswer`);
|
||||
}
|
||||
const mappingContext = `${context}.mapping[${i}]`
|
||||
const mp = {
|
||||
if: FromJSON.Tag(mapping.if, `${context}.mapping[${i}].if`),
|
||||
ifnot: (mapping.ifnot !== undefined ? FromJSON.Tag(mapping.ifnot, `${context}.mapping[${i}].ifnot`) : undefined),
|
||||
then: Translations.T(mapping.then),
|
||||
if: FromJSON.Tag(mapping.if, `${mappingContext}.if`),
|
||||
ifnot: (mapping.ifnot !== undefined ? FromJSON.Tag(mapping.ifnot, `${mappingContext}.ifnot`) : undefined),
|
||||
then: Translations.T(mapping.then, `{mappingContext}.then`),
|
||||
hideInAnswer: hideInAnswer
|
||||
};
|
||||
if (this.question) {
|
||||
if (hideInAnswer !== true && !mp.if.isUsableAsAnswer()) {
|
||||
if (hideInAnswer !== true && mp.if !== undefined && !mp.if.isUsableAsAnswer()) {
|
||||
throw `${context}.mapping[${i}].if: This value cannot be used to answer a question, probably because it contains a regex or an OR. Either change it or set 'hideInAnswer'`
|
||||
}
|
||||
|
||||
|
@ -114,6 +129,36 @@ export default class TagRenderingConfig {
|
|||
if (this.freeform && this.render === undefined) {
|
||||
throw `${context}: Detected a freeform key without rendering... Key: ${this.freeform.key} in ${context}`
|
||||
}
|
||||
|
||||
if(this.render && this.question && this.freeform === undefined){
|
||||
throw `${context}: Detected a tagrendering which takes input without freeform key in ${context}`
|
||||
}
|
||||
|
||||
if(!json.multiAnswer && this.mappings !== undefined && this.question !== undefined){
|
||||
let keys = []
|
||||
for (let i = 0; i < this.mappings.length; i++){
|
||||
const mapping = this.mappings[i];
|
||||
if(mapping.if === undefined){
|
||||
throw `${context}.mappings[${i}].if is undefined`
|
||||
}
|
||||
keys.push(...mapping.if.usedKeys())
|
||||
}
|
||||
keys = Utils.Dedup(keys)
|
||||
for (let i = 0; i < this.mappings.length; i++){
|
||||
const mapping = this.mappings[i];
|
||||
if(mapping.hideInAnswer){
|
||||
continue
|
||||
}
|
||||
|
||||
const usedKeys = mapping.if.usedKeys();
|
||||
for (const expectedKey of keys) {
|
||||
if(usedKeys.indexOf(expectedKey) < 0){
|
||||
const msg = `${context}.mappings[${i}]: This mapping only defines values for ${usedKeys.join(', ')}, but it should also give a value for ${expectedKey}`
|
||||
this.configuration_warnings.push(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.question !== undefined && json.multiAnswer) {
|
||||
if ((this.mappings?.length ?? 0) === 0) {
|
||||
|
@ -139,6 +184,40 @@ export default class TagRenderingConfig {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if it is known or not shown, false if the question should be asked
|
||||
* @constructor
|
||||
*/
|
||||
public IsKnown(tags: any): boolean {
|
||||
if (this.condition &&
|
||||
!this.condition.matchesProperties(tags)) {
|
||||
// Filtered away by the condition
|
||||
return true;
|
||||
}
|
||||
if(this.multiAnswer){
|
||||
for (const m of this.mappings) {
|
||||
if(TagUtils.MatchesMultiAnswer(m.if, tags)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const free = this.freeform?.key
|
||||
if(free !== undefined){
|
||||
return tags[free] !== undefined
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
if (this.GetRenderValue(tags) !== undefined) {
|
||||
// This value is known and can be rendered
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the correct rendering value (or undefined if not known)
|
||||
* @constructor
|
||||
|
|
|
@ -49,14 +49,64 @@ export interface TagRenderingConfigJson {
|
|||
* Allows fixed-tag inputs, shown either as radiobuttons or as checkboxes
|
||||
*/
|
||||
mappings?: {
|
||||
/**
|
||||
* If this condition is met, then the text under `then` will be shown.
|
||||
* If no value matches, and the user selects this mapping as an option, then these tags will be uploaded to OSM.
|
||||
*/
|
||||
if: AndOrTagConfigJson | string,
|
||||
/**
|
||||
* Only applicable if 'multiAnswer' is set.
|
||||
* This tag is applied if the respective checkbox is unset
|
||||
* If the condition `if` is met, the text `then` will be rendered.
|
||||
* If not known yet, the user will be presented with `then` as an option
|
||||
*/
|
||||
ifnot?: AndOrTagConfigJson | string,
|
||||
then: string | any
|
||||
hideInAnswer?: boolean
|
||||
then: string | any,
|
||||
/**
|
||||
* In some cases, multiple taggings exist (e.g. a default assumption, or a commonly mapped abbreviation and a fully written variation).
|
||||
*
|
||||
* In the latter case, a correct text should be shown, but only a single, canonical tagging should be selectable by the user.
|
||||
* In this case, one of the mappings can be hiden by setting this flag.
|
||||
*
|
||||
* To demonstrate an example making a default assumption:
|
||||
*
|
||||
* mappings: [
|
||||
* {
|
||||
* if: "access=", -- no access tag present, we assume accessible
|
||||
* then: "Accessible to the general public",
|
||||
* hideInAnswer: true
|
||||
* },
|
||||
* {
|
||||
* if: "access=yes",
|
||||
* then: "Accessible to the general public", -- the user selected this, we add that to OSM
|
||||
* },
|
||||
* {
|
||||
* if: "access=no",
|
||||
* then: "Not accessible to the public"
|
||||
* }
|
||||
* ]
|
||||
*
|
||||
*
|
||||
* For example, for an operator, we have `operator=Agentschap Natuur en Bos`, which is often abbreviated to `operator=ANB`.
|
||||
* Then, we would add two mappings:
|
||||
* {
|
||||
* if: "operator=Agentschap Natuur en Bos" -- the non-abbreviated version which should be uploaded
|
||||
* then: "Maintained by Agentschap Natuur en Bos"
|
||||
* },
|
||||
* {
|
||||
* if: "operator=ANB", -- we don't want to upload abbreviations
|
||||
* then: "Maintained by Agentschap Natuur en Bos"
|
||||
* hideInAnswer: true
|
||||
* }
|
||||
*/
|
||||
hideInAnswer?: boolean,
|
||||
/**
|
||||
* Only applicable if 'multiAnswer' is set.
|
||||
* This is for situations such as:
|
||||
* `accepts:coins=no` where one can select all the possible payment methods. However, we want to make explicit that some options _were not_ selected.
|
||||
* This can be done with `ifnot`
|
||||
* Note that we can not explicitly render this negative case to the user, we cannot show `does _not_ accept coins`.
|
||||
* If this is important to your usecase, consider using multiple radiobutton-fields without `multiAnswer`
|
||||
*/
|
||||
ifnot?: AndOrTagConfigJson | string
|
||||
|
||||
}[]
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue