forked from MapComplete/MapComplete
		
	First working plantnet UI
This commit is contained in:
		
							parent
							
								
									a8959fc934
								
							
						
					
					
						commit
						06f8cf7006
					
				
					 9 changed files with 216 additions and 39 deletions
				
			
		|  | @ -3,12 +3,11 @@ import BaseUIElement from "../BaseUIElement"; | |||
| 
 | ||||
| export class Button extends BaseUIElement { | ||||
|     private _text: BaseUIElement; | ||||
|     private _onclick: () => void; | ||||
| 
 | ||||
|     constructor(text: string | BaseUIElement, onclick: (() => void)) { | ||||
|     constructor(text: string | BaseUIElement, onclick: (() => void | Promise<void>)) { | ||||
|         super(); | ||||
|         this._text = Translations.W(text); | ||||
|         this._onclick = onclick; | ||||
|         this.onClick(onclick) | ||||
|     } | ||||
| 
 | ||||
|     protected InnerConstructElement(): HTMLElement { | ||||
|  | @ -20,7 +19,6 @@ export class Button extends BaseUIElement { | |||
|         const button = document.createElement("button") | ||||
|         button.type = "button" | ||||
|         button.appendChild(el) | ||||
|         button.onclick = this._onclick | ||||
|         form.appendChild(button) | ||||
|         return form; | ||||
|     } | ||||
|  |  | |||
|  | @ -1,5 +1,3 @@ | |||
| import { Utils } from "../Utils"; | ||||
| 
 | ||||
| /** | ||||
|  * A thin wrapper around a html element, which allows to generate a HTML-element. | ||||
|  * | ||||
|  | @ -11,7 +9,7 @@ export default abstract class BaseUIElement { | |||
|     protected isDestroyed = false; | ||||
|     private readonly clss: Set<string> = new Set<string>(); | ||||
|     private style: string; | ||||
|     private _onClick: () => void; | ||||
|     private _onClick: () => void | Promise<void>; | ||||
| 
 | ||||
|     public onClick(f: (() => void)) { | ||||
|         this._onClick = f; | ||||
|  | @ -127,12 +125,15 @@ export default abstract class BaseUIElement { | |||
| 
 | ||||
|             if (this._onClick !== undefined) { | ||||
|                 const self = this; | ||||
|                 el.onclick = (e) => { | ||||
|                 el.onclick = async (e) => { | ||||
|                     // @ts-ignore
 | ||||
|                     if (e.consumed) { | ||||
|                         return; | ||||
|                     } | ||||
|                     self._onClick(); | ||||
|                     const v = self._onClick(); | ||||
|                     if(typeof v === "object"){ | ||||
|                         await v | ||||
|                     } | ||||
|                     // @ts-ignore
 | ||||
|                     e.consumed = true; | ||||
|                 } | ||||
|  |  | |||
							
								
								
									
										103
									
								
								UI/BigComponents/PlantNetSpeciesSearch.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								UI/BigComponents/PlantNetSpeciesSearch.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,103 @@ | |||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import PlantNet from "../../Logic/Web/PlantNet"; | ||||
| import Loading from "../Base/Loading"; | ||||
| import Wikidata from "../../Logic/Web/Wikidata"; | ||||
| import WikidataPreviewBox from "../Wikipedia/WikidataPreviewBox"; | ||||
| import {Button} from "../Base/Button"; | ||||
| import Combine from "../Base/Combine"; | ||||
| import Title from "../Base/Title"; | ||||
| import WikipediaBox from "../Wikipedia/WikipediaBox"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| 
 | ||||
| 
 | ||||
| export default class PlantNetSpeciesSearch extends VariableUiElement { | ||||
|     /*** | ||||
|      * Given images, queries plantnet to search a species matching those images. | ||||
|      * A list of species will be presented to the user, after which they can confirm an item. | ||||
|      * The wikidata-url is returned in the callback when the user selects one | ||||
|      */ | ||||
|     constructor(images: Store<string[]>, onConfirm: (wikidataUrl: string) => Promise<void>) { | ||||
|         const t = Translations.t.plantDetection | ||||
|         super( | ||||
|             images | ||||
|                 .bind(images => { | ||||
|                     if (images.length === 0) { | ||||
|                         return null | ||||
|                     } | ||||
|                     return new UIEventSource({success: PlantNet.exampleResultPrunus}) /*/ UIEventSource.FromPromiseWithErr(PlantNet.query(images.slice(0,5))); //*/ | ||||
|                 }) | ||||
|                 .map(result => { | ||||
|                         if (result === undefined) { | ||||
|                             return new Loading(t.querying.Subs(images.data)) | ||||
|                         } | ||||
|                         if (result === null) { | ||||
|                             return t.takeImages | ||||
|                         } | ||||
|                         if (result["error"] !== undefined) { | ||||
|                             return t.error.Subs(<any>result).SetClass("alert") | ||||
|                         } | ||||
|                         console.log(result) | ||||
|                         const success = result["success"] | ||||
| 
 | ||||
|                         const selectedSpecies = new UIEventSource<string>(undefined) | ||||
|                         const speciesInformation = success.results | ||||
|                             .filter(species => species.score >= 0.005) | ||||
|                             .map(species => { | ||||
|                                     const wikidata = UIEventSource.FromPromise(Wikidata.Sparql<{ species }>(["?species", "?speciesLabel"], | ||||
|                                         ["?species wdt:P846 \"" + species.gbif.id + "\""])); | ||||
| 
 | ||||
|                                     const confirmButton = new Button(t.seeInfo, async() => { | ||||
|                                       await  selectedSpecies.setData(wikidata.data[0].species?.value) | ||||
|                                     }).SetClass("btn") | ||||
| 
 | ||||
|                                     const match = t.matchPercentage.Subs({match: Math.round(species.score * 100)}).SetClass("font-bold") | ||||
| 
 | ||||
|                                     const extraItems = new Combine([match, confirmButton]).SetClass("flex flex-col") | ||||
| 
 | ||||
|                                     return new WikidataPreviewBox(wikidata.map(wd => wd == undefined ? undefined : wd[0]?.species?.value), | ||||
|                                         { | ||||
|                                             whileLoading: new Loading( | ||||
|                                                 t.loadingWikidata.Subs({species: species.species.scientificNameWithoutAuthor})), | ||||
|                                             extraItems: [new Combine([extraItems])], | ||||
| 
 | ||||
|                                             imageStyle: "max-width: 8rem; width: unset; height: 8rem" | ||||
|                                         }) | ||||
|                                         .SetClass("border-2 border-subtle rounded-xl block mb-2") | ||||
|                                 } | ||||
|                             ); | ||||
|                         const plantOverview = new Combine([ | ||||
|                             new Title(t.overviewTitle), | ||||
|                             t.overviewIntro, | ||||
|                             t.overviewVerify.SetClass("font-bold"), | ||||
|                             ...speciesInformation]).SetClass("flex flex-col") | ||||
| 
 | ||||
| 
 | ||||
|                         return new VariableUiElement(selectedSpecies.map(wikidataSpecies => { | ||||
|                             if (wikidataSpecies === undefined) { | ||||
|                                 return plantOverview | ||||
|                             } | ||||
|                             const buttons = new Combine([ | ||||
|                                 new Button("Confirm", () => { | ||||
|                                     onConfirm(wikidataSpecies) | ||||
|                                 }).SetClass("btn"), | ||||
|                                 new Button("Back to plant overview", () => { | ||||
|                                     selectedSpecies.setData(undefined) | ||||
|                                 }).SetClass("btn btn-secondary") | ||||
|                             ]).SetClass("flex self-end"); | ||||
| 
 | ||||
|                             return new Combine([ | ||||
|                                 new WikipediaBox([wikidataSpecies], { | ||||
|                                     firstParagraphOnly: false, | ||||
|                                     noImages: false, | ||||
|                                     addHeader: false | ||||
|                                 }).SetClass("h-96"), | ||||
|                                 buttons | ||||
|                             ]).SetClass("flex flex-col self-end") | ||||
|                         })) | ||||
| 
 | ||||
|                     } | ||||
|                 )) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -61,6 +61,8 @@ import StatisticsPanel from "./BigComponents/StatisticsPanel"; | |||
| import {OsmFeature} from "../Models/OsmFeature"; | ||||
| import EditableTagRendering from "./Popup/EditableTagRendering"; | ||||
| import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"; | ||||
| import {ProvidedImage} from "../Logic/ImageProviders/ImageProvider"; | ||||
| import PlantNetSpeciesSearch from "./BigComponents/PlantNetSpeciesSearch"; | ||||
| 
 | ||||
| export interface SpecialVisualization { | ||||
|     funcName: string, | ||||
|  | @ -196,7 +198,7 @@ class NearbyImageVis implements SpecialVisualization { | |||
|                     new ChangeTagAction( | ||||
|                         id, | ||||
|                         new And(tags), | ||||
|                         tagSource, | ||||
|                         tagSource.data, | ||||
|                         { | ||||
|                             theme: state?.layoutToUse.id, | ||||
|                             changeType: "link-image" | ||||
|  | @ -1299,6 +1301,46 @@ export default class SpecialVisualizations { | |||
|                         const [layerId, __] = tagRenderingId.split(".") | ||||
|                         return [layerId] | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     funcName: "plantnet_detection", | ||||
| 
 | ||||
|                     docs: "Sends the images linked to the current object to plantnet.org and asks it what plant species is shown on it. The user can then select the correct species; the corresponding wikidata-identifier will then be added to the object (together with `source:species:wikidata=plantnet.org AI`). ", | ||||
|                     args: [{ | ||||
|                         name: "image_key", | ||||
|                         defaultValue: AllImageProviders.defaultKeys.join(","), | ||||
|                         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... Multiple values are allowed if ';'-separated " | ||||
|                     }], | ||||
|                     constr: (state, tags, args) => { | ||||
|                         let imagePrefixes: string[] = undefined; | ||||
|                         if (args.length > 0) { | ||||
|                             imagePrefixes = [].concat(...args.map(a => a.split(","))); | ||||
|                         } | ||||
| 
 | ||||
|                         const detect = new UIEventSource(false) | ||||
|                         return new Toggle( | ||||
|                             new Lazy(() => { | ||||
|                                 const allProvidedImages: Store<ProvidedImage[]> = AllImageProviders.LoadImagesFor(tags, imagePrefixes) | ||||
|                                 const allImages: Store<string[]> = allProvidedImages.map(pi => pi.map(pi => pi.url)) | ||||
|                                 return new PlantNetSpeciesSearch(allImages, async selectedWikidata => { | ||||
|                                     selectedWikidata = Wikidata.ExtractKey(selectedWikidata) | ||||
|                                     const change = new ChangeTagAction(tags.data.id, | ||||
|                                         new And([new Tag("species:wikidata", selectedWikidata), | ||||
|                                         new Tag("source:species:wikidata","PlantNet.org AI") | ||||
|                                         ]), | ||||
|                                         tags.data, | ||||
|                                         { | ||||
|                                             theme: state.layoutToUse.id, | ||||
|                                             changeType: "plantnet-ai-detection" | ||||
|                                         } | ||||
|                                         ) | ||||
|                                    await state.changes.applyAction(change) | ||||
|                                 }) | ||||
|                             }), | ||||
|                             new SubtleButton(undefined, "Detect plant species with plantnet.org").onClick(() => detect.setData(true)), | ||||
|                             detect | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
| 
 | ||||
|  |  | |||
|  | @ -57,7 +57,11 @@ export default class WikidataPreviewBox extends VariableUiElement { | |||
|         } | ||||
|     ] | ||||
| 
 | ||||
|     constructor(wikidataId: Store<string>, options?: {noImages?: boolean, whileLoading?: BaseUIElement | string, extraItems?: (BaseUIElement | string)[]}) { | ||||
|     constructor(wikidataId: Store<string>, options?: { | ||||
|         noImages?: boolean,  | ||||
|         imageStyle?: string, | ||||
|         whileLoading?: BaseUIElement | string,  | ||||
|         extraItems?: (BaseUIElement | string)[]}) { | ||||
|         let inited = false; | ||||
|         const wikidata = wikidataId | ||||
|             .stabilized(250) | ||||
|  | @ -87,7 +91,10 @@ export default class WikidataPreviewBox extends VariableUiElement { | |||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public static WikidataResponsePreview(wikidata: WikidataResponse, options?: {noImages?: boolean, extraItems?: (BaseUIElement | string)[]}): BaseUIElement { | ||||
|     public static WikidataResponsePreview(wikidata: WikidataResponse, options?: { | ||||
|         noImages?: boolean,  | ||||
|         imageStyle?: string, | ||||
|         extraItems?: (BaseUIElement | string)[]}): BaseUIElement { | ||||
|         let link = new Link( | ||||
|             new Combine([ | ||||
|                 wikidata.id, | ||||
|  | @ -111,7 +118,7 @@ export default class WikidataPreviewBox extends VariableUiElement { | |||
|         } | ||||
|         if (imageUrl && !options?.noImages) { | ||||
|             imageUrl = WikimediaImageProvider.singleton.PrepUrl(imageUrl).url | ||||
|             info = new Combine([new Img(imageUrl).SetStyle("max-width: 5rem; width: unset; height: 4rem").SetClass("rounded-xl mr-2"), | ||||
|             info = new Combine([new Img(imageUrl).SetStyle(options?.imageStyle ?? "max-width: 5rem; width: unset; height: 4rem").SetClass("rounded-xl mr-2"), | ||||
|                 info.SetClass("w-full")]).SetClass("flex") | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue