forked from MapComplete/MapComplete
Refactoring: allow to export the map as PNG
This commit is contained in:
parent
e36e9123f3
commit
7f8969146a
16 changed files with 207 additions and 66 deletions
|
@ -1,5 +1,7 @@
|
||||||
import { Utils } from "../Utils"
|
import { Utils } from "../Utils"
|
||||||
|
|
||||||
|
export type PriviligedLayerType = typeof Constants.priviliged_layers[number]
|
||||||
|
|
||||||
export default class Constants {
|
export default class Constants {
|
||||||
public static vNumber = "0.30.0"
|
public static vNumber = "0.30.0"
|
||||||
|
|
||||||
|
|
|
@ -15,3 +15,7 @@ export interface MapProperties {
|
||||||
|
|
||||||
readonly allowZooming: UIEventSource<true | boolean>
|
readonly allowZooming: UIEventSource<true | boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExportableMap {
|
||||||
|
exportAsPng(): Promise<Blob>
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import LayerConfig from "./ThemeConfig/LayerConfig"
|
||||||
import { UIEventSource } from "../Logic/UIEventSource"
|
import { UIEventSource } from "../Logic/UIEventSource"
|
||||||
import UserRelatedState from "../Logic/State/UserRelatedState"
|
import UserRelatedState from "../Logic/State/UserRelatedState"
|
||||||
import { Utils } from "../Utils"
|
import { Utils } from "../Utils"
|
||||||
|
import { LocalStorageSource } from "../Logic/Web/LocalStorageSource"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates if a menu is open, and if so, which tab is selected;
|
* Indicates if a menu is open, and if so, which tab is selected;
|
||||||
|
@ -11,12 +12,12 @@ import { Utils } from "../Utils"
|
||||||
*/
|
*/
|
||||||
export class MenuState {
|
export class MenuState {
|
||||||
private static readonly _themeviewTabs = ["intro", "filters", "download", "copyright"] as const
|
private static readonly _themeviewTabs = ["intro", "filters", "download", "copyright"] as const
|
||||||
public readonly themeIsOpened = new UIEventSource(true)
|
public readonly themeIsOpened: UIEventSource<boolean>
|
||||||
public readonly themeViewTabIndex: UIEventSource<number>
|
public readonly themeViewTabIndex: UIEventSource<number>
|
||||||
public readonly themeViewTab: UIEventSource<typeof MenuState._themeviewTabs[number]>
|
public readonly themeViewTab: UIEventSource<typeof MenuState._themeviewTabs[number]>
|
||||||
|
|
||||||
private static readonly _menuviewTabs = ["about", "settings", "community", "privacy"] as const
|
private static readonly _menuviewTabs = ["about", "settings", "community", "privacy"] as const
|
||||||
public readonly menuIsOpened = new UIEventSource(false)
|
public readonly menuIsOpened: UIEventSource<boolean>
|
||||||
public readonly menuViewTabIndex: UIEventSource<number>
|
public readonly menuViewTabIndex: UIEventSource<number>
|
||||||
public readonly menuViewTab: UIEventSource<typeof MenuState._menuviewTabs[number]>
|
public readonly menuViewTab: UIEventSource<typeof MenuState._menuviewTabs[number]>
|
||||||
|
|
||||||
|
@ -24,15 +25,20 @@ export class MenuState {
|
||||||
undefined
|
undefined
|
||||||
)
|
)
|
||||||
public highlightedUserSetting: UIEventSource<string> = new UIEventSource<string>(undefined)
|
public highlightedUserSetting: UIEventSource<string> = new UIEventSource<string>(undefined)
|
||||||
constructor() {
|
constructor(themeid: string = "") {
|
||||||
this.themeViewTabIndex = new UIEventSource(0)
|
if (themeid) {
|
||||||
|
themeid += "-"
|
||||||
|
}
|
||||||
|
this.themeIsOpened = LocalStorageSource.GetParsed(themeid + "thememenuisopened", true)
|
||||||
|
this.themeViewTabIndex = LocalStorageSource.GetParsed(themeid + "themeviewtabindex", 0)
|
||||||
this.themeViewTab = this.themeViewTabIndex.sync(
|
this.themeViewTab = this.themeViewTabIndex.sync(
|
||||||
(i) => MenuState._themeviewTabs[i],
|
(i) => MenuState._themeviewTabs[i],
|
||||||
[],
|
[],
|
||||||
(str) => MenuState._themeviewTabs.indexOf(<any>str)
|
(str) => MenuState._themeviewTabs.indexOf(<any>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(
|
this.menuViewTab = this.menuViewTabIndex.sync(
|
||||||
(i) => MenuState._menuviewTabs[i],
|
(i) => MenuState._menuviewTabs[i],
|
||||||
[],
|
[],
|
||||||
|
|
|
@ -424,7 +424,7 @@ export default class LayerConfig extends WithContextLoader {
|
||||||
if (mapRendering === undefined) {
|
if (mapRendering === undefined) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return mapRendering.GetBaseIcon(this.GetBaseTags())
|
return mapRendering.GetBaseIcon(this.GetBaseTags(), { noFullWidth: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
public GetBaseTags(): Record<string, string> {
|
public GetBaseTags(): Record<string, string> {
|
||||||
|
|
|
@ -136,7 +136,10 @@ export default class PointRenderingConfig extends WithContextLoader {
|
||||||
multiSpec: string,
|
multiSpec: string,
|
||||||
rotation: string,
|
rotation: string,
|
||||||
isBadge: boolean,
|
isBadge: boolean,
|
||||||
defaultElement: BaseUIElement = undefined
|
defaultElement: BaseUIElement = undefined,
|
||||||
|
options?: {
|
||||||
|
noFullWidth?: boolean
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
if (multiSpec === undefined) {
|
if (multiSpec === undefined) {
|
||||||
return defaultElement
|
return defaultElement
|
||||||
|
@ -150,11 +153,21 @@ export default class PointRenderingConfig extends WithContextLoader {
|
||||||
if (elements.length === 0) {
|
if (elements.length === 0) {
|
||||||
return defaultElement
|
return defaultElement
|
||||||
} else {
|
} 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<string, string>): BaseUIElement {
|
public GetBaseIcon(
|
||||||
|
tags?: Record<string, string>,
|
||||||
|
options?: {
|
||||||
|
noFullWidth?: boolean
|
||||||
|
}
|
||||||
|
): BaseUIElement {
|
||||||
tags = tags ?? { id: "node/-1" }
|
tags = tags ?? { id: "node/-1" }
|
||||||
let defaultPin: BaseUIElement = undefined
|
let defaultPin: BaseUIElement = undefined
|
||||||
if (this.label === undefined) {
|
if (this.label === undefined) {
|
||||||
|
@ -176,7 +189,7 @@ export default class PointRenderingConfig extends WithContextLoader {
|
||||||
// This is probably already prepared HTML
|
// This is probably already prepared HTML
|
||||||
return new FixedUiElement(Utils.SubstituteKeys(htmlDefs, tags))
|
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<Record<string, string>>): BaseUIElement {
|
public GetSimpleIcon(tags: Store<Record<string, string>>): BaseUIElement {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
WritableFeatureSource,
|
WritableFeatureSource,
|
||||||
} from "../Logic/FeatureSource/FeatureSource"
|
} from "../Logic/FeatureSource/FeatureSource"
|
||||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||||
import { MapProperties } from "./MapProperties"
|
import { ExportableMap, MapProperties } from "./MapProperties"
|
||||||
import LayerState from "../Logic/State/LayerState"
|
import LayerState from "../Logic/State/LayerState"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
||||||
|
@ -63,7 +63,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
|
|
||||||
readonly osmConnection: OsmConnection
|
readonly osmConnection: OsmConnection
|
||||||
readonly selectedElement: UIEventSource<Feature>
|
readonly selectedElement: UIEventSource<Feature>
|
||||||
readonly mapProperties: MapProperties
|
readonly mapProperties: MapProperties & ExportableMap
|
||||||
|
|
||||||
readonly dataIsLoading: Store<boolean> // TODO
|
readonly dataIsLoading: Store<boolean> // TODO
|
||||||
readonly guistate: MenuState
|
readonly guistate: MenuState
|
||||||
|
@ -82,7 +82,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
readonly lastClickObject: WritableFeatureSource
|
readonly lastClickObject: WritableFeatureSource
|
||||||
constructor(layout: LayoutConfig) {
|
constructor(layout: LayoutConfig) {
|
||||||
this.layout = layout
|
this.layout = layout
|
||||||
this.guistate = new MenuState()
|
this.guistate = new MenuState(layout.id)
|
||||||
this.map = new UIEventSource<MlMap>(undefined)
|
this.map = new UIEventSource<MlMap>(undefined)
|
||||||
const initial = new InitialMapPositioning(layout)
|
const initial = new InitialMapPositioning(layout)
|
||||||
this.mapProperties = new MapLibreAdaptor(this.map, initial)
|
this.mapProperties = new MapLibreAdaptor(this.map, initial)
|
||||||
|
|
|
@ -86,12 +86,6 @@ export default class AllDownloads extends ScrollableFullScreen {
|
||||||
state.featureSwitchExportAsPdf
|
state.featureSwitchExportAsPdf
|
||||||
)
|
)
|
||||||
|
|
||||||
const exportPanel = new Toggle(
|
|
||||||
new DownloadPanel(state),
|
|
||||||
undefined,
|
|
||||||
state.featureSwitchEnableExport
|
|
||||||
)
|
|
||||||
|
|
||||||
return new Combine([pdf, exportPanel]).SetClass("flex flex-col")
|
return pdf
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||||
import { Feature, FeatureCollection } from "geojson"
|
import { Feature, FeatureCollection } from "geojson"
|
||||||
import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore"
|
import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore"
|
||||||
import LayerState from "../../Logic/State/LayerState"
|
import LayerState from "../../Logic/State/LayerState"
|
||||||
|
import { PriviligedLayerType } from "../../Models/Constants"
|
||||||
|
|
||||||
export class DownloadPanel extends Toggle {
|
export class DownloadPanel extends Toggle {
|
||||||
constructor(state: SpecialVisualizationState) {
|
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(
|
||||||
|
<PriviligedLayerType>"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([
|
const downloadButtons = new Combine([
|
||||||
new Title(t.title),
|
new Title(t.title),
|
||||||
buttonGeoJson,
|
buttonGeoJson,
|
||||||
buttonCSV,
|
buttonCSV,
|
||||||
buttonSvg,
|
buttonSvg,
|
||||||
|
buttonPng,
|
||||||
includeMetaToggle,
|
includeMetaToggle,
|
||||||
t.licenseInfo.SetClass("link-underline"),
|
t.licenseInfo.SetClass("link-underline"),
|
||||||
]).SetClass("w-full flex flex-col")
|
]).SetClass("w-full flex flex-col")
|
||||||
|
|
|
@ -56,20 +56,10 @@ export default class LeftControls extends Combine {
|
||||||
)
|
)
|
||||||
|
|
||||||
new AllDownloads(guiState.downloadControlIsOpened, state)
|
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")
|
this.SetClass("flex flex-col")
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,14 +4,15 @@ import { Map as MlMap } from "maplibre-gl"
|
||||||
import { RasterLayerPolygon, RasterLayerProperties } from "../../Models/RasterLayers"
|
import { RasterLayerPolygon, RasterLayerProperties } from "../../Models/RasterLayers"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
import { BBox } from "../../Logic/BBox"
|
import { BBox } from "../../Logic/BBox"
|
||||||
import { MapProperties } from "../../Models/MapProperties"
|
import { ExportableMap, MapProperties } from "../../Models/MapProperties"
|
||||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||||
import MaplibreMap from "./MaplibreMap.svelte"
|
import MaplibreMap from "./MaplibreMap.svelte"
|
||||||
|
import html2canvas from "html2canvas"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties`
|
* 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 = [
|
private static maplibre_control_handlers = [
|
||||||
// "scrollZoom",
|
// "scrollZoom",
|
||||||
// "boxZoom",
|
// "boxZoom",
|
||||||
|
@ -125,23 +126,6 @@ export class MapLibreAdaptor implements MapProperties {
|
||||||
this.bounds.addCallbackAndRunD((bounds) => self.setBounds(bounds))
|
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
|
* Convenience constructor
|
||||||
*/
|
*/
|
||||||
|
@ -189,6 +173,113 @@ export class MapLibreAdaptor implements MapProperties {
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async exportAsPng(): Promise<Blob> {
|
||||||
|
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<void>((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<Blob>((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<Blob>((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) {
|
private SetZoom(z: number) {
|
||||||
const map = this._maplibreMap.data
|
const map = this._maplibreMap.data
|
||||||
if (!map || z === undefined) {
|
if (!map || z === undefined) {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
||||||
import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"
|
import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"
|
||||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||||
import { Changes } from "../Logic/Osm/Changes"
|
import { Changes } from "../Logic/Osm/Changes"
|
||||||
import { MapProperties } from "../Models/MapProperties"
|
import { ExportableMap, MapProperties } from "../Models/MapProperties"
|
||||||
import LayerState from "../Logic/State/LayerState"
|
import LayerState from "../Logic/State/LayerState"
|
||||||
import { Feature, Geometry } from "geojson"
|
import { Feature, Geometry } from "geojson"
|
||||||
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
||||||
|
@ -42,7 +42,7 @@ export interface SpecialVisualizationState {
|
||||||
/**
|
/**
|
||||||
* State of the main map
|
* State of the main map
|
||||||
*/
|
*/
|
||||||
readonly mapProperties: MapProperties
|
readonly mapProperties: MapProperties & ExportableMap
|
||||||
|
|
||||||
readonly selectedElement: UIEventSource<Feature>
|
readonly selectedElement: UIEventSource<Feature>
|
||||||
/**
|
/**
|
||||||
|
|
1
Utils.ts
1
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"
|
| "application/vnd.geo+json"
|
||||||
| "{gpx=application/gpx+xml}"
|
| "{gpx=application/gpx+xml}"
|
||||||
| "application/json"
|
| "application/json"
|
||||||
|
| "image/png"
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const element = document.createElement("a")
|
const element = document.createElement("a")
|
||||||
|
|
|
@ -2058,4 +2058,4 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -159,8 +159,10 @@
|
||||||
"download": {
|
"download": {
|
||||||
"downloadAsPdf": "Download a PDF of the current map",
|
"downloadAsPdf": "Download a PDF of the current map",
|
||||||
"downloadAsPdfHelper": "Ideal to print 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",
|
"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",
|
"downloadCSV": "Download visible data as CSV",
|
||||||
"downloadCSVHelper": "Compatible with LibreOffice Calc, Excel, …",
|
"downloadCSVHelper": "Compatible with LibreOffice Calc, Excel, …",
|
||||||
"downloadFeatureAsGeojson": "Download as GeoJSON-file",
|
"downloadFeatureAsGeojson": "Download as GeoJSON-file",
|
||||||
|
|
31
package-lock.json
generated
31
package-lock.json
generated
|
@ -19,6 +19,7 @@
|
||||||
"@turf/distance": "^6.5.0",
|
"@turf/distance": "^6.5.0",
|
||||||
"@turf/length": "^6.5.0",
|
"@turf/length": "^6.5.0",
|
||||||
"@turf/turf": "^6.5.0",
|
"@turf/turf": "^6.5.0",
|
||||||
|
"@types/html2canvas": "^1.0.0",
|
||||||
"@types/showdown": "^2.0.0",
|
"@types/showdown": "^2.0.0",
|
||||||
"chart.js": "^3.8.0",
|
"chart.js": "^3.8.0",
|
||||||
"country-language": "^0.1.7",
|
"country-language": "^0.1.7",
|
||||||
|
@ -29,6 +30,7 @@
|
||||||
"fake-dom": "^1.0.4",
|
"fake-dom": "^1.0.4",
|
||||||
"geojson2svg": "^1.3.3",
|
"geojson2svg": "^1.3.3",
|
||||||
"html-to-markdown": "^1.0.0",
|
"html-to-markdown": "^1.0.0",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"i18next-client": "^1.11.4",
|
"i18next-client": "^1.11.4",
|
||||||
"idb-keyval": "^6.0.3",
|
"idb-keyval": "^6.0.3",
|
||||||
"jest-mock": "^29.4.1",
|
"jest-mock": "^29.4.1",
|
||||||
|
@ -3644,6 +3646,15 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz",
|
||||||
"integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA=="
|
"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": {
|
"node_modules/@types/istanbul-lib-coverage": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
|
||||||
|
@ -4223,7 +4234,6 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6.0"
|
"node": ">= 0.6.0"
|
||||||
}
|
}
|
||||||
|
@ -4876,7 +4886,6 @@
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"utrie": "^1.0.2"
|
"utrie": "^1.0.2"
|
||||||
}
|
}
|
||||||
|
@ -6480,7 +6489,6 @@
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"css-line-break": "^2.1.0",
|
"css-line-break": "^2.1.0",
|
||||||
"text-segmentation": "^1.0.3"
|
"text-segmentation": "^1.0.3"
|
||||||
|
@ -10167,7 +10175,6 @@
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"utrie": "^1.0.2"
|
"utrie": "^1.0.2"
|
||||||
}
|
}
|
||||||
|
@ -11527,7 +11534,6 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"base64-arraybuffer": "^1.0.2"
|
"base64-arraybuffer": "^1.0.2"
|
||||||
}
|
}
|
||||||
|
@ -14804,6 +14810,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz",
|
||||||
"integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA=="
|
"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": {
|
"@types/istanbul-lib-coverage": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
|
||||||
|
@ -15288,8 +15302,7 @@
|
||||||
"base64-arraybuffer": {
|
"base64-arraybuffer": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"base64-js": {
|
"base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
|
@ -15763,7 +15776,6 @@
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"utrie": "^1.0.2"
|
"utrie": "^1.0.2"
|
||||||
}
|
}
|
||||||
|
@ -16991,7 +17003,6 @@
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"css-line-break": "^2.1.0",
|
"css-line-break": "^2.1.0",
|
||||||
"text-segmentation": "^1.0.3"
|
"text-segmentation": "^1.0.3"
|
||||||
|
@ -19717,7 +19728,6 @@
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"utrie": "^1.0.2"
|
"utrie": "^1.0.2"
|
||||||
}
|
}
|
||||||
|
@ -20828,7 +20838,6 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"base64-arraybuffer": "^1.0.2"
|
"base64-arraybuffer": "^1.0.2"
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,6 +71,7 @@
|
||||||
"@turf/distance": "^6.5.0",
|
"@turf/distance": "^6.5.0",
|
||||||
"@turf/length": "^6.5.0",
|
"@turf/length": "^6.5.0",
|
||||||
"@turf/turf": "^6.5.0",
|
"@turf/turf": "^6.5.0",
|
||||||
|
"@types/html2canvas": "^1.0.0",
|
||||||
"@types/showdown": "^2.0.0",
|
"@types/showdown": "^2.0.0",
|
||||||
"chart.js": "^3.8.0",
|
"chart.js": "^3.8.0",
|
||||||
"country-language": "^0.1.7",
|
"country-language": "^0.1.7",
|
||||||
|
@ -81,6 +82,7 @@
|
||||||
"fake-dom": "^1.0.4",
|
"fake-dom": "^1.0.4",
|
||||||
"geojson2svg": "^1.3.3",
|
"geojson2svg": "^1.3.3",
|
||||||
"html-to-markdown": "^1.0.0",
|
"html-to-markdown": "^1.0.0",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"i18next-client": "^1.11.4",
|
"i18next-client": "^1.11.4",
|
||||||
"idb-keyval": "^6.0.3",
|
"idb-keyval": "^6.0.3",
|
||||||
"jest-mock": "^29.4.1",
|
"jest-mock": "^29.4.1",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue