forked from MapComplete/MapComplete
This commit is contained in:
parent
3a2addbddc
commit
d5430891bf
22 changed files with 580 additions and 507 deletions
|
@ -124,6 +124,7 @@
|
|||
"helperArgs": [
|
||||
"name",
|
||||
{
|
||||
"multiple": "yes",
|
||||
"notInstanceOf": [
|
||||
"Q79007",
|
||||
"Q22698"
|
||||
|
|
|
@ -78,7 +78,14 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
return new SvelteUIElement(Wikimedia_commons_white).SetStyle("width:2em;height: 2em")
|
||||
}
|
||||
|
||||
public PrepUrl(value: string): ProvidedImage {
|
||||
public PrepUrl(value: NonNullable<string>): ProvidedImage
|
||||
public PrepUrl(value: undefined): undefined
|
||||
|
||||
public PrepUrl(value: string): ProvidedImage
|
||||
public PrepUrl(value: string | undefined): ProvidedImage | undefined{
|
||||
if(value === undefined){
|
||||
return undefined
|
||||
}
|
||||
value = WikimediaImageProvider.removeCommonsPrefix(value)
|
||||
|
||||
if (value.startsWith("File:")) {
|
||||
|
|
|
@ -119,6 +119,8 @@ export interface WikidataAdvancedSearchoptions extends WikidataSearchoptions {
|
|||
notInstanceOf?: number[]
|
||||
}
|
||||
|
||||
interface SparqlResult {results: { bindings: {item, label, description, num}[] }}
|
||||
|
||||
/**
|
||||
* Utility functions around wikidata
|
||||
*/
|
||||
|
@ -202,7 +204,7 @@ export default class Wikidata {
|
|||
} ORDER BY ASC(?num) LIMIT ${options?.maxCount ?? 20}`
|
||||
const url = wds.sparqlQuery(sparql)
|
||||
|
||||
const result = await Utils.downloadJson(url)
|
||||
const result = await Utils.downloadJson<SparqlResult>(url)
|
||||
/*The full uri of the wikidata-item*/
|
||||
|
||||
return result.results.bindings.map(({ item, label, description, num }) => ({
|
||||
|
@ -389,7 +391,7 @@ export default class Wikidata {
|
|||
' SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE]". }\n' +
|
||||
"}"
|
||||
const url = wds.sparqlQuery(query)
|
||||
const result = await Utils.downloadJsonCached(url, 24 * 60 * 60 * 1000)
|
||||
const result = await Utils.downloadJsonCached<SparqlResult>(url, 24 * 60 * 60 * 1000)
|
||||
return result.results.bindings
|
||||
}
|
||||
|
||||
|
@ -420,7 +422,7 @@ export default class Wikidata {
|
|||
}
|
||||
|
||||
const url = "https://www.wikidata.org/wiki/Special:EntityData/" + id + ".json"
|
||||
const entities = (await Utils.downloadJsonCached(url, 10000)).entities
|
||||
const entities = (await Utils.downloadJsonCached<{entities}>(url, 10000)).entities
|
||||
const firstKey = <string>Array.from(Object.keys(entities))[0] // Roundabout way to fetch the entity; it might have been a redirect
|
||||
const response = entities[firstKey]
|
||||
|
||||
|
|
|
@ -215,7 +215,7 @@ export default class Wikipedia {
|
|||
}
|
||||
|
||||
private async GetArticleUncachedAsync(pageName: string): Promise<string> {
|
||||
const response = await Utils.downloadJson(this.getDataUrl(pageName))
|
||||
const response = await Utils.downloadJson<any>(this.getDataUrl(pageName))
|
||||
if (response?.parse?.text === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
|
|
@ -289,6 +289,11 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
|
|||
* group: expert
|
||||
*/
|
||||
postfixDistinguished?: string
|
||||
/**
|
||||
* Extra arguments to configure the input element
|
||||
* group: hidden
|
||||
*/
|
||||
helperArgs: any
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -69,6 +69,7 @@ export default class TagRenderingConfig {
|
|||
readonly inline: boolean
|
||||
readonly default?: string
|
||||
readonly postfixDistinguished?: string
|
||||
readonly args?: any
|
||||
}
|
||||
|
||||
public readonly multiAnswer: boolean
|
||||
|
@ -203,6 +204,7 @@ export default class TagRenderingConfig {
|
|||
inline: json.freeform.inline ?? false,
|
||||
default: json.freeform.default,
|
||||
postfixDistinguished: json.freeform.postfixDistinguished?.trim(),
|
||||
args: json.freeform.helperArgs
|
||||
}
|
||||
if (json.freeform["extraTags"] !== undefined) {
|
||||
throw `Freeform.extraTags is defined. This should probably be 'freeform.addExtraTag' (at ${context})`
|
||||
|
|
|
@ -26,7 +26,7 @@ export default class SvelteUIElement<
|
|||
|
||||
constructor(svelteElement, props?: Props, events?: Events, slots?: Slots) {
|
||||
super()
|
||||
this._svelteComponent = svelteElement
|
||||
this._svelteComponent = <any> svelteElement
|
||||
this._props = props ?? <Props>{}
|
||||
this._events = events
|
||||
this._slots = slots
|
||||
|
|
59
src/UI/BigComponents/SearchField.svelte
Normal file
59
src/UI/BigComponents/SearchField.svelte
Normal file
|
@ -0,0 +1,59 @@
|
|||
<script lang="ts">
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import Hotkeys from "../Base/Hotkeys"
|
||||
import { createEventDispatcher, onDestroy } from "svelte"
|
||||
import { placeholder } from "../../Utils/placeholder"
|
||||
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { ariaLabel } from "../../Utils/ariaLabel"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
|
||||
|
||||
const dispatch = createEventDispatcher<{search: string}>()
|
||||
|
||||
export let searchValue: UIEventSource<string>
|
||||
export let placeholderText: Translation = Translations.t.general.search.search
|
||||
export let feedback = new UIEventSource<string>(undefined)
|
||||
|
||||
|
||||
let isRunning: boolean = false
|
||||
|
||||
let inputElement: HTMLInputElement
|
||||
|
||||
function _performSearch(){
|
||||
dispatch("search", searchValue.data)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="normal-background flex justify-between rounded-full">
|
||||
<form class="flex w-full flex-wrap" on:submit|preventDefault={() => {}}>
|
||||
{#if isRunning}
|
||||
<Loading>{Translations.t.general.search.searching}</Loading>
|
||||
{:else}
|
||||
<div class="flex w-full border border-gray-300 rounded-full">
|
||||
|
||||
<input
|
||||
type="search"
|
||||
class="w-full outline-none mx-2"
|
||||
bind:this={inputElement}
|
||||
on:keypress={(keypr) => {
|
||||
feedback.set(undefined)
|
||||
return keypr.key === "Enter" ? _performSearch() : undefined
|
||||
}}
|
||||
bind:value={$searchValue}
|
||||
use:placeholder={placeholderText}
|
||||
use:ariaLabel={Translations.t.general.search.search}
|
||||
/>
|
||||
<SearchIcon aria-hidden="true" class="h-6 w-6 self-end" on:click={event => _performSearch()} />
|
||||
</div>
|
||||
{#if $feedback !== undefined}
|
||||
<!-- The feedback is _always_ shown for screenreaders and to make sure that the searchfield can still be selected by tabbing-->
|
||||
<div class="alert" role="alert" aria-live="assertive">
|
||||
{$feedback}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
145
src/UI/InputElement/Helpers/WikidataInput.svelte
Normal file
145
src/UI/InputElement/Helpers/WikidataInput.svelte
Normal file
|
@ -0,0 +1,145 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Allows to search through wikidata and to select one value
|
||||
*/
|
||||
|
||||
|
||||
import Translations from "../../i18n/Translations"
|
||||
import Tr from "../../Base/Tr.svelte"
|
||||
import { ImmutableStore, Store, Stores, UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import Wikidata, { WikidataResponse } from "../../../Logic/Web/Wikidata"
|
||||
import Locale from "../../i18n/Locale"
|
||||
import SearchField from "../../BigComponents/SearchField.svelte"
|
||||
import Loading from "../../Base/Loading.svelte"
|
||||
import Wikidatapreview from "../../Wikipedia/Wikidatapreview.svelte"
|
||||
import { Utils } from "../../../Utils"
|
||||
import WikidataValidator from "../Validators/WikidataValidator"
|
||||
|
||||
const t = Translations.t.general.wikipedia
|
||||
|
||||
export let searchValue = new UIEventSource("Tom boonen")
|
||||
export let placeholder = t.searchWikidata
|
||||
export let allowMultiple = false
|
||||
|
||||
export let notInstanceOf: number[] = []
|
||||
export let instanceOf: number[] = []
|
||||
|
||||
export let value: UIEventSource<string>
|
||||
|
||||
let selectedWikidataSingle: WikidataResponse = undefined
|
||||
let selectedMany: Record<string, boolean> = {}
|
||||
let previouslySeen = new Map<string, WikidataResponse>()
|
||||
|
||||
$:{
|
||||
if (selectedWikidataSingle) {
|
||||
value.setData(selectedWikidataSingle.id)
|
||||
}
|
||||
}
|
||||
|
||||
$:{
|
||||
console.log(selectedMany)
|
||||
const v = []
|
||||
for (const id in selectedMany) {
|
||||
if (selectedMany[id]) {
|
||||
v.push(id)
|
||||
}
|
||||
}
|
||||
value.setData(v.join(";"))
|
||||
}
|
||||
|
||||
|
||||
let tooShort = new ImmutableStore<{ success: WikidataResponse[] }>({ success: undefined })
|
||||
|
||||
let searchResult: Store<{ success?: WikidataResponse[]; error?: any }> = searchValue
|
||||
.bind((searchText) => {
|
||||
if (searchText.length < 3 && !searchText.match(/[qQ][0-9]+/)) {
|
||||
return tooShort
|
||||
}
|
||||
const lang = Locale.language.data
|
||||
const key = lang + ":" + searchText
|
||||
let promise = WikidataValidator._searchCache.get(key)
|
||||
if (promise === undefined) {
|
||||
promise = Wikidata.searchAndFetch(searchText, {
|
||||
lang,
|
||||
maxCount: 5,
|
||||
notInstanceOf,
|
||||
instanceOf
|
||||
})
|
||||
WikidataValidator._searchCache.set(key, promise)
|
||||
}
|
||||
return Stores.FromPromiseWithErr(promise)
|
||||
})
|
||||
|
||||
let selectedWithoutSearch: Store<WikidataResponse[]> = searchResult.map(sr => {
|
||||
for (const wikidataItem of sr?.success ?? []) {
|
||||
console.log("Saving", wikidataItem.id)
|
||||
previouslySeen.set(wikidataItem.id, wikidataItem)
|
||||
}
|
||||
let knownIds: Set<string> = new Set(sr?.success?.map(item => item.id))
|
||||
const seen = [selectedWikidataSingle]
|
||||
for (const id in selectedMany) {
|
||||
if (selectedMany[id]) {
|
||||
const item = previouslySeen.get(id)
|
||||
seen.push(item)
|
||||
}
|
||||
}
|
||||
return Utils.NoNull(seen).filter(i => !knownIds.has(i.id))
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<h3>
|
||||
<Tr t={Translations.t.general.wikipedia.searchWikidata} />
|
||||
</h3>
|
||||
|
||||
<form>
|
||||
<SearchField {searchValue} placeholderText={placeholder}></SearchField>
|
||||
|
||||
{#if $searchValue.trim().length === 0}
|
||||
<Tr cls="w-full flex justify-center p-4" t={ t.doSearch} />
|
||||
{:else if $searchValue.trim().length < 3}
|
||||
<Tr t={ t.searchToShort} />
|
||||
{:else if $searchResult === undefined}
|
||||
<div class="w-full flex justify-center p-4">
|
||||
<Loading>
|
||||
<Tr t={Translations.t.general.loading} />
|
||||
</Loading>
|
||||
</div>
|
||||
{:else if $searchResult.error !== undefined}
|
||||
<div class="w-full flex justify-center p-4">
|
||||
<Tr cls="alert" t={t.failed} />
|
||||
</div>
|
||||
{:else if $searchResult.success}
|
||||
{#if $searchResult.success.length === 0}
|
||||
<Tr cls="w-full flex justify-center p-4" t={ t.noResults.Subs({search: $searchValue})} />
|
||||
{:else}
|
||||
{#each $searchResult.success as wikidata}
|
||||
|
||||
<label class="low-interaction m-4 p-2 rounded-xl flex items-center">
|
||||
{#if allowMultiple}
|
||||
<input type="checkbox" bind:checked={selectedMany[wikidata.id]} />
|
||||
{:else}
|
||||
<input type="radio" name="selectedWikidata" value={wikidata} bind:group={selectedWikidataSingle} />
|
||||
{/if}
|
||||
|
||||
<Wikidatapreview {wikidata} />
|
||||
</label>
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#each $selectedWithoutSearch as wikidata}
|
||||
|
||||
<label class="low-interaction m-4 p-2 rounded-xl flex items-center">
|
||||
{#if allowMultiple}
|
||||
<input type="checkbox" bind:checked={selectedMany[wikidata.id]} />
|
||||
{:else}
|
||||
<input type="radio" name="selectedWikidata" value={wikidata} bind:group={selectedWikidataSingle} />
|
||||
{/if}
|
||||
|
||||
<Wikidatapreview {wikidata} />
|
||||
</label>
|
||||
{/each}
|
||||
|
||||
</form>
|
||||
|
|
@ -19,6 +19,8 @@
|
|||
import OpeningHoursInput from "./Helpers/OpeningHoursInput.svelte"
|
||||
import SlopeInput from "./Helpers/SlopeInput.svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import WikidataInput from "./Helpers/WikidataInput.svelte"
|
||||
import WikidataInputHelper from "./WikidataInputHelper.svelte"
|
||||
|
||||
export let type: ValidatorType
|
||||
export let value: UIEventSource<string | object>
|
||||
|
@ -26,17 +28,13 @@
|
|||
export let feature: Feature
|
||||
export let args: (string | number | boolean)[] = undefined
|
||||
export let state: SpecialVisualizationState
|
||||
export let helperArgs: (string | number | boolean)[]
|
||||
export let key: string
|
||||
export let extraTags: UIEventSource<Record<string, string>>
|
||||
|
||||
let properties = { feature, args: args ?? [] }
|
||||
</script>
|
||||
|
||||
{#if type === "translation"}
|
||||
<TranslationInput {value} on:submit {args} />
|
||||
{:else if type === "direction"}
|
||||
<DirectionInput {value} mapProperties={InputHelpers.constructMapProperties(properties)} />
|
||||
<DirectionInput {value} mapProperties={InputHelpers.constructMapProperties( { feature, args: args ?? [] })} />
|
||||
{:else if type === "date"}
|
||||
<DateInput {value} />
|
||||
{:else if type === "color"}
|
||||
|
@ -52,5 +50,5 @@
|
|||
{:else if type === "slope"}
|
||||
<SlopeInput {value} {feature} {state} />
|
||||
{:else if type === "wikidata"}
|
||||
<ToSvelte construct={() => InputHelpers.constructWikidataHelper(value, properties)} />
|
||||
<WikidataInputHelper {value} {feature} {state} {args}/>
|
||||
{/if}
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
|
||||
import { MapProperties } from "../../Models/MapProperties"
|
||||
import WikidataSearchBox from "../Wikipedia/WikidataSearchBox"
|
||||
import Wikidata from "../../Logic/Web/Wikidata"
|
||||
import { Utils } from "../../Utils"
|
||||
import Locale from "../i18n/Locale"
|
||||
import { Feature } from "geojson"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
|
||||
|
@ -68,67 +64,5 @@ export default class InputHelpers {
|
|||
return mapProperties
|
||||
}
|
||||
|
||||
public static constructWikidataHelper(
|
||||
value: UIEventSource<string>,
|
||||
props: InputHelperProperties
|
||||
) {
|
||||
const inputHelperOptions = props
|
||||
const args = inputHelperOptions.args ?? []
|
||||
const searchKey: string = <string>args[0] ?? "name"
|
||||
|
||||
const searchFor: string =
|
||||
searchKey
|
||||
.split(";")
|
||||
.map((k) => inputHelperOptions.feature?.properties[k]?.toLowerCase())
|
||||
.find((foundValue) => !!foundValue) ?? ""
|
||||
|
||||
let searchForValue: UIEventSource<string> = new UIEventSource(searchFor)
|
||||
const options: any = args[1]
|
||||
if (searchFor !== undefined && options !== undefined) {
|
||||
const prefixes = <string[] | Record<string, string[]>>options["removePrefixes"] ?? []
|
||||
const postfixes = <string[] | Record<string, string[]>>options["removePostfixes"] ?? []
|
||||
const defaultValueCandidate = Locale.language.map((lg) => {
|
||||
const prefixesUnrwapped: RegExp[] = (
|
||||
Array.isArray(prefixes) ? prefixes : prefixes[lg] ?? []
|
||||
).map((s) => new RegExp("^" + s, "i"))
|
||||
const postfixesUnwrapped: RegExp[] = (
|
||||
Array.isArray(postfixes) ? postfixes : postfixes[lg] ?? []
|
||||
).map((s) => new RegExp(s + "$", "i"))
|
||||
let clipped = searchFor
|
||||
|
||||
for (const postfix of postfixesUnwrapped) {
|
||||
const match = searchFor.match(postfix)
|
||||
if (match !== null) {
|
||||
clipped = searchFor.substring(0, searchFor.length - match[0].length)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for (const prefix of prefixesUnrwapped) {
|
||||
const match = searchFor.match(prefix)
|
||||
if (match !== null) {
|
||||
clipped = searchFor.substring(match[0].length)
|
||||
break
|
||||
}
|
||||
}
|
||||
return clipped
|
||||
})
|
||||
|
||||
defaultValueCandidate.addCallbackAndRun((clipped) => searchForValue.setData(clipped))
|
||||
}
|
||||
|
||||
let instanceOf: number[] = Utils.NoNull(
|
||||
(options?.instanceOf ?? []).map((i) => Wikidata.QIdToNumber(i))
|
||||
)
|
||||
let notInstanceOf: number[] = Utils.NoNull(
|
||||
(options?.notInstanceOf ?? []).map((i) => Wikidata.QIdToNumber(i))
|
||||
)
|
||||
|
||||
return new WikidataSearchBox({
|
||||
value,
|
||||
searchText: searchForValue,
|
||||
instanceOf,
|
||||
notInstanceOf,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,101 @@
|
|||
import Combine from "../../Base/Combine"
|
||||
import Wikidata from "../../../Logic/Web/Wikidata"
|
||||
import Wikidata, { WikidataResponse } from "../../../Logic/Web/Wikidata"
|
||||
import WikidataSearchBox from "../../Wikipedia/WikidataSearchBox"
|
||||
import { Validator } from "../Validator"
|
||||
import { Translation } from "../../i18n/Translation"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import Title from "../../Base/Title"
|
||||
import Table from "../../Base/Table"
|
||||
|
||||
export default class WikidataValidator extends Validator {
|
||||
public static readonly _searchCache = new Map<string, Promise<WikidataResponse[]>>()
|
||||
|
||||
public static docs = new Combine([
|
||||
new Title("Helper arguments"),
|
||||
new Table(
|
||||
["name", "doc"],
|
||||
[
|
||||
[
|
||||
"key",
|
||||
"the value of this tag will initialize search (default: name). This can be a ';'-separated list in which case every key will be inspected. The non-null value will be used as search",
|
||||
],
|
||||
[
|
||||
"options",
|
||||
new Combine([
|
||||
"A JSON-object of type `{ removePrefixes: string[], removePostfixes: string[] }`.",
|
||||
new Table(
|
||||
["subarg", "doc"],
|
||||
[
|
||||
[
|
||||
"removePrefixes",
|
||||
"remove these snippets of text from the start of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes",
|
||||
],
|
||||
[
|
||||
"removePostfixes",
|
||||
"remove these snippets of text from the end of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes.",
|
||||
],
|
||||
[
|
||||
"instanceOf",
|
||||
"A list of Q-identifier which indicates that the search results _must_ be an entity of this type, e.g. [`Q5`](https://www.wikidata.org/wiki/Q5) for humans",
|
||||
],
|
||||
[
|
||||
"notInstanceof",
|
||||
"A list of Q-identifiers which indicates that the search results _must not_ be an entity of this type, e.g. [`Q79007`](https://www.wikidata.org/wiki/Q79007) to filter away all streets from the search results",
|
||||
],
|
||||
["multiple",
|
||||
"If 'yes' or 'true', will allow to select multiple values at once"]
|
||||
]
|
||||
),
|
||||
]),
|
||||
],
|
||||
]
|
||||
),
|
||||
new Title("Example usage"),
|
||||
`The following is the 'freeform'-part of a layer config which will trigger a search for the wikidata item corresponding with the name of the selected feature. It will also remove '-street', '-square', ... if found at the end of the name
|
||||
|
||||
\`\`\`json
|
||||
"freeform": {
|
||||
"key": "name:etymology:wikidata",
|
||||
"type": "wikidata",
|
||||
"helperArgs": [
|
||||
"name",
|
||||
{
|
||||
"removePostfixes": {"en": [
|
||||
"street",
|
||||
"boulevard",
|
||||
"path",
|
||||
"square",
|
||||
"plaza",
|
||||
],
|
||||
"nl": ["straat","plein","pad","weg",laan"],
|
||||
"fr":["route (de|de la|de l'| de le)"]
|
||||
},
|
||||
|
||||
"#": "Remove streets and parks from the search results:"
|
||||
"notInstanceOf": ["Q79007","Q22698"]
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
Another example is to search for species and trees:
|
||||
|
||||
\`\`\`json
|
||||
"freeform": {
|
||||
"key": "species:wikidata",
|
||||
"type": "wikidata",
|
||||
"helperArgs": [
|
||||
"species",
|
||||
{
|
||||
"instanceOf": [10884, 16521]
|
||||
}]
|
||||
}
|
||||
\`\`\`
|
||||
`,
|
||||
])
|
||||
constructor() {
|
||||
super("wikidata", new Combine(["A wikidata identifier, e.g. Q42.", WikidataSearchBox.docs]))
|
||||
super("wikidata", new Combine(["A wikidata identifier, e.g. Q42.", WikidataValidator.docs]))
|
||||
}
|
||||
|
||||
public isValid(str): boolean {
|
||||
|
@ -44,4 +132,48 @@ export default class WikidataValidator extends Validator {
|
|||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param searchTerm
|
||||
* @param postfixesToRemove
|
||||
* @param prefixesToRemove
|
||||
* @param language
|
||||
*
|
||||
*
|
||||
* WikidataValidator.removePostAndPrefixes("Elf-Julistraat", [], ["straat", "laan"], "nl") // => "Elf-Juli"
|
||||
* WikidataValidator.removePostAndPrefixes("Elf-Julistraat", [], {"nl":["straat", "laan"], "en": ["street"]}, "nl") // => "Elf-Juli"
|
||||
* WikidataValidator.removePostAndPrefixes("Elf-Julistraat", [], {"nl":["straat", "laan"], "en": ["street"]}, "en") // => "Elf-Julistraat"
|
||||
*/
|
||||
public static removePostAndPrefixes(searchTerm: string, prefixesToRemove: string[] | Record<string, string[]>, postfixesToRemove: string[] | Record<string, string[]>, language: string): string {
|
||||
const prefixes = prefixesToRemove
|
||||
const postfixes = postfixesToRemove
|
||||
const prefixesUnwrapped: RegExp[] = (
|
||||
Array.isArray(prefixes) ? prefixes : prefixes[language] ?? []
|
||||
).map((s) => new RegExp("^" + s, "i"))
|
||||
|
||||
const postfixesUnwrapped: RegExp[] = (
|
||||
Array.isArray(postfixes) ? postfixes : postfixes[language] ?? []
|
||||
).map((s) => new RegExp(s + "$", "i"))
|
||||
|
||||
|
||||
let clipped = searchTerm.trim()
|
||||
|
||||
for (const postfix of postfixesUnwrapped) {
|
||||
const match = searchTerm.trim().match(postfix)
|
||||
if (match !== null) {
|
||||
clipped = searchTerm.trim().substring(0, searchTerm.trim().length - match[0].length)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for (const prefix of prefixesUnwrapped) {
|
||||
const match = searchTerm.trim().match(prefix)
|
||||
if (match !== null) {
|
||||
clipped = searchTerm.trim().substring(match[0].length)
|
||||
break
|
||||
}
|
||||
}
|
||||
return clipped
|
||||
}
|
||||
}
|
||||
|
|
58
src/UI/InputElement/WikidataInputHelper.svelte
Normal file
58
src/UI/InputElement/WikidataInputHelper.svelte
Normal file
|
@ -0,0 +1,58 @@
|
|||
<script lang="ts">/**
|
||||
*
|
||||
Wrapper around 'WikidataInput.svelte' which handles the arguments
|
||||
*/
|
||||
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Locale from "../i18n/Locale"
|
||||
import { Utils } from "../../Utils"
|
||||
import Wikidata from "../../Logic/Web/Wikidata"
|
||||
import WikidataInput from "./Helpers/WikidataInput.svelte"
|
||||
import type { Feature } from "geojson"
|
||||
import { onDestroy } from "svelte"
|
||||
import WikidataValidator from "./Validators/WikidataValidator"
|
||||
|
||||
export let args: (string | number | boolean)[] = []
|
||||
export let feature: Feature
|
||||
|
||||
export let value: UIEventSource<string>
|
||||
|
||||
let searchKey: string = <string>args[0] ?? "name"
|
||||
|
||||
let searchFor: string =
|
||||
searchKey
|
||||
.split(";")
|
||||
.map((k) => feature?.properties[k]?.toLowerCase())
|
||||
.find((foundValue) => !!foundValue) ?? ""
|
||||
|
||||
const options: any = args[1]
|
||||
console.log(">>>", args)
|
||||
|
||||
let searchForValue: UIEventSource<string> = new UIEventSource(searchFor)
|
||||
|
||||
onDestroy(
|
||||
Locale.language.addCallbackAndRunD(lg => {
|
||||
console.log(options)
|
||||
if (searchFor !== undefined && options !== undefined) {
|
||||
const post = options["removePostfixes"] ?? []
|
||||
const pre = options["removePrefixes"] ?? []
|
||||
const clipped = WikidataValidator.removePostAndPrefixes(searchFor, pre, post, lg)
|
||||
console.log("Got clipped value:", clipped, post, pre)
|
||||
searchForValue.setData(clipped)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
let instanceOf: number[] = Utils.NoNull(
|
||||
(options?.instanceOf ?? []).map((i) => Wikidata.QIdToNumber(i))
|
||||
)
|
||||
let notInstanceOf: number[] = Utils.NoNull(
|
||||
(options?.notInstanceOf ?? []).map((i) => Wikidata.QIdToNumber(i))
|
||||
)
|
||||
|
||||
let allowMultipleArg = options?.["multiple"]
|
||||
let allowMultiple = allowMultipleArg === "yes" || (""+allowMultipleArg) === "true"
|
||||
</script>
|
||||
|
||||
<WikidataInput searchValue={searchForValue} {value} {instanceOf} {notInstanceOf} {allowMultiple}/>
|
|
@ -4,14 +4,13 @@
|
|||
*/
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import type { PlantNetSpeciesMatch } from "../../Logic/Web/PlantNet"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Wikidata from "../../Logic/Web/Wikidata"
|
||||
import NextButton from "../Base/NextButton.svelte"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import WikidataPreviewBox from "../Wikipedia/WikidataPreviewBox"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import WikidatapreviewWithLoading from "../Wikipedia/WikidatapreviewWithLoading.svelte"
|
||||
|
||||
export let species: PlantNetSpeciesMatch
|
||||
let wikidata = UIEventSource.FromPromise(
|
||||
|
@ -46,16 +45,12 @@
|
|||
/>
|
||||
</Loading>
|
||||
{:else}
|
||||
<ToSvelte
|
||||
construct={() =>
|
||||
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")}
|
||||
/>
|
||||
|
||||
<WikidatapreviewWithLoading wikidataId={wikidataId} imageStyle="max-width: 8rem; width: unset; height: 8rem">
|
||||
<div slot="extra">
|
||||
<Tr cls="thanks w-fit self-center" t={ t.matchPercentage
|
||||
.Subs({ match: Math.round(species.score * 100) })}/>
|
||||
</div>
|
||||
</WikidatapreviewWithLoading>
|
||||
{/if}
|
||||
</NextButton>
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
export let unvalidatedText: UIEventSource<string> = new UIEventSource<string>(value.data)
|
||||
export let config: TagRenderingConfig
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
export let extraTags: UIEventSource<Record<string, string>>
|
||||
|
||||
export let feature: Feature = undefined
|
||||
export let state: SpecialVisualizationState
|
||||
|
@ -28,8 +27,6 @@
|
|||
inline = false
|
||||
inline = config.freeform?.inline
|
||||
}
|
||||
let helperArgs = config.freeform?.helperArgs
|
||||
let key = config.freeform?.key
|
||||
|
||||
const dispatch = createEventDispatcher<{ selected }>()
|
||||
export let feedback: UIEventSource<Translation>
|
||||
|
@ -73,14 +70,11 @@
|
|||
{/if}
|
||||
|
||||
<InputHelper
|
||||
args={config.freeform.helperArgs}
|
||||
args={config.freeform.args}
|
||||
{feature}
|
||||
type={config.freeform.type}
|
||||
{value}
|
||||
{state}
|
||||
{helperArgs}
|
||||
{key}
|
||||
{extraTags}
|
||||
on:submit
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,34 +1,21 @@
|
|||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import Wikidata, { WikidataResponse } from "../../Logic/Web/Wikidata"
|
||||
import { Translation, TypedTranslation } from "../i18n/Translation"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import Loading from "../Base/Loading"
|
||||
import { TypedTranslation } from "../i18n/Translation"
|
||||
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 BaseUIElement from "../BaseUIElement"
|
||||
import { Utils } from "../../Utils"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import { default as Wikidata_icon } from "../../assets/svg/Wikidata.svelte"
|
||||
import Gender_male from "../../assets/svg/Gender_male.svelte"
|
||||
import Gender_female from "../../assets/svg/Gender_female.svelte"
|
||||
import Gender_inter from "../../assets/svg/Gender_inter.svelte"
|
||||
import Gender_trans from "../../assets/svg/Gender_trans.svelte"
|
||||
import Gender_queer from "../../assets/svg/Gender_queer.svelte"
|
||||
|
||||
export default class WikidataPreviewBox extends VariableUiElement {
|
||||
|
||||
export default class WikidataPreviewBox {
|
||||
private static isHuman = [{ p: 31 /*is a*/, q: 5 /* human */ }]
|
||||
// @ts-ignore
|
||||
private static extraProperties: {
|
||||
public static extraProperties: {
|
||||
requires?: { p: number; q?: number }[]
|
||||
property: string
|
||||
textMode?: Map<string, string>
|
||||
display:
|
||||
| TypedTranslation<{ value }>
|
||||
| Map<string, string | (() => BaseUIElement) /*If translation: Subs({value: * }) */>
|
||||
textMode?: Map<string, string>
|
||||
| Map<string, any>,
|
||||
}[] = [
|
||||
{
|
||||
requires: WikidataPreviewBox.isHuman,
|
||||
|
@ -36,34 +23,28 @@ export default class WikidataPreviewBox extends VariableUiElement {
|
|||
display: new Map([
|
||||
[
|
||||
"Q6581097",
|
||||
() => new SvelteUIElement(Gender_male).SetStyle("width: 1rem; height: auto"),
|
||||
Gender_male
|
||||
],
|
||||
[
|
||||
"Q6581072",
|
||||
() => new SvelteUIElement(Gender_female).SetStyle("width: 1rem; height: auto"),
|
||||
Gender_female
|
||||
],
|
||||
[
|
||||
"Q1097630",
|
||||
() => new SvelteUIElement(Gender_inter).SetStyle("width: 1rem; height: auto"),
|
||||
Gender_inter
|
||||
],
|
||||
[
|
||||
"Q1052281",
|
||||
() =>
|
||||
new SvelteUIElement(Gender_trans).SetStyle(
|
||||
"width: 1rem; height: auto"
|
||||
) /*'transwomen'*/,
|
||||
Gender_trans /*'transwomen'*/
|
||||
],
|
||||
[
|
||||
"Q2449503",
|
||||
() =>
|
||||
new SvelteUIElement(Gender_trans).SetStyle(
|
||||
"width: 1rem; height: auto"
|
||||
) /*'transmen'*/,
|
||||
Gender_trans /*'transmen'*/
|
||||
],
|
||||
[
|
||||
"Q48270",
|
||||
() => new SvelteUIElement(Gender_queer).SetStyle("width: 1rem; height: auto"),
|
||||
],
|
||||
Gender_queer
|
||||
]
|
||||
]),
|
||||
textMode: new Map([
|
||||
["Q6581097", "♂️"],
|
||||
|
@ -71,158 +52,19 @@ export default class WikidataPreviewBox extends VariableUiElement {
|
|||
["Q1097630", "⚥️"],
|
||||
["Q1052281", "🏳️⚧️" /*'transwomen'*/],
|
||||
["Q2449503", "🏳️⚧️" /*'transmen'*/],
|
||||
["Q48270", "🏳️🌈 ⚧"],
|
||||
]),
|
||||
["Q48270", "🏳️🌈 ⚧"]
|
||||
])
|
||||
},
|
||||
{
|
||||
property: "P569",
|
||||
requires: WikidataPreviewBox.isHuman,
|
||||
display: Translations.t.general.wikipedia.previewbox.born,
|
||||
display: Translations.t.general.wikipedia.previewbox.born
|
||||
},
|
||||
{
|
||||
property: "P570",
|
||||
requires: WikidataPreviewBox.isHuman,
|
||||
display: Translations.t.general.wikipedia.previewbox.died,
|
||||
},
|
||||
display: Translations.t.general.wikipedia.previewbox.died
|
||||
}
|
||||
]
|
||||
|
||||
constructor(
|
||||
wikidataId: Store<string>,
|
||||
options?: {
|
||||
noImages?: boolean
|
||||
imageStyle?: string
|
||||
whileLoading?: BaseUIElement | string
|
||||
extraItems?: (BaseUIElement | 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 options?.whileLoading
|
||||
}
|
||||
|
||||
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"]
|
||||
console.log(">>>> got wikidata", wikidata)
|
||||
return WikidataPreviewBox.WikidataResponsePreview(wikidata, options)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
public static WikidataResponsePreview(
|
||||
wikidata: WikidataResponse,
|
||||
options?: {
|
||||
noImages?: boolean
|
||||
imageStyle?: string
|
||||
extraItems?: (BaseUIElement | string)[]
|
||||
}
|
||||
): BaseUIElement {
|
||||
console.log(">>> constructing wikidata preview box", wikidata.labels)
|
||||
|
||||
const link = new Link(
|
||||
new Combine([
|
||||
wikidata.id,
|
||||
options?.noImages
|
||||
? wikidata.id
|
||||
: new SvelteUIElement(Wikidata_icon)
|
||||
.SetStyle("width: 2.5rem")
|
||||
.SetClass("block"),
|
||||
]).SetClass("flex"),
|
||||
Wikidata.IdToArticle(wikidata.id),
|
||||
true
|
||||
)?.SetClass("must-link")
|
||||
let info = new Combine([
|
||||
new Combine([
|
||||
Translation.fromMap(wikidata.labels)?.SetClass("font-bold"),
|
||||
link,
|
||||
]).SetClass("flex justify-between flex-wrap-reverse"),
|
||||
Translation.fromMap(wikidata.descriptions, true),
|
||||
WikidataPreviewBox.QuickFacts(wikidata, options),
|
||||
...(options?.extraItems ?? []),
|
||||
]).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 && !options?.noImages) {
|
||||
imageUrl = WikimediaImageProvider.singleton.PrepUrl(imageUrl).url
|
||||
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("p-2 w-full")
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
public static QuickFacts(
|
||||
wikidata: WikidataResponse,
|
||||
options?: { noImages?: boolean }
|
||||
): BaseUIElement {
|
||||
const els: BaseUIElement[] = []
|
||||
for (const extraProperty of WikidataPreviewBox.extraProperties) {
|
||||
let hasAllRequirements = true
|
||||
for (const requirement of extraProperty.requires) {
|
||||
if (!wikidata.claims?.has("P" + requirement.p)) {
|
||||
hasAllRequirements = false
|
||||
break
|
||||
}
|
||||
if (!wikidata.claims?.get("P" + requirement.p).has("Q" + requirement.q)) {
|
||||
hasAllRequirements = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!hasAllRequirements) {
|
||||
continue
|
||||
}
|
||||
|
||||
const key = extraProperty.property
|
||||
const display =
|
||||
(options?.noImages ? extraProperty.textMode : extraProperty.display) ??
|
||||
extraProperty.display
|
||||
if (wikidata.claims?.get(key) === undefined) {
|
||||
continue
|
||||
}
|
||||
const value: string[] = Array.from(wikidata.claims.get(key))
|
||||
|
||||
if (display instanceof Translation) {
|
||||
els.push(display.Subs({ value: value.join(", ") }).SetClass("m-2"))
|
||||
continue
|
||||
}
|
||||
const constructors = Utils.NoNull(value.map((property) => display.get(property)))
|
||||
const elems = constructors.map((v) => {
|
||||
if (typeof v === "string") {
|
||||
return new FixedUiElement(v)
|
||||
} else {
|
||||
return v()
|
||||
}
|
||||
})
|
||||
els.push(new Combine(elems).SetClass("flex m-2"))
|
||||
}
|
||||
if (els.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return new Combine(els).SetClass("flex")
|
||||
}
|
||||
}
|
||||
|
|
49
src/UI/Wikipedia/WikidataQuickfacts.svelte
Normal file
49
src/UI/Wikipedia/WikidataQuickfacts.svelte
Normal file
|
@ -0,0 +1,49 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import WikidataPreviewBox from "./WikidataPreviewBox"
|
||||
import { WikidataResponse } from "../../Logic/Web/Wikidata"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
|
||||
export let wikidata: WikidataResponse
|
||||
|
||||
let propertiesToRender = WikidataPreviewBox.extraProperties.filter(property => {
|
||||
for (const requirement of property.requires) {
|
||||
if (!wikidata.claims?.has("P" + requirement.p)) {
|
||||
return false
|
||||
}
|
||||
if (!wikidata.claims?.get("P" + requirement.p).has("Q" + requirement.q)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const key = property.property
|
||||
if (wikidata.claims?.get(key) === undefined) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
function getProperty(property: {property: string}){
|
||||
const key = property.property
|
||||
const value = Array.from(wikidata.claims?.get(key)).join(", ")
|
||||
return value
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{#if propertiesToRender.length > 0}
|
||||
<div class="flex justify-start items-center">
|
||||
{#each propertiesToRender as property}
|
||||
{#if typeof property.display === "string" }
|
||||
{property.display}
|
||||
{:else if property.display instanceof Translation}
|
||||
<Tr cls="m-2 shrink-0"
|
||||
t={property.display.Subs({value: getProperty(property)}) } />
|
||||
{:else}
|
||||
<svelte:component this={property.display.get(getProperty(property))} class="h-6 w-fit m-1"/>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -1,223 +0,0 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import { InputElement } from "../Input/InputElement"
|
||||
import { TextField } from "../Input/TextField"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { ImmutableStore, Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Wikidata, { WikidataResponse } from "../../Logic/Web/Wikidata"
|
||||
import Locale from "../i18n/Locale"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import WikidataPreviewBox from "./WikidataPreviewBox"
|
||||
import Title from "../Base/Title"
|
||||
import Svg from "../../Svg"
|
||||
import Loading from "../Base/Loading"
|
||||
import Table from "../Base/Table"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import Search from "../../assets/svg/Search.svelte"
|
||||
|
||||
export default class WikidataSearchBox extends InputElement<string> {
|
||||
public static docs = new Combine([
|
||||
new Title("Helper arguments"),
|
||||
new Table(
|
||||
["name", "doc"],
|
||||
[
|
||||
[
|
||||
"key",
|
||||
"the value of this tag will initialize search (default: name). This can be a ';'-separated list in which case every key will be inspected. The non-null value will be used as search",
|
||||
],
|
||||
[
|
||||
"options",
|
||||
new Combine([
|
||||
"A JSON-object of type `{ removePrefixes: string[], removePostfixes: string[] }`.",
|
||||
new Table(
|
||||
["subarg", "doc"],
|
||||
[
|
||||
[
|
||||
"removePrefixes",
|
||||
"remove these snippets of text from the start of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes",
|
||||
],
|
||||
[
|
||||
"removePostfixes",
|
||||
"remove these snippets of text from the end of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes.",
|
||||
],
|
||||
[
|
||||
"instanceOf",
|
||||
"A list of Q-identifier which indicates that the search results _must_ be an entity of this type, e.g. [`Q5`](https://www.wikidata.org/wiki/Q5) for humans",
|
||||
],
|
||||
[
|
||||
"notInstanceof",
|
||||
"A list of Q-identifiers which indicates that the search results _must not_ be an entity of this type, e.g. [`Q79007`](https://www.wikidata.org/wiki/Q79007) to filter away all streets from the search results",
|
||||
],
|
||||
]
|
||||
),
|
||||
]),
|
||||
],
|
||||
]
|
||||
),
|
||||
new Title("Example usage"),
|
||||
`The following is the 'freeform'-part of a layer config which will trigger a search for the wikidata item corresponding with the name of the selected feature. It will also remove '-street', '-square', ... if found at the end of the name
|
||||
|
||||
\`\`\`json
|
||||
"freeform": {
|
||||
"key": "name:etymology:wikidata",
|
||||
"type": "wikidata",
|
||||
"helperArgs": [
|
||||
"name",
|
||||
{
|
||||
"removePostfixes": {"en": [
|
||||
"street",
|
||||
"boulevard",
|
||||
"path",
|
||||
"square",
|
||||
"plaza",
|
||||
],
|
||||
"nl": ["straat","plein","pad","weg",laan"],
|
||||
"fr":["route (de|de la|de l'| de le)"]
|
||||
},
|
||||
|
||||
"#": "Remove streets and parks from the search results:"
|
||||
"notInstanceOf": ["Q79007","Q22698"]
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
Another example is to search for species and trees:
|
||||
|
||||
\`\`\`json
|
||||
"freeform": {
|
||||
"key": "species:wikidata",
|
||||
"type": "wikidata",
|
||||
"helperArgs": [
|
||||
"species",
|
||||
{
|
||||
"instanceOf": [10884, 16521]
|
||||
}]
|
||||
}
|
||||
\`\`\`
|
||||
`,
|
||||
])
|
||||
private static readonly _searchCache = new Map<string, Promise<WikidataResponse[]>>()
|
||||
private readonly wikidataId: UIEventSource<string>
|
||||
private readonly searchText: UIEventSource<string>
|
||||
private readonly instanceOf?: number[]
|
||||
private readonly notInstanceOf?: number[]
|
||||
|
||||
constructor(options?: {
|
||||
searchText?: UIEventSource<string>
|
||||
value?: UIEventSource<string>
|
||||
notInstanceOf?: number[]
|
||||
instanceOf?: number[]
|
||||
}) {
|
||||
super()
|
||||
this.searchText = options?.searchText
|
||||
this.wikidataId = options?.value ?? new UIEventSource<string>(undefined)
|
||||
this.instanceOf = options?.instanceOf
|
||||
this.notInstanceOf = options?.notInstanceOf
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<string> {
|
||||
return this.wikidataId
|
||||
}
|
||||
|
||||
IsValid(t: string): boolean {
|
||||
return t.startsWith("Q") && !isNaN(Number(t.substring(1)))
|
||||
}
|
||||
|
||||
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 tooShort = new ImmutableStore<{ success: WikidataResponse[] }>({ success: undefined })
|
||||
const searchResult: Store<{ success?: WikidataResponse[]; error?: any }> = searchField
|
||||
.GetValue()
|
||||
.bind((searchText) => {
|
||||
if (searchText.length < 3 && !searchText.match(/[qQ][0-9]+/)) {
|
||||
return tooShort
|
||||
}
|
||||
const lang = Locale.language.data
|
||||
const key = lang + ":" + searchText
|
||||
let promise = WikidataSearchBox._searchCache.get(key)
|
||||
if (promise === undefined) {
|
||||
promise = Wikidata.searchAndFetch(searchText, {
|
||||
lang,
|
||||
maxCount: 5,
|
||||
notInstanceOf: this.notInstanceOf,
|
||||
instanceOf: this.instanceOf,
|
||||
})
|
||||
WikidataSearchBox._searchCache.set(key, promise)
|
||||
}
|
||||
return Stores.FromPromiseWithErr(promise)
|
||||
})
|
||||
|
||||
const previews = new VariableUiElement(
|
||||
searchResult.map(
|
||||
(searchResultsOrFail) => {
|
||||
if (searchField.GetValue().data.length === 0) {
|
||||
return Translations.t.general.wikipedia.doSearch
|
||||
}
|
||||
|
||||
if (searchField.GetValue().data.length < 3) {
|
||||
return Translations.t.general.wikipedia.searchToShort
|
||||
}
|
||||
|
||||
if (searchResultsOrFail === undefined) {
|
||||
return new Loading(Translations.t.general.loading)
|
||||
}
|
||||
|
||||
if (searchResultsOrFail.error !== undefined) {
|
||||
return new Combine([
|
||||
Translations.t.general.wikipedia.failed.Clone().SetClass("alert"),
|
||||
searchResultsOrFail.error,
|
||||
])
|
||||
}
|
||||
|
||||
const searchResults = searchResultsOrFail.success
|
||||
if (searchResults.length === 0) {
|
||||
return Translations.t.general.wikipedia.noResults.Subs({
|
||||
search: searchField.GetValue().data ?? "",
|
||||
})
|
||||
}
|
||||
|
||||
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")
|
||||
},
|
||||
[searchField.GetValue()]
|
||||
)
|
||||
)
|
||||
|
||||
return new Combine([
|
||||
new Title(Translations.t.general.wikipedia.searchWikidata, 3).SetClass("m-2"),
|
||||
new Combine([
|
||||
new SvelteUIElement(Search).SetClass("w-6"),
|
||||
searchField.SetClass("m-2 w-full"),
|
||||
]).SetClass("flex"),
|
||||
previews,
|
||||
])
|
||||
.SetClass("flex flex-col border-2 border-black rounded-xl m-2 p-2")
|
||||
.ConstructElement()
|
||||
}
|
||||
}
|
42
src/UI/Wikipedia/Wikidatapreview.svelte
Normal file
42
src/UI/Wikipedia/Wikidatapreview.svelte
Normal file
|
@ -0,0 +1,42 @@
|
|||
<script lang="ts">
|
||||
|
||||
import Wikidata, { WikidataResponse } from "../../Logic/Web/Wikidata"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import { WikimediaImageProvider } from "../../Logic/ImageProviders/WikimediaImageProvider"
|
||||
import { default as Wikidata_icon } from "../../assets/svg/Wikidata.svelte"
|
||||
import WikidataQuickfacts from "./WikidataQuickfacts.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
|
||||
export let wikidata: WikidataResponse
|
||||
export let imageStyle: string = "max-width: 5rem; width: unset; height: 4rem"
|
||||
|
||||
let imageProperty: string | undefined = Array.from(wikidata?.claims?.get("P18") ?? [])[0]
|
||||
let imageUrl = WikimediaImageProvider.singleton.PrepUrl(imageProperty)?.url
|
||||
|
||||
</script>
|
||||
|
||||
<div class="flex w-full p-2">
|
||||
|
||||
{#if imageUrl}
|
||||
<img src={imageUrl} style={imageStyle} class="mr-2" />
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col w-full">
|
||||
|
||||
<div class="flex w-full justify-between">
|
||||
<Tr cls="font-bold" t={ Translation.fromMap(wikidata.labels) } />
|
||||
<a href={Wikidata.IdToArticle(wikidata.id)} target="_blank" class="flex must-link items-center">
|
||||
<Wikidata_icon class="w-10" /> {wikidata.id}
|
||||
</a>
|
||||
</div>
|
||||
<Tr t={ Translation.fromMap(wikidata.descriptions, true)} />
|
||||
|
||||
|
||||
|
||||
<div class="flex">
|
||||
<WikidataQuickfacts {wikidata} />
|
||||
</div>
|
||||
|
||||
<slot name="extra"></slot>
|
||||
</div>
|
||||
</div>
|
35
src/UI/Wikipedia/WikidatapreviewWithLoading.svelte
Normal file
35
src/UI/Wikipedia/WikidatapreviewWithLoading.svelte
Normal file
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts">
|
||||
import Wikidata, { WikidataResponse } from "../../Logic/Web/Wikidata"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import Wikidatapreview from "./Wikidatapreview.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
|
||||
export let wikidataId: Store<string>
|
||||
export let imageStyle: string = undefined
|
||||
|
||||
let wikidata: Store<{ success: WikidataResponse } | { error: any }> = wikidataId.stabilized(100).bind((id) => {
|
||||
if (id === undefined || id === "" || id === "Q") {
|
||||
return null
|
||||
}
|
||||
return Wikidata.LoadWikidataEntry(id)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
{#if $wikidata === undefined}
|
||||
<Loading>
|
||||
<Tr t={Translations.t.general.loading} />
|
||||
</Loading>
|
||||
{:else if $wikidata["error"]}
|
||||
<div class="alert">
|
||||
{$wikidata["error"]}
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
<Wikidatapreview {imageStyle} wikidata={$wikidata["success"]}>
|
||||
<slot name="extra" slot="extra" />
|
||||
</Wikidatapreview>
|
||||
{/if}
|
|
@ -10,6 +10,7 @@
|
|||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Wikipedia from "../../assets/svg/Wikipedia.svelte"
|
||||
import Wikidatapreview from "./Wikidatapreview.svelte"
|
||||
|
||||
/**
|
||||
* Shows a wikipedia-article + wikidata preview for the given item
|
||||
|
@ -31,9 +32,7 @@
|
|||
{/if}
|
||||
|
||||
{#if $wikipediaDetails.wikidata}
|
||||
<ToSvelte
|
||||
construct={() => WikidataPreviewBox.WikidataResponsePreview($wikipediaDetails.wikidata)}
|
||||
/>
|
||||
<Wikidatapreview wikidata={$wikipediaDetails.wikidata} />
|
||||
{/if}
|
||||
|
||||
{#if $wikipediaDetails.articleUrl}
|
||||
|
|
|
@ -26,9 +26,7 @@ export class Translation extends BaseUIElement {
|
|||
) {
|
||||
super()
|
||||
this._strictLanguages = strictLanguages
|
||||
if (strictLanguages) {
|
||||
console.log(">>> strict:", translations)
|
||||
}
|
||||
|
||||
if (translations === undefined) {
|
||||
console.error("Translation without content at " + context)
|
||||
throw `Translation without content (${context})`
|
||||
|
@ -138,7 +136,6 @@ export class Translation extends BaseUIElement {
|
|||
|
||||
static fromMap(transl: Map<string, string>, strictLanguages: boolean = false) {
|
||||
const translations = {}
|
||||
console.log("Strict:", strictLanguages)
|
||||
let hasTranslation = false
|
||||
transl?.forEach((value, key) => {
|
||||
translations[key] = value
|
||||
|
|
Loading…
Reference in a new issue