forked from MapComplete/MapComplete
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",
|
changeType: "conflation",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
allChanges.push(...(await addExtraTags.CreateChangeDescriptions(changes)))
|
allChanges.push(...(await addExtraTags.CreateChangeDescriptions()))
|
||||||
}
|
}
|
||||||
|
|
||||||
const newCoordinates = [...this.targetCoordinates]
|
const newCoordinates = [...this.targetCoordinates]
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Conversion } from "./Conversion"
|
import {Conversion} from "./Conversion"
|
||||||
import LayerConfig from "../LayerConfig"
|
import LayerConfig from "../LayerConfig"
|
||||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
import {LayerConfigJson} from "../Json/LayerConfigJson"
|
||||||
import Translations from "../../../UI/i18n/Translations"
|
import Translations from "../../../UI/i18n/Translations"
|
||||||
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"
|
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> {
|
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 }
|
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 }
|
return { ...translation.Subs(subs).translations, _context: translation.context }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ import Translations from "./i18n/Translations"
|
||||||
import Constants from "../Models/Constants"
|
import Constants from "../Models/Constants"
|
||||||
import LanguagePicker from "./LanguagePicker"
|
import LanguagePicker from "./LanguagePicker"
|
||||||
import IndexText from "./BigComponents/IndexText"
|
import IndexText from "./BigComponents/IndexText"
|
||||||
import { ImportViewerLinks } from "./BigComponents/UserInformation"
|
|
||||||
import { LoginToggle } from "./Popup/LoginButton"
|
import { LoginToggle } from "./Popup/LoginButton"
|
||||||
import { ImmutableStore } from "../Logic/UIEventSource"
|
import { ImmutableStore } from "../Logic/UIEventSource"
|
||||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||||
|
@ -29,7 +28,6 @@ export default class AllThemesGui {
|
||||||
osmConnection,
|
osmConnection,
|
||||||
featureSwitchUserbadge: new ImmutableStore(true),
|
featureSwitchUserbadge: new ImmutableStore(true),
|
||||||
}),
|
}),
|
||||||
new ImportViewerLinks(state.osmConnection),
|
|
||||||
Translations.t.general.aboutMapComplete.intro.SetClass("link-underline"),
|
Translations.t.general.aboutMapComplete.intro.SetClass("link-underline"),
|
||||||
new FixedUiElement("v" + Constants.vNumber).SetClass("block"),
|
new FixedUiElement("v" + Constants.vNumber).SetClass("block"),
|
||||||
])
|
])
|
||||||
|
|
|
@ -1,31 +1,30 @@
|
||||||
import { UIElement } from "../UIElement"
|
import {UIElement} from "../UIElement"
|
||||||
import BaseUIElement from "../BaseUIElement"
|
import BaseUIElement from "../BaseUIElement"
|
||||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
import {Store} from "../../Logic/UIEventSource"
|
||||||
import ExtraLinkConfig from "../../Models/ThemeConfig/ExtraLinkConfig"
|
import ExtraLinkConfig from "../../Models/ThemeConfig/ExtraLinkConfig"
|
||||||
import Img from "../Base/Img"
|
import Img from "../Base/Img"
|
||||||
import { SubtleButton } from "../Base/SubtleButton"
|
import {SubtleButton} from "../Base/SubtleButton"
|
||||||
import Toggle from "../Input/Toggle"
|
import Toggle from "../Input/Toggle"
|
||||||
import Loc from "../../Models/Loc"
|
|
||||||
import Locale from "../i18n/Locale"
|
import Locale from "../i18n/Locale"
|
||||||
import { Utils } from "../../Utils"
|
import {Utils} from "../../Utils"
|
||||||
import Svg from "../../Svg"
|
import Svg from "../../Svg"
|
||||||
import Translations from "../i18n/Translations"
|
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 {
|
export default class ExtraLinkButton extends UIElement {
|
||||||
private readonly _config: ExtraLinkConfig
|
private readonly _config: ExtraLinkConfig
|
||||||
private readonly state: {
|
private readonly state: ExtraLinkButtonState
|
||||||
layoutToUse: { id: string; title: Translation }
|
|
||||||
featureSwitchWelcomeMessage: UIEventSource<boolean>
|
|
||||||
locationControl: UIEventSource<Loc>
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
state: {
|
state: ExtraLinkButtonState,
|
||||||
featureSwitchWelcomeMessage: UIEventSource<boolean>
|
|
||||||
locationControl: UIEventSource<Loc>
|
|
||||||
layoutToUse: { id: string; title: Translation }
|
|
||||||
},
|
|
||||||
config: ExtraLinkConfig
|
config: ExtraLinkConfig
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
@ -41,7 +40,6 @@ export default class ExtraLinkButton extends UIElement {
|
||||||
const c = this._config
|
const c = this._config
|
||||||
|
|
||||||
const isIframe = window !== window.top
|
const isIframe = window !== window.top
|
||||||
|
|
||||||
if (c.requirements?.has("iframe") && !isIframe) {
|
if (c.requirements?.has("iframe") && !isIframe) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
@ -51,9 +49,9 @@ export default class ExtraLinkButton extends UIElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
let link: BaseUIElement
|
let link: BaseUIElement
|
||||||
const theme = this.state.layoutToUse?.id ?? ""
|
const theme = this.state.layout?.id ?? ""
|
||||||
const basepath = window.location.host
|
const basepath = window.location.host
|
||||||
const href = this.state.locationControl.map((loc) => {
|
const href = this.state.mapProperties.location.map((loc) => {
|
||||||
const subs = {
|
const subs = {
|
||||||
...loc,
|
...loc,
|
||||||
theme: theme,
|
theme: theme,
|
||||||
|
@ -61,7 +59,7 @@ export default class ExtraLinkButton extends UIElement {
|
||||||
language: Locale.language.data,
|
language: Locale.language.data,
|
||||||
}
|
}
|
||||||
return Utils.SubstituteKeys(c.href, subs)
|
return Utils.SubstituteKeys(c.href, subs)
|
||||||
})
|
}, [this.state.mapProperties.zoom])
|
||||||
|
|
||||||
let img: BaseUIElement = Svg.pop_out_ui()
|
let img: BaseUIElement = Svg.pop_out_ui()
|
||||||
if (c.icon !== undefined) {
|
if (c.icon !== undefined) {
|
||||||
|
@ -71,7 +69,7 @@ export default class ExtraLinkButton extends UIElement {
|
||||||
let text: Translation
|
let text: Translation
|
||||||
if (c.text === undefined) {
|
if (c.text === undefined) {
|
||||||
text = Translations.t.general.screenToSmall.Subs({
|
text = Translations.t.general.screenToSmall.Subs({
|
||||||
theme: this.state.layoutToUse.title,
|
theme: this.state.layout.title,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
text = c.text.Clone()
|
text = c.text.Clone()
|
||||||
|
@ -83,11 +81,11 @@ export default class ExtraLinkButton extends UIElement {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (c.requirements?.has("no-welcome-message")) {
|
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")) {
|
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
|
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 {VariableUiElement} from "../Base/VariableUIElement"
|
||||||
import { Translation } from "../i18n/Translation"
|
import {Translation} from "../i18n/Translation"
|
||||||
import Svg from "../../Svg"
|
import Svg from "../../Svg"
|
||||||
import Combine from "../Base/Combine"
|
import Combine from "../Base/Combine"
|
||||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
import {Store, UIEventSource} from "../../Logic/UIEventSource"
|
||||||
import { Utils } from "../../Utils"
|
import {Utils} from "../../Utils"
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../i18n/Translations"
|
||||||
import BaseUIElement from "../BaseUIElement"
|
import BaseUIElement from "../BaseUIElement"
|
||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
import {InputElement} from "../Input/InputElement"
|
||||||
import Loc from "../../Models/Loc"
|
import {CheckBox} from "../Input/Checkboxes"
|
||||||
import FilteredLayer from "../../Models/FilteredLayer"
|
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 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) {
|
constructor(state: SpecialVisualizationState) {
|
||||||
const layout = state?.layout
|
const layout = state?.layout
|
||||||
const tr = Translations.t.general.sharescreen
|
const tr = Translations.t.general.sharescreen
|
||||||
|
@ -63,7 +60,7 @@ export default class ShareScreen extends Combine {
|
||||||
return "layer-" + flayer.layerDef.id + "=" + flayer.isDisplayed.data
|
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)
|
state.mapProperties.rasterLayer.map((l) => l?.properties)
|
||||||
const currentBackground = new VariableUiElement(
|
const currentBackground = new VariableUiElement(
|
||||||
currentLayer.map((layer) => {
|
currentLayer.map((layer) => {
|
||||||
|
@ -93,13 +90,13 @@ export default class ShareScreen extends Combine {
|
||||||
(includeLayerSelection) => {
|
(includeLayerSelection) => {
|
||||||
if (includeLayerSelection) {
|
if (includeLayerSelection) {
|
||||||
return Utils.NoNull(
|
return Utils.NoNull(
|
||||||
state.layerState.filteredLayers.map(fLayerToParam)
|
Array.from( state.layerState.filteredLayers.values()).map(fLayerToParam)
|
||||||
).join("&")
|
).join("&")
|
||||||
} else {
|
} else {
|
||||||
return null
|
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> {
|
async exportAsPng(): Promise<Blob> {
|
||||||
const map = this._maplibreMap.data
|
const map = this._maplibreMap.data
|
||||||
if (map === undefined) {
|
if (!map) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,7 +317,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
|
|
||||||
private async awaitStyleIsLoaded(): Promise<void> {
|
private async awaitStyleIsLoaded(): Promise<void> {
|
||||||
const map = this._maplibreMap.data
|
const map = this._maplibreMap.data
|
||||||
if (map === undefined) {
|
if (!map) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
while (!map?.isStyleLoaded()) {
|
while (!map?.isStyleLoaded()) {
|
||||||
|
@ -335,7 +335,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
|
|
||||||
private async setBackground() {
|
private async setBackground() {
|
||||||
const map = this._maplibreMap.data
|
const map = this._maplibreMap.data
|
||||||
if (map === undefined) {
|
if (!map) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const background: RasterLayerProperties = this.rasterLayer?.data?.properties
|
const background: RasterLayerProperties = this.rasterLayer?.data?.properties
|
||||||
|
@ -381,7 +381,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
|
|
||||||
private setMaxBounds(bbox: undefined | BBox) {
|
private setMaxBounds(bbox: undefined | BBox) {
|
||||||
const map = this._maplibreMap.data
|
const map = this._maplibreMap.data
|
||||||
if (map === undefined) {
|
if (!map) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (bbox) {
|
if (bbox) {
|
||||||
|
@ -393,7 +393,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
|
|
||||||
private setAllowMoving(allow: true | boolean | undefined) {
|
private setAllowMoving(allow: true | boolean | undefined) {
|
||||||
const map = this._maplibreMap.data
|
const map = this._maplibreMap.data
|
||||||
if (map === undefined) {
|
if (!map) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (allow === false) {
|
if (allow === false) {
|
||||||
|
@ -409,7 +409,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
|
|
||||||
private setMinzoom(minzoom: number) {
|
private setMinzoom(minzoom: number) {
|
||||||
const map = this._maplibreMap.data
|
const map = this._maplibreMap.data
|
||||||
if (map === undefined) {
|
if (!map) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
map.setMinZoom(minzoom)
|
map.setMinZoom(minzoom)
|
||||||
|
@ -417,7 +417,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
|
|
||||||
private setMaxzoom(maxzoom: number) {
|
private setMaxzoom(maxzoom: number) {
|
||||||
const map = this._maplibreMap.data
|
const map = this._maplibreMap.data
|
||||||
if (map === undefined) {
|
if (!map) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
map.setMaxZoom(maxzoom)
|
map.setMaxZoom(maxzoom)
|
||||||
|
@ -425,7 +425,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
|
|
||||||
private setAllowZooming(allow: true | boolean | undefined) {
|
private setAllowZooming(allow: true | boolean | undefined) {
|
||||||
const map = this._maplibreMap.data
|
const map = this._maplibreMap.data
|
||||||
if (map === undefined) {
|
if (!map) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (allow === false) {
|
if (allow === false) {
|
||||||
|
@ -441,7 +441,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
|
|
||||||
private setBounds(bounds: BBox) {
|
private setBounds(bounds: BBox) {
|
||||||
const map = this._maplibreMap.data
|
const map = this._maplibreMap.data
|
||||||
if (map === undefined || bounds === undefined) {
|
if (!map || bounds === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const oldBounds = map.getBounds()
|
const oldBounds = map.getBounds()
|
||||||
|
|
|
@ -164,9 +164,9 @@
|
||||||
{#if config.freeform?.key}
|
{#if config.freeform?.key}
|
||||||
<label class="flex">
|
<label class="flex">
|
||||||
<input type="radio" bind:group={selectedMapping} name={"mappings-radio-"+config.id}
|
<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}
|
<FreeformInput {config} {tags} feature={selectedElement} value={freeformInput}
|
||||||
on:selected={() => selectedMapping = config.mappings.length }/>
|
on:selected={() => selectedMapping = config.mappings?.length }/>
|
||||||
</label>
|
</label>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -182,7 +182,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
{#if config.freeform?.key}
|
{#if config.freeform?.key}
|
||||||
<label class="flex">
|
<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]}>
|
bind:checked={checkedMappings[config.mappings.length]}>
|
||||||
<FreeformInput {config} {tags} feature={selectedElement} value={freeformInput}
|
<FreeformInput {config} {tags} feature={selectedElement} value={freeformInput}
|
||||||
on:selected={() => checkedMappings[config.mappings.length] = true}/>
|
on:selected={() => checkedMappings[config.mappings.length] = true}/>
|
||||||
|
|
|
@ -3,10 +3,9 @@ import Combine from "./Base/Combine"
|
||||||
import Title from "./Base/Title"
|
import Title from "./Base/Title"
|
||||||
import List from "./Base/List"
|
import List from "./Base/List"
|
||||||
import Translations from "./i18n/Translations"
|
import Translations from "./i18n/Translations"
|
||||||
import { QueryParameters } from "../Logic/Web/QueryParameters"
|
import {QueryParameters} from "../Logic/Web/QueryParameters"
|
||||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
||||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
||||||
import { DefaultGuiState } from "./DefaultGuiState"
|
|
||||||
|
|
||||||
export default class QueryParameterDocumentation {
|
export default class QueryParameterDocumentation {
|
||||||
private static QueryParamDocsIntro = [
|
private static QueryParamDocsIntro = [
|
||||||
|
@ -48,7 +47,6 @@ export default class QueryParameterDocumentation {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
new DefaultGuiState() // Init a featureSwitchState to init all the parameters
|
|
||||||
new FeatureSwitchState(dummyLayout)
|
new FeatureSwitchState(dummyLayout)
|
||||||
|
|
||||||
QueryParameters.GetQueryParameter(
|
QueryParameters.GetQueryParameter(
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
import OverlayToggle from "./BigComponents/OverlayToggle.svelte";
|
import OverlayToggle from "./BigComponents/OverlayToggle.svelte";
|
||||||
import LevelSelector from "./BigComponents/LevelSelector.svelte";
|
import LevelSelector from "./BigComponents/LevelSelector.svelte";
|
||||||
import Svg from "../Svg";
|
import Svg from "../Svg";
|
||||||
|
import ExtraLinkButton from "./BigComponents/ExtraLinkButton";
|
||||||
|
|
||||||
export let state: ThemeViewState;
|
export let state: ThemeViewState;
|
||||||
let layout = state.layout;
|
let layout = state.layout;
|
||||||
|
@ -97,6 +98,7 @@
|
||||||
construct={() =>(currentViewLayer.defaultIcon() ?? Svg.checkbox_empty_svg()).SetClass("w-8 h-8 cursor-pointer")}/>
|
construct={() =>(currentViewLayer.defaultIcon() ?? Svg.checkbox_empty_svg()).SetClass("w-8 h-8 cursor-pointer")}/>
|
||||||
</MapControlButton>
|
</MapControlButton>
|
||||||
{/if}
|
{/if}
|
||||||
|
<ToSvelte construct={() => new ExtraLinkButton(state, layout.extraLink)}></ToSvelte>
|
||||||
<If condition={state.featureSwitchIsTesting}>
|
<If condition={state.featureSwitchIsTesting}>
|
||||||
<span class="alert">
|
<span class="alert">
|
||||||
Testmode
|
Testmode
|
||||||
|
|
|
@ -1,130 +1,53 @@
|
||||||
import FeaturePipelineState from "../Logic/State/FeaturePipelineState"
|
import ThemeViewState from "../Models/ThemeViewState"
|
||||||
import MinimapImplementation from "../UI/Base/MinimapImplementation"
|
import SvelteUIElement from "../UI/Base/SvelteUIElement"
|
||||||
import { UIEventSource } from "../Logic/UIEventSource"
|
import MaplibreMap from "../UI/Map/MaplibreMap.svelte"
|
||||||
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 { Utils } from "../Utils"
|
import { Utils } from "../Utils"
|
||||||
|
import { UIEventSource } from "../Logic/UIEventSource"
|
||||||
|
|
||||||
export interface PngMapCreatorOptions {
|
export interface PngMapCreatorOptions {
|
||||||
readonly divId: string
|
|
||||||
readonly width: number
|
readonly width: number
|
||||||
readonly height: number
|
readonly height: number
|
||||||
readonly scaling?: 1 | number
|
|
||||||
readonly dummyMode?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PngMapCreator {
|
export class PngMapCreator {
|
||||||
private readonly _state: FeaturePipelineState | undefined
|
private static id = 0
|
||||||
private readonly _options: PngMapCreatorOptions
|
private readonly _options: PngMapCreatorOptions
|
||||||
|
private readonly _state: ThemeViewState
|
||||||
|
|
||||||
constructor(state: FeaturePipelineState | undefined, options: PngMapCreatorOptions) {
|
constructor(state: ThemeViewState, options: PngMapCreatorOptions) {
|
||||||
this._state = state
|
this._state = state
|
||||||
this._options = { ...options, scaling: options.scaling ?? 1 }
|
this._options = options
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a base64-encoded PNG image
|
* Creates a base64-encoded PNG image
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
public async CreatePng(format: "image"): Promise<string>
|
public async CreatePng(status: UIEventSource<string>): Promise<Blob> {
|
||||||
public async CreatePng(format: "blob"): Promise<Blob>
|
const div = document.createElement("div")
|
||||||
public async CreatePng(format: "image" | "blob"): Promise<string | Blob>
|
div.id = "mapdiv-" + PngMapCreator.id
|
||||||
public async CreatePng(format: "image" | "blob"): Promise<string | Blob> {
|
PngMapCreator.id++
|
||||||
// Lets first init the minimap and wait for all background tiles to load
|
const layout = this._state.layout
|
||||||
const minimap = await this.createAndLoadMinimap()
|
function setState(msg: string) {
|
||||||
const state = this._state
|
status.setData(layout.id + ": " + msg)
|
||||||
const dummyMode = this._options.dummyMode ?? false
|
}
|
||||||
return new Promise<string | Blob>((resolve, reject) => {
|
setState("Initializing map")
|
||||||
// Next: we prepare the features. Only fully contained features are shown
|
const map = this._state.map
|
||||||
minimap.leafletMap.addCallbackAndRunD(async (leaflet) => {
|
new SvelteUIElement(MaplibreMap, { map })
|
||||||
// Ping the featurepipeline to download what is needed
|
.SetStyle(
|
||||||
if (dummyMode) {
|
"width: " + this._options.width + "mm; height: " + this._options.height + "mm"
|
||||||
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)
|
.AttachTo("extradiv")
|
||||||
if (!state.featurePipeline.sufficientlyZoomed.data) {
|
setState("Waiting for the data")
|
||||||
console.warn("Not sufficiently zoomed!")
|
await this._state.dataIsLoading.AsPromise((loading) => !loading)
|
||||||
}
|
setState("Waiting for styles to be fully loaded")
|
||||||
|
while (!map?.data?.isStyleLoaded()) {
|
||||||
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)
|
await Utils.waitFor(250)
|
||||||
document
|
}
|
||||||
.getElementById(divId)
|
// Some extra buffer...
|
||||||
.removeChild(
|
await Utils.waitFor(1000)
|
||||||
/*Will fetch the cached htmlelement:*/ minimap.ConstructElement()
|
setState("Exporting png")
|
||||||
)
|
console.log("Loading for", this._state.layout.id, "is done")
|
||||||
return resolve(result)
|
return this._state.mapProperties.exportAsPng()
|
||||||
})
|
|
||||||
.catch((failreason) => {
|
|
||||||
console.error("Could no make a screenshot due to ", failreason)
|
|
||||||
reject(failreason)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
state.AddAllOverlaysToMap(minimap.leafletMap)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,20 +2,21 @@ import jsPDF, { Matrix } from "jspdf"
|
||||||
import { Translation, TypedTranslation } from "../UI/i18n/Translation"
|
import { Translation, TypedTranslation } from "../UI/i18n/Translation"
|
||||||
import { PngMapCreator } from "./pngMapCreator"
|
import { PngMapCreator } from "./pngMapCreator"
|
||||||
import { AllKnownLayouts } from "../Customizations/AllKnownLayouts"
|
import { AllKnownLayouts } from "../Customizations/AllKnownLayouts"
|
||||||
import { Store } from "../Logic/UIEventSource"
|
import "../assets/fonts/Ubuntu-M-normal.js"
|
||||||
import "../assets/templates/Ubuntu-M-normal.js"
|
import "../assets/fonts/Ubuntu-L-normal.js"
|
||||||
import "../assets/templates/Ubuntu-L-normal.js"
|
import "../assets/fonts/UbuntuMono-B-bold.js"
|
||||||
import "../assets/templates/UbuntuMono-B-bold.js"
|
|
||||||
import { makeAbsolute, parseSVG } from "svg-path-parser"
|
import { makeAbsolute, parseSVG } from "svg-path-parser"
|
||||||
import Translations from "../UI/i18n/Translations"
|
import Translations from "../UI/i18n/Translations"
|
||||||
import { Utils } from "../Utils"
|
import { Utils } from "../Utils"
|
||||||
import Constants from "../Models/Constants"
|
import Constants from "../Models/Constants"
|
||||||
import Hash from "../Logic/Web/Hash"
|
import Hash from "../Logic/Web/Hash"
|
||||||
import ThemeViewState from "../Models/ThemeViewState"
|
import ThemeViewState from "../Models/ThemeViewState"
|
||||||
|
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||||
|
import { FixedUiElement } from "../UI/Base/FixedUiElement"
|
||||||
|
|
||||||
class SvgToPdfInternals {
|
class SvgToPdfInternals {
|
||||||
private readonly doc: jsPDF
|
|
||||||
private static readonly dummyDoc: jsPDF = new jsPDF()
|
private static readonly dummyDoc: jsPDF = new jsPDF()
|
||||||
|
private readonly doc: jsPDF
|
||||||
private readonly matrices: Matrix[] = []
|
private readonly matrices: Matrix[] = []
|
||||||
private readonly matricesInverted: Matrix[] = []
|
private readonly matricesInverted: Matrix[] = []
|
||||||
|
|
||||||
|
@ -40,26 +41,6 @@ class SvgToPdfInternals {
|
||||||
this.currentMatrixInverted = this.doc.unitMatrix
|
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 {
|
public static extractMatrix(element: Element): Matrix {
|
||||||
const t = element.getAttribute("transform")
|
const t = element.getAttribute("transform")
|
||||||
if (t === null) {
|
if (t === null) {
|
||||||
|
@ -107,22 +88,6 @@ class SvgToPdfInternals {
|
||||||
return null
|
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> {
|
public static parseCss(styleContent: string, separator: string = ";"): Record<string, string> {
|
||||||
if (styleContent === undefined || styleContent === null) {
|
if (styleContent === undefined || styleContent === null) {
|
||||||
return {}
|
return {}
|
||||||
|
@ -137,41 +102,36 @@ class SvgToPdfInternals {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawRect(element: SVGRectElement) {
|
static attrNumber(element: Element, name: string, recurseup: boolean = true): number {
|
||||||
const x = Number(element.getAttribute("x"))
|
const a = SvgToPdfInternals.attr(element, name, recurseup)
|
||||||
const y = Number(element.getAttribute("y"))
|
const n = parseFloat(a)
|
||||||
const width = Number(element.getAttribute("width"))
|
if (!isNaN(n)) {
|
||||||
const height = Number(element.getAttribute("height"))
|
return n
|
||||||
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") {
|
return undefined
|
||||||
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"))
|
* Helper function to calculate where the given point will end up.
|
||||||
const y = Number(element.getAttribute("cy"))
|
* ALl the transforms of the parent elements are taking into account
|
||||||
const r = Number(element.getAttribute("r"))
|
* @param mapSpec
|
||||||
const css = SvgToPdfInternals.css(element)
|
* @constructor
|
||||||
if (css["fill-opacity"] !== "0" && css["fill"] !== "none") {
|
*/
|
||||||
this.doc.setFillColor(css["fill"] ?? "black")
|
static GetActualXY(mapSpec: SVGTSpanElement): { x: number; y: number } {
|
||||||
this.doc.circle(x, y, r, "F")
|
let runningM = SvgToPdfInternals.dummyDoc.unitMatrix
|
||||||
|
|
||||||
|
let e: Element = mapSpec
|
||||||
|
do {
|
||||||
|
const m = SvgToPdfInternals.extractMatrix(e)
|
||||||
|
if (m !== null) {
|
||||||
|
runningM = SvgToPdfInternals.dummyDoc.matrixMult(runningM, m)
|
||||||
}
|
}
|
||||||
if (css["stroke"] && css["stroke"] !== "none") {
|
e = e.parentElement
|
||||||
this.doc.setLineWidth(Number(css["stroke-width"] ?? 1))
|
} while (e !== null && e.parentElement != e)
|
||||||
this.doc.setDrawColor(css["stroke"] ?? "black")
|
|
||||||
this.doc.circle(x, y, r, "S")
|
const x = SvgToPdfInternals.attrNumber(mapSpec, "x")
|
||||||
}
|
const y = SvgToPdfInternals.attrNumber(mapSpec, "y")
|
||||||
return
|
return runningM.applyToPoint({ x, y })
|
||||||
}
|
}
|
||||||
|
|
||||||
private static attr(
|
private static attr(
|
||||||
|
@ -214,13 +174,119 @@ class SvgToPdfInternals {
|
||||||
return css
|
return css
|
||||||
}
|
}
|
||||||
|
|
||||||
static attrNumber(element: Element, name: string, recurseup: boolean = true): number {
|
applyMatrices(): void {
|
||||||
const a = SvgToPdfInternals.attr(element, name, recurseup)
|
let multiplied = this.doc.unitMatrix
|
||||||
const n = parseFloat(a)
|
let multipliedInv = this.doc.unitMatrix
|
||||||
if (!isNaN(n)) {
|
for (const matrix of this.matrices) {
|
||||||
return n
|
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) {
|
private drawTspan(tspan: Element) {
|
||||||
|
@ -427,129 +493,43 @@ class SvgToPdfInternals {
|
||||||
this.doc.fill()
|
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 {
|
export interface SvgToPdfOptions {
|
||||||
getFreeDiv: () => string
|
|
||||||
disableMaps?: false | true
|
disableMaps?: false | true
|
||||||
textSubstitutions?: Record<string, string>
|
textSubstitutions?: Record<string, string>
|
||||||
beforePage?: (i: number) => void
|
beforePage?: (i: number) => void
|
||||||
overrideLocation?: { lat: number; lon: number }
|
overrideLocation?: { lat: number; lon: number }
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SvgToPdfPage {
|
class SvgToPdfPage {
|
||||||
|
public readonly _svgRoot: SVGSVGElement
|
||||||
private images: Record<string, HTMLImageElement> = {}
|
private images: Record<string, HTMLImageElement> = {}
|
||||||
private rects: Record<string, SVGRectElement> = {}
|
private rects: Record<string, SVGRectElement> = {}
|
||||||
public readonly _svgRoot: SVGSVGElement
|
|
||||||
public readonly currentState: Store<string>
|
|
||||||
private readonly importedTranslations: Record<string, string> = {}
|
private readonly importedTranslations: Record<string, string> = {}
|
||||||
private readonly layerTranslations: Record<string, Record<string, any>> = {}
|
private readonly layerTranslations: Record<string, Record<string, any>> = {}
|
||||||
private readonly options: SvgToPdfOptions
|
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>{}
|
this.options = options ?? <SvgToPdfOptions>{}
|
||||||
const parser = new DOMParser()
|
const parser = new DOMParser()
|
||||||
const xmlDoc = parser.parseFromString(page, "image/svg+xml")
|
const xmlDoc = parser.parseFromString(page, "image/svg+xml")
|
||||||
this._svgRoot = xmlDoc.getElementsByTagName("svg")[0]
|
this._svgRoot = xmlDoc.getElementsByTagName("svg")[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadImage(element: Element): Promise<void> {
|
private static blobToBase64(blob): Promise<string> {
|
||||||
const xlink = element.getAttribute("xlink:href")
|
return new Promise((resolve, _) => {
|
||||||
let img = document.createElement("img")
|
const reader = new FileReader()
|
||||||
|
reader.onloadend = () => resolve(<string>reader.result)
|
||||||
if (xlink.startsWith("data:image/svg+xml;")) {
|
reader.readAsDataURL(blob)
|
||||||
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()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
public async PrepareLanguage(language: string) {
|
||||||
// Always fetch the remote data - it's cached anyway
|
// Always fetch the remote data - it's cached anyway
|
||||||
this.layerTranslations[language] = await Utils.downloadJsonCached(
|
this.layerTranslations[language] = await Utils.downloadJsonCached(
|
||||||
|
@ -888,31 +708,219 @@ export class SvgToPdfPage {
|
||||||
console.error("Could not get textFor from ", t, "for path", text)
|
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 {
|
export class SvgToPdf {
|
||||||
public static readonly templates: Record<
|
public static readonly templates: Record<
|
||||||
string,
|
"flyer_a4" | "poster_a3" | "poster_a2",
|
||||||
{ pages: string[]; description: string | Translation }
|
{ pages: string[]; description: string | Translation }
|
||||||
> = {
|
> = {
|
||||||
flyer_a4: {
|
flyer_a4: {
|
||||||
pages: [
|
pages: [
|
||||||
"/assets/templates/MapComplete-flyer.svg",
|
"./assets/templates/MapComplete-flyer.svg",
|
||||||
"/assets/templates/MapComplete-flyer.back.svg",
|
"./assets/templates/MapComplete-flyer.back.svg",
|
||||||
],
|
],
|
||||||
description: Translations.t.flyer.description,
|
description: Translations.t.flyer.description,
|
||||||
},
|
},
|
||||||
poster_a3: {
|
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)",
|
description: "A basic A3 poster (similar to the flyer)",
|
||||||
},
|
},
|
||||||
poster_a2: {
|
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",
|
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 _title: string
|
||||||
|
|
||||||
private readonly _pages: SvgToPdfPage[]
|
private readonly _pages: SvgToPdfPage[]
|
||||||
|
|
||||||
constructor(title: string, pages: string[], options?: SvgToPdfOptions) {
|
constructor(title: string, pages: string[], options?: SvgToPdfOptions) {
|
||||||
|
@ -926,24 +934,34 @@ export class SvgToPdf {
|
||||||
).length
|
).length
|
||||||
options.textSubstitutions["mapCount"] = mapCount
|
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> {
|
public async ConvertSvg(language: string): Promise<void> {
|
||||||
|
console.log("Building svg...")
|
||||||
const firstPage = this._pages[0]._svgRoot
|
const firstPage = this._pages[0]._svgRoot
|
||||||
const width = SvgToPdfInternals.attrNumber(firstPage, "width")
|
const width = SvgToPdfInternals.attrNumber(firstPage, "width")
|
||||||
const height = SvgToPdfInternals.attrNumber(firstPage, "height")
|
const height = SvgToPdfInternals.attrNumber(firstPage, "height")
|
||||||
const mode = width > height ? "landscape" : "portrait"
|
const mode = width > height ? "landscape" : "portrait"
|
||||||
|
|
||||||
await this.Prepare()
|
await this.Prepare()
|
||||||
|
console.log("Global prepare done")
|
||||||
for (const page of this._pages) {
|
for (const page of this._pages) {
|
||||||
await page.Prepare()
|
await page.Prepare()
|
||||||
await page.PrepareLanguage(language)
|
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])
|
const doc = new jsPDF(mode, undefined, [width, height])
|
||||||
doc.advancedAPI((advancedApi) => {
|
doc.advancedAPI((advancedApi) => {
|
||||||
for (let i = 0; i < this._pages.length; i++) {
|
for (let i = 0; i < this._pages.length; i++) {
|
||||||
|
console.log("Rendering page", i)
|
||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
const page = this._pages[i]._svgRoot
|
const page = this._pages[i]._svgRoot
|
||||||
const width = SvgToPdfInternals.attrNumber(page, "width")
|
const width = SvgToPdfInternals.attrNumber(page, "width")
|
||||||
|
@ -967,6 +985,7 @@ export class SvgToPdf {
|
||||||
this._pages[i].drawPage(advancedApi, i, language)
|
this._pages[i].drawPage(advancedApi, i, language)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
console.log("Exporting...")
|
||||||
await doc.save(this._title + "." + language + ".pdf")
|
await doc.save(this._title + "." + language + ".pdf")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
--popup-border: white;
|
--popup-border: white;
|
||||||
--shadow-color: #00000066;
|
--shadow-color: #00000066;
|
||||||
--variable-title-height: 0px; /* Set by javascript */
|
--variable-title-height: 0px; /* Set by javascript */
|
||||||
--return-to-the-map-height: 2em;
|
|
||||||
|
|
||||||
--image-carousel-height: 350px;
|
--image-carousel-height: 350px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1407,10 +1407,6 @@ video {
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-b-2 {
|
|
||||||
border-bottom-width: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.border-solid {
|
.border-solid {
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
}
|
}
|
||||||
|
@ -1588,10 +1584,6 @@ video {
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pb-8 {
|
|
||||||
padding-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-5 {
|
.pl-5 {
|
||||||
padding-left: 1.25rem;
|
padding-left: 1.25rem;
|
||||||
}
|
}
|
||||||
|
@ -1877,7 +1869,6 @@ video {
|
||||||
--catch-detail-color-contrast: white;
|
--catch-detail-color-contrast: white;
|
||||||
--non-active-tab-svg: var(--foreground-color);
|
--non-active-tab-svg: var(--foreground-color);
|
||||||
--shadow-color: #00000066;
|
--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. */
|
/* Technical variable to make some dynamic behaviour possible; set by javascript. */
|
||||||
--variable-title-height: 0px;
|
--variable-title-height: 0px;
|
||||||
|
|
|
@ -84,7 +84,6 @@
|
||||||
--non-active-tab-svg: var(--foreground-color);
|
--non-active-tab-svg: var(--foreground-color);
|
||||||
--shadow-color: #00000066;
|
--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. */
|
/* Technical variable to make some dynamic behaviour possible; set by javascript. */
|
||||||
|
@ -122,7 +121,6 @@ img {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.weblate-link {
|
.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 */
|
||||||
}
|
}
|
||||||
|
@ -394,7 +392,6 @@ li::marker {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************** Info box (box containing features and questions ******************/
|
/***************** Info box (box containing features and questions ******************/
|
||||||
|
|
||||||
input {
|
input {
|
||||||
|
@ -441,6 +438,7 @@ input {
|
||||||
-moz-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;
|
animation: glowing 1s ease-in-out infinite alternate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@-webkit-keyframes glowing {
|
@-webkit-keyframes glowing {
|
||||||
from {
|
from {
|
||||||
box-shadow: 0 0 20px 10px #eaaf2588, inset 0 0 0px 1px #eaaf25;
|
box-shadow: 0 0 20px 10px #eaaf2588, inset 0 0 0px 1px #eaaf25;
|
||||||
|
|
120
package-lock.json
generated
120
package-lock.json
generated
|
@ -49,7 +49,6 @@
|
||||||
"showdown": "^2.1.0",
|
"showdown": "^2.1.0",
|
||||||
"svg-path-parser": "^1.1.0",
|
"svg-path-parser": "^1.1.0",
|
||||||
"tailwindcss": "^3.1.8",
|
"tailwindcss": "^3.1.8",
|
||||||
"togpx": "^0.5.4",
|
|
||||||
"vite-node": "^0.28.3",
|
"vite-node": "^0.28.3",
|
||||||
"vitest": "^0.28.3",
|
"vitest": "^0.28.3",
|
||||||
"wikibase-sdk": "^7.14.0",
|
"wikibase-sdk": "^7.14.0",
|
||||||
|
@ -4301,23 +4300,6 @@
|
||||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
|
@ -7352,14 +7334,6 @@
|
||||||
"safe-buffer": "^5.0.1"
|
"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": {
|
"node_modules/kdbush": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-1.0.1.tgz",
|
||||||
|
@ -10265,36 +10239,6 @@
|
||||||
"node": ">=0.12.0"
|
"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": {
|
"node_modules/topojson-client": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz",
|
||||||
|
@ -12055,15 +11999,6 @@
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": 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": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
@ -15350,22 +15285,6 @@
|
||||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||||
"dev": true
|
"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": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
|
@ -17635,14 +17554,6 @@
|
||||||
"safe-buffer": "^5.0.1"
|
"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": {
|
"kdbush": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-1.0.1.tgz",
|
"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": {
|
"topojson-client": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz",
|
||||||
|
@ -21167,11 +21052,6 @@
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": 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": {
|
"xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"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 SpecialVisualizations from "./UI/SpecialVisualizations"
|
||||||
import InputHelpers from "./UI/InputElement/InputHelpers"
|
import InputHelpers from "./UI/InputElement/InputHelpers"
|
||||||
import BaseUIElement from "./UI/BaseUIElement"
|
import BaseUIElement from "./UI/BaseUIElement"
|
||||||
import { ImmutableStore, UIEventSource } from "./Logic/UIEventSource"
|
import { UIEventSource } from "./Logic/UIEventSource"
|
||||||
import { VariableUiElement } from "./UI/Base/VariableUIElement"
|
import { VariableUiElement } from "./UI/Base/VariableUIElement"
|
||||||
import { FixedUiElement } from "./UI/Base/FixedUiElement"
|
import { FixedUiElement } from "./UI/Base/FixedUiElement"
|
||||||
import Title from "./UI/Base/Title"
|
import Title from "./UI/Base/Title"
|
||||||
import SvelteUIElement from "./UI/Base/SvelteUIElement"
|
import SvelteUIElement from "./UI/Base/SvelteUIElement"
|
||||||
import ValidatedInput from "./UI/InputElement/ValidatedInput.svelte"
|
import ValidatedInput from "./UI/InputElement/ValidatedInput.svelte"
|
||||||
|
import { SvgToPdf } from "./Utils/svgToPdf"
|
||||||
|
import { Utils } from "./Utils"
|
||||||
|
|
||||||
function testspecial() {
|
function testspecial() {
|
||||||
const layout = new LayoutConfig(<any>theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
|
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")
|
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()
|
//testinput()
|
||||||
/*/
|
/*/
|
||||||
testspecial()
|
testspecial()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue