diff --git a/Models/Constants.ts b/Models/Constants.ts index f30672b2b..21bbe9223 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -1,5 +1,7 @@ import { Utils } from "../Utils" +export type PriviligedLayerType = typeof Constants.priviliged_layers[number] + export default class Constants { public static vNumber = "0.30.0" diff --git a/Models/MapProperties.ts b/Models/MapProperties.ts index 6e6da6c1a..4f5403998 100644 --- a/Models/MapProperties.ts +++ b/Models/MapProperties.ts @@ -15,3 +15,7 @@ export interface MapProperties { readonly allowZooming: UIEventSource } + +export interface ExportableMap { + exportAsPng(): Promise +} diff --git a/Models/MenuState.ts b/Models/MenuState.ts index d613b0048..43d314618 100644 --- a/Models/MenuState.ts +++ b/Models/MenuState.ts @@ -2,6 +2,7 @@ import LayerConfig from "./ThemeConfig/LayerConfig" import { UIEventSource } from "../Logic/UIEventSource" import UserRelatedState from "../Logic/State/UserRelatedState" import { Utils } from "../Utils" +import { LocalStorageSource } from "../Logic/Web/LocalStorageSource" /** * Indicates if a menu is open, and if so, which tab is selected; @@ -11,12 +12,12 @@ import { Utils } from "../Utils" */ export class MenuState { private static readonly _themeviewTabs = ["intro", "filters", "download", "copyright"] as const - public readonly themeIsOpened = new UIEventSource(true) + public readonly themeIsOpened: UIEventSource public readonly themeViewTabIndex: UIEventSource public readonly themeViewTab: UIEventSource private static readonly _menuviewTabs = ["about", "settings", "community", "privacy"] as const - public readonly menuIsOpened = new UIEventSource(false) + public readonly menuIsOpened: UIEventSource public readonly menuViewTabIndex: UIEventSource public readonly menuViewTab: UIEventSource @@ -24,15 +25,20 @@ export class MenuState { undefined ) public highlightedUserSetting: UIEventSource = new UIEventSource(undefined) - constructor() { - this.themeViewTabIndex = new UIEventSource(0) + constructor(themeid: string = "") { + if (themeid) { + themeid += "-" + } + this.themeIsOpened = LocalStorageSource.GetParsed(themeid + "thememenuisopened", true) + this.themeViewTabIndex = LocalStorageSource.GetParsed(themeid + "themeviewtabindex", 0) this.themeViewTab = this.themeViewTabIndex.sync( (i) => MenuState._themeviewTabs[i], [], (str) => MenuState._themeviewTabs.indexOf(str) ) - this.menuViewTabIndex = new UIEventSource(1) + this.menuIsOpened = LocalStorageSource.GetParsed(themeid + "menuisopened", false) + this.menuViewTabIndex = LocalStorageSource.GetParsed(themeid + "menuviewtabindex", 0) this.menuViewTab = this.menuViewTabIndex.sync( (i) => MenuState._menuviewTabs[i], [], diff --git a/Models/ThemeConfig/LayerConfig.ts b/Models/ThemeConfig/LayerConfig.ts index a8488b053..13212878c 100644 --- a/Models/ThemeConfig/LayerConfig.ts +++ b/Models/ThemeConfig/LayerConfig.ts @@ -424,7 +424,7 @@ export default class LayerConfig extends WithContextLoader { if (mapRendering === undefined) { return undefined } - return mapRendering.GetBaseIcon(this.GetBaseTags()) + return mapRendering.GetBaseIcon(this.GetBaseTags(), { noFullWidth: true }) } public GetBaseTags(): Record { diff --git a/Models/ThemeConfig/PointRenderingConfig.ts b/Models/ThemeConfig/PointRenderingConfig.ts index bb6c5cce8..0c51b0539 100644 --- a/Models/ThemeConfig/PointRenderingConfig.ts +++ b/Models/ThemeConfig/PointRenderingConfig.ts @@ -136,7 +136,10 @@ export default class PointRenderingConfig extends WithContextLoader { multiSpec: string, rotation: string, isBadge: boolean, - defaultElement: BaseUIElement = undefined + defaultElement: BaseUIElement = undefined, + options?: { + noFullWidth?: boolean + } ) { if (multiSpec === undefined) { return defaultElement @@ -150,11 +153,21 @@ export default class PointRenderingConfig extends WithContextLoader { if (elements.length === 0) { return defaultElement } else { - return new Combine(elements).SetClass("relative block w-full h-full") + const combine = new Combine(elements).SetClass("relative block") + if (options?.noFullWidth) { + return combine + } + combine.SetClass("w-full h-full") + return combine } } - public GetBaseIcon(tags?: Record): BaseUIElement { + public GetBaseIcon( + tags?: Record, + options?: { + noFullWidth?: boolean + } + ): BaseUIElement { tags = tags ?? { id: "node/-1" } let defaultPin: BaseUIElement = undefined if (this.label === undefined) { @@ -176,7 +189,7 @@ export default class PointRenderingConfig extends WithContextLoader { // This is probably already prepared HTML return new FixedUiElement(Utils.SubstituteKeys(htmlDefs, tags)) } - return PointRenderingConfig.FromHtmlMulti(htmlDefs, rotation, false, defaultPin) + return PointRenderingConfig.FromHtmlMulti(htmlDefs, rotation, false, defaultPin, options) } public GetSimpleIcon(tags: Store>): BaseUIElement { diff --git a/Models/ThemeViewState.ts b/Models/ThemeViewState.ts index 7d668fdb7..75b59559d 100644 --- a/Models/ThemeViewState.ts +++ b/Models/ThemeViewState.ts @@ -8,7 +8,7 @@ import { WritableFeatureSource, } from "../Logic/FeatureSource/FeatureSource" import { OsmConnection } from "../Logic/Osm/OsmConnection" -import { MapProperties } from "./MapProperties" +import { ExportableMap, MapProperties } from "./MapProperties" import LayerState from "../Logic/State/LayerState" import { Feature } from "geojson" import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" @@ -63,7 +63,7 @@ export default class ThemeViewState implements SpecialVisualizationState { readonly osmConnection: OsmConnection readonly selectedElement: UIEventSource - readonly mapProperties: MapProperties + readonly mapProperties: MapProperties & ExportableMap readonly dataIsLoading: Store // TODO readonly guistate: MenuState @@ -82,7 +82,7 @@ export default class ThemeViewState implements SpecialVisualizationState { readonly lastClickObject: WritableFeatureSource constructor(layout: LayoutConfig) { this.layout = layout - this.guistate = new MenuState() + this.guistate = new MenuState(layout.id) this.map = new UIEventSource(undefined) const initial = new InitialMapPositioning(layout) this.mapProperties = new MapLibreAdaptor(this.map, initial) diff --git a/UI/BigComponents/AllDownloads.ts b/UI/BigComponents/AllDownloads.ts index 09c2e33df..d8166e2d9 100644 --- a/UI/BigComponents/AllDownloads.ts +++ b/UI/BigComponents/AllDownloads.ts @@ -86,12 +86,6 @@ export default class AllDownloads extends ScrollableFullScreen { state.featureSwitchExportAsPdf ) - const exportPanel = new Toggle( - new DownloadPanel(state), - undefined, - state.featureSwitchEnableExport - ) - return new Combine([pdf, exportPanel]).SetClass("flex flex-col") - } + return pdf } diff --git a/UI/BigComponents/DownloadPanel.ts b/UI/BigComponents/DownloadPanel.ts index d46e0ab03..2cb856c8c 100644 --- a/UI/BigComponents/DownloadPanel.ts +++ b/UI/BigComponents/DownloadPanel.ts @@ -17,6 +17,7 @@ import { SpecialVisualizationState } from "../SpecialVisualization" import { Feature, FeatureCollection } from "geojson" import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore" import LayerState from "../../Logic/State/LayerState" +import { PriviligedLayerType } from "../../Models/Constants" export class DownloadPanel extends Toggle { constructor(state: SpecialVisualizationState) { @@ -86,11 +87,37 @@ export class DownloadPanel extends Toggle { ) }) + const buttonPng = new SubtleButton( + Svg.floppy_ui(), + new Combine([t.downloadAsPng.SetClass("font-bold"), t.downloadAsPngHelper]) + ).OnClickWithLoading(t.exporting, async () => { + const gpsLayer = state.layerState.filteredLayers.get( + "gps_location" + ) + const gpsIsDisplayed = gpsLayer.isDisplayed.data + try { + gpsLayer.isDisplayed.setData(false) + const png = await state.mapProperties.exportAsPng() + Utils.offerContentsAsDownloadableFile( + png, + `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.png`, + { + mimetype: "image/png", + } + ) + } catch (e) { + console.error(e) + } finally { + gpsLayer.isDisplayed.setData(gpsIsDisplayed) + } + }) + const downloadButtons = new Combine([ new Title(t.title), buttonGeoJson, buttonCSV, buttonSvg, + buttonPng, includeMetaToggle, t.licenseInfo.SetClass("link-underline"), ]).SetClass("w-full flex flex-col") diff --git a/UI/BigComponents/LeftControls.ts b/UI/BigComponents/LeftControls.ts index 3faf39937..4021c23d8 100644 --- a/UI/BigComponents/LeftControls.ts +++ b/UI/BigComponents/LeftControls.ts @@ -56,20 +56,10 @@ export default class LeftControls extends Combine { ) new AllDownloads(guiState.downloadControlIsOpened, state) - const toggledDownload = new MapControlButton(Svg.download_svg()).onClick(() => - guiState.downloadControlIsOpened.setData(true) - ) - const downloadButton = new Toggle( - toggledDownload, - undefined, - state.featureSwitchEnableExport.map( - (downloadEnabled) => downloadEnabled || state.featureSwitchExportAsPdf.data, - [state.featureSwitchExportAsPdf] - ) - ) - super([currentViewAction, downloadButton]) + + super([currentViewAction]) this.SetClass("flex flex-col") } diff --git a/UI/Map/MapLibreAdaptor.ts b/UI/Map/MapLibreAdaptor.ts index 74a9aa0f9..4b28bdea7 100644 --- a/UI/Map/MapLibreAdaptor.ts +++ b/UI/Map/MapLibreAdaptor.ts @@ -4,14 +4,15 @@ import { Map as MlMap } from "maplibre-gl" import { RasterLayerPolygon, RasterLayerProperties } from "../../Models/RasterLayers" import { Utils } from "../../Utils" import { BBox } from "../../Logic/BBox" -import { MapProperties } from "../../Models/MapProperties" +import { ExportableMap, MapProperties } from "../../Models/MapProperties" import SvelteUIElement from "../Base/SvelteUIElement" import MaplibreMap from "./MaplibreMap.svelte" +import html2canvas from "html2canvas" /** * The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties` */ -export class MapLibreAdaptor implements MapProperties { +export class MapLibreAdaptor implements MapProperties, ExportableMap { private static maplibre_control_handlers = [ // "scrollZoom", // "boxZoom", @@ -125,23 +126,6 @@ export class MapLibreAdaptor implements MapProperties { this.bounds.addCallbackAndRunD((bounds) => self.setBounds(bounds)) } - private updateStores() { - const map = this._maplibreMap.data - if (map === undefined) { - return - } - const dt = this.location.data - dt.lon = map.getCenter().lng - dt.lat = map.getCenter().lat - this.location.ping() - this.zoom.setData(Math.round(map.getZoom() * 10) / 10) - const bounds = map.getBounds() - const bbox = new BBox([ - [bounds.getEast(), bounds.getNorth()], - [bounds.getWest(), bounds.getSouth()], - ]) - this.bounds.setData(bbox) - } /** * Convenience constructor */ @@ -189,6 +173,113 @@ export class MapLibreAdaptor implements MapProperties { return url } + async exportAsPng(): Promise { + const map = this._maplibreMap.data + if (map === undefined) { + return undefined + } + + function setDPI(canvas, dpi) { + // Set up CSS size. + canvas.style.width = canvas.style.width || canvas.width + "px" + canvas.style.height = canvas.style.height || canvas.height + "px" + + // Resize canvas and scale future draws. + const scaleFactor = dpi / 96 + canvas.width = Math.ceil(canvas.width * scaleFactor) + canvas.height = Math.ceil(canvas.height * scaleFactor) + const ctx = canvas.getContext("2d") + ctx?.scale(scaleFactor, scaleFactor) + } + + // Total hack - see https://stackoverflow.com/questions/42483449/mapbox-gl-js-export-map-to-png-or-pdf + + const drawOn = document.createElement("canvas") + drawOn.width = document.documentElement.clientWidth + drawOn.height = document.documentElement.clientHeight + + setDPI(drawOn, 4 * 96) + + const destinationCtx = drawOn.getContext("2d") + { + // First, we draw the maplibre-map onto the canvas. This does not export markers + // Inspiration by https://github.com/mapbox/mapbox-gl-js/issues/2766 + + const promise = new Promise((resolve) => { + map.once("render", () => { + destinationCtx.drawImage(map.getCanvas(), 0, 0) + resolve() + }) + }) + + while (!map.isStyleLoaded()) { + console.log("Waiting to fully load the style...") + await Utils.waitFor(100) + } + map.triggerRepaint() + await promise + // Reset the canvas width and height + map.resize() + } + { + // now, we draw the markers on top of the map + + /* We use html2canvas for this, but disable the map canvas object itself: + * it cannot deal with this canvas object. + * + * We also have to patch up a few more objects + * */ + const container = map.getCanvasContainer() + const origHeight = container.style.height + const origStyle = map.getCanvas().style.display + try { + map.getCanvas().style.display = "none" + if (!container.style.height) { + container.style.height = document.documentElement.clientHeight + "px" + } + + const markerCanvas: HTMLCanvasElement = await html2canvas( + map.getCanvasContainer(), + { + backgroundColor: "#00000000", + canvas: drawOn, + } + ) + const markers = await new Promise((resolve) => + markerCanvas.toBlob((data) => resolve(data)) + ) + console.log("Markers:", markers, markerCanvas) + // destinationCtx.drawImage(markerCanvas, 0, 0) + } catch (e) { + console.error(e) + } finally { + map.getCanvas().style.display = origStyle + container.style.height = origHeight + } + } + + // At last, we return the actual blob + return new Promise((resolve) => drawOn.toBlob((data) => resolve(data))) + } + + private updateStores() { + const map = this._maplibreMap.data + if (map === undefined) { + return + } + const dt = this.location.data + dt.lon = map.getCenter().lng + dt.lat = map.getCenter().lat + this.location.ping() + this.zoom.setData(Math.round(map.getZoom() * 10) / 10) + const bounds = map.getBounds() + const bbox = new BBox([ + [bounds.getEast(), bounds.getNorth()], + [bounds.getWest(), bounds.getSouth()], + ]) + this.bounds.setData(bbox) + } + private SetZoom(z: number) { const map = this._maplibreMap.data if (!map || z === undefined) { diff --git a/UI/SpecialVisualization.ts b/UI/SpecialVisualization.ts index dd0d6db17..f249dfc10 100644 --- a/UI/SpecialVisualization.ts +++ b/UI/SpecialVisualization.ts @@ -4,7 +4,7 @@ import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource" import { OsmConnection } from "../Logic/Osm/OsmConnection" import { Changes } from "../Logic/Osm/Changes" -import { MapProperties } from "../Models/MapProperties" +import { ExportableMap, MapProperties } from "../Models/MapProperties" import LayerState from "../Logic/State/LayerState" import { Feature, Geometry } from "geojson" import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" @@ -42,7 +42,7 @@ export interface SpecialVisualizationState { /** * State of the main map */ - readonly mapProperties: MapProperties + readonly mapProperties: MapProperties & ExportableMap readonly selectedElement: UIEventSource /** diff --git a/Utils.ts b/Utils.ts index 4086627e3..9917f42ee 100644 --- a/Utils.ts +++ b/Utils.ts @@ -1045,6 +1045,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | "application/vnd.geo+json" | "{gpx=application/gpx+xml}" | "application/json" + | "image/png" } ) { const element = document.createElement("a") diff --git a/assets/tagRenderings/questions.json b/assets/tagRenderings/questions.json index 5b9cc48fd..f3ce9db0c 100644 --- a/assets/tagRenderings/questions.json +++ b/assets/tagRenderings/questions.json @@ -2058,4 +2058,4 @@ } ] } -} +} \ No newline at end of file diff --git a/langs/en.json b/langs/en.json index 3d0f1f673..e7212d651 100644 --- a/langs/en.json +++ b/langs/en.json @@ -159,8 +159,10 @@ "download": { "downloadAsPdf": "Download a PDF of the current map", "downloadAsPdfHelper": "Ideal to print the current map", + "downloadAsPng": "Download as image", + "downloadAsPngHelper": "Ideal to include in reports", "downloadAsSvg": "Download an SVG of the current map", - "downloadAsSvgHelper": "Compatible Inkscape or Adobe Illustrator; will need further processing ", + "downloadAsSvgHelper": "Compatible Inkscape or Adobe Illustrator; will need further processing", "downloadCSV": "Download visible data as CSV", "downloadCSVHelper": "Compatible with LibreOffice Calc, Excel, …", "downloadFeatureAsGeojson": "Download as GeoJSON-file", diff --git a/package-lock.json b/package-lock.json index 6eff6d6d4..ca43ed106 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@turf/distance": "^6.5.0", "@turf/length": "^6.5.0", "@turf/turf": "^6.5.0", + "@types/html2canvas": "^1.0.0", "@types/showdown": "^2.0.0", "chart.js": "^3.8.0", "country-language": "^0.1.7", @@ -29,6 +30,7 @@ "fake-dom": "^1.0.4", "geojson2svg": "^1.3.3", "html-to-markdown": "^1.0.0", + "html2canvas": "^1.4.1", "i18next-client": "^1.11.4", "idb-keyval": "^6.0.3", "jest-mock": "^29.4.1", @@ -3644,6 +3646,15 @@ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" }, + "node_modules/@types/html2canvas": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/html2canvas/-/html2canvas-1.0.0.tgz", + "integrity": "sha512-BJpVf+FIN9UERmzhbtUgpXj6XBZpG67FMgBLLoj9HZKd9XifcCpSV+UnFcwTZfEyun4U/KmCrrVOG7829L589w==", + "deprecated": "This is a stub types definition. html2canvas provides its own type definitions, so you do not need this installed.", + "dependencies": { + "html2canvas": "*" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -4223,7 +4234,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", - "optional": true, "engines": { "node": ">= 0.6.0" } @@ -4876,7 +4886,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", - "optional": true, "dependencies": { "utrie": "^1.0.2" } @@ -6480,7 +6489,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", - "optional": true, "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" @@ -10167,7 +10175,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", - "optional": true, "dependencies": { "utrie": "^1.0.2" } @@ -11527,7 +11534,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", - "optional": true, "dependencies": { "base64-arraybuffer": "^1.0.2" } @@ -14804,6 +14810,14 @@ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" }, + "@types/html2canvas": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/html2canvas/-/html2canvas-1.0.0.tgz", + "integrity": "sha512-BJpVf+FIN9UERmzhbtUgpXj6XBZpG67FMgBLLoj9HZKd9XifcCpSV+UnFcwTZfEyun4U/KmCrrVOG7829L589w==", + "requires": { + "html2canvas": "*" + } + }, "@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -15288,8 +15302,7 @@ "base64-arraybuffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", - "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", - "optional": true + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==" }, "base64-js": { "version": "1.5.1", @@ -15763,7 +15776,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", - "optional": true, "requires": { "utrie": "^1.0.2" } @@ -16991,7 +17003,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", - "optional": true, "requires": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" @@ -19717,7 +19728,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", - "optional": true, "requires": { "utrie": "^1.0.2" } @@ -20828,7 +20838,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", - "optional": true, "requires": { "base64-arraybuffer": "^1.0.2" } diff --git a/package.json b/package.json index 5926bd549..2aff8cc1a 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "@turf/distance": "^6.5.0", "@turf/length": "^6.5.0", "@turf/turf": "^6.5.0", + "@types/html2canvas": "^1.0.0", "@types/showdown": "^2.0.0", "chart.js": "^3.8.0", "country-language": "^0.1.7", @@ -81,6 +82,7 @@ "fake-dom": "^1.0.4", "geojson2svg": "^1.3.3", "html-to-markdown": "^1.0.0", + "html2canvas": "^1.4.1", "i18next-client": "^1.11.4", "idb-keyval": "^6.0.3", "jest-mock": "^29.4.1",