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,6 +985,27 @@ export default class PlantNet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PlantNetSpeciesMatch {
|
||||||
|
score: number
|
||||||
|
gbif: { id: string /*Actually a number*/ }
|
||||||
|
species: {
|
||||||
|
scientificNameWithoutAuthor: string
|
||||||
|
scientificNameAuthorship: string
|
||||||
|
genus: {
|
||||||
|
scientificNameWithoutAuthor: string
|
||||||
|
scientificNameAuthorship: string
|
||||||
|
scientificName: string
|
||||||
|
}
|
||||||
|
family: {
|
||||||
|
scientificNameWithoutAuthor: string
|
||||||
|
scientificNameAuthorship: string
|
||||||
|
scientificName: string
|
||||||
|
}
|
||||||
|
commonNames: string[]
|
||||||
|
scientificName: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface PlantNetResult {
|
export interface PlantNetResult {
|
||||||
query: {
|
query: {
|
||||||
project: string
|
project: string
|
||||||
|
@ -995,26 +1016,7 @@ export interface PlantNetResult {
|
||||||
language: string
|
language: string
|
||||||
preferedReferential: string
|
preferedReferential: string
|
||||||
bestMatch: string
|
bestMatch: string
|
||||||
results: {
|
results: PlantNetSpeciesMatch[]
|
||||||
score: number
|
|
||||||
gbif: { id: string /*Actually a number*/ }
|
|
||||||
species: {
|
|
||||||
scientificNameWithoutAuthor: string
|
|
||||||
scientificNameAuthorship: string
|
|
||||||
genus: {
|
|
||||||
scientificNameWithoutAuthor: string
|
|
||||||
scientificNameAuthorship: string
|
|
||||||
scientificName: string
|
|
||||||
}
|
|
||||||
family: {
|
|
||||||
scientificNameWithoutAuthor: string
|
|
||||||
scientificNameAuthorship: string
|
|
||||||
scientificName: string
|
|
||||||
}
|
|
||||||
commonNames: string[]
|
|
||||||
scientificName: string
|
|
||||||
}
|
|
||||||
}[]
|
|
||||||
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,45 +33,29 @@ 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 allProvidedImages: Store<ProvidedImage[]> = AllImageProviders.LoadImagesFor(
|
||||||
const toggle = new Toggle(
|
tags,
|
||||||
new Lazy(() => {
|
imagePrefixes
|
||||||
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.layout.id,
|
|
||||||
changeType: "plantnet-ai-detection",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
await state.changes.applyAction(change)
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
new SubtleButton(undefined, "Detect plant species with plantnet.org").onClick(() =>
|
|
||||||
detect.setData(true)
|
|
||||||
),
|
|
||||||
detect
|
|
||||||
)
|
)
|
||||||
|
const imageUrls: Store<string[]> = allProvidedImages.map((pi) => pi.map((pi) => pi.url))
|
||||||
|
|
||||||
return new Combine([
|
async function applySpecies(selectedWikidata) {
|
||||||
toggle,
|
selectedWikidata = Wikidata.ExtractKey(selectedWikidata)
|
||||||
new Combine([
|
const change = new ChangeTagAction(
|
||||||
Svg.plantnet_logo_svg().SetClass("w-10 h-10 p-1 mr-1 bg-white rounded-full"),
|
tags.data.id,
|
||||||
Translations.t.plantDetection.poweredByPlantnet,
|
new And([
|
||||||
]).SetClass("flex p-2 bg-gray-200 rounded-xl self-end"),
|
new Tag("species:wikidata", selectedWikidata),
|
||||||
]).SetClass("flex flex-col")
|
new Tag("source:species:wikidata", "PlantNet.org AI"),
|
||||||
|
]),
|
||||||
|
tags.data,
|
||||||
|
{
|
||||||
|
theme: state.layout.id,
|
||||||
|
changeType: "plantnet-ai-detection",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await state.changes.applyAction(change)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SvelteUIElement(PlantNet, { imageUrls, onConfirm: applySpecies })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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…
Reference in a new issue