From 5f04a695172934d4052ba7ee90ae378e66893d6a Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 20 Sep 2023 01:47:32 +0200 Subject: [PATCH] Refactoring: port PlantNet-detection to svelte, re-integrate wikipedia component --- langs/en.json | 6 +- package.json | 2 +- public/css/index-tailwind-output.css | 18 +-- src/Logic/Web/PlantNet.ts | 42 +++--- src/UI/Base/BackButton.svelte | 3 +- src/UI/Base/NextButton.svelte | 2 +- src/UI/BigComponents/PlantNetSpeciesSearch.ts | 127 ------------------ src/UI/PlantNet/PlantNet.svelte | 123 +++++++++++++++++ src/UI/PlantNet/PlantNetSpeciesList.svelte | 37 +++++ src/UI/PlantNet/SpeciesButton.svelte | 56 ++++++++ src/UI/Popup/PlantNetDetectionViz.ts | 68 ++++------ src/UI/SpecialVisualizations.ts | 2 +- src/UI/StylesheetTestGui.svelte | 4 + src/UI/Wikipedia/WikidataPreviewBox.ts | 2 +- src/UI/Wikipedia/WikidataSearchBox.ts | 2 +- src/UI/Wikipedia/WikipediaArticle.svelte | 6 +- src/UI/Wikipedia/WikipediaPanel.svelte | 2 +- src/index.css | 5 + 18 files changed, 297 insertions(+), 210 deletions(-) delete mode 100644 src/UI/BigComponents/PlantNetSpeciesSearch.ts create mode 100644 src/UI/PlantNet/PlantNet.svelte create mode 100644 src/UI/PlantNet/PlantNetSpeciesList.svelte create mode 100644 src/UI/PlantNet/SpeciesButton.svelte diff --git a/langs/en.json b/langs/en.json index cc9974402..7d75d0a7b 100644 --- a/langs/en.json +++ b/langs/en.json @@ -380,6 +380,7 @@ "born": "Born: {value}", "died": "Died: {value}" }, + "readMore": "Read the rest of the article", "searchToShort": "Your search query is too short, enter a longer text", "searchWikidata": "Search on Wikidata", "wikipediaboxTitle": "Wikipedia" @@ -498,7 +499,9 @@ }, "plantDetection": { "back": "Back to species overview", + "button": "Automatically detect the plant species using the AI of Plantnet.org", "confirm": "Select species", + "done": "The species has been applied", "error": "Something went wrong while detecting the tree species: {error}", "howTo": { "intro": "For optimal results,", @@ -515,7 +518,8 @@ "poweredByPlantnet": "Powered by plantnet.org", "querying": "Querying plantnet.org with {length} images", "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": { "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: Please refer to the privacy policy on OpenStreetMap.org for detailed information. We'd like to remind you that you can use a fictional name when signing up.", diff --git a/package.json b/package.json index fff429293..534568b5d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapcomplete", - "version": "0.32.0", + "version": "0.33.0", "repository": "https://github.com/pietervdvn/MapComplete", "description": "A small website to edit OSM easily", "bugs": "https://github.com/pietervdvn/MapComplete/issues", diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index 91a0cd7e0..e068f5f49 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -1096,6 +1096,10 @@ video { height: 2.75rem; } +.h-10 { + height: 2.5rem; +} + .h-48 { height: 12rem; } @@ -1104,10 +1108,6 @@ video { height: 10rem; } -.h-10 { - height: 2.5rem; -} - .h-80 { height: 20rem; } @@ -1709,11 +1709,6 @@ video { padding-right: 0.5rem; } -.py-2 { - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} - .pl-1 { padding-left: 0.25rem; } @@ -2209,6 +2204,11 @@ input[type=text] { border-radius: 0.5rem; } +.border-region { + border: 2px dashed var(--interactive-background); + border-radius: 0.5rem; +} + /******************* Styling of input elements **********************/ /** diff --git a/src/Logic/Web/PlantNet.ts b/src/Logic/Web/PlantNet.ts index d22cae763..dab705ad0 100644 --- a/src/Logic/Web/PlantNet.ts +++ b/src/Logic/Web/PlantNet.ts @@ -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 { query: { project: string @@ -995,26 +1016,7 @@ export interface PlantNetResult { language: string preferedReferential: string bestMatch: string - results: { - 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 - } - }[] + results: PlantNetSpeciesMatch[] version: string remainingIdentificationRequests: number } diff --git a/src/UI/Base/BackButton.svelte b/src/UI/Base/BackButton.svelte index 40f4d1eea..5f48bf7e6 100644 --- a/src/UI/Base/BackButton.svelte +++ b/src/UI/Base/BackButton.svelte @@ -10,12 +10,13 @@ const dispatch = createEventDispatcher<{ click }>() export let clss: string | undefined = undefined + export let imageClass: string | undefined = undefined dispatch("click")} options={{ extraClasses: twMerge("flex items-center", clss) }} > - + diff --git a/src/UI/Base/NextButton.svelte b/src/UI/Base/NextButton.svelte index a546fd8c1..6b4a64dd8 100644 --- a/src/UI/Base/NextButton.svelte +++ b/src/UI/Base/NextButton.svelte @@ -20,6 +20,6 @@
- +
diff --git a/src/UI/BigComponents/PlantNetSpeciesSearch.ts b/src/UI/BigComponents/PlantNetSpeciesSearch.ts deleted file mode 100644 index b7e503f6e..000000000 --- a/src/UI/BigComponents/PlantNetSpeciesSearch.ts +++ /dev/null @@ -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, onConfirm: (wikidataUrl: string) => Promise) { - 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(result).SetClass("alert") - } - console.log(result) - const success = result["success"] - - const selectedSpecies = new UIEventSource(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") - }) - ) - }) - ) - } -} diff --git a/src/UI/PlantNet/PlantNet.svelte b/src/UI/PlantNet/PlantNet.svelte new file mode 100644 index 000000000..170c7fc18 --- /dev/null +++ b/src/UI/PlantNet/PlantNet.svelte @@ -0,0 +1,123 @@ + + +
+ + {#if collapsedMode} + + {:else if $error !== undefined} + + {:else if $imageUrls.length === 0} + +
+ {collapsedMode = true}}> + + +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+ {:else if selectedOption === undefined} + speciesSelected(species.detail)}> + {collapsedMode = true}}> + + + {:else if !done} +
+
+ + +
+
+ {selectedOption = undefined}}> + + + { done = true; onConfirm(selectedOption); }} > + + +
+
+ {:else} + + + {done = false; selectedOption = undefined}}> + + + {/if} +
+ + +
+ +
diff --git a/src/UI/PlantNet/PlantNetSpeciesList.svelte b/src/UI/PlantNet/PlantNetSpeciesList.svelte new file mode 100644 index 000000000..5cb734156 --- /dev/null +++ b/src/UI/PlantNet/PlantNetSpeciesList.svelte @@ -0,0 +1,37 @@ + + +{#if $options === undefined} + + + +{:else} +
+
+ + +
+

+ +

+ + + {#each $options as species} + + {/each} +
+{/if} diff --git a/src/UI/PlantNet/SpeciesButton.svelte b/src/UI/PlantNet/SpeciesButton.svelte new file mode 100644 index 000000000..de7f87333 --- /dev/null +++ b/src/UI/PlantNet/SpeciesButton.svelte @@ -0,0 +1,56 @@ + + +{ + console.log("Dispatching: ", $wikidataId) + return dispatch("selected", $wikidataId); }}> + {#if $wikidata === undefined} + + + + {:else} + 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")}> + {/if} + diff --git a/src/UI/Popup/PlantNetDetectionViz.ts b/src/UI/Popup/PlantNetDetectionViz.ts index 198fc7a93..69397cabd 100644 --- a/src/UI/Popup/PlantNetDetectionViz.ts +++ b/src/UI/Popup/PlantNetDetectionViz.ts @@ -1,18 +1,14 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource" -import Toggle from "../Input/Toggle" -import Lazy from "../Base/Lazy" import { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider" import PlantNetSpeciesSearch from "../BigComponents/PlantNetSpeciesSearch" import Wikidata from "../../Logic/Web/Wikidata" import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" import { And } from "../../Logic/Tags/And" 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 { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" +import SvelteUIElement from "../Base/SvelteUIElement" +import PlantNet from "../PlantNet/PlantNet.svelte" export class PlantNetDetectionViz implements SpecialVisualization { funcName = "plantnet_detection" @@ -37,45 +33,29 @@ export class PlantNetDetectionViz implements SpecialVisualization { imagePrefixes = [].concat(...args.map((a) => a.split(","))) } - const detect = new UIEventSource(false) - const toggle = new Toggle( - new Lazy(() => { - const allProvidedImages: Store = AllImageProviders.LoadImagesFor( - tags, - imagePrefixes - ) - const allImages: Store = 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 allProvidedImages: Store = AllImageProviders.LoadImagesFor( + tags, + imagePrefixes ) + const imageUrls: Store = allProvidedImages.map((pi) => pi.map((pi) => pi.url)) - return new Combine([ - 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") + async function applySpecies(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) + } + + return new SvelteUIElement(PlantNet, { imageUrls, onConfirm: applySpecies }) } } diff --git a/src/UI/SpecialVisualizations.ts b/src/UI/SpecialVisualizations.ts index 213451e97..26a94ec7f 100644 --- a/src/UI/SpecialVisualizations.ts +++ b/src/UI/SpecialVisualizations.ts @@ -538,7 +538,7 @@ export default class SpecialVisualizations { const keys = args[0].split(";").map((k) => k.trim()) const wikiIds: Store = tagsSource.map((tags) => { 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, { wikiIds, diff --git a/src/UI/StylesheetTestGui.svelte b/src/UI/StylesheetTestGui.svelte index 4ad143006..7d40c735f 100644 --- a/src/UI/StylesheetTestGui.svelte +++ b/src/UI/StylesheetTestGui.svelte @@ -29,6 +29,10 @@ areas, where some buttons might appear.

+
+ Highly interactive area (mostly: active question) +
+