forked from MapComplete/MapComplete
		
	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
 | ||||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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) | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
|  |  | |||
|  | @ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue