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()}";
}
}
console.warn("Possible literal rendering:", json)
return new TagRenderingOptions({
freeform: {

View file

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

View file

@ -111,7 +111,10 @@ export class FilteredLayer {
if (this.filters.matches(tags)) {
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 lon = centerPoint.geometry.coordinates[0]
feature.properties["_lon"] = "" + lat; // We expect a string here for lat/lon
@ -252,7 +255,7 @@ export class FilteredLayer {
const popup = L.popup({}, marker);
let uiElement: UIElement;
let content = undefined;
let p = marker.bindPopup(popup)
let p = marker.bindPopup(popup)
.on("popupopen", () => {
if (content === undefined) {
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>("");
constructor(tags: UIEventSource<any>) {
constructor(tags: UIEventSource<any>, imagePrefix = "image", loadSpecial = true) {
super([]);
this._tags = tags;
@ -40,17 +40,17 @@ export class ImageSearcher extends UIEventSource<{key: string, url: string}[]> {
this._commons.addCallback(() => self.LoadCommons());
this._tags.addCallbackAndRun(() => self.LoadImages());
this._tags.addCallbackAndRun(() => self.LoadImages(imagePrefix, loadSpecial));
}
private AddImage(key: string, url: string) {
if (url === undefined || url === null || url === "") {
if (url === undefined || url === null || url === "") {
return;
}
for (const el of this.data) {
if (el.url === url) {
// This url is already seen -> don't add it
return;
}
}
@ -102,17 +102,18 @@ export class ImageSearcher extends UIEventSource<{key: string, url: string}[]> {
}
}
private LoadImages(imagePrefix: string = "image", loadAdditional = true): void {
const imageTag = this._tags.data.image;
private LoadImages(imagePrefix: string, loadAdditional: boolean): void {
console.log("Loading images from",this._tags)
const imageTag = this._tags.data[imagePrefix];
if (imageTag !== undefined) {
const bareImages = imageTag.split(";");
for (const bareImage of bareImages) {
this.AddImage("image", bareImage);
this.AddImage(imagePrefix, bareImage);
}
}
for (const key in this._tags.data) {
if (key.startsWith("image:")) {
if (key.startsWith(imagePrefix+":")) {
const url = this._tags.data[key]
this.AddImage(key, url);
}
@ -130,7 +131,7 @@ export class ImageSearcher extends UIEventSource<{key: string, url: string}[]> {
}
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){
return;
}
console.log("Sending ping",eventSource)
eventSource.ping();
this.uploadAll([], pending);
}

View file

@ -17,6 +17,7 @@ import State from "../../State";
import {VariableUiElement} from "../Base/VariableUIElement";
import {FromJSON} from "../../Customizations/JSON/FromJSON";
import ValidatedTextField from "../Input/ValidatedTextField";
import SpecialVisualizations from "../SpecialVisualizations";
export default class TagRenderingPanel extends InputElement<TagRenderingConfigJson> {
@ -83,7 +84,10 @@ export default class TagRenderingPanel extends InputElement<TagRenderingConfigJs
const settings: (string | SingleSetting<any>)[] = [
setting(
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` : "",
...(options?.disableQuestions ? [] : questionSettings),

View file

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

View file

@ -81,7 +81,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
const self = this;
this.currentTags = this._source.map(tags => {
this.currentTags = tags.map(tags => {
if (options.tagsPreprocessor === undefined) {
return tags;
@ -96,6 +96,7 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
return newTags;
}
);
tags.addCallback(() => self.currentTags.ping());
if (options.question !== undefined) {
this._question = options.question;
@ -516,7 +517,10 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
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 {
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
const el = new SubstitutedTranslation(tr, this.currentTags);
this.answerCache[tr.id] = el;
this.substitutedElements.push(el);
return el;
}

View file

@ -13,7 +13,7 @@ import {ImageUploadFlow} from "./Image/ImageUploadFlow";
export class SubstitutedTranslation extends UIElement {
private readonly tags: UIEventSource<any>;
private readonly translation: Translation;
private content: UIElement;
private content: UIElement[];
constructor(
translation: Translation,
@ -25,18 +25,19 @@ export class SubstitutedTranslation extends UIElement {
Locale.language.addCallbackAndRun(() => {
self.content = self.CreateContent();
self.Update();
})
});
this.dumbMode = false;
}
InnerRender(): string {
return this.content.Render();
return new Combine(this.content).Render();
}
CreateContent(): UIElement {
private CreateContent(): UIElement[] {
let txt = this.translation?.txt;
if (txt === undefined) {
return new FixedUiElement("")
return []
}
const tags = this.tags.data;
for (const key in tags) {
@ -44,11 +45,10 @@ export class SubstitutedTranslation extends UIElement {
txt = txt.split("{" + key + "}").join(tags[key]);
}
return new Combine(this.EvaluateSpecialComponents(txt));
return this.EvaluateSpecialComponents(txt);
}
public EvaluateSpecialComponents(template: string): UIElement[] {
private EvaluateSpecialComponents(template: string): UIElement[] {
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
const partBefore = this.EvaluateSpecialComponents(matched[1]);
const argument = matched[2];
const argument = matched[2].trim();
const partAfter = this.EvaluateSpecialComponents(matched[3]);
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);
return [...partBefore, element, ...partAfter]
} catch (e) {
@ -83,6 +95,7 @@ export default class SpecialVisualizations {
funcName: string,
constr: ((tagSource: UIEventSource<any>, argument: string[]) => UIElement),
docs: string,
example?: string,
args: { name: string, defaultValue?: string, doc: string }[]
}[] =
@ -91,16 +104,17 @@ export default class SpecialVisualizations {
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)",
args: [{
name: "image tag(s)",
defaultValue: "image,image:*,wikidata,wikipedia,wikimedia_commons",
doc: "Image tag(s) where images are searched"
}],
name: "image key/prefix",
defaultValue: "image",
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) => {
if (args.length > 0) {
console.error("TODO HANDLE THESE ARGS") // TODO FIXME
}
return new ImageCarousel(tags);
return new ImageCarousel(tags, args[0], args[1].toLowerCase() === "true");
}
},
@ -108,15 +122,12 @@ export default class SpecialVisualizations {
funcName: "image_upload",
docs: "Creates a button where a user can upload an image to IMGUR",
args: [{
name: "image-key",
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) => {
if (args.length > 0) {
console.error("TODO HANDLE THESE ARGS") // TODO FIXME
}
return new ImageUploadFlow(tags)
return new ImageUploadFlow(tags, args[0])
}
},
{
@ -125,7 +136,7 @@ export default class SpecialVisualizations {
args: [{
name: "key",
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) => {
let keyname = args[0];
@ -139,6 +150,7 @@ export default class SpecialVisualizations {
{
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)}",
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: [{
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";
SpecialVisualizations.HelpMessage.AttachTo("maindivgi")
SpecialVisualizations.HelpMessage.AttachTo("maindiv")
/*/