Graciously handle multiple entries in wikidata for fetching images and showing articles, verious bug fixes

This commit is contained in:
Pieter Vander Vennet 2021-10-07 22:06:47 +02:00
parent 8d52ef1106
commit a996ba2a7c
14 changed files with 231 additions and 90 deletions

View file

@ -1,17 +1,20 @@
import {UIEventSource} from "../UIEventSource"; import {UIEventSource} from "../UIEventSource";
import BaseUIElement from "../../UI/BaseUIElement"; import BaseUIElement from "../../UI/BaseUIElement";
import {LicenseInfo} from "./LicenseInfo"; import {LicenseInfo} from "./LicenseInfo";
import {Utils} from "../../Utils";
export interface ProvidedImage { export interface ProvidedImage {
url: string, key: string, provider: ImageProvider url: string,
key: string,
provider: ImageProvider
} }
export default abstract class ImageProvider { export default abstract class ImageProvider {
public abstract readonly defaultKeyPrefixes : string[] = ["mapillary", "image"] public abstract readonly defaultKeyPrefixes: string[] = ["mapillary", "image"]
private _cache = new Map<string, UIEventSource<LicenseInfo>>() private _cache = new Map<string, UIEventSource<LicenseInfo>>()
GetAttributionFor(url: string): UIEventSource<LicenseInfo> { GetAttributionFor(url: string): UIEventSource<LicenseInfo> {
const cached = this._cache.get(url); const cached = this._cache.get(url);
if (cached !== undefined) { if (cached !== undefined) {
@ -31,43 +34,49 @@ export default abstract class ImageProvider {
*/ */
public GetRelevantUrls(allTags: UIEventSource<any>, options?: { public GetRelevantUrls(allTags: UIEventSource<any>, options?: {
prefixes?: string[] prefixes?: string[]
}):UIEventSource<ProvidedImage[]> { }): UIEventSource<ProvidedImage[]> {
const prefixes = options?.prefixes ?? this.defaultKeyPrefixes const prefixes = options?.prefixes ?? this.defaultKeyPrefixes
if(prefixes === undefined){ if (prefixes === undefined) {
throw "The image provider"+this.constructor.name+" doesn't define `defaultKeyPrefixes`" throw "The image provider" + this.constructor.name + " doesn't define `defaultKeyPrefixes`"
} }
const relevantUrls = new UIEventSource<{ url: string; key: string; provider: ImageProvider }[]>([]) const relevantUrls = new UIEventSource<{ url: string; key: string; provider: ImageProvider }[]>([])
const seenValues = new Set<string>() const seenValues = new Set<string>()
const self = this const self =this
allTags.addCallbackAndRunD(tags => { allTags.addCallbackAndRunD(tags => {
for (const key in tags) { for (const key in tags) {
if(!prefixes.some(prefix => key.startsWith(prefix))){ if (!prefixes.some(prefix => key.startsWith(prefix))) {
continue continue
} }
const value = tags[key] const values = Utils.NoEmpty(tags[key]?.split(";")?.map(v => v.trim()) ?? [])
if(seenValues.has(value)){ for (const value of values) {
continue
} if (seenValues.has(value)) {
seenValues.add(value) continue
this.ExtractUrls(key, value).then(promises => {
for (const promise of promises ?? []) {
if(promise === undefined){
continue
}
promise.then(providedImage => {
if(providedImage === undefined){
return
}
relevantUrls.data.push(providedImage)
relevantUrls.ping()
})
} }
}) seenValues.add(value)
this.ExtractUrls(key, value).then(promises => {
console.log("Got ", promises.length, "promises for", value,"by",self.constructor.name)
for (const promise of promises ?? []) {
if (promise === undefined) {
continue
}
promise.then(providedImage => {
if (providedImage === undefined) {
return
}
relevantUrls.data.push(providedImage)
relevantUrls.ping()
})
}
})
}
} }
}) })
return relevantUrls return relevantUrls
} }
public abstract ExtractUrls(key: string, value: string) : Promise<Promise<ProvidedImage>[]>; public abstract ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]>;
} }

View file

@ -1,4 +1,5 @@
export class LicenseInfo { export class LicenseInfo {
title: string = ""
artist: string = ""; artist: string = "";
license: string = ""; license: string = "";
licenseShortName: string = ""; licenseShortName: string = "";

View file

@ -65,12 +65,26 @@ export class WikimediaImageProvider extends ImageProvider {
"&format=json&origin=*"; "&format=json&origin=*";
const data = await Utils.downloadJson(url) const data = await Utils.downloadJson(url)
const licenseInfo = new LicenseInfo(); const licenseInfo = new LicenseInfo();
const license = (data.query.pages[-1].imageinfo ?? [])[0]?.extmetadata; const pageInfo = data.query.pages[-1]
if(pageInfo === undefined){
return undefined;
}
const license = (pageInfo.imageinfo ?? [])[0]?.extmetadata;
if (license === undefined) { if (license === undefined) {
console.warn("The file", filename ,"has no usable metedata or license attached... Please fix the license info file yourself!") console.warn("The file", filename ,"has no usable metedata or license attached... Please fix the license info file yourself!")
return undefined; return undefined;
} }
let title = pageInfo.title
if(title.startsWith("File:")){
title= title.substr("File:".length)
}
if(title.endsWith(".jpg") || title.endsWith(".png")){
title = title.substring(0, title.length - 4)
}
licenseInfo.title = title
licenseInfo.artist = license.Artist?.value; licenseInfo.artist = license.Artist?.value;
licenseInfo.license = license.License?.value; licenseInfo.license = license.License?.value;
licenseInfo.copyrighted = license.Copyrighted?.value; licenseInfo.copyrighted = license.Copyrighted?.value;

View file

@ -47,7 +47,7 @@ export default class Wikidata {
const claimsList: any[] = entity.claims[claimId] const claimsList: any[] = entity.claims[claimId]
const values = new Set<string>() const values = new Set<string>()
for (const claim of claimsList) { for (const claim of claimsList) {
const value = claim.mainsnak?.datavalueq?.value; const value = claim.mainsnak?.datavalue?.value;
if(value !== undefined){ if(value !== undefined){
values.add(value) values.add(value)
} }

View file

@ -22,6 +22,10 @@ export default class Wikipedia {
"mw-selflink", "mw-selflink",
"hatnote" // Often redirects "hatnote" // Often redirects
] ]
private static readonly idsToRemove = [
"sjabloon_zie"
]
private static readonly _cache = new Map<string, UIEventSource<{ success: string } | { error: any }>>() private static readonly _cache = new Map<string, UIEventSource<{ success: string } | { error: any }>>()
@ -59,6 +63,13 @@ export default class Wikipedia {
} }
} }
for (const forbiddenId of Wikipedia.idsToRemove) {
const toRemove = content.querySelector("#"+forbiddenId)
toRemove?.parentElement?.removeChild(toRemove)
}
const links = Array.from(content.getElementsByTagName("a")) const links = Array.from(content.getElementsByTagName("a"))
// Rewrite relative links to absolute links + open them in a new tab // Rewrite relative links to absolute links + open them in a new tab

View file

@ -6,11 +6,16 @@ import {VariableUiElement} from "./VariableUIElement";
export class TabbedComponent extends Combine { export class TabbedComponent extends Combine {
constructor(elements: { header: BaseUIElement | string, content: BaseUIElement | string }[], openedTab: (UIEventSource<number> | number) = 0) { constructor(elements: { header: BaseUIElement | string, content: BaseUIElement | string }[],
openedTab: (UIEventSource<number> | number) = 0,
options?: {
leftOfHeader?: BaseUIElement
styleHeader?: (header: BaseUIElement) => void
}) {
const openedTabSrc = typeof (openedTab) === "number" ? new UIEventSource(openedTab) : (openedTab ?? new UIEventSource<number>(0)) const openedTabSrc = typeof (openedTab) === "number" ? new UIEventSource(openedTab) : (openedTab ?? new UIEventSource<number>(0))
const tabs: BaseUIElement[] = [] const tabs: BaseUIElement[] = [options?.leftOfHeader ]
const contentElements: BaseUIElement[] = []; const contentElements: BaseUIElement[] = [];
for (let i = 0; i < elements.length; i++) { for (let i = 0; i < elements.length; i++) {
let element = elements[i]; let element = elements[i];
@ -25,16 +30,19 @@ export class TabbedComponent extends Combine {
} }
}) })
const content = Translations.W(element.content) const content = Translations.W(element.content)
content.SetClass("relative p-4 w-full inline-block") content.SetClass("relative w-full inline-block")
contentElements.push(content); contentElements.push(content);
const tab = header.SetClass("block tab-single-header") const tab = header.SetClass("block tab-single-header")
tabs.push(tab) tabs.push(tab)
} }
const header = new Combine(tabs).SetClass("tabs-header-bar") const header = new Combine(tabs).SetClass("tabs-header-bar")
if(options?.styleHeader){
options.styleHeader(header)
}
const actualContent = new VariableUiElement( const actualContent = new VariableUiElement(
openedTabSrc.map(i => contentElements[i]) openedTabSrc.map(i => contentElements[i])
) ).SetStyle("max-height: inherit; height: inherit")
super([header, actualContent]) super([header, actualContent])
} }

View file

@ -73,6 +73,9 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
} }
); );
tabs.forEach(c => c.content.SetClass("p-4"))
tabsWithAboutMc.forEach(c => c.content.SetClass("p-4"))
return new Toggle( return new Toggle(
new TabbedComponent(tabsWithAboutMc, State.state.welcomeMessageOpenedTab), new TabbedComponent(tabsWithAboutMc, State.state.welcomeMessageOpenedTab),
new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab), new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab),

View file

@ -21,10 +21,11 @@ export default class Attribution extends VariableUiElement {
icon?.SetClass("block left").SetStyle("height: 2em; width: 2em; padding-right: 0.5em;"), icon?.SetClass("block left").SetStyle("height: 2em; width: 2em; padding-right: 0.5em;"),
new Combine([ new Combine([
Translations.W(license?.artist ?? ".").SetClass("block font-bold"), Translations.W(license?.title).SetClass("block"),
Translations.W(license?.artist ?? "").SetClass("block font-bold"),
Translations.W((license?.license ?? "") === "" ? "CC0" : (license?.license ?? "")) Translations.W((license?.license ?? "") === "" ? "CC0" : (license?.license ?? ""))
]).SetClass("flex flex-col") ]).SetClass("flex flex-col")
]).SetClass("flex flex-row bg-black text-white text-sm absolute bottom-0 left-0 p-0.5 pl-5 pr-3 rounded-lg") ]).SetClass("flex flex-row bg-black text-white text-sm absolute bottom-0 left-0 p-0.5 pl-5 pr-3 rounded-lg no-images")
})); }));
} }

View file

@ -100,7 +100,16 @@ export default class SpecialVisualizations {
], ],
example: "`{wikipedia()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the wikipedia page of whom the feature was named after. Also remember that these can be styled, e.g. `{wikipedia():max-height: 10rem}` to limit the height", example: "`{wikipedia()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the wikipedia page of whom the feature was named after. Also remember that these can be styled, e.g. `{wikipedia():max-height: 10rem}` to limit the height",
constr: (_, tagsSource, args) => constr: (_, tagsSource, args) =>
new WikipediaBox( tagsSource.map(tags => tags[args[0]])) new VariableUiElement(
tagsSource.map(tags => tags[args[0]])
.map(wikidata => {
const wikidatas : string[] =
Utils.NoEmpty(wikidata?.split(";")?.map(wd => wd.trim()) ?? [])
return new WikipediaBox(wikidatas)
})
)
}, },
{ {
funcName: "minimap", funcName: "minimap",

View file

@ -10,27 +10,73 @@ import Translations from "./i18n/Translations";
import Svg from "../Svg"; import Svg from "../Svg";
import Wikidata, {WikidataResponse} from "../Logic/Web/Wikidata"; import Wikidata, {WikidataResponse} from "../Logic/Web/Wikidata";
import Locale from "./i18n/Locale"; import Locale from "./i18n/Locale";
import Toggle from "./Input/Toggle";
import Link from "./Base/Link"; import Link from "./Base/Link";
import {TabbedComponent} from "./Base/TabbedComponent";
export default class WikipediaBox extends Toggle { export default class WikipediaBox extends Combine {
constructor(wikidataId: string | UIEventSource<string>) { constructor(wikidataIds: string[]) {
const wp = Translations.t.general.wikipedia;
if (typeof wikidataId === "string") { const mainContents = []
wikidataId = new UIEventSource(wikidataId)
const pages = wikidataIds.map(wdId => WikipediaBox.createLinkedContent(wdId))
if (wikidataIds.length == 1) {
const page = pages[0]
mainContents.push(
new Combine([
new Combine([Svg.wikipedia_ui()
.SetStyle("width: 1.5rem").SetClass("inline-block mr-3"), page.titleElement])
.SetClass("flex"),
page.linkElement
]).SetClass("flex justify-between align-middle"),
)
mainContents.push(page.contents)
} else if (wikidataIds.length > 1) {
const tabbed = new TabbedComponent(
pages.map(page => {
const contents = page.contents.SetClass("block").SetStyle("max-height: inherit; height: inherit; padding-bottom: 3.3rem")
return {
header: page.titleElement.SetClass("pl-2 pr-2"),
content: new Combine([
page.linkElement
.SetStyle("top: 2rem; right: 2.5rem;")
.SetClass("absolute subtle-background rounded-full p-3 opacity-50 hover:opacity-100 transition-opacity"),
contents
]).SetStyle("max-height: inherit; height: inherit").SetClass("relative")
}
}),
0,
{
leftOfHeader: Svg.wikipedia_ui().SetStyle("width: 1.5rem; align-self: center;").SetClass("mr-4"),
styleHeader: header => header.SetClass("subtle-background").SetStyle("height: 3.3rem")
}
)
tabbed.SetStyle("height: inherit; max-height: inherit; overflow: hidden")
mainContents.push(tabbed)
} }
super(mainContents)
this.SetClass("block rounded-xl subtle-background m-1 p-2 flex flex-col")
.SetStyle("max-height: inherit")
}
private static createLinkedContent(wikidataId: string): {
titleElement: BaseUIElement,
contents: BaseUIElement,
linkElement: BaseUIElement
} {
const wp = Translations.t.general.wikipedia;
const wikiLink: UIEventSource<[string, string] | "loading" | "failed" | "no page"> = const wikiLink: UIEventSource<[string, string] | "loading" | "failed" | "no page"> =
wikidataId Wikidata.LoadWikidataEntry(wikidataId)
.bind(id => {
if (id === undefined) {
return undefined
}
return Wikidata.LoadWikidataEntry(id);
})
.map(maybewikidata => { .map(maybewikidata => {
if (maybewikidata === undefined) { if (maybewikidata === undefined) {
return "loading" return "loading"
@ -72,38 +118,39 @@ export default class WikipediaBox extends Toggle {
const [pagetitle, language] = status const [pagetitle, language] = status
return WikipediaBox.createContents(pagetitle, language) return WikipediaBox.createContents(pagetitle, language)
}) })
).SetClass("overflow-auto normal-background rounded-lg") ).SetClass("overflow-auto normal-background rounded-lg")
const linkElement = new VariableUiElement(wikiLink.map(state => { const titleElement = new VariableUiElement(wikiLink.map(state => {
if (typeof state !== "string") { if (typeof state !== "string") {
const [pagetitle, language] = state const [pagetitle, language] = state
const url= `https://${language}.wikipedia.org/wiki/${pagetitle}` return new Title(pagetitle, 3)
}
//return new Title(Translations.t.general.wikipedia.wikipediaboxTitle.Clone(), 2)
return new Title(wikidataId,3)
}))
const linkElement = new VariableUiElement(wikiLink.map(state => {
if (typeof state !== "string") {
const [pagetitle, language] = state
const url = `https://${language}.wikipedia.org/wiki/${pagetitle}`
return new Link(Svg.pop_out_ui().SetStyle("width: 1.2rem").SetClass("block "), url, true) return new Link(Svg.pop_out_ui().SetStyle("width: 1.2rem").SetClass("block "), url, true)
} }
return undefined})) return undefined
.SetClass("flex items-center") }))
.SetClass("flex items-center")
const mainContent = new Combine([ return {
new Combine([ contents: contents,
new Combine([ linkElement: linkElement,
Svg.wikipedia_ui().SetStyle("width: 1.5rem").SetClass("mr-3"), titleElement: titleElement
new Title(Translations.t.general.wikipedia.wikipediaboxTitle.Clone(), 2), }
]).SetClass("flex"),
linkElement
]).SetClass("flex justify-between"),
contents]).SetClass("block rounded-xl subtle-background m-1 p-2 flex flex-col")
.SetStyle("max-height: inherit")
super(
mainContent,
undefined,
wikidataId.map(id => id !== undefined)
)
} }
/** /**
* Returns the actual content in a scrollable way * Returns the actual content in a scrollable way
* @param pagename * @param pagename

View file

@ -852,14 +852,14 @@ video {
margin-right: 0.75rem; margin-right: 0.75rem;
} }
.mb-2 {
margin-bottom: 0.5rem;
}
.mr-4 { .mr-4 {
margin-right: 1rem; margin-right: 1rem;
} }
.mb-2 {
margin-bottom: 0.5rem;
}
.mt-3 { .mt-3 {
margin-top: 0.75rem; margin-top: 0.75rem;
} }
@ -1251,14 +1251,14 @@ video {
border-radius: 0.25rem; border-radius: 0.25rem;
} }
.rounded-lg {
border-radius: 0.5rem;
}
.rounded-xl { .rounded-xl {
border-radius: 0.75rem; border-radius: 0.75rem;
} }
.rounded-lg {
border-radius: 0.5rem;
}
.border { .border {
border-width: 1px; border-width: 1px;
} }
@ -1386,6 +1386,14 @@ video {
padding: 0px; padding: 0px;
} }
.pl-2 {
padding-left: 0.5rem;
}
.pr-2 {
padding-right: 0.5rem;
}
.pl-6 { .pl-6 {
padding-left: 1.5rem; padding-left: 1.5rem;
} }
@ -1394,10 +1402,6 @@ video {
padding-top: 0.5rem; padding-top: 0.5rem;
} }
.pl-2 {
padding-left: 0.5rem;
}
.pt-3 { .pt-3 {
padding-top: 0.75rem; padding-top: 0.75rem;
} }
@ -1458,10 +1462,6 @@ video {
padding-top: 0px; padding-top: 0px;
} }
.pr-2 {
padding-right: 0.5rem;
}
.text-center { .text-center {
text-align: center; text-align: center;
} }
@ -1582,6 +1582,10 @@ video {
text-decoration: underline; text-decoration: underline;
} }
.opacity-50 {
opacity: 0.5;
}
.opacity-0 { .opacity-0 {
opacity: 0; opacity: 0;
} }
@ -1628,6 +1632,12 @@ video {
transition-duration: 150ms; transition-duration: 150ms;
} }
.transition-opacity {
transition-property: opacity;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.\!transition { .\!transition {
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter !important; transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter !important;
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter !important; transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter !important;
@ -1788,6 +1798,10 @@ svg, img {
height: 100%; height: 100%;
} }
.no-images img {
display: none;
}
.mapcontrol svg path { .mapcontrol svg path {
fill: var(--subtle-detail-color-contrast) !important; fill: var(--subtle-detail-color-contrast) !important;
} }
@ -2189,6 +2203,10 @@ li::marker {
color: rgba(30, 64, 175, var(--tw-text-opacity)); color: rgba(30, 64, 175, var(--tw-text-opacity));
} }
.hover\:opacity-100:hover {
opacity: 1;
}
.hover\:shadow-xl:hover { .hover\:shadow-xl:hover {
--tw-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); --tw-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);

View file

@ -8,7 +8,7 @@
justify-content: flex-start; justify-content: flex-start;
align-items: start; align-items: start;
background-color: var(--background-color); background-color: var(--background-color);
max-width: 100vw; max-width: 100%;
overflow-x: auto; overflow-x: auto;
} }

View file

@ -93,6 +93,10 @@ svg, img {
height: 100%; height: 100%;
} }
.no-images img {
display: none;
}
.mapcontrol svg path { .mapcontrol svg path {
fill: var(--subtle-detail-color-contrast) !important; fill: var(--subtle-detail-color-contrast) !important;
} }

File diff suppressed because one or more lines are too long