forked from MapComplete/MapComplete
Huge refactorings of JSON-parsing and Tagsfilter, other cleanups, warning cleanups and lots of small subtle bugfixes
This commit is contained in:
parent
9a5b35b9f3
commit
a57b7d93fa
113 changed files with 1565 additions and 2594 deletions
|
@ -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];
|
||||
|
|
|
@ -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, " "),
|
||||
'</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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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"});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
@ -135,8 +126,8 @@ export class ShareScreen extends UIElement {
|
|||
this._options = new VerticalCombine(optionCheckboxes)
|
||||
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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue