forked from MapComplete/MapComplete
First working plantnet UI
This commit is contained in:
parent
a8959fc934
commit
06f8cf7006
9 changed files with 216 additions and 39 deletions
|
@ -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<void>)) {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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<string> = new Set<string>();
|
||||
private style: string;
|
||||
private _onClick: () => void;
|
||||
private _onClick: () => void | Promise<void>;
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
103
UI/BigComponents/PlantNetSpeciesSearch.ts
Normal file
103
UI/BigComponents/PlantNetSpeciesSearch.ts
Normal 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")
|
||||
}))
|
||||
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
}
|
|
@ -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 <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
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -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;
|
||||
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")
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue