forked from MapComplete/MapComplete
		
	Refactoring: port PlantNet-detection to svelte, re-integrate wikipedia component
This commit is contained in:
		
							parent
							
								
									d1aa751e18
								
							
						
					
					
						commit
						5f04a69517
					
				
					 18 changed files with 297 additions and 210 deletions
				
			
		|  | @ -380,6 +380,7 @@ | ||||||
|                 "born": "Born: {value}", |                 "born": "Born: {value}", | ||||||
|                 "died": "Died: {value}" |                 "died": "Died: {value}" | ||||||
|             }, |             }, | ||||||
|  |             "readMore": "Read the rest of the article", | ||||||
|             "searchToShort": "Your search query is too short, enter a longer text", |             "searchToShort": "Your search query is too short, enter a longer text", | ||||||
|             "searchWikidata": "Search on Wikidata", |             "searchWikidata": "Search on Wikidata", | ||||||
|             "wikipediaboxTitle": "Wikipedia" |             "wikipediaboxTitle": "Wikipedia" | ||||||
|  | @ -498,7 +499,9 @@ | ||||||
|     }, |     }, | ||||||
|     "plantDetection": { |     "plantDetection": { | ||||||
|         "back": "Back to species overview", |         "back": "Back to species overview", | ||||||
|  |         "button": "Automatically detect the plant species using the AI of Plantnet.org", | ||||||
|         "confirm": "Select species", |         "confirm": "Select species", | ||||||
|  |         "done": "The species has been applied", | ||||||
|         "error": "Something went wrong while detecting the tree species: {error}", |         "error": "Something went wrong while detecting the tree species: {error}", | ||||||
|         "howTo": { |         "howTo": { | ||||||
|             "intro": "For optimal results,", |             "intro": "For optimal results,", | ||||||
|  | @ -515,7 +518,8 @@ | ||||||
|         "poweredByPlantnet": "Powered by <a href='https://plantnet.org' target='_blank'>plantnet.org</a>", |         "poweredByPlantnet": "Powered by <a href='https://plantnet.org' target='_blank'>plantnet.org</a>", | ||||||
|         "querying": "Querying plantnet.org with {length} images", |         "querying": "Querying plantnet.org with {length} images", | ||||||
|         "seeInfo": "See more information about the species", |         "seeInfo": "See more information about the species", | ||||||
|         "takeImages": "Take images of the tree to automatically detect the tree type" |         "takeImages": "Take images of the tree to automatically detect the tree type", | ||||||
|  |         "tryAgain": "Select a different species" | ||||||
|     }, |     }, | ||||||
|     "privacy": { |     "privacy": { | ||||||
|         "editing": "When you make a change to the map, this change is recorded on OpenStreetMap and is publicly available to anyone. A changeset made with MapComplete includes the following data: <ul><li>The changes you made</li><li>Your username</li><li>When this change is made</li><li>The theme you used while making the change</li><li>The language of the user interface</li><li>An indication of how close you were to changed objects. Other mappers can use this information to determine if a change was made based on survey or on remote research</li></ul> Please refer to <a href='https://wiki.osmfoundation.org/wiki/Privacy_Policy' target='_blank'>the privacy policy on OpenStreetMap.org</a> for detailed information. We'd like to remind you that you can use a fictional name when signing up.", |         "editing": "When you make a change to the map, this change is recorded on OpenStreetMap and is publicly available to anyone. A changeset made with MapComplete includes the following data: <ul><li>The changes you made</li><li>Your username</li><li>When this change is made</li><li>The theme you used while making the change</li><li>The language of the user interface</li><li>An indication of how close you were to changed objects. Other mappers can use this information to determine if a change was made based on survey or on remote research</li></ul> Please refer to <a href='https://wiki.osmfoundation.org/wiki/Privacy_Policy' target='_blank'>the privacy policy on OpenStreetMap.org</a> for detailed information. We'd like to remind you that you can use a fictional name when signing up.", | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| { | { | ||||||
|   "name": "mapcomplete", |   "name": "mapcomplete", | ||||||
|   "version": "0.32.0", |   "version": "0.33.0", | ||||||
|   "repository": "https://github.com/pietervdvn/MapComplete", |   "repository": "https://github.com/pietervdvn/MapComplete", | ||||||
|   "description": "A small website to edit OSM easily", |   "description": "A small website to edit OSM easily", | ||||||
|   "bugs": "https://github.com/pietervdvn/MapComplete/issues", |   "bugs": "https://github.com/pietervdvn/MapComplete/issues", | ||||||
|  |  | ||||||
|  | @ -1096,6 +1096,10 @@ video { | ||||||
|   height: 2.75rem; |   height: 2.75rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .h-10 { | ||||||
|  |   height: 2.5rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .h-48 { | .h-48 { | ||||||
|   height: 12rem; |   height: 12rem; | ||||||
| } | } | ||||||
|  | @ -1104,10 +1108,6 @@ video { | ||||||
|   height: 10rem; |   height: 10rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .h-10 { |  | ||||||
|   height: 2.5rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .h-80 { | .h-80 { | ||||||
|   height: 20rem; |   height: 20rem; | ||||||
| } | } | ||||||
|  | @ -1709,11 +1709,6 @@ video { | ||||||
|   padding-right: 0.5rem; |   padding-right: 0.5rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .py-2 { |  | ||||||
|   padding-top: 0.5rem; |  | ||||||
|   padding-bottom: 0.5rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .pl-1 { | .pl-1 { | ||||||
|   padding-left: 0.25rem; |   padding-left: 0.25rem; | ||||||
| } | } | ||||||
|  | @ -2209,6 +2204,11 @@ input[type=text] { | ||||||
|   border-radius: 0.5rem; |   border-radius: 0.5rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .border-region { | ||||||
|  |   border: 2px dashed var(--interactive-background); | ||||||
|  |   border-radius: 0.5rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /******************* Styling of input elements **********************/ | /******************* Styling of input elements **********************/ | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  |  | ||||||
|  | @ -985,17 +985,7 @@ export default class PlantNet { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface PlantNetResult { | export interface PlantNetSpeciesMatch { | ||||||
|     query: { |  | ||||||
|         project: string |  | ||||||
|         images: string[] |  | ||||||
|         organs: string[] |  | ||||||
|         includeRelatedImages: boolean |  | ||||||
|     } |  | ||||||
|     language: string |  | ||||||
|     preferedReferential: string |  | ||||||
|     bestMatch: string |  | ||||||
|     results: { |  | ||||||
|     score: number |     score: number | ||||||
|     gbif: { id: string /*Actually a number*/ } |     gbif: { id: string /*Actually a number*/ } | ||||||
|     species: { |     species: { | ||||||
|  | @ -1014,7 +1004,19 @@ export interface PlantNetResult { | ||||||
|         commonNames: string[] |         commonNames: string[] | ||||||
|         scientificName: string |         scientificName: string | ||||||
|     } |     } | ||||||
|     }[] | } | ||||||
|  | 
 | ||||||
|  | export interface PlantNetResult { | ||||||
|  |     query: { | ||||||
|  |         project: string | ||||||
|  |         images: string[] | ||||||
|  |         organs: string[] | ||||||
|  |         includeRelatedImages: boolean | ||||||
|  |     } | ||||||
|  |     language: string | ||||||
|  |     preferedReferential: string | ||||||
|  |     bestMatch: string | ||||||
|  |     results: PlantNetSpeciesMatch[] | ||||||
|     version: string |     version: string | ||||||
|     remainingIdentificationRequests: number |     remainingIdentificationRequests: number | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -10,12 +10,13 @@ | ||||||
| 
 | 
 | ||||||
|   const dispatch = createEventDispatcher<{ click }>() |   const dispatch = createEventDispatcher<{ click }>() | ||||||
|   export let clss: string | undefined = undefined |   export let clss: string | undefined = undefined | ||||||
|  |   export let imageClass: string | undefined = undefined | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <SubtleButton | <SubtleButton | ||||||
|   on:click={() => dispatch("click")} |   on:click={() => dispatch("click")} | ||||||
|   options={{ extraClasses: twMerge("flex items-center", clss) }} |   options={{ extraClasses: twMerge("flex items-center", clss) }} | ||||||
| > | > | ||||||
|   <ChevronLeftIcon class="h-12 w-12" slot="image" /> |   <ChevronLeftIcon class={imageClass ?? "h-12 w-12"} slot="image" /> | ||||||
|   <slot slot="message" /> |   <slot slot="message" /> | ||||||
| </SubtleButton> | </SubtleButton> | ||||||
|  |  | ||||||
|  | @ -20,6 +20,6 @@ | ||||||
|   <slot name="image" slot="image" /> |   <slot name="image" slot="image" /> | ||||||
|   <div class="flex w-full items-center justify-between" slot="message"> |   <div class="flex w-full items-center justify-between" slot="message"> | ||||||
|     <slot /> |     <slot /> | ||||||
|     <ChevronRightIcon class="h-12 w-12" /> |     <ChevronRightIcon class="h-12 w-12 shrink-0" /> | ||||||
|   </div> |   </div> | ||||||
| </SubtleButton> | </SubtleButton> | ||||||
|  |  | ||||||
|  | @ -1,127 +0,0 @@ | ||||||
| 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 Translations from "../i18n/Translations" |  | ||||||
| import List from "../Base/List" |  | ||||||
| import Svg from "../../Svg" |  | ||||||
| 
 |  | ||||||
| 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 UIEventSource.FromPromiseWithErr(PlantNet.query(images.slice(0, 5))) |  | ||||||
|                 }) |  | ||||||
|                 .map((result) => { |  | ||||||
|                     if (images.data.length === 0) { |  | ||||||
|                         return new Combine([ |  | ||||||
|                             t.takeImages, |  | ||||||
|                             t.howTo.intro, |  | ||||||
|                             new List([t.howTo.li0, t.howTo.li1, t.howTo.li2, t.howTo.li3]), |  | ||||||
|                         ]).SetClass("flex flex-col") |  | ||||||
|                     } |  | ||||||
|                     if (result === undefined) { |  | ||||||
|                         return new Loading(t.querying.Subs(images.data)) |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     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 |  | ||||||
|                             } |  | ||||||
|                             return new Combine([ |  | ||||||
|                                 new Button( |  | ||||||
|                                     new Combine([ |  | ||||||
|                                         Svg.back_svg().SetClass( |  | ||||||
|                                             "w-6 mr-1 bg-white rounded-full p-1" |  | ||||||
|                                         ), |  | ||||||
|                                         t.back, |  | ||||||
|                                     ]).SetClass("flex"), |  | ||||||
|                                     () => { |  | ||||||
|                                         selectedSpecies.setData(undefined) |  | ||||||
|                                     } |  | ||||||
|                                 ).SetClass("btn btn-secondary"), |  | ||||||
| 
 |  | ||||||
|                                 new Button( |  | ||||||
|                                     new Combine([ |  | ||||||
|                                         Svg.confirm_svg().SetClass("w-6 mr-1"), |  | ||||||
|                                         t.confirm, |  | ||||||
|                                     ]).SetClass("flex"), |  | ||||||
|                                     () => { |  | ||||||
|                                         onConfirm(wikidataSpecies) |  | ||||||
|                                     } |  | ||||||
|                                 ).SetClass("btn"), |  | ||||||
|                             ]).SetClass("flex justify-between") |  | ||||||
|                         }) |  | ||||||
|                     ) |  | ||||||
|                 }) |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										123
									
								
								src/UI/PlantNet/PlantNet.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								src/UI/PlantNet/PlantNet.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,123 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import Translations from "../i18n/Translations"; | ||||||
|  |   import Tr from "../Base/Tr.svelte"; | ||||||
|  |   import PlantNetSpeciesList from "./PlantNetSpeciesList.svelte"; | ||||||
|  |   import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"; | ||||||
|  |   import type { PlantNetSpeciesMatch } from "../../Logic/Web/PlantNet"; | ||||||
|  |   import PlantNet from "../../Logic/Web/PlantNet"; | ||||||
|  |   import { XCircleIcon } from "@babeard/svelte-heroicons/solid"; | ||||||
|  |   import BackButton from "../Base/BackButton.svelte"; | ||||||
|  |   import NextButton from "../Base/NextButton.svelte"; | ||||||
|  |   import WikipediaPanel from "../Wikipedia/WikipediaPanel.svelte"; | ||||||
|  |   import { createEventDispatcher } from "svelte"; | ||||||
|  |   import ToSvelte from "../Base/ToSvelte.svelte"; | ||||||
|  |   import Svg from "../../Svg"; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * The main entry point for the plantnet wizard | ||||||
|  |    */ | ||||||
|  |   const t = Translations.t.plantDetection; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * All the URLs pointing to images of the selected feature. | ||||||
|  |    * We need to feed them into Plantnet when applicable | ||||||
|  |    */ | ||||||
|  |   export let imageUrls: Store<string[]>; | ||||||
|  |   export let onConfirm: (wikidataId: string) => void; | ||||||
|  |   const dispatch = createEventDispatcher<{ selected: string }>(); | ||||||
|  |   let collapsedMode = true; | ||||||
|  |   let options: UIEventSource<PlantNetSpeciesMatch[]> = new UIEventSource<PlantNetSpeciesMatch[]>(undefined); | ||||||
|  | 
 | ||||||
|  |   let error: string = undefined; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * The Wikidata-id of the species to apply | ||||||
|  |    */ | ||||||
|  |   let selectedOption: string; | ||||||
|  | 
 | ||||||
|  |   let done = false; | ||||||
|  | 
 | ||||||
|  |   function speciesSelected(species: PlantNetSpeciesMatch) { | ||||||
|  |     console.log("Selected:", species); | ||||||
|  |     selectedOption = species; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async function detectSpecies() { | ||||||
|  |     collapsedMode = false; | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  | 
 | ||||||
|  |       const result = await PlantNet.query(imageUrls.data.slice(0, 5)); | ||||||
|  |       options.set(result.results.filter(r => r.score > 0.005).slice(0, 8)); | ||||||
|  |     } catch (e) { | ||||||
|  |       error = e; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <div class="flex flex-col"> | ||||||
|  | 
 | ||||||
|  |   {#if collapsedMode} | ||||||
|  |     <button class="w-full" on:click={detectSpecies}> | ||||||
|  |       <Tr t={t.button} /> | ||||||
|  |     </button> | ||||||
|  |   {:else if $error !== undefined} | ||||||
|  |     <Tr cls="alert" t={t.error.Subs({error})} /> | ||||||
|  |   {:else if $imageUrls.length === 0} | ||||||
|  |     <!-- No urls are available, show the explanation instead--> | ||||||
|  |     <div class=" border-region p-2 mb-1 relative"> | ||||||
|  |       <XCircleIcon class="absolute top-0 right-0 w-8 h-8 m-4 cursor-pointer" | ||||||
|  |                    on:click={() => {collapsedMode = true}}></XCircleIcon> | ||||||
|  |       <Tr t={t.takeImages} /> | ||||||
|  |       <Tr t={ t.howTo.intro} /> | ||||||
|  |       <ul> | ||||||
|  |         <li> | ||||||
|  |           <Tr t={t.howTo.li0} /> | ||||||
|  |         </li> | ||||||
|  |         <li> | ||||||
|  |           <Tr t={t.howTo.li1} /> | ||||||
|  |         </li> | ||||||
|  |         <li> | ||||||
|  |           <Tr t={t.howTo.li2} /> | ||||||
|  |         </li> | ||||||
|  |         <li> | ||||||
|  |           <Tr t={t.howTo.li3} /> | ||||||
|  |         </li> | ||||||
|  |       </ul> | ||||||
|  |     </div> | ||||||
|  |   {:else if selectedOption === undefined} | ||||||
|  |     <PlantNetSpeciesList {options} numberOfImages={$imageUrls.length} | ||||||
|  |                          on:selected={(species) => speciesSelected(species.detail)}> | ||||||
|  |       <XCircleIcon slot="upper-right" class="w-8 h-8 m-4 cursor-pointer" | ||||||
|  |                    on:click={() => {collapsedMode = true}}></XCircleIcon> | ||||||
|  | 
 | ||||||
|  |     </PlantNetSpeciesList> | ||||||
|  |   {:else if !done} | ||||||
|  |     <div class="flex flex-col border-interactive"> | ||||||
|  |       <div class="m-2"> | ||||||
|  | 
 | ||||||
|  |         <WikipediaPanel wikiIds={new ImmutableStore([selectedOption])} /> | ||||||
|  |       </div> | ||||||
|  |       <div class="flex justify-between"> | ||||||
|  |         <BackButton on:click={() => {selectedOption = undefined}}> | ||||||
|  |           <Tr t={t.back} /> | ||||||
|  |         </BackButton> | ||||||
|  |         <NextButton clss="primary shrink-0" on:click={() => { done = true; onConfirm(selectedOption); }} > | ||||||
|  |           <Tr t={t.confirm} /> | ||||||
|  |         </NextButton> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   {:else} | ||||||
|  |     <!-- done ! --> | ||||||
|  |     <Tr t={t.done} cls="thanks w-full" /> | ||||||
|  |     <BackButton imageClass="w-6 h-6 shrink-0" clss="p-1 m-0" on:click={() => {done = false; selectedOption = undefined}}> | ||||||
|  |       <Tr t={t.tryAgain} /> | ||||||
|  |     </BackButton> | ||||||
|  |   {/if} | ||||||
|  |   <div class="flex p-2 bg-gray-200 rounded-xl self-end"> | ||||||
|  |     <ToSvelte construct={Svg.plantnet_logo_svg().SetClass("w-10 h-10 pr-1 mr-1 bg-white rounded-full")} /> | ||||||
|  |     <Tr t={t.poweredByPlantnet} /> | ||||||
|  |   </div> | ||||||
|  | 
 | ||||||
|  | </div> | ||||||
							
								
								
									
										37
									
								
								src/UI/PlantNet/PlantNetSpeciesList.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/UI/PlantNet/PlantNetSpeciesList.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,37 @@ | ||||||
|  | <script lang="ts">/** | ||||||
|  |  * Show the list of options to choose from | ||||||
|  |  */ | ||||||
|  | import type { PlantNetSpeciesMatch } from "../../Logic/Web/PlantNet"; | ||||||
|  | import { Store } from "../../Logic/UIEventSource"; | ||||||
|  | import Translations from "../i18n/Translations"; | ||||||
|  | import Tr from "../Base/Tr.svelte"; | ||||||
|  | import Loading from "../Base/Loading.svelte"; | ||||||
|  | import SpeciesButton from "./SpeciesButton.svelte"; | ||||||
|  | 
 | ||||||
|  | const t = Translations.t.plantDetection; | ||||||
|  | 
 | ||||||
|  | export let options: Store<PlantNetSpeciesMatch[]>; | ||||||
|  | export let numberOfImages: number; | ||||||
|  | 
 | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | {#if $options === undefined} | ||||||
|  |   <Loading> | ||||||
|  |     <Tr t={t.querying.Subs({length: numberOfImages})} /> | ||||||
|  |   </Loading> | ||||||
|  | {:else} | ||||||
|  |   <div class="low-interaction border-interactive flex p-2 flex-col relative"> | ||||||
|  |     <div class="absolute top-0 right-0" > | ||||||
|  |        | ||||||
|  |     <slot name="upper-right"/> | ||||||
|  |     </div> | ||||||
|  |     <h3> | ||||||
|  |       <Tr t={t.overviewTitle} /> | ||||||
|  |     </h3> | ||||||
|  |     <Tr t={t.overviewIntro} /> | ||||||
|  |     <Tr cls="font-bold" t={t.overviewVerify} /> | ||||||
|  |     {#each $options as species} | ||||||
|  |       <SpeciesButton {species} on:selected/> | ||||||
|  |       {/each} | ||||||
|  |   </div> | ||||||
|  | {/if} | ||||||
							
								
								
									
										56
									
								
								src/UI/PlantNet/SpeciesButton.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/UI/PlantNet/SpeciesButton.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,56 @@ | ||||||
|  | <script lang="ts">/** | ||||||
|  |  * A button to select a single species | ||||||
|  |  */ | ||||||
|  | import { createEventDispatcher } from "svelte"; | ||||||
|  | import type { PlantNetSpeciesMatch } from "../../Logic/Web/PlantNet"; | ||||||
|  | import { UIEventSource } from "../../Logic/UIEventSource"; | ||||||
|  | import Wikidata from "../../Logic/Web/Wikidata"; | ||||||
|  | import NextButton from "../Base/NextButton.svelte"; | ||||||
|  | import Loading from "../Base/Loading.svelte"; | ||||||
|  | import WikidataPreviewBox from "../Wikipedia/WikidataPreviewBox"; | ||||||
|  | import Tr from "../Base/Tr.svelte"; | ||||||
|  | import Translations from "../i18n/Translations"; | ||||||
|  | import ToSvelte from "../Base/ToSvelte.svelte"; | ||||||
|  | 
 | ||||||
|  | export let species: PlantNetSpeciesMatch; | ||||||
|  | let wikidata = UIEventSource.FromPromise( | ||||||
|  |   Wikidata.Sparql<{ species }>( | ||||||
|  |     ["?species", "?speciesLabel"], | ||||||
|  |     ["?species wdt:P846 \"" + species.gbif.id + "\""] | ||||||
|  |   ) | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | const dispatch = createEventDispatcher<{ selected: string /* wikidata-id*/ }>(); | ||||||
|  | const t = Translations.t.plantDetection; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * PlantNet give us a GBIF-id, but we want the Wikidata-id instead. | ||||||
|  |  * We look this up in wikidata | ||||||
|  |  */ | ||||||
|  | const wikidataId: Store<string> = UIEventSource.FromPromise( | ||||||
|  |   Wikidata.Sparql<{ species }>( | ||||||
|  |     ["?species", "?speciesLabel"], | ||||||
|  |     ["?species wdt:P846 \"" + species.gbif.id + "\""] | ||||||
|  |   ) | ||||||
|  | ).mapD(wd => wd[0]?.species?.value); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <NextButton on:click={() =>{  | ||||||
|  |   console.log("Dispatching: ", $wikidataId) | ||||||
|  |   return dispatch("selected", $wikidataId); }}> | ||||||
|  |   {#if $wikidata === undefined} | ||||||
|  |     <Loading> | ||||||
|  |       <Tr t={ t.loadingWikidata.Subs({ | ||||||
|  |         species: species.species.scientificNameWithoutAuthor, | ||||||
|  |       })} /> | ||||||
|  |     </Loading> | ||||||
|  |   {:else} | ||||||
|  |     <ToSvelte construct={() => new WikidataPreviewBox(wikidataId, | ||||||
|  |      { imageStyle: "max-width: 8rem; width: unset; height: 8rem", | ||||||
|  |      extraItems: [t.matchPercentage | ||||||
|  |   .Subs({ match: Math.round(species.score * 100) }) | ||||||
|  |   .SetClass("thanks w-fit self-center")] | ||||||
|  |      }).SetClass("w-full")}></ToSvelte> | ||||||
|  |   {/if} | ||||||
|  | </NextButton> | ||||||
|  | @ -1,18 +1,14 @@ | ||||||
| import { Store, UIEventSource } from "../../Logic/UIEventSource" | import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import Toggle from "../Input/Toggle" |  | ||||||
| import Lazy from "../Base/Lazy" |  | ||||||
| import { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider" | import { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider" | ||||||
| import PlantNetSpeciesSearch from "../BigComponents/PlantNetSpeciesSearch" | import PlantNetSpeciesSearch from "../BigComponents/PlantNetSpeciesSearch" | ||||||
| import Wikidata from "../../Logic/Web/Wikidata" | import Wikidata from "../../Logic/Web/Wikidata" | ||||||
| import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" | import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" | ||||||
| import { And } from "../../Logic/Tags/And" | import { And } from "../../Logic/Tags/And" | ||||||
| import { Tag } from "../../Logic/Tags/Tag" | import { Tag } from "../../Logic/Tags/Tag" | ||||||
| import { SubtleButton } from "../Base/SubtleButton" |  | ||||||
| import Combine from "../Base/Combine" |  | ||||||
| import Svg from "../../Svg" |  | ||||||
| import Translations from "../i18n/Translations" |  | ||||||
| import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders" | import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders" | ||||||
| import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|  | import SvelteUIElement from "../Base/SvelteUIElement" | ||||||
|  | import PlantNet from "../PlantNet/PlantNet.svelte" | ||||||
| 
 | 
 | ||||||
| export class PlantNetDetectionViz implements SpecialVisualization { | export class PlantNetDetectionViz implements SpecialVisualization { | ||||||
|     funcName = "plantnet_detection" |     funcName = "plantnet_detection" | ||||||
|  | @ -37,17 +33,13 @@ export class PlantNetDetectionViz implements SpecialVisualization { | ||||||
|             imagePrefixes = [].concat(...args.map((a) => a.split(","))) |             imagePrefixes = [].concat(...args.map((a) => a.split(","))) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const detect = new UIEventSource(false) |  | ||||||
|         const toggle = new Toggle( |  | ||||||
|             new Lazy(() => { |  | ||||||
|         const allProvidedImages: Store<ProvidedImage[]> = AllImageProviders.LoadImagesFor( |         const allProvidedImages: Store<ProvidedImage[]> = AllImageProviders.LoadImagesFor( | ||||||
|             tags, |             tags, | ||||||
|             imagePrefixes |             imagePrefixes | ||||||
|         ) |         ) | ||||||
|                 const allImages: Store<string[]> = allProvidedImages.map((pi) => |         const imageUrls: Store<string[]> = allProvidedImages.map((pi) => pi.map((pi) => pi.url)) | ||||||
|                     pi.map((pi) => pi.url) | 
 | ||||||
|                 ) |         async function applySpecies(selectedWikidata) { | ||||||
|                 return new PlantNetSpeciesSearch(allImages, async (selectedWikidata) => { |  | ||||||
|             selectedWikidata = Wikidata.ExtractKey(selectedWikidata) |             selectedWikidata = Wikidata.ExtractKey(selectedWikidata) | ||||||
|             const change = new ChangeTagAction( |             const change = new ChangeTagAction( | ||||||
|                 tags.data.id, |                 tags.data.id, | ||||||
|  | @ -62,20 +54,8 @@ export class PlantNetDetectionViz implements SpecialVisualization { | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
|             await state.changes.applyAction(change) |             await state.changes.applyAction(change) | ||||||
|                 }) |         } | ||||||
|             }), |  | ||||||
|             new SubtleButton(undefined, "Detect plant species with plantnet.org").onClick(() => |  | ||||||
|                 detect.setData(true) |  | ||||||
|             ), |  | ||||||
|             detect |  | ||||||
|         ) |  | ||||||
| 
 | 
 | ||||||
|         return new Combine([ |         return new SvelteUIElement(PlantNet, { imageUrls, onConfirm: applySpecies }) | ||||||
|             toggle, |  | ||||||
|             new Combine([ |  | ||||||
|                 Svg.plantnet_logo_svg().SetClass("w-10 h-10 p-1 mr-1 bg-white rounded-full"), |  | ||||||
|                 Translations.t.plantDetection.poweredByPlantnet, |  | ||||||
|             ]).SetClass("flex p-2 bg-gray-200 rounded-xl self-end"), |  | ||||||
|         ]).SetClass("flex flex-col") |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -538,7 +538,7 @@ export default class SpecialVisualizations { | ||||||
|                     const keys = args[0].split(";").map((k) => k.trim()) |                     const keys = args[0].split(";").map((k) => k.trim()) | ||||||
|                     const wikiIds: Store<string[]> = tagsSource.map((tags) => { |                     const wikiIds: Store<string[]> = tagsSource.map((tags) => { | ||||||
|                         const key = keys.find((k) => tags[k] !== undefined && tags[k] !== "") |                         const key = keys.find((k) => tags[k] !== undefined && tags[k] !== "") | ||||||
|                         return tags[key]?.split(";")?.map((id) => id.trim()) |                         return tags[key]?.split(";")?.map((id) => id.trim()) ?? [] | ||||||
|                     }) |                     }) | ||||||
|                     return new SvelteUIElement(WikipediaPanel, { |                     return new SvelteUIElement(WikipediaPanel, { | ||||||
|                         wikiIds, |                         wikiIds, | ||||||
|  |  | ||||||
|  | @ -29,6 +29,10 @@ | ||||||
|       areas, where some buttons might appear. |       areas, where some buttons might appear. | ||||||
|     </p> |     </p> | ||||||
| 
 | 
 | ||||||
|  |     <div class="border-interactive interactive"> | ||||||
|  |       Highly interactive area (mostly: active question) | ||||||
|  |     </div> | ||||||
|  |      | ||||||
|     <div class="flex"> |     <div class="flex"> | ||||||
|       <button class="primary"> |       <button class="primary"> | ||||||
|         <ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")} /> |         <ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")} /> | ||||||
|  |  | ||||||
|  | @ -126,7 +126,7 @@ export default class WikidataPreviewBox extends VariableUiElement { | ||||||
|             new Combine([ |             new Combine([ | ||||||
|                 Translation.fromMap(wikidata.labels)?.SetClass("font-bold"), |                 Translation.fromMap(wikidata.labels)?.SetClass("font-bold"), | ||||||
|                 link, |                 link, | ||||||
|             ]).SetClass("flex justify-between"), |             ]).SetClass("flex justify-between flex-wrap-reverse"), | ||||||
|             Translation.fromMap(wikidata.descriptions), |             Translation.fromMap(wikidata.descriptions), | ||||||
|             WikidataPreviewBox.QuickFacts(wikidata, options), |             WikidataPreviewBox.QuickFacts(wikidata, options), | ||||||
|             ...(options?.extraItems ?? []), |             ...(options?.extraItems ?? []), | ||||||
|  |  | ||||||
|  | @ -131,7 +131,7 @@ Another example is to search for species and trees: | ||||||
|         const searchResult: Store<{ success?: WikidataResponse[]; error?: any }> = searchField |         const searchResult: Store<{ success?: WikidataResponse[]; error?: any }> = searchField | ||||||
|             .GetValue() |             .GetValue() | ||||||
|             .bind((searchText) => { |             .bind((searchText) => { | ||||||
|                 if (searchText.length < 3) { |                 if (searchText.length < 3 && !searchText.match(/[qQ][0-9]+/)) { | ||||||
|                     return tooShort |                     return tooShort | ||||||
|                 } |                 } | ||||||
|                 const lang = Locale.language.data |                 const lang = Locale.language.data | ||||||
|  |  | ||||||
|  | @ -11,7 +11,7 @@ | ||||||
|   import Translations from "../i18n/Translations"; |   import Translations from "../i18n/Translations"; | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Small helper |    * Shows a wikipedia-article + wikidata preview for the given item | ||||||
|    */ |    */ | ||||||
|   export let wikipediaDetails: Store<FullWikipediaDetails>; |   export let wikipediaDetails: Store<FullWikipediaDetails>; | ||||||
| </script> | </script> | ||||||
|  | @ -23,9 +23,11 @@ | ||||||
|     <Tr t={Translations.t.general.wikipedia.fromWikipedia} /> |     <Tr t={Translations.t.general.wikipedia.fromWikipedia} /> | ||||||
|   </a> |   </a> | ||||||
| {/if} | {/if} | ||||||
|  | 
 | ||||||
| {#if $wikipediaDetails.wikidata} | {#if $wikipediaDetails.wikidata} | ||||||
|   <ToSvelte construct={WikidataPreviewBox.WikidataResponsePreview($wikipediaDetails.wikidata)} /> |   <ToSvelte construct={WikidataPreviewBox.WikidataResponsePreview($wikipediaDetails.wikidata)} /> | ||||||
| {/if} | {/if} | ||||||
|  | 
 | ||||||
| {#if $wikipediaDetails.articleUrl} | {#if $wikipediaDetails.articleUrl} | ||||||
| 
 | 
 | ||||||
|   {#if $wikipediaDetails.firstParagraph === "" || $wikipediaDetails.firstParagraph === undefined} |   {#if $wikipediaDetails.firstParagraph === "" || $wikipediaDetails.firstParagraph === undefined} | ||||||
|  | @ -42,7 +44,7 @@ | ||||||
|             style={(open ? "transform: rotate(90deg); " : "") + |             style={(open ? "transform: rotate(90deg); " : "") + | ||||||
|               "  transition: all .25s linear; width: 1.5rem; height: 1.5rem"} |               "  transition: all .25s linear; width: 1.5rem; height: 1.5rem"} | ||||||
|           /> |           /> | ||||||
|           Read the rest of the article |           <Tr t={Translations.t.general.wikipedia.readMore}/> | ||||||
|         </span> |         </span> | ||||||
|       </DisclosureButton> |       </DisclosureButton> | ||||||
|       <DisclosurePanel> |       <DisclosurePanel> | ||||||
|  |  | ||||||
|  | @ -16,7 +16,7 @@ | ||||||
|    */ |    */ | ||||||
|   export let wikiIds: Store<string[]>; |   export let wikiIds: Store<string[]>; | ||||||
|   let wikipediaStores: Store<Store<FullWikipediaDetails>[]> = Locale.language.bind((language) => |   let wikipediaStores: Store<Store<FullWikipediaDetails>[]> = Locale.language.bind((language) => | ||||||
|     wikiIds.map((wikiIds) => wikiIds.map((id) => Wikipedia.fetchArticleAndWikidata(id, language))) |     wikiIds?.map((wikiIds) => wikiIds?.map((id) => Wikipedia.fetchArticleAndWikidata(id, language))) | ||||||
|   ); |   ); | ||||||
|   let _wikipediaStores; |   let _wikipediaStores; | ||||||
|   onDestroy( |   onDestroy( | ||||||
|  |  | ||||||
|  | @ -154,6 +154,11 @@ input[type=text] { | ||||||
|     border-radius: 0.5rem; |     border-radius: 0.5rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .border-region { | ||||||
|  |     border: 2px dashed var(--interactive-background); | ||||||
|  |     border-radius: 0.5rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| /******************* Styling of input elements **********************/ | /******************* Styling of input elements **********************/ | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue