Refactoring: port wikipedia panel to Svelte
This commit is contained in:
parent
24f7610d0a
commit
d8e14927c8
32 changed files with 362 additions and 847 deletions
|
@ -1,5 +1,5 @@
|
|||
import { Utils } from "../../Utils"
|
||||
import { UIEventSource } from "../UIEventSource"
|
||||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import * as wds from "wikidata-sdk"
|
||||
|
||||
export class WikidataResponse {
|
||||
|
@ -131,11 +131,10 @@ export default class Wikidata {
|
|||
"Lexeme:",
|
||||
].map((str) => str.toLowerCase())
|
||||
|
||||
private static readonly _cache = new Map<
|
||||
private static readonly _storeCache = new Map<
|
||||
string,
|
||||
UIEventSource<{ success: WikidataResponse } | { error: any }>
|
||||
Store<{ success: WikidataResponse } | { error: any }>
|
||||
>()
|
||||
|
||||
/**
|
||||
* Same as LoadWikidataEntry, but wrapped into a UIEventSource
|
||||
* @param value
|
||||
|
@ -143,14 +142,14 @@ export default class Wikidata {
|
|||
*/
|
||||
public static LoadWikidataEntry(
|
||||
value: string | number
|
||||
): UIEventSource<{ success: WikidataResponse } | { error: any }> {
|
||||
): Store<{ success: WikidataResponse } | { error: any }> {
|
||||
const key = this.ExtractKey(value)
|
||||
const cached = Wikidata._cache.get(key)
|
||||
if (cached !== undefined) {
|
||||
const cached = Wikidata._storeCache.get(key)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
const src = UIEventSource.FromPromiseWithErr(Wikidata.LoadWikidataEntryAsync(key))
|
||||
Wikidata._cache.set(key, src)
|
||||
Wikidata._storeCache.set(key, src)
|
||||
return src
|
||||
}
|
||||
|
||||
|
@ -278,6 +277,9 @@ export default class Wikidata {
|
|||
*
|
||||
* Wikidata.ExtractKey("https://www.wikidata.org/wiki/Lexeme:L614072") // => "L614072"
|
||||
* Wikidata.ExtractKey("http://www.wikidata.org/entity/Q55008046") // => "Q55008046"
|
||||
* Wikidata.ExtractKey("Q55008046") // => "Q55008046"
|
||||
* Wikidata.ExtractKey("A55008046") // => undefined
|
||||
* Wikidata.ExtractKey("Q55008046X") // => undefined
|
||||
*/
|
||||
public static ExtractKey(value: string | number): string {
|
||||
if (typeof value === "number") {
|
||||
|
@ -385,11 +387,24 @@ export default class Wikidata {
|
|||
return result.results.bindings
|
||||
}
|
||||
|
||||
private static _cache = new Map<string, Promise<WikidataResponse>>()
|
||||
public static async LoadWikidataEntryAsync(value: string | number): Promise<WikidataResponse> {
|
||||
const key = "" + value
|
||||
const cached = Wikidata._cache.get(key)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
const uncached = Wikidata.LoadWikidataEntryUncachedAsync(value)
|
||||
Wikidata._cache.set(key, uncached)
|
||||
return uncached
|
||||
}
|
||||
/**
|
||||
* Loads a wikidata page
|
||||
* @returns the entity of the given value
|
||||
*/
|
||||
public static async LoadWikidataEntryAsync(value: string | number): Promise<WikidataResponse> {
|
||||
private static async LoadWikidataEntryUncachedAsync(
|
||||
value: string | number
|
||||
): Promise<WikidataResponse> {
|
||||
const id = Wikidata.ExtractKey(value)
|
||||
if (id === undefined) {
|
||||
console.warn("Could not extract a wikidata entry from", value)
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
/**
|
||||
* Some usefull utility functions around the wikipedia API
|
||||
*/
|
||||
import { Utils } from "../../Utils"
|
||||
import { UIEventSource } from "../UIEventSource"
|
||||
import { WikipediaBoxOptions } from "../../UI/Wikipedia/WikipediaBox"
|
||||
import Wikidata, { WikidataResponse } from "./Wikidata"
|
||||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
|
||||
export interface FullWikipediaDetails {
|
||||
articleUrl?: string
|
||||
language?: string
|
||||
pagename?: string
|
||||
fullArticle?: string
|
||||
firstParagraph?: string
|
||||
restOfArticle?: string
|
||||
wikidata?: WikidataResponse
|
||||
title?: string
|
||||
}
|
||||
|
||||
export default class Wikipedia {
|
||||
/**
|
||||
|
@ -26,11 +34,8 @@ export default class Wikipedia {
|
|||
|
||||
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, Promise<string>>()
|
||||
private static _fullDetailsCache = new Map<string, Store<FullWikipediaDetails>>()
|
||||
public readonly backend: string
|
||||
|
||||
constructor(options?: { language?: "en" | string } | { backend?: string }) {
|
||||
|
@ -56,23 +61,81 @@ export default class Wikipedia {
|
|||
}
|
||||
|
||||
/**
|
||||
* Extracts the actual pagename; returns undefined if this came from a different wikimedia entry
|
||||
* Fetch all useful information for the given entity.
|
||||
*
|
||||
* new Wikipedia({backend: "https://wiki.openstreetmap.org"}).extractPageName("https://wiki.openstreetmap.org/wiki/NL:Speelbos") // => "NL:Speelbos"
|
||||
* new Wikipedia().extractPageName("https://wiki.openstreetmap.org/wiki/NL:Speelbos") // => undefined
|
||||
*/
|
||||
public extractPageName(input: string): string | undefined {
|
||||
if (!input.startsWith(this.backend)) {
|
||||
return undefined
|
||||
public static fetchArticleAndWikidata(
|
||||
wikidataOrPageId: string,
|
||||
preferedLanguage: string
|
||||
): Store<FullWikipediaDetails> {
|
||||
const cachekey = preferedLanguage + wikidataOrPageId
|
||||
const cached = Wikipedia._fullDetailsCache.get(cachekey)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
input = input.substring(this.backend.length)
|
||||
console.log("Constructing store for", cachekey)
|
||||
const store = new UIEventSource<FullWikipediaDetails>({}, cachekey)
|
||||
Wikipedia._fullDetailsCache.set(cachekey, store)
|
||||
|
||||
const matched = input.match("/?wiki/(.+)")
|
||||
if (matched === undefined || matched === null) {
|
||||
return undefined
|
||||
// Are we dealing with a wikidata item?
|
||||
const wikidataId = Wikidata.ExtractKey(wikidataOrPageId)
|
||||
if (!wikidataId) {
|
||||
// We are dealing with a wikipedia identifier, e.g. 'NL:articlename', 'https://nl.wikipedia.org/wiki/article', ...
|
||||
const { language, pageName } = Wikipedia.extractLanguageAndName(wikidataOrPageId)
|
||||
store.data.articleUrl = new Wikipedia({ language }).getPageUrl(pageName)
|
||||
store.data.language = language
|
||||
store.data.pagename = pageName
|
||||
store.data.title = pageName
|
||||
} else {
|
||||
// Jup, this is a wikidata item
|
||||
// Lets fetch the wikidata
|
||||
store.data.title = wikidataId
|
||||
Wikidata.LoadWikidataEntryAsync(wikidataId).then((wikidata) => {
|
||||
store.data.wikidata = wikidata
|
||||
store.ping()
|
||||
// With the wikidata, we can search for the appropriate wikipedia page
|
||||
const preferredLanguage = [
|
||||
preferedLanguage,
|
||||
"en",
|
||||
Array.from(wikidata.wikisites.keys())[0],
|
||||
]
|
||||
|
||||
for (const language of preferredLanguage) {
|
||||
const pagetitle = wikidata.wikisites.get(language)
|
||||
if (pagetitle) {
|
||||
store.data.articleUrl = new Wikipedia({ language }).getPageUrl(pagetitle)
|
||||
store.data.pagename = pagetitle
|
||||
store.data.language = language
|
||||
store.data.title = pagetitle
|
||||
store.ping()
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
const [_, pageName] = matched
|
||||
return pageName
|
||||
|
||||
// Now that the pageURL has been setup, we can focus on downloading the actual article
|
||||
// We setup a listener. As soon as the article-URL is know, we'll fetch the actual page
|
||||
// This url can either be set by the Wikidata-response or directly if we are dealing with a wikipedia-url
|
||||
store.addCallbackAndRun((data) => {
|
||||
if (data.language === undefined || data.pagename === undefined) {
|
||||
return
|
||||
}
|
||||
const wikipedia = new Wikipedia({ language: data.language })
|
||||
wikipedia.GetArticleHtml(data.pagename).then((article) => {
|
||||
data.fullArticle = article
|
||||
const content = document.createElement("div")
|
||||
content.innerHTML = article
|
||||
const firstParagraph = content.getElementsByTagName("p").item(0)
|
||||
data.firstParagraph = firstParagraph.innerHTML
|
||||
content.removeChild(firstParagraph)
|
||||
data.restOfArticle = content.innerHTML
|
||||
store.ping()
|
||||
})
|
||||
return true // unregister
|
||||
})
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
private static getBackendUrl(
|
||||
|
@ -90,18 +153,24 @@ export default class Wikipedia {
|
|||
return backend
|
||||
}
|
||||
|
||||
public GetArticle(
|
||||
pageName: string,
|
||||
options: WikipediaBoxOptions
|
||||
): UIEventSource<{ success: string } | { error: any }> {
|
||||
const key = this.backend + ":" + pageName + ":" + (options.firstParagraphOnly ?? false)
|
||||
const cached = Wikipedia._cache.get(key)
|
||||
if (cached !== undefined) {
|
||||
return cached
|
||||
/**
|
||||
* Extracts the actual pagename; returns undefined if this came from a different wikimedia entry
|
||||
*
|
||||
* new Wikipedia({backend: "https://wiki.openstreetmap.org"}).extractPageName("https://wiki.openstreetmap.org/wiki/NL:Speelbos") // => "NL:Speelbos"
|
||||
* new Wikipedia().extractPageName("https://wiki.openstreetmap.org/wiki/NL:Speelbos") // => undefined
|
||||
*/
|
||||
public extractPageName(input: string): string | undefined {
|
||||
if (!input.startsWith(this.backend)) {
|
||||
return undefined
|
||||
}
|
||||
const v = UIEventSource.FromPromiseWithErr(this.GetArticleAsync(pageName, options))
|
||||
Wikipedia._cache.set(key, v)
|
||||
return v
|
||||
input = input.substring(this.backend.length)
|
||||
|
||||
const matched = input.match("/?wiki/(.+)")
|
||||
if (matched === undefined || matched === null) {
|
||||
return undefined
|
||||
}
|
||||
const [_, pageName] = matched
|
||||
return pageName
|
||||
}
|
||||
|
||||
public getDataUrl(pageName: string): string {
|
||||
|
@ -172,12 +241,23 @@ export default class Wikipedia {
|
|||
})
|
||||
}
|
||||
|
||||
public async GetArticleAsync(
|
||||
pageName: string,
|
||||
options: {
|
||||
firstParagraphOnly?: false | boolean
|
||||
/**
|
||||
* Returns the innerHTML for the given article as string.
|
||||
* Some cleanup is applied to this.
|
||||
*
|
||||
* This method uses a static, local cache, so each article will be retrieved only once via the network
|
||||
*/
|
||||
public GetArticleHtml(pageName: string): Promise<string> {
|
||||
const cacheKey = this.backend + "/" + pageName
|
||||
if (Wikipedia._cache.has(cacheKey)) {
|
||||
return Wikipedia._cache.get(cacheKey)
|
||||
}
|
||||
): Promise<string | undefined> {
|
||||
const promise = this.GetArticleUncachedAsync(pageName)
|
||||
Wikipedia._cache.set(cacheKey, promise)
|
||||
return promise
|
||||
}
|
||||
|
||||
private async GetArticleUncachedAsync(pageName: string): Promise<string> {
|
||||
const response = await Utils.downloadJson(this.getDataUrl(pageName))
|
||||
if (response?.parse?.text === undefined) {
|
||||
return undefined
|
||||
|
@ -213,10 +293,6 @@ export default class Wikipedia {
|
|||
link.href = `${this.backend}${link.getAttribute("href")}`
|
||||
})
|
||||
|
||||
if (options?.firstParagraphOnly) {
|
||||
return content.getElementsByTagName("p").item(0).innerHTML
|
||||
}
|
||||
|
||||
return content.innerHTML
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeome
|
|||
import { NewGeometryFromChangesFeatureSource } from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource"
|
||||
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"
|
||||
import ShowOverlayRasterLayer from "../UI/Map/ShowOverlayRasterLayer"
|
||||
import { Utils } from "../Utils"
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -263,6 +264,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
}
|
||||
this.lastClickObject.features.setData([])
|
||||
})
|
||||
|
||||
if (this.layout.customCss !== undefined && window.location.pathname.indexOf("theme") >= 0) {
|
||||
Utils.LoadCustomCss(this.layout.customCss)
|
||||
}
|
||||
}
|
||||
|
||||
private initHotkeys() {
|
||||
|
@ -371,14 +376,19 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
.get("range")
|
||||
?.isDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true)
|
||||
|
||||
// The following layers are _not_ indexed; they trigger to much and thus trigger the metatagging
|
||||
const dontInclude = new Set(["gps_location", "gps_location_history", "gps_track"])
|
||||
this.layerState.filteredLayers.forEach((flayer) => {
|
||||
const features: FeatureSource = specialLayers[flayer.layerDef.id]
|
||||
const id = flayer.layerDef.id
|
||||
const features: FeatureSource = specialLayers[id]
|
||||
if (features === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
this.featureProperties.trackFeatureSource(features)
|
||||
this.indexedFeatures.addSource(features)
|
||||
if (!dontInclude.has(id)) {
|
||||
this.featureProperties.trackFeatureSource(features)
|
||||
this.indexedFeatures.addSource(features)
|
||||
}
|
||||
new ShowDataLayer(this.map, {
|
||||
features,
|
||||
doShowLayer: flayer.isDisplayed,
|
||||
|
|
|
@ -1,163 +0,0 @@
|
|||
import Svg from "../../Svg"
|
||||
import Combine from "./Combine"
|
||||
import { FixedUiElement } from "./FixedUiElement"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Hash from "../../Logic/Web/Hash"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Title from "./Title"
|
||||
import Hotkeys from "./Hotkeys"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
||||
/**
|
||||
*
|
||||
* The scrollableFullScreen is a bit of a peculiar component:
|
||||
* - It shows a title and some contents, constructed from the respective functions passed into the constructor
|
||||
* - When the element is 'activated', one clone of title+contents is attached to the fullscreen
|
||||
* - The element itself will - upon rendering - also show the title and contents (allthough it'll be a different clone)
|
||||
*
|
||||
*
|
||||
*/
|
||||
export default class ScrollableFullScreen {
|
||||
private static readonly empty = ScrollableFullScreen.initEmpty()
|
||||
private static _currentlyOpen: ScrollableFullScreen
|
||||
public isShown: UIEventSource<boolean>
|
||||
private hashToShow: string
|
||||
private _fullscreencomponent: BaseUIElement
|
||||
private _resetScrollSignal: UIEventSource<void> = new UIEventSource<void>(undefined)
|
||||
private _setHash: boolean
|
||||
|
||||
constructor(
|
||||
title: (options: { mode: string }) => BaseUIElement,
|
||||
content: (options: {
|
||||
mode: string
|
||||
resetScrollSignal: UIEventSource<void>
|
||||
}) => BaseUIElement,
|
||||
hashToShow: string,
|
||||
isShown: UIEventSource<boolean> = new UIEventSource<boolean>(false),
|
||||
options?: {
|
||||
setHash?: boolean
|
||||
}
|
||||
) {
|
||||
this.hashToShow = hashToShow
|
||||
this.isShown = isShown
|
||||
this._setHash = options?.setHash ?? true
|
||||
|
||||
if ((hashToShow === undefined || hashToShow === "") && this._setHash) {
|
||||
throw "HashToShow should be defined as it is vital for the 'back' key functionality"
|
||||
}
|
||||
|
||||
const mobileOptions = {
|
||||
mode: "mobile",
|
||||
resetScrollSignal: this._resetScrollSignal,
|
||||
}
|
||||
|
||||
this._fullscreencomponent = this.BuildComponent(
|
||||
title(mobileOptions),
|
||||
content(mobileOptions).SetClass("pb-20")
|
||||
)
|
||||
|
||||
const self = this
|
||||
if (this._setHash) {
|
||||
Hash.hash.addCallback((h) => {
|
||||
if (h === undefined) {
|
||||
isShown.setData(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
isShown.addCallbackD((isShown) => {
|
||||
if (isShown) {
|
||||
// We first must set the hash, then activate the panel
|
||||
// If the order is wrong, this will cause the panel to disactivate again
|
||||
ScrollableFullScreen._currentlyOpen = self
|
||||
self.Activate()
|
||||
} else {
|
||||
if (self.hashToShow !== undefined) {
|
||||
Hash.hash.setData(undefined)
|
||||
}
|
||||
// Some cleanup...
|
||||
ScrollableFullScreen.collapse()
|
||||
}
|
||||
})
|
||||
if (isShown.data) {
|
||||
ScrollableFullScreen._currentlyOpen = self
|
||||
this.Activate()
|
||||
}
|
||||
}
|
||||
|
||||
private static initEmpty(): FixedUiElement {
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ nomod: "Escape", onUp: true },
|
||||
Translations.t.hotkeyDocumentation.closeSidebar,
|
||||
ScrollableFullScreen.collapse
|
||||
)
|
||||
|
||||
return new FixedUiElement("")
|
||||
}
|
||||
public static collapse() {
|
||||
const fs = document.getElementById("fullscreen")
|
||||
if (fs !== null) {
|
||||
ScrollableFullScreen.empty.AttachTo("fullscreen")
|
||||
fs.classList.add("hidden")
|
||||
}
|
||||
|
||||
const opened = ScrollableFullScreen._currentlyOpen
|
||||
if (opened !== undefined) {
|
||||
opened?.isShown?.setData(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Actually show this in the 'fullscreen'-div
|
||||
* @constructor
|
||||
*/
|
||||
public Activate(): void {
|
||||
if (this.hashToShow && this.hashToShow !== "" && this._setHash) {
|
||||
Hash.hash.setData(this.hashToShow)
|
||||
}
|
||||
this.isShown.setData(true)
|
||||
this._fullscreencomponent.AttachTo("fullscreen")
|
||||
const fs = document.getElementById("fullscreen")
|
||||
ScrollableFullScreen._currentlyOpen = this
|
||||
fs?.classList?.remove("hidden")
|
||||
}
|
||||
|
||||
private BuildComponent(title: BaseUIElement, content: BaseUIElement): BaseUIElement {
|
||||
const returnToTheMap = new Combine([
|
||||
Svg.back_svg().SetClass("block md:hidden w-12 h-12 p-2 svg-foreground"),
|
||||
Svg.close_svg().SetClass("hidden md:block w-12 h-12 p-3 svg-foreground"),
|
||||
]).SetClass("rounded-full p-0 flex-shrink-0 self-center")
|
||||
|
||||
returnToTheMap.onClick(() => {
|
||||
this.isShown.setData(false)
|
||||
Hash.hash.setData(undefined)
|
||||
})
|
||||
|
||||
title = new Title(title, 2)
|
||||
title.SetClass(
|
||||
"text-l sm:text-xl md:text-2xl w-full p-0 max-h-20vh overflow-y-auto self-center"
|
||||
)
|
||||
|
||||
const contentWrapper = new Combine([content]).SetClass(
|
||||
"block p-2 md:pt-4 w-full h-full overflow-y-auto"
|
||||
)
|
||||
|
||||
this._resetScrollSignal.addCallback((_) => {
|
||||
contentWrapper.ScrollToTop()
|
||||
})
|
||||
|
||||
return new Combine([
|
||||
new Combine([
|
||||
new Combine([returnToTheMap, title]).SetClass(
|
||||
"border-b-1 border-black shadow bg-white flex flex-shrink-0 pt-1 pb-1 md:pt-0 md:pb-0"
|
||||
),
|
||||
contentWrapper,
|
||||
// We add an ornament which takes around 5em. This is in order to make sure the Web UI doesn't hide
|
||||
]).SetClass("flex flex-col h-full relative bg-white"),
|
||||
]).SetClass(
|
||||
"fixed top-0 left-0 right-0 h-screen w-screen md:w-auto md:relative z-above-controls md:rounded-xl overflow-hidden"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
import Translations from "../i18n/Translations"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Combine from "./Combine"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { VariableUiElement } from "./VariableUIElement"
|
||||
|
||||
export class TabbedComponent extends Combine {
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
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 tabs: BaseUIElement[] = [options?.leftOfHeader]
|
||||
const contentElements: BaseUIElement[] = []
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
let element = elements[i]
|
||||
const header = Translations.W(element.header).onClick(() => openedTabSrc.setData(i))
|
||||
openedTabSrc.addCallbackAndRun((selected) => {
|
||||
if (selected >= elements.length) {
|
||||
selected = 0
|
||||
}
|
||||
if (selected === i) {
|
||||
header.SetClass("tab-active")
|
||||
header.RemoveClass("tab-non-active")
|
||||
} else {
|
||||
header.SetClass("tab-non-active")
|
||||
header.RemoveClass("tab-active")
|
||||
}
|
||||
})
|
||||
const content = Translations.W(element.content)
|
||||
content.SetClass("relative w-full inline-block")
|
||||
contentElements.push(content)
|
||||
const tab = header.SetClass("block tab-single-header")
|
||||
tabs.push(tab)
|
||||
}
|
||||
|
||||
const header = new Combine(tabs).SetClass("tabs-header-bar")
|
||||
if (options?.styleHeader) {
|
||||
options.styleHeader(header)
|
||||
}
|
||||
const actualContent = new VariableUiElement(
|
||||
openedTabSrc.map((i) => contentElements[i])
|
||||
).SetStyle("max-height: inherit; height: inherit")
|
||||
super([header, actualContent])
|
||||
}
|
||||
}
|
|
@ -66,3 +66,15 @@
|
|||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
|
||||
<style>
|
||||
.tab-selected {
|
||||
background-color: rgb(59 130 246);
|
||||
color: rgb(255 255 255);
|
||||
}
|
||||
|
||||
.tab-unselected {
|
||||
background-color: rgb(255 255 255);
|
||||
color: rgb(0 0 0);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,62 +1,33 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Svg from "../../Svg"
|
||||
import ExportPDF from "../ExportPDF"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import BaseLayer from "../../Models/BaseLayer"
|
||||
import Loc from "../../Models/Loc"
|
||||
|
||||
interface DownloadState {
|
||||
filteredLayers: UIEventSource<FilteredLayer[]>
|
||||
featurePipeline: FeaturePipeline
|
||||
layoutToUse: LayoutConfig
|
||||
currentBounds: UIEventSource<BBox>
|
||||
backgroundLayer: UIEventSource<BaseLayer>
|
||||
locationControl: UIEventSource<Loc>
|
||||
featureSwitchExportAsPdf: UIEventSource<boolean>
|
||||
featureSwitchEnableExport: UIEventSource<boolean>
|
||||
}
|
||||
|
||||
export default class AllDownloads extends ScrollableFullScreen {
|
||||
export default class AllDownloads extends SubtleButton {
|
||||
constructor(
|
||||
isShown: UIEventSource<boolean>,
|
||||
state: {
|
||||
filteredLayers: UIEventSource<FilteredLayer[]>
|
||||
featurePipeline: FeaturePipeline
|
||||
layoutToUse: LayoutConfig
|
||||
currentBounds: UIEventSource<BBox>
|
||||
backgroundLayer: UIEventSource<BaseLayer>
|
||||
locationControl: UIEventSource<Loc>
|
||||
featureSwitchExportAsPdf: UIEventSource<boolean>
|
||||
featureSwitchEnableExport: UIEventSource<boolean>
|
||||
}
|
||||
) {
|
||||
super(AllDownloads.GenTitle, () => AllDownloads.GeneratePanel(state), "downloads", isShown)
|
||||
}
|
||||
|
||||
private static GenTitle(): BaseUIElement {
|
||||
return Translations.t.general.download.title
|
||||
.Clone()
|
||||
.SetClass("text-2xl break-words font-bold p-2")
|
||||
}
|
||||
|
||||
private static GeneratePanel(state: DownloadState): BaseUIElement {
|
||||
const isExporting = new UIEventSource(false, "Pdf-is-exporting")
|
||||
const generatePdf = () => {
|
||||
isExporting.setData(true)
|
||||
new ExportPDF({
|
||||
freeDivId: "belowmap",
|
||||
background: state.backgroundLayer,
|
||||
location: state.locationControl,
|
||||
features: state.featurePipeline,
|
||||
layout: state.layoutToUse,
|
||||
}).isRunning.addCallbackAndRun((isRunning) => isExporting.setData(isRunning))
|
||||
}
|
||||
|
@ -78,6 +49,6 @@ export default class AllDownloads extends ScrollableFullScreen {
|
|||
isExporting
|
||||
)
|
||||
|
||||
return new SubtleButton(icon, text)
|
||||
super(icon, text)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,6 @@ import AllDownloads from "./AllDownloads"
|
|||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Lazy from "../Base/Lazy"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import FeatureInfoBox from "../Popup/FeatureInfoBox"
|
||||
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
|
||||
import { DefaultGuiState } from "../DefaultGuiState"
|
||||
|
||||
export default class LeftControls extends Combine {
|
||||
|
@ -57,8 +55,6 @@ export default class LeftControls extends Combine {
|
|||
|
||||
new AllDownloads(guiState.downloadControlIsOpened, state)
|
||||
|
||||
|
||||
|
||||
super([currentViewAction])
|
||||
|
||||
this.SetClass("flex flex-col")
|
||||
|
|
|
@ -7,7 +7,6 @@ import WikidataPreviewBox from "../Wikipedia/WikidataPreviewBox"
|
|||
import { Button } from "../Base/Button"
|
||||
import Combine from "../Base/Combine"
|
||||
import Title from "../Base/Title"
|
||||
import WikipediaBox from "../Wikipedia/WikipediaBox"
|
||||
import Translations from "../i18n/Translations"
|
||||
import List from "../Base/List"
|
||||
import Svg from "../../Svg"
|
||||
|
@ -97,7 +96,7 @@ export default class PlantNetSpeciesSearch extends VariableUiElement {
|
|||
if (wikidataSpecies === undefined) {
|
||||
return plantOverview
|
||||
}
|
||||
const buttons = new Combine([
|
||||
return new Combine([
|
||||
new Button(
|
||||
new Combine([
|
||||
Svg.back_svg().SetClass(
|
||||
|
@ -120,15 +119,6 @@ export default class PlantNetSpeciesSearch extends VariableUiElement {
|
|||
}
|
||||
).SetClass("btn"),
|
||||
]).SetClass("flex justify-between")
|
||||
|
||||
return new Combine([
|
||||
new WikipediaBox([wikidataSpecies], {
|
||||
firstParagraphOnly: false,
|
||||
noImages: false,
|
||||
addHeader: false,
|
||||
}).SetClass("h-96"),
|
||||
buttons,
|
||||
]).SetClass("flex flex-col self-end")
|
||||
})
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
import { Utils } from "../Utils"
|
||||
import Toggle from "./Input/Toggle"
|
||||
import LeftControls from "./BigComponents/LeftControls"
|
||||
import RightControls from "./BigComponents/RightControls"
|
||||
import CenterMessageBox from "./CenterMessageBox"
|
||||
import ScrollableFullScreen from "./Base/ScrollableFullScreen"
|
||||
import Translations from "./i18n/Translations"
|
||||
import { DefaultGuiState } from "./DefaultGuiState"
|
||||
import Combine from "./Base/Combine"
|
||||
import ExtraLinkButton from "./BigComponents/ExtraLinkButton"
|
||||
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
|
||||
import CopyrightPanel from "./BigComponents/CopyrightPanel"
|
||||
|
||||
/**
|
||||
* The default MapComplete GUI initializer
|
||||
|
@ -25,17 +21,6 @@ export default class DefaultGUI {
|
|||
}
|
||||
|
||||
public setup() {
|
||||
this.SetupUIElements()
|
||||
|
||||
if (
|
||||
this.state.layoutToUse.customCss !== undefined &&
|
||||
window.location.pathname.indexOf("index") >= 0
|
||||
) {
|
||||
Utils.LoadCustomCss(this.state.layoutToUse.customCss)
|
||||
}
|
||||
}
|
||||
|
||||
private SetupUIElements() {
|
||||
const extraLink = Toggle.If(
|
||||
state.featureSwitchExtraLinkEnabled,
|
||||
() => new ExtraLinkButton(state, state.layoutToUse.extraLink)
|
||||
|
|
|
@ -14,8 +14,6 @@ import { VariableUiElement } from "../Base/VariableUIElement"
|
|||
import Loading from "../Base/Loading"
|
||||
import { LoginToggle } from "../Popup/LoginButton"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { DefaultGuiState } from "../DefaultGuiState"
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
|
||||
export class ImageUploadFlow extends Toggle {
|
||||
|
|
|
@ -11,33 +11,15 @@ import Loc from "../../Models/Loc"
|
|||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import { FlowStep } from "./FlowStep"
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||
import Title from "../Base/Title"
|
||||
import CheckBoxes from "../Input/Checkboxes"
|
||||
import AllTagsPanel from "../Popup/AllTagsPanel.svelte"
|
||||
import { Feature, Point } from "geojson"
|
||||
import DivContainer from "../Base/DivContainer"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
|
||||
import ShowDataLayer from "../Map/ShowDataLayer"
|
||||
|
||||
class PreviewPanel extends ScrollableFullScreen {
|
||||
constructor(tags: UIEventSource<any>) {
|
||||
super(
|
||||
(_) => new FixedUiElement("Element to import"),
|
||||
(_) =>
|
||||
new Combine([
|
||||
"The tags are:",
|
||||
new SvelteUIElement(AllTagsPanel, { tags }),
|
||||
]).SetClass("flex flex-col"),
|
||||
"element"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the data to import on a map, asks for the correct layer to be selected
|
||||
*/
|
||||
|
@ -111,7 +93,6 @@ export class MapPreview
|
|||
const currentBounds = new UIEventSource<BBox>(undefined)
|
||||
const { ui, mapproperties, map } = MapLibreAdaptor.construct()
|
||||
|
||||
|
||||
ui.SetClass("w-full").SetStyle("height: 500px")
|
||||
|
||||
layerPicker.GetValue().addCallbackAndRunD((layerToShow) => {
|
||||
|
@ -119,7 +100,6 @@ export class MapPreview
|
|||
layer: layerToShow,
|
||||
zoomToFeatures: true,
|
||||
features: new StaticFeatureSource(matching),
|
||||
buildPopup: (tag) => new PreviewPanel(tag),
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -34,62 +34,4 @@ export default class WikidataValidator extends Validator {
|
|||
}
|
||||
return out
|
||||
}
|
||||
|
||||
public inputHelper(currentValue, inputHelperOptions) {
|
||||
const args = inputHelperOptions.args ?? []
|
||||
const searchKey = args[0] ?? "name"
|
||||
|
||||
const searchFor = <string>(
|
||||
(inputHelperOptions.feature?.properties[searchKey]?.toLowerCase() ?? "")
|
||||
)
|
||||
|
||||
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: currentValue,
|
||||
searchText: searchForValue,
|
||||
instanceOf,
|
||||
notInstanceOf,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Combine from "../Base/Combine"
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
|
||||
import Svg from "../../Svg"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
||||
export default class FeatureInfoBox extends ScrollableFullScreen {
|
||||
public constructor(
|
||||
tags: UIEventSource<any>,
|
||||
layerConfig: LayerConfig,
|
||||
state: FeaturePipelineState,
|
||||
options?: {
|
||||
hashToShow?: string
|
||||
isShown?: UIEventSource<boolean>
|
||||
setHash?: true | boolean
|
||||
}
|
||||
) {
|
||||
const showAllQuestions = state.featureSwitchShowAllQuestions.map(
|
||||
(fsShow) => fsShow || state.showAllQuestionsAtOnce.data,
|
||||
[state.showAllQuestionsAtOnce]
|
||||
)
|
||||
super(
|
||||
() => undefined,
|
||||
() => FeatureInfoBox.GenerateContent(tags, layerConfig),
|
||||
options?.hashToShow ?? tags.data.id ?? "item",
|
||||
options?.isShown,
|
||||
options
|
||||
)
|
||||
|
||||
if (layerConfig === undefined) {
|
||||
throw "Undefined layerconfig"
|
||||
}
|
||||
}
|
||||
|
||||
public static GenerateContent(tags: UIEventSource<any>): BaseUIElement {
|
||||
return new Toggle(
|
||||
new Combine([
|
||||
Svg.delete_icon_svg().SetClass("w-8 h-8"),
|
||||
Translations.t.delete.isDeleted,
|
||||
]).SetClass("flex justify-center font-bold items-center"),
|
||||
new Combine([]).SetClass("block"),
|
||||
tags.map((t) => t["_deleted"] == "yes")
|
||||
)
|
||||
}
|
||||
}
|
|
@ -19,14 +19,13 @@ import { ConflateButton, ImportPointButton, ImportWayButton } from "./Popup/Impo
|
|||
import TagApplyButton from "./Popup/TagApplyButton"
|
||||
import { CloseNoteButton } from "./Popup/CloseNoteButton"
|
||||
import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis"
|
||||
import { Stores, UIEventSource } from "../Logic/UIEventSource"
|
||||
import { Store, Stores, UIEventSource } from "../Logic/UIEventSource"
|
||||
import AllTagsPanel from "./Popup/AllTagsPanel.svelte"
|
||||
import AllImageProviders from "../Logic/ImageProviders/AllImageProviders"
|
||||
import { ImageCarousel } from "./Image/ImageCarousel"
|
||||
import { ImageUploadFlow } from "./Image/ImageUploadFlow"
|
||||
import { VariableUiElement } from "./Base/VariableUIElement"
|
||||
import { Utils } from "../Utils"
|
||||
import WikipediaBox from "./Wikipedia/WikipediaBox"
|
||||
import Wikidata, { WikidataResponse } from "../Logic/Web/Wikidata"
|
||||
import { Translation } from "./i18n/Translation"
|
||||
import Translations from "./i18n/Translations"
|
||||
|
@ -80,6 +79,7 @@ import { OsmId, OsmTags, WayId } from "../Models/OsmFeature"
|
|||
import MoveWizard from "./Popup/MoveWizard"
|
||||
import SplitRoadWizard from "./Popup/SplitRoadWizard"
|
||||
import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz"
|
||||
import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte"
|
||||
|
||||
class NearbyImageVis implements SpecialVisualization {
|
||||
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
||||
|
@ -628,7 +628,7 @@ export default class SpecialVisualizations {
|
|||
|
||||
{
|
||||
funcName: "wikipedia",
|
||||
docs: "A box showing the corresponding wikipedia article - based on the wikidata tag",
|
||||
docs: "A box showing the corresponding wikipedia article(s) - based on the **wikidata** tag.",
|
||||
args: [
|
||||
{
|
||||
name: "keyToShowWikipediaFor",
|
||||
|
@ -638,23 +638,15 @@ 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",
|
||||
constr: (_, tagsSource, args) => {
|
||||
constr: (_, tagsSource, args, feature, layer) => {
|
||||
const keys = args[0].split(";").map((k) => k.trim())
|
||||
return new VariableUiElement(
|
||||
tagsSource
|
||||
.map((tags) => {
|
||||
const key = keys.find(
|
||||
(k) => tags[k] !== undefined && tags[k] !== ""
|
||||
)
|
||||
return tags[key]
|
||||
})
|
||||
.map((wikidata) => {
|
||||
const wikidatas: string[] = Utils.NoEmpty(
|
||||
wikidata?.split(";")?.map((wd) => wd.trim()) ?? []
|
||||
)
|
||||
return new WikipediaBox(wikidatas)
|
||||
})
|
||||
)
|
||||
const wikiIds: Store<string[]> = tagsSource.map((tags) => {
|
||||
const key = keys.find((k) => tags[k] !== undefined && tags[k] !== "")
|
||||
return tags[key]?.split(";")?.map((id) => id.trim())
|
||||
})
|
||||
return new SvelteUIElement(WikipediaPanel, {
|
||||
wikiIds,
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -276,17 +276,3 @@
|
|||
</FloatOver>
|
||||
</If>
|
||||
|
||||
|
||||
<style>
|
||||
/* WARNING: This is just for demonstration.
|
||||
Using :global() in this way can be risky. */
|
||||
:global(.tab-selected) {
|
||||
background-color: rgb(59 130 246);
|
||||
color: rgb(255 255 255);
|
||||
}
|
||||
|
||||
:global(.tab-unselected) {
|
||||
background-color: rgb(255 255 255);
|
||||
color: rgb(0 0 0);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,20 +8,12 @@ import Locale from "../i18n/Locale"
|
|||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import WikidataPreviewBox from "./WikidataPreviewBox"
|
||||
import Title from "../Base/Title"
|
||||
import WikipediaBox from "./WikipediaBox"
|
||||
import Svg from "../../Svg"
|
||||
import Loading from "../Base/Loading"
|
||||
import Table from "../Base/Table"
|
||||
|
||||
export default class WikidataSearchBox extends InputElement<string> {
|
||||
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[]
|
||||
|
||||
public static docs = new Combine([
|
||||
,
|
||||
new Title("Helper arguments"),
|
||||
new Table(
|
||||
["name", "doc"],
|
||||
|
@ -100,6 +92,11 @@ Another example is to search for species and trees:
|
|||
\`\`\`
|
||||
`,
|
||||
])
|
||||
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>
|
||||
|
@ -207,25 +204,15 @@ Another example is to search for species and trees:
|
|||
)
|
||||
)
|
||||
|
||||
const full = new Combine([
|
||||
return 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.split(";"))
|
||||
})
|
||||
).SetStyle("max-height:12.5rem"),
|
||||
full,
|
||||
]).ConstructElement()
|
||||
])
|
||||
.SetClass("flex flex-col border-2 border-black rounded-xl m-2 p-2")
|
||||
.ConstructElement()
|
||||
}
|
||||
}
|
||||
|
|
46
UI/Wikipedia/WikipediaArticle.svelte
Normal file
46
UI/Wikipedia/WikipediaArticle.svelte
Normal file
|
@ -0,0 +1,46 @@
|
|||
<script lang="ts">
|
||||
import type { FullWikipediaDetails } from "../../Logic/Web/Wikipedia";
|
||||
import { Store } from "../../Logic/UIEventSource";
|
||||
import FromHtml from "../Base/FromHtml.svelte";
|
||||
import Loading from "../Base/Loading.svelte";
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from "@rgossiaux/svelte-headlessui";
|
||||
import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import WikidataPreviewBox from "./WikidataPreviewBox";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
||||
/**
|
||||
* Small helper
|
||||
*/
|
||||
export let wikipediaDetails: Store<FullWikipediaDetails>;
|
||||
</script>
|
||||
|
||||
<a href={$wikipediaDetails.articleUrl} target="_blank" rel="noreferrer" class="flex">
|
||||
<img src="./assets/svg/wikipedia.svg" class="w-6 h-6"/>
|
||||
<Tr t={Translations.t.general.wikipedia.fromWikipedia}/>
|
||||
</a>
|
||||
|
||||
{#if $wikipediaDetails.wikidata}
|
||||
<ToSvelte construct={WikidataPreviewBox.WikidataResponsePreview($wikipediaDetails.wikidata)} />
|
||||
{/if}
|
||||
|
||||
{#if $wikipediaDetails.firstParagraph === "" || $wikipediaDetails.firstParagraph === undefined}
|
||||
<Loading >
|
||||
<Tr t={Translations.t.general.wikipedia.loading}/>
|
||||
</Loading>
|
||||
{:else}
|
||||
<FromHtml src={$wikipediaDetails.firstParagraph} />
|
||||
<Disclosure let:open>
|
||||
<DisclosureButton>
|
||||
<span class="flex">
|
||||
<ChevronRightIcon class="w-6 h-6" style={open ? "transform: rotate(90deg);" : ""} />
|
||||
Read the rest of the article
|
||||
|
||||
</span>
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel>
|
||||
<FromHtml src={$wikipediaDetails.restOfArticle} />
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
{/if}
|
|
@ -1,321 +0,0 @@
|
|||
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 { Store, 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"
|
||||
import { Paragraph } from "../Base/Paragraph"
|
||||
|
||||
export interface WikipediaBoxOptions {
|
||||
addHeader: boolean
|
||||
firstParagraphOnly: boolean
|
||||
noImages: boolean
|
||||
currentState?: UIEventSource<"loading" | "loaded" | "error">
|
||||
}
|
||||
|
||||
export default class WikipediaBox extends Combine {
|
||||
constructor(wikidataIds: string[], options?: WikipediaBoxOptions) {
|
||||
const mainContents = []
|
||||
options = options ?? { addHeader: false, firstParagraphOnly: true, noImages: false }
|
||||
const pages = wikidataIds.map((entry) =>
|
||||
WikipediaBox.createLinkedContent(entry.trim(), options)
|
||||
)
|
||||
if (wikidataIds.length == 1) {
|
||||
const page = pages[0]
|
||||
mainContents.push(
|
||||
new Combine([
|
||||
new Combine([
|
||||
options.noImages
|
||||
? undefined
|
||||
: 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.SetClass("overflow-auto normal-background rounded-lg"))
|
||||
} else if (wikidataIds.length > 1) {
|
||||
const tabbed = new TabbedComponent(
|
||||
pages.map((page) => {
|
||||
const contents = page.contents
|
||||
.SetClass("overflow-auto normal-background rounded-lg 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: options.noImages
|
||||
? undefined
|
||||
: Svg.wikipedia_svg()
|
||||
.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(
|
||||
entry: string,
|
||||
options: WikipediaBoxOptions
|
||||
): {
|
||||
titleElement: BaseUIElement
|
||||
contents: BaseUIElement
|
||||
linkElement: BaseUIElement
|
||||
} {
|
||||
if (entry.match("[qQ][0-9]+")) {
|
||||
return WikipediaBox.createWikidatabox(entry, options)
|
||||
} else {
|
||||
return WikipediaBox.createWikipediabox(entry, options)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a '<language>:<article-name>'-string, constructs the wikipedia article
|
||||
*/
|
||||
private static createWikipediabox(
|
||||
wikipediaArticle: string,
|
||||
options: WikipediaBoxOptions
|
||||
): {
|
||||
titleElement: BaseUIElement
|
||||
contents: BaseUIElement
|
||||
linkElement: BaseUIElement
|
||||
} {
|
||||
const wp = Translations.t.general.wikipedia
|
||||
|
||||
const article = Wikipedia.extractLanguageAndName(wikipediaArticle)
|
||||
if (article === undefined) {
|
||||
return {
|
||||
titleElement: undefined,
|
||||
contents: wp.noWikipediaPage,
|
||||
linkElement: undefined,
|
||||
}
|
||||
}
|
||||
const wikipedia = new Wikipedia({ language: article.language })
|
||||
const url = wikipedia.getPageUrl(article.pageName)
|
||||
const linkElement = new Link(
|
||||
Svg.pop_out_svg().SetStyle("width: 1.2rem").SetClass("block "),
|
||||
url,
|
||||
true
|
||||
).SetClass("flex items-center enable-links")
|
||||
|
||||
return {
|
||||
titleElement: new Title(article.pageName, 3),
|
||||
contents: WikipediaBox.createContents(article.pageName, wikipedia, options),
|
||||
linkElement,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a `Q1234`, constructs a wikipedia box (if a wikipedia page is available) or wikidata box as fallback.
|
||||
*
|
||||
*/
|
||||
private static createWikidatabox(
|
||||
wikidataId: string,
|
||||
options: WikipediaBoxOptions
|
||||
): {
|
||||
titleElement: BaseUIElement
|
||||
contents: BaseUIElement
|
||||
linkElement: BaseUIElement
|
||||
} {
|
||||
const wp = Translations.t.general.wikipedia
|
||||
|
||||
const wikiLink: Store<
|
||||
| [string, string, WikidataResponse]
|
||||
| "loading"
|
||||
| "failed"
|
||||
| ["no page", WikidataResponse]
|
||||
> = Wikidata.LoadWikidataEntry(wikidataId).map(
|
||||
(maybewikidata) => {
|
||||
if (maybewikidata === undefined) {
|
||||
return "loading"
|
||||
}
|
||||
if (maybewikidata["error"] !== undefined) {
|
||||
return "failed"
|
||||
}
|
||||
const wikidata = <WikidataResponse>maybewikidata["success"]
|
||||
if (wikidata === undefined) {
|
||||
return "failed"
|
||||
}
|
||||
if (wikidata.wikisites.size === 0) {
|
||||
return ["no page", wikidata]
|
||||
}
|
||||
|
||||
const preferredLanguage = [
|
||||
Locale.language.data,
|
||||
"en",
|
||||
Array.from(wikidata.wikisites.keys())[0],
|
||||
]
|
||||
let language
|
||||
let pagetitle
|
||||
let i = 0
|
||||
do {
|
||||
language = preferredLanguage[i]
|
||||
pagetitle = wikidata.wikisites.get(language)
|
||||
i++
|
||||
} while (pagetitle === undefined)
|
||||
return [pagetitle, language, wikidata]
|
||||
},
|
||||
[Locale.language]
|
||||
)
|
||||
|
||||
const contents = new VariableUiElement(
|
||||
wikiLink.map((status) => {
|
||||
if (status === "loading") {
|
||||
return new Loading(wp.loading.Clone()).SetClass("pl-6 pt-2")
|
||||
}
|
||||
|
||||
if (status === "failed") {
|
||||
return wp.failed.Clone().SetClass("alert p-4")
|
||||
}
|
||||
if (status[0] == "no page") {
|
||||
const [_, wd] = <[string, WikidataResponse]>status
|
||||
options.currentState?.setData("loaded")
|
||||
return new Combine([
|
||||
WikidataPreviewBox.WikidataResponsePreview(wd),
|
||||
wp.noWikipediaPage.Clone().SetClass("subtle"),
|
||||
]).SetClass("flex flex-col p-4")
|
||||
}
|
||||
|
||||
const [pagetitle, language, wd] = <[string, string, WikidataResponse]>status
|
||||
const wikipedia = new Wikipedia({ language })
|
||||
const quickFacts = WikidataPreviewBox.QuickFacts(wd)
|
||||
return WikipediaBox.createContents(pagetitle, wikipedia, {
|
||||
topBar: quickFacts,
|
||||
...options,
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const titleElement = new VariableUiElement(
|
||||
wikiLink.map((state) => {
|
||||
if (typeof state !== "string") {
|
||||
const [pagetitle, _] = state
|
||||
if (pagetitle === "no page") {
|
||||
const wd = <WikidataResponse>state[1]
|
||||
return new Title(Translation.fromMap(wd.labels), 3)
|
||||
}
|
||||
return new Title(pagetitle, 3)
|
||||
}
|
||||
return new Link(
|
||||
new Title(wikidataId, 3),
|
||||
"https://www.wikidata.org/wiki/" + wikidataId,
|
||||
true
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
const linkElement = new VariableUiElement(
|
||||
wikiLink.map((state) => {
|
||||
if (typeof state !== "string") {
|
||||
const [pagetitle, language] = state
|
||||
const popout = options.noImages
|
||||
? "Source"
|
||||
: Svg.pop_out_svg().SetStyle("width: 1.2rem").SetClass("block")
|
||||
if (pagetitle === "no page") {
|
||||
const wd = <WikidataResponse>state[1]
|
||||
return new Link(popout, "https://www.wikidata.org/wiki/" + wd.id, true)
|
||||
}
|
||||
|
||||
const url = `https://${language}.wikipedia.org/wiki/${pagetitle}`
|
||||
return new Link(popout, url, true)
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
).SetClass("flex items-center enable-links")
|
||||
|
||||
return {
|
||||
contents: contents,
|
||||
linkElement: linkElement,
|
||||
titleElement: titleElement,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the actual content in a scrollable way for the given wikipedia page
|
||||
*/
|
||||
private static createContents(
|
||||
pagename: string,
|
||||
wikipedia: Wikipedia,
|
||||
options: {
|
||||
topBar?: BaseUIElement
|
||||
} & WikipediaBoxOptions
|
||||
): BaseUIElement {
|
||||
const htmlContent = wikipedia.GetArticle(pagename, options)
|
||||
const wp = Translations.t.general.wikipedia
|
||||
const contents: VariableUiElement = new VariableUiElement(
|
||||
htmlContent.map((htmlContent) => {
|
||||
if (htmlContent === undefined) {
|
||||
// Still loading
|
||||
return new Loading(wp.loading.Clone())
|
||||
}
|
||||
if (htmlContent["success"] !== undefined) {
|
||||
let content: BaseUIElement = new FixedUiElement(htmlContent["success"])
|
||||
if (options?.addHeader) {
|
||||
content = new Combine([
|
||||
new Paragraph(
|
||||
new Link(wp.fromWikipedia, wikipedia.getPageUrl(pagename), true)
|
||||
),
|
||||
new Paragraph(content),
|
||||
])
|
||||
}
|
||||
return content.SetClass("wikipedia-article")
|
||||
}
|
||||
if (htmlContent["error"]) {
|
||||
console.warn("Loading wikipage failed due to", htmlContent["error"])
|
||||
return wp.failed.Clone().SetClass("alert p-4")
|
||||
}
|
||||
|
||||
return undefined
|
||||
})
|
||||
)
|
||||
|
||||
htmlContent.addCallbackAndRunD((c) => {
|
||||
if (c["success"] !== undefined) {
|
||||
options.currentState?.setData("loaded")
|
||||
} else if (c["error"] !== undefined) {
|
||||
options.currentState?.setData("error")
|
||||
} else {
|
||||
options.currentState?.setData("loading")
|
||||
}
|
||||
})
|
||||
|
||||
return new Combine([
|
||||
options?.topBar?.SetClass("border-2 border-grey rounded-lg m-1 mb-0"),
|
||||
contents.SetClass("block pl-6 pt-2"),
|
||||
])
|
||||
}
|
||||
}
|
7
UI/Wikipedia/WikipediaBoxOptions.ts
Normal file
7
UI/Wikipedia/WikipediaBoxOptions.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
|
||||
export interface WikipediaBoxOptions {
|
||||
addHeader?: boolean
|
||||
firstParagraphOnly?: true | boolean
|
||||
allowToAdd?: boolean
|
||||
}
|
56
UI/Wikipedia/WikipediaPanel.svelte
Normal file
56
UI/Wikipedia/WikipediaPanel.svelte
Normal file
|
@ -0,0 +1,56 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Shows one or more wikidata info boxes or wikipedia articles in a tabbed component.
|
||||
*/
|
||||
import type { FullWikipediaDetails } from "../../Logic/Web/Wikipedia";
|
||||
import Wikipedia from "../../Logic/Web/Wikipedia";
|
||||
import Locale from "../i18n/Locale";
|
||||
import { Store } from "../../Logic/UIEventSource";
|
||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui";
|
||||
import WikipediaTitle from "./WikipediaTitle.svelte";
|
||||
import WikipediaArticle from "./WikipediaArticle.svelte";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
|
||||
/**
|
||||
* Either a wikidata item or a '<language>:<article>' link
|
||||
*/
|
||||
export let wikiIds: Store<string[]>;
|
||||
let wikipediaStores: Store<Store<FullWikipediaDetails>[]> = Locale.language.bind(language =>
|
||||
wikiIds.map(wikiIds => wikiIds.map(id => Wikipedia.fetchArticleAndWikidata(id, language))));
|
||||
let _wikipediaStores;
|
||||
onDestroy(wikipediaStores.addCallbackAndRunD(wikipediaStores => {
|
||||
_wikipediaStores = wikipediaStores;
|
||||
}));
|
||||
</script>
|
||||
{#if _wikipediaStores !== undefined}
|
||||
<TabGroup>
|
||||
<TabList>
|
||||
{#each _wikipediaStores as store (store.tag)}
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||
<WikipediaTitle wikipediaDetails={store} />
|
||||
</Tab>
|
||||
{/each}
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
|
||||
{#each _wikipediaStores as store (store.tag)}
|
||||
<TabPanel>
|
||||
<WikipediaArticle wikipediaDetails={store} />
|
||||
|
||||
</TabPanel>
|
||||
{/each}
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
{/if}
|
||||
<style>
|
||||
.tab-selected {
|
||||
background-color: rgb(59 130 246);
|
||||
color: rgb(255 255 255);
|
||||
}
|
||||
|
||||
.tab-unselected {
|
||||
background-color: rgb(255 255 255);
|
||||
color: rgb(0 0 0);
|
||||
}
|
||||
</style>
|
13
UI/Wikipedia/WikipediaTitle.svelte
Normal file
13
UI/Wikipedia/WikipediaTitle.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import type { FullWikipediaDetails } from "../../Logic/Web/Wikipedia";
|
||||
import { Store } from "../../Logic/UIEventSource";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
/**
|
||||
* Small helper
|
||||
*/
|
||||
export let wikipediaDetails: Store<FullWikipediaDetails>
|
||||
</script>
|
||||
|
||||
{$wikipediaDetails.title}
|
||||
|
|
@ -44,4 +44,4 @@
|
|||
}
|
||||
],
|
||||
"syncSelection": "global"
|
||||
}
|
||||
}
|
|
@ -2110,4 +2110,4 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -695,4 +695,4 @@
|
|||
"enableShareScreen": false,
|
||||
"enableMoreQuests": false,
|
||||
"credits": "Pieter Vander Vennet, Rob Nickerson, Russ Garrett"
|
||||
}
|
||||
}
|
|
@ -362,6 +362,7 @@
|
|||
"general": "On this map, you can see, edit and add <i>points of interest</i>. Zoom around to see the POI, tap one to see or edit the information. All data is sourced from and saved to OpenStreetMap, which can be freely reused."
|
||||
},
|
||||
"wikipedia": {
|
||||
"addEntry": "Add another Wikipedia page",
|
||||
"createNewWikidata": "Create a new Wikidata item",
|
||||
"doSearch": "Search above to see results",
|
||||
"failed": "Loading the Wikipedia entry failed",
|
||||
|
|
|
@ -131,6 +131,13 @@
|
|||
"question": "Quin és el nom de la xarxa per a l'accés inalàmbric a internet?",
|
||||
"render": "El nom de la xarxa és <b>{internet_access:ssid}</b>"
|
||||
},
|
||||
"just_created": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
"then": "Acabeu de crear aquest element! Gràcies per compartir aquesta informació amb el mon i ajudar a persones al voltant del món."
|
||||
}
|
||||
}
|
||||
},
|
||||
"level": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
|
|
|
@ -131,6 +131,13 @@
|
|||
"question": "Wie lautet der Netzwerkname für den drahtlosen Internetzugang?",
|
||||
"render": "Der Netzwerkname lautet <b>{internet_access:ssid}</b>"
|
||||
},
|
||||
"just_created": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
"then": "Sie haben gerade dieses Element erstellt! Vielen Dank, dass Sie diese Informationen mit der Welt teilen und Menschen weltweit helfen."
|
||||
}
|
||||
}
|
||||
},
|
||||
"level": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
|
|
|
@ -131,6 +131,13 @@
|
|||
"question": "What is the network name for the wireless internet access?",
|
||||
"render": "The network name is <b>{internet_access:ssid}</b>"
|
||||
},
|
||||
"just_created": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
"then": "You just created this element! Thanks for sharing this info with the world and helping people worldwide."
|
||||
}
|
||||
}
|
||||
},
|
||||
"last_edit": {
|
||||
"render": {
|
||||
"special": {
|
||||
|
|
|
@ -131,6 +131,13 @@
|
|||
"question": "Quel est le nom du réseau pour l'accès Internet sans fil ?",
|
||||
"render": "Le nom du réseau est <b>{internet_access:ssid}</b>"
|
||||
},
|
||||
"just_created": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
"then": "Vous venez de créer cet élément ! Merci d'avoir partagé cette information avec le monde et d'aider les autres personnes."
|
||||
}
|
||||
}
|
||||
},
|
||||
"level": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
|
|
|
@ -131,6 +131,13 @@
|
|||
"question": "Wat is de netwerknaam voor de draadloze internettoegang?",
|
||||
"render": "De netwerknaam is <b>{internet_access:ssid}</b>"
|
||||
},
|
||||
"just_created": {
|
||||
"mappings": {
|
||||
"0": {
|
||||
"then": "Je hebt dit punt net toegevoegd! Bedankt om deze info met iedereen te delen en om de mensen wereldwijd te helpen."
|
||||
}
|
||||
}
|
||||
},
|
||||
"last_edit": {
|
||||
"render": {
|
||||
"special": {
|
||||
|
|
15
test.ts
15
test.ts
|
@ -9,10 +9,12 @@ import { UIEventSource } from "./Logic/UIEventSource"
|
|||
import { VariableUiElement } from "./UI/Base/VariableUIElement"
|
||||
import { FixedUiElement } from "./UI/Base/FixedUiElement"
|
||||
import Title from "./UI/Base/Title"
|
||||
import WaySplitMap from "./UI/BigComponents/WaySplitMap.svelte"
|
||||
import { WikipediaBoxOptions } from "./UI/Wikipedia/WikipediaBoxOptions"
|
||||
import Wikipedia from "./Logic/Web/Wikipedia"
|
||||
import WikipediaPanel from "./UI/Wikipedia/WikipediaPanel.svelte"
|
||||
import SvelteUIElement from "./UI/Base/SvelteUIElement"
|
||||
import { OsmObject } from "./Logic/Osm/OsmObject"
|
||||
import SplitRoadWizard from "./UI/Popup/SplitRoadWizard"
|
||||
import LanguagePicker from "./UI/LanguagePicker"
|
||||
import { Utils } from "./Utils"
|
||||
|
||||
function testspecial() {
|
||||
const layout = new LayoutConfig(<any>theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
|
||||
|
@ -47,7 +49,12 @@ function testinput() {
|
|||
}
|
||||
|
||||
async function testWaySplit() {
|
||||
new SplitRoadWizard("way/28717919", {}).SetClass("w-full h-full").AttachTo("maindiv")
|
||||
const ids = new UIEventSource(["Q42", "Q1"])
|
||||
new SvelteUIElement(WikipediaPanel, { wikiIds: ids, addEntry: true }).AttachTo("maindiv")
|
||||
new LanguagePicker(["en", "nl"]).AttachTo("extradiv")
|
||||
await Utils.waitFor(5000)
|
||||
ids.data.push("Q430")
|
||||
ids.ping()
|
||||
}
|
||||
testWaySplit().then((_) => console.log("inited"))
|
||||
//testinput()
|
||||
|
|
Loading…
Reference in a new issue