diff --git a/Logic/Osm/Actions/ChangeTagAction.ts b/Logic/Osm/Actions/ChangeTagAction.ts index c4a08f34f0..199eab28da 100644 --- a/Logic/Osm/Actions/ChangeTagAction.ts +++ b/Logic/Osm/Actions/ChangeTagAction.ts @@ -2,15 +2,17 @@ import OsmChangeAction from "./OsmChangeAction"; import {Changes} from "../Changes"; import {ChangeDescription} from "./ChangeDescription"; import {TagsFilter} from "../../Tags/TagsFilter"; +import {OsmTags} from "../../../Models/OsmFeature"; export default class ChangeTagAction extends OsmChangeAction { private readonly _elementId: string; private readonly _tagsFilter: TagsFilter; - private readonly _currentTags: any; + private readonly _currentTags: Record | OsmTags; private readonly _meta: { theme: string, changeType: string }; constructor(elementId: string, - tagsFilter: TagsFilter, currentTags: any, meta: { + tagsFilter: TagsFilter, + currentTags: Record, meta: { theme: string, changeType: "answer" | "soft-delete" | "add-image" | string }) { diff --git a/UI/Base/Button.ts b/UI/Base/Button.ts index 8d4e0dc1e1..aa5fc299ef 100644 --- a/UI/Base/Button.ts +++ b/UI/Base/Button.ts @@ -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)) { 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; } diff --git a/UI/BaseUIElement.ts b/UI/BaseUIElement.ts index 4c084d0884..a9e0765609 100644 --- a/UI/BaseUIElement.ts +++ b/UI/BaseUIElement.ts @@ -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 = new Set(); private style: string; - private _onClick: () => void; + private _onClick: () => void | Promise; 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; } diff --git a/UI/BigComponents/PlantNetSpeciesSearch.ts b/UI/BigComponents/PlantNetSpeciesSearch.ts new file mode 100644 index 0000000000..f1b2ba434d --- /dev/null +++ b/UI/BigComponents/PlantNetSpeciesSearch.ts @@ -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, onConfirm: (wikidataUrl: string) => Promise) { + 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(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 + } + 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") + })) + + } + )) + } + +} \ No newline at end of file diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 062563118f..3cccea1f06 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -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 image is given, the first picture URL will be added as image, the second as image:0, the third as image:1, 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 = 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.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 + ) + } } ] diff --git a/UI/Wikipedia/WikidataPreviewBox.ts b/UI/Wikipedia/WikidataPreviewBox.ts index 2a15fcaf4c..2af1f230da 100644 --- a/UI/Wikipedia/WikidataPreviewBox.ts +++ b/UI/Wikipedia/WikidataPreviewBox.ts @@ -57,7 +57,11 @@ export default class WikidataPreviewBox extends VariableUiElement { } ] - constructor(wikidataId: Store, options?: {noImages?: boolean, whileLoading?: BaseUIElement | string, extraItems?: (BaseUIElement | string)[]}) { + constructor(wikidataId: Store, 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") } diff --git a/assets/layers/tree_node/tree_node.json b/assets/layers/tree_node/tree_node.json index dc0074d174..5ad4d91242 100644 --- a/assets/layers/tree_node/tree_node.json +++ b/assets/layers/tree_node/tree_node.json @@ -104,6 +104,11 @@ } ] }, + { + "id": "plantnet", + "render": "{plantnet_detection()}", + "condition": "species:wikidata=" + }, { "id": "tree-species-wikidata", "question": { diff --git a/css/index-tailwind-output.css b/css/index-tailwind-output.css index 5b3eb4abf2..4983438de4 100644 --- a/css/index-tailwind-output.css +++ b/css/index-tailwind-output.css @@ -858,6 +858,10 @@ video { margin-bottom: 0.75rem; } +.mb-2 { + margin-bottom: 0.5rem; +} + .ml-3 { margin-left: 0.75rem; } @@ -866,14 +870,6 @@ video { margin-bottom: 1rem; } -.mt-8 { - margin-top: 2rem; -} - -.mt-4 { - margin-top: 1rem; -} - .mt-2 { margin-top: 0.5rem; } @@ -886,6 +882,10 @@ video { margin-right: 2rem; } +.mt-4 { + margin-top: 1rem; +} + .mt-6 { margin-top: 1.5rem; } @@ -910,10 +910,6 @@ video { margin-right: 1rem; } -.mb-2 { - margin-bottom: 0.5rem; -} - .ml-2 { margin-left: 0.5rem; } @@ -934,6 +930,10 @@ video { margin-top: 0px; } +.mt-8 { + margin-top: 2rem; +} + .mb-8 { margin-bottom: 2rem; } @@ -1054,6 +1054,10 @@ video { height: 6rem; } +.h-96 { + height: 24rem; +} + .h-64 { height: 16rem; } @@ -1162,6 +1166,10 @@ video { width: 2rem; } +.w-1\/3 { + width: 33.333333%; +} + .w-4 { width: 1rem; } @@ -1407,6 +1415,10 @@ video { border-radius: 9999px; } +.rounded-xl { + border-radius: 0.75rem; +} + .rounded-3xl { border-radius: 1.5rem; } @@ -1423,10 +1435,6 @@ video { border-radius: 0.5rem; } -.rounded-xl { - border-radius: 0.75rem; -} - .rounded-sm { border-radius: 0.125rem; } @@ -1436,14 +1444,14 @@ video { border-bottom-left-radius: 0.25rem; } -.border { - border-width: 1px; -} - .border-2 { border-width: 2px; } +.border { + border-width: 1px; +} + .border-4 { border-width: 4px; } @@ -2866,10 +2874,6 @@ input { width: 75%; } - .lg\:w-1\/3 { - width: 33.333333%; - } - .lg\:w-1\/4 { width: 25%; } @@ -2878,6 +2882,10 @@ input { width: 16.666667%; } + .lg\:w-1\/3 { + width: 33.333333%; + } + .lg\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } diff --git a/langs/en.json b/langs/en.json index f359f52a9e..88b6948085 100644 --- a/langs/en.json +++ b/langs/en.json @@ -685,6 +685,17 @@ "typeText": "Type some text to add a comment", "warnAnonymous": "You are not logged in. We won't be able to contact you to resolve your issue." }, + "plantDetection": { + "error": "Something went wrong while detecting the tree species: {error}", + "loadingWikidata": "Loading information about {species}", + "matchPercentage": "{match}% match", + "overviewIntro": "The AI on PlantNet.org thinks the images show the species below.", + "overviewTitle": "Automatically detected species", + "overviewVerify": "Please verify that correct species and link it to the tree", + "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" + }, "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:
  • The changes you made
  • Your username
  • When this change is made
  • The theme you used while making the change
  • The language of the user interface
  • 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
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.", "editingTitle": "When making changes",