From 9c961d32b3d0d46ace263da253f9f2b3f4d19afc Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 17 Sep 2022 03:24:01 +0200 Subject: [PATCH] Add UI flow to generate flyers --- Logic/UIEventSource.ts | 12 + Logic/Web/QueryParameters.ts | 7 + UI/BigComponents/SearchAndGo.ts | 4 +- UI/Input/Checkboxes.ts | 5 +- UI/Input/LocationInput.ts | 33 +- Utils/svgToPdf.ts | 42 +- assets/templates/MapComplete-flyer.back.svg | 287 +++++++++---- assets/templates/MapComplete-flyer.svg | 430 ++++++++++---------- css/index-tailwind-output.css | 403 +++++------------- test.ts | 215 +++++++++- 10 files changed, 814 insertions(+), 624 deletions(-) diff --git a/Logic/UIEventSource.ts b/Logic/UIEventSource.ts index ab1261fd9..97f312236 100644 --- a/Logic/UIEventSource.ts +++ b/Logic/UIEventSource.ts @@ -678,6 +678,18 @@ export class UIEventSource extends Store { public map(f: (t: T) => J, extraSources: Store[] = []): Store { return new MappedStore(this, f, extraSources, this._callbacks, f(this.data)) } + /** + * Monoidal map which results in a read-only store. 'undefined' is passed 'as is' + * Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)' + */ + public mapD(f: (t: T) => J, extraSources: Store[] = []): Store { + return new MappedStore(this, t => { + if(t === undefined){ + return undefined + } + return f(t) + }, extraSources, this._callbacks, this.data === undefined ? undefined : f(this.data)) + } /** * Two way sync with functions in both directions diff --git a/Logic/Web/QueryParameters.ts b/Logic/Web/QueryParameters.ts index 96903106c..398247281 100644 --- a/Logic/Web/QueryParameters.ts +++ b/Logic/Web/QueryParameters.ts @@ -108,4 +108,11 @@ export class QueryParameters { history.replaceState(null, "", "?" + parts.join("&") + Hash.Current()) } } + + static ClearAll() { + for (const name in QueryParameters.knownSources) { + QueryParameters.knownSources[name].setData("") + } + QueryParameters._wasInitialized.clear() + } } diff --git a/UI/BigComponents/SearchAndGo.ts b/UI/BigComponents/SearchAndGo.ts index 8b01330a1..2418e55e6 100644 --- a/UI/BigComponents/SearchAndGo.ts +++ b/UI/BigComponents/SearchAndGo.ts @@ -10,7 +10,7 @@ import Combine from "../Base/Combine" import Locale from "../i18n/Locale" export default class SearchAndGo extends Combine { - constructor(state: { leafletMap: UIEventSource; selectedElement: UIEventSource }) { + constructor(state: { leafletMap: UIEventSource; selectedElement?: UIEventSource }) { const goButton = Svg.search_ui().SetClass("w-8 h-8 full-rounded border-black float-right") const placeholder = new UIEventSource(Translations.t.general.search.search) @@ -63,7 +63,7 @@ export default class SearchAndGo extends Combine { [bb[0], bb[2]], [bb[1], bb[3]], ] - state.selectedElement.setData(undefined) + state.selectedElement?.setData(undefined) Hash.hash.setData(poi.osm_type + "/" + poi.osm_id) state.leafletMap.data.fitBounds(bounds) placeholder.setData(Translations.t.general.search.search) diff --git a/UI/Input/Checkboxes.ts b/UI/Input/Checkboxes.ts index 108130d6c..94e689935 100644 --- a/UI/Input/Checkboxes.ts +++ b/UI/Input/Checkboxes.ts @@ -3,11 +3,12 @@ import { UIEventSource } from "../../Logic/UIEventSource" import { Utils } from "../../Utils" import BaseUIElement from "../BaseUIElement" import InputElementMap from "./InputElementMap" +import Translations from "../i18n/Translations"; export class CheckBox extends InputElementMap { - constructor(el: BaseUIElement, defaultValue?: boolean) { + constructor(el: (BaseUIElement | string), defaultValue?: boolean) { super( - new CheckBoxes([el]), + new CheckBoxes([Translations.T(el)]), (x0, x1) => x0 === x1, (t) => t.length > 0, (x) => (x ? [0] : []) diff --git a/UI/Input/LocationInput.ts b/UI/Input/LocationInput.ts index 11a928b94..67ce435b7 100644 --- a/UI/Input/LocationInput.ts +++ b/UI/Input/LocationInput.ts @@ -18,6 +18,7 @@ import * as matchpoint from "../../assets/layers/matchpoint/matchpoint.json" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; import FilteredLayer from "../../Models/FilteredLayer"; import {ElementStorage} from "../../Logic/ElementStorage"; +import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; export default class LocationInput extends BaseUIElement @@ -56,16 +57,16 @@ export default class LocationInput readonly allElements: ElementStorage } - constructor(options: { + constructor(options?: { minZoom?: number mapBackground?: UIEventSource snapTo?: UIEventSource<{ feature: any }[]> maxSnapDistance?: number snappedPointTags?: any requiresSnapping?: boolean - centerLocation: UIEventSource + centerLocation?: UIEventSource bounds?: UIEventSource - state: { + state?: { readonly filteredLayers: Store; readonly backgroundLayer: UIEventSource; readonly layoutToUse: LayoutConfig; @@ -74,15 +75,17 @@ export default class LocationInput } }) { super() - this._snapTo = options.snapTo?.map((features) => + this._snapTo = options?.snapTo?.map((features) => features?.filter((feat) => feat.feature.geometry.type !== "Point") ) - this._maxSnapDistance = options.maxSnapDistance - this._centerLocation = options.centerLocation - this._snappedPointTags = options.snappedPointTags - this._bounds = options.bounds - this._minZoom = options.minZoom - this._state = options.state + this._maxSnapDistance = options?.maxSnapDistance + this._centerLocation = options?.centerLocation ?? new UIEventSource({ + lat: 0, lon: 0, zoom: 0 + }) + this._snappedPointTags = options?.snappedPointTags + this._bounds = options?.bounds + this._minZoom = options?.minZoom + this._state = options?.state if (this._snapTo === undefined) { this._value = this._centerLocation } else { @@ -102,7 +105,7 @@ export default class LocationInput this._matching_layer = LocationInput.matchLayer } - this._snappedPoint = options.centerLocation.map( + this._snappedPoint = this._centerLocation.map( (loc) => { if (loc === undefined) { return undefined @@ -139,17 +142,17 @@ export default class LocationInput } if (min === undefined || min.properties.dist * 1000 > self._maxSnapDistance) { - if (options.requiresSnapping) { + if (options?.requiresSnapping) { return undefined } else { return { type: "Feature", - properties: options.snappedPointTags ?? min.properties, + properties: options?.snappedPointTags ?? min.properties, geometry: {type: "Point", coordinates: [loc.lon, loc.lat]}, } } } - min.properties = options.snappedPointTags ?? min.properties + min.properties = options?.snappedPointTags ?? min.properties self.snappedOnto.setData(matchedWay) return min }, @@ -165,7 +168,7 @@ export default class LocationInput } }) } - this.mapBackground = options.mapBackground ?? this._state?.backgroundLayer + this.mapBackground = options?.mapBackground ?? this._state?.backgroundLayer ?? new UIEventSource(AvailableBaseLayers.osmCarto) this.SetClass("block h-full") this.clickLocation = new UIEventSource(undefined) diff --git a/Utils/svgToPdf.ts b/Utils/svgToPdf.ts index 675008f5e..6d989096a 100644 --- a/Utils/svgToPdf.ts +++ b/Utils/svgToPdf.ts @@ -12,6 +12,8 @@ import Translations from "../UI/i18n/Translations"; import {Utils} from "../Utils"; import Locale from "../UI/i18n/Locale"; import Constants from "../Models/Constants"; +import {QueryParameters} from "../Logic/Web/QueryParameters"; +import Hash from "../Logic/Web/Hash"; class SvgToPdfInternals { private readonly doc: jsPDF; @@ -497,6 +499,7 @@ export interface SvgToPdfOptions { disableMaps?: false | true textSubstitutions?: Record, beforePage?: (i: number) => void, + overrideLocation?: {lat: number, lon: number} } @@ -505,7 +508,6 @@ export class SvgToPdfPage { private images: Record = {} private rects: Record = {} public readonly _svgRoot: SVGSVGElement; - public readonly _usedTranslations: Set = new Set() public readonly currentState: Store private readonly importedTranslations: Record = {} private readonly layerTranslations: Record> = {} @@ -566,12 +568,7 @@ export class SvgToPdfPage { if (element.tagName === "tspan" && element.childElementCount == 0) { const specialValues = element.textContent.split(" ").filter(t => t.startsWith("$")) for (let specialValue of specialValues) { - const translationMatch = specialValue.match(/\$([a-zA-Z0-9._-]+)(.*)/) - if (translationMatch !== null) { - this._usedTranslations.add(translationMatch[1]) - } const importMatch = element.textContent.match(/\$import ([a-zA-Z-_0-9.? ]+) as ([a-zA-Z0-9]+)/) - if (importMatch !== null) { const [, pathRaw, as] = importMatch this.importedTranslations[as] = pathRaw @@ -672,11 +669,15 @@ export class SvgToPdfPage { } const zoom = Number(params["zoom"] ?? params["z"] ?? 14); + Hash.hash.setData(undefined) + history.replaceState(null, "", "") + const state = new FeaturePipelineState(layout) + state.locationControl.addCallbackAndRunD(l => console.trace("Location is",l)) state.locationControl.setData({ zoom, - lat: Number(params["lat"] ?? 51.05016), - lon: Number(params["lon"] ?? 3.717842) + lat: this.options?.overrideLocation?.lat ?? Number(params["lat"] ?? 51.05016), + lon: this.options?.overrideLocation?.lon ?? Number(params["lon"] ?? 3.717842) }) const fl = state.filteredLayers.data @@ -738,12 +739,16 @@ export class SvgToPdfPage { textElement.parentElement.removeChild(textElement) } - public async Prepare(language: string) { + public async PrepareLanguage(language: string){ // Always fetch the remote data - it's cached anyway this.layerTranslations[language] = await Utils.downloadJsonCached("https://raw.githubusercontent.com/pietervdvn/MapComplete/develop/langs/layers/" + language + ".json", 24 * 60 * 60 * 1000) const shared_questions = await Utils.downloadJsonCached("https://raw.githubusercontent.com/pietervdvn/MapComplete/develop/langs/shared-questions/" + language + ".json", 24 * 60 * 60 * 1000) this.layerTranslations[language]["shared-questions"] = shared_questions["shared_questions"] + } + + public async Prepare() { + if (this._isPrepared) { return } @@ -778,9 +783,18 @@ export class SvgToPdfPage { export class SvgToPdf { + public static readonly templates : Record= { + flyer_a4:{pages: ["/assets/templates/MapComplete-flyer.svg","/assets/templates/MapComplete-flyer.back.svg"], description: Translations.t.flyer.description} + } + private readonly _pages: SvgToPdfPage[] constructor(pages: string[], options?: SvgToPdfOptions) { + options = options ?? {} + options.textSubstitutions = options.textSubstitutions ?? {} + const mapCount = "" + Array.from(AllKnownLayouts.allKnownLayouts.values()).filter(th => !th.hideFromOverview).length; + options.textSubstitutions["mapCount"] = mapCount + this._pages = pages.map(page => new SvgToPdfPage(page, options)) } @@ -791,8 +805,10 @@ export class SvgToPdf { const height = SvgToPdfInternals.attrNumber(firstPage, "height") const mode = width > height ? "landscape" : "portrait" + await this.Prepare() for (const page of this._pages) { - await page.Prepare(language) + await page.Prepare() + await page.PrepareLanguage(language) } Locale.language.setData(language) @@ -815,4 +831,10 @@ export class SvgToPdf { } + public async Prepare(): Promise { + for (const page of this._pages) { + await page.Prepare() + } + return this + } } diff --git a/assets/templates/MapComplete-flyer.back.svg b/assets/templates/MapComplete-flyer.back.svg index 1b8cc55d4..9f2976afd 100644 --- a/assets/templates/MapComplete-flyer.back.svg +++ b/assets/templates/MapComplete-flyer.back.svg @@ -26,9 +26,9 @@ showgrid="false" showguides="true" inkscape:guide-bbox="true" - inkscape:zoom="1.0430996" - inkscape:cx="836.4494" - inkscape:cy="155.30636" + inkscape:zoom="1.9704628" + inkscape:cx="1058.3808" + inkscape:cy="146.15856" inkscape:window-width="1920" inkscape:window-height="1007" inkscape:window-x="0" @@ -335,14 +335,14 @@ + + + $map(theme:aed,z:14,lat:$map(theme:aed,z:14,lat:51.2098,lon:3.2284) + id="tspan43986">51.2098,lon:3.2284) $flyer.toerisme_vlaanderen + id="tspan43990">$flyer.toerisme_vlaanderen + width="90.0849" + height="80.149857" + x="5.3507514" + y="122.62533" /> $map(theme:toerisme_vlaanderen,layers:none$map(theme:toerisme_vlaandere,layer-charging_station_ebikes:force,lat:n,layers:none,layer-51.02403,lon:5.1, z:10) + id="tspan44004">charging_station_ebikes:force,lat:50.8552,lon:4.3156, z:10) + width="93.815002" + height="59.787514" + x="101.18518" + y="57.08865" /> $map(theme:cyclofix,z:14,lat:51.05016,lon:$map(theme:cyclofix,z:14,lat:51.05016,lon:3.717842,layers:none,layer-3.717842,layers:none,layer-bike_repair_station:true,layer-bike_repair_station:true,layer-drinking_water:true,layer-bike_cafe:true,layer-drinking_water:true,layer-bicycle_tube_vending_machine: true) + y="734.84886" + id="tspan44032">bike_cafe:true,layer-bicycle_tube_vending_machine: true) $map(theme:artwork,z:15,lat:51.2098,lon:$map(theme:artwork,z:15,lat:51.2098,lon:3.2284,background:AGIV) + id="tspan44042">3.2284,background:AGIV) $map(theme:cyclestreets,z:12,lat:51.2098,lon:$map(theme:cyclestreets,3.2284) + id="tspan44050">z:15,lat:51.02802,lon:4.48029, scaling:3) @@ -596,14 +631,14 @@ style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect890);display:inline;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.264848;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">$map(theme:benches,z:14,lat:51.2098,lon:$map(theme:benches,z:14,lat:51.2098,lon:3.2284, layers:none, layer-bench:force) + id="tspan44062">3.2284, layers:none, layer-bench:force) $flyer.aerial + id="tspan44066">$flyer.aerial $flyer.examples + id="tspan44070">$flyer.examples @@ -691,9 +726,103 @@ style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect5917);fill:#000000;fill-opacity:1;stroke:none">$flyer.lines_too + id="tspan44074">$flyer.lines_too + + $map(theme:onwheels,z:18,lat:50.86622,lon:4.35012,layer-governments:false) + $flyer.onwheels + + + + + + + + + + $flyer.cyclofix + + + + + + + + + $flyer.title + id="tspan44096">$flyer.title + @@ -369,8 +373,8 @@ + x="197.45207" + y="20.174683" /> + x="-0.19642605" + y="-0.022700155" /> + x="201.28694" + y="42.436249" /> $flyer.frontParagraph + id="tspan3360">$flyer.frontParagraph $flyer.tagline + id="tspan3364">$flyer.tagline $flyer.title + id="tspan3368">$flyer.title $flyer.whatIsOsm + id="tspan3370">$flyer.whatIsOsm $flyer.mapcomplete.title + id="tspan3374">$flyer.mapcomplete.title $flyer.mapcomplete.intro + id="tspan3378">$flyer.mapcomplete.intro + id="tspan3382"> $list(flyer.mapcomplete.li) + id="tspan3386">$list(flyer.mapcomplete.li) + id="tspan3390"> + id="tspan3394"> + id="tspan3398"> + id="tspan3402"> + id="tspan3406"> + id="tspan3410"> + id="tspan3414"> $flyer.mapcomplete.customize + id="tspan3418">$flyer.mapcomplete.customize + x="267.50586" + y="179.70633" /> $flyer.callToAction$flyer.callToAction + id="tspan3424"> + id="tspan3428"> $flyer.osm + id="tspan3432">$flyer.osm $flyer.editing.title + id="tspan3436">$flyer.editing.title $flyer.editing.intro + id="tspan3440">$flyer.editing.intro + x="3.6189272" + y="57.751057" /> @@ -757,14 +761,14 @@ $nr.title.render + x="5.0875249" + y="64.134544">$nr.title.render $import layer.nature_reserve as nr + id="tspan3444">$import layer.nature_reserve as nr $import layer.nature_reserve.tagRenderings.Dogs? as nrd + id="tspan3448">$import layer.nature_reserve.tagRenderings.Dogs? as nrd $import layer.nature_reserve.tagRenderings.Access tag as nra + id="tspan3452">$import layer.nature_reserve.tagRenderings.Access tag as nra $import layer.nature_reserve.tagRenderings.Surface area as nrsa + id="tspan3456">$import layer.nature_reserve.tagRenderings.Surface area as nrsa $import flyer.fakeui as fui + id="tspan3460">$import flyer.fakeui as fui $import general as g + id="tspan3464">$import general as g $set(_surface:ha, 13.2) + id="tspan3468">$set(_surface:ha, 13.2) + x="5.1848149" + y="65.896461" /> + x="9.2479706" + y="102.74724" /> $image.addPicture + id="tspan3472">$image.addPicture + x="4.7014012" + y="95.61039" /> Pieter Vander Vennet $fui.add_images + id="tspan3476">$fui.add_images $fui.see_images + id="tspan3480">$fui.see_images + x="58.188828" + y="59.032669" /> $fui.wikipedia + id="tspan3484">$fui.wikipedia CC0 $nra.mappings.0.then + id="tspan3488">$nra.mappings.0.then + x="58.677414" + y="123.13071" /> $nrsa.render + id="tspan3492">$nrsa.render $fui.attributes + id="tspan3496">$fui.attributes + transform="translate(-204.4586,7.6866814)"> $fui.edit + id="tspan3500">$fui.edit www.example.org/nature + id="tspan3504">www.example.org/nature + x="58.677414" + y="131.37161" /> + transform="matrix(0.7575687,0.65275544,0.65275544,-0.7575687,-207.76936,48.39017)"> + x="6.8218541" + y="142.10246" /> $g.wikipedia.fromWikipedia + id="tspan3508">$g.wikipedia.fromWikipedia $fui.question + id="tspan3512">$fui.question $general.save + id="tspan3516">$general.save $nrd.question + id="tspan3520">$nrd.question $nrd.mappings.0.then + id="tspan3524">$nrd.mappings.0.then $nrd.mappings.1.then + id="tspan3528">$nrd.mappings.1.then $nrd.mappings.2.then + id="tspan3532">$nrd.mappings.2.then Skip + id="tspan3536">Skip diff --git a/css/index-tailwind-output.css b/css/index-tailwind-output.css index 7efd1c66b..5348e48b1 100644 --- a/css/index-tailwind-output.css +++ b/css/index-tailwind-output.css @@ -728,12 +728,8 @@ video { margin: 0.25rem; } -.m-0 { - margin: 0px; -} - -.m-auto { - margin: auto; +.m-2 { + margin: 0.5rem; } .m-4 { @@ -744,14 +740,14 @@ video { margin: 1.25rem; } -.m-2 { - margin: 0.5rem; -} - .m-0\.5 { margin: 0.125rem; } +.m-0 { + margin: 0px; +} + .m-3 { margin: 0.75rem; } @@ -764,21 +760,6 @@ video { margin: 1px; } -.mx-4 { - margin-left: 1rem; - margin-right: 1rem; -} - -.mx-auto { - margin-left: auto; - margin-right: auto; -} - -.my-auto { - margin-top: auto; - margin-bottom: auto; -} - .my-2 { margin-top: 0.5rem; margin-bottom: 0.5rem; @@ -794,26 +775,6 @@ video { margin-bottom: 0.75rem; } -.mt-20 { - margin-top: 5rem; -} - -.mr-4 { - margin-right: 1rem; -} - -.ml-2 { - margin-left: 0.5rem; -} - -.mt-3 { - margin-top: 0.75rem; -} - -.mr-1 { - margin-right: 0.25rem; -} - .ml-3 { margin-left: 0.75rem; } @@ -842,6 +803,10 @@ video { margin-top: 1.5rem; } +.mr-1 { + margin-right: 0.25rem; +} + .mr-2 { margin-right: 0.5rem; } @@ -858,14 +823,26 @@ video { margin-bottom: 6rem; } +.mr-4 { + margin-right: 1rem; +} + .mb-2 { margin-bottom: 0.5rem; } +.ml-2 { + margin-left: 0.5rem; +} + .ml-12 { margin-left: 3rem; } +.mt-3 { + margin-top: 0.75rem; +} + .mb-10 { margin-bottom: 2.5rem; } @@ -967,22 +944,6 @@ video { height: min-content; } -.h-16 { - height: 4rem; -} - -.h-12 { - height: 3rem; -} - -.h-20 { - height: 5rem; -} - -.h-4 { - height: 1rem; -} - .h-64 { height: 16rem; } @@ -991,10 +952,18 @@ video { height: 2.5rem; } +.h-12 { + height: 3rem; +} + .h-8 { height: 2rem; } +.h-4 { + height: 1rem; +} + .h-1\/2 { height: 50%; } @@ -1019,6 +988,10 @@ video { height: 24rem; } +.h-16 { + height: 4rem; +} + .h-0 { height: 0px; } @@ -1031,10 +1004,6 @@ video { height: 12rem; } -.max-h-4 { - max-height: 1rem; -} - .max-h-7 { max-height: 1.75rem; } @@ -1047,32 +1016,20 @@ video { max-height: 8rem; } -.max-h-8 { - max-height: 2rem; +.max-h-4 { + max-height: 1rem; } -.min-h-screen { - min-height: 100vh; +.max-h-8 { + max-height: 2rem; } .w-full { width: 100%; } -.w-12 { - width: 3rem; -} - -.w-0 { - width: 0px; -} - -.w-20 { - width: 5rem; -} - -.w-4 { - width: 1rem; +.w-96 { + width: 24rem; } .w-24 { @@ -1091,10 +1048,22 @@ video { width: 2.5rem; } +.w-12 { + width: 3rem; +} + .w-8 { width: 2rem; } +.w-4 { + width: 1rem; +} + +.w-0 { + width: 0px; +} + .w-screen { width: 100vw; } @@ -1130,10 +1099,6 @@ video { min-width: min-content; } -.max-w-screen-md { - max-width: 768px; -} - .max-w-full { max-width: 100%; } @@ -1251,10 +1216,6 @@ video { align-content: flex-start; } -.items-start { - align-items: flex-start; -} - .items-end { align-items: flex-end; } @@ -1352,24 +1313,24 @@ video { border-radius: 9999px; } -.rounded { - border-radius: 0.25rem; -} - -.rounded-lg { - border-radius: 0.5rem; +.rounded-xl { + border-radius: 0.75rem; } .rounded-3xl { border-radius: 1.5rem; } +.rounded { + border-radius: 0.25rem; +} + .rounded-md { border-radius: 0.375rem; } -.rounded-xl { - border-radius: 0.75rem; +.rounded-lg { + border-radius: 0.5rem; } .rounded-sm { @@ -1381,14 +1342,14 @@ video { border-bottom-left-radius: 0.25rem; } -.border { - border-width: 1px; -} - .border-2 { border-width: 2px; } +.border { + border-width: 1px; +} + .border-4 { border-width: 4px; } @@ -1405,13 +1366,9 @@ video { border-bottom-width: 1px; } -.border-gray-600 { +.border-black { --tw-border-opacity: 1; - border-color: rgb(75 85 99 / var(--tw-border-opacity)); -} - -.border-transparent { - border-color: transparent; + border-color: rgb(0 0 0 / var(--tw-border-opacity)); } .border-gray-500 { @@ -1419,11 +1376,6 @@ video { border-color: rgb(107 114 128 / var(--tw-border-opacity)); } -.border-black { - --tw-border-opacity: 1; - border-color: rgb(0 0 0 / var(--tw-border-opacity)); -} - .border-gray-400 { --tw-border-opacity: 1; border-color: rgb(156 163 175 / var(--tw-border-opacity)); @@ -1458,15 +1410,6 @@ video { background-color: rgb(255 255 255 / var(--tw-bg-opacity)); } -.bg-indigo-100 { - --tw-bg-opacity: 1; - background-color: rgb(224 231 255 / var(--tw-bg-opacity)); -} - -.bg-transparent { - background-color: transparent; -} - .bg-gray-200 { --tw-bg-opacity: 1; background-color: rgb(229 231 235 / var(--tw-bg-opacity)); @@ -1482,6 +1425,11 @@ video { background-color: rgb(156 163 175 / var(--tw-bg-opacity)); } +.bg-indigo-100 { + --tw-bg-opacity: 1; + background-color: rgb(224 231 255 / var(--tw-bg-opacity)); +} + .bg-black { --tw-bg-opacity: 1; background-color: rgb(0 0 0 / var(--tw-bg-opacity)); @@ -1507,22 +1455,10 @@ video { background-color: rgb(254 202 202 / var(--tw-bg-opacity)); } -.object-cover { - object-fit: cover; -} - .p-3 { padding: 0.75rem; } -.p-1 { - padding: 0.25rem; -} - -.p-0 { - padding: 0px; -} - .p-4 { padding: 1rem; } @@ -1531,25 +1467,18 @@ video { padding: 0.5rem; } +.p-1 { + padding: 0.25rem; +} + +.p-0 { + padding: 0px; +} + .p-0\.5 { padding: 0.125rem; } -.px-10 { - padding-left: 2.5rem; - padding-right: 2.5rem; -} - -.py-6 { - padding-top: 1.5rem; - padding-bottom: 1.5rem; -} - -.px-1 { - padding-left: 0.25rem; - padding-right: 0.25rem; -} - .px-0 { padding-left: 0px; padding-right: 0px; @@ -1560,16 +1489,8 @@ video { padding-right: 1rem; } -.pt-4 { - padding-top: 1rem; -} - -.pr-4 { - padding-right: 1rem; -} - -.pt-5 { - padding-top: 1.25rem; +.pl-4 { + padding-left: 1rem; } .pl-1 { @@ -1584,10 +1505,6 @@ video { padding-bottom: 3rem; } -.pl-4 { - padding-left: 1rem; -} - .pl-2 { padding-left: 0.5rem; } @@ -1624,6 +1541,10 @@ video { padding-right: 0.75rem; } +.pr-4 { + padding-right: 1rem; +} + .pl-3 { padding-left: 0.75rem; } @@ -1661,11 +1582,6 @@ video { line-height: 1.75rem; } -.text-sm { - font-size: 0.875rem; - line-height: 1.25rem; -} - .text-lg { font-size: 1.125rem; line-height: 1.75rem; @@ -1681,6 +1597,11 @@ video { line-height: 2.5rem; } +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + .text-3xl { font-size: 1.875rem; line-height: 2.25rem; @@ -1691,18 +1612,6 @@ video { line-height: 1.5rem; } -.font-light { - font-weight: 300; -} - -.font-medium { - font-weight: 500; -} - -.font-semibold { - font-weight: 600; -} - .font-bold { font-weight: 700; } @@ -1711,6 +1620,10 @@ video { font-weight: 800; } +.font-semibold { + font-weight: 600; +} + .uppercase { text-transform: uppercase; } @@ -1732,16 +1645,16 @@ video { letter-spacing: -0.025em; } +.text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + .text-gray-900 { --tw-text-opacity: 1; color: rgb(17 24 39 / var(--tw-text-opacity)); } -.text-gray-700 { - --tw-text-opacity: 1; - color: rgb(55 65 81 / var(--tw-text-opacity)); -} - .text-gray-800 { --tw-text-opacity: 1; color: rgb(31 41 55 / var(--tw-text-opacity)); @@ -1752,11 +1665,6 @@ video { color: rgb(107 114 128 / var(--tw-text-opacity)); } -.text-white { - --tw-text-opacity: 1; - color: rgb(255 255 255 / var(--tw-text-opacity)); -} - .text-green-600 { --tw-text-opacity: 1; color: rgb(22 163 74 / var(--tw-text-opacity)); @@ -1772,25 +1680,20 @@ video { text-decoration-line: line-through; } -.opacity-0 { - opacity: 0; -} - .opacity-50 { opacity: 0.5; } +.opacity-0 { + opacity: 0; +} + .shadow { --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } -.outline-none { - outline: 2px solid transparent; - outline-offset: 2px; -} - .outline { outline-style: solid; } @@ -2612,31 +2515,11 @@ input { margin-left: 1.5rem; } -.hover\:border-blue-700:hover { - --tw-border-opacity: 1; - border-color: rgb(29 78 216 / var(--tw-border-opacity)); -} - .hover\:bg-indigo-200:hover { --tw-bg-opacity: 1; background-color: rgb(199 210 254 / var(--tw-bg-opacity)); } -.hover\:bg-blue-600:hover { - --tw-bg-opacity: 1; - background-color: rgb(37 99 235 / var(--tw-bg-opacity)); -} - -.hover\:text-white:hover { - --tw-text-opacity: 1; - color: rgb(255 255 255 / var(--tw-text-opacity)); -} - -.hover\:text-blue-600:hover { - --tw-text-opacity: 1; - color: rgb(37 99 235 / var(--tw-text-opacity)); -} - .hover\:text-blue-800:hover { --tw-text-opacity: 1; color: rgb(30 64 175 / var(--tw-text-opacity)); @@ -2657,39 +2540,12 @@ input { color: var(--unsubtle-detail-color-contrast); } -.focus\:text-gray-600:focus { - --tw-text-opacity: 1; - color: rgb(75 85 99 / var(--tw-text-opacity)); -} - -.focus\:outline-none:focus { - outline: 2px solid transparent; - outline-offset: 2px; -} - @media (min-width: 640px) { - .sm\:m-0 { - margin: 0px; - } - - .sm\:mx-0 { - margin-left: 0px; - margin-right: 0px; - } - .sm\:mx-auto { margin-left: auto; margin-right: auto; } - .sm\:mr-10 { - margin-right: 2.5rem; - } - - .sm\:ml-2 { - margin-left: 0.5rem; - } - .sm\:mr-2 { margin-right: 0.5rem; } @@ -2702,34 +2558,18 @@ input { display: flex; } - .sm\:h-32 { - height: 8rem; - } - - .sm\:h-8 { - height: 2rem; - } - .sm\:h-24 { height: 6rem; } - .sm\:w-auto { - width: auto; - } - - .sm\:w-32 { - width: 8rem; - } - - .sm\:w-8 { - width: 2rem; - } - .sm\:w-24 { width: 6rem; } + .sm\:w-auto { + width: auto; + } + .sm\:max-w-sm { max-width: 24rem; } @@ -2786,10 +2626,6 @@ input { padding-left: 0.5rem; } - .sm\:pt-0 { - padding-top: 0px; - } - .sm\:pt-1 { padding-top: 0.25rem; } @@ -2798,11 +2634,6 @@ input { text-align: center; } - .sm\:text-3xl { - font-size: 1.875rem; - line-height: 2.25rem; - } - .sm\:text-xl { font-size: 1.25rem; line-height: 1.75rem; @@ -2832,11 +2663,6 @@ input { margin: 0.5rem; } - .md\:mx-auto { - margin-left: auto; - margin-right: auto; - } - .md\:mt-5 { margin-top: 1.25rem; } @@ -2865,11 +2691,6 @@ input { height: 3rem; } - .md\:w-max { - width: -webkit-max-content; - width: max-content; - } - .md\:w-2\/6 { width: 33.333333%; } @@ -2878,6 +2699,11 @@ input { width: auto; } + .md\:w-max { + width: -webkit-max-content; + width: max-content; + } + .md\:grid-flow-row { grid-auto-flow: row; } @@ -2922,11 +2748,6 @@ input { padding-bottom: 0px; } - .md\:text-base { - font-size: 1rem; - line-height: 1.5rem; - } - .md\:text-2xl { font-size: 1.5rem; line-height: 2rem; diff --git a/test.ts b/test.ts index 2076d400c..69eb61f75 100644 --- a/test.ts +++ b/test.ts @@ -1,11 +1,29 @@ import MinimapImplementation from "./UI/Base/MinimapImplementation"; import {Utils} from "./Utils"; -import {SvgToPdf, SvgToPdfOptions} from "./Utils/svgToPdf"; import {AllKnownLayouts} from "./Customizations/AllKnownLayouts"; import LayerConfig from "./Models/ThemeConfig/LayerConfig"; import Constants from "./Models/Constants"; import {And} from "./Logic/Tags/And"; import {Tag} from "./Logic/Tags/Tag"; +import {FlowPanelFactory, FlowStep} from "./UI/ImportFlow/FlowStep"; +import Title from "./UI/Base/Title"; +import Combine from "./UI/Base/Combine"; +import {ImmutableStore, Store, UIEventSource} from "./Logic/UIEventSource"; +import {RadioButton} from "./UI/Input/RadioButton"; +import {InputElement} from "./UI/Input/InputElement"; +import {FixedInputElement} from "./UI/Input/FixedInputElement"; +import List from "./UI/Base/List"; +import {VariableUiElement} from "./UI/Base/VariableUIElement"; +import BaseUIElement from "./UI/BaseUIElement"; +import LeftIndex from "./UI/Base/LeftIndex"; +import {SvgToPdf, SvgToPdfOptions} from "./Utils/svgToPdf"; +import Img from "./UI/Base/Img"; +import Toggle from "./UI/Input/Toggle"; +import CheckBoxes, {CheckBox} from "./UI/Input/Checkboxes"; +import Loading from "./UI/Base/Loading"; +import Minimap from "./UI/Base/Minimap"; +import {FixedUiElement} from "./UI/Base/FixedUiElement"; +import SearchAndGo from "./UI/BigComponents/SearchAndGo"; MinimapImplementation.initialize() @@ -22,12 +40,9 @@ async function main() { { // Dirty hack! // Make the charging-station layer simpler to allow querying it by overpass - const chargingStationLayer: LayerConfig = AllKnownLayouts.allKnownLayouts.get("toerisme_vlaanderen").layers.find(l => l.id === "charging_station") - chargingStationLayer.filters = [] - const bikechargingStationLayer : LayerConfig = AllKnownLayouts.allKnownLayouts.get("toerisme_vlaanderen").layers.find(l => l.id === "charging_station_ebikes") - - bikechargingStationLayer.source.osmTags = new And([new Tag("amenity","charging_station"), new Tag("bicycle","yes")]) - Constants.defaultOverpassUrls.splice(0,1) // remove overpass-api.de for this run + const bikechargingStationLayer: LayerConfig = AllKnownLayouts.allKnownLayouts.get("toerisme_vlaanderen").layers.find(l => l.id === "charging_station_ebikes") + bikechargingStationLayer.source.osmTags = new And([new Tag("amenity", "charging_station"), new Tag("bicycle", "yes")]) + Constants.defaultOverpassUrls.splice(0, 1) // remove overpass-api.de for this run } @@ -36,15 +51,191 @@ async function main() { const options = { getFreeDiv: createElement, - textSubstitutions: { - mapCount: "" + Array.from(AllKnownLayouts.allKnownLayouts.values()).filter(th => !th.hideFromOverview).length - }, disableMaps: false } - const front = await new SvgToPdf([svg, svgBack], options) + const front = await new SvgToPdf([svg, svgBack], options) await front.ConvertSvg("Flyer-nl.pdf", "nl") await front.ConvertSvg("Flyer-en.pdf", "en") } -main().then(() => console.log("Done!")) +class SelectTemplate extends Combine implements FlowStep { + readonly IsValid: Store; + readonly Value: Store; + + constructor() { + const elements: InputElement<{ pages: string[] }>[] = [] + for (const templateName in SvgToPdf.templates) { + const template = SvgToPdf.templates[templateName] + elements.push(new FixedInputElement(template.description, new UIEventSource(template))) + } + const radio = new RadioButton(elements, {selectFirstAsDefault: true}) + + const loaded: Store<{ success: string[] } | { error: any }> = radio.GetValue().bind(template => { + if (template === undefined) { + return undefined + } + const urls = template.pages.map(p => SelectTemplate.ToUrl(p)) + const dloadAll: Promise = Promise.all(urls.map(url => Utils.download(url))) + + return UIEventSource.FromPromiseWithErr(dloadAll) + }) + const preview = new VariableUiElement( + loaded.map(pages => { + if (pages === undefined) { + return new Loading() + } + if (pages["err"] !== undefined) { + return new FixedUiElement("Loading preview failed: " + pages["err"]).SetClass("alert") + } + const els: BaseUIElement[] = [] + for (const pageSrc of pages["success"]) { + 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 + } + return window.location.protocol + "//" + window.location.host + "/" + spec + } + +} + +class SelectPdfOptions extends Combine implements FlowStep<{ pages: string[], options: SvgToPdfOptions }> { + readonly IsValid: Store; + readonly Value: Store<{ pages: string[], options: SvgToPdfOptions }>; + + constructor(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, + options: { + 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; + readonly Value: Store<{ svgToPdf: SvgToPdf, languages: string[] }>; + + constructor(pages: string[], options: SvgToPdfOptions) { + const svgToPdf = new SvgToPdf(pages, options) + const languageOptions = [ + new FixedInputElement("Nederlands", "nl"), + new FixedInputElement("English", "en") + ] + const languages = new CheckBoxes(languageOptions) + + const isPrepared = UIEventSource.FromPromiseWithErr(svgToPdf.Prepare()) + + super([ + new Title("Select languages..."), + languages, + 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 = languages.GetValue().data.map(i => languageOptions[i].GetValue().data) + return {svgToPdf, languages: langs} + } + return undefined; + }, [languages.GetValue()]) + this.IsValid = this.Value.map(v => v !== undefined) + } + +} + + +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("Template" + "_" + lng + ".pdf", lng).then(() => true)) + .map(x => x !== undefined && x["success"] === true) + ))) + ]); + } +} + +const {flow, furthestStep, titles} = FlowPanelFactory.start( + new Title("Select template"), new SelectTemplate() +).then(new Title("Select options"), (pages) => new SelectPdfOptions(pages, createElement)) + .then("Generate maps...", ({pages, options}) => new PreparePdf(pages, options)) + .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")) + +new LeftIndex(leftContents, flow).AttachTo("maindiv") +// main().then(() => console.log("Done!"))