Refactoring: remove import flow, fix various issues, get PDF-export working (but not quite)
This commit is contained in:
parent
2149fc1a1d
commit
f7eaec2243
36 changed files with 739 additions and 3930 deletions
|
@ -465,7 +465,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
|||
changeType: "conflation",
|
||||
}
|
||||
)
|
||||
allChanges.push(...(await addExtraTags.CreateChangeDescriptions(changes)))
|
||||
allChanges.push(...(await addExtraTags.CreateChangeDescriptions()))
|
||||
}
|
||||
|
||||
const newCoordinates = [...this.targetCoordinates]
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Conversion } from "./Conversion"
|
||||
import {Conversion} from "./Conversion"
|
||||
import LayerConfig from "../LayerConfig"
|
||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import {LayerConfigJson} from "../Json/LayerConfigJson"
|
||||
import Translations from "../../../UI/i18n/Translations"
|
||||
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"
|
||||
import { Translation, TypedTranslation } from "../../../UI/i18n/Translation"
|
||||
import {Translation, TypedTranslation} from "../../../UI/i18n/Translation"
|
||||
|
||||
export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, LayerConfigJson> {
|
||||
/**
|
||||
|
@ -82,7 +82,7 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, L
|
|||
return { ...translation.translations, _context: translation.context }
|
||||
}
|
||||
|
||||
function trs<T>(translation: TypedTranslation<T>, subs: T): object {
|
||||
function trs<T>(translation: TypedTranslation<T>, subs: T): Record<string, string> {
|
||||
return { ...translation.Subs(subs).translations, _context: translation.context }
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ import Translations from "./i18n/Translations"
|
|||
import Constants from "../Models/Constants"
|
||||
import LanguagePicker from "./LanguagePicker"
|
||||
import IndexText from "./BigComponents/IndexText"
|
||||
import { ImportViewerLinks } from "./BigComponents/UserInformation"
|
||||
import { LoginToggle } from "./Popup/LoginButton"
|
||||
import { ImmutableStore } from "../Logic/UIEventSource"
|
||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||
|
@ -29,7 +28,6 @@ export default class AllThemesGui {
|
|||
osmConnection,
|
||||
featureSwitchUserbadge: new ImmutableStore(true),
|
||||
}),
|
||||
new ImportViewerLinks(state.osmConnection),
|
||||
Translations.t.general.aboutMapComplete.intro.SetClass("link-underline"),
|
||||
new FixedUiElement("v" + Constants.vNumber).SetClass("block"),
|
||||
])
|
||||
|
|
|
@ -1,31 +1,30 @@
|
|||
import { UIElement } from "../UIElement"
|
||||
import {UIElement} from "../UIElement"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import {Store} from "../../Logic/UIEventSource"
|
||||
import ExtraLinkConfig from "../../Models/ThemeConfig/ExtraLinkConfig"
|
||||
import Img from "../Base/Img"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import {SubtleButton} from "../Base/SubtleButton"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import Loc from "../../Models/Loc"
|
||||
import Locale from "../i18n/Locale"
|
||||
import { Utils } from "../../Utils"
|
||||
import {Utils} from "../../Utils"
|
||||
import Svg from "../../Svg"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import {Translation} from "../i18n/Translation"
|
||||
|
||||
interface ExtraLinkButtonState {
|
||||
layout: { id: string; title: Translation }
|
||||
featureSwitches: { featureSwitchWelcomeMessage: Store<boolean> },
|
||||
mapProperties: {
|
||||
location: Store<{ lon: number, lat: number }>;
|
||||
zoom: Store<number>
|
||||
}
|
||||
}
|
||||
export default class ExtraLinkButton extends UIElement {
|
||||
private readonly _config: ExtraLinkConfig
|
||||
private readonly state: {
|
||||
layoutToUse: { id: string; title: Translation }
|
||||
featureSwitchWelcomeMessage: UIEventSource<boolean>
|
||||
locationControl: UIEventSource<Loc>
|
||||
}
|
||||
private readonly state: ExtraLinkButtonState
|
||||
|
||||
constructor(
|
||||
state: {
|
||||
featureSwitchWelcomeMessage: UIEventSource<boolean>
|
||||
locationControl: UIEventSource<Loc>
|
||||
layoutToUse: { id: string; title: Translation }
|
||||
},
|
||||
state: ExtraLinkButtonState,
|
||||
config: ExtraLinkConfig
|
||||
) {
|
||||
super()
|
||||
|
@ -41,19 +40,18 @@ export default class ExtraLinkButton extends UIElement {
|
|||
const c = this._config
|
||||
|
||||
const isIframe = window !== window.top
|
||||
|
||||
if (c.requirements?.has("iframe") && !isIframe) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (c.requirements?.has("no-iframe") && isIframe) {
|
||||
return undefined
|
||||
return undefined
|
||||
}
|
||||
|
||||
let link: BaseUIElement
|
||||
const theme = this.state.layoutToUse?.id ?? ""
|
||||
const theme = this.state.layout?.id ?? ""
|
||||
const basepath = window.location.host
|
||||
const href = this.state.locationControl.map((loc) => {
|
||||
const href = this.state.mapProperties.location.map((loc) => {
|
||||
const subs = {
|
||||
...loc,
|
||||
theme: theme,
|
||||
|
@ -61,7 +59,7 @@ export default class ExtraLinkButton extends UIElement {
|
|||
language: Locale.language.data,
|
||||
}
|
||||
return Utils.SubstituteKeys(c.href, subs)
|
||||
})
|
||||
}, [this.state.mapProperties.zoom])
|
||||
|
||||
let img: BaseUIElement = Svg.pop_out_ui()
|
||||
if (c.icon !== undefined) {
|
||||
|
@ -71,7 +69,7 @@ export default class ExtraLinkButton extends UIElement {
|
|||
let text: Translation
|
||||
if (c.text === undefined) {
|
||||
text = Translations.t.general.screenToSmall.Subs({
|
||||
theme: this.state.layoutToUse.title,
|
||||
theme: this.state.layout.title,
|
||||
})
|
||||
} else {
|
||||
text = c.text.Clone()
|
||||
|
@ -83,11 +81,11 @@ export default class ExtraLinkButton extends UIElement {
|
|||
})
|
||||
|
||||
if (c.requirements?.has("no-welcome-message")) {
|
||||
link = new Toggle(undefined, link, this.state.featureSwitchWelcomeMessage)
|
||||
link = new Toggle(undefined, link, this.state.featureSwitches.featureSwitchWelcomeMessage)
|
||||
}
|
||||
|
||||
if (c.requirements?.has("welcome-message")) {
|
||||
link = new Toggle(link, undefined, this.state.featureSwitchWelcomeMessage)
|
||||
link = new Toggle(link, undefined, this.state.featureSwitches.featureSwitchWelcomeMessage)
|
||||
}
|
||||
|
||||
return link
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import MapControlButton from "../MapControlButton"
|
||||
import Svg from "../../Svg"
|
||||
import AllDownloads from "./AllDownloads"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Lazy from "../Base/Lazy"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { DefaultGuiState } from "../DefaultGuiState"
|
||||
|
||||
export default class LeftControls extends Combine {
|
||||
constructor(state: FeaturePipelineState, guiState: DefaultGuiState) {
|
||||
const currentViewFL = state.currentView?.layer
|
||||
const currentViewAction = new Toggle(
|
||||
new Lazy(() => {
|
||||
const feature: Store<any> = state.currentView.features.map((ffs) => ffs[0])
|
||||
const icon = new VariableUiElement(
|
||||
feature.map((feature) => {
|
||||
const defaultIcon = Svg.checkbox_empty_svg()
|
||||
if (feature === undefined) {
|
||||
return defaultIcon
|
||||
}
|
||||
const tags = { ...feature.properties, button: "yes" }
|
||||
const elem = currentViewFL.layerDef.mapRendering[0]?.GetSimpleIcon(
|
||||
new UIEventSource(tags)
|
||||
)
|
||||
if (elem === undefined) {
|
||||
return defaultIcon
|
||||
}
|
||||
return elem
|
||||
})
|
||||
).SetClass("inline-block w-full h-full")
|
||||
|
||||
feature.map((feature) => {
|
||||
if (feature === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const tagsSource = state.allElements.getEventSourceById(feature.properties.id)
|
||||
return new FeatureInfoBox(tagsSource, currentViewFL.layerDef, state, {
|
||||
hashToShow: "currentview",
|
||||
isShown: guiState.currentViewControlIsOpened,
|
||||
})
|
||||
})
|
||||
|
||||
return new MapControlButton(icon)
|
||||
}).onClick(() => {
|
||||
guiState.currentViewControlIsOpened.setData(true)
|
||||
}),
|
||||
|
||||
undefined,
|
||||
new UIEventSource<boolean>(
|
||||
currentViewFL !== undefined && currentViewFL?.layerDef?.tagRenderings !== null
|
||||
)
|
||||
)
|
||||
|
||||
new AllDownloads(guiState.downloadControlIsOpened, state)
|
||||
|
||||
super([currentViewAction])
|
||||
|
||||
this.SetClass("flex flex-col")
|
||||
}
|
||||
}
|
|
@ -1,22 +1,19 @@
|
|||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import {VariableUiElement} from "../Base/VariableUIElement"
|
||||
import {Translation} from "../i18n/Translation"
|
||||
import Svg from "../../Svg"
|
||||
import Combine from "../Base/Combine"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Utils } from "../../Utils"
|
||||
import {Store, UIEventSource} from "../../Logic/UIEventSource"
|
||||
import {Utils} from "../../Utils"
|
||||
import Translations from "../i18n/Translations"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import Loc from "../../Models/Loc"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import { InputElement } from "../Input/InputElement"
|
||||
import { CheckBox } from "../Input/Checkboxes"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import {InputElement} from "../Input/InputElement"
|
||||
import {CheckBox} from "../Input/Checkboxes"
|
||||
import {SubtleButton} from "../Base/SubtleButton"
|
||||
import LZString from "lz-string"
|
||||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import {SpecialVisualizationState} from "../SpecialVisualization"
|
||||
|
||||
export default class ShareScreen extends Combine {
|
||||
class ShareScreen extends Combine{
|
||||
constructor(state: SpecialVisualizationState) {
|
||||
const layout = state?.layout
|
||||
const tr = Translations.t.general.sharescreen
|
||||
|
@ -63,7 +60,7 @@ export default class ShareScreen extends Combine {
|
|||
return "layer-" + flayer.layerDef.id + "=" + flayer.isDisplayed.data
|
||||
}
|
||||
|
||||
const currentLayer: Store<{ id: string; name: string } | undefined> =
|
||||
const currentLayer: Store<{ id: string; name: string | Record<string, string> } | undefined> =
|
||||
state.mapProperties.rasterLayer.map((l) => l?.properties)
|
||||
const currentBackground = new VariableUiElement(
|
||||
currentLayer.map((layer) => {
|
||||
|
@ -93,13 +90,13 @@ export default class ShareScreen extends Combine {
|
|||
(includeLayerSelection) => {
|
||||
if (includeLayerSelection) {
|
||||
return Utils.NoNull(
|
||||
state.layerState.filteredLayers.map(fLayerToParam)
|
||||
Array.from( state.layerState.filteredLayers.values()).map(fLayerToParam)
|
||||
).join("&")
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
state.filteredLayers.data.map((flayer) => flayer.isDisplayed)
|
||||
Array.from(state.layerState.filteredLayers.values()).map((flayer) => flayer.isDisplayed)
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -1,88 +0,0 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import LanguagePicker from "../LanguagePicker"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { LoginToggle } from "../Popup/LoginButton"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import LoggedInUserIndicator from "../LoggedInUserIndicator"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import Loc from "../../Models/Loc"
|
||||
import { DefaultGuiState } from "../DefaultGuiState"
|
||||
|
||||
export default class ThemeIntroductionPanel extends Combine {
|
||||
constructor(
|
||||
isShown: UIEventSource<boolean>,
|
||||
currentTab: UIEventSource<number>,
|
||||
state: {
|
||||
featureSwitchMoreQuests: UIEventSource<boolean>
|
||||
featureSwitchAddNew: UIEventSource<boolean>
|
||||
featureSwitchUserbadge: UIEventSource<boolean>
|
||||
layoutToUse: LayoutConfig
|
||||
osmConnection: OsmConnection
|
||||
currentBounds: Store<BBox>
|
||||
locationControl: UIEventSource<Loc>
|
||||
defaultGuiState: DefaultGuiState
|
||||
},
|
||||
guistate?: { userInfoIsOpened: UIEventSource<boolean> }
|
||||
) {
|
||||
const t = Translations.t.general
|
||||
const layout = state.layoutToUse
|
||||
|
||||
const languagePicker = new LanguagePicker(layout.language, t.pickLanguage.Clone())
|
||||
|
||||
const toTheMap = new SubtleButton(
|
||||
undefined,
|
||||
t.openTheMap.Clone().SetClass("text-xl font-bold w-full text-center")
|
||||
)
|
||||
.onClick(() => {
|
||||
isShown.setData(false)
|
||||
})
|
||||
.SetClass("only-on-mobile")
|
||||
|
||||
const loggedInUserInfo = new LoggedInUserIndicator(state.osmConnection, {
|
||||
firstLine: Translations.t.general.welcomeBack.Clone(),
|
||||
})
|
||||
if (guistate?.userInfoIsOpened) {
|
||||
loggedInUserInfo.onClick(() => {
|
||||
guistate.userInfoIsOpened.setData(true)
|
||||
})
|
||||
}
|
||||
|
||||
const loginStatus = new Toggle(
|
||||
new LoginToggle(
|
||||
loggedInUserInfo,
|
||||
new Combine([
|
||||
Translations.t.general.loginWithOpenStreetMap.SetClass("text-xl font-bold"),
|
||||
Translations.t.general.loginOnlyNeededToEdit.Clone().SetClass("font-bold"),
|
||||
]).SetClass("flex flex-col"),
|
||||
state
|
||||
),
|
||||
undefined,
|
||||
state.featureSwitchUserbadge
|
||||
)
|
||||
|
||||
const hasPresets = layout.layers.some((l) => l.presets?.length > 0)
|
||||
super([
|
||||
layout.description.Clone().SetClass("block mb-4"),
|
||||
new Combine([
|
||||
t.welcomeExplanation.general,
|
||||
hasPresets
|
||||
? Toggle.If(state.featureSwitchAddNew, () => t.welcomeExplanation.addNew)
|
||||
: undefined,
|
||||
]).SetClass("flex flex-col mt-2"),
|
||||
|
||||
toTheMap,
|
||||
loginStatus.SetClass("block mt-6 pt-2 md:border-t-2 border-dotted border-gray-400"),
|
||||
layout.descriptionTail?.Clone().SetClass("block mt-4"),
|
||||
|
||||
languagePicker?.SetClass("block mt-4 pb-8 border-b-2 border-dotted border-gray-400"),
|
||||
|
||||
...layout.CustomCodeSnippets(),
|
||||
])
|
||||
|
||||
this.SetClass("link-underline")
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
import Translations from "../i18n/Translations"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import Combine from "../Base/Combine"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Svg from "../../Svg"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Img from "../Base/Img"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import Link from "../Base/Link"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Loc from "../../Models/Loc"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Showdown from "showdown"
|
||||
import LanguagePicker from "../LanguagePicker"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import Constants from "../../Models/Constants"
|
||||
|
||||
export class ImportViewerLinks extends VariableUiElement {
|
||||
constructor(osmConnection: OsmConnection) {
|
||||
super(
|
||||
osmConnection.userDetails.map((ud) => {
|
||||
if (ud.csCount < Constants.userJourney.importHelperUnlock) {
|
||||
return undefined
|
||||
}
|
||||
return new Combine([
|
||||
new SubtleButton(undefined, Translations.t.importHelper.title, {
|
||||
url: "import_helper.html",
|
||||
}),
|
||||
new SubtleButton(Svg.note_svg(), Translations.t.importInspector.title, {
|
||||
url: "import_viewer.html",
|
||||
}),
|
||||
])
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class UserInformationMainPanel extends VariableUiElement {
|
||||
private readonly settings: UIEventSource<Record<string, BaseUIElement>>
|
||||
private readonly userInfoFocusedQuestion?: UIEventSource<string>
|
||||
|
||||
constructor(
|
||||
osmConnection: OsmConnection,
|
||||
locationControl: UIEventSource<Loc>,
|
||||
layout: LayoutConfig,
|
||||
isOpened: UIEventSource<boolean>,
|
||||
userInfoFocusedQuestion?: UIEventSource<string>
|
||||
) {
|
||||
const settings = new UIEventSource<Record<string, BaseUIElement>>({})
|
||||
|
||||
super()
|
||||
this.settings = settings
|
||||
this.userInfoFocusedQuestion = userInfoFocusedQuestion
|
||||
const self = this
|
||||
userInfoFocusedQuestion.addCallbackD((_) => {
|
||||
self.focusOnSelectedQuestion()
|
||||
})
|
||||
}
|
||||
|
||||
public focusOnSelectedQuestion() {
|
||||
const focusedId = this.userInfoFocusedQuestion.data
|
||||
console.log("Focusing on", focusedId, this.settings.data[focusedId])
|
||||
if (focusedId === undefined) {
|
||||
return
|
||||
}
|
||||
this.settings.data[focusedId]?.ScrollIntoView()
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
import Toggle from "./Input/Toggle"
|
||||
import LeftControls from "./BigComponents/LeftControls"
|
||||
import CenterMessageBox from "./CenterMessageBox"
|
||||
import { DefaultGuiState } from "./DefaultGuiState"
|
||||
import Combine from "./Base/Combine"
|
||||
import ExtraLinkButton from "./BigComponents/ExtraLinkButton"
|
||||
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
|
||||
|
||||
/**
|
||||
* The default MapComplete GUI initializer
|
||||
*
|
||||
* Adds a welcome pane, control buttons, ... etc to index.html
|
||||
*/
|
||||
export default class DefaultGUI {
|
||||
private readonly guiState: DefaultGuiState
|
||||
private readonly geolocationHandler: GeoLocationHandler | undefined
|
||||
|
||||
constructor(guiState: DefaultGuiState) {
|
||||
this.guiState = guiState
|
||||
}
|
||||
|
||||
public setup() {
|
||||
const extraLink = Toggle.If(
|
||||
state.featureSwitchExtraLinkEnabled,
|
||||
() => new ExtraLinkButton(state, state.layoutToUse.extraLink)
|
||||
)
|
||||
|
||||
new Combine([extraLink]).SetClass("flex flex-col").AttachTo("top-left")
|
||||
|
||||
new Combine([
|
||||
new ExtraLinkButton(state, {
|
||||
...state.layoutToUse.extraLink,
|
||||
newTab: true,
|
||||
requirements: new Set<
|
||||
"iframe" | "no-iframe" | "welcome-message" | "no-welcome-message"
|
||||
>(),
|
||||
}),
|
||||
])
|
||||
.SetClass("flex items-center justify-center normal-background h-full")
|
||||
.AttachTo("on-small-screen")
|
||||
|
||||
const guiState = this.guiState
|
||||
new LeftControls(state, guiState).AttachTo("bottom-left")
|
||||
|
||||
new CenterMessageBox(state).AttachTo("centermessage")
|
||||
document?.getElementById("centermessage")?.classList?.add("pointer-events-none")
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
import { UIEventSource } from "../Logic/UIEventSource"
|
||||
import Hash from "../Logic/Web/Hash"
|
||||
|
||||
export class DefaultGuiState {
|
||||
public readonly welcomeMessageIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>(
|
||||
false
|
||||
)
|
||||
|
||||
public readonly menuIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
|
||||
public readonly downloadControlIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>(
|
||||
false
|
||||
)
|
||||
public readonly filterViewIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
public readonly copyrightViewIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>(
|
||||
false
|
||||
)
|
||||
public readonly currentViewControlIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>(
|
||||
false
|
||||
)
|
||||
public readonly userInfoIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
public readonly userInfoFocusedQuestion: UIEventSource<string> = new UIEventSource<string>(
|
||||
undefined
|
||||
)
|
||||
|
||||
private readonly sources: Record<string, UIEventSource<boolean>> = {
|
||||
welcome: this.welcomeMessageIsOpened,
|
||||
download: this.downloadControlIsOpened,
|
||||
filters: this.filterViewIsOpened,
|
||||
copyright: this.copyrightViewIsOpened,
|
||||
currentview: this.currentViewControlIsOpened,
|
||||
userinfo: this.userInfoIsOpened,
|
||||
}
|
||||
|
||||
constructor() {
|
||||
const self = this
|
||||
this.userInfoIsOpened.addCallback((isOpen) => {
|
||||
if (!isOpen) {
|
||||
console.log("Resetting focused question")
|
||||
self.userInfoFocusedQuestion.setData(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
this.sources[Hash.hash.data?.toLowerCase()]?.setData(true)
|
||||
|
||||
if (Hash.hash.data === "" || Hash.hash.data === undefined) {
|
||||
this.welcomeMessageIsOpened.setData(true)
|
||||
}
|
||||
}
|
||||
|
||||
public closeAll(except) {
|
||||
for (const sourceKey in this.sources) {
|
||||
this.sources[sourceKey].setData(false)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,128 +0,0 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import { FlowStep } from "./FlowStep"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import ValidatedTextField from "../Input/ValidatedTextField"
|
||||
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
|
||||
import Title from "../Base/Title"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Svg from "../../Svg"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
export class AskMetadata
|
||||
extends Combine
|
||||
implements
|
||||
FlowStep<{
|
||||
features: any[]
|
||||
wikilink: string
|
||||
intro: string
|
||||
source: string
|
||||
theme: string
|
||||
}>
|
||||
{
|
||||
public readonly Value: Store<{
|
||||
features: any[]
|
||||
wikilink: string
|
||||
intro: string
|
||||
source: string
|
||||
theme: string
|
||||
}>
|
||||
public readonly IsValid: Store<boolean>
|
||||
|
||||
constructor(params: { features: any[]; theme: string }) {
|
||||
const t = Translations.t.importHelper.askMetadata
|
||||
const introduction = ValidatedTextField.ForType("text").ConstructInputElement({
|
||||
value: LocalStorageSource.Get("import-helper-introduction-text"),
|
||||
inputStyle: "width: 100%",
|
||||
})
|
||||
|
||||
const wikilink = ValidatedTextField.ForType("url").ConstructInputElement({
|
||||
value: LocalStorageSource.Get("import-helper-wikilink-text"),
|
||||
inputStyle: "width: 100%",
|
||||
})
|
||||
|
||||
const source = ValidatedTextField.ForType("string").ConstructInputElement({
|
||||
value: LocalStorageSource.Get("import-helper-source-text"),
|
||||
inputStyle: "width: 100%",
|
||||
})
|
||||
|
||||
super([
|
||||
new Title(t.title),
|
||||
t.intro.Subs({ count: params.features.length }),
|
||||
t.giveDescription,
|
||||
introduction.SetClass("w-full border border-black"),
|
||||
t.giveSource,
|
||||
source.SetClass("w-full border border-black"),
|
||||
t.giveWikilink,
|
||||
wikilink.SetClass("w-full border border-black"),
|
||||
new VariableUiElement(
|
||||
wikilink.GetValue().map((wikilink) => {
|
||||
try {
|
||||
const url = new URL(wikilink)
|
||||
if (url.hostname.toLowerCase() !== "wiki.openstreetmap.org") {
|
||||
return t.shouldBeOsmWikilink.SetClass("alert")
|
||||
}
|
||||
|
||||
if (url.pathname.toLowerCase() === "/wiki/main_page") {
|
||||
return t.shouldNotBeHomepage.SetClass("alert")
|
||||
}
|
||||
} catch (e) {
|
||||
return t.shouldBeUrl.SetClass("alert")
|
||||
}
|
||||
})
|
||||
),
|
||||
t.orDownload,
|
||||
new SubtleButton(Svg.download_svg(), t.downloadGeojson).OnClickWithLoading(
|
||||
"Preparing your download",
|
||||
async () => {
|
||||
const geojson = {
|
||||
type: "FeatureCollection",
|
||||
features: params.features,
|
||||
}
|
||||
Utils.offerContentsAsDownloadableFile(
|
||||
JSON.stringify(geojson),
|
||||
"prepared_import_" + params.theme + ".geojson",
|
||||
{
|
||||
mimetype: "application/vnd.geo+json",
|
||||
}
|
||||
)
|
||||
}
|
||||
),
|
||||
])
|
||||
this.SetClass("flex flex-col")
|
||||
|
||||
this.Value = introduction.GetValue().map(
|
||||
(intro) => {
|
||||
return {
|
||||
features: params.features,
|
||||
wikilink: wikilink.GetValue().data,
|
||||
intro,
|
||||
source: source.GetValue().data,
|
||||
theme: params.theme,
|
||||
}
|
||||
},
|
||||
[wikilink.GetValue(), source.GetValue()]
|
||||
)
|
||||
|
||||
this.IsValid = this.Value.map((obj) => {
|
||||
if (obj === undefined) {
|
||||
return false
|
||||
}
|
||||
if ([obj.features, obj.intro, obj.wikilink, obj.source].some((v) => v === undefined)) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(obj.wikilink)
|
||||
if (url.hostname.toLowerCase() !== "wiki.openstreetmap.org") {
|
||||
return false
|
||||
}
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,196 +0,0 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import { FlowStep } from "./FlowStep"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import CreateNoteImportLayer from "../../Models/ThemeConfig/Conversion/CreateNoteImportLayer"
|
||||
import FilteredLayer, { FilterState } from "../../Models/FilteredLayer"
|
||||
import GeoJsonSource from "../../Logic/FeatureSource/Sources/GeoJsonSource"
|
||||
import MetaTagging from "../../Logic/MetaTagging"
|
||||
import FilteringFeatureSource from "../../Logic/FeatureSource/Sources/FilteringFeatureSource"
|
||||
import Minimap from "../Base/Minimap"
|
||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
|
||||
import FeatureInfoBox from "../Popup/FeatureInfoBox"
|
||||
import { ImportUtils } from "./ImportUtils"
|
||||
import import_candidate from "../../assets/layers/import_candidate/import_candidate.json"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import Title from "../Base/Title"
|
||||
import Loading from "../Base/Loading"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import known_layers from "../../assets/generated/known_layers.json"
|
||||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Feature } from "geojson"
|
||||
import DivContainer from "../Base/DivContainer"
|
||||
|
||||
/**
|
||||
* Filters out points for which the import-note already exists, to prevent duplicates
|
||||
*/
|
||||
export class CompareToAlreadyExistingNotes
|
||||
extends Combine
|
||||
implements FlowStep<{ bbox: BBox; layer: LayerConfig; features: any[]; theme: string }>
|
||||
{
|
||||
public IsValid: Store<boolean>
|
||||
public Value: Store<{ bbox: BBox; layer: LayerConfig; features: any[]; theme: string }>
|
||||
|
||||
constructor(state, params: { bbox: BBox; layer: LayerConfig; features: any[]; theme: string }) {
|
||||
const t = Translations.t.importHelper.compareToAlreadyExistingNotes
|
||||
const layerConfig = known_layers.layers.filter((l) => l.id === params.layer.id)[0]
|
||||
if (layerConfig === undefined) {
|
||||
console.error("WEIRD: layer not found in the builtin layer overview")
|
||||
}
|
||||
const importLayerJson = new CreateNoteImportLayer(150).convertStrict(
|
||||
<LayerConfigJson>layerConfig,
|
||||
"CompareToAlreadyExistingNotes"
|
||||
)
|
||||
const importLayer = new LayerConfig(importLayerJson, "import-layer-dynamic")
|
||||
const flayer: FilteredLayer = {
|
||||
appliedFilters: new UIEventSource<Map<string, FilterState>>(
|
||||
new Map<string, FilterState>()
|
||||
),
|
||||
isDisplayed: new UIEventSource<boolean>(true),
|
||||
layerDef: importLayer,
|
||||
}
|
||||
const allNotesWithinBbox = new GeoJsonSource(flayer, params.bbox.padAbsolute(0.0001))
|
||||
|
||||
allNotesWithinBbox.features.map((f) =>
|
||||
MetaTagging.addMetatags(
|
||||
f,
|
||||
{
|
||||
getFeaturesWithin: () => [],
|
||||
getFeatureById: () => undefined,
|
||||
},
|
||||
importLayer,
|
||||
state,
|
||||
{
|
||||
includeDates: true,
|
||||
// We assume that the non-dated metatags are already set by the cache generator
|
||||
includeNonDates: true,
|
||||
}
|
||||
)
|
||||
)
|
||||
const alreadyOpenImportNotes = new FilteringFeatureSource(
|
||||
state,
|
||||
undefined,
|
||||
allNotesWithinBbox
|
||||
)
|
||||
const map = Minimap.createMiniMap()
|
||||
map.SetClass("w-full").SetStyle("height: 500px")
|
||||
|
||||
const comparisonMap = Minimap.createMiniMap({
|
||||
location: map.location,
|
||||
})
|
||||
comparisonMap.SetClass("w-full").SetStyle("height: 500px")
|
||||
|
||||
new ShowDataLayer({
|
||||
layerToShow: importLayer,
|
||||
state,
|
||||
zoomToFeatures: true,
|
||||
leafletMap: map.leafletMap,
|
||||
features: alreadyOpenImportNotes,
|
||||
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state),
|
||||
})
|
||||
|
||||
const maxDistance = new UIEventSource<number>(10)
|
||||
|
||||
const partitionedImportPoints = ImportUtils.partitionFeaturesIfNearby(
|
||||
params,
|
||||
alreadyOpenImportNotes.features.map((ff) => ({ features: ff.map((ff) => ff.feature) })),
|
||||
maxDistance
|
||||
)
|
||||
|
||||
new ShowDataLayer({
|
||||
layerToShow: new LayerConfig(import_candidate),
|
||||
state,
|
||||
zoomToFeatures: true,
|
||||
leafletMap: comparisonMap.leafletMap,
|
||||
features: new StaticFeatureSource(
|
||||
partitionedImportPoints.map((p) => <Feature[]>p.hasNearby)
|
||||
),
|
||||
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state),
|
||||
})
|
||||
|
||||
super([
|
||||
new Title(t.titleLong),
|
||||
new VariableUiElement(
|
||||
alreadyOpenImportNotes.features.map(
|
||||
(notesWithImport) => {
|
||||
if (
|
||||
allNotesWithinBbox.state.data !== undefined &&
|
||||
allNotesWithinBbox.state.data["error"] !== undefined
|
||||
) {
|
||||
const error = allNotesWithinBbox.state.data["error"]
|
||||
t.loadingFailed.Subs({ error })
|
||||
}
|
||||
if (
|
||||
allNotesWithinBbox.features.data === undefined ||
|
||||
allNotesWithinBbox.features.data.length === 0
|
||||
) {
|
||||
return new Loading(t.loading)
|
||||
}
|
||||
if (notesWithImport.length === 0) {
|
||||
return t.noPreviousNotesFound.SetClass("thanks")
|
||||
}
|
||||
return new Combine([
|
||||
t.mapExplanation.Subs(params.features),
|
||||
map,
|
||||
new DivContainer("fullscreen"),
|
||||
new VariableUiElement(
|
||||
partitionedImportPoints.map(({ noNearby, hasNearby }) => {
|
||||
if (noNearby.length === 0) {
|
||||
// Nothing can be imported
|
||||
return t.completelyImported
|
||||
.SetClass("alert w-full block")
|
||||
.SetStyle("padding: 0.5rem")
|
||||
}
|
||||
|
||||
if (hasNearby.length === 0) {
|
||||
// All points can be imported
|
||||
return t.nothingNearby
|
||||
.SetClass("thanks w-full block")
|
||||
.SetStyle("padding: 0.5rem")
|
||||
}
|
||||
|
||||
return new Combine([
|
||||
t.someNearby
|
||||
.Subs({
|
||||
hasNearby: hasNearby.length,
|
||||
distance: maxDistance.data,
|
||||
})
|
||||
.SetClass("alert"),
|
||||
t.wontBeImported,
|
||||
comparisonMap.SetClass("w-full"),
|
||||
]).SetClass("w-full")
|
||||
})
|
||||
),
|
||||
]).SetClass("flex flex-col")
|
||||
},
|
||||
[allNotesWithinBbox.features, allNotesWithinBbox.state]
|
||||
)
|
||||
),
|
||||
])
|
||||
this.SetClass("flex flex-col")
|
||||
this.Value = partitionedImportPoints.map(({ noNearby }) => ({
|
||||
features: noNearby,
|
||||
bbox: params.bbox,
|
||||
layer: params.layer,
|
||||
theme: params.theme,
|
||||
}))
|
||||
|
||||
this.IsValid = alreadyOpenImportNotes.features.map(
|
||||
(ff) => {
|
||||
if (allNotesWithinBbox.features.data.length === 0) {
|
||||
// Not yet loaded
|
||||
return false
|
||||
}
|
||||
if (ff.length == 0) {
|
||||
// No import notes at all
|
||||
return true
|
||||
}
|
||||
|
||||
return partitionedImportPoints.data.noNearby.length > 0 // at least _something_ can be imported
|
||||
},
|
||||
[partitionedImportPoints, allNotesWithinBbox.features]
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import { FlowStep } from "./FlowStep"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Link from "../Base/Link"
|
||||
import CheckBoxes from "../Input/Checkboxes"
|
||||
import Title from "../Base/Title"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
||||
export class ConfirmProcess
|
||||
extends Combine
|
||||
implements FlowStep<{ features: any[]; theme: string }>
|
||||
{
|
||||
public IsValid: Store<boolean>
|
||||
public Value: Store<{ features: any[]; theme: string }>
|
||||
|
||||
constructor(v: { features: any[]; theme: string }) {
|
||||
const t = Translations.t.importHelper.confirmProcess
|
||||
const elements = [
|
||||
new Link(
|
||||
t.readImportGuidelines,
|
||||
"https://wiki.openstreetmap.org/wiki/Import_guidelines",
|
||||
true
|
||||
),
|
||||
t.contactedCommunity,
|
||||
t.licenseIsCompatible,
|
||||
t.wikipageIsMade,
|
||||
]
|
||||
const toConfirm = new CheckBoxes(elements)
|
||||
|
||||
super([new Title(t.titleLong), toConfirm])
|
||||
this.SetClass("link-underline")
|
||||
this.IsValid = toConfirm.GetValue().map((selected) => elements.length == selected.length)
|
||||
this.Value = new UIEventSource<{ features: any[]; theme: string }>(v)
|
||||
}
|
||||
}
|
|
@ -1,359 +0,0 @@
|
|||
import { BBox } from "../../Logic/BBox";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import Combine from "../Base/Combine";
|
||||
import Title from "../Base/Title";
|
||||
import { Overpass } from "../../Logic/Osm/Overpass";
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource";
|
||||
import Constants from "../../Models/Constants";
|
||||
import RelationsTracker from "../../Logic/Osm/RelationsTracker";
|
||||
import { VariableUiElement } from "../Base/VariableUIElement";
|
||||
import { FlowStep } from "./FlowStep";
|
||||
import Loading from "../Base/Loading";
|
||||
import { SubtleButton } from "../Base/SubtleButton";
|
||||
import Svg from "../../Svg";
|
||||
import { Utils } from "../../Utils";
|
||||
import { IdbLocalStorage } from "../../Logic/Web/IdbLocalStorage";
|
||||
import Minimap from "../Base/Minimap";
|
||||
import BaseLayer from "../../Models/BaseLayer";
|
||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
||||
import Loc from "../../Models/Loc";
|
||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||
import ValidatedTextField from "../Input/ValidatedTextField";
|
||||
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource";
|
||||
import import_candidate from "../../assets/layers/import_candidate/import_candidate.json";
|
||||
import { GeoOperations } from "../../Logic/GeoOperations";
|
||||
import FeatureInfoBox from "../Popup/FeatureInfoBox";
|
||||
import { ImportUtils } from "./ImportUtils";
|
||||
import Translations from "../i18n/Translations";
|
||||
import currentview from "../../assets/layers/current_view/current_view.json";
|
||||
import { CheckBox } from "../Input/Checkboxes";
|
||||
import { Feature, FeatureCollection, Point } from "geojson";
|
||||
import DivContainer from "../Base/DivContainer";
|
||||
|
||||
/**
|
||||
* Given the data to import, the bbox and the layer, will query overpass for similar items
|
||||
*/
|
||||
export default class ConflationChecker
|
||||
extends Combine
|
||||
implements FlowStep<{ features: Feature<Point>[]; theme: string }>
|
||||
{
|
||||
public readonly IsValid
|
||||
public readonly Value: Store<{ features: Feature<Point>[]; theme: string }>
|
||||
|
||||
constructor(
|
||||
state,
|
||||
params: { bbox: BBox; layer: LayerConfig; theme: string; features: Feature<Point>[] }
|
||||
) {
|
||||
const t = Translations.t.importHelper.conflationChecker
|
||||
|
||||
const bbox = params.bbox.padAbsolute(0.0001)
|
||||
const layer = params.layer
|
||||
|
||||
const toImport: { features: any[] } = params
|
||||
let overpassStatus = new UIEventSource<
|
||||
{ error: string } | "running" | "success" | "idle" | "cached"
|
||||
>("idle")
|
||||
|
||||
function loadDataFromOverpass() {
|
||||
// Load the data!
|
||||
const url = Constants.defaultOverpassUrls[1]
|
||||
const relationTracker = new RelationsTracker()
|
||||
const overpass = new Overpass(
|
||||
params.layer.source.osmTags,
|
||||
[],
|
||||
url,
|
||||
new UIEventSource<number>(180),
|
||||
relationTracker,
|
||||
true
|
||||
)
|
||||
console.log("Loading from overpass!")
|
||||
overpassStatus.setData("running")
|
||||
overpass.queryGeoJson(bbox).then(
|
||||
([data, date]) => {
|
||||
console.log(
|
||||
"Received overpass-data: ",
|
||||
data.features.length,
|
||||
"features are loaded at ",
|
||||
date
|
||||
)
|
||||
overpassStatus.setData("success")
|
||||
fromLocalStorage.setData([data, date])
|
||||
},
|
||||
(error) => {
|
||||
overpassStatus.setData({ error })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const fromLocalStorage = IdbLocalStorage.Get<[any, Date]>(
|
||||
"importer-overpass-cache-" + layer.id,
|
||||
{
|
||||
whenLoaded: (v) => {
|
||||
if (v !== undefined && v !== null) {
|
||||
console.log("Loaded from local storage:", v)
|
||||
overpassStatus.setData("cached")
|
||||
} else {
|
||||
loadDataFromOverpass()
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const cacheAge = fromLocalStorage.map((d) => {
|
||||
if (d === undefined || d[1] === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const [_, loadedDate] = d
|
||||
return (new Date().getTime() - loadedDate.getTime()) / 1000
|
||||
})
|
||||
cacheAge.addCallbackD((timeDiff) => {
|
||||
if (timeDiff < 24 * 60 * 60) {
|
||||
// Recently cached!
|
||||
overpassStatus.setData("cached")
|
||||
return
|
||||
} else {
|
||||
loadDataFromOverpass()
|
||||
}
|
||||
})
|
||||
|
||||
const geojson: Store<FeatureCollection> = fromLocalStorage.map((d) => {
|
||||
if (d === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return d[0]
|
||||
})
|
||||
|
||||
const background = new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto)
|
||||
const location = new UIEventSource<Loc>({ lat: 0, lon: 0, zoom: 1 })
|
||||
const currentBounds = new UIEventSource<BBox>(undefined)
|
||||
const zoomLevel = ValidatedTextField.ForType("pnat").ConstructInputElement({
|
||||
value: LocalStorageSource.GetParsed<string>("importer-zoom-level", "0"),
|
||||
})
|
||||
zoomLevel.SetClass("ml-1 border border-black")
|
||||
const osmLiveData = Minimap.createMiniMap({
|
||||
allowMoving: true,
|
||||
location,
|
||||
background,
|
||||
bounds: currentBounds,
|
||||
})
|
||||
osmLiveData.SetClass("w-full").SetStyle("height: 500px")
|
||||
|
||||
const geojsonFeatures: Store<Feature[]> = geojson.map(
|
||||
(geojson) => {
|
||||
if (geojson?.features === undefined) {
|
||||
return []
|
||||
}
|
||||
const currentZoom = zoomLevel.GetValue().data
|
||||
const zoomedEnough: boolean = osmLiveData.location.data.zoom >= Number(currentZoom)
|
||||
if (currentZoom !== undefined && !zoomedEnough) {
|
||||
return []
|
||||
}
|
||||
const bounds = osmLiveData.bounds.data
|
||||
if (bounds === undefined) {
|
||||
return geojson.features
|
||||
}
|
||||
return geojson.features.filter((f) => BBox.get(f).overlapsWith(bounds))
|
||||
},
|
||||
[osmLiveData.bounds, zoomLevel.GetValue()]
|
||||
)
|
||||
|
||||
const preview = new StaticFeatureSource(geojsonFeatures)
|
||||
|
||||
new ShowDataLayer({
|
||||
layerToShow: new LayerConfig(currentview),
|
||||
state,
|
||||
leafletMap: osmLiveData.leafletMap,
|
||||
popup: undefined,
|
||||
zoomToFeatures: true,
|
||||
features: StaticFeatureSource.fromGeojson([bbox.asGeoJson({})]),
|
||||
})
|
||||
|
||||
new ShowDataLayer({
|
||||
layerToShow: layer,
|
||||
state,
|
||||
leafletMap: osmLiveData.leafletMap,
|
||||
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, { setHash: false }),
|
||||
zoomToFeatures: false,
|
||||
features: preview,
|
||||
})
|
||||
|
||||
new ShowDataLayer({
|
||||
layerToShow: new LayerConfig(import_candidate),
|
||||
state,
|
||||
leafletMap: osmLiveData.leafletMap,
|
||||
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, { setHash: false }),
|
||||
zoomToFeatures: false,
|
||||
features: StaticFeatureSource.fromGeojson(toImport.features),
|
||||
})
|
||||
|
||||
const nearbyCutoff = ValidatedTextField.ForType("pnat").ConstructInputElement()
|
||||
nearbyCutoff.SetClass("ml-1 border border-black")
|
||||
nearbyCutoff.GetValue().syncWith(LocalStorageSource.Get("importer-cutoff", "25"), true)
|
||||
|
||||
const matchedFeaturesMap = Minimap.createMiniMap({
|
||||
allowMoving: true,
|
||||
background,
|
||||
})
|
||||
matchedFeaturesMap.SetClass("w-full").SetStyle("height: 500px")
|
||||
|
||||
// Featuresource showing OSM-features which are nearby a toImport-feature
|
||||
const geojsonMapped: Store<Feature[]> = geojson.map(
|
||||
(osmData) => {
|
||||
if (osmData?.features === undefined) {
|
||||
return []
|
||||
}
|
||||
const maxDist = Number(nearbyCutoff.GetValue().data)
|
||||
return osmData.features.filter((f) =>
|
||||
toImport.features.some(
|
||||
(imp) =>
|
||||
maxDist >=
|
||||
GeoOperations.distanceBetween(
|
||||
imp.geometry.coordinates,
|
||||
GeoOperations.centerpointCoordinates(f)
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
[nearbyCutoff.GetValue().stabilized(500)]
|
||||
)
|
||||
const nearbyFeatures = new StaticFeatureSource(geojsonMapped)
|
||||
const paritionedImport = ImportUtils.partitionFeaturesIfNearby(
|
||||
toImport,
|
||||
geojson,
|
||||
nearbyCutoff.GetValue().map(Number)
|
||||
)
|
||||
|
||||
// Featuresource showing OSM-features which are nearby a toImport-feature
|
||||
const toImportWithNearby = new StaticFeatureSource(
|
||||
paritionedImport.map((els) => <Feature[]>els?.hasNearby ?? [])
|
||||
)
|
||||
toImportWithNearby.features.addCallback((nearby) =>
|
||||
console.log("The following features are near an already existing object:", nearby)
|
||||
)
|
||||
|
||||
new ShowDataLayer({
|
||||
layerToShow: new LayerConfig(import_candidate),
|
||||
state,
|
||||
leafletMap: matchedFeaturesMap.leafletMap,
|
||||
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, { setHash: false }),
|
||||
zoomToFeatures: false,
|
||||
features: toImportWithNearby,
|
||||
})
|
||||
const showOsmLayer = new CheckBox(t.showOsmLayerInConflationMap, true)
|
||||
new ShowDataLayer({
|
||||
layerToShow: layer,
|
||||
state,
|
||||
leafletMap: matchedFeaturesMap.leafletMap,
|
||||
popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, { setHash: false }),
|
||||
zoomToFeatures: true,
|
||||
features: nearbyFeatures,
|
||||
doShowLayer: showOsmLayer.GetValue(),
|
||||
})
|
||||
|
||||
const conflationMaps = new Combine([
|
||||
new VariableUiElement(
|
||||
geojson.map((geojson) => {
|
||||
if (geojson === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return new SubtleButton(Svg.download_svg(), t.downloadOverpassData).onClick(
|
||||
() => {
|
||||
Utils.offerContentsAsDownloadableFile(
|
||||
JSON.stringify(geojson, null, " "),
|
||||
"mapcomplete-" + layer.id + ".geojson",
|
||||
{
|
||||
mimetype: "application/json+geo",
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
),
|
||||
new VariableUiElement(
|
||||
cacheAge.map((age) => {
|
||||
if (age === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (age < 0) {
|
||||
return t.cacheExpired
|
||||
}
|
||||
return new Combine([
|
||||
t.loadedDataAge.Subs({ age: Utils.toHumanTime(age) }),
|
||||
new SubtleButton(Svg.reload_svg().SetClass("h-8"), t.reloadTheCache)
|
||||
.onClick(loadDataFromOverpass)
|
||||
.SetClass("h-12"),
|
||||
])
|
||||
})
|
||||
),
|
||||
|
||||
new Title(t.titleLive),
|
||||
t.importCandidatesCount.Subs({ count: toImport.features.length }),
|
||||
new VariableUiElement(
|
||||
geojson.map((geojson) => {
|
||||
if (
|
||||
geojson?.features?.length === undefined ||
|
||||
geojson?.features?.length === 0
|
||||
) {
|
||||
return t.nothingLoaded.Subs(layer).SetClass("alert")
|
||||
}
|
||||
return new Combine([
|
||||
t.osmLoaded.Subs({ count: geojson.features.length, name: layer.name }),
|
||||
])
|
||||
})
|
||||
),
|
||||
osmLiveData,
|
||||
new Combine([
|
||||
t.zoomLevelSelection,
|
||||
zoomLevel,
|
||||
new VariableUiElement(
|
||||
osmLiveData.location.map((location) => {
|
||||
return t.zoomIn.Subs(<any>{ current: location.zoom })
|
||||
})
|
||||
),
|
||||
]).SetClass("flex"),
|
||||
new DivContainer("fullscreen"),
|
||||
new Title(t.titleNearby),
|
||||
new Combine([t.mapShowingNearbyIntro, nearbyCutoff]).SetClass("flex"),
|
||||
new VariableUiElement(
|
||||
toImportWithNearby.features.map((feats) =>
|
||||
t.nearbyWarn.Subs({ count: feats.length }).SetClass("alert")
|
||||
)
|
||||
),
|
||||
t.setRangeToZero,
|
||||
matchedFeaturesMap,
|
||||
showOsmLayer,
|
||||
]).SetClass("flex flex-col")
|
||||
super([
|
||||
new Title(t.title),
|
||||
new VariableUiElement(
|
||||
overpassStatus.map((d) => {
|
||||
if (d === "idle") {
|
||||
return new Loading(t.states.idle)
|
||||
}
|
||||
if (d === "running") {
|
||||
return new Loading(t.states.running)
|
||||
}
|
||||
if (d["error"] !== undefined) {
|
||||
return t.states.error.Subs({ error: d["error"] }).SetClass("alert")
|
||||
}
|
||||
|
||||
if (d === "cached") {
|
||||
return conflationMaps
|
||||
}
|
||||
if (d === "success") {
|
||||
return conflationMaps
|
||||
}
|
||||
return t.states.unexpected.Subs({ state: d }).SetClass("alert")
|
||||
})
|
||||
),
|
||||
])
|
||||
|
||||
this.Value = paritionedImport.map((feats) => ({
|
||||
theme: params.theme,
|
||||
features: <any>feats?.noNearby,
|
||||
layer: params.layer,
|
||||
}))
|
||||
this.IsValid = this.Value.map((v) => v?.features !== undefined && v.features.length > 0)
|
||||
}
|
||||
}
|
|
@ -1,136 +0,0 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Title from "../Base/Title"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import Loading from "../Base/Loading"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Svg from "../../Svg"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
|
||||
export class CreateNotes extends Combine {
|
||||
public static createNoteContentsUi(
|
||||
feature: { properties: any; geometry: { coordinates: [number, number] } },
|
||||
options: { wikilink: string; intro: string; source: string; theme: string }
|
||||
): (Translation | string)[] {
|
||||
const src = feature.properties["source"] ?? feature.properties["src"] ?? options.source
|
||||
delete feature.properties["source"]
|
||||
delete feature.properties["src"]
|
||||
let extraNote = ""
|
||||
if (feature.properties["note"]) {
|
||||
extraNote = feature.properties["note"] + "\n"
|
||||
delete feature.properties["note"]
|
||||
}
|
||||
|
||||
const tags: string[] = []
|
||||
for (const key in feature.properties) {
|
||||
if (feature.properties[key] === null || feature.properties[key] === undefined) {
|
||||
console.warn("Null or undefined key for ", feature.properties)
|
||||
continue
|
||||
}
|
||||
if (feature.properties[key] === "") {
|
||||
continue
|
||||
}
|
||||
tags.push(
|
||||
key +
|
||||
"=" +
|
||||
(feature.properties[key] + "")
|
||||
.replace(/=/, "\\=")
|
||||
.replace(/;/g, "\\;")
|
||||
.replace(/\n/g, "\\n")
|
||||
)
|
||||
}
|
||||
const lat = feature.geometry.coordinates[1]
|
||||
const lon = feature.geometry.coordinates[0]
|
||||
const note = Translations.t.importHelper.noteParts
|
||||
return [
|
||||
options.intro,
|
||||
extraNote,
|
||||
note.datasource.Subs({ source: src }),
|
||||
note.wikilink.Subs(options),
|
||||
"",
|
||||
note.importEasily,
|
||||
`https://mapcomplete.osm.be/${options.theme}.html?z=18&lat=${lat}&lon=${lon}#import`,
|
||||
...tags,
|
||||
]
|
||||
}
|
||||
|
||||
public static createNoteContents(
|
||||
feature: { properties: any; geometry: { coordinates: [number, number] } },
|
||||
options: { wikilink: string; intro: string; source: string; theme: string }
|
||||
): string[] {
|
||||
return CreateNotes.createNoteContentsUi(feature, options).map((trOrStr) => {
|
||||
if (typeof trOrStr === "string") {
|
||||
return trOrStr
|
||||
}
|
||||
return trOrStr.txt
|
||||
})
|
||||
}
|
||||
|
||||
constructor(
|
||||
state: { osmConnection: OsmConnection },
|
||||
v: { features: any[]; wikilink: string; intro: string; source: string; theme: string }
|
||||
) {
|
||||
const t = Translations.t.importHelper.createNotes
|
||||
const createdNotes: UIEventSource<number[]> = new UIEventSource<number[]>([])
|
||||
const failed = new UIEventSource<string[]>([])
|
||||
const currentNote = createdNotes.map((n) => n.length)
|
||||
|
||||
for (const f of v.features) {
|
||||
const lat = f.geometry.coordinates[1]
|
||||
const lon = f.geometry.coordinates[0]
|
||||
const text = CreateNotes.createNoteContents(f, v).join("\n")
|
||||
|
||||
state.osmConnection.openNote(lat, lon, text).then(
|
||||
({ id }) => {
|
||||
createdNotes.data.push(id)
|
||||
createdNotes.ping()
|
||||
},
|
||||
(err) => {
|
||||
failed.data.push(err)
|
||||
failed.ping()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
super([
|
||||
new Title(t.title),
|
||||
t.loading,
|
||||
new Toggle(
|
||||
new Loading(
|
||||
new VariableUiElement(
|
||||
currentNote.map((count) =>
|
||||
t.creating.Subs({
|
||||
count,
|
||||
total: v.features.length,
|
||||
})
|
||||
)
|
||||
)
|
||||
),
|
||||
new Combine([
|
||||
Svg.party_svg().SetClass("w-24"),
|
||||
t.done.Subs({ count: v.features.length }).SetClass("thanks"),
|
||||
new SubtleButton(Svg.note_svg(), t.openImportViewer, {
|
||||
url: "import_viewer.html",
|
||||
}),
|
||||
]),
|
||||
currentNote.map((count) => count < v.features.length)
|
||||
),
|
||||
new VariableUiElement(
|
||||
failed.map((failed) => {
|
||||
if (failed.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return new Combine([
|
||||
new FixedUiElement("Some entries failed").SetClass("alert"),
|
||||
...failed,
|
||||
]).SetClass("flex flex-col")
|
||||
})
|
||||
),
|
||||
])
|
||||
this.SetClass("flex flex-col")
|
||||
}
|
||||
}
|
|
@ -1,151 +0,0 @@
|
|||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Combine from "../Base/Combine"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Svg from "../../Svg"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import { UIElement } from "../UIElement"
|
||||
|
||||
export interface FlowStep<T> extends BaseUIElement {
|
||||
readonly IsValid: Store<boolean>
|
||||
readonly Value: Store<T>
|
||||
}
|
||||
|
||||
export class FlowPanelFactory<T> {
|
||||
private _initial: FlowStep<any>
|
||||
private _steps: ((x: any) => FlowStep<any>)[]
|
||||
private _stepNames: (string | BaseUIElement)[]
|
||||
|
||||
private constructor(
|
||||
initial: FlowStep<any>,
|
||||
steps: ((x: any) => FlowStep<any>)[],
|
||||
stepNames: (string | BaseUIElement)[]
|
||||
) {
|
||||
this._initial = initial
|
||||
this._steps = steps
|
||||
this._stepNames = stepNames
|
||||
}
|
||||
|
||||
public static start<TOut>(
|
||||
name: { title: BaseUIElement },
|
||||
step: FlowStep<TOut>
|
||||
): FlowPanelFactory<TOut> {
|
||||
return new FlowPanelFactory(step, [], [name.title])
|
||||
}
|
||||
|
||||
public then<TOut>(
|
||||
name: string | { title: BaseUIElement },
|
||||
construct: (t: T) => FlowStep<TOut>
|
||||
): FlowPanelFactory<TOut> {
|
||||
return new FlowPanelFactory<TOut>(
|
||||
this._initial,
|
||||
this._steps.concat([construct]),
|
||||
this._stepNames.concat([name["title"] ?? name])
|
||||
)
|
||||
}
|
||||
|
||||
public finish(
|
||||
name: string | BaseUIElement,
|
||||
construct: (t: T, backButton?: BaseUIElement) => BaseUIElement
|
||||
): {
|
||||
flow: BaseUIElement
|
||||
furthestStep: UIEventSource<number>
|
||||
titles: (string | BaseUIElement)[]
|
||||
} {
|
||||
const furthestStep = new UIEventSource(0)
|
||||
// Construct all the flowpanels step by step (in reverse order)
|
||||
const nextConstr: ((t: any, back?: UIElement) => BaseUIElement)[] = this._steps.map(
|
||||
(_) => undefined
|
||||
)
|
||||
nextConstr.push(construct)
|
||||
for (let i = this._steps.length - 1; i >= 0; i--) {
|
||||
const createFlowStep: (value) => FlowStep<any> = this._steps[i]
|
||||
const isConfirm = i == this._steps.length - 1
|
||||
nextConstr[i] = (value, backButton) => {
|
||||
const flowStep = createFlowStep(value)
|
||||
furthestStep.setData(i + 1)
|
||||
const panel = new FlowPanel(flowStep, nextConstr[i + 1], backButton, isConfirm)
|
||||
panel.isActive.addCallbackAndRun((active) => {
|
||||
if (active) {
|
||||
furthestStep.setData(i + 1)
|
||||
}
|
||||
})
|
||||
return panel
|
||||
}
|
||||
}
|
||||
|
||||
const flow = new FlowPanel(this._initial, nextConstr[0])
|
||||
flow.isActive.addCallbackAndRun((active) => {
|
||||
if (active) {
|
||||
furthestStep.setData(0)
|
||||
}
|
||||
})
|
||||
return {
|
||||
flow,
|
||||
furthestStep,
|
||||
titles: this._stepNames,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class FlowPanel<T> extends Toggle {
|
||||
public isActive: UIEventSource<boolean>
|
||||
|
||||
constructor(
|
||||
initial: FlowStep<T>,
|
||||
constructNextstep: (input: T, backButton: BaseUIElement) => BaseUIElement,
|
||||
backbutton?: BaseUIElement,
|
||||
isConfirm = false
|
||||
) {
|
||||
const t = Translations.t.general
|
||||
|
||||
const currentStepActive = new UIEventSource(true)
|
||||
|
||||
let nextStep: UIEventSource<BaseUIElement> = new UIEventSource<BaseUIElement>(undefined)
|
||||
const backButtonForNextStep = new SubtleButton(Svg.back_svg(), t.back).onClick(() => {
|
||||
currentStepActive.setData(true)
|
||||
})
|
||||
|
||||
let elements: (BaseUIElement | string)[] = []
|
||||
const isError = new UIEventSource(false)
|
||||
if (initial !== undefined) {
|
||||
// Startup the flow
|
||||
elements = [
|
||||
initial,
|
||||
|
||||
new Combine([
|
||||
backbutton,
|
||||
new Toggle(
|
||||
new SubtleButton(
|
||||
isConfirm
|
||||
? Svg.checkmark_svg()
|
||||
: Svg.back_svg().SetStyle("transform: rotate(180deg);"),
|
||||
isConfirm ? t.confirm : t.next
|
||||
).onClick(() => {
|
||||
try {
|
||||
const v = initial.Value.data
|
||||
nextStep.setData(constructNextstep(v, backButtonForNextStep))
|
||||
currentStepActive.setData(false)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
isError.setData(true)
|
||||
}
|
||||
}),
|
||||
new SubtleButton(Svg.invalid_svg(), t.notValid),
|
||||
initial.IsValid
|
||||
),
|
||||
new Toggle(t.error.SetClass("alert"), undefined, isError),
|
||||
]).SetClass("flex w-full justify-end space-x-2"),
|
||||
]
|
||||
}
|
||||
|
||||
super(
|
||||
new Combine(elements).SetClass("h-full flex flex-col justify-between"),
|
||||
new VariableUiElement(nextStep),
|
||||
currentStepActive
|
||||
)
|
||||
this.isActive = currentStepActive
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import LanguagePicker from "../LanguagePicker"
|
||||
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { FlowPanelFactory } from "./FlowStep"
|
||||
import { RequestFile } from "./RequestFile"
|
||||
import { PreviewAttributesPanel } from "./PreviewPanel"
|
||||
import ConflationChecker from "./ConflationChecker"
|
||||
import { AskMetadata } from "./AskMetadata"
|
||||
import { ConfirmProcess } from "./ConfirmProcess"
|
||||
import { CreateNotes } from "./CreateNotes"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import List from "../Base/List"
|
||||
import { CompareToAlreadyExistingNotes } from "./CompareToAlreadyExistingNotes"
|
||||
import Introdution from "./Introdution"
|
||||
import LoginToImport from "./LoginToImport"
|
||||
import { MapPreview } from "./MapPreview"
|
||||
import LeftIndex from "../Base/LeftIndex"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import SelectTheme from "./SelectTheme"
|
||||
|
||||
export default class ImportHelperGui extends LeftIndex {
|
||||
constructor() {
|
||||
const state = new UserRelatedState(undefined)
|
||||
const t = Translations.t.importHelper
|
||||
const { flow, furthestStep, titles } = FlowPanelFactory.start(
|
||||
t.introduction,
|
||||
new Introdution()
|
||||
)
|
||||
.then(t.login, (_) => new LoginToImport(state))
|
||||
.then(t.selectFile, (_) => new RequestFile())
|
||||
.then(t.previewAttributes, (geojson) => new PreviewAttributesPanel(state, geojson))
|
||||
.then(t.mapPreview, (geojson) => new MapPreview(state, geojson))
|
||||
.then(t.selectTheme, (v) => new SelectTheme(v))
|
||||
.then(
|
||||
t.compareToAlreadyExistingNotes,
|
||||
(v) => new CompareToAlreadyExistingNotes(state, v)
|
||||
)
|
||||
.then(t.conflationChecker, (v) => new ConflationChecker(state, v))
|
||||
.then(t.confirmProcess, (v) => new ConfirmProcess(v))
|
||||
.then(t.askMetadata, (v) => new AskMetadata(v))
|
||||
.finish(t.createNotes.title, (v) => new CreateNotes(state, v))
|
||||
|
||||
const toc = new List(
|
||||
titles.map(
|
||||
(title, i) =>
|
||||
new VariableUiElement(
|
||||
furthestStep.map((currentStep) => {
|
||||
if (i > currentStep) {
|
||||
return new Combine([title]).SetClass("subtle")
|
||||
}
|
||||
if (i == currentStep) {
|
||||
return new Combine([title]).SetClass("font-bold")
|
||||
}
|
||||
if (i < currentStep) {
|
||||
return title
|
||||
}
|
||||
})
|
||||
)
|
||||
),
|
||||
true
|
||||
)
|
||||
|
||||
const leftContents: BaseUIElement[] = [
|
||||
new SubtleButton(undefined, t.gotoImportViewer, {
|
||||
url: "import_viewer.html",
|
||||
}),
|
||||
toc,
|
||||
new Toggle(t.testMode.SetClass("block alert"), undefined, state.featureSwitchIsTesting),
|
||||
new LanguagePicker(
|
||||
Translations.t.importHelper.title.SupportedLanguages(),
|
||||
""
|
||||
)?.SetClass("mt-4 self-end flex-col"),
|
||||
].map((el) => el?.SetClass("pl-4"))
|
||||
|
||||
super(leftContents, flow)
|
||||
}
|
||||
}
|
||||
|
||||
MinimapImplementation.initialize()
|
||||
new ImportHelperGui().AttachTo("main")
|
|
@ -1,44 +0,0 @@
|
|||
import { Store } from "../../Logic/UIEventSource"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import { Feature, Point } from "geojson"
|
||||
|
||||
export class ImportUtils {
|
||||
public static partitionFeaturesIfNearby(
|
||||
toPartitionFeatureCollection: { features: Feature[] },
|
||||
compareWith: Store<{ features: Feature[] }>,
|
||||
cutoffDistanceInMeters: Store<number>
|
||||
): Store<{ hasNearby: Feature[]; noNearby: Feature[] }> {
|
||||
return compareWith.map(
|
||||
(osmData) => {
|
||||
if (osmData?.features === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (osmData.features.length === 0) {
|
||||
return { noNearby: toPartitionFeatureCollection.features, hasNearby: [] }
|
||||
}
|
||||
const maxDist = cutoffDistanceInMeters.data
|
||||
|
||||
const hasNearby = []
|
||||
const noNearby = []
|
||||
for (const toImportElement of toPartitionFeatureCollection.features) {
|
||||
const hasNearbyFeature = osmData.features.some(
|
||||
(f) =>
|
||||
maxDist >=
|
||||
GeoOperations.distanceBetween(
|
||||
<any>(<Point>toImportElement.geometry).coordinates,
|
||||
GeoOperations.centerpointCoordinates(f)
|
||||
)
|
||||
)
|
||||
if (hasNearbyFeature) {
|
||||
hasNearby.push(toImportElement)
|
||||
} else {
|
||||
noNearby.push(toImportElement)
|
||||
}
|
||||
}
|
||||
|
||||
return { hasNearby, noNearby }
|
||||
},
|
||||
[cutoffDistanceInMeters]
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,800 +0,0 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { Utils } from "../../Utils"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Title from "../Base/Title"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Loading from "../Base/Loading"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import Link from "../Base/Link"
|
||||
import { DropDown } from "../Input/DropDown"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Svg from "../../Svg"
|
||||
import Toggle, { ClickableToggle } from "../Input/Toggle"
|
||||
import Table from "../Base/Table"
|
||||
import LeftIndex from "../Base/LeftIndex"
|
||||
import Toggleable, { Accordeon } from "../Base/Toggleable"
|
||||
import TableOfContents from "../Base/TableOfContents"
|
||||
import { LoginToggle } from "../Popup/LoginButton"
|
||||
import { QueryParameters } from "../../Logic/Web/QueryParameters"
|
||||
import Lazy from "../Base/Lazy"
|
||||
import { Button } from "../Base/Button"
|
||||
import ChartJs from "../Base/ChartJs"
|
||||
|
||||
interface NoteProperties {
|
||||
id: number
|
||||
url: string
|
||||
date_created: string
|
||||
closed_at?: string
|
||||
status: "open" | "closed"
|
||||
comments: {
|
||||
date: string
|
||||
uid: number
|
||||
user: string
|
||||
text: string
|
||||
html: string
|
||||
}[]
|
||||
}
|
||||
|
||||
interface NoteState {
|
||||
props: NoteProperties
|
||||
theme: string
|
||||
intro: string
|
||||
dateStr: string
|
||||
status:
|
||||
| "imported"
|
||||
| "already_mapped"
|
||||
| "invalid"
|
||||
| "closed"
|
||||
| "not_found"
|
||||
| "open"
|
||||
| "has_comments"
|
||||
}
|
||||
|
||||
class DownloadStatisticsButton extends SubtleButton {
|
||||
constructor(states: NoteState[][]) {
|
||||
super(Svg.statistics_svg(), "Download statistics")
|
||||
this.onClick(() => {
|
||||
const st: NoteState[] = [].concat(...states)
|
||||
|
||||
const fields = [
|
||||
"id",
|
||||
"status",
|
||||
"theme",
|
||||
"date_created",
|
||||
"date_closed",
|
||||
"days_open",
|
||||
"intro",
|
||||
"...comments",
|
||||
]
|
||||
const values: string[][] = st.map((note) => {
|
||||
return [
|
||||
note.props.id + "",
|
||||
note.status,
|
||||
note.theme,
|
||||
note.props.date_created?.substr(0, note.props.date_created.length - 3),
|
||||
note.props.closed_at?.substr(0, note.props.closed_at.length - 3) ?? "",
|
||||
JSON.stringify(note.intro),
|
||||
...note.props.comments.map(
|
||||
(c) => JSON.stringify(c.user) + ": " + JSON.stringify(c.text)
|
||||
),
|
||||
]
|
||||
})
|
||||
|
||||
Utils.offerContentsAsDownloadableFile(
|
||||
[fields, ...values].map((c) => c.join(", ")).join("\n"),
|
||||
"mapcomplete_import_notes_overview.csv",
|
||||
{
|
||||
mimetype: "text/csv",
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class MassAction extends Combine {
|
||||
constructor(state: UserRelatedState, props: NoteProperties[]) {
|
||||
const textField = ValidatedTextField.ForType("text").ConstructInputElement()
|
||||
|
||||
const actions = new DropDown<{
|
||||
predicate: (p: NoteProperties) => boolean
|
||||
action: (p: NoteProperties) => Promise<void>
|
||||
}>("On which notes should an action be performed?", [
|
||||
{
|
||||
value: undefined,
|
||||
shown: <string | BaseUIElement>"Pick an option...",
|
||||
},
|
||||
{
|
||||
value: {
|
||||
predicate: (p) => p.status === "open",
|
||||
action: async (p) => {
|
||||
const txt = textField.GetValue().data
|
||||
state.osmConnection.closeNote(p.id, txt)
|
||||
},
|
||||
},
|
||||
shown: "Add comment to every open note and close all notes",
|
||||
},
|
||||
{
|
||||
value: {
|
||||
predicate: (p) => p.status === "open",
|
||||
action: async (p) => {
|
||||
const txt = textField.GetValue().data
|
||||
state.osmConnection.addCommentToNote(p.id, txt)
|
||||
},
|
||||
},
|
||||
shown: "Add comment to every open note",
|
||||
},
|
||||
/*
|
||||
{
|
||||
// This was a one-off for one of the first imports
|
||||
value:{
|
||||
predicate: p => p.status === "open" && p.comments[0].text.split("\n").find(l => l.startsWith("note=")) !== undefined,
|
||||
action: async p => {
|
||||
const note = p.comments[0].text.split("\n").find(l => l.startsWith("note=")).substr("note=".length)
|
||||
state.osmConnection.addCommentToNode(p.id, note)
|
||||
}
|
||||
},
|
||||
shown:"On every open note, read the 'note='-tag and and this note as comment. (This action ignores the textfield)"
|
||||
},//*/
|
||||
])
|
||||
|
||||
const handledNotesCounter = new UIEventSource<number>(undefined)
|
||||
const apply = new SubtleButton(Svg.checkmark_svg(), "Apply action").onClick(async () => {
|
||||
const { predicate, action } = actions.GetValue().data
|
||||
for (let i = 0; i < props.length; i++) {
|
||||
handledNotesCounter.setData(i)
|
||||
const prop = props[i]
|
||||
if (!predicate(prop)) {
|
||||
continue
|
||||
}
|
||||
await action(prop)
|
||||
}
|
||||
handledNotesCounter.setData(props.length)
|
||||
})
|
||||
super([
|
||||
actions,
|
||||
textField.SetClass("w-full border border-black"),
|
||||
new Toggle(
|
||||
new Toggle(
|
||||
apply,
|
||||
|
||||
new Toggle(
|
||||
new Loading(
|
||||
new VariableUiElement(
|
||||
handledNotesCounter.map((state) => {
|
||||
if (state === props.length) {
|
||||
return "All done!"
|
||||
}
|
||||
return (
|
||||
"Handling note " + (state + 1) + " out of " + props.length
|
||||
)
|
||||
})
|
||||
)
|
||||
),
|
||||
new Combine([Svg.checkmark_svg().SetClass("h-8"), "All done!"]).SetClass(
|
||||
"thanks flex p-4"
|
||||
),
|
||||
handledNotesCounter.map((s) => s < props.length)
|
||||
),
|
||||
handledNotesCounter.map((s) => s === undefined)
|
||||
),
|
||||
|
||||
new VariableUiElement(
|
||||
textField
|
||||
.GetValue()
|
||||
.map(
|
||||
(txt) =>
|
||||
"Type a text of at least 15 characters to apply the action. Currently, there are " +
|
||||
(txt?.length ?? 0) +
|
||||
" characters"
|
||||
)
|
||||
).SetClass("alert"),
|
||||
actions
|
||||
.GetValue()
|
||||
.map(
|
||||
(v) => v !== undefined && textField.GetValue()?.data?.length > 15,
|
||||
[textField.GetValue()]
|
||||
)
|
||||
),
|
||||
new Toggle(
|
||||
new FixedUiElement("Testmode enable").SetClass("alert"),
|
||||
undefined,
|
||||
state.featureSwitchIsTesting
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
class Statistics extends Combine {
|
||||
private static r() {
|
||||
return Math.floor(Math.random() * 256)
|
||||
}
|
||||
|
||||
private static randomColour(): string {
|
||||
return "rgba(" + Statistics.r() + "," + Statistics.r() + "," + Statistics.r() + ")"
|
||||
}
|
||||
private static CreatePieByAuthor(closed_by: Record<string, number[]>): ChartJs {
|
||||
const importers = Object.keys(closed_by)
|
||||
importers.sort((a, b) => closed_by[b].at(-1) - closed_by[a].at(-1))
|
||||
return new ChartJs(<any>{
|
||||
type: "doughnut",
|
||||
data: {
|
||||
labels: importers,
|
||||
datasets: [
|
||||
{
|
||||
label: "Closed by",
|
||||
data: importers.map((k) => closed_by[k].at(-1)),
|
||||
backgroundColor: importers.map((_) => Statistics.randomColour()),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private static CreateStatePie(noteStates: NoteState[]) {
|
||||
const colors = {
|
||||
imported: "#0aa323",
|
||||
already_mapped: "#00bbff",
|
||||
invalid: "#ff0000",
|
||||
closed: "#000000",
|
||||
not_found: "#ff6d00",
|
||||
open: "#626262",
|
||||
has_comments: "#a8a8a8",
|
||||
}
|
||||
const knownStates = Object.keys(colors)
|
||||
const byState = knownStates.map(
|
||||
(targetState) => noteStates.filter((ns) => ns.status === targetState).length
|
||||
)
|
||||
|
||||
return new ChartJs(<any>{
|
||||
type: "doughnut",
|
||||
data: {
|
||||
labels: knownStates.map(
|
||||
(state, i) =>
|
||||
state + " " + Math.floor((100 * byState[i]) / noteStates.length) + "%"
|
||||
),
|
||||
datasets: [
|
||||
{
|
||||
label: "Status by",
|
||||
data: byState,
|
||||
backgroundColor: knownStates.map((state) => colors[state]),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
constructor(noteStates: NoteState[]) {
|
||||
if (noteStates.length === 0) {
|
||||
super([])
|
||||
return
|
||||
}
|
||||
// We assume all notes are created at the same time
|
||||
let dateOpened = new Date(noteStates[0].dateStr)
|
||||
for (const noteState of noteStates) {
|
||||
const openDate = new Date(noteState.dateStr)
|
||||
if (openDate < dateOpened) {
|
||||
dateOpened = openDate
|
||||
}
|
||||
}
|
||||
const today = new Date()
|
||||
const daysBetween = (today.getTime() - dateOpened.getTime()) / (1000 * 60 * 60 * 24)
|
||||
const ranges = {
|
||||
dates: [],
|
||||
is_open: [],
|
||||
}
|
||||
const closed_by: Record<string, number[]> = {}
|
||||
|
||||
for (const noteState of noteStates) {
|
||||
const closing_user = noteState.props.comments.at(-1).user
|
||||
if (closed_by[closing_user] === undefined) {
|
||||
closed_by[closing_user] = []
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = -1; i < daysBetween; i++) {
|
||||
const dt = new Date(dateOpened.getTime() + 24 * 60 * 60 * 1000 * i)
|
||||
let open_count = 0
|
||||
|
||||
for (const closing_user in closed_by) {
|
||||
closed_by[closing_user].push(0)
|
||||
}
|
||||
|
||||
for (const noteState of noteStates) {
|
||||
const openDate = new Date(noteState.dateStr)
|
||||
if (openDate > dt) {
|
||||
// Not created at this point
|
||||
continue
|
||||
}
|
||||
if (noteState.props.closed_at === undefined) {
|
||||
open_count++
|
||||
} else if (
|
||||
new Date(noteState.props.closed_at.substring(0, 10)).getTime() > dt.getTime()
|
||||
) {
|
||||
open_count++
|
||||
} else {
|
||||
const closing_user = noteState.props.comments.at(-1).user
|
||||
const user_count = closed_by[closing_user]
|
||||
user_count[user_count.length - 1] += 1
|
||||
}
|
||||
}
|
||||
|
||||
ranges.dates.push(
|
||||
new Date(dateOpened.getTime() + i * 1000 * 60 * 60 * 24)
|
||||
.toISOString()
|
||||
.substring(0, 10)
|
||||
)
|
||||
ranges.is_open.push(open_count)
|
||||
}
|
||||
|
||||
const labels = ranges.dates.map((i) => "" + i)
|
||||
const data = {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "Total open",
|
||||
data: ranges.is_open,
|
||||
fill: false,
|
||||
borderColor: "rgb(75, 192, 192)",
|
||||
tension: 0.1,
|
||||
},
|
||||
],
|
||||
}
|
||||
for (const closing_user in closed_by) {
|
||||
if (closed_by[closing_user].at(-1) <= 10) {
|
||||
continue
|
||||
}
|
||||
data.datasets.push({
|
||||
label: "Closed by " + closing_user,
|
||||
data: closed_by[closing_user],
|
||||
fill: false,
|
||||
borderColor: Statistics.randomColour(),
|
||||
tension: 0.1,
|
||||
})
|
||||
}
|
||||
|
||||
super([
|
||||
new ChartJs({
|
||||
type: "line",
|
||||
data,
|
||||
options: {
|
||||
scales: <any>{
|
||||
yAxes: [
|
||||
{
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
new Combine([
|
||||
Statistics.CreatePieByAuthor(closed_by),
|
||||
Statistics.CreateStatePie(noteStates),
|
||||
])
|
||||
.SetClass("flex w-full h-32")
|
||||
.SetStyle("width: 40rem"),
|
||||
])
|
||||
this.SetClass("block w-full h-64 border border-red")
|
||||
}
|
||||
}
|
||||
|
||||
class NoteTable extends Combine {
|
||||
private static individualActions: [() => BaseUIElement, string][] = [
|
||||
[Svg.not_found_svg, "This feature does not exist"],
|
||||
[Svg.addSmall_svg, "imported"],
|
||||
[Svg.duplicate_svg, "Already mapped"],
|
||||
]
|
||||
|
||||
constructor(noteStates: NoteState[], state?: UserRelatedState) {
|
||||
const typicalComment = noteStates[0].props.comments[0].html
|
||||
|
||||
const table = new Table(
|
||||
["id", "status", "last comment", "last modified by", "actions"],
|
||||
noteStates.map((ns) => NoteTable.noteField(ns, state)),
|
||||
{ sortable: true }
|
||||
).SetClass("zebra-table link-underline")
|
||||
|
||||
super([
|
||||
new Title("Mass apply an action on " + noteStates.length + " notes below"),
|
||||
state !== undefined
|
||||
? new MassAction(
|
||||
state,
|
||||
noteStates.map((ns) => ns.props)
|
||||
).SetClass("block")
|
||||
: undefined,
|
||||
table,
|
||||
new Title("Example note", 4),
|
||||
new FixedUiElement(typicalComment).SetClass("literal-code link-underline"),
|
||||
])
|
||||
this.SetClass("flex flex-col")
|
||||
}
|
||||
|
||||
private static noteField(ns: NoteState, state: UserRelatedState) {
|
||||
const link = new Link(
|
||||
"" + ns.props.id,
|
||||
"https://openstreetmap.org/note/" + ns.props.id,
|
||||
true
|
||||
)
|
||||
let last_comment = ""
|
||||
const last_comment_props = ns.props.comments[ns.props.comments.length - 1]
|
||||
const before_last_comment = ns.props.comments[ns.props.comments.length - 2]
|
||||
if (ns.props.comments.length > 1) {
|
||||
last_comment = last_comment_props.text
|
||||
if (last_comment === undefined && before_last_comment?.uid === last_comment_props.uid) {
|
||||
last_comment = before_last_comment.text
|
||||
}
|
||||
}
|
||||
const statusIcon = BatchView.icons[ns.status]().SetClass("h-4 w-4 shrink-0")
|
||||
const togglestate = new UIEventSource(false)
|
||||
const changed = new UIEventSource<string>(undefined)
|
||||
|
||||
const lazyButtons = new Lazy(() =>
|
||||
new Combine(
|
||||
this.individualActions.map(([img, text]) =>
|
||||
img()
|
||||
.onClick(async () => {
|
||||
if (ns.props.status === "closed") {
|
||||
await state.osmConnection.reopenNote(ns.props.id)
|
||||
}
|
||||
await state.osmConnection.closeNote(ns.props.id, text)
|
||||
changed.setData(text)
|
||||
})
|
||||
.SetClass("h-8 w-8")
|
||||
)
|
||||
).SetClass("flex")
|
||||
)
|
||||
|
||||
const appliedButtons = new VariableUiElement(
|
||||
changed.map((currentState) => (currentState === undefined ? lazyButtons : currentState))
|
||||
)
|
||||
|
||||
const buttons = Toggle.If(
|
||||
state?.osmConnection?.isLoggedIn,
|
||||
() =>
|
||||
new ClickableToggle(
|
||||
appliedButtons,
|
||||
new Button("edit...", () => {
|
||||
console.log("Enabling...")
|
||||
togglestate.setData(true)
|
||||
}),
|
||||
togglestate
|
||||
)
|
||||
)
|
||||
return [
|
||||
link,
|
||||
new Combine([statusIcon, ns.status]).SetClass("flex"),
|
||||
last_comment,
|
||||
new Link(
|
||||
last_comment_props.user,
|
||||
"https://www.openstreetmap.org/user/" + last_comment_props.user,
|
||||
true
|
||||
),
|
||||
buttons,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class BatchView extends Toggleable {
|
||||
public static icons = {
|
||||
open: Svg.compass_svg,
|
||||
has_comments: Svg.speech_bubble_svg,
|
||||
imported: Svg.addSmall_svg,
|
||||
already_mapped: Svg.checkmark_svg,
|
||||
not_found: Svg.not_found_svg,
|
||||
closed: Svg.close_svg,
|
||||
invalid: Svg.invalid_svg,
|
||||
}
|
||||
|
||||
constructor(noteStates: NoteState[], state?: UserRelatedState) {
|
||||
noteStates.sort((a, b) => a.props.id - b.props.id)
|
||||
|
||||
const { theme, intro, dateStr } = noteStates[0]
|
||||
|
||||
const statusHist = new Map<string, number>()
|
||||
for (const noteState of noteStates) {
|
||||
const st = noteState.status
|
||||
const c = statusHist.get(st) ?? 0
|
||||
statusHist.set(st, c + 1)
|
||||
}
|
||||
|
||||
const unresolvedTotal =
|
||||
(statusHist.get("open") ?? 0) + (statusHist.get("has_comments") ?? 0)
|
||||
const badges: BaseUIElement[] = [
|
||||
new FixedUiElement(dateStr).SetClass("literal-code rounded-full"),
|
||||
new FixedUiElement(noteStates.length + " total")
|
||||
.SetClass("literal-code rounded-full ml-1 border-4 border-gray")
|
||||
.onClick(() => filterOn.setData(undefined)),
|
||||
unresolvedTotal === 0
|
||||
? new Combine([Svg.party_svg().SetClass("h-6 m-1"), "All done!"]).SetClass(
|
||||
"flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border border-black"
|
||||
)
|
||||
: new FixedUiElement(
|
||||
Math.round(100 - (100 * unresolvedTotal) / noteStates.length) + "%"
|
||||
).SetClass("literal-code rounded-full ml-1"),
|
||||
]
|
||||
|
||||
const filterOn = new UIEventSource<string>(undefined)
|
||||
Object.keys(BatchView.icons).forEach((status) => {
|
||||
const count = statusHist.get(status)
|
||||
if (count === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const normal = new Combine([
|
||||
BatchView.icons[status]().SetClass("h-6 m-1"),
|
||||
count + " " + status,
|
||||
]).SetClass("flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border border-black")
|
||||
const selected = new Combine([
|
||||
BatchView.icons[status]().SetClass("h-6 m-1"),
|
||||
count + " " + status,
|
||||
]).SetClass(
|
||||
"flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border-4 border-black animate-pulse"
|
||||
)
|
||||
|
||||
const toggle = new ClickableToggle(
|
||||
selected,
|
||||
normal,
|
||||
filterOn.sync(
|
||||
(f) => f === status,
|
||||
[],
|
||||
(selected, previous) => {
|
||||
if (selected) {
|
||||
return status
|
||||
}
|
||||
if (previous === status) {
|
||||
return undefined
|
||||
}
|
||||
return previous
|
||||
}
|
||||
)
|
||||
).ToggleOnClick()
|
||||
|
||||
badges.push(toggle)
|
||||
})
|
||||
|
||||
const fullTable = new Combine([
|
||||
new NoteTable(noteStates, state),
|
||||
new Statistics(noteStates),
|
||||
])
|
||||
|
||||
super(
|
||||
new Combine([
|
||||
new Title(theme + ": " + intro, 2),
|
||||
new Combine(badges).SetClass("flex flex-wrap"),
|
||||
]),
|
||||
|
||||
new VariableUiElement(
|
||||
filterOn.map((filter) => {
|
||||
if (filter === undefined) {
|
||||
return fullTable
|
||||
}
|
||||
const notes = noteStates.filter((ns) => ns.status === filter)
|
||||
return new Combine([new NoteTable(notes, state), new Statistics(notes)])
|
||||
})
|
||||
),
|
||||
{
|
||||
closeOnClick: false,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class ImportInspector extends VariableUiElement {
|
||||
constructor(
|
||||
userDetails: { uid: number } | { display_name: string; search?: string },
|
||||
state: UserRelatedState
|
||||
) {
|
||||
let url
|
||||
|
||||
if (userDetails["uid"] !== undefined) {
|
||||
url =
|
||||
"https://api.openstreetmap.org/api/0.6/notes/search.json?user=" +
|
||||
userDetails["uid"] +
|
||||
"&closed=730&limit=10000&sort=created_at&q=%23import"
|
||||
} else {
|
||||
url =
|
||||
"https://api.openstreetmap.org/api/0.6/notes/search.json?display_name=" +
|
||||
encodeURIComponent(userDetails["display_name"]) +
|
||||
"&limit=10000&closed=730&sort=created_at&q="
|
||||
if (userDetails["search"] !== "") {
|
||||
url += userDetails["search"]
|
||||
} else {
|
||||
url += "#import"
|
||||
}
|
||||
}
|
||||
const notes: UIEventSource<
|
||||
{ error: string } | { success: { features: { properties: NoteProperties }[] } }
|
||||
> = UIEventSource.FromPromiseWithErr(Utils.downloadJson(url))
|
||||
super(
|
||||
notes.map((notes) => {
|
||||
if (notes === undefined) {
|
||||
return new Loading("Loading notes which mention '#import'")
|
||||
}
|
||||
if (notes["error"] !== undefined) {
|
||||
return new FixedUiElement("Something went wrong: " + notes["error"]).SetClass(
|
||||
"alert"
|
||||
)
|
||||
}
|
||||
// We only care about the properties here
|
||||
let props: NoteProperties[] = notes["success"].features.map((f) => f.properties)
|
||||
if (userDetails["uid"]) {
|
||||
props = props.filter((n) => n.comments[0].uid === userDetails["uid"])
|
||||
}
|
||||
if (userDetails["display_name"] !== undefined) {
|
||||
const display_name = <string>userDetails["display_name"]
|
||||
props = props.filter((n) => n.comments[0].user === display_name)
|
||||
}
|
||||
|
||||
const perBatch: NoteState[][] = Array.from(
|
||||
ImportInspector.SplitNotesIntoBatches(props).values()
|
||||
)
|
||||
const els: Toggleable[] = perBatch.map(
|
||||
(noteStates) => new BatchView(noteStates, state)
|
||||
)
|
||||
|
||||
const accordeon = new Accordeon(els)
|
||||
let contents = []
|
||||
if (state?.osmConnection?.isLoggedIn?.data) {
|
||||
contents = [
|
||||
new Title(Translations.t.importInspector.title, 1),
|
||||
new SubtleButton(undefined, "Create a new batch of imports", {
|
||||
url: "import_helper.html",
|
||||
}),
|
||||
]
|
||||
}
|
||||
contents.push(accordeon)
|
||||
contents.push(
|
||||
new Combine([
|
||||
new Title("Statistics for all notes"),
|
||||
new Statistics([].concat(...perBatch)),
|
||||
])
|
||||
)
|
||||
const content = new Combine(contents)
|
||||
return new LeftIndex(
|
||||
[
|
||||
new TableOfContents(content, { noTopLevel: true, maxDepth: 1 }).SetClass(
|
||||
"subtle"
|
||||
),
|
||||
new DownloadStatisticsButton(perBatch),
|
||||
],
|
||||
content
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates distinct batches of note, where 'date', 'intro' and 'theme' are identical
|
||||
*/
|
||||
private static SplitNotesIntoBatches(props: NoteProperties[]): Map<string, NoteState[]> {
|
||||
const perBatch = new Map<string, NoteState[]>()
|
||||
const prefix = "https://mapcomplete.osm.be/"
|
||||
for (const prop of props) {
|
||||
const lines = prop.comments[0].text.split("\n")
|
||||
const trigger = lines.findIndex((l) => l.startsWith(prefix) && l.endsWith("#import"))
|
||||
if (trigger < 0) {
|
||||
continue
|
||||
}
|
||||
let theme = lines[trigger].substr(prefix.length)
|
||||
theme = theme.substr(0, theme.indexOf("."))
|
||||
const date = Utils.ParseDate(prop.date_created)
|
||||
const dateStr = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate()
|
||||
const key = theme + lines[0] + dateStr
|
||||
if (!perBatch.has(key)) {
|
||||
perBatch.set(key, [])
|
||||
}
|
||||
let status:
|
||||
| "open"
|
||||
| "closed"
|
||||
| "imported"
|
||||
| "invalid"
|
||||
| "already_mapped"
|
||||
| "not_found"
|
||||
| "has_comments" = "open"
|
||||
|
||||
function has(keywords: string[], comment: string): boolean {
|
||||
return keywords.some((keyword) => comment.toLowerCase().indexOf(keyword) >= 0)
|
||||
}
|
||||
|
||||
if (prop.closed_at !== undefined) {
|
||||
const lastComment = prop.comments[prop.comments.length - 1].text.toLowerCase()
|
||||
if (has(["does not exist", "bestaat niet", "geen"], lastComment)) {
|
||||
status = "not_found"
|
||||
} else if (
|
||||
has(
|
||||
[
|
||||
"already mapped",
|
||||
"reeds",
|
||||
"dubbele note",
|
||||
"stond er al",
|
||||
"stonden er al",
|
||||
"staat er al",
|
||||
"staan er al",
|
||||
"stond al",
|
||||
"stonden al",
|
||||
"staat al",
|
||||
"staan al",
|
||||
],
|
||||
lastComment
|
||||
)
|
||||
) {
|
||||
status = "already_mapped"
|
||||
} else if (
|
||||
lastComment.indexOf("invalid") >= 0 ||
|
||||
lastComment.indexOf("incorrect") >= 0
|
||||
) {
|
||||
status = "invalid"
|
||||
} else if (
|
||||
has(
|
||||
[
|
||||
"imported",
|
||||
"erbij",
|
||||
"toegevoegd",
|
||||
"added",
|
||||
"gemapped",
|
||||
"gemapt",
|
||||
"mapped",
|
||||
"done",
|
||||
"openstreetmap.org/changeset",
|
||||
],
|
||||
lastComment
|
||||
)
|
||||
) {
|
||||
status = "imported"
|
||||
} else {
|
||||
status = "closed"
|
||||
}
|
||||
} else if (prop.comments.length > 1) {
|
||||
status = "has_comments"
|
||||
}
|
||||
|
||||
perBatch.get(key).push({
|
||||
props: prop,
|
||||
intro: lines[0],
|
||||
theme,
|
||||
dateStr,
|
||||
status,
|
||||
})
|
||||
}
|
||||
return perBatch
|
||||
}
|
||||
}
|
||||
|
||||
class ImportViewerGui extends LoginToggle {
|
||||
constructor() {
|
||||
const state = new UserRelatedState(undefined)
|
||||
const displayNameParam = QueryParameters.GetQueryParameter(
|
||||
"user",
|
||||
"",
|
||||
"The username of the person whom you want to see the notes for"
|
||||
)
|
||||
const searchParam = QueryParameters.GetQueryParameter(
|
||||
"search",
|
||||
"",
|
||||
"A text that should be included in the first comment of the note to be shown"
|
||||
)
|
||||
super(
|
||||
new VariableUiElement(
|
||||
state.osmConnection.userDetails.map(
|
||||
(ud) => {
|
||||
const display_name = displayNameParam.data
|
||||
const search = searchParam.data
|
||||
if (display_name !== "" || search !== "") {
|
||||
return new ImportInspector({ display_name, search }, undefined)
|
||||
}
|
||||
return new ImportInspector(ud, state)
|
||||
},
|
||||
[displayNameParam, searchParam]
|
||||
)
|
||||
),
|
||||
"Login to inspect your import flows",
|
||||
state
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
new ImportViewerGui().AttachTo("main")
|
|
@ -1,43 +0,0 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import { FlowStep } from "./FlowStep"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Title from "../Base/Title"
|
||||
import { CreateNotes } from "./CreateNotes"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
|
||||
export default class Introdution extends Combine implements FlowStep<void> {
|
||||
readonly IsValid: UIEventSource<boolean>
|
||||
readonly Value: UIEventSource<void>
|
||||
|
||||
constructor() {
|
||||
const example = CreateNotes.createNoteContentsUi(
|
||||
{
|
||||
properties: {
|
||||
some_key: "some_value",
|
||||
note: "a note in the original dataset",
|
||||
},
|
||||
geometry: {
|
||||
coordinates: [3.4, 51.2],
|
||||
},
|
||||
},
|
||||
{
|
||||
wikilink:
|
||||
"https://wiki.openstreetmap.org/wiki/Imports/<documentation of your import>",
|
||||
intro: "There might be an XYZ here",
|
||||
theme: "theme",
|
||||
source: "source of the data",
|
||||
}
|
||||
).map((el) => (el === "" ? new FixedUiElement("").SetClass("block") : el))
|
||||
|
||||
super([
|
||||
new Title(Translations.t.importHelper.introduction.title),
|
||||
Translations.t.importHelper.introduction.description,
|
||||
Translations.t.importHelper.introduction.importFormat,
|
||||
new Combine([new Combine(example).SetClass("flex flex-col")]).SetClass("literal-code"),
|
||||
])
|
||||
this.SetClass("flex flex-col")
|
||||
this.IsValid = new UIEventSource<boolean>(true)
|
||||
this.Value = new UIEventSource<void>(undefined)
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import { FlowStep } from "./FlowStep"
|
||||
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Title from "../Base/Title"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { LoginToggle } from "../Popup/LoginButton"
|
||||
import Img from "../Base/Img"
|
||||
import Constants from "../../Models/Constants"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Svg from "../../Svg"
|
||||
import MoreScreen from "../BigComponents/MoreScreen"
|
||||
import CheckBoxes from "../Input/Checkboxes"
|
||||
|
||||
export default class LoginToImport extends Combine implements FlowStep<UserRelatedState> {
|
||||
readonly IsValid: Store<boolean>
|
||||
readonly Value: Store<UserRelatedState>
|
||||
|
||||
private static readonly whitelist = [15015689]
|
||||
|
||||
constructor(state: UserRelatedState) {
|
||||
const t = Translations.t.importHelper.login
|
||||
const check = new CheckBoxes([
|
||||
new VariableUiElement(
|
||||
state.osmConnection.userDetails.map((ud) => t.loginIsCorrect.Subs(ud))
|
||||
),
|
||||
])
|
||||
const isValid = state.osmConnection.userDetails.map(
|
||||
(ud) =>
|
||||
LoginToImport.whitelist.indexOf(ud.uid) >= 0 ||
|
||||
ud.csCount >= Constants.userJourney.importHelperUnlock
|
||||
)
|
||||
super([
|
||||
new Title(t.userAccountTitle),
|
||||
new LoginToggle(
|
||||
new VariableUiElement(
|
||||
state.osmConnection.userDetails.map((ud) => {
|
||||
if (ud === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return new Combine([
|
||||
new Img(ud.img ?? "./assets/svgs/help.svg").SetClass(
|
||||
"w-16 h-16 rounded-full"
|
||||
),
|
||||
t.loggedInWith.Subs(ud),
|
||||
new SubtleButton(
|
||||
Svg.logout_svg().SetClass("h-8"),
|
||||
Translations.t.general.logout
|
||||
).onClick(() => state.osmConnection.LogOut()),
|
||||
check,
|
||||
])
|
||||
})
|
||||
),
|
||||
t.loginRequired,
|
||||
state
|
||||
),
|
||||
new Toggle(
|
||||
undefined,
|
||||
new Combine([
|
||||
t.lockNotice.Subs(Constants.userJourney).SetClass("alert"),
|
||||
MoreScreen.CreateProffessionalSerivesButton(),
|
||||
]),
|
||||
isValid
|
||||
),
|
||||
])
|
||||
this.Value = new UIEventSource<UserRelatedState>(state)
|
||||
this.IsValid = isValid.map(
|
||||
(isValid) => isValid && check.GetValue().data.length > 0,
|
||||
[check.GetValue()]
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,162 +0,0 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts"
|
||||
import { DropDown } from "../Input/DropDown"
|
||||
import { Utils } from "../../Utils"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import Loc from "../../Models/Loc"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { FlowStep } from "./FlowStep"
|
||||
import Title from "../Base/Title"
|
||||
import CheckBoxes from "../Input/Checkboxes"
|
||||
import { Feature, Point } from "geojson"
|
||||
import DivContainer from "../Base/DivContainer"
|
||||
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
|
||||
import ShowDataLayer from "../Map/ShowDataLayer"
|
||||
|
||||
/**
|
||||
* Shows the data to import on a map, asks for the correct layer to be selected
|
||||
*/
|
||||
export class MapPreview
|
||||
extends Combine
|
||||
implements FlowStep<{ bbox: BBox; layer: LayerConfig; features: Feature<Point>[] }>
|
||||
{
|
||||
public readonly IsValid: Store<boolean>
|
||||
public readonly Value: Store<{ bbox: BBox; layer: LayerConfig; features: any[] }>
|
||||
|
||||
constructor(state: UserRelatedState, geojson: { features: Feature[] }) {
|
||||
const t = Translations.t.importHelper.mapPreview
|
||||
|
||||
const propertyKeys = new Set<string>()
|
||||
for (const f of geojson.features) {
|
||||
Object.keys(f.properties).forEach((key) => propertyKeys.add(key))
|
||||
}
|
||||
|
||||
const availableLayers = AllKnownLayouts.AllPublicLayers().filter(
|
||||
(l) => l.name !== undefined && l.source !== undefined
|
||||
)
|
||||
const layerPicker = new DropDown(
|
||||
t.selectLayer,
|
||||
[{ shown: t.selectLayer, value: undefined }].concat(
|
||||
availableLayers.map((l) => ({
|
||||
shown: l.name,
|
||||
value: l,
|
||||
}))
|
||||
)
|
||||
)
|
||||
|
||||
let autodetected = new UIEventSource(false)
|
||||
for (const layer of availableLayers) {
|
||||
const mismatched = geojson.features.some(
|
||||
(f) => !layer.source.osmTags.matchesProperties(f.properties)
|
||||
)
|
||||
if (!mismatched) {
|
||||
console.log("Autodected layer", layer.id)
|
||||
layerPicker.GetValue().setData(layer)
|
||||
layerPicker.GetValue().addCallback((_) => autodetected.setData(false))
|
||||
autodetected.setData(true)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const withId = geojson.features.map((f, i) => {
|
||||
const copy = Utils.Clone(f)
|
||||
copy.properties.id = "to-import/" + i
|
||||
return copy
|
||||
})
|
||||
|
||||
// Create a store which has only features matching the selected layer
|
||||
const matching: Store<Feature[]> = layerPicker.GetValue().map((layer: LayerConfig) => {
|
||||
if (layer === undefined) {
|
||||
console.log("No matching layer found")
|
||||
return []
|
||||
}
|
||||
const matching: Feature[] = []
|
||||
|
||||
for (const feature of withId) {
|
||||
if (layer.source.osmTags.matchesProperties(feature.properties)) {
|
||||
matching.push(feature)
|
||||
}
|
||||
}
|
||||
console.log("Matching features: ", matching)
|
||||
|
||||
return matching
|
||||
})
|
||||
const background = new UIEventSource<RasterLayerPolygon>(AvailableRasterLayers.osmCarto)
|
||||
const location = new UIEventSource<Loc>({ lat: 0, lon: 0, zoom: 1 })
|
||||
const currentBounds = new UIEventSource<BBox>(undefined)
|
||||
const { ui, mapproperties, map } = MapLibreAdaptor.construct()
|
||||
|
||||
ui.SetClass("w-full").SetStyle("height: 500px")
|
||||
|
||||
layerPicker.GetValue().addCallbackAndRunD((layerToShow) => {
|
||||
new ShowDataLayer(map, {
|
||||
layer: layerToShow,
|
||||
zoomToFeatures: true,
|
||||
features: new StaticFeatureSource(matching),
|
||||
})
|
||||
})
|
||||
|
||||
const bbox = matching.map((feats) =>
|
||||
BBox.bboxAroundAll(
|
||||
feats.map((f) => new BBox([(<Feature<Point>>f).geometry.coordinates]))
|
||||
)
|
||||
)
|
||||
|
||||
const mismatchIndicator = new VariableUiElement(
|
||||
matching.map((matching) => {
|
||||
if (matching === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const diff = geojson.features.length - matching.length
|
||||
if (diff === 0) {
|
||||
return undefined
|
||||
}
|
||||
const obligatory = layerPicker
|
||||
.GetValue()
|
||||
.data?.source?.osmTags?.asHumanString(false, false, {})
|
||||
return t.mismatch.Subs({ count: diff, tags: obligatory }).SetClass("alert")
|
||||
})
|
||||
)
|
||||
|
||||
const confirm = new CheckBoxes([t.confirm])
|
||||
super([
|
||||
new Title(t.title, 1),
|
||||
layerPicker,
|
||||
new Toggle(t.autodetected.SetClass("thanks"), undefined, autodetected),
|
||||
mismatchIndicator,
|
||||
ui,
|
||||
new DivContainer("fullscreen"),
|
||||
confirm,
|
||||
])
|
||||
|
||||
this.Value = bbox.map(
|
||||
(bbox) => ({
|
||||
bbox,
|
||||
features: matching.data,
|
||||
layer: layerPicker.GetValue().data,
|
||||
}),
|
||||
[layerPicker.GetValue(), matching]
|
||||
)
|
||||
|
||||
this.IsValid = matching.map(
|
||||
(matching) => {
|
||||
if (matching === undefined) {
|
||||
return false
|
||||
}
|
||||
if (confirm.GetValue().data.length !== 1) {
|
||||
return false
|
||||
}
|
||||
const diff = geojson.features.length - matching.length
|
||||
return diff === 0
|
||||
},
|
||||
[confirm.GetValue()]
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Utils } from "../../Utils"
|
||||
import { FlowStep } from "./FlowStep"
|
||||
import Title from "../Base/Title"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Histogram from "../BigComponents/Histogram"
|
||||
import Toggleable from "../Base/Toggleable"
|
||||
import List from "../Base/List"
|
||||
import CheckBoxes from "../Input/Checkboxes"
|
||||
import { Feature, Point } from "geojson"
|
||||
|
||||
/**
|
||||
* Shows the attributes by value, requests to check them of
|
||||
*/
|
||||
export class PreviewAttributesPanel
|
||||
extends Combine
|
||||
implements FlowStep<{ features: Feature<Point>[] }>
|
||||
{
|
||||
public readonly IsValid: Store<boolean>
|
||||
public readonly Value: Store<{ features: Feature<Point>[] }>
|
||||
|
||||
constructor(state: UserRelatedState, geojson: { features: Feature<Point>[] }) {
|
||||
const t = Translations.t.importHelper.previewAttributes
|
||||
|
||||
const propertyKeys = new Set<string>()
|
||||
for (const f of geojson.features) {
|
||||
Object.keys(f.properties).forEach((key) => propertyKeys.add(key))
|
||||
}
|
||||
|
||||
const attributeOverview: BaseUIElement[] = []
|
||||
|
||||
const n = geojson.features.length
|
||||
for (const key of Array.from(propertyKeys)) {
|
||||
const values = Utils.NoNull(geojson.features.map((f) => f.properties[key]))
|
||||
const allSame = !values.some((v) => v !== values[0])
|
||||
let countSummary: BaseUIElement
|
||||
if (values.length === n) {
|
||||
countSummary = t.allAttributesSame
|
||||
} else {
|
||||
countSummary = t.someHaveSame.Subs({
|
||||
count: values.length,
|
||||
percentage: Math.floor((100 * values.length) / n),
|
||||
})
|
||||
}
|
||||
if (allSame) {
|
||||
attributeOverview.push(new Title(key + "=" + values[0]))
|
||||
attributeOverview.push(countSummary)
|
||||
continue
|
||||
}
|
||||
|
||||
const uniqueCount = new Set(values).size
|
||||
if (uniqueCount !== values.length && uniqueCount < 15) {
|
||||
attributeOverview.push()
|
||||
// There are some overlapping values: histogram time!
|
||||
let hist: BaseUIElement = new Combine([
|
||||
countSummary,
|
||||
new Histogram(new UIEventSource<string[]>(values), "Value", "Occurence", {
|
||||
sortMode: "count-rev",
|
||||
}),
|
||||
]).SetClass("flex flex-col")
|
||||
|
||||
const title = new Title(key + "=*")
|
||||
if (uniqueCount > 15) {
|
||||
hist = new Toggleable(title, hist.SetClass("block")).Collapse()
|
||||
} else {
|
||||
attributeOverview.push(title)
|
||||
}
|
||||
|
||||
attributeOverview.push(hist)
|
||||
continue
|
||||
}
|
||||
|
||||
// All values are different or too much unique values, we add a boring (but collapsable) list
|
||||
attributeOverview.push(
|
||||
new Toggleable(new Title(key + "=*"), new Combine([countSummary, new List(values)]))
|
||||
)
|
||||
}
|
||||
|
||||
const confirm = new CheckBoxes([t.inspectLooksCorrect])
|
||||
|
||||
super([
|
||||
new Title(t.inspectDataTitle.Subs({ count: geojson.features.length })),
|
||||
"Extra remark: An attribute with 'source' or 'src' will be added as 'source' into the map pin; an attribute 'note' will be added into the map pin as well. These values won't be imported",
|
||||
...attributeOverview,
|
||||
confirm,
|
||||
])
|
||||
|
||||
this.Value = new UIEventSource<{ features: Feature<Point>[] }>(geojson)
|
||||
this.IsValid = confirm.GetValue().map((selected) => selected.length == 1)
|
||||
}
|
||||
}
|
|
@ -1,188 +0,0 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import { Store, Stores } from "../../Logic/UIEventSource"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Title from "../Base/Title"
|
||||
import InputElementMap from "../Input/InputElementMap"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import FileSelectorButton from "../Input/FileSelectorButton"
|
||||
import { FlowStep } from "./FlowStep"
|
||||
import { parse } from "papaparse"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||
import { Feature, Point } from "geojson"
|
||||
|
||||
class FileSelector extends InputElementMap<FileList, { name: string; contents: Promise<string> }> {
|
||||
constructor(label: BaseUIElement) {
|
||||
super(
|
||||
new FileSelectorButton(label, { allowMultiple: false, acceptType: "*" }),
|
||||
(x0, x1) => {
|
||||
// Total hack: x1 is undefined is the backvalue - we effectively make this a one-way-story
|
||||
return x1 === undefined || x0 === x1
|
||||
},
|
||||
(filelist) => {
|
||||
if (filelist === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const file = filelist.item(0)
|
||||
return { name: file.name, contents: file.text() }
|
||||
},
|
||||
(_) => undefined
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The first step in the import flow: load a file and validate that it is a correct geojson or CSV file
|
||||
*/
|
||||
export class RequestFile extends Combine implements FlowStep<{ features: any[] }> {
|
||||
public readonly IsValid: Store<boolean>
|
||||
/**
|
||||
* The loaded GeoJSON
|
||||
*/
|
||||
public readonly Value: Store<{ features: Feature<Point>[] }>
|
||||
|
||||
constructor() {
|
||||
const t = Translations.t.importHelper.selectFile
|
||||
const csvSelector = new FileSelector(new SubtleButton(undefined, t.description))
|
||||
const loadedFiles = new VariableUiElement(
|
||||
csvSelector.GetValue().map((file) => {
|
||||
if (file === undefined) {
|
||||
return t.noFilesLoaded.SetClass("alert")
|
||||
}
|
||||
return t.loadedFilesAre.Subs({ file: file.name }).SetClass("thanks")
|
||||
})
|
||||
)
|
||||
|
||||
const text = Stores.flatten(
|
||||
csvSelector.GetValue().map((v) => {
|
||||
if (v === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return Stores.FromPromise(v.contents)
|
||||
})
|
||||
)
|
||||
|
||||
const asGeoJson: Store<any | { error: string | BaseUIElement }> = text.map(
|
||||
(src: string) => {
|
||||
if (src === undefined) {
|
||||
return undefined
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(src)
|
||||
if (parsed["type"] !== "FeatureCollection") {
|
||||
return { error: t.errNotFeatureCollection }
|
||||
}
|
||||
if (parsed.features.some((f) => f.geometry.type != "Point")) {
|
||||
return { error: t.errPointsOnly }
|
||||
}
|
||||
parsed.features.forEach((f) => {
|
||||
const props = f.properties
|
||||
for (const key in props) {
|
||||
if (
|
||||
props[key] === undefined ||
|
||||
props[key] === null ||
|
||||
props[key] === ""
|
||||
) {
|
||||
delete props[key]
|
||||
}
|
||||
if (!TagUtils.isValidKey(key)) {
|
||||
return { error: "Probably an invalid key: " + key }
|
||||
}
|
||||
}
|
||||
})
|
||||
return parsed
|
||||
} catch (e) {
|
||||
// Loading as CSV
|
||||
var lines: string[][] = <any>parse(src).data
|
||||
const header = lines[0]
|
||||
lines.splice(0, 1)
|
||||
if (header.indexOf("lat") < 0 || header.indexOf("lon") < 0) {
|
||||
return { error: t.errNoLatOrLon }
|
||||
}
|
||||
|
||||
if (header.some((h) => h.trim() == "")) {
|
||||
return { error: t.errNoName }
|
||||
}
|
||||
|
||||
if (new Set(header).size !== header.length) {
|
||||
return { error: t.errDuplicate }
|
||||
}
|
||||
|
||||
const features = []
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const attrs = lines[i]
|
||||
if (attrs.length == 0 || (attrs.length == 1 && attrs[0] == "")) {
|
||||
// empty line
|
||||
continue
|
||||
}
|
||||
const properties = {}
|
||||
for (let i = 0; i < header.length; i++) {
|
||||
const v = attrs[i]
|
||||
if (v === undefined || v === "") {
|
||||
continue
|
||||
}
|
||||
properties[header[i]] = v
|
||||
}
|
||||
const coordinates = [Number(properties["lon"]), Number(properties["lat"])]
|
||||
delete properties["lat"]
|
||||
delete properties["lon"]
|
||||
if (coordinates.some(isNaN)) {
|
||||
return { error: "A coordinate could not be parsed for line " + (i + 2) }
|
||||
}
|
||||
const f = {
|
||||
type: "Feature",
|
||||
properties,
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates,
|
||||
},
|
||||
}
|
||||
features.push(f)
|
||||
}
|
||||
|
||||
return {
|
||||
type: "FeatureCollection",
|
||||
features,
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const errorIndicator = new VariableUiElement(
|
||||
asGeoJson.map((v) => {
|
||||
if (v === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (v?.error === undefined) {
|
||||
return undefined
|
||||
}
|
||||
let err: BaseUIElement
|
||||
if (typeof v.error === "string") {
|
||||
err = new FixedUiElement(v.error)
|
||||
} else if (v.error.Clone !== undefined) {
|
||||
err = v.error.Clone()
|
||||
} else {
|
||||
err = v.error
|
||||
}
|
||||
return err.SetClass("alert")
|
||||
})
|
||||
)
|
||||
|
||||
super([
|
||||
new Title(t.title, 1),
|
||||
t.fileFormatDescription,
|
||||
t.fileFormatDescriptionCsv,
|
||||
t.fileFormatDescriptionGeoJson,
|
||||
csvSelector,
|
||||
loadedFiles,
|
||||
errorIndicator,
|
||||
])
|
||||
this.SetClass("flex flex-col wi")
|
||||
this.IsValid = asGeoJson.map(
|
||||
(geojson) => geojson !== undefined && geojson["error"] === undefined
|
||||
)
|
||||
this.Value = asGeoJson
|
||||
}
|
||||
}
|
|
@ -1,192 +0,0 @@
|
|||
import { FlowStep } from "./FlowStep"
|
||||
import Combine from "../Base/Combine"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { InputElement } from "../Input/InputElement"
|
||||
import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts"
|
||||
import { FixedInputElement } from "../Input/FixedInputElement"
|
||||
import Img from "../Base/Img"
|
||||
import Title from "../Base/Title"
|
||||
import { RadioButton } from "../Input/RadioButton"
|
||||
import { And } from "../../Logic/Tags/And"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Toggleable from "../Base/Toggleable"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import PresetConfig from "../../Models/ThemeConfig/PresetConfig"
|
||||
import List from "../Base/List"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
||||
export default class SelectTheme
|
||||
extends Combine
|
||||
implements
|
||||
FlowStep<{
|
||||
features: any[]
|
||||
theme: string
|
||||
layer: LayerConfig
|
||||
bbox: BBox
|
||||
}>
|
||||
{
|
||||
public readonly Value: Store<{
|
||||
features: any[]
|
||||
theme: string
|
||||
layer: LayerConfig
|
||||
bbox: BBox
|
||||
}>
|
||||
public readonly IsValid: Store<boolean>
|
||||
|
||||
constructor(params: { features: any[]; layer: LayerConfig; bbox: BBox }) {
|
||||
const t = Translations.t.importHelper.selectTheme
|
||||
let options: InputElement<string>[] = Array.from(AllKnownLayouts.allKnownLayouts.values())
|
||||
.filter((th) => th.layers.some((l) => l.id === params.layer.id))
|
||||
.filter((th) => th.id !== "personal")
|
||||
.map(
|
||||
(th) =>
|
||||
new FixedInputElement<string>(
|
||||
new Combine([
|
||||
new Img(th.icon).SetClass("block h-12 w-12 br-4"),
|
||||
new Title(th.title),
|
||||
]).SetClass("flex items-center"),
|
||||
th.id
|
||||
)
|
||||
)
|
||||
|
||||
const themeRadios = new RadioButton<string>(options, {
|
||||
selectFirstAsDefault: false,
|
||||
})
|
||||
|
||||
const applicablePresets = themeRadios.GetValue().map((theme) => {
|
||||
if (theme === undefined) {
|
||||
return []
|
||||
}
|
||||
// we get the layer with the correct ID via the actual theme config, as the actual theme might have different presets due to overrides
|
||||
const themeConfig = AllKnownLayouts.allKnownLayouts.get(theme)
|
||||
const layer = themeConfig.layers.find((l) => l.id === params.layer.id)
|
||||
return layer.presets
|
||||
})
|
||||
|
||||
const nonMatchedElements = applicablePresets.map((presets) => {
|
||||
if (presets === undefined || presets.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return params.features.filter(
|
||||
(feat) =>
|
||||
!presets.some((preset) =>
|
||||
new And(preset.tags).matchesProperties(feat.properties)
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
super([
|
||||
new Title(t.title),
|
||||
t.intro,
|
||||
themeRadios,
|
||||
new VariableUiElement(
|
||||
applicablePresets.map(
|
||||
(applicablePresets) => {
|
||||
if (themeRadios.GetValue().data === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (applicablePresets === undefined || applicablePresets.length === 0) {
|
||||
return t.noMatchingPresets.SetClass("alert")
|
||||
}
|
||||
},
|
||||
[themeRadios.GetValue()]
|
||||
)
|
||||
),
|
||||
|
||||
new VariableUiElement(
|
||||
nonMatchedElements.map(
|
||||
(unmatched) =>
|
||||
SelectTheme.nonMatchedElementsPanel(unmatched, applicablePresets.data),
|
||||
[applicablePresets]
|
||||
)
|
||||
),
|
||||
])
|
||||
this.SetClass("flex flex-col")
|
||||
|
||||
this.Value = themeRadios.GetValue().map((theme) => ({
|
||||
features: params.features,
|
||||
layer: params.layer,
|
||||
bbox: params.bbox,
|
||||
theme,
|
||||
}))
|
||||
|
||||
this.IsValid = this.Value.map(
|
||||
(obj) => {
|
||||
if (obj === undefined) {
|
||||
return false
|
||||
}
|
||||
if ([obj.theme, obj.features].some((v) => v === undefined)) {
|
||||
return false
|
||||
}
|
||||
if (applicablePresets.data === undefined || applicablePresets.data.length === 0) {
|
||||
return false
|
||||
}
|
||||
if ((nonMatchedElements.data?.length ?? 0) > 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
[applicablePresets]
|
||||
)
|
||||
}
|
||||
|
||||
private static nonMatchedElementsPanel(
|
||||
unmatched: any[],
|
||||
applicablePresets: PresetConfig[]
|
||||
): BaseUIElement {
|
||||
if (unmatched === undefined || unmatched.length === 0) {
|
||||
return
|
||||
}
|
||||
const t = Translations.t.importHelper.selectTheme
|
||||
|
||||
const applicablePresetsOverview = applicablePresets.map((preset) =>
|
||||
t.needsTags
|
||||
.Subs({
|
||||
title: preset.title,
|
||||
tags: preset.tags.map((t) => t.asHumanString()).join(" & "),
|
||||
})
|
||||
.SetClass("thanks")
|
||||
)
|
||||
|
||||
const unmatchedPanels: BaseUIElement[] = []
|
||||
for (const feat of unmatched) {
|
||||
const parts: BaseUIElement[] = []
|
||||
parts.push(
|
||||
new Combine(
|
||||
Object.keys(feat.properties).map((k) => k + "=" + feat.properties[k])
|
||||
).SetClass("flex flex-col")
|
||||
)
|
||||
|
||||
for (const preset of applicablePresets) {
|
||||
const tags = new And(preset.tags).asChange({})
|
||||
const missing = []
|
||||
for (const { k, v } of tags) {
|
||||
if (preset[k] === undefined) {
|
||||
missing.push(t.missing.Subs({ k, v }))
|
||||
} else if (feat.properties[k] !== v) {
|
||||
missing.push(t.misMatch.Subs({ k, v, properties: feat.properties }))
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
parts.push(
|
||||
new Combine([t.notApplicable.Subs(preset), new List(missing)]).SetClass(
|
||||
"flex flex-col alert"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
unmatchedPanels.push(new Combine(parts).SetClass("flex flex-col"))
|
||||
}
|
||||
|
||||
return new Combine([
|
||||
t.displayNonMatchingCount.Subs(unmatched).SetClass("alert"),
|
||||
...applicablePresetsOverview,
|
||||
new Toggleable(new Title(t.unmatchedTitle), new Combine(unmatchedPanels)),
|
||||
]).SetClass("flex flex-col")
|
||||
}
|
||||
}
|
|
@ -196,7 +196,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
|
||||
async exportAsPng(): Promise<Blob> {
|
||||
const map = this._maplibreMap.data
|
||||
if (map === undefined) {
|
||||
if (!map) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
@ -317,7 +317,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
|
||||
private async awaitStyleIsLoaded(): Promise<void> {
|
||||
const map = this._maplibreMap.data
|
||||
if (map === undefined) {
|
||||
if (!map) {
|
||||
return
|
||||
}
|
||||
while (!map?.isStyleLoaded()) {
|
||||
|
@ -335,7 +335,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
|
||||
private async setBackground() {
|
||||
const map = this._maplibreMap.data
|
||||
if (map === undefined) {
|
||||
if (!map) {
|
||||
return
|
||||
}
|
||||
const background: RasterLayerProperties = this.rasterLayer?.data?.properties
|
||||
|
@ -381,7 +381,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
|
||||
private setMaxBounds(bbox: undefined | BBox) {
|
||||
const map = this._maplibreMap.data
|
||||
if (map === undefined) {
|
||||
if (!map) {
|
||||
return
|
||||
}
|
||||
if (bbox) {
|
||||
|
@ -393,7 +393,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
|
||||
private setAllowMoving(allow: true | boolean | undefined) {
|
||||
const map = this._maplibreMap.data
|
||||
if (map === undefined) {
|
||||
if (!map) {
|
||||
return
|
||||
}
|
||||
if (allow === false) {
|
||||
|
@ -409,7 +409,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
|
||||
private setMinzoom(minzoom: number) {
|
||||
const map = this._maplibreMap.data
|
||||
if (map === undefined) {
|
||||
if (!map) {
|
||||
return
|
||||
}
|
||||
map.setMinZoom(minzoom)
|
||||
|
@ -417,7 +417,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
|
||||
private setMaxzoom(maxzoom: number) {
|
||||
const map = this._maplibreMap.data
|
||||
if (map === undefined) {
|
||||
if (!map) {
|
||||
return
|
||||
}
|
||||
map.setMaxZoom(maxzoom)
|
||||
|
@ -425,7 +425,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
|
||||
private setAllowZooming(allow: true | boolean | undefined) {
|
||||
const map = this._maplibreMap.data
|
||||
if (map === undefined) {
|
||||
if (!map) {
|
||||
return
|
||||
}
|
||||
if (allow === false) {
|
||||
|
@ -441,7 +441,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
|
||||
private setBounds(bounds: BBox) {
|
||||
const map = this._maplibreMap.data
|
||||
if (map === undefined || bounds === undefined) {
|
||||
if (!map || bounds === undefined) {
|
||||
return
|
||||
}
|
||||
const oldBounds = map.getBounds()
|
||||
|
|
|
@ -164,9 +164,9 @@
|
|||
{#if config.freeform?.key}
|
||||
<label class="flex">
|
||||
<input type="radio" bind:group={selectedMapping} name={"mappings-radio-"+config.id}
|
||||
value={config.mappings.length}>
|
||||
value={config.mappings?.length}>
|
||||
<FreeformInput {config} {tags} feature={selectedElement} value={freeformInput}
|
||||
on:selected={() => selectedMapping = config.mappings.length }/>
|
||||
on:selected={() => selectedMapping = config.mappings?.length }/>
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -182,7 +182,7 @@
|
|||
{/each}
|
||||
{#if config.freeform?.key}
|
||||
<label class="flex">
|
||||
<input type="checkbox" name={"mappings-checkbox-"+config.id+"-"+config.mappings.length}
|
||||
<input type="checkbox" name={"mappings-checkbox-"+config.id+"-"+config.mappings?.length}
|
||||
bind:checked={checkedMappings[config.mappings.length]}>
|
||||
<FreeformInput {config} {tags} feature={selectedElement} value={freeformInput}
|
||||
on:selected={() => checkedMappings[config.mappings.length] = true}/>
|
||||
|
|
|
@ -3,10 +3,9 @@ import Combine from "./Base/Combine"
|
|||
import Title from "./Base/Title"
|
||||
import List from "./Base/List"
|
||||
import Translations from "./i18n/Translations"
|
||||
import { QueryParameters } from "../Logic/Web/QueryParameters"
|
||||
import {QueryParameters} from "../Logic/Web/QueryParameters"
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
||||
import { DefaultGuiState } from "./DefaultGuiState"
|
||||
|
||||
export default class QueryParameterDocumentation {
|
||||
private static QueryParamDocsIntro = [
|
||||
|
@ -48,7 +47,6 @@ export default class QueryParameterDocumentation {
|
|||
},
|
||||
],
|
||||
})
|
||||
new DefaultGuiState() // Init a featureSwitchState to init all the parameters
|
||||
new FeatureSwitchState(dummyLayout)
|
||||
|
||||
QueryParameters.GetQueryParameter(
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
import OverlayToggle from "./BigComponents/OverlayToggle.svelte";
|
||||
import LevelSelector from "./BigComponents/LevelSelector.svelte";
|
||||
import Svg from "../Svg";
|
||||
import ExtraLinkButton from "./BigComponents/ExtraLinkButton";
|
||||
|
||||
export let state: ThemeViewState;
|
||||
let layout = state.layout;
|
||||
|
@ -97,10 +98,11 @@
|
|||
construct={() =>(currentViewLayer.defaultIcon() ?? Svg.checkbox_empty_svg()).SetClass("w-8 h-8 cursor-pointer")}/>
|
||||
</MapControlButton>
|
||||
{/if}
|
||||
<ToSvelte construct={() => new ExtraLinkButton(state, layout.extraLink)}></ToSvelte>
|
||||
<If condition={state.featureSwitchIsTesting}>
|
||||
<span class="alert">
|
||||
Testmode
|
||||
</span>
|
||||
<span class="alert">
|
||||
Testmode
|
||||
</span>
|
||||
</If>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -1,130 +1,53 @@
|
|||
import FeaturePipelineState from "../Logic/State/FeaturePipelineState"
|
||||
import MinimapImplementation from "../UI/Base/MinimapImplementation"
|
||||
import { UIEventSource } from "../Logic/UIEventSource"
|
||||
import Loc from "../Models/Loc"
|
||||
import ShowDataLayer from "../UI/ShowDataLayer/ShowDataLayer"
|
||||
import { BBox } from "../Logic/BBox"
|
||||
import Minimap from "../UI/Base/Minimap"
|
||||
import AvailableBaseLayers from "../Logic/Actors/AvailableBaseLayers"
|
||||
import ThemeViewState from "../Models/ThemeViewState"
|
||||
import SvelteUIElement from "../UI/Base/SvelteUIElement"
|
||||
import MaplibreMap from "../UI/Map/MaplibreMap.svelte"
|
||||
import { Utils } from "../Utils"
|
||||
import { UIEventSource } from "../Logic/UIEventSource"
|
||||
|
||||
export interface PngMapCreatorOptions {
|
||||
readonly divId: string
|
||||
readonly width: number
|
||||
readonly height: number
|
||||
readonly scaling?: 1 | number
|
||||
readonly dummyMode?: boolean
|
||||
}
|
||||
|
||||
export class PngMapCreator {
|
||||
private readonly _state: FeaturePipelineState | undefined
|
||||
private static id = 0
|
||||
private readonly _options: PngMapCreatorOptions
|
||||
private readonly _state: ThemeViewState
|
||||
|
||||
constructor(state: FeaturePipelineState | undefined, options: PngMapCreatorOptions) {
|
||||
constructor(state: ThemeViewState, options: PngMapCreatorOptions) {
|
||||
this._state = state
|
||||
this._options = { ...options, scaling: options.scaling ?? 1 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a minimap, waits till all needed tiles are loaded before returning
|
||||
* @private
|
||||
*/
|
||||
private async createAndLoadMinimap(): Promise<MinimapImplementation> {
|
||||
const state = this._state
|
||||
const options = this._options
|
||||
const baselayer =
|
||||
AvailableBaseLayers.layerOverview.find(
|
||||
(bl) => bl.id === state.layoutToUse.defaultBackgroundId
|
||||
) ?? AvailableBaseLayers.osmCarto
|
||||
return new Promise((resolve) => {
|
||||
const minimap = Minimap.createMiniMap({
|
||||
location: new UIEventSource<Loc>(state.locationControl.data), // We remove the link between the old and the new UI-event source as moving the map while the export is running fucks up the screenshot
|
||||
background: new UIEventSource(baselayer),
|
||||
allowMoving: false,
|
||||
onFullyLoaded: (_) =>
|
||||
window.setTimeout(() => {
|
||||
resolve(<MinimapImplementation>minimap)
|
||||
}, 250),
|
||||
})
|
||||
const style = `width: ${options.width * options.scaling}mm; height: ${
|
||||
options.height * options.scaling
|
||||
}mm;`
|
||||
minimap.SetStyle(style)
|
||||
minimap.AttachTo(options.divId)
|
||||
})
|
||||
this._options = options
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a base64-encoded PNG image
|
||||
* @constructor
|
||||
*/
|
||||
public async CreatePng(format: "image"): Promise<string>
|
||||
public async CreatePng(format: "blob"): Promise<Blob>
|
||||
public async CreatePng(format: "image" | "blob"): Promise<string | Blob>
|
||||
public async CreatePng(format: "image" | "blob"): Promise<string | Blob> {
|
||||
// Lets first init the minimap and wait for all background tiles to load
|
||||
const minimap = await this.createAndLoadMinimap()
|
||||
const state = this._state
|
||||
const dummyMode = this._options.dummyMode ?? false
|
||||
return new Promise<string | Blob>((resolve, reject) => {
|
||||
// Next: we prepare the features. Only fully contained features are shown
|
||||
minimap.leafletMap.addCallbackAndRunD(async (leaflet) => {
|
||||
// Ping the featurepipeline to download what is needed
|
||||
if (dummyMode) {
|
||||
console.warn("Dummy mode is active - not loading map layers")
|
||||
} else {
|
||||
const bounds = BBox.fromLeafletBounds(
|
||||
leaflet.getBounds().pad(0.1).pad(-state.layoutToUse.widenFactor)
|
||||
)
|
||||
state.currentBounds.setData(bounds)
|
||||
if (!state.featurePipeline.sufficientlyZoomed.data) {
|
||||
console.warn("Not sufficiently zoomed!")
|
||||
}
|
||||
|
||||
if (state.featurePipeline.runningQuery.data) {
|
||||
// A query is running!
|
||||
// Let's wait for it to complete
|
||||
console.log("Waiting for the query to complete")
|
||||
await state.featurePipeline.runningQuery.AsPromise(
|
||||
(isRunning) => !isRunning
|
||||
)
|
||||
console.log("Query has completeted!")
|
||||
}
|
||||
|
||||
state.featurePipeline.GetTilesPerLayerWithin(bounds, (tile) => {
|
||||
if (tile.layer.layerDef.id.startsWith("note_import")) {
|
||||
// Don't export notes to import
|
||||
return
|
||||
}
|
||||
new ShowDataLayer({
|
||||
features: tile,
|
||||
leafletMap: minimap.leafletMap,
|
||||
layerToShow: tile.layer.layerDef,
|
||||
doShowLayer: tile.layer.isDisplayed,
|
||||
state: undefined,
|
||||
})
|
||||
})
|
||||
await Utils.waitFor(2000)
|
||||
}
|
||||
minimap
|
||||
.TakeScreenshot(format)
|
||||
.then(async (result) => {
|
||||
const divId = this._options.divId
|
||||
await Utils.waitFor(250)
|
||||
document
|
||||
.getElementById(divId)
|
||||
.removeChild(
|
||||
/*Will fetch the cached htmlelement:*/ minimap.ConstructElement()
|
||||
)
|
||||
return resolve(result)
|
||||
})
|
||||
.catch((failreason) => {
|
||||
console.error("Could no make a screenshot due to ", failreason)
|
||||
reject(failreason)
|
||||
})
|
||||
})
|
||||
|
||||
state.AddAllOverlaysToMap(minimap.leafletMap)
|
||||
})
|
||||
public async CreatePng(status: UIEventSource<string>): Promise<Blob> {
|
||||
const div = document.createElement("div")
|
||||
div.id = "mapdiv-" + PngMapCreator.id
|
||||
PngMapCreator.id++
|
||||
const layout = this._state.layout
|
||||
function setState(msg: string) {
|
||||
status.setData(layout.id + ": " + msg)
|
||||
}
|
||||
setState("Initializing map")
|
||||
const map = this._state.map
|
||||
new SvelteUIElement(MaplibreMap, { map })
|
||||
.SetStyle(
|
||||
"width: " + this._options.width + "mm; height: " + this._options.height + "mm"
|
||||
)
|
||||
.AttachTo("extradiv")
|
||||
setState("Waiting for the data")
|
||||
await this._state.dataIsLoading.AsPromise((loading) => !loading)
|
||||
setState("Waiting for styles to be fully loaded")
|
||||
while (!map?.data?.isStyleLoaded()) {
|
||||
await Utils.waitFor(250)
|
||||
}
|
||||
// Some extra buffer...
|
||||
await Utils.waitFor(1000)
|
||||
setState("Exporting png")
|
||||
console.log("Loading for", this._state.layout.id, "is done")
|
||||
return this._state.mapProperties.exportAsPng()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,20 +2,21 @@ import jsPDF, { Matrix } from "jspdf"
|
|||
import { Translation, TypedTranslation } from "../UI/i18n/Translation"
|
||||
import { PngMapCreator } from "./pngMapCreator"
|
||||
import { AllKnownLayouts } from "../Customizations/AllKnownLayouts"
|
||||
import { Store } from "../Logic/UIEventSource"
|
||||
import "../assets/templates/Ubuntu-M-normal.js"
|
||||
import "../assets/templates/Ubuntu-L-normal.js"
|
||||
import "../assets/templates/UbuntuMono-B-bold.js"
|
||||
import "../assets/fonts/Ubuntu-M-normal.js"
|
||||
import "../assets/fonts/Ubuntu-L-normal.js"
|
||||
import "../assets/fonts/UbuntuMono-B-bold.js"
|
||||
import { makeAbsolute, parseSVG } from "svg-path-parser"
|
||||
import Translations from "../UI/i18n/Translations"
|
||||
import { Utils } from "../Utils"
|
||||
import Constants from "../Models/Constants"
|
||||
import Hash from "../Logic/Web/Hash"
|
||||
import ThemeViewState from "../Models/ThemeViewState"
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import { FixedUiElement } from "../UI/Base/FixedUiElement"
|
||||
|
||||
class SvgToPdfInternals {
|
||||
private readonly doc: jsPDF
|
||||
private static readonly dummyDoc: jsPDF = new jsPDF()
|
||||
private readonly doc: jsPDF
|
||||
private readonly matrices: Matrix[] = []
|
||||
private readonly matricesInverted: Matrix[] = []
|
||||
|
||||
|
@ -40,26 +41,6 @@ class SvgToPdfInternals {
|
|||
this.currentMatrixInverted = this.doc.unitMatrix
|
||||
}
|
||||
|
||||
applyMatrices(): void {
|
||||
let multiplied = this.doc.unitMatrix
|
||||
let multipliedInv = this.doc.unitMatrix
|
||||
for (const matrix of this.matrices) {
|
||||
multiplied = this.doc.matrixMult(multiplied, matrix)
|
||||
}
|
||||
for (const matrix of this.matricesInverted) {
|
||||
multipliedInv = this.doc.matrixMult(multiplied, matrix)
|
||||
}
|
||||
this.currentMatrix = multiplied
|
||||
this.currentMatrixInverted = multipliedInv
|
||||
}
|
||||
|
||||
addMatrix(m: Matrix) {
|
||||
this.matrices.push(m)
|
||||
this.matricesInverted.push(m.inversed())
|
||||
this.doc.setCurrentTransformationMatrix(m)
|
||||
this.applyMatrices()
|
||||
}
|
||||
|
||||
public static extractMatrix(element: Element): Matrix {
|
||||
const t = element.getAttribute("transform")
|
||||
if (t === null) {
|
||||
|
@ -107,22 +88,6 @@ class SvgToPdfInternals {
|
|||
return null
|
||||
}
|
||||
|
||||
public setTransform(element: Element): boolean {
|
||||
const m = SvgToPdfInternals.extractMatrix(element)
|
||||
if (m === null) {
|
||||
return false
|
||||
}
|
||||
this.addMatrix(m)
|
||||
return true
|
||||
}
|
||||
|
||||
public undoTransform(): void {
|
||||
this.matrices.pop()
|
||||
const i = this.matricesInverted.pop()
|
||||
this.doc.setCurrentTransformationMatrix(i)
|
||||
this.applyMatrices()
|
||||
}
|
||||
|
||||
public static parseCss(styleContent: string, separator: string = ";"): Record<string, string> {
|
||||
if (styleContent === undefined || styleContent === null) {
|
||||
return {}
|
||||
|
@ -137,41 +102,36 @@ class SvgToPdfInternals {
|
|||
return r
|
||||
}
|
||||
|
||||
private drawRect(element: SVGRectElement) {
|
||||
const x = Number(element.getAttribute("x"))
|
||||
const y = Number(element.getAttribute("y"))
|
||||
const width = Number(element.getAttribute("width"))
|
||||
const height = Number(element.getAttribute("height"))
|
||||
const ry = SvgToPdfInternals.attrNumber(element, "ry", false) ?? 0
|
||||
const rx = SvgToPdfInternals.attrNumber(element, "rx", false) ?? 0
|
||||
const css = SvgToPdfInternals.css(element)
|
||||
if (css["fill-opacity"] !== "0" && css["fill"] !== "none") {
|
||||
this.doc.setFillColor(css["fill"] ?? "black")
|
||||
this.doc.roundedRect(x, y, width, height, rx, ry, "F")
|
||||
static attrNumber(element: Element, name: string, recurseup: boolean = true): number {
|
||||
const a = SvgToPdfInternals.attr(element, name, recurseup)
|
||||
const n = parseFloat(a)
|
||||
if (!isNaN(n)) {
|
||||
return n
|
||||
}
|
||||
if (css["stroke"] && css["stroke"] !== "none") {
|
||||
this.doc.setLineWidth(Number(css["stroke-width"] ?? 1))
|
||||
this.doc.setDrawColor(css["stroke"] ?? "black")
|
||||
this.doc.roundedRect(x, y, width, height, rx, ry, "S")
|
||||
}
|
||||
return
|
||||
return undefined
|
||||
}
|
||||
|
||||
private drawCircle(element: SVGCircleElement) {
|
||||
const x = Number(element.getAttribute("cx"))
|
||||
const y = Number(element.getAttribute("cy"))
|
||||
const r = Number(element.getAttribute("r"))
|
||||
const css = SvgToPdfInternals.css(element)
|
||||
if (css["fill-opacity"] !== "0" && css["fill"] !== "none") {
|
||||
this.doc.setFillColor(css["fill"] ?? "black")
|
||||
this.doc.circle(x, y, r, "F")
|
||||
}
|
||||
if (css["stroke"] && css["stroke"] !== "none") {
|
||||
this.doc.setLineWidth(Number(css["stroke-width"] ?? 1))
|
||||
this.doc.setDrawColor(css["stroke"] ?? "black")
|
||||
this.doc.circle(x, y, r, "S")
|
||||
}
|
||||
return
|
||||
/**
|
||||
* Helper function to calculate where the given point will end up.
|
||||
* ALl the transforms of the parent elements are taking into account
|
||||
* @param mapSpec
|
||||
* @constructor
|
||||
*/
|
||||
static GetActualXY(mapSpec: SVGTSpanElement): { x: number; y: number } {
|
||||
let runningM = SvgToPdfInternals.dummyDoc.unitMatrix
|
||||
|
||||
let e: Element = mapSpec
|
||||
do {
|
||||
const m = SvgToPdfInternals.extractMatrix(e)
|
||||
if (m !== null) {
|
||||
runningM = SvgToPdfInternals.dummyDoc.matrixMult(runningM, m)
|
||||
}
|
||||
e = e.parentElement
|
||||
} while (e !== null && e.parentElement != e)
|
||||
|
||||
const x = SvgToPdfInternals.attrNumber(mapSpec, "x")
|
||||
const y = SvgToPdfInternals.attrNumber(mapSpec, "y")
|
||||
return runningM.applyToPoint({ x, y })
|
||||
}
|
||||
|
||||
private static attr(
|
||||
|
@ -214,13 +174,119 @@ class SvgToPdfInternals {
|
|||
return css
|
||||
}
|
||||
|
||||
static attrNumber(element: Element, name: string, recurseup: boolean = true): number {
|
||||
const a = SvgToPdfInternals.attr(element, name, recurseup)
|
||||
const n = parseFloat(a)
|
||||
if (!isNaN(n)) {
|
||||
return n
|
||||
applyMatrices(): void {
|
||||
let multiplied = this.doc.unitMatrix
|
||||
let multipliedInv = this.doc.unitMatrix
|
||||
for (const matrix of this.matrices) {
|
||||
multiplied = this.doc.matrixMult(multiplied, matrix)
|
||||
}
|
||||
return undefined
|
||||
for (const matrix of this.matricesInverted) {
|
||||
multipliedInv = this.doc.matrixMult(multiplied, matrix)
|
||||
}
|
||||
this.currentMatrix = multiplied
|
||||
this.currentMatrixInverted = multipliedInv
|
||||
}
|
||||
|
||||
addMatrix(m: Matrix) {
|
||||
this.matrices.push(m)
|
||||
this.matricesInverted.push(m.inversed())
|
||||
this.doc.setCurrentTransformationMatrix(m)
|
||||
this.applyMatrices()
|
||||
}
|
||||
|
||||
public setTransform(element: Element): boolean {
|
||||
const m = SvgToPdfInternals.extractMatrix(element)
|
||||
if (m === null) {
|
||||
return false
|
||||
}
|
||||
this.addMatrix(m)
|
||||
return true
|
||||
}
|
||||
|
||||
public undoTransform(): void {
|
||||
this.matrices.pop()
|
||||
const i = this.matricesInverted.pop()
|
||||
this.doc.setCurrentTransformationMatrix(i)
|
||||
this.applyMatrices()
|
||||
}
|
||||
|
||||
public handleElement(element: SVGSVGElement | Element): void {
|
||||
const isTransformed = this.setTransform(element)
|
||||
try {
|
||||
if (element.tagName === "tspan") {
|
||||
if (element.childElementCount == 0) {
|
||||
this.drawTspan(element)
|
||||
} else {
|
||||
for (let child of Array.from(element.children)) {
|
||||
this.handleElement(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (element.tagName === "image") {
|
||||
this.drawImage(element)
|
||||
}
|
||||
|
||||
if (element.tagName === "path") {
|
||||
this.drawPath(<any>element)
|
||||
}
|
||||
|
||||
if (element.tagName === "g" || element.tagName === "text") {
|
||||
for (let child of Array.from(element.children)) {
|
||||
this.handleElement(child)
|
||||
}
|
||||
}
|
||||
|
||||
if (element.tagName === "rect") {
|
||||
this.drawRect(<any>element)
|
||||
}
|
||||
|
||||
if (element.tagName === "circle") {
|
||||
this.drawCircle(<any>element)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Could not handle element", element, "due to", e)
|
||||
}
|
||||
if (isTransformed) {
|
||||
this.undoTransform()
|
||||
}
|
||||
}
|
||||
|
||||
private drawRect(element: SVGRectElement) {
|
||||
const x = Number(element.getAttribute("x"))
|
||||
const y = Number(element.getAttribute("y"))
|
||||
const width = Number(element.getAttribute("width"))
|
||||
const height = Number(element.getAttribute("height"))
|
||||
const ry = SvgToPdfInternals.attrNumber(element, "ry", false) ?? 0
|
||||
const rx = SvgToPdfInternals.attrNumber(element, "rx", false) ?? 0
|
||||
const css = SvgToPdfInternals.css(element)
|
||||
if (css["fill-opacity"] !== "0" && css["fill"] !== "none") {
|
||||
this.doc.setFillColor(css["fill"] ?? "black")
|
||||
this.doc.roundedRect(x, y, width, height, rx, ry, "F")
|
||||
}
|
||||
if (css["stroke"] && css["stroke"] !== "none") {
|
||||
this.doc.setLineWidth(Number(css["stroke-width"] ?? 1))
|
||||
this.doc.setDrawColor(css["stroke"] ?? "black")
|
||||
this.doc.roundedRect(x, y, width, height, rx, ry, "S")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
private drawCircle(element: SVGCircleElement) {
|
||||
const x = Number(element.getAttribute("cx"))
|
||||
const y = Number(element.getAttribute("cy"))
|
||||
const r = Number(element.getAttribute("r"))
|
||||
const css = SvgToPdfInternals.css(element)
|
||||
if (css["fill-opacity"] !== "0" && css["fill"] !== "none") {
|
||||
this.doc.setFillColor(css["fill"] ?? "black")
|
||||
this.doc.circle(x, y, r, "F")
|
||||
}
|
||||
if (css["stroke"] && css["stroke"] !== "none") {
|
||||
this.doc.setLineWidth(Number(css["stroke-width"] ?? 1))
|
||||
this.doc.setDrawColor(css["stroke"] ?? "black")
|
||||
this.doc.circle(x, y, r, "S")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
private drawTspan(tspan: Element) {
|
||||
|
@ -427,129 +493,43 @@ class SvgToPdfInternals {
|
|||
this.doc.fill()
|
||||
}
|
||||
}
|
||||
|
||||
public handleElement(element: SVGSVGElement | Element): void {
|
||||
const isTransformed = this.setTransform(element)
|
||||
try {
|
||||
if (element.tagName === "tspan") {
|
||||
if (element.childElementCount == 0) {
|
||||
this.drawTspan(element)
|
||||
} else {
|
||||
for (let child of Array.from(element.children)) {
|
||||
this.handleElement(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (element.tagName === "image") {
|
||||
this.drawImage(element)
|
||||
}
|
||||
|
||||
if (element.tagName === "path") {
|
||||
this.drawPath(<any>element)
|
||||
}
|
||||
|
||||
if (element.tagName === "g" || element.tagName === "text") {
|
||||
for (let child of Array.from(element.children)) {
|
||||
this.handleElement(child)
|
||||
}
|
||||
}
|
||||
|
||||
if (element.tagName === "rect") {
|
||||
this.drawRect(<any>element)
|
||||
}
|
||||
|
||||
if (element.tagName === "circle") {
|
||||
this.drawCircle(<any>element)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Could not handle element", element, "due to", e)
|
||||
}
|
||||
if (isTransformed) {
|
||||
this.undoTransform()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to calculate where the given point will end up.
|
||||
* ALl the transforms of the parent elements are taking into account
|
||||
* @param mapSpec
|
||||
* @constructor
|
||||
*/
|
||||
static GetActualXY(mapSpec: SVGTSpanElement): { x: number; y: number } {
|
||||
let runningM = SvgToPdfInternals.dummyDoc.unitMatrix
|
||||
|
||||
let e: Element = mapSpec
|
||||
do {
|
||||
const m = SvgToPdfInternals.extractMatrix(e)
|
||||
if (m !== null) {
|
||||
runningM = SvgToPdfInternals.dummyDoc.matrixMult(runningM, m)
|
||||
}
|
||||
e = e.parentElement
|
||||
} while (e !== null && e.parentElement != e)
|
||||
|
||||
const x = SvgToPdfInternals.attrNumber(mapSpec, "x")
|
||||
const y = SvgToPdfInternals.attrNumber(mapSpec, "y")
|
||||
return runningM.applyToPoint({ x, y })
|
||||
}
|
||||
}
|
||||
|
||||
export interface SvgToPdfOptions {
|
||||
getFreeDiv: () => string
|
||||
disableMaps?: false | true
|
||||
textSubstitutions?: Record<string, string>
|
||||
beforePage?: (i: number) => void
|
||||
overrideLocation?: { lat: number; lon: number }
|
||||
}
|
||||
|
||||
export class SvgToPdfPage {
|
||||
class SvgToPdfPage {
|
||||
public readonly _svgRoot: SVGSVGElement
|
||||
private images: Record<string, HTMLImageElement> = {}
|
||||
private rects: Record<string, SVGRectElement> = {}
|
||||
public readonly _svgRoot: SVGSVGElement
|
||||
public readonly currentState: Store<string>
|
||||
private readonly importedTranslations: Record<string, string> = {}
|
||||
private readonly layerTranslations: Record<string, Record<string, any>> = {}
|
||||
private readonly options: SvgToPdfOptions
|
||||
/**
|
||||
* Small indicator for humans
|
||||
* @private
|
||||
*/
|
||||
private readonly _state: UIEventSource<string>
|
||||
private _isPrepared = false
|
||||
private state: UIEventSource<string>
|
||||
|
||||
constructor(page: string, options?: SvgToPdfOptions) {
|
||||
constructor(page: string, state: UIEventSource<string>, options?: SvgToPdfOptions) {
|
||||
this._state = state
|
||||
this.options = options ?? <SvgToPdfOptions>{}
|
||||
const parser = new DOMParser()
|
||||
const xmlDoc = parser.parseFromString(page, "image/svg+xml")
|
||||
this._svgRoot = xmlDoc.getElementsByTagName("svg")[0]
|
||||
}
|
||||
|
||||
private loadImage(element: Element): Promise<void> {
|
||||
const xlink = element.getAttribute("xlink:href")
|
||||
let img = document.createElement("img")
|
||||
|
||||
if (xlink.startsWith("data:image/svg+xml;")) {
|
||||
const base64src = xlink
|
||||
let svgXml = atob(
|
||||
base64src.substring(base64src.indexOf(";base64,") + ";base64,".length)
|
||||
)
|
||||
const parser = new DOMParser()
|
||||
const xmlDoc = parser.parseFromString(svgXml, "text/xml")
|
||||
const svgRoot = xmlDoc.getElementsByTagName("svg")[0]
|
||||
const svgWidthStr = svgRoot.getAttribute("width")
|
||||
const svgHeightStr = svgRoot.getAttribute("height")
|
||||
const svgWidth = parseFloat(svgWidthStr)
|
||||
const svgHeight = parseFloat(svgHeightStr)
|
||||
if (!svgWidthStr.endsWith("px")) {
|
||||
svgRoot.setAttribute("width", svgWidth + "px")
|
||||
}
|
||||
if (!svgHeightStr.endsWith("px")) {
|
||||
svgRoot.setAttribute("height", svgHeight + "px")
|
||||
}
|
||||
img.src = "data:image/svg+xml;base64," + btoa(svgRoot.outerHTML)
|
||||
} else {
|
||||
img.src = xlink
|
||||
}
|
||||
|
||||
this.images[xlink] = img
|
||||
return new Promise((resolve) => {
|
||||
img.onload = (_) => {
|
||||
resolve()
|
||||
}
|
||||
private static blobToBase64(blob): Promise<string> {
|
||||
return new Promise((resolve, _) => {
|
||||
const reader = new FileReader()
|
||||
reader.onloadend = () => resolve(<string>reader.result)
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -628,166 +608,6 @@ export class SvgToPdfPage {
|
|||
}
|
||||
}
|
||||
|
||||
private _isPrepared = false
|
||||
|
||||
private async prepareMap(mapSpec: SVGTSpanElement): Promise<void> {
|
||||
// Upper left point of the tspan
|
||||
const { x, y } = SvgToPdfInternals.GetActualXY(mapSpec)
|
||||
|
||||
let textElement: Element = mapSpec
|
||||
// We recurse up to get the actual, full specification
|
||||
while (textElement.tagName !== "text") {
|
||||
textElement = textElement.parentElement
|
||||
}
|
||||
const spec = textElement.textContent
|
||||
const match = spec.match(/\$map\(([^)]+)\)$/)
|
||||
if (match === null) {
|
||||
throw "Invalid mapspec:" + spec
|
||||
}
|
||||
const params = SvgToPdfInternals.parseCss(match[1], ",")
|
||||
|
||||
let smallestRect: SVGRectElement = undefined
|
||||
let smallestSurface: number = undefined
|
||||
// We iterate over all the rectangles and pick the smallest (by surface area) that contains the upper left point of the tspan
|
||||
for (const id in this.rects) {
|
||||
const rect = this.rects[id]
|
||||
const rx = SvgToPdfInternals.attrNumber(rect, "x")
|
||||
const ry = SvgToPdfInternals.attrNumber(rect, "y")
|
||||
const w = SvgToPdfInternals.attrNumber(rect, "width")
|
||||
const h = SvgToPdfInternals.attrNumber(rect, "height")
|
||||
const inBounds = rx <= x && x <= rx + w && ry <= y && y <= ry + h
|
||||
if (!inBounds) {
|
||||
continue
|
||||
}
|
||||
const surface = w * h
|
||||
if (smallestSurface === undefined || smallestSurface > surface) {
|
||||
smallestSurface = surface
|
||||
smallestRect = rect
|
||||
}
|
||||
}
|
||||
|
||||
if (smallestRect === undefined) {
|
||||
throw (
|
||||
"No rectangle found around " +
|
||||
spec +
|
||||
". Draw a rectangle around it, the map will be projected on that one"
|
||||
)
|
||||
}
|
||||
|
||||
const svgImage = document.createElement("image")
|
||||
svgImage.setAttribute("x", smallestRect.getAttribute("x"))
|
||||
svgImage.setAttribute("y", smallestRect.getAttribute("y"))
|
||||
const width = SvgToPdfInternals.attrNumber(smallestRect, "width")
|
||||
const height = SvgToPdfInternals.attrNumber(smallestRect, "height")
|
||||
svgImage.setAttribute("width", "" + width)
|
||||
svgImage.setAttribute("height", "" + height)
|
||||
|
||||
let layout = AllKnownLayouts.allKnownLayouts.get(params["theme"])
|
||||
if (layout === undefined) {
|
||||
console.error("Could not show map with parameters", params)
|
||||
throw (
|
||||
"Theme not found:" + params["theme"] + ". Use theme: to define which theme to use. "
|
||||
)
|
||||
}
|
||||
layout.widenFactor = 0
|
||||
layout.overpassTimeout = 600
|
||||
layout.defaultBackgroundId = params["background"] ?? layout.defaultBackgroundId
|
||||
for (const paramsKey in params) {
|
||||
if (paramsKey.startsWith("layer-")) {
|
||||
const layerName = paramsKey.substring("layer-".length)
|
||||
const key = params[paramsKey].toLowerCase().trim()
|
||||
const layer = layout.layers.find((l) => l.id === layerName)
|
||||
if (layer === undefined) {
|
||||
throw "No layer found for " + paramsKey
|
||||
}
|
||||
if (key === "force") {
|
||||
layer.minzoom = 0
|
||||
layer.minzoomVisible = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
const zoom = Number(params["zoom"] ?? params["z"] ?? 14)
|
||||
|
||||
Hash.hash.setData(undefined)
|
||||
// QueryParameters.ClearAll()
|
||||
|
||||
const state = new ThemeViewState(layout)
|
||||
state.mapProperties.location.setData({
|
||||
lat: this.options?.overrideLocation?.lat ?? Number(params["lat"] ?? 51.05016),
|
||||
lon: this.options?.overrideLocation?.lon ?? Number(params["lon"] ?? 3.717842),
|
||||
})
|
||||
state.mapProperties.zoom.setData(zoom)
|
||||
|
||||
console.log("Params are", params, params["layers"] === "none")
|
||||
|
||||
const fl = Array.from(state.layerState.filteredLayers.values())
|
||||
for (const filteredLayer of fl) {
|
||||
if (params["layer-" + filteredLayer.layerDef.id] !== undefined) {
|
||||
filteredLayer.isDisplayed.setData(
|
||||
params["layer-" + filteredLayer.layerDef.id].trim().toLowerCase() !== "false"
|
||||
)
|
||||
} else if (params["layers"] === "none") {
|
||||
filteredLayer.isDisplayed.setData(false)
|
||||
} else if (filteredLayer.layerDef.id.startsWith("note_import")) {
|
||||
filteredLayer.isDisplayed.setData(false)
|
||||
}
|
||||
}
|
||||
|
||||
for (const paramsKey in params) {
|
||||
if (paramsKey.startsWith("layer-")) {
|
||||
const layerName = paramsKey.substring("layer-".length)
|
||||
const key = params[paramsKey].toLowerCase().trim()
|
||||
const isDisplayed = key === "true" || key === "force"
|
||||
const layer = fl.find((l) => l.layerDef.id === layerName)
|
||||
console.log(
|
||||
"Setting ",
|
||||
layer?.layerDef?.id,
|
||||
" to visibility",
|
||||
isDisplayed,
|
||||
"(minzoom:",
|
||||
layer?.layerDef?.minzoomVisible,
|
||||
layer?.layerDef?.minzoom,
|
||||
")"
|
||||
)
|
||||
layer.isDisplayed.setData(isDisplayed)
|
||||
if (key === "force") {
|
||||
layer.layerDef.minzoom = 0
|
||||
layer.layerDef.minzoomVisible = 0
|
||||
layer.isDisplayed.addCallback((isDisplayed) => {
|
||||
if (!isDisplayed) {
|
||||
console.warn("Forcing layer " + paramsKey + " as true")
|
||||
layer.isDisplayed.setData(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pngCreator = new PngMapCreator(state, {
|
||||
width,
|
||||
height,
|
||||
scaling: Number(params["scaling"] ?? 1.5),
|
||||
divId: this.options.getFreeDiv(),
|
||||
dummyMode: this.options.disableMaps,
|
||||
})
|
||||
const png = await pngCreator.CreatePng("image")
|
||||
|
||||
svgImage.setAttribute("xlink:href", png)
|
||||
smallestRect.parentElement.insertBefore(svgImage, smallestRect)
|
||||
await this.prepareElement(svgImage, [])
|
||||
|
||||
const smallestRectCss = SvgToPdfInternals.parseCss(smallestRect.getAttribute("style"))
|
||||
smallestRectCss["fill-opacity"] = "0"
|
||||
smallestRect.setAttribute(
|
||||
"style",
|
||||
Object.keys(smallestRectCss)
|
||||
.map((k) => k + ":" + smallestRectCss[k])
|
||||
.join(";")
|
||||
)
|
||||
|
||||
textElement.parentElement.removeChild(textElement)
|
||||
}
|
||||
|
||||
public async PrepareLanguage(language: string) {
|
||||
// Always fetch the remote data - it's cached anyway
|
||||
this.layerTranslations[language] = await Utils.downloadJsonCached(
|
||||
|
@ -888,31 +708,219 @@ export class SvgToPdfPage {
|
|||
console.error("Could not get textFor from ", t, "for path", text)
|
||||
}
|
||||
}
|
||||
|
||||
private loadImage(element: Element): Promise<void> {
|
||||
const xlink = element.getAttribute("xlink:href")
|
||||
let img = document.createElement("img")
|
||||
|
||||
if (xlink.startsWith("data:image/svg+xml;")) {
|
||||
const base64src = xlink
|
||||
let svgXml = atob(
|
||||
base64src.substring(base64src.indexOf(";base64,") + ";base64,".length)
|
||||
)
|
||||
const parser = new DOMParser()
|
||||
const xmlDoc = parser.parseFromString(svgXml, "text/xml")
|
||||
const svgRoot = xmlDoc.getElementsByTagName("svg")[0]
|
||||
const svgWidthStr = svgRoot.getAttribute("width")
|
||||
const svgHeightStr = svgRoot.getAttribute("height")
|
||||
const svgWidth = parseFloat(svgWidthStr)
|
||||
const svgHeight = parseFloat(svgHeightStr)
|
||||
if (!svgWidthStr.endsWith("px")) {
|
||||
svgRoot.setAttribute("width", svgWidth + "px")
|
||||
}
|
||||
if (!svgHeightStr.endsWith("px")) {
|
||||
svgRoot.setAttribute("height", svgHeight + "px")
|
||||
}
|
||||
img.src = "data:image/svg+xml;base64," + btoa(svgRoot.outerHTML)
|
||||
} else {
|
||||
img.src = xlink
|
||||
}
|
||||
|
||||
this.images[xlink] = img
|
||||
return new Promise((resolve) => {
|
||||
img.onload = (_) => {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private async prepareMap(mapSpec: SVGTSpanElement): Promise<void> {
|
||||
// Upper left point of the tspan
|
||||
const { x, y } = SvgToPdfInternals.GetActualXY(mapSpec)
|
||||
|
||||
let textElement: Element = mapSpec
|
||||
// We recurse up to get the actual, full specification
|
||||
while (textElement.tagName !== "text") {
|
||||
textElement = textElement.parentElement
|
||||
}
|
||||
const spec = textElement.textContent
|
||||
const match = spec.match(/\$map\(([^)]+)\)$/)
|
||||
if (match === null) {
|
||||
throw "Invalid mapspec:" + spec
|
||||
}
|
||||
const params = SvgToPdfInternals.parseCss(match[1], ",")
|
||||
|
||||
let smallestRect: SVGRectElement = undefined
|
||||
let smallestSurface: number = undefined
|
||||
// We iterate over all the rectangles and pick the smallest (by surface area) that contains the upper left point of the tspan
|
||||
for (const id in this.rects) {
|
||||
const rect = this.rects[id]
|
||||
const rx = SvgToPdfInternals.attrNumber(rect, "x")
|
||||
const ry = SvgToPdfInternals.attrNumber(rect, "y")
|
||||
const w = SvgToPdfInternals.attrNumber(rect, "width")
|
||||
const h = SvgToPdfInternals.attrNumber(rect, "height")
|
||||
const inBounds = rx <= x && x <= rx + w && ry <= y && y <= ry + h
|
||||
if (!inBounds) {
|
||||
continue
|
||||
}
|
||||
const surface = w * h
|
||||
if (smallestSurface === undefined || smallestSurface > surface) {
|
||||
smallestSurface = surface
|
||||
smallestRect = rect
|
||||
}
|
||||
}
|
||||
|
||||
if (smallestRect === undefined) {
|
||||
throw (
|
||||
"No rectangle found around " +
|
||||
spec +
|
||||
". Draw a rectangle around it, the map will be projected on that one"
|
||||
)
|
||||
}
|
||||
|
||||
const svgImage = document.createElement("image")
|
||||
svgImage.setAttribute("x", smallestRect.getAttribute("x"))
|
||||
svgImage.setAttribute("y", smallestRect.getAttribute("y"))
|
||||
const width = SvgToPdfInternals.attrNumber(smallestRect, "width")
|
||||
const height = SvgToPdfInternals.attrNumber(smallestRect, "height")
|
||||
svgImage.setAttribute("width", "" + width)
|
||||
svgImage.setAttribute("height", "" + height)
|
||||
|
||||
let layout = AllKnownLayouts.allKnownLayouts.get(params["theme"])
|
||||
if (layout === undefined) {
|
||||
console.error("Could not show map with parameters", params)
|
||||
throw (
|
||||
"Theme not found:" + params["theme"] + ". Use theme: to define which theme to use. "
|
||||
)
|
||||
}
|
||||
layout.widenFactor = 0
|
||||
layout.overpassTimeout = 600
|
||||
layout.defaultBackgroundId = params["background"] ?? layout.defaultBackgroundId
|
||||
for (const paramsKey in params) {
|
||||
if (paramsKey.startsWith("layer-")) {
|
||||
const layerName = paramsKey.substring("layer-".length)
|
||||
const key = params[paramsKey].toLowerCase().trim()
|
||||
const layer = layout.layers.find((l) => l.id === layerName)
|
||||
if (layer === undefined) {
|
||||
throw "No layer found for " + paramsKey
|
||||
}
|
||||
if (key === "force") {
|
||||
layer.minzoom = 0
|
||||
layer.minzoomVisible = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
const zoom = Number(params["zoom"] ?? params["z"] ?? 14)
|
||||
|
||||
Hash.hash.setData(undefined)
|
||||
// QueryParameters.ClearAll()
|
||||
const state = new ThemeViewState(layout)
|
||||
state.mapProperties.location.setData({
|
||||
lat: this.options?.overrideLocation?.lat ?? Number(params["lat"] ?? 51.05016),
|
||||
lon: this.options?.overrideLocation?.lon ?? Number(params["lon"] ?? 3.717842),
|
||||
})
|
||||
state.mapProperties.zoom.setData(zoom)
|
||||
|
||||
console.log("Params are", params, params["layers"] === "none")
|
||||
|
||||
const fl = Array.from(state.layerState.filteredLayers.values())
|
||||
for (const filteredLayer of fl) {
|
||||
if (params["layer-" + filteredLayer.layerDef.id] !== undefined) {
|
||||
filteredLayer.isDisplayed.setData(
|
||||
params["layer-" + filteredLayer.layerDef.id].trim().toLowerCase() !== "false"
|
||||
)
|
||||
} else if (params["layers"] === "none") {
|
||||
filteredLayer.isDisplayed.setData(false)
|
||||
} else if (filteredLayer.layerDef.id.startsWith("note_import")) {
|
||||
filteredLayer.isDisplayed.setData(false)
|
||||
}
|
||||
}
|
||||
|
||||
for (const paramsKey in params) {
|
||||
if (paramsKey.startsWith("layer-")) {
|
||||
const layerName = paramsKey.substring("layer-".length)
|
||||
const key = params[paramsKey].toLowerCase().trim()
|
||||
const isDisplayed = key === "true" || key === "force"
|
||||
const layer = fl.find((l) => l.layerDef.id === layerName)
|
||||
console.log(
|
||||
"Setting ",
|
||||
layer?.layerDef?.id,
|
||||
" to visibility",
|
||||
isDisplayed,
|
||||
"(minzoom:",
|
||||
layer?.layerDef?.minzoomVisible,
|
||||
layer?.layerDef?.minzoom,
|
||||
")"
|
||||
)
|
||||
layer.isDisplayed.setData(isDisplayed)
|
||||
if (key === "force") {
|
||||
layer.layerDef.minzoom = 0
|
||||
layer.layerDef.minzoomVisible = 0
|
||||
layer.isDisplayed.addCallback((isDisplayed) => {
|
||||
if (!isDisplayed) {
|
||||
console.warn("Forcing layer " + paramsKey + " as true")
|
||||
layer.isDisplayed.setData(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("Creating a map width ", width, height, params.scalingFactor)
|
||||
const pngCreator = new PngMapCreator(state, {
|
||||
width: width * 4,
|
||||
height: height * 4,
|
||||
})
|
||||
const png = await pngCreator.CreatePng(this._state)
|
||||
svgImage.setAttribute("xlink:href", await SvgToPdfPage.blobToBase64(png))
|
||||
smallestRect.parentElement.insertBefore(svgImage, smallestRect)
|
||||
await this.prepareElement(svgImage, [])
|
||||
const smallestRectCss = SvgToPdfInternals.parseCss(smallestRect.getAttribute("style"))
|
||||
smallestRectCss["fill-opacity"] = "0"
|
||||
smallestRect.setAttribute(
|
||||
"style",
|
||||
Object.keys(smallestRectCss)
|
||||
.map((k) => k + ":" + smallestRectCss[k])
|
||||
.join(";")
|
||||
)
|
||||
|
||||
textElement.parentElement.removeChild(textElement)
|
||||
}
|
||||
}
|
||||
|
||||
export class SvgToPdf {
|
||||
public static readonly templates: Record<
|
||||
string,
|
||||
"flyer_a4" | "poster_a3" | "poster_a2",
|
||||
{ pages: string[]; description: string | Translation }
|
||||
> = {
|
||||
flyer_a4: {
|
||||
pages: [
|
||||
"/assets/templates/MapComplete-flyer.svg",
|
||||
"/assets/templates/MapComplete-flyer.back.svg",
|
||||
"./assets/templates/MapComplete-flyer.svg",
|
||||
"./assets/templates/MapComplete-flyer.back.svg",
|
||||
],
|
||||
description: Translations.t.flyer.description,
|
||||
},
|
||||
poster_a3: {
|
||||
pages: ["/assets/templates/MapComplete-poster-a3.svg"],
|
||||
pages: ["./assets/templates/MapComplete-poster-a3.svg"],
|
||||
description: "A basic A3 poster (similar to the flyer)",
|
||||
},
|
||||
poster_a2: {
|
||||
pages: ["/assets/templates/MapComplete-poster-a2.svg"],
|
||||
pages: ["./assets/templates/MapComplete-poster-a2.svg"],
|
||||
description: "A basic A2 poster (similar to the flyer); scaled up from the A3 poster",
|
||||
},
|
||||
}
|
||||
public readonly status: Store<string>
|
||||
public readonly _status: UIEventSource<string>
|
||||
private readonly _title: string
|
||||
|
||||
private readonly _pages: SvgToPdfPage[]
|
||||
|
||||
constructor(title: string, pages: string[], options?: SvgToPdfOptions) {
|
||||
|
@ -926,24 +934,34 @@ export class SvgToPdf {
|
|||
).length
|
||||
options.textSubstitutions["mapCount"] = mapCount
|
||||
|
||||
this._pages = pages.map((page) => new SvgToPdfPage(page, options))
|
||||
const state = new UIEventSource<string>("Initializing...")
|
||||
this.status = state
|
||||
this._status = state
|
||||
this._pages = pages.map((page) => new SvgToPdfPage(page, state, options))
|
||||
}
|
||||
|
||||
public async ConvertSvg(language: string): Promise<void> {
|
||||
console.log("Building svg...")
|
||||
const firstPage = this._pages[0]._svgRoot
|
||||
const width = SvgToPdfInternals.attrNumber(firstPage, "width")
|
||||
const height = SvgToPdfInternals.attrNumber(firstPage, "height")
|
||||
const mode = width > height ? "landscape" : "portrait"
|
||||
|
||||
await this.Prepare()
|
||||
console.log("Global prepare done")
|
||||
for (const page of this._pages) {
|
||||
await page.Prepare()
|
||||
await page.PrepareLanguage(language)
|
||||
}
|
||||
|
||||
this._status.setData("Maps are rendered, building pdf")
|
||||
new FixedUiElement("").AttachTo("extradiv")
|
||||
console.log("Pages are prepared")
|
||||
|
||||
const doc = new jsPDF(mode, undefined, [width, height])
|
||||
doc.advancedAPI((advancedApi) => {
|
||||
for (let i = 0; i < this._pages.length; i++) {
|
||||
console.log("Rendering page", i)
|
||||
if (i > 0) {
|
||||
const page = this._pages[i]._svgRoot
|
||||
const width = SvgToPdfInternals.attrNumber(page, "width")
|
||||
|
@ -967,6 +985,7 @@ export class SvgToPdf {
|
|||
this._pages[i].drawPage(advancedApi, i, language)
|
||||
}
|
||||
})
|
||||
console.log("Exporting...")
|
||||
await doc.save(this._title + "." + language + ".pdf")
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
--popup-border: white;
|
||||
--shadow-color: #00000066;
|
||||
--variable-title-height: 0px; /* Set by javascript */
|
||||
--return-to-the-map-height: 2em;
|
||||
|
||||
--image-carousel-height: 350px;
|
||||
}
|
||||
|
|
|
@ -1407,10 +1407,6 @@ video {
|
|||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.border-b-2 {
|
||||
border-bottom-width: 2px;
|
||||
}
|
||||
|
||||
.border-solid {
|
||||
border-style: solid;
|
||||
}
|
||||
|
@ -1588,10 +1584,6 @@ video {
|
|||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.pb-8 {
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.pl-5 {
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
@ -1852,8 +1844,8 @@ video {
|
|||
|
||||
:root {
|
||||
/* The main colour scheme of mapcomplete is configured here.
|
||||
* For a custom styling, set 'customCss' in your layoutConfig and overwrite some of these.
|
||||
*/
|
||||
* For a custom styling, set 'customCss' in your layoutConfig and overwrite some of these.
|
||||
*/
|
||||
/* Main color of the application: the background and text colours */
|
||||
--background-color: white;
|
||||
/* Main text colour. Also styles some elements, such as the 'close popup'-button or 'back-arrow' (in mobile) */
|
||||
|
@ -1861,23 +1853,22 @@ video {
|
|||
/* A colour to indicate an error or warning */
|
||||
--alert-color: #fee4d1;
|
||||
/**
|
||||
* Base colour of interactive elements, mainly the 'subtle button'
|
||||
*
|
||||
*/
|
||||
* Base colour of interactive elements, mainly the 'subtle button'
|
||||
*
|
||||
*/
|
||||
--subtle-detail-color: #dbeafe;
|
||||
--subtle-detail-color-contrast: black;
|
||||
--subtle-detail-color-light-contrast: lightgrey;
|
||||
/**
|
||||
* A stronger variant of the 'subtle-detail-colour'
|
||||
* Used as subtle button hover
|
||||
*/
|
||||
* A stronger variant of the 'subtle-detail-colour'
|
||||
* Used as subtle button hover
|
||||
*/
|
||||
--unsubtle-detail-color: #bfdbfe;
|
||||
--unsubtle-detail-color-contrast: black;
|
||||
--catch-detail-color: #3a3aeb;
|
||||
--catch-detail-color-contrast: white;
|
||||
--non-active-tab-svg: var(--foreground-color);
|
||||
--shadow-color: #00000066;
|
||||
--return-to-the-map-height: 2em;
|
||||
--image-carousel-height: 350px;
|
||||
/* Technical variable to make some dynamic behaviour possible; set by javascript. */
|
||||
--variable-title-height: 0px;
|
||||
|
|
520
index.css
520
index.css
|
@ -12,486 +12,484 @@
|
|||
@tailwind utilities;
|
||||
|
||||
@layer utilities {
|
||||
.z-above-map {
|
||||
z-index: 10000;
|
||||
}
|
||||
.z-above-map {
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.z-above-controls {
|
||||
z-index: 10001;
|
||||
}
|
||||
.z-above-controls {
|
||||
z-index: 10001;
|
||||
}
|
||||
|
||||
.w-160 {
|
||||
width: 40rem;
|
||||
}
|
||||
.w-160 {
|
||||
width: 40rem;
|
||||
}
|
||||
|
||||
.bg-subtle {
|
||||
background-color: var(--subtle-detail-color);
|
||||
color: var(--subtle-detail-color-contrast);
|
||||
}
|
||||
.bg-subtle {
|
||||
background-color: var(--subtle-detail-color);
|
||||
color: var(--subtle-detail-color-contrast);
|
||||
}
|
||||
|
||||
.bg-unsubtle {
|
||||
background-color: var(--unsubtle-detail-color);
|
||||
color: var(--unsubtle-detail-color-contrast);
|
||||
}
|
||||
.bg-unsubtle {
|
||||
background-color: var(--unsubtle-detail-color);
|
||||
color: var(--unsubtle-detail-color-contrast);
|
||||
}
|
||||
|
||||
.bg-catch {
|
||||
background-color: var(--catch-detail-color);
|
||||
color: var(--catch-detail-color-contrast);
|
||||
}
|
||||
.bg-catch {
|
||||
background-color: var(--catch-detail-color);
|
||||
color: var(--catch-detail-color-contrast);
|
||||
}
|
||||
|
||||
.rounded-left-full {
|
||||
border-bottom-left-radius: 999rem;
|
||||
border-top-left-radius: 999rem;
|
||||
}
|
||||
.rounded-left-full {
|
||||
border-bottom-left-radius: 999rem;
|
||||
border-top-left-radius: 999rem;
|
||||
}
|
||||
|
||||
.rounded-right-full {
|
||||
border-bottom-right-radius: 999rem;
|
||||
border-top-right-radius: 999rem;
|
||||
}
|
||||
.rounded-right-full {
|
||||
border-bottom-right-radius: 999rem;
|
||||
border-top-right-radius: 999rem;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
/* The main colour scheme of mapcomplete is configured here.
|
||||
* For a custom styling, set 'customCss' in your layoutConfig and overwrite some of these.
|
||||
*/
|
||||
/* The main colour scheme of mapcomplete is configured here.
|
||||
* For a custom styling, set 'customCss' in your layoutConfig and overwrite some of these.
|
||||
*/
|
||||
|
||||
/* Main color of the application: the background and text colours */
|
||||
--background-color: white;
|
||||
/* Main text colour. Also styles some elements, such as the 'close popup'-button or 'back-arrow' (in mobile) */
|
||||
--foreground-color: black;
|
||||
/* Main color of the application: the background and text colours */
|
||||
--background-color: white;
|
||||
/* Main text colour. Also styles some elements, such as the 'close popup'-button or 'back-arrow' (in mobile) */
|
||||
--foreground-color: black;
|
||||
|
||||
/* A colour to indicate an error or warning */
|
||||
--alert-color: #fee4d1;
|
||||
/* A colour to indicate an error or warning */
|
||||
--alert-color: #fee4d1;
|
||||
|
||||
/**
|
||||
* Base colour of interactive elements, mainly the 'subtle button'
|
||||
*
|
||||
*/
|
||||
--subtle-detail-color: #dbeafe;
|
||||
--subtle-detail-color-contrast: black;
|
||||
--subtle-detail-color-light-contrast: lightgrey;
|
||||
/**
|
||||
* Base colour of interactive elements, mainly the 'subtle button'
|
||||
*
|
||||
*/
|
||||
--subtle-detail-color: #dbeafe;
|
||||
--subtle-detail-color-contrast: black;
|
||||
--subtle-detail-color-light-contrast: lightgrey;
|
||||
|
||||
/**
|
||||
* A stronger variant of the 'subtle-detail-colour'
|
||||
* Used as subtle button hover
|
||||
*/
|
||||
--unsubtle-detail-color: #bfdbfe;
|
||||
--unsubtle-detail-color-contrast: black;
|
||||
/**
|
||||
* A stronger variant of the 'subtle-detail-colour'
|
||||
* Used as subtle button hover
|
||||
*/
|
||||
--unsubtle-detail-color: #bfdbfe;
|
||||
--unsubtle-detail-color-contrast: black;
|
||||
|
||||
--catch-detail-color: #3a3aeb;
|
||||
--catch-detail-color-contrast: white;
|
||||
--catch-detail-color: #3a3aeb;
|
||||
--catch-detail-color-contrast: white;
|
||||
|
||||
--non-active-tab-svg: var(--foreground-color);
|
||||
--shadow-color: #00000066;
|
||||
--non-active-tab-svg: var(--foreground-color);
|
||||
--shadow-color: #00000066;
|
||||
|
||||
--return-to-the-map-height: 2em;
|
||||
--image-carousel-height: 350px;
|
||||
--image-carousel-height: 350px;
|
||||
|
||||
/* Technical variable to make some dynamic behaviour possible; set by javascript. */
|
||||
--variable-title-height: 0px;
|
||||
/* Technical variable to make some dynamic behaviour possible; set by javascript. */
|
||||
--variable-title-height: 0px;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
min-height: -webkit-fill-available;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: var(--background-color);
|
||||
color: var(--foreground-color);
|
||||
font-family: "Helvetica Neue", Arial, sans-serif;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
min-height: -webkit-fill-available;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: var(--background-color);
|
||||
color: var(--foreground-color);
|
||||
font-family: "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
svg,
|
||||
img {
|
||||
box-sizing: content-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: content-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.no-images img {
|
||||
/* Used solely in 'imageAttribution' */
|
||||
display: none;
|
||||
/* Used solely in 'imageAttribution' */
|
||||
display: none;
|
||||
}
|
||||
|
||||
.text-white a {
|
||||
/* Used solely in 'imageAttribution' */
|
||||
color: var(--background-color);
|
||||
/* Used solely in 'imageAttribution' */
|
||||
color: var(--background-color);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.weblate-link {
|
||||
/* Weblate-links are the little translation icon next to translatable sentences. Due to their special nature, they are exempt from some rules */
|
||||
/* Weblate-links are the little translation icon next to translatable sentences. Due to their special nature, they are exempt from some rules */
|
||||
}
|
||||
|
||||
|
||||
a {
|
||||
color: var(--foreground-color);
|
||||
color: var(--foreground-color);
|
||||
}
|
||||
|
||||
|
||||
.btn {
|
||||
line-height: 1.25rem;
|
||||
--tw-text-opacity: 1;
|
||||
color: var(--catch-detail-color-contrast);
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: var(--catch-detail-color);
|
||||
display: inline-flex;
|
||||
border-radius: 1.5rem;
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
padding-left: 1.25rem;
|
||||
padding-right: 1.25rem;
|
||||
font-size: large;
|
||||
font-weight: bold;
|
||||
transition: 100ms;
|
||||
/*-- invisible border: rendered on hover*/
|
||||
border: 3px solid var(--unsubtle-detail-color);
|
||||
line-height: 1.25rem;
|
||||
--tw-text-opacity: 1;
|
||||
color: var(--catch-detail-color-contrast);
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: var(--catch-detail-color);
|
||||
display: inline-flex;
|
||||
border-radius: 1.5rem;
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
padding-left: 1.25rem;
|
||||
padding-right: 1.25rem;
|
||||
font-size: large;
|
||||
font-weight: bold;
|
||||
transition: 100ms;
|
||||
/*-- invisible border: rendered on hover*/
|
||||
border: 3px solid var(--unsubtle-detail-color);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
border: 3px solid var(--catch-detail-color);
|
||||
border: 3px solid var(--catch-detail-color);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--catch-detail-color);
|
||||
filter: saturate(0.5);
|
||||
background-color: var(--catch-detail-color);
|
||||
filter: saturate(0.5);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: var(--catch-detail-color);
|
||||
filter: unset;
|
||||
background-color: var(--catch-detail-color);
|
||||
filter: unset;
|
||||
}
|
||||
|
||||
.btn-disabled {
|
||||
filter: saturate(0.3);
|
||||
cursor: default;
|
||||
filter: saturate(0.3);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.btn-disabled:hover {
|
||||
border: 3px solid var(--unsubtle-detail-color);
|
||||
border: 3px solid var(--unsubtle-detail-color);
|
||||
}
|
||||
|
||||
/* slider */
|
||||
input[type="range"].vertical {
|
||||
writing-mode: bt-lr; /* IE */
|
||||
-webkit-appearance: slider-vertical; /* Chromium */
|
||||
cursor: pointer;
|
||||
writing-mode: bt-lr; /* IE */
|
||||
-webkit-appearance: slider-vertical; /* Chromium */
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
input[type="range"].elevator::-moz-range-thumb {
|
||||
background-color: #00000000 !important;
|
||||
background-image: url("/assets/svg/elevator_wheelchair.svg");
|
||||
width: 150px !important;
|
||||
height: 30px !important;
|
||||
border: 2px;
|
||||
border-style: solid;
|
||||
background-size: contain;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
border-image: linear-gradient(to right, black 50%, transparent 50%) 100% 1;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
input[type="range"].elevator::-moz-range-thumb {
|
||||
background-color: #00000000 !important;
|
||||
background-image: url("/assets/svg/elevator_wheelchair.svg");
|
||||
width: 150px !important;
|
||||
height: 30px !important;
|
||||
border: 2px;
|
||||
border-style: solid;
|
||||
background-size: contain;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
border-image: linear-gradient(to right, black 50%, transparent 50%) 100% 1;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.rounded-left-full {
|
||||
border-bottom-left-radius: 999rem;
|
||||
border-top-left-radius: 999rem;
|
||||
border-bottom-left-radius: 999rem;
|
||||
border-top-left-radius: 999rem;
|
||||
}
|
||||
|
||||
.rounded-right-full {
|
||||
border-bottom-right-radius: 999rem;
|
||||
border-top-right-radius: 999rem;
|
||||
border-bottom-right-radius: 999rem;
|
||||
border-top-right-radius: 999rem;
|
||||
}
|
||||
|
||||
.link-underline a {
|
||||
text-decoration: underline 1px var(--foreground-color);
|
||||
text-decoration: underline 1px var(--foreground-color);
|
||||
}
|
||||
|
||||
a.link-underline {
|
||||
text-decoration: underline 1px var(--foreground-color);
|
||||
text-decoration: underline 1px var(--foreground-color);
|
||||
}
|
||||
|
||||
.link-no-underline a {
|
||||
text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-left: 0.5em;
|
||||
padding-left: 0.2em;
|
||||
margin-top: 0.1em;
|
||||
margin-left: 0.5em;
|
||||
padding-left: 0.2em;
|
||||
margin-top: 0.1em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: large;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.3em;
|
||||
font-weight: bold;
|
||||
font-size: large;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.3em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: larger;
|
||||
margin-top: 0.6em;
|
||||
margin-bottom: 0;
|
||||
font-weight: bold;
|
||||
font-size: larger;
|
||||
margin-top: 0.6em;
|
||||
margin-bottom: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: larger;
|
||||
margin-top: 0.6em;
|
||||
margin-bottom: 0;
|
||||
font-weight: bolder;
|
||||
font-size: larger;
|
||||
margin-top: 0.6em;
|
||||
margin-bottom: 0;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
p {
|
||||
padding-top: 0.1em;
|
||||
padding-top: 0.1em;
|
||||
}
|
||||
|
||||
li::marker {
|
||||
content: "•";
|
||||
content: "•";
|
||||
}
|
||||
|
||||
.subtle-background {
|
||||
background: var(--subtle-detail-color);
|
||||
color: var(--subtle-detail-color-contrast);
|
||||
background: var(--subtle-detail-color);
|
||||
color: var(--subtle-detail-color-contrast);
|
||||
}
|
||||
|
||||
.normal-background {
|
||||
background: var(--background-color);
|
||||
color: var(--foreground-color);
|
||||
background: var(--background-color);
|
||||
color: var(--foreground-color);
|
||||
}
|
||||
|
||||
.subtle-lighter {
|
||||
color: var(--subtle-detail-color-light-contrast);
|
||||
color: var(--subtle-detail-color-light-contrast);
|
||||
}
|
||||
|
||||
.border-attention-catch {
|
||||
border: 5px solid var(--catch-detail-color);
|
||||
border: 5px solid var(--catch-detail-color);
|
||||
}
|
||||
|
||||
.border-attention {
|
||||
border-color: var(--catch-detail-color);
|
||||
border-color: var(--catch-detail-color);
|
||||
}
|
||||
|
||||
.direction-svg svg path {
|
||||
fill: var(--catch-detail-color) !important;
|
||||
fill: var(--catch-detail-color) !important;
|
||||
}
|
||||
|
||||
.block-ruby {
|
||||
display: block ruby;
|
||||
display: block ruby;
|
||||
}
|
||||
|
||||
.disable-links a {
|
||||
pointer-events: none;
|
||||
text-decoration: none !important;
|
||||
color: var(--subtle-detail-color-contrast) !important;
|
||||
pointer-events: none;
|
||||
text-decoration: none !important;
|
||||
color: var(--subtle-detail-color-contrast) !important;
|
||||
}
|
||||
|
||||
.enable-links a {
|
||||
pointer-events: unset;
|
||||
text-decoration: underline !important;
|
||||
color: unset !important;
|
||||
pointer-events: unset;
|
||||
text-decoration: underline !important;
|
||||
color: unset !important;
|
||||
}
|
||||
|
||||
.disable-links a.must-link,
|
||||
.disable-links .must-link a {
|
||||
/* Hide links if they are disabled */
|
||||
display: none;
|
||||
/* Hide links if they are disabled */
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selected svg:not(.noselect *) path.selectable {
|
||||
stroke: white !important;
|
||||
stroke-width: 20px !important;
|
||||
overflow: visible !important;
|
||||
-webkit-animation: glowing-drop-shadow 1s ease-in-out infinite alternate;
|
||||
-moz-animation: glowing-drop-shadow 1s ease-in-out infinite alternate;
|
||||
animation: glowing-drop-shadow 1s ease-in-out infinite alternate;
|
||||
stroke: white !important;
|
||||
stroke-width: 20px !important;
|
||||
overflow: visible !important;
|
||||
-webkit-animation: glowing-drop-shadow 1s ease-in-out infinite alternate;
|
||||
-moz-animation: glowing-drop-shadow 1s ease-in-out infinite alternate;
|
||||
animation: glowing-drop-shadow 1s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.selected svg {
|
||||
overflow: visible !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
|
||||
@-webkit-keyframes glowing-drop-shadow {
|
||||
from {
|
||||
filter: drop-shadow(5px 5px 60px rgb(128 128 128 / 0.6));
|
||||
}
|
||||
to {
|
||||
filter: drop-shadow(5px 5px 80px rgb(0.5 0.5 0.5 / 0.8));
|
||||
}
|
||||
from {
|
||||
filter: drop-shadow(5px 5px 60px rgb(128 128 128 / 0.6));
|
||||
}
|
||||
to {
|
||||
filter: drop-shadow(5px 5px 80px rgb(0.5 0.5 0.5 / 0.8));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**************** GENERIC ****************/
|
||||
|
||||
.alert {
|
||||
background-color: var(--alert-color);
|
||||
color: var(--foreground-color);
|
||||
font-weight: bold;
|
||||
border-radius: 1em;
|
||||
margin: 0.25em;
|
||||
text-align: center;
|
||||
padding: 0.15em 0.3em;
|
||||
background-color: var(--alert-color);
|
||||
color: var(--foreground-color);
|
||||
font-weight: bold;
|
||||
border-radius: 1em;
|
||||
margin: 0.25em;
|
||||
text-align: center;
|
||||
padding: 0.15em 0.3em;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
box-shadow: 0 0 10px #ff5353;
|
||||
height: min-content;
|
||||
box-shadow: 0 0 10px #ff5353;
|
||||
height: min-content;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: 0 0 10px var(--shadow-color);
|
||||
box-shadow: 0 0 10px var(--shadow-color);
|
||||
}
|
||||
|
||||
.title-font span {
|
||||
font-size: xx-large !important;
|
||||
font-weight: bold;
|
||||
font-size: xx-large !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.soft {
|
||||
background-color: var(--subtle-detail-color);
|
||||
color: var(--subtle-detail-color-contrast);
|
||||
font-weight: bold;
|
||||
border-radius: 1em;
|
||||
margin: 0.25em;
|
||||
text-align: center;
|
||||
padding: 0.15em 0.3em;
|
||||
background-color: var(--subtle-detail-color);
|
||||
color: var(--subtle-detail-color-contrast);
|
||||
font-weight: bold;
|
||||
border-radius: 1em;
|
||||
margin: 0.25em;
|
||||
text-align: center;
|
||||
padding: 0.15em 0.3em;
|
||||
}
|
||||
|
||||
.subtle {
|
||||
color: #999;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.link-underline .subtle a {
|
||||
text-decoration: underline 1px #7193bb88;
|
||||
color: #7193bb;
|
||||
text-decoration: underline 1px #7193bb88;
|
||||
color: #7193bb;
|
||||
}
|
||||
|
||||
.thanks {
|
||||
background-color: #43d904;
|
||||
font-weight: bold;
|
||||
border-radius: 1em;
|
||||
margin: 0.25em;
|
||||
text-align: center;
|
||||
padding: 0.15em 0.3em;
|
||||
background-color: #43d904;
|
||||
font-weight: bold;
|
||||
border-radius: 1em;
|
||||
margin: 0.25em;
|
||||
text-align: center;
|
||||
padding: 0.15em 0.3em;
|
||||
}
|
||||
|
||||
@keyframes slide {
|
||||
/* This is the animation on the marker to add a new point - it slides through all the possible presets */
|
||||
from {
|
||||
transform: translateX(0%);
|
||||
}
|
||||
/* This is the animation on the marker to add a new point - it slides through all the possible presets */
|
||||
from {
|
||||
transform: translateX(0%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(calc(-100% + 42px));
|
||||
}
|
||||
to {
|
||||
transform: translateX(calc(-100% + 42px));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************** Info box (box containing features and questions ******************/
|
||||
|
||||
input {
|
||||
color: var(--foreground-color);
|
||||
color: var(--foreground-color);
|
||||
}
|
||||
|
||||
.literal-code {
|
||||
display: inline-block;
|
||||
background-color: lightgray;
|
||||
padding: 0.5em;
|
||||
word-break: break-word;
|
||||
color: black;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
background-color: lightgray;
|
||||
padding: 0.5em;
|
||||
word-break: break-word;
|
||||
color: black;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/** Switch layout **/
|
||||
.small-image img {
|
||||
height: 1em;
|
||||
max-width: 1em;
|
||||
height: 1em;
|
||||
max-width: 1em;
|
||||
}
|
||||
|
||||
.small-image {
|
||||
height: 1em;
|
||||
max-width: 1em;
|
||||
height: 1em;
|
||||
max-width: 1em;
|
||||
}
|
||||
|
||||
.slideshow-item img {
|
||||
height: var(--image-carousel-height);
|
||||
width: unset;
|
||||
height: var(--image-carousel-height);
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.animate-height {
|
||||
transition: max-height 0.5s ease-in-out;
|
||||
overflow-y: hidden;
|
||||
transition: max-height 0.5s ease-in-out;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.zebra-table tr:nth-child(even) {
|
||||
background-color: #f2f2f2;
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
|
||||
.glowing-shadow {
|
||||
-webkit-animation: glowing 1s ease-in-out infinite alternate;
|
||||
-moz-animation: glowing 1s ease-in-out infinite alternate;
|
||||
animation: glowing 1s ease-in-out infinite alternate;
|
||||
-webkit-animation: glowing 1s ease-in-out infinite alternate;
|
||||
-moz-animation: glowing 1s ease-in-out infinite alternate;
|
||||
animation: glowing 1s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@-webkit-keyframes glowing {
|
||||
from {
|
||||
box-shadow: 0 0 20px 10px #eaaf2588, inset 0 0 0px 1px #eaaf25;
|
||||
}
|
||||
to {
|
||||
box-shadow: 0 0 20px 20px #eaaf2588, inset 0 0 5px 1px #eaaf25;
|
||||
}
|
||||
from {
|
||||
box-shadow: 0 0 20px 10px #eaaf2588, inset 0 0 0px 1px #eaaf25;
|
||||
}
|
||||
to {
|
||||
box-shadow: 0 0 20px 20px #eaaf2588, inset 0 0 5px 1px #eaaf25;
|
||||
}
|
||||
}
|
||||
|
||||
.mapping-icon-small-height {
|
||||
/* A mapping icon type */
|
||||
height: 1.5rem;
|
||||
margin-right: 0.5rem;
|
||||
width: unset;
|
||||
/* A mapping icon type */
|
||||
height: 1.5rem;
|
||||
margin-right: 0.5rem;
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.mapping-icon-medium-height {
|
||||
/* A mapping icon type */
|
||||
height: 3rem;
|
||||
margin-right: 0.5rem;
|
||||
width: unset;
|
||||
/* A mapping icon type */
|
||||
height: 3rem;
|
||||
margin-right: 0.5rem;
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.mapping-icon-large-height {
|
||||
/* A mapping icon type */
|
||||
height: 5rem;
|
||||
margin-right: 0.5rem;
|
||||
width: unset;
|
||||
/* A mapping icon type */
|
||||
height: 5rem;
|
||||
margin-right: 0.5rem;
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.mapping-icon-small {
|
||||
/* A mapping icon type */
|
||||
width: 1.5rem;
|
||||
max-height: 1.5rem;
|
||||
margin-right: 0.5rem;
|
||||
/* A mapping icon type */
|
||||
width: 1.5rem;
|
||||
max-height: 1.5rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.mapping-icon-medium {
|
||||
/* A mapping icon type */
|
||||
width: 3rem;
|
||||
max-height: 3rem;
|
||||
margin-right: 1rem;
|
||||
margin-left: 1rem;
|
||||
/* A mapping icon type */
|
||||
width: 3rem;
|
||||
max-height: 3rem;
|
||||
margin-right: 1rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.mapping-icon-large {
|
||||
/* A mapping icon type */
|
||||
width: 6rem;
|
||||
max-height: 5rem;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 1.5rem;
|
||||
margin-left: 1.5rem;
|
||||
/* A mapping icon type */
|
||||
width: 6rem;
|
||||
max-height: 5rem;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 1.5rem;
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
|
120
package-lock.json
generated
120
package-lock.json
generated
|
@ -49,7 +49,6 @@
|
|||
"showdown": "^2.1.0",
|
||||
"svg-path-parser": "^1.1.0",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"togpx": "^0.5.4",
|
||||
"vite-node": "^0.28.3",
|
||||
"vitest": "^0.28.3",
|
||||
"wikibase-sdk": "^7.14.0",
|
||||
|
@ -4301,23 +4300,6 @@
|
|||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/bops": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/bops/-/bops-0.0.6.tgz",
|
||||
"integrity": "sha512-EWD8/Ei9o/h/wmR3w/YL/8dGKe4rSFHlaO8VNNcuXnjXjeTgxdcmhjPf9hRCYlqTrBPZbKaht+FxZKahcob5UQ==",
|
||||
"dependencies": {
|
||||
"base64-js": "0.0.2",
|
||||
"to-utf8": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bops/node_modules/base64-js": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.2.tgz",
|
||||
"integrity": "sha512-Pj9L87dCdGcKlSqPVUjD+q96pbIx1zQQLb2CUiWURfjiBELv84YX+0nGnKmyT/9KkC7PQk7UN1w+Al8bBozaxQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
|
@ -7352,14 +7334,6 @@
|
|||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jxon": {
|
||||
"version": "2.0.0-beta.5",
|
||||
"resolved": "https://registry.npmjs.org/jxon/-/jxon-2.0.0-beta.5.tgz",
|
||||
"integrity": "sha512-Ot7muZ0v2cmgQ1k+e6bpNcz6E3q2zHssvzYubbKTk5nIEvBLqJfiS6/uivU2ujqKZQlORcjKqcyx6D9X6BEAkQ==",
|
||||
"dependencies": {
|
||||
"xmldom": "^0.1.21"
|
||||
}
|
||||
},
|
||||
"node_modules/kdbush": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-1.0.1.tgz",
|
||||
|
@ -10265,36 +10239,6 @@
|
|||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/to-utf8": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-utf8/-/to-utf8-0.0.1.tgz",
|
||||
"integrity": "sha512-zks18/TWT1iHO3v0vFp5qLKOG27m67ycq/Y7a7cTiRuUNlc4gf3HGnkRgMv0NyhnfTamtkYBJl+YeD1/j07gBQ=="
|
||||
},
|
||||
"node_modules/togpx": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/togpx/-/togpx-0.5.4.tgz",
|
||||
"integrity": "sha512-1LY9ZjBrCYbcWfD63ZaqRw53U0tkigGC9fOdhRaTZH6yrmhlQGbbsEVTyCFzagvbPO36sZuBz0SrEQ+paaCPiw==",
|
||||
"dependencies": {
|
||||
"concat-stream": "~1.0.1",
|
||||
"jxon": "~2.0.0-beta.5",
|
||||
"optimist": "~0.3.5",
|
||||
"xmldom": "~0.1.17"
|
||||
},
|
||||
"bin": {
|
||||
"togpx": "togpx"
|
||||
}
|
||||
},
|
||||
"node_modules/togpx/node_modules/concat-stream": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.0.1.tgz",
|
||||
"integrity": "sha512-nAHFsgeRVVvZ+aB3S1gLeN73fQ+tdOcw075BHbXMbC6MY0h6nqAkEeqPVCw8kRuDJJZDvaUjxI4jZv2FD0Tl8A==",
|
||||
"engines": [
|
||||
"node >= 0.8.0"
|
||||
],
|
||||
"dependencies": {
|
||||
"bops": "0.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/topojson-client": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz",
|
||||
|
@ -12055,15 +11999,6 @@
|
|||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/xmldom": {
|
||||
"version": "0.1.31",
|
||||
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz",
|
||||
"integrity": "sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==",
|
||||
"deprecated": "Deprecated due to CVE-2021-21366 resolved in 0.5.0",
|
||||
"engines": {
|
||||
"node": ">=0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
@ -15350,22 +15285,6 @@
|
|||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||
"dev": true
|
||||
},
|
||||
"bops": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/bops/-/bops-0.0.6.tgz",
|
||||
"integrity": "sha512-EWD8/Ei9o/h/wmR3w/YL/8dGKe4rSFHlaO8VNNcuXnjXjeTgxdcmhjPf9hRCYlqTrBPZbKaht+FxZKahcob5UQ==",
|
||||
"requires": {
|
||||
"base64-js": "0.0.2",
|
||||
"to-utf8": "0.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"base64-js": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.2.tgz",
|
||||
"integrity": "sha512-Pj9L87dCdGcKlSqPVUjD+q96pbIx1zQQLb2CUiWURfjiBELv84YX+0nGnKmyT/9KkC7PQk7UN1w+Al8bBozaxQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
|
@ -17635,14 +17554,6 @@
|
|||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"jxon": {
|
||||
"version": "2.0.0-beta.5",
|
||||
"resolved": "https://registry.npmjs.org/jxon/-/jxon-2.0.0-beta.5.tgz",
|
||||
"integrity": "sha512-Ot7muZ0v2cmgQ1k+e6bpNcz6E3q2zHssvzYubbKTk5nIEvBLqJfiS6/uivU2ujqKZQlORcjKqcyx6D9X6BEAkQ==",
|
||||
"requires": {
|
||||
"xmldom": "^0.1.21"
|
||||
}
|
||||
},
|
||||
"kdbush": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-1.0.1.tgz",
|
||||
|
@ -19802,32 +19713,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"to-utf8": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-utf8/-/to-utf8-0.0.1.tgz",
|
||||
"integrity": "sha512-zks18/TWT1iHO3v0vFp5qLKOG27m67ycq/Y7a7cTiRuUNlc4gf3HGnkRgMv0NyhnfTamtkYBJl+YeD1/j07gBQ=="
|
||||
},
|
||||
"togpx": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/togpx/-/togpx-0.5.4.tgz",
|
||||
"integrity": "sha512-1LY9ZjBrCYbcWfD63ZaqRw53U0tkigGC9fOdhRaTZH6yrmhlQGbbsEVTyCFzagvbPO36sZuBz0SrEQ+paaCPiw==",
|
||||
"requires": {
|
||||
"concat-stream": "~1.0.1",
|
||||
"jxon": "~2.0.0-beta.5",
|
||||
"optimist": "~0.3.5",
|
||||
"xmldom": "~0.1.17"
|
||||
},
|
||||
"dependencies": {
|
||||
"concat-stream": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.0.1.tgz",
|
||||
"integrity": "sha512-nAHFsgeRVVvZ+aB3S1gLeN73fQ+tdOcw075BHbXMbC6MY0h6nqAkEeqPVCw8kRuDJJZDvaUjxI4jZv2FD0Tl8A==",
|
||||
"requires": {
|
||||
"bops": "0.0.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"topojson-client": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz",
|
||||
|
@ -21167,11 +21052,6 @@
|
|||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"xmldom": {
|
||||
"version": "0.1.31",
|
||||
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz",
|
||||
"integrity": "sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ=="
|
||||
},
|
||||
"xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
|
15
test.ts
15
test.ts
|
@ -5,12 +5,14 @@ import Combine from "./UI/Base/Combine"
|
|||
import SpecialVisualizations from "./UI/SpecialVisualizations"
|
||||
import InputHelpers from "./UI/InputElement/InputHelpers"
|
||||
import BaseUIElement from "./UI/BaseUIElement"
|
||||
import { ImmutableStore, UIEventSource } from "./Logic/UIEventSource"
|
||||
import { UIEventSource } from "./Logic/UIEventSource"
|
||||
import { VariableUiElement } from "./UI/Base/VariableUIElement"
|
||||
import { FixedUiElement } from "./UI/Base/FixedUiElement"
|
||||
import Title from "./UI/Base/Title"
|
||||
import SvelteUIElement from "./UI/Base/SvelteUIElement"
|
||||
import ValidatedInput from "./UI/InputElement/ValidatedInput.svelte"
|
||||
import { SvgToPdf } from "./Utils/svgToPdf"
|
||||
import { Utils } from "./Utils"
|
||||
|
||||
function testspecial() {
|
||||
const layout = new LayoutConfig(<any>theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
|
||||
|
@ -47,6 +49,17 @@ function testinput() {
|
|||
new Combine(els).SetClass("flex flex-col").AttachTo("maindiv")
|
||||
}
|
||||
|
||||
async function testPdf() {
|
||||
const svgs = await Promise.all(
|
||||
SvgToPdf.templates["flyer_a4"].pages.map((url) => Utils.download(url))
|
||||
)
|
||||
console.log("Building svg")
|
||||
const pdf = new SvgToPdf("Test", svgs, {})
|
||||
new VariableUiElement(pdf.status).AttachTo("maindiv")
|
||||
await pdf.ConvertSvg("nl")
|
||||
}
|
||||
|
||||
testPdf().then((_) => console.log("All done"))
|
||||
//testinput()
|
||||
/*/
|
||||
testspecial()
|
||||
|
|
Loading…
Reference in a new issue