mapcomplete/UI/BigComponents/PdfExportGui.ts

347 lines
14 KiB
TypeScript

import Combine from "../Base/Combine";
import {FlowPanelFactory, FlowStep} from "../ImportFlow/FlowStep";
import {ImmutableStore, Store, UIEventSource} from "../../Logic/UIEventSource";
import {InputElement} from "../Input/InputElement";
import {SvgToPdf, SvgToPdfOptions} from "../../Utils/svgToPdf";
import {FixedInputElement} from "../Input/FixedInputElement";
import {FixedUiElement} from "../Base/FixedUiElement";
import FileSelectorButton from "../Input/FileSelectorButton";
import InputElementMap from "../Input/InputElementMap";
import {RadioButton} from "../Input/RadioButton";
import {Utils} from "../../Utils";
import {VariableUiElement} from "../Base/VariableUIElement";
import Loading from "../Base/Loading";
import BaseUIElement from "../BaseUIElement";
import Img from "../Base/Img";
import Title from "../Base/Title";
import {CheckBox} from "../Input/Checkboxes";
import Minimap from "../Base/Minimap";
import SearchAndGo from "./SearchAndGo";
import Toggle from "../Input/Toggle";
import List from "../Base/List";
import LeftIndex from "../Base/LeftIndex";
import Constants from "../../Models/Constants";
import Toggleable from "../Base/Toggleable";
import Lazy from "../Base/Lazy";
import LinkToWeblate from "../Base/LinkToWeblate";
import Link from "../Base/Link";
import {SearchablePillsSelector} from "../Input/SearchableMappingsSelector";
import * as languages from "../../assets/language_translations.json"
import {Translation} from "../i18n/Translation";
class SelectTemplate extends Combine implements FlowStep<{ title: string, pages: string[] }> {
readonly IsValid: Store<boolean>;
readonly Value: Store<{ title: string, pages: string[] }>;
constructor() {
const elements: InputElement<{ templateName: string, pages: string[] }>[] = []
for (const templateName in SvgToPdf.templates) {
const template = SvgToPdf.templates[templateName]
elements.push(new FixedInputElement(
new Combine([new FixedUiElement(templateName).SetClass("font-bold pr-2"),
template.description
])
, new UIEventSource({templateName, pages: template.pages})))
}
const file = new FileSelectorButton(new FixedUiElement("Select an svg image which acts as template"), {
acceptType: "image/svg+xml",
allowMultiple: true
})
const fileMapped = new InputElementMap<FileList, { templateName: string, pages: string[], fromFile: true }>(file, (x0, x1) => x0 === x1,
(filelist) => {
if (filelist === undefined) {
return undefined;
}
const pages = []
let templateName: string = undefined;
for (const file of Array.from(filelist)) {
if (templateName == undefined) {
templateName = file.name.substring(file.name.lastIndexOf("/") + 1)
templateName = templateName.substring(0, templateName.lastIndexOf("."))
}
pages.push(file.text())
}
return {
templateName,
pages,
fromFile: true
}
},
_ => undefined
)
elements.push(fileMapped)
const radio = new RadioButton(elements, {selectFirstAsDefault: true})
const loaded: Store<{ success: { title: string, pages: string[] } } | { error: any }> = radio.GetValue().bind(template => {
if (template === undefined) {
return undefined
}
if (template["fromFile"]) {
return UIEventSource.FromPromiseWithErr(Promise.all(template.pages).then(pages => ({
title: template.templateName,
pages
})))
}
const urls = template.pages.map(p => SelectTemplate.ToUrl(p))
const dloadAll: Promise<{ title: string, pages: string[] }> = Promise.all(urls.map(url => Utils.download(url))).then(pages => ({
pages,
title: template.templateName
}))
return UIEventSource.FromPromiseWithErr(dloadAll)
})
const preview = new VariableUiElement(
loaded.map(pages => {
if (pages === undefined) {
return new Loading()
}
if (pages["error"] !== undefined) {
return new FixedUiElement("Loading preview failed: " + pages["err"]).SetClass("alert")
}
const svgs = pages["success"].pages
if (svgs.length === 0) {
return new FixedUiElement("No pages loaded...").SetClass("alert")
}
const els: BaseUIElement[] = []
for (const pageSrc of svgs) {
const el = new Img(pageSrc, true)
.SetClass("w-96 m-2 border-black border-2")
els.push(el)
}
return new Combine(els).SetClass("flex border border-subtle rounded-xl");
})
)
super([
new Title("Select template"),
radio,
new Title("Preview"),
preview
]);
this.Value = loaded.map(l => l === undefined ? undefined : l["success"])
this.IsValid = this.Value.map(v => v !== undefined)
}
public static ToUrl(spec: string) {
if (spec.startsWith("http")) {
return spec
}
let path = window.location.pathname
path = path.substring(0, path.lastIndexOf("/"))
return window.location.protocol + "//" + window.location.host + path + "/" + spec
}
}
class SelectPdfOptions extends Combine implements FlowStep<{ title: string, pages: string[], options: SvgToPdfOptions }> {
readonly IsValid: Store<boolean>;
readonly Value: Store<{ title: string, pages: string[], options: SvgToPdfOptions }>;
constructor(title: string, pages: string[], getFreeDiv: () => string) {
const dummy = new CheckBox("Don't add data to the map (to quickly preview the PDF)", false)
const overrideMapLocation = new CheckBox("Override map location: use a selected location instead of the location set in the template", false)
const locationInput = Minimap.createMiniMap().SetClass("block w-full")
const searchField = new SearchAndGo({leafletMap: locationInput.leafletMap})
const selectLocation =
new Combine([
new Toggle(new Combine([new Title("Select override location"), searchField]).SetClass("flex"), undefined, overrideMapLocation.GetValue()),
new Toggle(locationInput.SetStyle("height: 20rem"), undefined, overrideMapLocation.GetValue()).SetStyle("height: 20rem")
]).SetClass("block").SetStyle("height: 25rem")
super([new Title("Select options"),
dummy,
overrideMapLocation,
selectLocation
]);
this.Value = dummy.GetValue().map((disableMaps) => {
return {
pages,
title,
options: <SvgToPdfOptions>{
disableMaps,
getFreeDiv,
overrideLocation: overrideMapLocation.GetValue().data ? locationInput.location.data : undefined
}
}
}, [overrideMapLocation.GetValue(), locationInput.location])
this.IsValid = new ImmutableStore(true)
}
}
class PreparePdf extends Combine implements FlowStep<{ svgToPdf: SvgToPdf, languages: string[] }> {
readonly IsValid: Store<boolean>;
readonly Value: Store<{ svgToPdf: SvgToPdf, languages: string[] }>;
constructor(title: string, pages: string[], options: SvgToPdfOptions) {
const svgToPdf = new SvgToPdf(title, pages, options)
const languageOptions = [
new FixedInputElement("Nederlands", "nl"),
new FixedInputElement("English", "en")
]
const langs: string[] = Array.from(Object.keys(languages["default"] ?? languages))
console.log("Available languages are:", langs)
const languageSelector = new SearchablePillsSelector(
langs.map(l => ({
show: new Translation(languages[l]),
value: l,
mainTerm: languages[l]
})), {
mode: "select-many"
}
)
const isPrepared = UIEventSource.FromPromiseWithErr(svgToPdf.Prepare())
super([
new Title("Select languages..."),
languageSelector,
new Toggle(
new Loading("Preparing maps..."),
undefined,
isPrepared.map(p => p === undefined)
)
]);
this.Value = isPrepared.map(isPrepped => {
if (isPrepped === undefined) {
return undefined
}
if (isPrepped["success"] !== undefined) {
const svgToPdf = isPrepped["success"]
const langs = languageSelector.GetValue().data
console.log("Languages are", langs)
if (langs.length === 0) {
return undefined
}
return {svgToPdf, languages: langs}
}
return undefined;
}, [languageSelector.GetValue()])
this.IsValid = this.Value.map(v => v !== undefined)
}
}
class InspectStrings extends Toggle implements FlowStep<{ svgToPdf: SvgToPdf, languages: string[] }> {
readonly IsValid: Store<boolean>;
readonly Value: Store<{ svgToPdf: SvgToPdf; languages: string[] }>;
constructor(svgToPdf: SvgToPdf, languages: string[]) {
const didLoadLanguages = UIEventSource.FromPromiseWithErr(svgToPdf.PrepareLanguages(languages)).map(l => l !== undefined && l["success"] !== undefined)
super(new Combine([
new Title("Inspect translation strings"),
...languages.map(l => new Lazy(() => InspectStrings.createOverviewPanel(svgToPdf, l)))
]),
new Loading(),
didLoadLanguages
);
this.Value = new ImmutableStore({svgToPdf, languages})
this.IsValid = didLoadLanguages
}
private static createOverviewPanel(svgToPdf: SvgToPdf, language: string): BaseUIElement {
const elements: BaseUIElement[] = []
let foundTranslations = 0
const allKeys = Array.from(svgToPdf.translationKeys())
for (const translationKey of allKeys) {
let spec = translationKey
if (translationKey.startsWith("layer.")) {
spec = "layers:" + translationKey.substring(6)
} else {
spec = "core:" + translationKey
}
const translated = svgToPdf.getTranslation("$" + translationKey, language, true)
if (translated) {
foundTranslations++
}
const linkToWeblate = new Link(spec, LinkToWeblate.hrefToWeblate(language, spec), true).SetClass("font-bold link-underline")
elements.push(new Combine([
linkToWeblate,
"&nbsp;",
translated ?? new FixedUiElement("No translation found!").SetClass("alert")
]))
}
return new Toggleable(
new Title("Translations for " + language),
new Combine([
`${foundTranslations}/${allKeys.length} of translations are found (${Math.floor(100 * foundTranslations / allKeys.length)}%)`,
"The following keys are used:",
new List(elements)
]),
{closeOnClick: false, height: "15rem"})
}
}
class SavePdf extends Combine {
constructor(svgToPdf: SvgToPdf, languages: string[]) {
super([
new Title("Generating your pdfs..."),
new List(languages.map(lng => new Toggle(
lng + " is done!",
new Loading("Creating pdf for " + lng),
UIEventSource.FromPromiseWithErr(svgToPdf.ConvertSvg(lng).then(() => true))
.map(x => x !== undefined && x["success"] === true)
)))
]);
}
}
export class PdfExportGui extends LeftIndex {
constructor(freeDivId: string) {
let i = 0
const createDiv = (): string => {
const div = document.createElement("div")
div.id = "freediv-" + (i++)
document.getElementById(freeDivId).append(div)
return div.id
}
Constants.defaultOverpassUrls.splice(0, 1)
const {flow, furthestStep, titles} = FlowPanelFactory.start(
new Title("Select template"), new SelectTemplate()
).then(new Title("Select options"), ({title, pages}) => new SelectPdfOptions(title, pages, createDiv))
.then("Generate maps...", ({title, pages, options}) => new PreparePdf(title, pages, options))
.then("Inspect translations", (({svgToPdf, languages}) => new InspectStrings(svgToPdf, languages)))
.finish("Generating...", ({svgToPdf, languages}) => new SavePdf(svgToPdf, languages))
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[] = [
toc
].map((el) => el?.SetClass("pl-4"))
super(leftContents, flow)
}
}