SVG export functionality

This commit is contained in:
Tatu Laras
2025-06-04 10:25:42 +03:00
parent f785f14de8
commit 1b1fdca44f
5 changed files with 77 additions and 15 deletions
@@ -6,6 +6,7 @@ import { TableView } from '../TableView/TableView'
import { useNotify } from '@/hooks/notification'
import { LocalitiesMap } from '../Map/LocalitiesMap'
import { generateKml } from '@/util/kml'
import { generateSvg } from '../Map/generateSvg'
const decimalCount = (num: number) => {
const numAsString = num.toString()
@@ -397,6 +398,17 @@ export const LocalityTable = ({ selectorFn }: { selectorFn?: (newObject: Localit
a.click()
}
const svgExport = <T extends MRT_RowData>(table: MRT_TableInstance<T>) => {
const rowData: Locality[] = table.getPrePaginationRowModel().rows.map(row => row.original as unknown as Locality)
const dataString = generateSvg(rowData)
const blob = new Blob([dataString], { type: 'image/svg+xml' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'map.svg'
a.click()
}
const checkRowRestriction = (row: Locality) => {
return !!row.loc_status
}
@@ -416,6 +428,7 @@ export const LocalityTable = ({ selectorFn }: { selectorFn?: (newObject: Localit
url="locality"
combinedExport={combinedExport}
kmlExport={kmlExport}
svgExport={svgExport}
exportIsLoading={isLoading}
enableColumnFilterModes={true}
/>
@@ -131,7 +131,7 @@ export const LocalitiesMap = ({ localitiesQueryData, localitiesQueryIsFetching }
<div ref={mapRef}></div>
</div>
{!localitiesQueryIsFetching && (
{!localitiesQueryIsFetching && localitiesQueryData && (
<div className="button-row">
<Button variant="contained" startIcon={<MapIcon />} onClick={() => setIsOpen(v => !v)}>
{isOpen ? 'Close' : 'Open'} map
+46 -14
View File
@@ -1,29 +1,61 @@
import { SVG } from '@svgdotjs/svg.js'
import { borders } from './country_borders_WGS84'
import { Locality } from '@/shared/types'
type CoordinateConversion = 'web_mercanter'
interface PixelCoordinates {
x: number
y: number
}
const webMercanterConvert = (lat: number, long: number): PixelCoordinates => {
const zoomLevel = 2
// Converts decimal latitude longitude (degrees) coordinates to pixel coordinates,
// using width as the total map width.
const webMercatorConvert = (lat: number, long: number, width: number): PixelCoordinates => {
const pi = 3.141592653589793
const latRadians = lat * (pi / 180)
const longRadians = long * (pi / 180)
return {
x: (1 / (pi * 2)) * (2 ^ zoomLevel) * (pi + long),
y: (1 / (pi * 2)) * (2 ^ zoomLevel) * (pi - Math.log(Math.tan(pi / 4 + lat / 2))),
x: (1 / (pi * 2)) * (pi + longRadians) * width,
y: (1 / (pi * 2)) * (pi - Math.log(Math.tan(pi / 4 + latRadians / 2))) * width,
}
}
export const generateSvg = (conversion: CoordinateConversion) => {
// Generates and returns an SVG image of localities on a map.
export const generateSvg = (
localities: Locality[],
mapBorderColor = '#000000',
mapFillColor = '#ffffff',
markerBorderColor = '#ff0000',
markerFillColor = '#ffffff',
transparent = true
) => {
// Generate picture
const draw = SVG()
const polygon = draw.polygon('0,0 100,50 50,100').fill('none').stroke({ width: 1 })
const dataString = draw.svg()
const blob = new Blob([dataString], { type: 'text/kml' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'map.svg'
a.click()
const width = 2048
draw.viewbox(0, 0, width, width)
if (!transparent) draw.polygon(`0,0 ${width},0 ${width},${width} 0,${width}`).fill('#ffffff')
borders.forEach(country_border => {
const points: string[] = []
country_border.forEach(coordinate => {
const pixelCoords = webMercatorConvert(coordinate[0], coordinate[1], width)
points.push(`${pixelCoords.x},${pixelCoords.y}`)
})
const polygon = points.join(' ')
draw.polygon(polygon).stroke({ width: 1, color: mapBorderColor }).fill(mapFillColor)
})
localities.forEach(locality => {
const pixelCoords = webMercatorConvert(locality.dec_lat, locality.dec_long, width)
const radius = 3
const topLeftX = pixelCoords.x - radius
const topLeftY = pixelCoords.y - radius
draw.circle(radius).move(topLeftX, topLeftY).stroke({ width: 1, color: markerBorderColor }).fill(markerFillColor)
})
return draw.svg()
}
@@ -13,6 +13,7 @@ export const TableToolBar = <T extends MRT_RowData>({
table,
combinedExport,
kmlExport,
svgExport,
exportIsLoading,
isCrossSearchTable,
selectorFn,
@@ -21,6 +22,7 @@ export const TableToolBar = <T extends MRT_RowData>({
table: MRT_TableInstance<T>
combinedExport?: (lids: number[]) => Promise<void>
kmlExport?: (table: MRT_TableInstance<T>) => void
svgExport?: (table: MRT_TableInstance<T>) => void
exportIsLoading?: boolean
isCrossSearchTable?: boolean
selectorFn?: (id: T) => void
@@ -93,6 +95,18 @@ export const TableToolBar = <T extends MRT_RowData>({
Export KML
</MenuItem>
)}
{svgExport && (
<MenuItem
onClick={() => {
svgExport(table)
handleClose()
}}
>
Export SVG map
</MenuItem>
)}
{combinedExport && (
<Box>
<Tooltip
@@ -39,6 +39,7 @@ export const TableView = <T extends MRT_RowData>({
title,
combinedExport,
kmlExport,
svgExport,
exportIsLoading,
isCrossSearchTable,
enableColumnFilterModes,
@@ -55,6 +56,7 @@ export const TableView = <T extends MRT_RowData>({
url?: string
combinedExport?: (lids: number[]) => Promise<void>
kmlExport?: (table: MRT_TableInstance<T>) => void
svgExport?: (table: MRT_TableInstance<T>) => void
exportIsLoading?: boolean
isCrossSearchTable?: boolean
enableColumnFilterModes?: boolean
@@ -207,6 +209,7 @@ export const TableView = <T extends MRT_RowData>({
table={table}
combinedExport={combinedExport}
kmlExport={kmlExport}
svgExport={svgExport}
exportIsLoading={exportIsLoading}
isCrossSearchTable={isCrossSearchTable}
showNewButton={editRights.new && title != 'Locality-Species-Cross-Search'}