Refactoring: port wikipedia panel to Svelte

This commit is contained in:
Pieter Vander Vennet 2023-04-21 16:02:36 +02:00
parent 24f7610d0a
commit d8e14927c8
32 changed files with 362 additions and 847 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

@ -0,0 +1,7 @@
import { UIEventSource } from "../../Logic/UIEventSource"
export interface WikipediaBoxOptions {
addHeader?: boolean
firstParagraphOnly?: true | boolean
allowToAdd?: boolean
}

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

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

View file

@ -44,4 +44,4 @@
}
],
"syncSelection": "global"
}
}

View file

@ -2110,4 +2110,4 @@
}
]
}
}
}

View file

@ -695,4 +695,4 @@
"enableShareScreen": false,
"enableMoreQuests": false,
"credits": "Pieter Vander Vennet, Rob Nickerson, Russ Garrett"
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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