Add a wikidata search box
This commit is contained in:
parent
54bc4f24da
commit
b5a2ee1757
21 changed files with 4141 additions and 3590 deletions
|
@ -26,19 +26,19 @@ export default class AllImageProviders {
|
|||
private static _cache: Map<string, UIEventSource<ProvidedImage[]>> = new Map<string, UIEventSource<ProvidedImage[]>>()
|
||||
|
||||
public static LoadImagesFor(tags: UIEventSource<any>, tagKey?: string): UIEventSource<ProvidedImage[]> {
|
||||
const id = tags.data.id
|
||||
if (id === undefined) {
|
||||
if (tags.data.id === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const cached = this._cache.get(tags.data.id)
|
||||
const cacheKey = tags.data.id+tagKey
|
||||
const cached = this._cache.get(cacheKey)
|
||||
if (cached !== undefined) {
|
||||
return cached
|
||||
}
|
||||
|
||||
|
||||
const source = new UIEventSource([])
|
||||
this._cache.set(id, source)
|
||||
this._cache.set(cacheKey, source)
|
||||
const allSources = []
|
||||
for (const imageProvider of AllImageProviders.ImageAttributionSource) {
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
|
||||
}
|
||||
|
||||
private PrepareUrl(value: string): string {
|
||||
private static PrepareUrl(value: string): string {
|
||||
|
||||
if (value.toLowerCase().startsWith("https://commons.wikimedia.org/wiki/")) {
|
||||
return value;
|
||||
|
@ -97,18 +97,18 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
|
||||
}
|
||||
|
||||
private async UrlForImage(image: string): Promise<ProvidedImage> {
|
||||
private UrlForImage(image: string): ProvidedImage {
|
||||
if (!image.startsWith("File:")) {
|
||||
image = "File:" + image
|
||||
}
|
||||
return {url: this.PrepareUrl(image), key: undefined, provider: this}
|
||||
return {url: WikimediaImageProvider.PrepareUrl(image), key: undefined, provider: this}
|
||||
}
|
||||
|
||||
private startsWithCommonsPrefix(value: string){
|
||||
private static startsWithCommonsPrefix(value: string): boolean{
|
||||
return WikimediaImageProvider.commonsPrefixes.some(prefix => value.startsWith(prefix))
|
||||
}
|
||||
|
||||
private removeCommonsPrefix(value: string){
|
||||
private static removeCommonsPrefix(value: string): string{
|
||||
if(value.startsWith("https://upload.wikimedia.org/")){
|
||||
value = value.substring(value.lastIndexOf("/") + 1)
|
||||
value = decodeURIComponent(value)
|
||||
|
@ -130,26 +130,38 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
return value;
|
||||
}
|
||||
|
||||
public PrepUrl(value: string): ProvidedImage {
|
||||
const hasCommonsPrefix = WikimediaImageProvider.startsWithCommonsPrefix(value)
|
||||
value = WikimediaImageProvider.removeCommonsPrefix(value)
|
||||
|
||||
if (value.startsWith("File:")) {
|
||||
return this.UrlForImage(value)
|
||||
}
|
||||
|
||||
// We do a last effort and assume this is a file
|
||||
return this.UrlForImage("File:" + value)
|
||||
}
|
||||
|
||||
public async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
|
||||
const hasCommonsPrefix = this.startsWithCommonsPrefix(value)
|
||||
const hasCommonsPrefix = WikimediaImageProvider.startsWithCommonsPrefix(value)
|
||||
if(key !== undefined && key !== this.commons_key && !hasCommonsPrefix){
|
||||
return []
|
||||
}
|
||||
|
||||
value = this.removeCommonsPrefix(value)
|
||||
value = WikimediaImageProvider.removeCommonsPrefix(value)
|
||||
if (value.startsWith("Category:")) {
|
||||
const urls = await Wikimedia.GetCategoryContents(value)
|
||||
return urls.filter(url => url.startsWith("File:")).map(image => this.UrlForImage(image))
|
||||
return urls.filter(url => url.startsWith("File:")).map(image => Promise.resolve(this.UrlForImage(image)))
|
||||
}
|
||||
if (value.startsWith("File:")) {
|
||||
return [this.UrlForImage(value)]
|
||||
return [Promise.resolve(this.UrlForImage(value))]
|
||||
}
|
||||
if (value.startsWith("http")) {
|
||||
// PRobably an error
|
||||
return []
|
||||
}
|
||||
// We do a last effort and assume this is a file
|
||||
return [this.UrlForImage("File:" + value)]
|
||||
return [Promise.resolve(this.UrlForImage("File:" + value))]
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -75,6 +75,27 @@ export class UIEventSource<T> {
|
|||
promise?.catch(err => console.warn("Promise failed:", err))
|
||||
return src
|
||||
}
|
||||
|
||||
public AsPromise(): Promise<T>{
|
||||
const self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
if(self.data !== undefined){
|
||||
resolve(self.data)
|
||||
}else{
|
||||
self.addCallbackD(data => {
|
||||
resolve(data)
|
||||
return true; // return true to unregister as we only need to be called once
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public WaitForPromise(promise: Promise<T>, onFail: ((any) => void)): UIEventSource<T> {
|
||||
const self = this;
|
||||
promise?.then(d => self.setData(d))
|
||||
promise?.catch(err =>onFail(err))
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a promise into a UIVentsource, sets the UIEVentSource when the result is calculated.
|
||||
|
@ -195,16 +216,20 @@ export class UIEventSource<T> {
|
|||
const sink = new UIEventSource<X>(undefined)
|
||||
const seenEventSources = new Set<UIEventSource<X>>();
|
||||
mapped.addCallbackAndRun(newEventSource => {
|
||||
|
||||
if (newEventSource === undefined) {
|
||||
if (newEventSource === null) {
|
||||
sink.setData(null)
|
||||
} else if (newEventSource === undefined) {
|
||||
sink.setData(undefined)
|
||||
} else if (!seenEventSources.has(newEventSource)) {
|
||||
}else if (!seenEventSources.has(newEventSource)) {
|
||||
seenEventSources.add(newEventSource)
|
||||
newEventSource.addCallbackAndRun(resultData => {
|
||||
if (mapped.data === newEventSource) {
|
||||
sink.setData(resultData);
|
||||
}
|
||||
})
|
||||
}else{
|
||||
// Already seen, so we don't have to add a callback, just update the value
|
||||
sink.setData(newEventSource.data)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -12,6 +12,11 @@ export interface WikidataResponse {
|
|||
commons: string
|
||||
}
|
||||
|
||||
export interface WikidataSearchoptions {
|
||||
lang?: "en" | string,
|
||||
maxCount?: 20 | number
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility functions around wikidata
|
||||
*/
|
||||
|
@ -47,10 +52,14 @@ export default class Wikidata {
|
|||
const claimsList: any[] = entity.claims[claimId]
|
||||
const values = new Set<string>()
|
||||
for (const claim of claimsList) {
|
||||
const value = claim.mainsnak?.datavalue?.value;
|
||||
if(value !== undefined){
|
||||
values.add(value)
|
||||
let value = claim.mainsnak?.datavalue?.value;
|
||||
if (value === undefined) {
|
||||
continue;
|
||||
}
|
||||
if(value.id !== undefined){
|
||||
value = value.id
|
||||
}
|
||||
values.add(value)
|
||||
}
|
||||
claims.set(claimId, values);
|
||||
}
|
||||
|
@ -77,6 +86,82 @@ export default class Wikidata {
|
|||
return src;
|
||||
}
|
||||
|
||||
public static async search(
|
||||
search: string,
|
||||
options?:WikidataSearchoptions,
|
||||
page = 1
|
||||
): Promise<{
|
||||
id: string,
|
||||
label: string,
|
||||
description: string
|
||||
}[]> {
|
||||
const maxCount = options?.maxCount ?? 20
|
||||
let pageCount = Math.min(maxCount,50)
|
||||
const start = page * pageCount - pageCount;
|
||||
const lang = (options?.lang ?? "en")
|
||||
const url =
|
||||
"https://www.wikidata.org/w/api.php?action=wbsearchentities&search=" +
|
||||
search +
|
||||
"&language=" +
|
||||
lang +
|
||||
"&limit="+pageCount+"&continue=" +
|
||||
start +
|
||||
"&format=json&uselang=" +
|
||||
lang +
|
||||
"&type=item&origin=*"+
|
||||
"&props=" ;// props= removes some unused values in the result
|
||||
const response = await Utils.downloadJson(url)
|
||||
|
||||
const result : any[] = response.search
|
||||
|
||||
if(result.length < pageCount){
|
||||
// No next page
|
||||
return result;
|
||||
}
|
||||
if(result.length < maxCount){
|
||||
const newOptions = {...options}
|
||||
newOptions.maxCount = maxCount - result.length
|
||||
result.push(...await Wikidata.search(search,
|
||||
newOptions,
|
||||
page + 1
|
||||
))
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static async searchAndFetch(
|
||||
search: string,
|
||||
options?:WikidataSearchoptions
|
||||
) : Promise<WikidataResponse[]>
|
||||
{
|
||||
const maxCount = options.maxCount
|
||||
// We provide some padding to filter away invalid values
|
||||
options.maxCount = Math.ceil((options.maxCount ?? 20) * 1.5)
|
||||
const searchResults = await Wikidata.search(search, options)
|
||||
const maybeResponses = await Promise.all(searchResults.map(async r => {
|
||||
try{
|
||||
return await Wikidata.LoadWikidataEntry(r.id).AsPromise()
|
||||
}catch(e){
|
||||
console.error(e)
|
||||
return undefined;
|
||||
}
|
||||
}))
|
||||
const responses = maybeResponses
|
||||
.map(r => <WikidataResponse> r["success"])
|
||||
.filter(wd => {
|
||||
if(wd === undefined){
|
||||
return false;
|
||||
}
|
||||
if(wd.claims.get("P31" /*Instance of*/)?.has("Q4167410"/* Wikimedia Disambiguation page*/)){
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
responses.splice(maxCount, responses.length - maxCount)
|
||||
return responses
|
||||
}
|
||||
|
||||
private static ExtractKey(value: string | number) : number{
|
||||
if (typeof value === "number") {
|
||||
return value
|
||||
|
@ -99,6 +184,7 @@ export default class Wikidata {
|
|||
return n;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads a wikidata page
|
||||
* @returns the entity of the given value
|
||||
|
@ -109,7 +195,7 @@ export default class Wikidata {
|
|||
console.warn("Could not extract a wikidata entry from", value)
|
||||
return undefined;
|
||||
}
|
||||
console.log("Requesting wikidata with id", id)
|
||||
|
||||
const url = "https://www.wikidata.org/wiki/Special:EntityData/Q" + id + ".json";
|
||||
const response = await Utils.downloadJson(url)
|
||||
return Wikidata.ParseResponse(response.entities["Q" + id])
|
||||
|
|
|
@ -52,7 +52,7 @@ export interface TagRenderingConfigJson {
|
|||
* Extra parameters to initialize the input helper arguments.
|
||||
* For semantics, see the 'SpecialInputElements.md'
|
||||
*/
|
||||
helperArgs?: (string | number | boolean)[];
|
||||
helperArgs?: (string | number | boolean | any)[];
|
||||
/**
|
||||
* If a value is added with the textfield, these extra tag is addded.
|
||||
* Useful to add a 'fixme=freeform textfield used - to be checked'
|
||||
|
|
|
@ -30,10 +30,15 @@ export default class Combine extends BaseUIElement {
|
|||
if (subEl === undefined || subEl === null) {
|
||||
continue;
|
||||
}
|
||||
try{
|
||||
|
||||
const subHtml = subEl.ConstructElement()
|
||||
if (subHtml !== undefined) {
|
||||
el.appendChild(subHtml)
|
||||
}
|
||||
}catch(e){
|
||||
console.error("Could not generate subelement in combine due to ", e)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const domExc = e as DOMException
|
||||
|
|
|
@ -12,10 +12,11 @@ import ImgurUploader from "../../Logic/ImageProviders/ImgurUploader";
|
|||
import UploadFlowStateUI from "../BigComponents/UploadFlowStateUI";
|
||||
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
|
||||
export class ImageUploadFlow extends Toggle {
|
||||
|
||||
constructor(tagsSource: UIEventSource<any>, imagePrefix: string = "image") {
|
||||
constructor(tagsSource: UIEventSource<any>, imagePrefix: string = "image", text: string = undefined) {
|
||||
const uploader = new ImgurUploader(url => {
|
||||
// A file was uploaded - we add it to the tags of the object
|
||||
|
||||
|
@ -43,10 +44,17 @@ export class ImageUploadFlow extends Toggle {
|
|||
const licensePicker = new LicensePicker()
|
||||
|
||||
const t = Translations.t.image;
|
||||
|
||||
let labelContent : BaseUIElement
|
||||
if(text === undefined) {
|
||||
labelContent = Translations.t.image.addPicture.Clone().SetClass("block align-middle mt-1 ml-3 text-4xl ")
|
||||
}else{
|
||||
labelContent = new FixedUiElement(text).SetClass("block align-middle mt-1 ml-3 text-2xl ")
|
||||
}
|
||||
const label = new Combine([
|
||||
Svg.camera_plus_ui().SetClass("block w-12 h-12 p-1"),
|
||||
Translations.t.image.addPicture.Clone().SetClass("block align-middle mt-1 ml-3")
|
||||
]).SetClass("p-2 border-4 border-black rounded-full text-4xl font-bold h-full align-middle w-full flex justify-center")
|
||||
Svg.camera_plus_ui().SetClass("block w-12 h-12 p-1 text-4xl "),
|
||||
labelContent
|
||||
]).SetClass("p-2 border-4 border-black rounded-full font-bold h-full align-middle w-full flex justify-center")
|
||||
|
||||
const fileSelector = new FileSelectorButton(label)
|
||||
fileSelector.GetValue().addCallback(filelist => {
|
||||
|
|
|
@ -16,6 +16,7 @@ import LengthInput from "./LengthInput";
|
|||
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||
import {Unit} from "../../Models/Unit";
|
||||
import {FixedInputElement} from "./FixedInputElement";
|
||||
import WikidataSearchBox from "../Wikipedia/WikidataSearchBox";
|
||||
|
||||
interface TextFieldDef {
|
||||
name: string,
|
||||
|
@ -147,7 +148,7 @@ export default class ValidatedTextField {
|
|||
),
|
||||
ValidatedTextField.tp(
|
||||
"wikidata",
|
||||
"A wikidata identifier, e.g. Q42",
|
||||
"A wikidata identifier, e.g. Q42. Input helper arguments: [ key: the value of this tag will initialize search (default: name), options: { removePrefixes: string[], removePostfixes: string[] } these prefixes and postfixes will be removed from the initial search value]",
|
||||
(str) => {
|
||||
if (str === undefined) {
|
||||
return false;
|
||||
|
@ -163,7 +164,40 @@ export default class ValidatedTextField {
|
|||
str = str.substr(wd.length)
|
||||
}
|
||||
return str.toUpperCase();
|
||||
}),
|
||||
},
|
||||
(currentValue, inputHelperOptions) => {
|
||||
const args = inputHelperOptions.args
|
||||
const searchKey = args[0] ?? "name"
|
||||
|
||||
let searchFor = <string>inputHelperOptions.feature?.properties[searchKey]?.toLowerCase()
|
||||
|
||||
const options = args[1]
|
||||
if (searchFor !== undefined && options !== undefined) {
|
||||
const prefixes = <string[]>options["removePrefixes"]
|
||||
const postfixes = <string[]>options["removePostfixes"]
|
||||
|
||||
for (const postfix of postfixes ?? []) {
|
||||
if (searchFor.endsWith(postfix)) {
|
||||
searchFor = searchFor.substring(0, searchFor.length - postfix.length)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const prefix of prefixes ?? []) {
|
||||
if (searchFor.startsWith(prefix)) {
|
||||
searchFor = searchFor.substring(prefix.length)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return new WikidataSearchBox({
|
||||
value: currentValue,
|
||||
searchText: new UIEventSource<string>(searchFor)
|
||||
})
|
||||
}
|
||||
),
|
||||
|
||||
ValidatedTextField.tp(
|
||||
"int",
|
||||
|
@ -361,13 +395,13 @@ export default class ValidatedTextField {
|
|||
// This implies:
|
||||
// We have to create a dropdown with applicable denominations, and fuse those values
|
||||
const unit = options.unit
|
||||
|
||||
|
||||
|
||||
|
||||
const isSingular = input.GetValue().map(str => str?.trim() === "1")
|
||||
|
||||
const unitDropDown =
|
||||
unit.denominations.length === 1 ?
|
||||
new FixedInputElement( unit.denominations[0].getToggledHuman(isSingular), unit.denominations[0])
|
||||
new FixedInputElement(unit.denominations[0].getToggledHuman(isSingular), unit.denominations[0])
|
||||
: new DropDown("",
|
||||
unit.denominations.map(denom => {
|
||||
return {
|
||||
|
@ -378,17 +412,17 @@ export default class ValidatedTextField {
|
|||
)
|
||||
unitDropDown.GetValue().setData(unit.defaultDenom)
|
||||
unitDropDown.SetClass("w-min")
|
||||
|
||||
const fixedDenom = unit.denominations.length === 1 ? unit.denominations[0] : undefined
|
||||
|
||||
const fixedDenom = unit.denominations.length === 1 ? unit.denominations[0] : undefined
|
||||
input = new CombinedInputElement(
|
||||
input,
|
||||
unitDropDown,
|
||||
// combine the value from the textfield and the dropdown into the resulting value that should go into OSM
|
||||
(text, denom) => {
|
||||
if(denom === undefined){
|
||||
if (denom === undefined) {
|
||||
return text
|
||||
}
|
||||
return denom?.canonicalValue(text, true)
|
||||
return denom?.canonicalValue(text, true)
|
||||
},
|
||||
(valueWithDenom: string) => {
|
||||
// Take the value from OSM and feed it into the textfield and the dropdown
|
||||
|
|
|
@ -26,7 +26,7 @@ import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSou
|
|||
import ShowDataMultiLayer from "./ShowDataLayer/ShowDataMultiLayer";
|
||||
import Minimap from "./Base/Minimap";
|
||||
import AllImageProviders from "../Logic/ImageProviders/AllImageProviders";
|
||||
import WikipediaBox from "./WikipediaBox";
|
||||
import WikipediaBox from "./Wikipedia/WikipediaBox";
|
||||
|
||||
export interface SpecialVisualization {
|
||||
funcName: string,
|
||||
|
@ -83,9 +83,13 @@ export default class SpecialVisualizations {
|
|||
name: "image-key",
|
||||
doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)",
|
||||
defaultValue: "image"
|
||||
},{
|
||||
name:"label",
|
||||
doc:"The text to show on the button",
|
||||
defaultValue: "Add image"
|
||||
}],
|
||||
constr: (state: State, tags, args) => {
|
||||
return new ImageUploadFlow(tags, args[0])
|
||||
return new ImageUploadFlow(tags, args[0], args[1])
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
80
UI/Wikipedia/WikidataPreviewBox.ts
Normal file
80
UI/Wikipedia/WikidataPreviewBox.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Wikidata, {WikidataResponse} from "../../Logic/Web/Wikidata";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Loading from "../Base/Loading";
|
||||
import {Transform} from "stream";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Combine from "../Base/Combine";
|
||||
import Img from "../Base/Img";
|
||||
import {WikimediaImageProvider} from "../../Logic/ImageProviders/WikimediaImageProvider";
|
||||
import Link from "../Base/Link";
|
||||
import Svg from "../../Svg";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class WikidataPreviewBox extends VariableUiElement {
|
||||
|
||||
constructor(wikidataId : UIEventSource<string>) {
|
||||
let inited = false;
|
||||
const wikidata = wikidataId
|
||||
.stabilized(250)
|
||||
.bind(id => {
|
||||
if (id === undefined || id === "" || id === "Q") {
|
||||
return null;
|
||||
}
|
||||
inited = true;
|
||||
return Wikidata.LoadWikidataEntry(id)
|
||||
})
|
||||
|
||||
super(wikidata.map(maybeWikidata => {
|
||||
if(maybeWikidata === null || !inited){
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if(maybeWikidata === undefined){
|
||||
return new Loading(Translations.t.general.loading)
|
||||
}
|
||||
|
||||
if (maybeWikidata["error"] !== undefined) {
|
||||
return new FixedUiElement(maybeWikidata["error"]).SetClass("alert")
|
||||
}
|
||||
const wikidata = <WikidataResponse> maybeWikidata["success"]
|
||||
return WikidataPreviewBox.WikidataResponsePreview(wikidata)
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
public static WikidataResponsePreview(wikidata: WikidataResponse): BaseUIElement{
|
||||
let link = new Link(
|
||||
new Combine([
|
||||
wikidata.id,
|
||||
Svg.wikidata_ui().SetStyle("width: 2.5rem").SetClass("block")
|
||||
]).SetClass("flex"),
|
||||
"https://wikidata.org/wiki/"+wikidata.id ,true)
|
||||
|
||||
let info = new Combine( [
|
||||
new Combine([Translation.fromMap(wikidata.labels).SetClass("font-bold"),
|
||||
link]).SetClass("flex justify-between"),
|
||||
Translation.fromMap(wikidata.descriptions)
|
||||
]).SetClass("flex flex-col link-underline")
|
||||
|
||||
|
||||
let imageUrl = undefined
|
||||
if(wikidata.claims.get("P18")?.size > 0){
|
||||
imageUrl = Array.from(wikidata.claims.get("P18"))[0]
|
||||
}
|
||||
|
||||
|
||||
if(imageUrl){
|
||||
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.SetClass("w-full")]).SetClass("flex")
|
||||
}
|
||||
|
||||
info.SetClass("p-2 w-full")
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
}
|
119
UI/Wikipedia/WikidataSearchBox.ts
Normal file
119
UI/Wikipedia/WikidataSearchBox.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
import Combine from "../Base/Combine";
|
||||
import {InputElement} from "../Input/InputElement";
|
||||
import {TextField} from "../Input/TextField";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Wikidata, {WikidataResponse} from "../../Logic/Web/Wikidata";
|
||||
import Locale from "../i18n/Locale";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import WikidataPreviewBox from "./WikidataPreviewBox";
|
||||
import Title from "../Base/Title";
|
||||
import WikipediaBox from "./WikipediaBox";
|
||||
import Svg from "../../Svg";
|
||||
import Link from "../Base/Link";
|
||||
|
||||
export default class WikidataSearchBox extends InputElement<string> {
|
||||
|
||||
private readonly wikidataId: UIEventSource<string>
|
||||
private readonly searchText: UIEventSource<string>
|
||||
|
||||
constructor(options?: {
|
||||
searchText?: UIEventSource<string>,
|
||||
value?: UIEventSource<string>
|
||||
}) {
|
||||
super();
|
||||
this.searchText = options?.searchText
|
||||
this.wikidataId = options?.value ?? new UIEventSource<string>(undefined);
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<string> {
|
||||
return this.wikidataId;
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
|
||||
const searchField = new TextField({
|
||||
placeholder: Translations.t.general.wikipedia.searchWikidata,
|
||||
value: this.searchText,
|
||||
inputStyle: "width: calc(100% - 0.5rem); border: 1px solid black"
|
||||
|
||||
})
|
||||
const selectedWikidataId = this.wikidataId
|
||||
|
||||
const lastSearchResults = new UIEventSource<WikidataResponse[]>([])
|
||||
const searchFailMessage = new UIEventSource(undefined)
|
||||
searchField.GetValue().addCallbackAndRunD(searchText => {
|
||||
if (searchText.length < 3) {
|
||||
return;
|
||||
}
|
||||
searchFailMessage.setData(undefined)
|
||||
lastSearchResults.WaitForPromise(
|
||||
Wikidata.searchAndFetch(searchText, {
|
||||
lang: Locale.language.data,
|
||||
maxCount: 5
|
||||
}
|
||||
), err => searchFailMessage.setData(err))
|
||||
|
||||
})
|
||||
|
||||
|
||||
const previews = new VariableUiElement(lastSearchResults.map(searchResults => {
|
||||
if (searchFailMessage.data !== undefined) {
|
||||
return new Combine([Translations.t.general.wikipedia.failed.Clone().SetClass("alert"), searchFailMessage.data])
|
||||
}
|
||||
|
||||
if(searchResults.length === 0){
|
||||
return Translations.t.general.wikipedia.noResults.Subs({search: searchField.GetValue().data ?? ""})
|
||||
}
|
||||
|
||||
if (searchResults.length === 0) {
|
||||
return Translations.t.general.wikipedia.doSearch
|
||||
}
|
||||
|
||||
return new Combine(searchResults.map(wikidataresponse => {
|
||||
const el = WikidataPreviewBox.WikidataResponsePreview(wikidataresponse).SetClass("rounded-xl p-1 sm:p-2 md:p-3 m-px border-2 sm:border-4 transition-colors")
|
||||
el.onClick(() => {
|
||||
selectedWikidataId.setData(wikidataresponse.id)
|
||||
})
|
||||
selectedWikidataId.addCallbackAndRunD(selected => {
|
||||
if (selected === wikidataresponse.id) {
|
||||
el.SetClass("subtle-background border-attention")
|
||||
} else {
|
||||
el.RemoveClass("subtle-background")
|
||||
el.RemoveClass("border-attention")
|
||||
}
|
||||
})
|
||||
return el;
|
||||
|
||||
})).SetClass("flex flex-col")
|
||||
|
||||
}, [searchFailMessage]))
|
||||
|
||||
//
|
||||
const full = new Combine([
|
||||
new Title(Translations.t.general.wikipedia.searchWikidata, 3).SetClass("m-2"),
|
||||
new Combine([
|
||||
Svg.search_ui().SetStyle("width: 1.5rem"),
|
||||
searchField.SetClass("m-2 w-full")]).SetClass("flex"),
|
||||
previews
|
||||
]).SetClass("flex flex-col border-2 border-black rounded-xl m-2 p-2")
|
||||
|
||||
return new Combine([
|
||||
new VariableUiElement(selectedWikidataId.map(wid => {
|
||||
if (wid === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return new WikipediaBox([wid]);
|
||||
})).SetStyle("max-height:12.5rem"),
|
||||
full
|
||||
]).ConstructElement();
|
||||
}
|
||||
|
||||
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
|
||||
IsValid(t: string): boolean {
|
||||
return t.startsWith("Q") && !isNaN(Number(t.substring(1)));
|
||||
}
|
||||
|
||||
}
|
|
@ -1,18 +1,19 @@
|
|||
import {UIEventSource} from "../Logic/UIEventSource";
|
||||
import {VariableUiElement} from "./Base/VariableUIElement";
|
||||
import Wikipedia from "../Logic/Web/Wikipedia";
|
||||
import Loading from "./Base/Loading";
|
||||
import {FixedUiElement} from "./Base/FixedUiElement";
|
||||
import Combine from "./Base/Combine";
|
||||
import BaseUIElement from "./BaseUIElement";
|
||||
import Title from "./Base/Title";
|
||||
import Translations from "./i18n/Translations";
|
||||
import Svg from "../Svg";
|
||||
import Wikidata, {WikidataResponse} from "../Logic/Web/Wikidata";
|
||||
import Locale from "./i18n/Locale";
|
||||
import Link from "./Base/Link";
|
||||
import {TabbedComponent} from "./Base/TabbedComponent";
|
||||
import {Translation} from "./i18n/Translation";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import Locale from "../i18n/Locale";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
import Svg from "../../Svg";
|
||||
import Combine from "../Base/Combine";
|
||||
import Title from "../Base/Title";
|
||||
import Wikipedia from "../../Logic/Web/Wikipedia";
|
||||
import Wikidata, {WikidataResponse} from "../../Logic/Web/Wikidata";
|
||||
import {TabbedComponent} from "../Base/TabbedComponent";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Loading from "../Base/Loading";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Link from "../Base/Link";
|
||||
import WikidataPreviewBox from "./WikidataPreviewBox";
|
||||
|
||||
export default class WikipediaBox extends Combine {
|
||||
|
||||
|
@ -116,7 +117,7 @@ export default class WikipediaBox extends Combine {
|
|||
if (status[0] == "no page") {
|
||||
const [_, wd] = <[string, WikidataResponse]> status
|
||||
return new Combine([
|
||||
Translation.fromMap(wd.descriptions) ,
|
||||
WikidataPreviewBox.WikidataResponsePreview(wd),
|
||||
wp.noWikipediaPage.Clone().SetClass("subtle")]).SetClass("flex flex-col p-4")
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load diff
|
@ -3,7 +3,8 @@
|
|||
"render": "{image_carousel()}{image_upload()}"
|
||||
},
|
||||
"wikipedia": {
|
||||
"render": "{wikipedia():max-height:25rem}"
|
||||
"render": "{wikipedia():max-height:25rem}",
|
||||
"condition": "wikidata~*"
|
||||
},
|
||||
"reviews": {
|
||||
"render": "{reviews()}"
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
"socialImage": "",
|
||||
"layers": [
|
||||
{
|
||||
"id": "has_a_name",
|
||||
"id": "has_etymology",
|
||||
"name": {
|
||||
"en": "Has etymolgy",
|
||||
"nl": "Heeft etymology info"
|
||||
|
@ -71,6 +71,21 @@
|
|||
"render": {
|
||||
"*": "{wikipedia(name:etymology:wikidata):max-height:30rem}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "wikidata-embed",
|
||||
"render": {
|
||||
"*": "<iframe src='https://m.wikidata.org/wiki/{name:etymology:wikidata}' style='width: 100%; height: 25rem'></iframe>"
|
||||
},
|
||||
"condition": "name:etymology:wikidata~*"
|
||||
},
|
||||
"wikipedia",
|
||||
{
|
||||
"id": "street-name-sign-image",
|
||||
"render": {
|
||||
"en": "{image_carousel(image:streetsign)}<br/>{image_upload(image:streetsign, Add image of a street name sign)}",
|
||||
"nl": "{image_carousel(image:streetsign)}<br/>{image_upload(image:streetsign, Voeg afbeelding van straatnaambordje toe)}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"icon": {
|
||||
|
@ -86,7 +101,114 @@
|
|||
"render": "#00f"
|
||||
},
|
||||
"presets": []
|
||||
},
|
||||
{
|
||||
"id": "etymology_missing",
|
||||
"name": {
|
||||
"en": "No etymology data yet",
|
||||
"nl": "Zonder etymology"
|
||||
},
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"and": [
|
||||
"name~*",
|
||||
{
|
||||
"or": [
|
||||
"highway~*",
|
||||
"building~*",
|
||||
"amenity=place_of_worship",
|
||||
"man_made=bridge",
|
||||
"heritage~*",
|
||||
"leisure=park",
|
||||
"leisure=nature_reserve",
|
||||
"landuse=forest",
|
||||
"natural=water"
|
||||
]
|
||||
},
|
||||
{
|
||||
"#": "We remove various features which often are too big and clutter the map",
|
||||
"and": [
|
||||
"healtcare=",
|
||||
"shop=",
|
||||
"school=",
|
||||
"place=",
|
||||
"landuse!=residential"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"minZoom": 16,
|
||||
"title": {
|
||||
"render": {
|
||||
"*": "{name}"
|
||||
}
|
||||
},
|
||||
"tagRenderings": [
|
||||
{
|
||||
"id": "name-origin-wikidata",
|
||||
"question": {
|
||||
"en": "What is the wikidata entry for the thing this feature is named after?",
|
||||
"nl": "Wat is de wikidata-entry voor het object waarnaar dit vernoemd is?<br /><span class='subtle'>Plak hier de wikidata entry of sla over</span>"
|
||||
},
|
||||
"render": {
|
||||
"*": "{name:etymology:wikidata}"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "name:etymology:wikidata",
|
||||
"type": "wikidata",
|
||||
"helperArgs": [
|
||||
"name",
|
||||
{
|
||||
"removePostfixes": [
|
||||
"steenweg",
|
||||
"heirbaan",
|
||||
"baan",
|
||||
"straat",
|
||||
"street",
|
||||
"weg",
|
||||
"dreef",
|
||||
"laan",
|
||||
"boulevard",
|
||||
"pad",
|
||||
"path",
|
||||
"plein",
|
||||
"square",
|
||||
"plaza",
|
||||
"wegel",
|
||||
"kerk",
|
||||
"church",
|
||||
"kaai"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "name-origin",
|
||||
"question": {
|
||||
"en": "What is the origin of this name?",
|
||||
"nl": "Naar wat is dit vernoemd?"
|
||||
},
|
||||
"render": {
|
||||
"en": "<div class='subtle'>This feature is named after</div><br />{name:etymology{",
|
||||
"nl": "<div class='subtle'>Dit is vernoemd naar</div><br />{name:etymology{"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "name:etymology"
|
||||
},
|
||||
"condition": "name:etymology:wikidata="
|
||||
},
|
||||
"wikipedia",
|
||||
{
|
||||
"id": "street-name-sign-image",
|
||||
"render": {
|
||||
"en": "{image_carousel(image:streetsign)}<br/>{image_upload(image:streetsign, Add image of a street name sign)}",
|
||||
"nl": "{image_carousel(image:streetsign)}<br/>{image_upload(image:streetsign, Voeg afbeelding van straatnaambordje toe)}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"color": "#fcb35388"
|
||||
}
|
||||
],
|
||||
"roamingRenderings": []
|
||||
]
|
||||
}
|
|
@ -824,6 +824,10 @@ video {
|
|||
margin: 1rem;
|
||||
}
|
||||
|
||||
.m-px {
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
.my-2 {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
@ -848,18 +852,14 @@ video {
|
|||
margin-left: 0.75rem;
|
||||
}
|
||||
|
||||
.mr-3 {
|
||||
margin-right: 0.75rem;
|
||||
.mb-2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.mr-4 {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-3 {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
@ -912,6 +912,10 @@ video {
|
|||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.mr-3 {
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
@ -1251,14 +1255,14 @@ video {
|
|||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.rounded-xl {
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.rounded-xl {
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
@ -1390,18 +1394,6 @@ video {
|
|||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.pr-2 {
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.pl-6 {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.pt-2 {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.pt-3 {
|
||||
padding-top: 0.75rem;
|
||||
}
|
||||
|
@ -1462,6 +1454,18 @@ video {
|
|||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.pr-2 {
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.pl-6 {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.pt-2 {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -1582,14 +1586,14 @@ video {
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.opacity-50 {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.opacity-0 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.opacity-50 {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.opacity-40 {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
@ -1632,6 +1636,12 @@ video {
|
|||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.transition-colors {
|
||||
transition-property: background-color, border-color, color, fill, stroke;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.transition-opacity {
|
||||
transition-property: opacity;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
@ -1902,6 +1912,10 @@ li::marker {
|
|||
border: 5px solid var(--catch-detail-color);
|
||||
}
|
||||
|
||||
.border-attention {
|
||||
border-color: var(--catch-detail-color);
|
||||
}
|
||||
|
||||
.direction-svg svg path {
|
||||
fill: var(--catch-detail-color) !important;
|
||||
}
|
||||
|
@ -2223,6 +2237,10 @@ li::marker {
|
|||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.sm\:m-1 {
|
||||
margin: 0.25rem;
|
||||
}
|
||||
|
||||
.sm\:mx-auto {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
@ -2268,6 +2286,10 @@ li::marker {
|
|||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sm\:border-4 {
|
||||
border-width: 4px;
|
||||
}
|
||||
|
||||
.sm\:p-0\.5 {
|
||||
padding: 0.125rem;
|
||||
}
|
||||
|
@ -2284,6 +2306,10 @@ li::marker {
|
|||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.sm\:p-2 {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.sm\:pl-2 {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
@ -2383,6 +2409,10 @@ li::marker {
|
|||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.md\:p-3 {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.md\:pt-4 {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
|
|
@ -195,6 +195,10 @@ li::marker {
|
|||
border: 5px solid var(--catch-detail-color);
|
||||
}
|
||||
|
||||
.border-attention {
|
||||
border-color: var(--catch-detail-color);
|
||||
}
|
||||
|
||||
.direction-svg svg path {
|
||||
fill: var(--catch-detail-color) !important;
|
||||
}
|
||||
|
|
|
@ -222,7 +222,11 @@
|
|||
"wikipediaboxTitle": "Wikipedia",
|
||||
"failed":"Loading the wikipedia entry failed",
|
||||
"loading": "Loading Wikipedia...",
|
||||
"noWikipediaPage": "This wikidata item has no corresponding wikipedia page yet."
|
||||
"noWikipediaPage": "This wikidata item has no corresponding wikipedia page yet.",
|
||||
"searchWikidata": "Search on wikidata",
|
||||
"doSearch": "Search above to see results",
|
||||
"noResults": "Nothing found for <i>{search}</i>",
|
||||
"createNewWikidata": "Create a new wikidata item"
|
||||
}
|
||||
},
|
||||
"favourite": {
|
||||
|
|
|
@ -752,6 +752,24 @@
|
|||
"tagRenderings": {
|
||||
"simple etymology": {
|
||||
"render": "Named after {name:etymology}"
|
||||
},
|
||||
"street-name-sign-image": {
|
||||
"render": "{image_carousel(image:streetsign)}<br/>{image_upload(image:streetsign, Add image of a street name sign)}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"1": {
|
||||
"name": "No etymology data yet",
|
||||
"tagRenderings": {
|
||||
"name-origin": {
|
||||
"question": "What is the origin of this name?",
|
||||
"render": "<div class='subtle'>This feature is named after</div><br />{name:etymology{"
|
||||
},
|
||||
"name-origin-wikidata": {
|
||||
"question": "What is the wikidata entry for the thing this feature is named after?"
|
||||
},
|
||||
"street-name-sign-image": {
|
||||
"render": "{image_carousel(image:streetsign)}<br/>{image_upload(image:streetsign, Add image of a street name sign)}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -633,6 +633,24 @@
|
|||
"tagRenderings": {
|
||||
"simple etymology": {
|
||||
"render": "Vernoemd naar {name:etymology}"
|
||||
},
|
||||
"street-name-sign-image": {
|
||||
"render": "{image_carousel(image:streetsign)}<br/>{image_upload(image:streetsign, Voeg afbeelding van straatnaambordje toe)}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"1": {
|
||||
"name": "Zonder etymology",
|
||||
"tagRenderings": {
|
||||
"name-origin": {
|
||||
"question": "Naar wat is dit vernoemd?",
|
||||
"render": "<div class='subtle'>Dit is vernoemd naar</div><br />{name:etymology{"
|
||||
},
|
||||
"name-origin-wikidata": {
|
||||
"question": "Wat is de wikidata-entry voor het object waarnaar dit vernoemd is?<br /><span class='subtle'>Plak hier de wikidata entry of sla over</span>"
|
||||
},
|
||||
"street-name-sign-image": {
|
||||
"render": "{image_carousel(image:streetsign)}<br/>{image_upload(image:streetsign, Voeg afbeelding van straatnaambordje toe)}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
28
test.ts
28
test.ts
|
@ -1,26 +1,6 @@
|
|||
import FeatureInfoBox from "./UI/Popup/FeatureInfoBox";
|
||||
import WikidataPreviewBox from "./UI/Wikipedia/WikidataPreviewBox";
|
||||
import {UIEventSource} from "./Logic/UIEventSource";
|
||||
import AllKnownLayers from "./Customizations/AllKnownLayers";
|
||||
import State from "./State";
|
||||
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
|
||||
import Wikidata from "./Logic/Web/Wikidata";
|
||||
import WikidataSearchBox from "./UI/Wikipedia/WikidataSearchBox";
|
||||
|
||||
State.state = new State(AllKnownLayouts.allKnownLayouts.get("charging_stations"))
|
||||
State.state.changes.pendingChanges.setData([])
|
||||
const geojson = {
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [51.0, 4]
|
||||
},
|
||||
properties:
|
||||
{
|
||||
id: "node/42",
|
||||
amenity: "charging_station",
|
||||
}
|
||||
}
|
||||
State.state.allElements.addOrGetElement(geojson)
|
||||
const tags = State.state.allElements.getEventSourceById("node/42")
|
||||
new FeatureInfoBox(
|
||||
tags,
|
||||
AllKnownLayers.sharedLayers.get("charging_station")
|
||||
).AttachTo("maindiv")
|
||||
new WikidataSearchBox({searchText: new UIEventSource("Brugge")}).AttachTo("maindiv")
|
Loading…
Reference in a new issue