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[],
@ -76,8 +75,7 @@ export class LayerDefinition {
color: string,
weight?: number,
icon: {
iconUrl: string,
iconSize: number[],
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;
private readonly _tagsFilter: TagsFilter;
private readonly _embedded: TagDependantUIElementConstructor;
private readonly _invert: boolean;
constructor(tagsFilter : TagsFilter, embedded: TagDependantUIElementConstructor) {
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,
@ -54,9 +52,6 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
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;
}

View file

@ -1,5 +1,3 @@
import {Layout} from "./Customizations/Layout";
import Locale from "./UI/i18n/Locale";
import Translations from "./UI/i18n/Translations";
import {TabbedComponent} from "./UI/Base/TabbedComponent";
import {ShareScreen} from "./UI/ShareScreen";
@ -8,12 +6,8 @@ import {CheckBox} from "./UI/Input/CheckBox";
import Combine from "./UI/Base/Combine";
import {UIElement} from "./UI/UIElement";
import {MoreScreen} from "./UI/MoreScreen";
import {Tag} from "./Logic/TagsFilter";
import {FilteredLayer} from "./Logic/FilteredLayer";
import {FeatureInfoBox} from "./UI/FeatureInfoBox";
import {ElementStorage} from "./Logic/ElementStorage";
import {Changes} from "./Logic/Osm/Changes";
import {OsmConnection} from "./Logic/Osm/OsmConnection";
import {BaseLayers, Basemap} from "./Logic/Leaflet/Basemap";
import {State} from "./State";
import {WelcomeMessage} from "./UI/WelcomeMessage";
@ -50,30 +44,30 @@ export class InitUiElements {
const layoutToUse = State.state.layoutToUse.data;
let welcome: UIElement = new WelcomeMessage();
if (layoutToUse.name === PersonalLayout.NAME) {
if (layoutToUse.id === PersonalLayout.NAME) {
welcome = new PersonalLayersPanel();
}
const tabs = [
{header: Img.AsImageElement(layoutToUse.icon), content: welcome},
{header: `<img src='${'./assets/osm-logo.svg'}'>`, content: Translations.t.general.openStreetMapIntro},
{header: `<img src='./assets/osm-logo.svg'>`, content: Translations.t.general.openStreetMapIntro},
]
if (State.state.featureSwitchShareScreen.data) {
tabs.push({header: `<img src='${'./assets/share.svg'}'>`, content: new ShareScreen()});
tabs.push({header: `<img src='./assets/share.svg'>`, content: new ShareScreen()});
}
if (State.state.featureSwitchMoreQuests.data){
tabs.push({
header: `<img src='${'./assets/add.svg'}'>`
header: `<img src='./assets/add.svg'>`
, content: new MoreScreen()
});
}
const fullOptions = new TabbedComponent(tabs);
const fullOptions = new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab);
return fullOptions;
@ -94,7 +88,6 @@ export class InitUiElements {
new Combine(["<span class='open-button'>", help, "</span>"])
, true
).AttachTo("messagesbox");
let dontCloseYet = true;
const openedTime = new Date().getTime();
State.state.locationControl.addCallback(() => {
if (new Date().getTime() - openedTime < 15 * 1000) {
@ -107,7 +100,7 @@ export class InitUiElements {
const fullOptions2 = this.CreateWelcomePane();
State.state.fullScreenMessage.setData(fullOptions2)
new FixedUiElement(`<div class='collapse-button-img' class="shadow"><img src='assets/help.svg' alt='help'></div>`).onClick(() => {
new FixedUiElement(`<div class='collapse-button-img shadow'><img src='assets/help.svg' alt='help'></div>`).onClick(() => {
State.state.fullScreenMessage.setData(fullOptions2)
}).AttachTo("help-button-mobile");

View file

@ -48,9 +48,4 @@ export class ElementStorage {
}
console.log("Can not find eventsource with id ", elementId);
}
removeId(oldId: string) {
delete this._elements[oldId];
}
}

View file

@ -1,4 +1,4 @@
import {TagsFilter, TagUtils} from "./TagsFilter";
import {TagsFilter, TagUtils} from "./Tags";
import {UIEventSource} from "./UIEventSource";
import L from "leaflet"
import {GeoOperations} from "./GeoOperations";
@ -21,6 +21,7 @@ export class FilteredLayer {
public readonly name: string | UIElement;
public readonly filters: TagsFilter;
public readonly isDisplayed: UIEventSource<boolean> = new UIEventSource(true);
private readonly combinedIsDisplayed : UIEventSource<boolean>;
public readonly layerDef: LayerDefinition;
private readonly _maxAllowedOverlap: number;
@ -29,8 +30,8 @@ export class FilteredLayer {
/** The featurecollection from overpass
*/
private _dataFromOverpass : any[];
private _wayHandling: number;
private _dataFromOverpass: any[];
private readonly _wayHandling: number;
/** List of new elements, geojson features
*/
private _newElements = [];
@ -60,7 +61,12 @@ export class FilteredLayer {
this.filters = layerDef.overpassFilter;
this._maxAllowedOverlap = layerDef.maxAllowedOverlapPercentage;
const self = this;
this.isDisplayed.addCallback(function (isDisplayed) {
this.combinedIsDisplayed = this.isDisplayed.map<boolean>(isDisplayed => {
return isDisplayed && State.state.locationControl.data.zoom >= self.layerDef.minzoom
},
[State.state.locationControl]
);
this.combinedIsDisplayed.addCallback(function (isDisplayed) {
const map = State.state.bm.map;
if (self._geolayer !== undefined && self._geolayer !== null) {
if (isDisplayed) {
@ -91,7 +97,8 @@ export class FilteredLayer {
const selfFeatures = [];
for (let feature of geojson.features) {
// feature.properties contains all the properties
var tags = TagUtils.proprtiesToKV(feature.properties);
const tags = TagUtils.proprtiesToKV(feature.properties);
if (this.filters.matches(tags)) {
const centerPoint = GeoOperations.centerpoint(feature);
feature.properties["_surface"] = ""+GeoOperations.surfaceAreaInSqMeters(feature);
@ -204,7 +211,6 @@ export class FilteredLayer {
style: function (feature) {
return self._style(feature.properties);
},
pointToLayer: function (feature, latLng) {
const style = self._style(feature.properties);
let marker;
@ -231,7 +237,7 @@ export class FilteredLayer {
const uiElement = self._showOnPopup(eventSource, feature);
const popup = L.popup({}, marker).setContent(uiElement.Render());
marker.bindPopup(popup)
.on("popupopen", (popup) => {
.on("popupopen", () => {
uiElement.Activate();
uiElement.Update();
});
@ -264,7 +270,7 @@ export class FilteredLayer {
eventSource.addCallback(feature.updateStyle);
layer.on("click", function (e) {
const prevSelectedElement = State.state.selectedElement.data?.feature.updateStyle();
State.state.selectedElement.data?.feature.updateStyle();
State.state.selectedElement.setData({feature: feature});
feature.updateStyle()
if (feature.geometry.type === "Point") {
@ -273,12 +279,12 @@ export class FilteredLayer {
const uiElement = self._showOnPopup(eventSource, feature);
const popup = L.popup({
L.popup({
autoPan: true,
})
.setContent(uiElement.Render())
}).setContent(uiElement.Render())
.setLatLng(e.latlng)
.openOn(State.state.bm.map);
uiElement.Update();
uiElement.Activate();
L.DomEvent.stop(e); // Marks the event as consumed
@ -286,7 +292,7 @@ export class FilteredLayer {
}
});
if (this.isDisplayed.data) {
if (this.combinedIsDisplayed.data) {
this._geolayer.addTo(State.state.bm.map);
}
}

View file

@ -77,37 +77,6 @@ export class GeoOperations {
return false;
}
/**
* Simple check: that every point of the polygon is inside the container
* @param polygon
* @param container
*/
private static isPolygonInside(polygon, container) {
for (const coor of polygon.geometry.coordinates[0]) {
if (!GeoOperations.inside(coor, container)) {
return false;
}
}
return true;
}
/**
* Simple check: one point of the polygon is inside the container
* @param polygon
* @param container
*/
private static isPolygonTouching(polygon, container) {
for (const coor of polygon.geometry.coordinates[0]) {
if (GeoOperations.inside(coor, container)) {
return true;
}
}
return false;
}
private static inside(pointCoordinate, feature): boolean {
// ray-casting algorithm based on
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
@ -124,7 +93,7 @@ export class GeoOperations {
let poly = feature.geometry.coordinates[0];
var inside = false;
for (var i = 0, j = poly.length - 1; i < poly.length; j = i++) {
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
const coori = poly[i];
const coorj = poly[j];
@ -133,7 +102,7 @@ export class GeoOperations {
const xj = coorj[0];
const yj = coorj[1];
var intersect = ((yi > y) != (yj > y))
const intersect = ((yi > y) != (yj > y))
&& (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) {
inside = !inside;
@ -146,7 +115,7 @@ export class GeoOperations {
}
class BBox {
class BBox{
readonly maxLat: number;
readonly maxLon: number;
@ -188,10 +157,8 @@ class BBox {
if (this.minLon > other.maxLon) {
return false;
}
if (this.minLat > other.maxLat) {
return false;
}
return true;
return this.minLat <= other.maxLat;
}
static get(feature) {

View file

@ -1,12 +1,11 @@
import {WikimediaImage} from "../UI/Image/WikimediaImage";
import {SimpleImageElement} from "../UI/Image/SimpleImageElement";
import {UIElement} from "../UI/UIElement";
import {Changes} from "./Osm/Changes";
import {ImgurImage} from "../UI/Image/ImgurImage";
import {State} from "../State";
import {ImagesInCategory, Wikidata, Wikimedia} from "./Web/Wikimedia";
import {UIEventSource} from "./UIEventSource";
import {Tag} from "./TagsFilter";
import {Tag} from "./Tags";
/**
* There are multiple way to fetch images for an object
@ -150,7 +149,6 @@ export class ImageSearcher extends UIEventSource<string[]> {
}
for (const key in this._tags.data) {
// @ts-ignore
if (key.startsWith("image:")) {
const url = this._tags.data[key]
this.AddImage(url);

View file

@ -1,9 +1,8 @@
import {Or, TagsFilter} from "./TagsFilter";
import {Or, TagsFilter} from "./Tags";
import {UIEventSource} from "./UIEventSource";
import {FilteredLayer} from "./FilteredLayer";
import {Bounds} from "./Bounds";
import {Overpass} from "./Osm/Overpass";
import {Basemap} from "./Leaflet/Basemap";
import {State} from "../State";
export class LayerUpdater {
@ -18,20 +17,20 @@ export class LayerUpdater {
* If the map location changes, we check for each layer if it is loaded:
* we start checking the bounds at the first zoom level the layer might operate. If in bounds - no reload needed, otherwise we continue walking down
*/
private previousBounds: Map<number, Bounds[]> = new Map<number, Bounds[]>();
private readonly previousBounds: Map<number, Bounds[]> = new Map<number, Bounds[]>();
/**
* The most important layer should go first, as that one gets first pick for the questions
* @param map
* @param minzoom
* @param layers
*/
constructor(state: State) {
const self = this;
let minzoom = Math.min(...state.layoutToUse.data.layers.map(layer => layer.minzoom));
this.sufficentlyZoomed = State.state.locationControl.map(location => location.zoom >= minzoom);
this.sufficentlyZoomed = State.state.locationControl.map(location => {
let minzoom = Math.min(...state.layoutToUse.data.layers.map(layer => layer.minzoom ?? 18));
return location.zoom >= minzoom;
}, [state.layoutToUse]
);
for (let i = 0; i < 25; i++) {
// This update removes all data on all layers -> erase the map on lower levels too
this.previousBounds.set(i, []);
@ -47,7 +46,7 @@ export class LayerUpdater {
}
private GetFilter(state: State) {
var filters: TagsFilter[] = [];
const filters: TagsFilter[] = [];
state = state ?? State.state;
for (const layer of state.layoutToUse.data.layers) {
if (state.locationControl.data.zoom < layer.minzoom) {
@ -142,15 +141,14 @@ export class LayerUpdater {
const w = Math.max(-180, bounds.getWest() - diff);
const queryBounds = {north: n, east: e, south: s, west: w};
const z = state.locationControl.data.zoom;
this.previousBounds.get(z).push(queryBounds);
const z = Math.floor(state.locationControl.data.zoom);
this.runningQuery.setData(true);
const self = this;
const overpass = new Overpass(filter);
overpass.queryGeoJson(queryBounds,
function (data) {
self.previousBounds.get(z).push(queryBounds);
self.handleData(data)
},
function (reason) {
@ -162,33 +160,21 @@ export class LayerUpdater {
private IsInBounds(state: State, bounds: Bounds): boolean {
if (this.previousBounds === undefined) {
return false;
}
const b = state.bm.map.getBounds();
if (b.getSouth() < bounds.south) {
return false;
}
if (b.getNorth() > bounds.north) {
return false;
}
if (b.getEast() > bounds.east) {
return false;
}
if (b.getWest() < bounds.west) {
return false;
}
return true;
return b.getSouth() >= bounds.south &&
b.getNorth() <= bounds.north &&
b.getEast() <= bounds.east &&
b.getWest() >= bounds.west;
}
public ForceRefresh(){
this.previousBounds = undefined;
public ForceRefresh() {
for (let i = 0; i < 25; i++) {
this.previousBounds.set(i, []);
}
}
}

View file

@ -60,12 +60,12 @@ export class Basemap {
// @ts-ignore
public map: Map;
public readonly map: Map;
public Location: UIEventSource<{ zoom: number, lat: number, lon: number }>;
public LastClickLocation: UIEventSource<{ lat: number, lon: number }> = new UIEventSource<{ lat: number, lon: number }>(undefined)
private _previousLayer: L.tileLayer = undefined;
public CurrentLayer: UIEventSource<{
public readonly Location: UIEventSource<{ zoom: number, lat: number, lon: number }>;
public readonly LastClickLocation: UIEventSource<{ lat: number, lon: number }> = new UIEventSource<{ lat: number, lon: number }>(undefined)
private _previousLayer: L.tileLayer = undefined;
public readonly CurrentLayer: UIEventSource<{
id: string,
name: string,
layer: L.tileLayer

View file

@ -7,10 +7,10 @@ import {Basemap} from "./Basemap";
export class GeoLocationHandler extends UIElement {
private _isActive: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private _permission: UIEventSource<string> = new UIEventSource<string>("");
private readonly _isActive: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly _permission: UIEventSource<string> = new UIEventSource<string>("");
private _marker: any;
private _hasLocation: UIEventSource<boolean>;
private readonly _hasLocation: UIEventSource<boolean>;
constructor() {
super(undefined);
@ -84,13 +84,13 @@ export class GeoLocationHandler extends UIElement {
}
if (this._hasLocation.data) {
return "<img src='assets/crosshair-blue.svg' alt='locate me'>";
return "<img src='./assets/crosshair-blue.svg' alt='locate me'>";
}
if (this._isActive.data) {
return "<img src='assets/crosshair-blue-center.svg' alt='locate me'>";
return "<img src='./assets/crosshair-blue-center.svg' alt='locate me'>";
}
return "<img src='assets/crosshair.svg' alt='locate me'>";
return "<img src='./assets/crosshair.svg' alt='locate me'>";
}

View file

@ -1,6 +1,4 @@
import {Basemap} from "./Basemap";
import L from "leaflet";
import {UIEventSource} from "../UIEventSource";
import {UIElement} from "../../UI/UIElement";
import {State} from "../../State";

View file

@ -2,11 +2,8 @@
* Handles all changes made to OSM.
* Needs an authenticator via OsmConnection
*/
import {UIEventSource} from "../UIEventSource";
import {OsmConnection} from "./OsmConnection";
import {OsmNode, OsmObject} from "./OsmObject";
import {And, Tag, TagsFilter} from "../TagsFilter";
import {ElementStorage} from "../ElementStorage";
import {And, Tag, TagsFilter} from "../Tags";
import {State} from "../../State";
import {Utils} from "../../Utils";
@ -163,6 +160,8 @@ export class Changes {
console.log("Beginning upload...");
// At last, we build the changeset and upload
State.state.osmConnection.UploadChangeset(
State.state.layoutToUse.data,
State.state.allElements,
function (csId) {
let modifications = "";
@ -190,11 +189,10 @@ export class Changes {
}
if (modifications.length > 0) {
changes +=
"<modify>" +
"<modify>\n" +
modifications +
"</modify>";
"\n</modify>";
}
changes += "</osmChange>";

View file

@ -1,6 +1,8 @@
import {State} from "../../State";
import {OsmConnection, UserDetails} from "./OsmConnection";
import {UIEventSource} from "../UIEventSource";
import {ElementStorage} from "../ElementStorage";
import {Layout} from "../../Customizations/Layout";
import {State} from "../../State";
export class ChangesetHandler {
@ -22,11 +24,14 @@ export class ChangesetHandler {
}
public UploadChangeset(generateChangeXML: (csid: string) => string,
continuation: () => void) {
public UploadChangeset(
layout: Layout,
allElements: ElementStorage,
generateChangeXML: (csid: string) => string,
continuation: () => void) {
if (this._dryRun) {
var changesetXML = generateChangeXML("123456");
const changesetXML = generateChangeXML("123456");
console.log(changesetXML);
continuation();
return;
@ -34,14 +39,14 @@ export class ChangesetHandler {
const self = this;
if (this.currentChangeset.data === undefined || this.currentChangeset.data === "") {
// We have to open a new changeset
this.OpenChangeset((csId) => {
this.OpenChangeset(layout,(csId) => {
this.currentChangeset.setData(csId);
const changeset = generateChangeXML(csId);
console.log(changeset);
self.AddChange(csId, changeset,
allElements,
() => {
},
(e) => {
@ -55,6 +60,7 @@ export class ChangesetHandler {
self.AddChange(
csId,
generateChangeXML(csId),
allElements,
() => {
},
(e) => {
@ -62,7 +68,7 @@ export class ChangesetHandler {
// Mark the CS as closed...
this.currentChangeset.setData("");
// ... and try again. As the cs is closed, no recursive loop can exist
self.UploadChangeset(generateChangeXML, continuation);
self.UploadChangeset(layout, allElements, generateChangeXML, continuation);
}
)
@ -71,9 +77,10 @@ export class ChangesetHandler {
}
private OpenChangeset(continuation: (changesetId: string) => void) {
private OpenChangeset(
layout : Layout,
continuation: (changesetId: string) => void) {
const layout = State.state.layoutToUse.data;
const commentExtra = layout.changesetMessage !== undefined? " - "+layout.changesetMessage : "";
this.auth.xhr({
method: 'PUT',
@ -81,8 +88,8 @@ export class ChangesetHandler {
options: {header: {'Content-Type': 'text/xml'}},
content: [`<osm><changeset>`,
`<tag k="created_by" v="MapComplete ${State.vNumber}" />`,
`<tag k="comment" v="Adding data with #MapComplete for theme #${layout.name}${commentExtra}"/>`,
`<tag k="theme" v="${layout.name}"/>`,
`<tag k="comment" v="Adding data with #MapComplete for theme #${layout.id}${commentExtra}"/>`,
`<tag k="theme" v="${layout.id}"/>`,
layout.maintainer !== undefined ? `<tag k="theme-creator" v="${layout.maintainer}"/>` : "",
`</changeset></osm>`].join("")
}, function (err, response) {
@ -98,6 +105,7 @@ export class ChangesetHandler {
private AddChange(changesetId: string,
changesetXML: string,
allElements: ElementStorage,
continuation: ((changesetId: string, idMapping: any) => void),
onFail: ((changesetId: string) => void) = undefined) {
this.auth.xhr({
@ -113,7 +121,7 @@ export class ChangesetHandler {
}
return;
}
const mapping = ChangesetHandler.parseUploadChangesetResponse(response);
const mapping = ChangesetHandler.parseUploadChangesetResponse(response, allElements);
console.log("Uploaded changeset ", changesetId);
continuation(changesetId, mapping);
});
@ -145,7 +153,7 @@ export class ChangesetHandler {
});
}
public static parseUploadChangesetResponse(response: XMLDocument) {
private static parseUploadChangesetResponse(response: XMLDocument, allElements: ElementStorage) {
const nodes = response.getElementsByTagName("node");
// @ts-ignore
for (const node of nodes) {
@ -157,9 +165,9 @@ export class ChangesetHandler {
continue;
}
console.log("Rewriting id: ", oldId, "-->", newId);
const element = State.state.allElements.getElement("node/" + oldId);
const element = allElements.getElement("node/" + oldId);
element.data.id = "node/" + newId;
State.state.allElements.addElementById("node/" + newId, element);
allElements.addElementById("node/" + newId, element);
element.ping();
}

View file

@ -1,10 +1,10 @@
// @ts-ignore
import osmAuth from "osm-auth";
import {UIEventSource} from "../UIEventSource";
import {State} from "../../State";
import {All} from "../../Customizations/Layouts/All";
import {OsmPreferences} from "./OsmPreferences";
import {ChangesetHandler} from "./ChangesetHandler";
import {Layout} from "../../Customizations/Layout";
import {ElementStorage} from "../ElementStorage";
export class UserDetails {
@ -32,8 +32,7 @@ export class OsmConnection {
constructor(dryRun: boolean, oauth_token: UIEventSource<string>,
// Used to keep multiple changesets open and to write to the correct changeset
layoutName: string,
singlePage: boolean = true,
useDevServer:boolean = false) {
singlePage: boolean = true) {
let pwaStandAloneMode = false;
try {
@ -55,7 +54,6 @@ export class OsmConnection {
oauth_secret: 'wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI',
singlepage: false,
auto: true,
url: useDevServer ? "https://master.apis.dev.openstreetmap.org" : undefined
});
} else {
@ -65,7 +63,6 @@ export class OsmConnection {
singlepage: true,
landing: window.location.href,
auto: true,
url: useDevServer ? "https://master.apis.dev.openstreetmap.org" : undefined
});
}
@ -97,9 +94,12 @@ export class OsmConnection {
}
public UploadChangeset(generateChangeXML: (csid: string) => string,
public UploadChangeset(
layout: Layout,
allElements: ElementStorage,
generateChangeXML: (csid: string) => string,
continuation: () => void = () => {}) {
this.changesetHandler.UploadChangeset(generateChangeXML, continuation);
this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML, continuation);
}
public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
@ -168,11 +168,12 @@ export class OsmConnection {
data.unreadMessages = parseInt(messages.getAttribute("unread"));
data.totalMessages = parseInt(messages.getAttribute("count"));
self.userDetails.ping();
for (const action of self._onLoggedIn) {
action(self.userDetails.data);
}
self._onLoggedIn = [];
self.userDetails.ping();
});
}

View file

@ -1,18 +1,16 @@
/**
* Helps in uplaoding, by generating the rigth title, decription and by adding the tag to the changeset
*/
import {Changes} from "./Changes";
import {UIEventSource} from "../UIEventSource";
import {ImageUploadFlow} from "../../UI/ImageUploadFlow";
import {UserDetails} from "./OsmConnection";
import {SlideShow} from "../../UI/SlideShow";
import {State} from "../../State";
import {Tag} from "../TagsFilter";
import {Tag} from "../Tags";
export class OsmImageUploadHandler {
private _tags: UIEventSource<any>;
private _slideShow: SlideShow;
private _preferedLicense: UIEventSource<string>;
private readonly _tags: UIEventSource<any>;
private readonly _slideShow: SlideShow;
private readonly _preferedLicense: UIEventSource<string>;
constructor(tags: UIEventSource<any>,
preferedLicense: UIEventSource<string>,

View file

@ -18,13 +18,21 @@ export abstract class OsmObject {
const splitted = id.split("/");
const type = splitted[0];
const idN = splitted[1];
const newContinuation = (element: OsmObject) => {
console.log("Received: ",element);
continuation(element);
}
switch (type) {
case("node"):
return new OsmNode(idN).Download(continuation);
return new OsmNode(idN).Download(newContinuation);
case("way"):
return new OsmWay(idN).Download(continuation);
return new OsmWay(idN).Download(newContinuation);
case("relation"):
return new OsmRelation(idN).Download(continuation);
return new OsmRelation(idN).Download(newContinuation);
}
}
@ -38,11 +46,9 @@ export abstract class OsmObject {
* @param string
* @constructor
*/
private Escape(string: string) {
while (string.indexOf('"') >= 0) {
string = string.replace('"', '&quot;');
}
return string;
private static Escape(string: string) {
return string.replace(/"/g, '&quot;')
.replace(/&/g, "&amp;");
}
/**
@ -54,7 +60,7 @@ export abstract class OsmObject {
for (const key in this.tags) {
const v = this.tags[key];
if (v !== "") {
tags += ' <tag k="' + this.Escape(key) + '" v="' + this.Escape(this.tags[key]) + '"/>\n'
tags += ' <tag k="' + OsmObject.Escape(key) + '" v="' + OsmObject.Escape(this.tags[key]) + '"/>\n'
}
}
return tags;

View file

@ -1,6 +1,5 @@
import {UIEventSource} from "../UIEventSource";
import {OsmConnection, UserDetails} from "./OsmConnection";
import {All} from "../../Customizations/Layouts/All";
import {Utils} from "../../Utils";
export class OsmPreferences {
@ -68,8 +67,6 @@ export class OsmPreferences {
}
if (l > 25) {
throw "Length to long";
source.setData(undefined);
return;
}
const prefsCount = Number(l);
let str = "";
@ -154,7 +151,7 @@ export class OsmPreferences {
method: 'DELETE',
path: '/api/0.6/user/preferences/' + k,
options: {header: {'Content-Type': 'text/plain'}},
}, function (error, result) {
}, function (error) {
if (error) {
console.log("Could not remove preference", error);
return;
@ -172,7 +169,7 @@ export class OsmPreferences {
path: '/api/0.6/user/preferences/' + k,
options: {header: {'Content-Type': 'text/plain'}},
content: v
}, function (error, result) {
}, function (error) {
if (error) {
console.log(`Could not set preference "${k}"'`, error);
return;

View file

@ -1,11 +1,11 @@
/**
* Interfaces overpass to get all the latest data
*/
import {Bounds} from "../Bounds";
import {TagsFilter} from "../TagsFilter";
import {TagsFilter} from "../Tags";
import $ from "jquery"
import * as OsmToGeoJson from "osmtogeojson";
/**
* Interfaces overpass to get all the latest data
*/
export class Overpass {
private _filter: TagsFilter
public static testUrl: string = null

View file

@ -6,14 +6,8 @@ import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
import Combine from "../UI/Base/Combine";
import {Img} from "../UI/Img";
import {CheckBox} from "../UI/Input/CheckBox";
import {VerticalCombine} from "../UI/Base/VerticalCombine";
import {FixedUiElement} from "../UI/Base/FixedUiElement";
import {SubtleButton} from "../UI/Base/SubtleButton";
import {PersonalLayout} from "./PersonalLayout";
import {All} from "../Customizations/Layouts/All";
import {Layout} from "../Customizations/Layout";
import {TagDependantUIElement} from "../Customizations/UIElementConstructor";
import {TagRendering} from "../Customizations/TagRendering";
export class PersonalLayersPanel extends UIElement {
private checkboxes: UIElement[] = [];
@ -22,7 +16,6 @@ export class PersonalLayersPanel extends UIElement {
super(State.state.favouriteLayers);
this.ListenTo(State.state.osmConnection.userDetails);
const t = Translations.t.favourite;
this.UpdateView([]);
const self = this;
@ -38,9 +31,9 @@ export class PersonalLayersPanel extends UIElement {
const favs = State.state.favouriteLayers.data ?? [];
const controls = new Map<string, UIEventSource<boolean>>();
const allLayouts = AllKnownLayouts.layoutsList.concat(extraThemes);
console.log("ALL LAYOUTS", allLayouts)
for (const layout of allLayouts) {
if (layout.name === PersonalLayout.NAME) {
if (layout.id === PersonalLayout.NAME) {
continue;
}
if (layout.hideFromOverview &&
@ -60,10 +53,9 @@ export class PersonalLayersPanel extends UIElement {
this.checkboxes.push(header);
for (const layer of layout.layers) {
let icon = layer.icon;
if (icon !== undefined && typeof (icon) !== "string") {
icon = icon.GetContent({"id": "node/-1"}) ?? "./assets/bug.svg";
icon = icon.GetContent({"id": "node/-1"}).txt ?? "./assets/bug.svg";
}
const image = (layer.icon ? `<img src='${layer.icon}'>` : Img.checkmark);
const noimage = (layer.icon ? `<img src='${layer.icon}'>` : Img.no_checkmark);

View file

@ -5,7 +5,7 @@ export abstract class TagsFilter {
abstract asOverpass(): string[]
abstract substituteValues(tags: any) : TagsFilter;
matchesProperties(properties: any) : boolean{
matchesProperties(properties: Map<string, string>): boolean {
return this.matches(TagUtils.proprtiesToKV(properties));
}
@ -13,80 +13,60 @@ export abstract class TagsFilter {
}
export class Regex extends TagsFilter {
private _k: string;
private _r: string;
export class RegexTag extends TagsFilter {
private readonly key: RegExp;
private readonly value: RegExp;
private readonly invert: boolean;
constructor(k: string, r: string) {
constructor(key: RegExp, value: RegExp, invert: boolean = false) {
super();
this._k = k;
this._r = r;
this.key = key;
this.value = value;
this.invert = invert;
}
asOverpass(): string[] {
return ["['" + this._k + "'~'" + this._r + "']"];
return [`['${this.key.source}'${this.invert ? "!" : ""}~'${this.value.source}']`];
}
matches(tags: { k: string; v: string }[]): boolean {
if(!(tags instanceof Array)){
throw "You used 'matches' on something that is not a list. Did you mean to use 'matchesProperties'?"
}
for (const tag of tags) {
if (tag.k === this._k) {
if (tag.v === "") {
// This tag has been removed
return false;
}
if (this._r === "*") {
// Any is allowed
return true;
}
const matchCount =tag.v.match(this._r)?.length;
return (matchCount ?? 0) > 0;
if (tag.k.match(this.key)) {
return tag.v.match(this.value) !== null;
}
}
return false;
}
substituteValues(tags: any) : TagsFilter{
throw "Substituting values is not supported on regex tags"
console.warn("Not substituting values on regex tags");
return this;
}
asHumanString() {
return this._k+"~="+this._r;
return `${this.key}${this.invert ? "!" : ""}~${this.value}`;
}
}
export class Tag extends TagsFilter {
public key: string
public value: string | RegExp
public invertValue: boolean
constructor(key: string | RegExp, value: string | RegExp, invertValue = false) {
if (value instanceof RegExp && invertValue) {
throw new Error("Unsupported combination: RegExp value and inverted value (use regex to invert the match)")
}
public value: string
constructor(key: string, value: string) {
super()
// @ts-ignore
this.key = key
// @ts-ignore
this.value = value
this.invertValue = invertValue
}
private static regexOrStrMatches(regexOrStr: string | RegExp, testStr: string) {
if (typeof regexOrStr === 'string') {
return regexOrStr === testStr
} else if (regexOrStr instanceof RegExp) {
return (regexOrStr as RegExp).test(testStr)
if(key === undefined || key === ""){
throw "Invalid key";
}
if(value === undefined){
throw "Invalid value";
}
if(value === undefined || value === "*"){
console.warn(`Got suspicious tag ${key}=* ; did you mean ${key}!~*`)
}
throw new Error("<regexOrStr> must be of type RegExp or string")
}
matches(tags: { k: string; v: string }[]): boolean {
@ -95,70 +75,36 @@ export class Tag extends TagsFilter {
}
for (const tag of tags) {
if (Tag.regexOrStrMatches(this.key, tag.k)) {
if (this.key == tag.k) {
if (tag.v === "") {
// This tag has been removed -> always matches false
return false;
}
if (this.value === "*") {
// Any is allowed (as long as the tag is not empty)
if (this.value === tag.v) {
return true;
}
if(this.value === tag.v){
return !this.invertValue;
}
return Tag.regexOrStrMatches(this.value, tag.v) !== this.invertValue
}
}
return this.invertValue
return false;
}
asOverpass(): string[] {
// @ts-ignore
const keyIsRegex = this.key instanceof RegExp
// @ts-ignore
const key = keyIsRegex ? (this.key as RegExp).source : this.key
// @ts-ignore
const valIsRegex = this.value instanceof RegExp
// @ts-ignore
const val = valIsRegex ? (this.value as RegExp).source : this.value
const regexKeyPrefix = keyIsRegex ? '~' : ''
const anyVal = this.value === "*"
if (anyVal && !keyIsRegex) {
return [`[${regexKeyPrefix}"${key}"]`];
}
if (this.value === "") {
// NOT having this key
return ['[!"' + key + '"]'];
return ['[!"' + this.key + '"]'];
}
const compareOperator = (valIsRegex || keyIsRegex) ? '~' : (this.invertValue ? '!=' : '=')
return [`[${regexKeyPrefix}"${key}"${compareOperator}"${keyIsRegex && anyVal ? '.' : val}"]`];
return [`["${this.key}"="${this.value}"]`];
}
substituteValues(tags: any) {
if (typeof this.value !== 'string') {
throw new Error("substituteValues() only possible with tag value of type string")
}
return new Tag(this.key, TagUtils.ApplyTemplate(this.value as string, tags));
}
asHumanString(linkToWiki: boolean, shorten: boolean) {
let v = ""
if (typeof (this.value) === "string") {
v = this.value;
} else {
// value is a regex
v = this.value.source;
}
let v = this.value;
if (shorten) {
v = Utils.EllipsesAfter(v, 25);
}
@ -167,26 +113,11 @@ export class Tag extends TagsFilter {
`=` +
`<a href='https://wiki.openstreetmap.org/wiki/Tag:${this.key}%3D${this.value}' target='_blank'>${v}</a>`
}
if (typeof (this.value) === "string") {
return this.key + (this.invertValue ? "!=": "=") + v;
}else{
// value is a regex
return this.key + "~=" + this.value.source;
}
return this.key + "=" + v;
}
}
export function anyValueExcept(key: string, exceptValue: string) {
return new And([
new Tag(key, "*"),
new Tag(key, exceptValue, true)
])
}
export class Or extends TagsFilter {
public or: TagsFilter[]
@ -248,8 +179,8 @@ export class And extends TagsFilter {
return true;
}
private combine(filter: string, choices: string[]): string[] {
var values = []
private static combine(filter: string, choices: string[]): string[] {
const values = [];
for (const or of choices) {
values.push(filter + or);
}
@ -257,19 +188,18 @@ export class And extends TagsFilter {
}
asOverpass(): string[] {
var allChoices: string[] = null;
let allChoices: string[] = null;
for (const andElement of this.and) {
var andElementFilter = andElement.asOverpass();
const andElementFilter = andElement.asOverpass();
if (allChoices === null) {
allChoices = andElementFilter;
continue;
}
var newChoices: string[] = []
for (var choice of allChoices) {
const newChoices: string[] = [];
for (const choice of allChoices) {
newChoices.push(
...this.combine(choice, andElementFilter)
...And.combine(choice, andElementFilter)
)
}
allChoices = newChoices;
@ -291,31 +221,6 @@ export class And extends TagsFilter {
}
export class Not extends TagsFilter{
private not: TagsFilter;
constructor(not: TagsFilter) {
super();
this.not = not;
}
asOverpass(): string[] {
throw "Not supported yet"
}
matches(tags: { k: string; v: string }[]): boolean {
return !this.not.matches(tags);
}
substituteValues(tags: any): TagsFilter {
return new Not(this.not.substituteValues(tags));
}
asHumanString(linkToWiki: boolean, shorten: boolean) {
return "!" + this.not.asHumanString(linkToWiki, shorten);
}
}
export class TagUtils {
static proprtiesToKV(properties: any): { k: string, v: string }[] {

View file

@ -43,7 +43,7 @@ export class Imgur {
const apiUrl = 'https://api.imgur.com/3/image/'+hash;
const apiKey = '7070e7167f0a25a';
var settings = {
const settings = {
async: true,
crossDomain: true,
processData: false,
@ -86,7 +86,7 @@ export class Imgur {
const apiUrl = 'https://api.imgur.com/3/image';
const apiKey = '7070e7167f0a25a';
var settings = {
const settings = {
async: true,
crossDomain: true,
processData: false,
@ -99,7 +99,7 @@ export class Imgur {
},
mimeType: 'multipart/form-data',
};
var formData = new FormData();
const formData = new FormData();
formData.append('image', blob);
formData.append("title", title);
formData.append("description", description)

View file

@ -23,7 +23,7 @@ export class Wikimedia {
"api.php?action=query&prop=imageinfo&iiprop=extmetadata&" +
"titles=" + filename +
"&format=json&origin=*";
$.getJSON(url, function (data, status) {
$.getJSON(url, function (data) {
const licenseInfo = new LicenseInfo();
const license = data.query.pages[-1].imageinfo[0].extmetadata;

View file

@ -1,19 +1,18 @@
import {UIElement} from "./UI/UIElement";
import {Layout} from "./Customizations/Layout";
import {Utils} from "./Utils";
import {LayerDefinition, Preset} from "./Customizations/LayerDefinition";
import {Preset} from "./Customizations/LayerDefinition";
import {ElementStorage} from "./Logic/ElementStorage";
import {Changes} from "./Logic/Osm/Changes";
import {OsmConnection} from "./Logic/Osm/OsmConnection";
import Locale from "./UI/i18n/Locale";
import {VariableUiElement} from "./UI/Base/VariableUIElement";
import Translations from "./UI/i18n/Translations";
import {FilteredLayer} from "./Logic/FilteredLayer";
import {LayerUpdater} from "./Logic/LayerUpdater";
import {UIEventSource} from "./Logic/UIEventSource";
import {LocalStorageSource} from "./Logic/Web/LocalStorageSource";
import {QueryParameters} from "./Logic/Web/QueryParameters";
import {CustomLayoutFromJSON} from "./Customizations/JSON/CustomLayoutFromJSON";
import {FromJSON} from "./Customizations/JSON/FromJSON";
/**
* Contains the global state: a bunch of UI-event sources
@ -24,7 +23,7 @@ export class State {
// The singleton of the global state
public static state: State;
public static vNumber = "0.0.7c mutlizoom";
public static vNumber = "0.0.7d Refactored";
// The user journey states thresholds when a new feature gets unlocked
public static userJourney = {
@ -124,6 +123,9 @@ export class State {
public layoutDefinition: string;
public installedThemes: UIEventSource<{ layout: Layout; definition: string }[]>;
public welcomeMessageOpenedTab = QueryParameters.GetQueryParameter("tab","0").map<number>(
str => isNaN(Number(str)) ? 0 : Number(str),[],n => ""+n
);
constructor(layoutToUse: Layout, useDevServer = false) {
const self = this;
@ -175,9 +177,8 @@ export class State {
this.osmConnection = new OsmConnection(
testParam === "true",
QueryParameters.GetQueryParameter("oauth_token", undefined),
layoutToUse.name,
true,
testParam === "dev"
layoutToUse.id,
true
);
@ -201,7 +202,7 @@ export class State {
}
try {
installedThemes.push({
layout: CustomLayoutFromJSON.FromQueryParam(customLayout.data),
layout: FromJSON.FromBase64(customLayout.data),
definition: customLayout.data
});
} catch (e) {

View file

@ -7,8 +7,8 @@ export class TabbedComponent extends UIElement {
private headers: UIElement[] = [];
private content: UIElement[] = [];
constructor(elements: { header: UIElement | string, content: UIElement | string }[]) {
super(new UIEventSource<number>(0));
constructor(elements: { header: UIElement | string, content: UIElement | string }[], openedTab : UIEventSource<number> = new UIEventSource<number>(0)) {
super(openedTab);
const self = this;
for (let i = 0; i < elements.length; i++) {
let element = elements[i];

View file

@ -1,57 +0,0 @@
import {LayoutConfigJson} from "../../Customizations/JSON/CustomLayoutFromJSON";
import {UIEventSource} from "../../Logic/UIEventSource";
import {UIElement} from "../UIElement";
import Combine from "../Base/Combine";
import {Button} from "../Base/Button";
import {VariableUiElement} from "../Base/VariableUIElement";
export class Preview extends UIElement {
private url: UIEventSource<string>;
private config: UIEventSource<LayoutConfigJson>;
private currentPreview = new UIEventSource<string>("")
private reloadButton: Button;
private otherPreviews: VariableUiElement;
constructor(url: UIEventSource<string>, testurl: UIEventSource<string>, config: UIEventSource<LayoutConfigJson>) {
super(undefined);
this.config = config;
this.url = url;
this.reloadButton = new Button("Reload the preview", () => {
this.currentPreview.setData(`<iframe width="99%" height="70%" src="${this.url.data}"></iframe>` +
'<p class="alert">The above preview is in testmode. Changes will not be sent to OSM, so feel free to add points and answer questions</p> ',
);
});
this.ListenTo(this.currentPreview);
this.otherPreviews = new VariableUiElement(this.url.map(url => {
return [
`<h2>Your link</h2>`,
'<span class="alert">Bookmark the link below</span><br/>',
'MapComplete has no backend. The <i>entire</i> theme configuration is saved in the following URL. This means that this URL is needed to revive and change your MapComplete instance.<br/>',
`<a target='_blank' href='${this.url.data}'>${this.url.data}</a><br/>`,
'<h2>JSON-configuration</h2>',
'You can see the configuration in JSON format below.<br/>',
'<span class=\'literal-code iframe-code-block\' style="width:95%">',
JSON.stringify(this.config.data, null, 2).replace(/\n/g, "<br/>").replace(/ /g, "&nbsp;"),
'</span>'
].join("")
}));
}
InnerRender(): string {
const url = this.url.data;
return new Combine([
new VariableUiElement(this.currentPreview),
this.reloadButton,
"<h2>Statistics</h2>",
"We track statistics with goatcounter. <a href='https://pietervdvn.goatcounter.com' target='_blank'>The statistics can be seen by anyone, so if you want to see where your theme ends up, click here</a>",
this.otherPreviews
]).Render();
}
}

View file

@ -1,712 +0,0 @@
import {UIElement} from "../UIElement";
import {VerticalCombine} from "../Base/VerticalCombine";
import {VariableUiElement} from "../Base/VariableUIElement";
import Combine from "../Base/Combine";
import {
CustomLayoutFromJSON,
LayerConfigJson,
LayoutConfigJson,
TagRenderingConfigJson
} from "../../Customizations/JSON/CustomLayoutFromJSON";
import {TabbedComponent} from "../Base/TabbedComponent";
import {UIEventSource} from "../../Logic/UIEventSource";
import {OsmConnection, UserDetails} from "../../Logic/Osm/OsmConnection";
import {Button} from "../Base/Button";
import {FixedUiElement} from "../Base/FixedUiElement";
import {TextField, ValidatedTextField} from "../Input/TextField";
import {Tag} from "../../Logic/TagsFilter";
import {DropDown} from "../Input/DropDown";
import {TagRendering} from "../../Customizations/TagRendering";
import {LayerDefinition} from "../../Customizations/LayerDefinition";
import {State} from "../../State";
TagRendering.injectFunction();
function TagsToString(tags: string | string [] | { k: string, v: string }[]) {
if (tags === undefined) {
return undefined;
}
if (typeof (tags) == "string") {
return tags;
}
const newTags = [];
console.log(tags)
for (const tag of tags) {
if (typeof (tag) == "string") {
newTags.push(tag)
} else {
newTags.push(tag.k + "=" + tag.v);
}
}
return newTags.join(",");
}
// Defined below, as it needs some context/closure
let createFieldUI: (label: string, key: string, root: any, options: { deflt?: string, type?: string, description: string, emptyAllowed?: boolean }) => UIElement;
let pingThemeObject: () => void;
class MappingGenerator extends UIElement {
private elements: UIElement[];
constructor(tagRendering: TagRenderingConfigJson,
mapping: { if: string | string[] | { k: string, v: string }[] }) {
super(undefined);
this.CreateElements(tagRendering, mapping)
}
private CreateElements(tagRendering: TagRenderingConfigJson,
mapping) {
{
const self = this;
this.elements = [
new FixedUiElement("A mapping shows a specific piece of text if a specific tag is present. If no mapping is known and no key matches (and the question is defined), then the mappings show up as radio buttons to answer the question and to update OSM"),
createFieldUI("If these tags apply", "if", mapping, {
type: "tags",
description: "The tags that have to be present. Use <span class='literal-code'>key=</span> to indicate an implicit assumption. 'key=' can be used to indicate: 'if this key is missing'"
}),
createFieldUI("Then: show this text", "then", mapping, {description: "The text that is shown"}),
new Button("Remove this mapping", () => {
for (let i = 0; i < tagRendering.mappings.length; i++) {
if (tagRendering.mappings[i] === mapping) {
tagRendering.mappings.splice(i, 1);
self.elements = [
new FixedUiElement("Tag mapping removed")
]
self.Update();
pingThemeObject();
break;
}
}
})
];
}
}
InnerRender(): string {
const combine = new VerticalCombine(this.elements).SetClass("bordered");
return combine.Render();
}
}
class TagRenderingGenerator
extends UIElement {
private elements: UIElement[];
constructor(fullConfig: UIEventSource<LayoutConfigJson>,
layerConfig: LayerConfigJson,
tagRendering: TagRenderingConfigJson,
options: { header: string, description: string, removable: boolean, hideQuestion: boolean }) {
super(undefined);
this.CreateElements(fullConfig, layerConfig, tagRendering, options)
}
private CreateElements(fullConfig: UIEventSource<LayoutConfigJson>, layerConfig: LayerConfigJson, tagRendering: TagRenderingConfigJson,
options: { header: string, description: string, removable: boolean, hideQuestion: boolean }) {
const self = this;
this.elements = [
new FixedUiElement(`<h3>${options.header}</h3>`),
new FixedUiElement(options.description),
options.hideQuestion ? new FixedUiElement("") : createFieldUI("Key", "key", tagRendering, {
deflt: "name",
type: "key",
description: "Optional. A single key, such as <span class='literal-code'>name</span> &npbs, &npbs <span class='literal-code'>surface</span>. If the object contains a tag with the specified key, the rendering below will be shown. Use <span class='literal-code'>*</span> if you want to show the rendering by default. Note that a mapping overrides this"
}),
createFieldUI("Rendering", "render", tagRendering, {
deflt: "The name of this object is {name}",
description: "Optional. If the above key is present, this rendering will be used. Note that <span class='literal-code'>{key}</span> will be replaced by the value - if that key is present. This is _not_ limited to the given key above, it is allowed to use multiple subsitutions." +
"If the above key is _not_ present, the question will be shown and the rendering will be used as answer with {key} as textfield"
}),
options.hideQuestion ? new FixedUiElement("") : createFieldUI("Type", "type", tagRendering, {
deflt: "string",
description: "Input validation of this type",
type: "typeSelector",
}),
options.hideQuestion ? new FixedUiElement("") :
createFieldUI("Question", "question", tagRendering, {
deflt: "",
description: "Optional. If 'key' is not present (or not given) and none of the mappings below match, then this will be shown as question. Users are then able to answer this question and save the data to OSM. If no question is given, values can still be shown but not answered",
type: "string"
}),
options.hideQuestion ? new FixedUiElement("") :
createFieldUI("Extra tags", "addExtraTags", tagRendering,
{
deflt: "",
type: "tags",
emptyAllowed: true,
description: "Optional. If the freeform text field is used to fill out the tag, these tags are applied as well. The main use case is to flag the object for review. (A prime example is access. A few predefined values are given and the option to fill out something. Here, one can add e.g. <span class='literal-code'>fixme=access was filled out by user, value might not be correct</span>"
}),
options.hideQuestion ? new FixedUiElement("") : createFieldUI(
"Only show if", "condition", tagRendering,
{
deflt: "",
type: "tags",
emptyAllowed: true,
description: "Only show this question/rendering if the object also has the specified tag. This can be useful to ask a follow up question only if the prerequisite is met"
}
),
...(tagRendering.mappings ?? []).map((mapping) => {
return new MappingGenerator(tagRendering, mapping)
}),
new Button("Add mapping", () => {
if (tagRendering.mappings === undefined) {
tagRendering.mappings = []
}
tagRendering.mappings.push({if: "", then: ""});
self.CreateElements(fullConfig, layerConfig, tagRendering, options);
self.Update();
})
]
if (!!options.removable) {
const b = new Button("Remove this tag rendering", () => {
for (let i = 0; i < layerConfig.tagRenderings.length; i++) {
if (layerConfig.tagRenderings[i] === tagRendering) {
layerConfig.tagRenderings.splice(i, 1);
self.elements = [
new FixedUiElement("Tag rendering removed")
]
self.Update();
break;
}
}
});
this.elements.push(b);
}
}
InnerRender(): string {
const combine = new VerticalCombine(this.elements).SetClass("bordered");
return combine.Render();
}
}
class PresetGenerator extends UIElement {
private elements: UIElement[];
constructor(fullConfig: UIEventSource<LayoutConfigJson>, layerConfig: LayerConfigJson,
preset0: { title?: string, description?: string, icon?: string, tags?: string | string[] | { k: string, v: string }[] }) {
super(undefined);
const self = this;
this.elements = [
new FixedUiElement("<h3>Preset</h3>"),
new FixedUiElement("A preset allows the user to add a new point at a location that was clicked. Note that one layer can have zero, one or multiple presets"),
createFieldUI("Title", "title", preset0, {
description: "The title of this preset, shown in the 'add new {Title} here'-dialog"
}),
createFieldUI("Description", "description", preset0,
{
deflt: layerConfig.description,
type: "string",
description: "A description shown alongside the 'add new'-button"
}),
createFieldUI("tags", "tags", preset0,
{
deflt: TagsToString(layerConfig.overpassTags), type: "tags",
description: "The tags that are added to the newly created point"
}),
new Button("Remove this preset", () => {
for (let i = 0; i < layerConfig.presets.length; i++) {
if (layerConfig.presets[i] === preset0) {
layerConfig.presets.splice(i, 1);
self.elements = [
new FixedUiElement("Preset removed")
]
self.Update();
pingThemeObject();
break;
}
}
})
]
}
InnerRender(): string {
const combine = new VerticalCombine(this.elements).SetClass("bordered");
return combine.Render();
}
}
class LayerGenerator extends UIElement {
private fullConfig: UIEventSource<LayoutConfigJson>;
private layerConfig: UIEventSource<LayerConfigJson>;
private generateField: ((label: string, key: string, root: any, deflt?: string) => UIElement);
private uielements: UIElement[];
constructor(fullConfig: UIEventSource<LayoutConfigJson>,
layerConfig: LayerConfigJson) {
super(undefined);
this.layerConfig = new UIEventSource<LayerConfigJson>(layerConfig);
this.fullConfig = fullConfig;
this.CreateElements(fullConfig, layerConfig)
}
private CreateElements(fullConfig: UIEventSource<LayoutConfigJson>, layerConfig: LayerConfigJson) {
// Init some defaults
layerConfig.title = layerConfig.title ?? {
key: "*",
addExtraTags: "",
mappings: [],
question: "",
render: "Title",
type: "text"
};
layerConfig.title.key = "*";
layerConfig.icon = layerConfig.icon ?? {
key: "*",
addExtraTags: "",
mappings: [],
question: "",
render: "./assets/bug.svg",
type: "text"
};
layerConfig.icon.key = "*";
layerConfig.color = layerConfig.color ?? {
key: "*",
addExtraTags: "",
mappings: [],
question: "",
render: "#00f",
type: "text"
};
layerConfig.color.key = "*";
layerConfig.width = layerConfig.width?? {
key: "*",
addExtraTags: "",
mappings: [],
question: "",
render: "10",
type: "nat"
};
layerConfig.width.key = "*"
const self = this;
this.uielements = [
new FixedUiElement("<p>A layer is a collection of related objects which have the same or very similar tags renderings. In general, all objects of one layer have the same icon (or at least very similar icons)</p>"),
createFieldUI("Name", "name", layerConfig, {description: "The name of this layer"}),
createFieldUI("A description of objects for this layer", "description", layerConfig, {description: "The description of this layer"}),
createFieldUI("Minimum zoom level", "minzoom", layerConfig, {
type: "nat",
deflt: "12",
description: "The minimum zoom level to start loading data. This is mainly limited by the expected number of objects: if there are a lot of objects, then pick something higher. A generous bounding box is put around the map, so some scrolling should be possible"
}),
createFieldUI("The tags to load from overpass", "overpassTags", layerConfig, {
type: "tags",
description: "Tags to load from overpass. " +
"The format is <span class='literal-code'>key=value&key0=value0&key1=value1</span>, e.g. <span class='literal-code'>amenity=public_bookcase</span> or <span class='literal-code'>amenity=compressed_air&bicycle=yes</span>." +
"Special values are:" +
"<ul>" +
"<li> <span class='literal-code'>key=*</span> to indicate that this key can be anything</li>. " +
"<li><span class='literal-code'>key=</span> means 'key is NOT present'</li>" +
"<li><span class='literal-code'>key!=value</span> means 'key does NOT have this value'</li>" +
"<li><span class='literal-code'>key~=regex</span> indicates a regex, e.g. <b>highway~=residential|tertiary</b></li>"+
"</ul>"+
". E.g. something that is indoor, not private and has no name tag can be queried as <span class='literal-code'>indoor=yes&name=&access!=private</span>"
}),
createFieldUI("Wayhandling","wayHandling", layerConfig, {
type:"wayhandling",
description: "Specifies how ways (lines and areas) are handled: either the way is shown, a center point is shown or both"
}),
new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.title, {
header: "Title element",
description: "This element is shown in the title of the popup in a header-tag",
removable: false,
hideQuestion: true
}),
new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.icon , {
header: "Icon",
description: "This decides which icon is used to represent an element on the map. Leave blank if you don't want icons to pop up",
removable: false,
hideQuestion: true
}),
new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.color, {
header: "Colour",
description: "This decides which color is used to represent a way on the map. Note that if an icon is defined as well, the icon will be showed too",
removable: false,
hideQuestion: true
}),
new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.width , {
header: "Line thickness",
description: "This decides the line thickness of ways (in pixels)",
removable: false,
hideQuestion: true
}),
...layerConfig.tagRenderings.map(tr => new TagRenderingGenerator(fullConfig, layerConfig, tr, {
header: "Tag rendering",
description: "A single tag rendering",
removable: true,
hideQuestion: false
})),
new Button("Add a tag rendering", () => {
layerConfig.tagRenderings.push({
key: undefined,
addExtraTags: undefined,
mappings: [],
question: undefined,
render: undefined,
type: "text"
});
self.CreateElements(fullConfig, layerConfig);
self.Update();
}),
...layerConfig.presets.map(preset => new PresetGenerator(fullConfig, layerConfig, preset)),
new Button("Add a preset", () => {
layerConfig.presets.push({
icon: undefined,
title: "",
description: "",
tags: TagsToString(layerConfig.overpassTags)
});
self.CreateElements(fullConfig, layerConfig);
self.Update();
}),
new Button("Remove this layer", () => {
const layers = fullConfig.data.layers;
for (let i = 0; i < layers.length; i++) {
if(layers[i] === layerConfig){
layers.splice(i, 1);
break;
}
}
self.Update();
pingThemeObject();
})
]
}
InnerRender(): string {
return new VerticalCombine(this.uielements).Render();
}
}
class AllLayerComponent extends UIElement {
private tabs: TabbedComponent;
private config: UIEventSource<LayoutConfigJson>;
constructor(config: UIEventSource<LayoutConfigJson>) {
super(undefined);
this.config = config;
const self = this;
let previousLayerAmount = config.data.layers.length;
config.addCallback((data) => {
if (data.layers.length != previousLayerAmount) {
previousLayerAmount = data.layers.length;
self.UpdateTabs();
self.Update();
}
});
this.UpdateTabs();
}
private UpdateTabs() {
const layerPanes: { header: UIElement | string, content: UIElement | string }[] = [];
const config = this.config;
for (const layer of this.config.data.layers) {
const iconUrl = CustomLayoutFromJSON.TagRenderingFromJson(layer?.icon)
?.GetContent({id: "node/-1"});
const header = this.config.map(() => {
return `<img src="${iconUrl ?? "./assets/help.svg"}">`
});
layerPanes.push({
header: new VariableUiElement(header),
content: new LayerGenerator(config, layer)
})
}
layerPanes.push({
header: "<img src='./assets/add.svg'>",
content: new Button("Add a new layer", () => {
config.data.layers.push({
name: "",
title: {
key: "*",
render: "Title"
},
icon: {
key: "*",
render: "./assets/bug.svg"
},
color: {
key: "*",
render: "#0000ff"
},
width: {
key:"*",
render: "10"
},
description: "",
minzoom: 12,
overpassTags: "",
wayHandling: LayerDefinition.WAYHANDLING_CENTER_AND_WAY,
presets: [],
tagRenderings: []
});
config.ping();
})
})
this.tabs = new TabbedComponent(layerPanes);
}
InnerRender(): string {
return this.tabs.Render();
}
}
export class ThemeGenerator extends UIElement {
private readonly userDetails: UIEventSource<UserDetails>;
public readonly themeObject: UIEventSource<LayoutConfigJson>;
private readonly allQuestionFields: UIElement[];
public url: UIEventSource<string>;
public testurl: UIEventSource<string>;
private loginButton: Button
constructor(connection: OsmConnection, windowHash) {
super(connection.userDetails);
this.userDetails = connection.userDetails;
this.loginButton = new Button("Log in with OSM", () => {
connection.AttemptLogin()
})
const defaultTheme = {layers: [], icon: "./assets/bug.svg"};
let loadedTheme = undefined;
if (windowHash !== undefined && windowHash.length > 4) {
loadedTheme = JSON.parse(atob(windowHash));
}
this.themeObject = new UIEventSource<LayoutConfigJson>(loadedTheme ?? defaultTheme);
const jsonObjectRoot = this.themeObject.data;
connection.userDetails.addCallback((userDetails) => {
jsonObjectRoot.maintainer = userDetails.name;
});
jsonObjectRoot.maintainer = connection.userDetails.data.name;
const base64 = this.themeObject.map(JSON.stringify).map(btoa);
let baseUrl = "https://pietervdvn.github.io/MapComplete";
if (window.location.hostname === "127.0.0.1") {
baseUrl = "http://127.0.0.1:1234";
}
this.url = base64.map((data) => `${baseUrl}/index.html?userlayout=${this.themeObject.data.name}#${data}`);
this.testurl = base64.map((data) => `${baseUrl}/index.html?test=true&userlayout=${this.themeObject.data.name}#${data}`);
const self = this;
pingThemeObject = () => {self.themeObject.ping()};
createFieldUI = (label, key, root, options) => {
options = options ?? {description: "?"};
options.type = options.type ?? "string";
const value = new UIEventSource<string>(TagsToString(root[key]) ?? options?.deflt);
let textField: UIElement;
if (options.type === "typeSelector") {
const options: { value: string, shown: string | UIElement }[] = [];
for (const possibleType in ValidatedTextField.inputValidation) {
if (possibleType !== "$") {
options.push({value: possibleType, shown: possibleType});
}
}
textField = new DropDown<string>("",
options,
value)
} else if (options.type === "wayhandling") {
const options: { value: string, shown: string | UIElement }[] =
[{value: "" + LayerDefinition.WAYHANDLING_DEFAULT, shown: "Show a line/area as line/area"},
{
value: "" + LayerDefinition.WAYHANDLING_CENTER_AND_WAY,
shown: "Show a line/area as line/area AND show an icon at the center"
},
{
value: "" + LayerDefinition.WAYHANDLING_CENTER_ONLY,
shown: "Only show the centerpoint of a way"
}];
textField = new DropDown<string>("",
options,
value)
} else if (options.type === "key") {
textField = new TextField<string>({
placeholder: "single key",
startValidated: false,
value:value,
toString: str => str,
fromString: str => {
if(str === undefined){
return "";
}
if (str === "*") {
return str;
}
str = str.trim();
if (str.match("^_*[a-zA-Z]*[a-zA-Z0-9:_]*$") == null) {
return undefined;
}
return str;
}
})
} else if (options.type === "tags") {
textField = ValidatedTextField.TagTextField(value.map(CustomLayoutFromJSON.TagsFromJson, [], tags => {
if (tags === undefined) {
return undefined;
}
return tags.map((tag: Tag) => tag.key + "=" + tag.value).join("&");
}), options?.emptyAllowed ?? false);
} else if (options.type === "img" || options.type === "colour") {
textField = new TextField<string>({
placeholder: options.type,
fromString: (str) => str,
toString: (str) => str,
value: value,
startValidated: true
});
} else if (options.type) {
textField = ValidatedTextField.ValidatedTextField(options.type, {value: value});
} else {
textField = new TextField<string>({
placeholder: options.type,
fromString: (str) => str,
toString: (str) => str,
value: value,
startValidated: true
});
}
let sendingPing = false;
value.addCallback((v) => {
if (v === undefined || v === "") {
delete root[key];
} else {
root[key] = v;
}
if(!sendingPing){
sendingPing = true;
self.themeObject.ping(); // We assume the root is a part of the themeObject
sendingPing = false;
}
});
self.themeObject.addCallback(() => {
value.setData(root[key]);
})
return new Combine([
label,
textField,
"<br>",
"<span class='subtle'>" + options.description + "</span>"
]);
}
this.allQuestionFields = [
createFieldUI("Name of this theme", "name", jsonObjectRoot, {description: "An identifier for this theme"}),
createFieldUI("Title", "title", jsonObjectRoot, {
deflt: "Title",
description: "The title of this theme, as shown in the welcome message and in the title bar of the browser"
}),
createFieldUI("icon", "icon", jsonObjectRoot, {
deflt: "./assets/bug.svg",
type: "img",
description: "The icon representing this MapComplete instance. It is shown in the welcome message and -if adopted as official theme- used as favicon and to browse themes"
}),
createFieldUI("Description", "description", jsonObjectRoot, {
description: "Shown in the welcome message",
deflt: "Description"
}),
createFieldUI("The supported language", "language", jsonObjectRoot, {
description: "The language of this mapcomplete instance. MapComplete can be translated, see <a href='https://github.com/pietervdvn/MapComplete#translating-mapcomplete' target='_blank'> here for more information</a>",
deflt: "en"
}),
createFieldUI("startLat", "startLat", jsonObjectRoot, {
type: "float",
deflt: "0",
description: "The latitude where this theme should start. Note that this is only for completely fresh users, as the last location is saved"
}),
createFieldUI("startLon", "startLon", jsonObjectRoot, {
type: "float",
deflt: "0",
description: "The longitude where this theme should start. Note that this is only for completely fresh users, as the last location is saved"
}),
createFieldUI("startzoom", "startZoom", jsonObjectRoot, {
type: "nat",
deflt: "12",
description: "The initial zoom level where the map is located"
}),
createFieldUI("Query widening factor", "widenFactor", jsonObjectRoot, {
type: "pfloat",
deflt: "0.05",
description: "When a query is run, the current map view is taken and a margin with a certain factor is added to allow panning and zooming. If you are running heavy queries (e.g. highway=residential), to much data is returned. In that case, lower the widenfactor, e.g. to 0.01-0.02"
}),
new AllLayerComponent(this.themeObject)
]
}
InnerRender(): string {
if (!this.userDetails.data.loggedIn) {
return new Combine(["Not logged in. You need to be logged in to create a theme.", this.loginButton]).Render();
}
if (this.userDetails.data.csCount < State.userJourney.themeGeneratorUnlock ) {
return `You need at least ${State.userJourney.themeGeneratorUnlock} changesets to create your own theme.`;
}
return new VerticalCombine([
...this.allQuestionFields,
]).Render();
}
}

View file

@ -1,13 +1,11 @@
import {UIElement} from "./UIElement";
import {ImageCarousel} from "./Image/ImageCarousel";
import {VerticalCombine} from "./Base/VerticalCombine";
import {OsmLink} from "../Customizations/Questions/OsmLink";
import {WikipediaLink} from "../Customizations/Questions/WikipediaLink";
import {And} from "../Logic/TagsFilter";
import {And} from "../Logic/Tags";
import {TagDependantUIElement, TagDependantUIElementConstructor} from "../Customizations/UIElementConstructor";
import Translations from "./i18n/Translations";
import {Changes} from "../Logic/Osm/Changes";
import {UserDetails} from "../Logic/Osm/OsmConnection";
import {FixedUiElement} from "./Base/FixedUiElement";
import {State} from "../State";
import {TagRenderingOptions} from "../Customizations/TagRenderingOptions";
@ -23,13 +21,11 @@ export class FeatureInfoBox extends UIElement {
/**
* The tags, wrapped in a global event source
*/
private _tagsES: UIEventSource<any>;
private _changes: Changes;
private _title: UIElement;
private _osmLink: UIElement;
private _wikipedialink: UIElement;
private readonly _tagsES: UIEventSource<any>;
private readonly _changes: Changes;
private readonly _title: UIElement;
private readonly _osmLink: UIElement;
private readonly _wikipedialink: UIElement;
private _infoboxes: TagDependantUIElement[];
private _oneSkipped = Translations.t.general.oneSkippedQuestion.Clone();

View file

@ -11,6 +11,7 @@ import {
TagDependantUIElementConstructor
} from "../../Customizations/UIElementConstructor";
import {State} from "../../State";
import Translation from "../i18n/Translation";
export class ImageCarouselConstructor implements TagDependantUIElementConstructor{
IsKnown(properties: any): boolean {
@ -29,8 +30,8 @@ export class ImageCarouselConstructor implements TagDependantUIElementConstructo
return new ImageCarousel(dependencies.tags);
}
GetContent(tags: any): string {
return undefined;
GetContent(tags: any): Translation {
return new Translation({"en":"Images without upload"});
}
}

View file

@ -7,6 +7,7 @@ import {ImageCarousel} from "./ImageCarousel";
import {ImageUploadFlow} from "../ImageUploadFlow";
import {OsmImageUploadHandler} from "../../Logic/Osm/OsmImageUploadHandler";
import {State} from "../../State";
import Translation from "../i18n/Translation";
export class ImageCarouselWithUploadConstructor implements TagDependantUIElementConstructor{
IsKnown(properties: any): boolean {
@ -25,8 +26,8 @@ export class ImageCarouselWithUploadConstructor implements TagDependantUIElement
return new ImageCarouselWithUpload(dependencies);
}
GetContent(tags: any): string {
return undefined;
GetContent(tags: any): Translation {
return new Translation({"en":"Image carousel with uploader"});
}
}
@ -38,7 +39,6 @@ class ImageCarouselWithUpload extends TagDependantUIElement {
super(dependencies.tags);
const tags = dependencies.tags;
this._imageElement = new ImageCarousel(tags);
const userDetails = State.state.osmConnection.userDetails;
const license = State.state.osmConnection.GetPreference( "pictures-license");
this._pictureUploader = new OsmImageUploadHandler(tags, license, this._imageElement.slideshow).getUI();

View file

@ -4,16 +4,13 @@ import Translations from "../i18n/Translations";
import {UIEventSource} from "../../Logic/UIEventSource";
import * as EmailValidator from "email-validator";
import {parsePhoneNumberFromString} from "libphonenumber-js";
import {TagRenderingOptions} from "../../Customizations/TagRenderingOptions";
import {CustomLayoutFromJSON} from "../../Customizations/JSON/CustomLayoutFromJSON";
import {And, Tag} from "../../Logic/TagsFilter";
export class ValidatedTextField {
public static inputValidation = {
"$": (str) => true,
"string": (str) => true,
"date": (str) => true, // TODO validate and add a date picker
"wikidata": (str) => true, // TODO validate wikidata IDS
"$": () => true,
"string": () => true,
"date": () => true, // TODO validate and add a date picker
"wikidata": () => true, // TODO validate wikidata IDS
"int": (str) => {str = ""+str; return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str))},
"nat": (str) => {str = ""+str; return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) > 0},
"float": (str) => !isNaN(Number(str)),
@ -31,75 +28,18 @@ export class ValidatedTextField {
return parsePhoneNumberFromString(str, country.toUpperCase()).formatInternational()
}
}
public static TagTextField(value: UIEventSource<Tag[]> = undefined, allowEmpty: boolean) {
allowEmpty = allowEmpty ?? false;
return new TextField<Tag[]>({
placeholder: "Tags",
fromString: str => {
const tags = CustomLayoutFromJSON.TagsFromJson(str);
console.log("Parsed",str," --> ",tags)
if (tags === []) {
if (allowEmpty) {
return []
} else {
return undefined;
}
}
return tags;
}
,
toString: (tags: Tag[]) => {
if (tags === undefined || tags === []) {
if (allowEmpty) {
return "";
} else {
return undefined;
}
}
return new And(tags).asHumanString(false, false);
},
value: value,
startValidated: true
}
)
}
public static
ValidatedTextField(type: string, options: { value?: UIEventSource<string>, country?: string })
: TextField<string> {
let isValid = ValidatedTextField.inputValidation[type];
if (isValid === undefined
) {
throw "Invalid type for textfield: " + type
}
let formatter = ValidatedTextField.formatting[type] ?? ((str) => str);
return new TextField<string>({
placeholder: type,
toString: str => str,
fromString: str => isValid(str, options?.country) ? formatter(str, options.country) : undefined,
value: options.value,
startValidated: true
})
}
}
export class TextField<T> extends InputElement<T> {
private value: UIEventSource<string>;
private mappedValue: UIEventSource<T>;
/**
* Pings and has the value data
*/
public enterPressed = new UIEventSource<string>(undefined);
private _placeholder: UIElement;
private _fromString?: (string: string) => T;
private _toString: (t: T) => string;
private startValidated: boolean;
private readonly value: UIEventSource<string>;
private readonly mappedValue: UIEventSource<T>;
public readonly enterPressed = new UIEventSource<string>(undefined);
private readonly _placeholder: UIElement;
private readonly _fromString?: (string: string) => T;
private readonly _toString: (t: T) => string;
private readonly startValidated: boolean;
constructor(options: {
@ -157,14 +97,6 @@ export class TextField<T> extends InputElement<T> {
GetValue(): UIEventSource<T> {
return this.mappedValue;
}
ShowValue(t: T): boolean {
if (!this.IsValid(t)) {
return false;
}
this.mappedValue.setData(t);
}
InnerRender(): string {
return `<form onSubmit='return false' class='form-text-field'>` +
`<input type='text' placeholder='${this._placeholder.InnerRender()}' id='text-${this.id}'>` +
@ -178,7 +110,7 @@ export class TextField<T> extends InputElement<T> {
}
this.mappedValue.addCallback((data) => {
field.className = this.mappedValue.data !== undefined ? "valid" : "invalid";
field.className = data !== undefined ? "valid" : "invalid";
});
field.className = this.mappedValue.data !== undefined ? "valid" : "invalid";

View file

@ -7,10 +7,7 @@ import {SubtleButton} from "./Base/SubtleButton";
import {State} from "../State";
import {VariableUiElement} from "./Base/VariableUIElement";
import {PersonalLayout} from "../Logic/PersonalLayout";
import {FixedUiElement} from "./Base/FixedUiElement";
import {Layout} from "../Customizations/Layout";
import {CustomLayoutFromJSON} from "../Customizations/JSON/CustomLayoutFromJSON";
import {All} from "../Customizations/Layouts/All";
export class MoreScreen extends UIElement {
@ -22,26 +19,29 @@ export class MoreScreen extends UIElement {
}
private createLinkButton(layout: Layout, customThemeDefinition: string = undefined) {
private static createLinkButton(layout: Layout, customThemeDefinition: string = undefined) {
if (layout === undefined) {
return undefined;
}
if (layout.hideFromOverview) {
if (State.state.osmConnection.GetPreference("hidden-theme-" + layout.name + "-enabled").data !== "true") {
if (State.state.osmConnection.GetPreference("hidden-theme-" + layout.id + "-enabled").data !== "true") {
return undefined;
}
}
if (layout.name === State.state.layoutToUse.data.name) {
if (layout.id === State.state.layoutToUse.data.id) {
return undefined;
}
const currentLocation = State.state.locationControl.data;
let linkText =
`./${layout.name.toLowerCase()}.html?z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}`
`./${layout.id.toLowerCase()}.html?z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}`
if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
linkText = `./index.html?layout=${layout.name}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}`
linkText = `./index.html?layout=${layout.id}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}`
}
if (customThemeDefinition) {
linkText = `./index.html?userlayout=${layout.name}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}#${customThemeDefinition}`
linkText = `./index.html?userlayout=${layout.id}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}#${customThemeDefinition}`
}
@ -86,20 +86,21 @@ export class MoreScreen extends UIElement {
continue;
}
}
if(layout.name !== k){
if (layout.id !== k) {
continue; // This layout was added multiple time due to an uppercase
}
els.push(this.createLinkButton(layout));
els.push(MoreScreen.createLinkButton(layout));
}
const customThemesNames = State.state.installedThemes.data ?? [];
if (customThemesNames !== []) {
if (customThemesNames.length > 0) {
console.log(customThemesNames)
els.push(Translations.t.general.customThemeIntro)
}
for (const installed of State.state.installedThemes.data) {
els.push(this.createLinkButton(installed.layout, installed.definition));
for (const installed of State.state.installedThemes.data) {
els.push(MoreScreen.createLinkButton(installed.layout, installed.definition));
}
}

View file

@ -11,19 +11,15 @@ import {Basemap} from "../Logic/Leaflet/Basemap";
import {FilteredLayer} from "../Logic/FilteredLayer";
import {Utils} from "../Utils";
import {UIEventSource} from "../Logic/UIEventSource";
import {UserDetails} from "../Logic/Osm/OsmConnection";
import Translation from "./i18n/Translation";
import {SubtleButton} from "./Base/SubtleButton";
export class ShareScreen extends UIElement {
private _shareButton: UIElement;
private _options: UIElement;
private _iframeCode: UIElement;
private _link: UIElement;
private _linkStatus: UIEventSource<string | UIElement>;
private _editLayout: UIElement;
private readonly _options: UIElement;
private readonly _iframeCode: UIElement;
private readonly _link: UIElement;
private readonly _linkStatus: UIEventSource<string | UIElement>;
private readonly _editLayout: UIElement;
constructor() {
super(undefined)
@ -33,8 +29,8 @@ export class ShareScreen extends UIElement {
const optionParts: (UIEventSource<string>)[] = [];
const includeLocation = new CheckBox(
new Combine([Img.checkmark, "Include current location"]),
new Combine([Img.no_checkmark, "Include current location"]),
new Combine([Img.checkmark, tr.fsIncludeCurrentLocation]),
new Combine([Img.no_checkmark, tr.fsIncludeCurrentLocation]),
true
)
optionCheckboxes.push(includeLocation);
@ -52,11 +48,7 @@ export class ShareScreen extends UIElement {
const currentLayer: UIEventSource<{ id: string, name: string, layer: any }> = (State.state.bm as Basemap).CurrentLayer;
const currentBackground = new VariableUiElement(
currentLayer.map(
(layer) => `Include the current background choice <b>${layer.name}</b>`
)
);
const currentBackground = tr.fsIncludeCurrentBackgroundMap.Subs({name: layout.id});
const includeCurrentBackground = new CheckBox(
new Combine([Img.checkmark, currentBackground]),
new Combine([Img.no_checkmark, currentBackground]),
@ -73,8 +65,8 @@ export class ShareScreen extends UIElement {
const includeLayerChoices = new CheckBox(
new Combine([Img.checkmark, "Include the current layer choices"]),
new Combine([Img.no_checkmark, "Include the current layer choices"]),
new Combine([Img.checkmark, tr.fsIncludeCurrentLayers]),
new Combine([Img.no_checkmark, tr.fsIncludeCurrentLayers]),
true
)
optionCheckboxes.push(includeLayerChoices);
@ -110,8 +102,7 @@ export class ShareScreen extends UIElement {
const checkbox = new CheckBox(
new Combine([Img.checkmark, Translations.W(swtch.human)]),
new Combine([Img.no_checkmark, Translations.W(swtch.human)]),
swtch.reverse ? false : true
new Combine([Img.no_checkmark, Translations.W(swtch.human)]), !swtch.reverse
);
optionCheckboxes.push(checkbox);
optionParts.push(checkbox.isEnabled.map((isEn) => {
@ -136,7 +127,7 @@ export class ShareScreen extends UIElement {
const url = currentLocation.map(() => {
let literalText = "https://pietervdvn.github.io/MapComplete/" + layout.name + ".html"
let literalText = "https://pietervdvn.github.io/MapComplete/" + layout.id.toLowerCase() + ".html"
const parts = Utils.NoEmpty(Utils.NoNull(optionParts.map((eventSource) => eventSource.data)));
@ -190,18 +181,18 @@ export class ShareScreen extends UIElement {
const self = this;
this._link = new VariableUiElement(
url.map((url) => {
return `<input type="text" value=" ${url}" id="code-link--copyable" style="width:90%"readonly>`
return `<input type="text" value=" ${url}" id="code-link--copyable" style="width:90%">`
})
).onClick(async () => {
const shareData = {
title: Translations.W(layout.name)?.InnerRender() ?? "",
title: Translations.W(layout.id)?.InnerRender() ?? "",
text: Translations.W(layout.description)?.InnerRender() ?? "",
url: self._link.data,
}
function rejected() {
var copyText = document.getElementById("code-link--copyable");
const copyText = document.getElementById("code-link--copyable");
// @ts-ignore
copyText.select();

View file

@ -1,5 +1,5 @@
import {UIElement} from "./UIElement";
import {Tag, TagUtils} from "../Logic/TagsFilter";
import {Tag, TagUtils} from "../Logic/Tags";
import {FilteredLayer} from "../Logic/FilteredLayer";
import Translations from "./i18n/Translations";
import Combine from "./Base/Combine";
@ -8,8 +8,6 @@ import Locale from "./i18n/Locale";
import {State} from "../State";
import {UIEventSource} from "../Logic/UIEventSource";
import {UserDetails} from "../Logic/Osm/OsmConnection";
import {FixedUiElement} from "./Base/FixedUiElement";
import {Utils} from "../Utils";
/**
@ -44,8 +42,7 @@ export class SimpleAddUI extends UIElement {
this._loginButton = Translations.t.general.add.pleaseLogin.Clone().onClick(() => State.state.osmConnection.AttemptLogin());
this._addButtons = [];
this.clss = "add-ui"
this.SetClass("add-ui");
const self = this;
for (const layer of State.state.filteredLayers.data) {
@ -67,7 +64,7 @@ export class SimpleAddUI extends UIElement {
const csCount = State.state.osmConnection.userDetails.data.csCount;
let tagInfo = "";
if (csCount > State.userJourney.tagsVisibleAt) {
tagInfo = preset.tags.map(t => t.asHumanString(false)).join("&");
tagInfo = preset.tags.map(t => t.asHumanString(false, true)).join("&");
tagInfo = `<br/><span class='subtle'>${tagInfo}</span>`
}
const button: UIElement =
@ -115,7 +112,6 @@ export class SimpleAddUI extends UIElement {
}
private CreatePoint(tags: Tag[], layerToAddTo: FilteredLayer) {
const self = this;
return () => {
const loc = State.state.bm.LastClickLocation.data;
@ -139,7 +135,7 @@ export class SimpleAddUI extends UIElement {
let tagInfo = "";
const csCount = State.state.osmConnection.userDetails.data.csCount;
if (csCount > State.userJourney.tagsVisibleAt) {
tagInfo = this._confirmPreset.data .tags.map(t => t.asHumanString(csCount > State.userJourney.tagsVisibleAndWikiLinked)).join("&");
tagInfo = this._confirmPreset.data .tags.map(t => t.asHumanString(csCount > State.userJourney.tagsVisibleAndWikiLinked, true)).join("&");
tagInfo = `<br/>More information about the preset: ${tagInfo}`
}
@ -197,14 +193,7 @@ export class SimpleAddUI extends UIElement {
return new Combine([header, Translations.t.general.add.stillLoading]).Render()
}
var html = "";
for (const button of this._addButtons) {
html += button.Render();
}
return header.Render() + new Combine([html], "add-popup-all-buttons").Render();
return header.Render() + new Combine(this._addButtons, "add-popup-all-buttons").Render();
}

View file

@ -1,19 +1,14 @@
import {UIElement} from "../UI/UIElement";
import {OsmConnection, UserDetails} from "../Logic/Osm/OsmConnection";
import Locale from "../UI/i18n/Locale";
import {State} from "../State";
import {Layout} from "../Customizations/Layout";
import Translations from "./i18n/Translations";
import {VariableUiElement} from "./Base/VariableUIElement";
import {Utils} from "../Utils";
import {UIEventSource} from "../Logic/UIEventSource";
import Combine from "./Base/Combine";
export class WelcomeMessage extends UIElement {
private readonly layout: Layout;
private languagePicker: UIElement;
private osmConnection: OsmConnection;
private readonly description: UIElement;
private readonly plzLogIn: UIElement;

View file

@ -1,7 +1,5 @@
import {UIElement} from "../UIElement"
import Locale from "./Locale"
import {FixedUiElement} from "../Base/FixedUiElement";
import {TagUtils} from "../../Logic/TagsFilter";
import Combine from "../Base/Combine";
@ -17,6 +15,9 @@ export default class Translation extends UIElement {
const combined = [];
const parts = template.split("{" + k + "}");
const el: string | UIElement = text[k];
if(el === undefined){
continue;
}
let rtext: string = "";
if (typeof (el) === "string") {
rtext = el;
@ -51,6 +52,7 @@ export default class Translation extends UIElement {
for (const i in this.translations) {
return this.translations[i]; // Return a random language
}
console.log("Missing language ",Locale.language.data,"for",this.translations)
return "Missing translation"
}
@ -62,6 +64,10 @@ export default class Translation extends UIElement {
constructor(translations: object) {
super(Locale.language)
let count = 0;
for (const translationsKey in translations) {
count++;
}
this.translations = translations
}

View file

@ -128,19 +128,16 @@ export default class Translations {
question: new T({
en: 'Is this parking covered? Also select "covered" for indoor parkings.',
nl: 'Is deze parking overdekt? Selecteer ook "overdekt" voor fietsparkings binnen een gebouw.',
fr: 'TODO: fr',
gl: 'Este aparcadoiro está cuberto? Tamén escolle "cuberto" para aparcadoiros interiores.'
}),
yes: new T({
en: 'This parking is covered (it has a roof)',
nl: 'Deze parking is overdekt (er is een afdak)',
fr: 'TODO: fr',
gl: 'Este aparcadoiro está cuberto (ten un teito)'
}),
no: new T({
en: 'This parking is not covered',
nl: 'Deze parking is niet overdekt',
fr: 'TODO: fr',
gl: 'Este aparcadoiro non está cuberto'
})
},
@ -148,19 +145,16 @@ export default class Translations {
question: new T({
en: "How many bicycles fit in this bicycle parking (including possible cargo bicycles)?",
nl: "Voor hoeveel fietsen is er bij deze fietsparking plaats (inclusief potentiëel bakfietsen)?",
fr: "TODO: fr",
gl: "Cantas bicicletas caben neste aparcadoiro de bicicletas (incluídas as posíbeis bicicletas de carga)?"
}),
template: new T({
en: "This parking fits $nat$ bikes",
nl: "Deze parking heeft plaats voor $nat$ fietsen",
fr: "TODO: fr",
gl: "Neste aparcadoiro caben $nat$ bicicletas"
}),
render: new T({
en: "Place for {capacity} bikes (in total)",
nl: "Plaats voor {capacity} fietsen (in totaal)",
fr: "TODO: fr",
gl: "Lugar para {capacity} bicicletas (en total)"
}),
},
@ -1308,6 +1302,18 @@ export default class Translations {
en: "Enable the 'geolocate-me' button (mobile only)",
gl: "Activar o botón de 'xeolocalizarme' (só móbil)",
nl: "Toon het knopje voor geolocalisatie (enkel op mobiel)"
}),
fsIncludeCurrentBackgroundMap: new T({
en: "Include the current background choice <b>{name}</b>",
nl: "Gebruik de huidige achtergrond <b>{name}</b>"
}),
fsIncludeCurrentLayers: new T({
en: "Include the current layer choices",
nl: "Toon enkel de huidig getoonde lagen"
}),
fsIncludeCurrentLocation: new T({
en: "Include current location",
nl: "Start op de huidige locatie"
})
},
morescreen: {

View file

@ -1,134 +1,171 @@
{
"id": "aed",
"title": {
"en": "Open AED Map",
"fr": "Carte AED",
"nl": "Open AED-kaart"
},
"maintainer": "MapComplete",
"icon": "./assets/themes/aed/aed.svg",
"description": {
"en": "On this map, one can find and mark nearby defibrillators",
"fr": "Sur cette carte, vous pouvez trouver et améliorer les informations sur les défibrillateurs",
"nl": "Op deze kaart kan je informatie over AEDs vinden en verbeteren"
},
"language": [
"en",
"fr"
"fr",
"nl"
],
"startLat": "0",
"startLon": "0",
"startZoom": "12",
"maintainer": "Pieter Vander Vennet",
"version": "2020-08-29",
"startLat": 0,
"startLon": 0,
"startZoom": 12,
"layers": [
{
"name": "Defibrillator",
"id": "Defibrillator",
"name": {
"en": "Defibrillators",
"fr": "Défibrillateurs",
"nl": "Defibrillatoren"
},
"overpassTags": "emergency=defibrillator",
"minzoom": 12,
"title": {
"key": "*",
"render": {
"en": "Defibrillator",
"fr": "Défibrillateur"
"fr": "Défibrillateur",
"nl": "Defibrillator"
}
},
"icon": {
"key": "*",
"render": "./assets/themes/aed/aed.svg"
},
"color": {
"render": "#0000ff",
"key": "*"
},
"description": {
"en": "A defibrillator",
"fr": "Un défibrillateur"
},
"minzoom": "12",
"icon": "./assets/themes/aed/aed.svg",
"color": "#0000ff",
"presets": [
{
"title": {
"en": "Defibrillator",
"fr": "Défibrillateur"
"fr": "Défibrillateur",
"nl": "Defibrillator"
},
"tags": "emergency=defibrillator",
"description": "A defibrillator"
"tags": [
"emergency=defibrillator"
]
}
],
"tagRenderings": [
"pictures",
{
"mappings": [
{
"question": {
"en": "Is this defibrillator located indoors?",
"fr": "Ce défibrillateur est-il disposé en intérieur ?",
"nl": "Hangt deze defibrillator binnen of buiten?"
},
"if": "indoor=yes",
"then": {
"en": "This defibrillator is located indoors",
"fr": "Ce défibrillateur est en intérieur (dans un batiment)"
},
"if": "indoor=yes"
"fr": "Ce défibrillateur est en intérieur (dans un batiment)",
"nl": "Deze defibrillator bevindt zich in een gebouw"
}
},
{
"if": "indoor=no",
"then": {
"en": "This defibrillator is located outdoors",
"fr": "Ce défibrillateur est situé en extérieur"
},
"if": "indoor=no"
"fr": "Ce défibrillateur est situé en extérieur",
"nl": "Deze defibrillator hangt buiten"
}
}
],
"question": {
"en": "Is this defibrillator located indoors?",
"fr": "Ce défibrillateur est-il disposé en intérieur ?"
},
"type": "text"
]
},
{
"question": {
"en": "Is this defibrillator freely accessible?",
"fr": "Ce défibrillateur est-il librement accessible?",
"nl": "Is deze defibrillator vrij toegankelijk?"
},
"render": {
"en": "Access is {access}",
"nl": "Toegankelijkheid is {access}",
"fr": "{access} accessible"
},
"condition": "indoor=yes",
"freeform": {
"key": "access",
"addExtraTags": "fixme=Freeform field used for access - doublecheck the value"
},
"mappings": [
{
"if": "access=yes",
"then": {
"en": "Publicly accessible",
"fr": "Librement accessible"
},
"if": "access=yes"
"fr": "Librement accessible",
"nl": "Publiek toegankelijk"
}
},
{
"if": "access=public",
"then": {
"en": "Publicly accessible",
"fr": "Librement accessible",
"nl": "Publiek toegankelijk"
},
"hideInAnswer": true
},
{
"if": "access=customers",
"then": {
"en": "Only accessible to customers",
"fr": "Réservé aux clients du lieu"
},
"if": "access=customers"
"fr": "Réservé aux clients du lieu",
"nl": "Enkel toegankeleijk voor klanten"
}
},
{
"if": "access=private",
"then": {
"en": "Not accessible to the general public (e.g. only accesible to staff, the owners, ...)",
"fr": "Non accessible au public (par exemple réservé au personnel, au propriétaire, ...)"
"fr": "Non accessible au public (par exemple réservé au personnel, au propriétaire, ...)",
"nl": "Niet toegankelijk voor het publiek (bv. enkel voor personneel, de eigenaar, ...)"
}
}
],
"question": {
"en": "Is this defibrillator freely accessible?",
"fr": "Ce défibrillateur est-il librement accessible ?"
},
"type": "text",
"key": "access",
"condition": "indoor=yes"
]
},
{
"key": "level",
"mappings": [],
"question": {
"en": "On which floor is this defibrillator located?",
"fr": "À quel étage est situé ce défibrillateur ?"
"fr": "À quel étage est situé ce défibrillateur?",
"nl": "Op welke verdieping bevindt deze defibrillator zich?"
},
"condition": {
"and": [
"indoor=yes",
"access!~private"
]
},
"freeform": {
"key": "level",
"type": "int"
},
"type": "int",
"render": {
"en": "This defibrallator is on floor {level}",
"fr": "Ce défibrillateur est à l'étage {level}"
},
"condition": "indoor=yes&access!=private"
"fr": "Ce défibrillateur est à l'étage {level}",
"nl": "De defibrillator bevindt zicht op verdieping {level}"
}
},
{
"key": "defibrillator:location",
"mappings": [],
"render": "{defibrillator:location}",
"question": {
"en": "Please give some explanation on where the defibrillator can be found",
"fr": "Veuillez indiquez plus précisément où se situe le défibrillateur"
"fr": "Veuillez indiquez plus précisément où se situe le défibrillateur",
"nl": "Gelieve meer informatie te geven over de exacte locatie van de defibrillator"
},
"type": "text",
"render": "{defibrillator:location}"
"freeform": {
"type": "text",
"key": "defibrillator:location"
}
}
],
"overpassTags": "emergency=defibrillator"
]
}
],
"title": "Open AED Map",
"icon": "./assets/themes/aed/aed.svg",
"name": "aed",
"description": {
"en": "On this map, one can find and mark nearby defibrillators",
"fr": "Sur cette carte, vous pouvez trouver et améliorer les informations sur les défibrillateurs"
}
]
}

Some files were not shown because too many files have changed in this diff Show more