Add a wikidata search box

This commit is contained in:
pietervdvn 2021-10-08 04:33:39 +02:00
parent 54bc4f24da
commit b5a2ee1757
21 changed files with 4141 additions and 3590 deletions

View file

@ -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) {

View file

@ -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))]
}

View file

@ -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)
}
})

View file

@ -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])

View file

@ -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'

View file

@ -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

View file

@ -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 => {

View file

@ -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

View file

@ -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])
}
},
{

View 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
}
}

View 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)));
}
}

View file

@ -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

View file

@ -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()}"

View file

@ -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": []
]
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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": {

View file

@ -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)}"
}
}
}

View file

@ -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
View file

@ -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")