Add further support for special UI-elements; add documentation, fix a few bugs
This commit is contained in:
parent
3ab3cef249
commit
07e611bf10
12 changed files with 113 additions and 55 deletions
|
@ -145,7 +145,6 @@ export class FromJSON {
|
|||
json = "{image_carousel()}{image_upload()}";
|
||||
}
|
||||
}
|
||||
console.warn("Possible literal rendering:", json)
|
||||
|
||||
return new TagRenderingOptions({
|
||||
freeform: {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,7 +40,7 @@ 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));
|
||||
|
||||
}
|
||||
|
||||
|
@ -48,9 +48,9 @@ export class ImageSearcher extends UIEventSource<{key: string, url: string}[]> {
|
|||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ export class Changes {
|
|||
if(pending.length === 0){
|
||||
return;
|
||||
}
|
||||
console.log("Sending ping",eventSource)
|
||||
eventSource.ping();
|
||||
this.uploadAll([], pending);
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
2
test.ts
2
test.ts
|
@ -2,7 +2,7 @@
|
|||
|
||||
import SpecialVisualizations from "./UI/SpecialVisualizations";
|
||||
|
||||
SpecialVisualizations.HelpMessage.AttachTo("maindivgi")
|
||||
SpecialVisualizations.HelpMessage.AttachTo("maindiv")
|
||||
|
||||
|
||||
/*/
|
||||
|
|
Loading…
Reference in a new issue