Add further support for special UI-elements; add documentation, fix a few bugs

This commit is contained in:
Pieter Vander Vennet 2020-10-17 02:37:53 +02:00
parent 3ab3cef249
commit 07e611bf10
12 changed files with 113 additions and 55 deletions

View file

@ -145,7 +145,6 @@ export class FromJSON {
json = "{image_carousel()}{image_upload()}"; json = "{image_carousel()}{image_upload()}";
} }
} }
console.warn("Possible literal rendering:", json)
return new TagRenderingOptions({ return new TagRenderingOptions({
freeform: { freeform: {

View file

@ -17,6 +17,7 @@ export class ElementStorage {
addElement(element): UIEventSource<any> { addElement(element): UIEventSource<any> {
const eventSource = new UIEventSource<any>(element.properties); const eventSource = new UIEventSource<any>(element.properties);
console.log("Creating a new tag storate for ", element.properties.id)
this._elements[element.properties.id] = eventSource; this._elements[element.properties.id] = eventSource;
return eventSource; return eventSource;
} }

View file

@ -111,7 +111,10 @@ export class FilteredLayer {
if (this.filters.matches(tags)) { if (this.filters.matches(tags)) {
const centerPoint = GeoOperations.centerpoint(feature); const centerPoint = GeoOperations.centerpoint(feature);
feature.properties["_surface"] = "" + GeoOperations.surfaceAreaInSqMeters(feature); const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature);
feature.properties["_surface"] = "" + sqMeters;
feature.properties["_surface:ha"] = "" + Math.floor(sqMeters / 1000)/10;
const lat = centerPoint.geometry.coordinates[1]; const lat = centerPoint.geometry.coordinates[1];
const lon = centerPoint.geometry.coordinates[0] const lon = centerPoint.geometry.coordinates[0]
feature.properties["_lon"] = "" + lat; // We expect a string here for lat/lon feature.properties["_lon"] = "" + lat; // We expect a string here for lat/lon
@ -252,7 +255,7 @@ export class FilteredLayer {
const popup = L.popup({}, marker); const popup = L.popup({}, marker);
let uiElement: UIElement; let uiElement: UIElement;
let content = undefined; let content = undefined;
let p = marker.bindPopup(popup) let p = marker.bindPopup(popup)
.on("popupopen", () => { .on("popupopen", () => {
if (content === undefined) { if (content === undefined) {
uiElement = self._showOnPopup(eventSource, feature); uiElement = self._showOnPopup(eventSource, feature);

View file

@ -28,7 +28,7 @@ export class ImageSearcher extends UIEventSource<{key: string, url: string}[]> {
private readonly _commons = new UIEventSource<string>(""); private readonly _commons = new UIEventSource<string>("");
constructor(tags: UIEventSource<any>) { constructor(tags: UIEventSource<any>, imagePrefix = "image", loadSpecial = true) {
super([]); super([]);
this._tags = tags; this._tags = tags;
@ -40,17 +40,17 @@ export class ImageSearcher extends UIEventSource<{key: string, url: string}[]> {
this._commons.addCallback(() => self.LoadCommons()); this._commons.addCallback(() => self.LoadCommons());
this._tags.addCallbackAndRun(() => self.LoadImages()); this._tags.addCallbackAndRun(() => self.LoadImages(imagePrefix, loadSpecial));
} }
private AddImage(key: string, url: string) { private AddImage(key: string, url: string) {
if (url === undefined || url === null || url === "") { if (url === undefined || url === null || url === "") {
return; return;
} }
for (const el of this.data) { for (const el of this.data) {
if (el.url === url) { if (el.url === url) {
// This url is already seen -> don't add it
return; return;
} }
} }
@ -102,17 +102,18 @@ export class ImageSearcher extends UIEventSource<{key: string, url: string}[]> {
} }
} }
private LoadImages(imagePrefix: string = "image", loadAdditional = true): void { private LoadImages(imagePrefix: string, loadAdditional: boolean): void {
const imageTag = this._tags.data.image; console.log("Loading images from",this._tags)
const imageTag = this._tags.data[imagePrefix];
if (imageTag !== undefined) { if (imageTag !== undefined) {
const bareImages = imageTag.split(";"); const bareImages = imageTag.split(";");
for (const bareImage of bareImages) { for (const bareImage of bareImages) {
this.AddImage("image", bareImage); this.AddImage(imagePrefix, bareImage);
} }
} }
for (const key in this._tags.data) { for (const key in this._tags.data) {
if (key.startsWith("image:")) { if (key.startsWith(imagePrefix+":")) {
const url = this._tags.data[key] const url = this._tags.data[key]
this.AddImage(key, url); this.AddImage(key, url);
} }
@ -130,7 +131,7 @@ export class ImageSearcher extends UIEventSource<{key: string, url: string}[]> {
} }
if (this._tags.data.mapillary) { if (this._tags.data.mapillary) {
this.AddImage("mapillary", "https://www.mapillary.com/map/im/" + this._tags.data.mapillary) this.AddImage(undefined,"https://www.mapillary.com/map/im/" + this._tags.data.mapillary)
} }
} }

View file

@ -29,6 +29,7 @@ export class Changes {
if(pending.length === 0){ if(pending.length === 0){
return; return;
} }
console.log("Sending ping",eventSource)
eventSource.ping(); eventSource.ping();
this.uploadAll([], pending); this.uploadAll([], pending);
} }

View file

@ -17,6 +17,7 @@ import State from "../../State";
import {VariableUiElement} from "../Base/VariableUIElement"; import {VariableUiElement} from "../Base/VariableUIElement";
import {FromJSON} from "../../Customizations/JSON/FromJSON"; import {FromJSON} from "../../Customizations/JSON/FromJSON";
import ValidatedTextField from "../Input/ValidatedTextField"; import ValidatedTextField from "../Input/ValidatedTextField";
import SpecialVisualizations from "../SpecialVisualizations";
export default class TagRenderingPanel extends InputElement<TagRenderingConfigJson> { export default class TagRenderingPanel extends InputElement<TagRenderingConfigJson> {
@ -83,7 +84,10 @@ export default class TagRenderingPanel extends InputElement<TagRenderingConfigJs
const settings: (string | SingleSetting<any>)[] = [ const settings: (string | SingleSetting<any>)[] = [
setting( setting(
options?.noLanguage ? new TextField({placeholder:"Rendering"}) : options?.noLanguage ? new TextField({placeholder:"Rendering"}) :
new MultiLingualTextFields(languages), "render", "Value to show", " Renders this value. Note that <span class='literal-code'>{key}</span>-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."), new MultiLingualTextFields(languages), "render", "Value to show",
"Renders this value. Note that <span class='literal-code'>{key}</span>-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." +
"<br/><br/>" +
"Furhtermore, some special functions are supported:"+SpecialVisualizations.HelpMessage.Render()),
questionsNotUnlocked ? `You need at least ${State.userJourney.themeGeneratorFullUnlock} changesets to unlock the 'question'-field and to use your theme to edit OSM data` : "", questionsNotUnlocked ? `You need at least ${State.userJourney.themeGeneratorFullUnlock} changesets to unlock the 'question'-field and to use your theme to edit OSM data` : "",
...(options?.disableQuestions ? [] : questionSettings), ...(options?.disableQuestions ? [] : questionSettings),

View file

@ -11,9 +11,9 @@ export class ImageCarousel extends TagDependantUIElement {
public readonly slideshow: SlideShow; public readonly slideshow: SlideShow;
constructor(tags: UIEventSource<any>) { constructor(tags: UIEventSource<any>, imagePrefix: string = "image", loadSpecial: boolean =true) {
super(tags); super(tags);
const searcher : UIEventSource<{url:string}[]> = new ImageSearcher(tags); const searcher : UIEventSource<{url:string}[]> = new ImageSearcher(tags, imagePrefix, loadSpecial);
const uiElements = searcher.map((imageURLS: {key: string, url:string}[]) => { const uiElements = searcher.map((imageURLS: {key: string, url:string}[]) => {
const uiElements: UIElement[] = []; const uiElements: UIElement[] = [];
for (const url of imageURLS) { for (const url of imageURLS) {

View file

@ -17,10 +17,12 @@ export class ImageUploadFlow extends UIElement {
private readonly _didFail: UIEventSource<boolean> = new UIEventSource<boolean>(false); private readonly _didFail: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly _allDone: UIEventSource<boolean> = new UIEventSource<boolean>(false); private readonly _allDone: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly _connectButton: UIElement; private readonly _connectButton: UIElement;
private readonly _imagePrefix: string;
constructor(tags: UIEventSource<any>) { constructor(tags: UIEventSource<any>, imagePrefix: string = "image") {
super(State.state.osmConnection.userDetails); super(State.state.osmConnection.userDetails);
this._tags = tags; this._tags = tags;
this._imagePrefix = imagePrefix;
this.ListenTo(this._isUploading); this.ListenTo(this._isUploading);
this.ListenTo(this._didFail); this.ListenTo(this._didFail);
@ -131,20 +133,21 @@ export class ImageUploadFlow extends UIElement {
private handleSuccessfulUpload(url) { private handleSuccessfulUpload(url) {
const tags = this._tags.data; const tags = this._tags.data;
let key = "image"; let key = this._imagePrefix;
if (tags["image"] !== undefined) { if (tags[this._imagePrefix] !== undefined) {
let freeIndex = 0; let freeIndex = 0;
while (tags["image:" + freeIndex] !== undefined) { while (tags[this._imagePrefix + ":" + freeIndex] !== undefined) {
freeIndex++; freeIndex++;
} }
key = "image:" + freeIndex; key = this._imagePrefix + ":" + freeIndex;
} }
console.log("Adding image:" + key, url); console.log("Adding image:" + key, url);
State.state.changes.addTag(tags.id, new Tag(key, url)); State.state.changes.addTag(tags.id, new Tag(key, url));
} }
private handleFiles(files) { private handleFiles(files) {
console.log("Received images from the user, starting upload")
this._isUploading.setData(files.length); this._isUploading.setData(files.length);
this._allDone.setData(false); this._allDone.setData(false);
@ -189,7 +192,6 @@ export class ImageUploadFlow extends UIElement {
InnerUpdate(htmlElement: HTMLElement) { InnerUpdate(htmlElement: HTMLElement) {
super.InnerUpdate(htmlElement); super.InnerUpdate(htmlElement);
const user = State.state.osmConnection.userDetails.data;
this._licensePicker.Update() this._licensePicker.Update()
const form = document.getElementById('fileselector-form-' + this.id) as HTMLFormElement const form = document.getElementById('fileselector-form-' + this.id) as HTMLFormElement
@ -197,8 +199,7 @@ export class ImageUploadFlow extends UIElement {
const self = this const self = this
function submitHandler() { function submitHandler() {
const files = $(selector).prop('files'); self.handleFiles($(selector).prop('files'))
self.handleFiles(files)
} }
if (selector != null && form != null) { if (selector != null && form != null) {
@ -206,8 +207,6 @@ export class ImageUploadFlow extends UIElement {
submitHandler() submitHandler()
} }
form.addEventListener('submit', e => { form.addEventListener('submit', e => {
console.log(e)
alert('wait')
e.preventDefault() e.preventDefault()
submitHandler() submitHandler()
}) })

View file

@ -81,7 +81,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
const self = this; const self = this;
this.currentTags = this._source.map(tags => { this.currentTags = tags.map(tags => {
if (options.tagsPreprocessor === undefined) { if (options.tagsPreprocessor === undefined) {
return tags; return tags;
@ -96,6 +96,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
return newTags; return newTags;
} }
); );
tags.addCallback(() => self.currentTags.ping());
if (options.question !== undefined) { if (options.question !== undefined) {
this._question = options.question; this._question = options.question;
@ -516,7 +517,10 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
this._editButton.Update(); this._editButton.Update();
} }
private answerCache = {} private readonly answerCache = {}
// Makes sure that the elements receive updates
// noinspection JSMismatchedCollectionQueryUpdate
private readonly substitutedElements : UIElement[]= [];
private ApplyTemplate(template: string | Translation): UIElement { private ApplyTemplate(template: string | Translation): UIElement {
const tr = Translations.WT(template); const tr = Translations.WT(template);
@ -526,6 +530,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
// We have to cache these elemnts, otherwise it is to slow // We have to cache these elemnts, otherwise it is to slow
const el = new SubstitutedTranslation(tr, this.currentTags); const el = new SubstitutedTranslation(tr, this.currentTags);
this.answerCache[tr.id] = el; this.answerCache[tr.id] = el;
this.substitutedElements.push(el);
return el; return el;
} }

View file

@ -13,7 +13,7 @@ import {ImageUploadFlow} from "./Image/ImageUploadFlow";
export class SubstitutedTranslation extends UIElement { export class SubstitutedTranslation extends UIElement {
private readonly tags: UIEventSource<any>; private readonly tags: UIEventSource<any>;
private readonly translation: Translation; private readonly translation: Translation;
private content: UIElement; private content: UIElement[];
constructor( constructor(
translation: Translation, translation: Translation,
@ -25,18 +25,19 @@ export class SubstitutedTranslation extends UIElement {
Locale.language.addCallbackAndRun(() => { Locale.language.addCallbackAndRun(() => {
self.content = self.CreateContent(); self.content = self.CreateContent();
self.Update(); self.Update();
}) });
this.dumbMode = false;
} }
InnerRender(): string { InnerRender(): string {
return this.content.Render(); return new Combine(this.content).Render();
} }
CreateContent(): UIElement { private CreateContent(): UIElement[] {
let txt = this.translation?.txt; let txt = this.translation?.txt;
if (txt === undefined) { if (txt === undefined) {
return new FixedUiElement("") return []
} }
const tags = this.tags.data; const tags = this.tags.data;
for (const key in tags) { for (const key in tags) {
@ -44,11 +45,10 @@ export class SubstitutedTranslation extends UIElement {
txt = txt.split("{" + key + "}").join(tags[key]); txt = txt.split("{" + key + "}").join(tags[key]);
} }
return this.EvaluateSpecialComponents(txt);
return new Combine(this.EvaluateSpecialComponents(txt));
} }
public EvaluateSpecialComponents(template: string): UIElement[] { private EvaluateSpecialComponents(template: string): UIElement[] {
for (const knownSpecial of SpecialVisualizations.specialVisualizations) { for (const knownSpecial of SpecialVisualizations.specialVisualizations) {
@ -58,10 +58,22 @@ export class SubstitutedTranslation extends UIElement {
// We found a special component that should be brought to live // We found a special component that should be brought to live
const partBefore = this.EvaluateSpecialComponents(matched[1]); const partBefore = this.EvaluateSpecialComponents(matched[1]);
const argument = matched[2]; const argument = matched[2].trim();
const partAfter = this.EvaluateSpecialComponents(matched[3]); const partAfter = this.EvaluateSpecialComponents(matched[3]);
try { try {
const args = argument.trim().split(",").map(str => str.trim()); const args = knownSpecial.args.map(arg => arg.defaultValue ?? "");
if (argument.length > 0) {
const realArgs = argument.split(",").map(str => str.trim());
for (let i = 0; i < realArgs.length; i++) {
if (args.length <= i) {
args.push(realArgs[i]);
} else {
args[i] = realArgs[i];
}
}
}
const element = knownSpecial.constr(this.tags, args); const element = knownSpecial.constr(this.tags, args);
return [...partBefore, element, ...partAfter] return [...partBefore, element, ...partAfter]
} catch (e) { } catch (e) {
@ -83,6 +95,7 @@ export default class SpecialVisualizations {
funcName: string, funcName: string,
constr: ((tagSource: UIEventSource<any>, argument: string[]) => UIElement), constr: ((tagSource: UIEventSource<any>, argument: string[]) => UIElement),
docs: string, docs: string,
example?: string,
args: { name: string, defaultValue?: string, doc: string }[] args: { name: string, defaultValue?: string, doc: string }[]
}[] = }[] =
@ -91,16 +104,17 @@ export default class SpecialVisualizations {
funcName: "image_carousel", funcName: "image_carousel",
docs: "Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)", docs: "Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)",
args: [{ args: [{
name: "image tag(s)", name: "image key/prefix",
defaultValue: "image,image:*,wikidata,wikipedia,wikimedia_commons", defaultValue: "image",
doc: "Image tag(s) where images are searched" doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... "
}], },
{
name: "smart search",
defaultValue: "true",
doc: "Also include images given via 'Wikidata', 'wikimedia_commons' and 'mapillary"
}],
constr: (tags, args) => { constr: (tags, args) => {
if (args.length > 0) { return new ImageCarousel(tags, args[0], args[1].toLowerCase() === "true");
console.error("TODO HANDLE THESE ARGS") // TODO FIXME
}
return new ImageCarousel(tags);
} }
}, },
@ -108,15 +122,12 @@ export default class SpecialVisualizations {
funcName: "image_upload", funcName: "image_upload",
docs: "Creates a button where a user can upload an image to IMGUR", docs: "Creates a button where a user can upload an image to IMGUR",
args: [{ args: [{
name: "image-key",
doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)", doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)",
defaultValue: "image", name: "image-key" defaultValue: "image"
}], }],
constr: (tags, args) => { constr: (tags, args) => {
if (args.length > 0) { return new ImageUploadFlow(tags, args[0])
console.error("TODO HANDLE THESE ARGS") // TODO FIXME
}
return new ImageUploadFlow(tags)
} }
}, },
{ {
@ -125,7 +136,7 @@ export default class SpecialVisualizations {
args: [{ args: [{
name: "key", name: "key",
defaultValue: "opening_hours", defaultValue: "opening_hours",
doc: "The tag from which the table is constructed" doc: "The tagkey from which the table is constructed."
}], }],
constr: (tagSource: UIEventSource<any>, args) => { constr: (tagSource: UIEventSource<any>, args) => {
let keyname = args[0]; let keyname = args[0];
@ -139,6 +150,7 @@ export default class SpecialVisualizations {
{ {
funcName: "live", funcName: "live",
docs: "Downloads a JSON from the given URL, e.g. '{live(example.org/data.json, shorthand:x.y.z, other:a.b.c, shorthand)}' will download the given file, will create an object {shorthand: json[x][y][z], other: json[a][b][c] out of it and will return 'other' or 'json[a][b][c]. This is made to use in combination with tags, e.g. {live({url}, {url:format}, needed_value)}", docs: "Downloads a JSON from the given URL, e.g. '{live(example.org/data.json, shorthand:x.y.z, other:a.b.c, shorthand)}' will download the given file, will create an object {shorthand: json[x][y][z], other: json[a][b][c] out of it and will return 'other' or 'json[a][b][c]. This is made to use in combination with tags, e.g. {live({url}, {url:format}, needed_value)}",
example: "{live({url},{url:format},hour)} {live(https://data.mobility.brussels/bike/api/counts/?request=live&featureID=CB2105,hour:data.hour_cnt;day:data.day_cnt;year:data.year_cnt,hour)}",
args: [{ args: [{
name: "Url", doc: "The URL to load" name: "Url", doc: "The URL to load"
}, { }, {
@ -157,5 +169,38 @@ export default class SpecialVisualizations {
} }
] ]
static HelpMessage: UIElement = SpecialVisualizations.GenHelpMessage();
private static GenHelpMessage() {
const helpTexts =
SpecialVisualizations.specialVisualizations.map(viz => new Combine(
[
`<h3>${viz.funcName}</h3>`,
viz.docs,
"<ol>",
...viz.args.map(arg => new Combine([
"<li>",
"<b>" + arg.name + "</b>: ",
arg.doc,
arg.defaultValue === undefined ? "" : (" Default: <span class='literal-code'>" + arg.defaultValue + "</span>"),
"</li>"
])),
"</ol>",
"<b>Example usage: </b>",
new FixedUiElement(
viz.example ?? "{" + viz.funcName + "(" + viz.args.map(arg => arg.defaultValue).join(",") + ")}"
).SetClass("literal-code"),
]
));
return new Combine([
"In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.",
...helpTexts
]
);
}
} }

File diff suppressed because one or more lines are too long

View file

@ -2,7 +2,7 @@
import SpecialVisualizations from "./UI/SpecialVisualizations"; import SpecialVisualizations from "./UI/SpecialVisualizations";
SpecialVisualizations.HelpMessage.AttachTo("maindivgi") SpecialVisualizations.HelpMessage.AttachTo("maindiv")
/*/ /*/