First working plantnet UI

This commit is contained in:
Pieter Vander Vennet 2022-08-22 19:16:37 +02:00
parent a8959fc934
commit 06f8cf7006
9 changed files with 216 additions and 39 deletions

View file

@ -2,15 +2,17 @@ import OsmChangeAction from "./OsmChangeAction";
import {Changes} from "../Changes"; import {Changes} from "../Changes";
import {ChangeDescription} from "./ChangeDescription"; import {ChangeDescription} from "./ChangeDescription";
import {TagsFilter} from "../../Tags/TagsFilter"; import {TagsFilter} from "../../Tags/TagsFilter";
import {OsmTags} from "../../../Models/OsmFeature";
export default class ChangeTagAction extends OsmChangeAction { export default class ChangeTagAction extends OsmChangeAction {
private readonly _elementId: string; private readonly _elementId: string;
private readonly _tagsFilter: TagsFilter; private readonly _tagsFilter: TagsFilter;
private readonly _currentTags: any; private readonly _currentTags: Record<string, string> | OsmTags;
private readonly _meta: { theme: string, changeType: string }; private readonly _meta: { theme: string, changeType: string };
constructor(elementId: string, constructor(elementId: string,
tagsFilter: TagsFilter, currentTags: any, meta: { tagsFilter: TagsFilter,
currentTags: Record<string, string>, meta: {
theme: string, theme: string,
changeType: "answer" | "soft-delete" | "add-image" | string changeType: "answer" | "soft-delete" | "add-image" | string
}) { }) {

View file

@ -3,12 +3,11 @@ import BaseUIElement from "../BaseUIElement";
export class Button extends BaseUIElement { export class Button extends BaseUIElement {
private _text: BaseUIElement; private _text: BaseUIElement;
private _onclick: () => void;
constructor(text: string | BaseUIElement, onclick: (() => void)) { constructor(text: string | BaseUIElement, onclick: (() => void | Promise<void>)) {
super(); super();
this._text = Translations.W(text); this._text = Translations.W(text);
this._onclick = onclick; this.onClick(onclick)
} }
protected InnerConstructElement(): HTMLElement { protected InnerConstructElement(): HTMLElement {
@ -20,7 +19,6 @@ export class Button extends BaseUIElement {
const button = document.createElement("button") const button = document.createElement("button")
button.type = "button" button.type = "button"
button.appendChild(el) button.appendChild(el)
button.onclick = this._onclick
form.appendChild(button) form.appendChild(button)
return form; return form;
} }

View file

@ -1,5 +1,3 @@
import { Utils } from "../Utils";
/** /**
* A thin wrapper around a html element, which allows to generate a HTML-element. * 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; protected isDestroyed = false;
private readonly clss: Set<string> = new Set<string>(); private readonly clss: Set<string> = new Set<string>();
private style: string; private style: string;
private _onClick: () => void; private _onClick: () => void | Promise<void>;
public onClick(f: (() => void)) { public onClick(f: (() => void)) {
this._onClick = f; this._onClick = f;
@ -127,12 +125,15 @@ export default abstract class BaseUIElement {
if (this._onClick !== undefined) { if (this._onClick !== undefined) {
const self = this; const self = this;
el.onclick = (e) => { el.onclick = async (e) => {
// @ts-ignore // @ts-ignore
if (e.consumed) { if (e.consumed) {
return; return;
} }
self._onClick(); const v = self._onClick();
if(typeof v === "object"){
await v
}
// @ts-ignore // @ts-ignore
e.consumed = true; e.consumed = true;
} }

View file

@ -0,0 +1,103 @@
import {VariableUiElement} from "../Base/VariableUIElement";
import {Store, UIEventSource} from "../../Logic/UIEventSource";
import PlantNet from "../../Logic/Web/PlantNet";
import Loading from "../Base/Loading";
import Wikidata from "../../Logic/Web/Wikidata";
import WikidataPreviewBox from "../Wikipedia/WikidataPreviewBox";
import {Button} from "../Base/Button";
import Combine from "../Base/Combine";
import Title from "../Base/Title";
import WikipediaBox from "../Wikipedia/WikipediaBox";
import Translations from "../i18n/Translations";
export default class PlantNetSpeciesSearch extends VariableUiElement {
/***
* Given images, queries plantnet to search a species matching those images.
* A list of species will be presented to the user, after which they can confirm an item.
* The wikidata-url is returned in the callback when the user selects one
*/
constructor(images: Store<string[]>, onConfirm: (wikidataUrl: string) => Promise<void>) {
const t = Translations.t.plantDetection
super(
images
.bind(images => {
if (images.length === 0) {
return null
}
return new UIEventSource({success: PlantNet.exampleResultPrunus}) /*/ UIEventSource.FromPromiseWithErr(PlantNet.query(images.slice(0,5))); //*/
})
.map(result => {
if (result === undefined) {
return new Loading(t.querying.Subs(images.data))
}
if (result === null) {
return t.takeImages
}
if (result["error"] !== undefined) {
return t.error.Subs(<any>result).SetClass("alert")
}
console.log(result)
const success = result["success"]
const selectedSpecies = new UIEventSource<string>(undefined)
const speciesInformation = success.results
.filter(species => species.score >= 0.005)
.map(species => {
const wikidata = UIEventSource.FromPromise(Wikidata.Sparql<{ species }>(["?species", "?speciesLabel"],
["?species wdt:P846 \"" + species.gbif.id + "\""]));
const confirmButton = new Button(t.seeInfo, async() => {
await selectedSpecies.setData(wikidata.data[0].species?.value)
}).SetClass("btn")
const match = t.matchPercentage.Subs({match: Math.round(species.score * 100)}).SetClass("font-bold")
const extraItems = new Combine([match, confirmButton]).SetClass("flex flex-col")
return new WikidataPreviewBox(wikidata.map(wd => wd == undefined ? undefined : wd[0]?.species?.value),
{
whileLoading: new Loading(
t.loadingWikidata.Subs({species: species.species.scientificNameWithoutAuthor})),
extraItems: [new Combine([extraItems])],
imageStyle: "max-width: 8rem; width: unset; height: 8rem"
})
.SetClass("border-2 border-subtle rounded-xl block mb-2")
}
);
const plantOverview = new Combine([
new Title(t.overviewTitle),
t.overviewIntro,
t.overviewVerify.SetClass("font-bold"),
...speciesInformation]).SetClass("flex flex-col")
return new VariableUiElement(selectedSpecies.map(wikidataSpecies => {
if (wikidataSpecies === undefined) {
return plantOverview
}
const buttons = new Combine([
new Button("Confirm", () => {
onConfirm(wikidataSpecies)
}).SetClass("btn"),
new Button("Back to plant overview", () => {
selectedSpecies.setData(undefined)
}).SetClass("btn btn-secondary")
]).SetClass("flex self-end");
return new Combine([
new WikipediaBox([wikidataSpecies], {
firstParagraphOnly: false,
noImages: false,
addHeader: false
}).SetClass("h-96"),
buttons
]).SetClass("flex flex-col self-end")
}))
}
))
}
}

View file

@ -61,6 +61,8 @@ import StatisticsPanel from "./BigComponents/StatisticsPanel";
import {OsmFeature} from "../Models/OsmFeature"; import {OsmFeature} from "../Models/OsmFeature";
import EditableTagRendering from "./Popup/EditableTagRendering"; import EditableTagRendering from "./Popup/EditableTagRendering";
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"; import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig";
import {ProvidedImage} from "../Logic/ImageProviders/ImageProvider";
import PlantNetSpeciesSearch from "./BigComponents/PlantNetSpeciesSearch";
export interface SpecialVisualization { export interface SpecialVisualization {
funcName: string, funcName: string,
@ -196,7 +198,7 @@ class NearbyImageVis implements SpecialVisualization {
new ChangeTagAction( new ChangeTagAction(
id, id,
new And(tags), new And(tags),
tagSource, tagSource.data,
{ {
theme: state?.layoutToUse.id, theme: state?.layoutToUse.id,
changeType: "link-image" changeType: "link-image"
@ -1299,6 +1301,46 @@ export default class SpecialVisualizations {
const [layerId, __] = tagRenderingId.split(".") const [layerId, __] = tagRenderingId.split(".")
return [layerId] return [layerId]
} }
},
{
funcName: "plantnet_detection",
docs: "Sends the images linked to the current object to plantnet.org and asks it what plant species is shown on it. The user can then select the correct species; the corresponding wikidata-identifier will then be added to the object (together with `source:species:wikidata=plantnet.org AI`). ",
args: [{
name: "image_key",
defaultValue: AllImageProviders.defaultKeys.join(","),
doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... Multiple values are allowed if ';'-separated "
}],
constr: (state, tags, args) => {
let imagePrefixes: string[] = undefined;
if (args.length > 0) {
imagePrefixes = [].concat(...args.map(a => a.split(",")));
}
const detect = new UIEventSource(false)
return new Toggle(
new Lazy(() => {
const allProvidedImages: Store<ProvidedImage[]> = AllImageProviders.LoadImagesFor(tags, imagePrefixes)
const allImages: Store<string[]> = allProvidedImages.map(pi => pi.map(pi => pi.url))
return new PlantNetSpeciesSearch(allImages, async selectedWikidata => {
selectedWikidata = Wikidata.ExtractKey(selectedWikidata)
const change = new ChangeTagAction(tags.data.id,
new And([new Tag("species:wikidata", selectedWikidata),
new Tag("source:species:wikidata","PlantNet.org AI")
]),
tags.data,
{
theme: state.layoutToUse.id,
changeType: "plantnet-ai-detection"
}
)
await state.changes.applyAction(change)
})
}),
new SubtleButton(undefined, "Detect plant species with plantnet.org").onClick(() => detect.setData(true)),
detect
)
}
} }
] ]

View file

@ -57,7 +57,11 @@ export default class WikidataPreviewBox extends VariableUiElement {
} }
] ]
constructor(wikidataId: Store<string>, options?: {noImages?: boolean, whileLoading?: BaseUIElement | string, extraItems?: (BaseUIElement | string)[]}) { constructor(wikidataId: Store<string>, options?: {
noImages?: boolean,
imageStyle?: string,
whileLoading?: BaseUIElement | string,
extraItems?: (BaseUIElement | string)[]}) {
let inited = false; let inited = false;
const wikidata = wikidataId const wikidata = wikidataId
.stabilized(250) .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( let link = new Link(
new Combine([ new Combine([
wikidata.id, wikidata.id,
@ -111,7 +118,7 @@ export default class WikidataPreviewBox extends VariableUiElement {
} }
if (imageUrl && !options?.noImages) { if (imageUrl && !options?.noImages) {
imageUrl = WikimediaImageProvider.singleton.PrepUrl(imageUrl).url 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") info.SetClass("w-full")]).SetClass("flex")
} }

View file

@ -104,6 +104,11 @@
} }
] ]
}, },
{
"id": "plantnet",
"render": "{plantnet_detection()}",
"condition": "species:wikidata="
},
{ {
"id": "tree-species-wikidata", "id": "tree-species-wikidata",
"question": { "question": {

View file

@ -858,6 +858,10 @@ video {
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
.mb-2 {
margin-bottom: 0.5rem;
}
.ml-3 { .ml-3 {
margin-left: 0.75rem; margin-left: 0.75rem;
} }
@ -866,14 +870,6 @@ video {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.mt-8 {
margin-top: 2rem;
}
.mt-4 {
margin-top: 1rem;
}
.mt-2 { .mt-2 {
margin-top: 0.5rem; margin-top: 0.5rem;
} }
@ -886,6 +882,10 @@ video {
margin-right: 2rem; margin-right: 2rem;
} }
.mt-4 {
margin-top: 1rem;
}
.mt-6 { .mt-6 {
margin-top: 1.5rem; margin-top: 1.5rem;
} }
@ -910,10 +910,6 @@ video {
margin-right: 1rem; margin-right: 1rem;
} }
.mb-2 {
margin-bottom: 0.5rem;
}
.ml-2 { .ml-2 {
margin-left: 0.5rem; margin-left: 0.5rem;
} }
@ -934,6 +930,10 @@ video {
margin-top: 0px; margin-top: 0px;
} }
.mt-8 {
margin-top: 2rem;
}
.mb-8 { .mb-8 {
margin-bottom: 2rem; margin-bottom: 2rem;
} }
@ -1054,6 +1054,10 @@ video {
height: 6rem; height: 6rem;
} }
.h-96 {
height: 24rem;
}
.h-64 { .h-64 {
height: 16rem; height: 16rem;
} }
@ -1162,6 +1166,10 @@ video {
width: 2rem; width: 2rem;
} }
.w-1\/3 {
width: 33.333333%;
}
.w-4 { .w-4 {
width: 1rem; width: 1rem;
} }
@ -1407,6 +1415,10 @@ video {
border-radius: 9999px; border-radius: 9999px;
} }
.rounded-xl {
border-radius: 0.75rem;
}
.rounded-3xl { .rounded-3xl {
border-radius: 1.5rem; border-radius: 1.5rem;
} }
@ -1423,10 +1435,6 @@ video {
border-radius: 0.5rem; border-radius: 0.5rem;
} }
.rounded-xl {
border-radius: 0.75rem;
}
.rounded-sm { .rounded-sm {
border-radius: 0.125rem; border-radius: 0.125rem;
} }
@ -1436,14 +1444,14 @@ video {
border-bottom-left-radius: 0.25rem; border-bottom-left-radius: 0.25rem;
} }
.border {
border-width: 1px;
}
.border-2 { .border-2 {
border-width: 2px; border-width: 2px;
} }
.border {
border-width: 1px;
}
.border-4 { .border-4 {
border-width: 4px; border-width: 4px;
} }
@ -2866,10 +2874,6 @@ input {
width: 75%; width: 75%;
} }
.lg\:w-1\/3 {
width: 33.333333%;
}
.lg\:w-1\/4 { .lg\:w-1\/4 {
width: 25%; width: 25%;
} }
@ -2878,6 +2882,10 @@ input {
width: 16.666667%; width: 16.666667%;
} }
.lg\:w-1\/3 {
width: 33.333333%;
}
.lg\:grid-cols-3 { .lg\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }

View file

@ -685,6 +685,17 @@
"typeText": "Type some text to add a comment", "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." "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 <a href='https://plantnet.org/' target='_blank'>PlantNet.org</a> 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": { "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.",
"editingTitle": "When making changes", "editingTitle": "When making changes",