Merge branch 'main' into codex/character-set-mojibake-check

This commit is contained in:
Kari Lintulaakso
2026-05-20 14:14:56 +03:00
committed by GitHub
8 changed files with 455 additions and 10 deletions
@@ -1,5 +1,5 @@
import { useMemo } from 'react'
import { type MRT_ColumnDef } from 'material-react-table'
import { type MRT_ColumnDef, type MRT_RowData, type MRT_TableInstance } from 'material-react-table'
import { CrossSearch, formatDevelopmentalCrownType } from '@/shared/types'
import { TableView } from '../TableView/TableView'
import type { ColumnVisibilityGroup } from '../TableView/TableToolBar'
@@ -12,6 +12,11 @@ import { OccurrenceDwcExportMenuItem } from '@/components/Occurrence/OccurrenceD
import { OccurrenceDwcDpExportMenuItem } from '@/components/Occurrence/OccurrenceDwcDpExportMenuItem'
import { OccurrenceFullDarwinCoreExportMenuItem } from '@/components/Occurrence/OccurrenceFullDarwinCoreExportMenuItem'
import { matchesCountryOrContinent } from '@/shared/validators/countryContinents'
import {
exportOccurrenceMapKml,
exportOccurrenceMapSvg,
getUniqueCrossSearchMapExportLocalities,
} from '@/components/Species/localitySpeciesMapExport'
export const CrossSearchTable = ({ selectorFn }: { selectorFn?: (newObject: CrossSearch) => void }) => {
const { sqlLimit, sqlOffset, sqlColumnFilters, sqlOrderBy } = usePageContext()
@@ -1072,6 +1077,19 @@ export const CrossSearchTable = ({ selectorFn }: { selectorFn?: (newObject: Cros
return !!row.loc_status
}
const getExportLocalities = <T extends MRT_RowData>(table: MRT_TableInstance<T>) => {
const rows = table.getPrePaginationRowModel().rows.map(row => row.original as unknown as CrossSearch)
return getUniqueCrossSearchMapExportLocalities(rows)
}
const kmlExport = <T extends MRT_RowData>(table: MRT_TableInstance<T>) => {
exportOccurrenceMapKml(table, 'occurrences', getExportLocalities)
}
const svgExport = async <T extends MRT_RowData>(table: MRT_TableInstance<T>) => {
await exportOccurrenceMapSvg(table, 'occurrences-map', getExportLocalities)
}
return (
<>
<LocalitiesMap localities={localitiesData} isFetching={localitiesFetching || isFetching} />
@@ -1090,6 +1108,8 @@ export const CrossSearchTable = ({ selectorFn }: { selectorFn?: (newObject: Cros
enableColumnFilterModes={true}
serverSidePagination={true}
isCrossSearchTable={true}
kmlExport={kmlExport}
svgExport={svgExport}
isError={isError}
error={error}
renderExtraExportMenuItems={handleClose => (
@@ -4,6 +4,7 @@ import {
MRT_PaginationState,
MRT_Row,
MRT_RowData,
MRT_TableInstance,
MRT_TableOptions,
MRT_VisibilityState,
type MRT_ColumnDef,
@@ -128,6 +129,8 @@ type DetailTabTableEditProps<T extends MRT_RowData> = {
muiTableBodyRowProps?: MRT_TableOptions<T>['muiTableBodyRowProps']
tableName?: string
tableContainerMaxHeight?: string | number
kmlExport?: (table: MRT_TableInstance<T>) => void | Promise<void>
svgExport?: (table: MRT_TableInstance<T>) => void | Promise<void>
}
type DetailTabTableProps<T extends MRT_RowData> = DetailTabTableReadSelectProps<T> | DetailTabTableEditProps<T>
@@ -199,6 +202,8 @@ const DetailTabEditableTable = <T extends MRT_RowData>({
muiTableBodyRowProps,
tableName = 'table',
tableContainerMaxHeight = '60vh',
kmlExport,
svgExport,
}: DetailTabTableEditProps<T>) => {
const [pagination, setPagination] = useState<MRT_PaginationState>(paginationState ?? defaultEditPagination)
@@ -230,7 +235,9 @@ const DetailTabEditableTable = <T extends MRT_RowData>({
return (
<Box>
{enableTopToolbar && <DetailTabEditableToolbar table={table} tableName={tableName} />}
{enableTopToolbar && (
<DetailTabEditableToolbar table={table} tableName={tableName} kmlExport={kmlExport} svgExport={svgExport} />
)}
<MaterialReactTable table={table} />
</Box>
)
@@ -239,9 +246,13 @@ const DetailTabEditableTable = <T extends MRT_RowData>({
const DetailTabEditableToolbar = <T extends MRT_RowData>({
table,
tableName,
kmlExport,
svgExport,
}: {
table: ReturnType<typeof useMaterialReactTable<T>>
tableName: string
kmlExport?: (table: MRT_TableInstance<T>) => void | Promise<void>
svgExport?: (table: MRT_TableInstance<T>) => void | Promise<void>
}) => {
const user = useUser()
@@ -251,7 +262,13 @@ const DetailTabEditableToolbar = <T extends MRT_RowData>({
<div className="table-top-row">
<Box display="flex" alignItems="center" gap={1}>
<TableHelp showFiltering showSorting showMultiSorting showColumnVisibility showExport />
<TableToolBar<T> table={table} tableName={tableName} hideLeftButtons={true} />
<TableToolBar<T>
table={table}
tableName={tableName}
hideLeftButtons={true}
kmlExport={kmlExport}
svgExport={svgExport}
/>
</Box>
</div>
)
@@ -1,6 +1,6 @@
import { EditDataType, RowState } from '@/shared/types'
import { CircularProgress, Box, Button, Tooltip } from '@mui/material'
import { type MRT_ColumnDef, type MRT_Row, type MRT_RowData } from 'material-react-table'
import { type MRT_ColumnDef, type MRT_Row, type MRT_RowData, type MRT_TableInstance } from 'material-react-table'
import { useDetailContext } from '../Context/DetailContext'
import RemoveCircleOutlineIcon from '@mui/icons-material/RemoveCircleOutline'
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'
@@ -34,6 +34,8 @@ export const EditableTable = <
url,
getDetailPath,
checkRowRestriction,
kmlExport,
svgExport,
}: {
tableData?: Array<T> | null
editTableData?: Array<EditDataType<T>> | null
@@ -47,6 +49,8 @@ export const EditableTable = <
url?: string
getDetailPath?: (row: T) => string
checkRowRestriction?: (row: T) => boolean
kmlExport?: (table: MRT_TableInstance<T>) => void | Promise<void>
svgExport?: (table: MRT_TableInstance<T>) => void | Promise<void>
}) => {
const { editData, setEditData, mode, data, validator, fieldsWithErrors, setFieldsWithErrors } =
useDetailContext<ParentType>()
@@ -172,6 +176,8 @@ export const EditableTable = <
enableSorting={enableAdvancedTableControls}
enableRowActions={!mode.read || Boolean(checkRowRestriction)}
renderRowActions={resolveRenderRowActions()}
kmlExport={kmlExport}
svgExport={svgExport}
muiTableBodyRowProps={({ row }: { row: MRT_Row<T> }) => ({
'data-cy': idFieldName ? `table-row-${String(row.original[idFieldName])}` : undefined,
onClick: () => {
@@ -4,13 +4,18 @@ import { EditingModal } from '@/components/DetailView/common/EditingModal'
import { Grouped } from '@/components/DetailView/common/tabLayoutHelpers'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { Box, TextField } from '@mui/material'
import { MRT_ColumnDef, MRT_Row } from 'material-react-table'
import { MRT_ColumnDef, MRT_Row, MRT_RowData, MRT_TableInstance } from 'material-react-table'
import { useForm } from 'react-hook-form'
import { calculateNormalizedMesowearScore } from '@/shared/utils/mesowear'
import { applyDefaultSpeciesOrdering, hasActiveSortingInSearch } from '@/components/DetailView/common/DetailTabTable'
import { useLocation } from 'react-router-dom'
import { useMemo } from 'react'
import { occurrenceLabels } from '@/constants/occurrenceLabels'
import {
exportOccurrenceMapKml,
exportOccurrenceMapSvg,
getUniqueLocalityOccurrenceMapExportLocalities,
} from '@/components/Species/localitySpeciesMapExport'
const hasMesowearScoreInputs = (row: LocalitySpecies) => {
return (
@@ -215,6 +220,19 @@ export const OccurrencesTab = () => {
return Object.keys(errors).length === 0
}
const getExportLocalities = <T extends MRT_RowData>(table: MRT_TableInstance<T>) => {
const rows = table.getPrePaginationRowModel().rows.map(row => row.original as unknown as LocalitySpecies)
return getUniqueLocalityOccurrenceMapExportLocalities(data, rows)
}
const kmlExport = <T extends MRT_RowData>(table: MRT_TableInstance<T>) => {
exportOccurrenceMapKml(table, 'locality-occurrences', getExportLocalities)
}
const svgExport = async <T extends MRT_RowData>(table: MRT_TableInstance<T>) => {
await exportOccurrenceMapSvg(table, 'locality-occurrences-map', getExportLocalities)
}
const editingModal = (
<EditingModal buttonText={occurrenceLabels.addNewButton} onSave={onSave}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1em' }}>
@@ -238,6 +256,8 @@ export const OccurrencesTab = () => {
idFieldName="species_id"
url="occurrence"
getDetailPath={row => `/occurrence/${row.lid}/${row.species_id}`}
kmlExport={kmlExport}
svgExport={svgExport}
/>
</Grouped>
)
@@ -4,7 +4,7 @@ import { EditingModal } from '@/components/DetailView/common/EditingModal'
import { Grouped } from '@/components/DetailView/common/tabLayoutHelpers'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { Box, TextField } from '@mui/material'
import { MRT_ColumnDef, MRT_Row } from 'material-react-table'
import { MRT_ColumnDef, MRT_Row, MRT_RowData, MRT_TableInstance } from 'material-react-table'
import { matchesCountryOrContinent } from '@/shared/validators/countryContinents'
import { useForm } from 'react-hook-form'
import { calculateNormalizedMesowearScore } from '@/shared/utils/mesowear'
@@ -12,6 +12,11 @@ import { applyDefaultSpeciesOrdering, hasActiveSortingInSearch } from '@/compone
import { useLocation } from 'react-router-dom'
import { useMemo } from 'react'
import { occurrenceLabels } from '@/constants/occurrenceLabels'
import {
exportOccurrenceMapKml,
exportOccurrenceMapSvg,
getUniqueSpeciesLocalityMapExportLocalities,
} from '../localitySpeciesMapExport'
const hasMesowearScoreInputs = (row: SpeciesLocality) => {
return (
@@ -197,6 +202,19 @@ export const LocalitySpeciesTab = () => {
return Object.keys(errors).length === 0
}
const getExportLocalities = <T extends MRT_RowData>(table: MRT_TableInstance<T>) => {
const rows = table.getPrePaginationRowModel().rows.map(row => row.original as unknown as SpeciesLocality)
return getUniqueSpeciesLocalityMapExportLocalities(data, rows)
}
const kmlExport = <T extends MRT_RowData>(table: MRT_TableInstance<T>) => {
exportOccurrenceMapKml(table, 'locality-species', getExportLocalities)
}
const svgExport = async <T extends MRT_RowData>(table: MRT_TableInstance<T>) => {
await exportOccurrenceMapSvg(table, 'locality-species-map', getExportLocalities)
}
const editingModal = (
<EditingModal buttonText={occurrenceLabels.addNewButton} onSave={onSave}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1em' }}>
@@ -224,6 +242,8 @@ export const LocalitySpeciesTab = () => {
url="occurrence"
getDetailPath={row => `/occurrence/${row.lid}/${row.species_id}`}
checkRowRestriction={row => Boolean(row.now_loc?.loc_status)}
kmlExport={kmlExport}
svgExport={svgExport}
/>
</Grouped>
)
@@ -0,0 +1,187 @@
import type { CrossSearch, Locality, LocalityDetailsType, LocalitySpecies, SpeciesLocality } from '@/shared/types'
import { generateKml } from '@/util/kml'
import { currentDateAsString } from '@/shared/currentDateAsString'
import type { MRT_RowData, MRT_TableInstance } from 'material-react-table'
type SpeciesSource = {
species_id?: number | null
genus_name?: string | null
species_name?: string | null
unique_identifier?: string | null
}
export type MapExportLocality = Locality & {
species?: string[]
}
type OccurrenceMapExportRow = {
locality: MapExportLocalitySource | null | undefined
species: SpeciesSource | null
}
type MapExportLocalitySource = {
lid?: unknown
loc_name?: unknown
country?: unknown
dms_lat?: unknown
dms_long?: unknown
dec_lat?: unknown
dec_long?: unknown
max_age?: unknown
min_age?: unknown
bfa_max?: unknown
bfa_min?: unknown
altitude?: unknown
appr_num_spm?: unknown
species?: string[]
}
const toFiniteNumber = (value: unknown): number | null => {
if (value === null || value === undefined || value === '') return null
const asNumber = Number(value)
return Number.isFinite(asNumber) ? asNumber : null
}
const toStringValue = (value: unknown): string => {
if (value === null || value === undefined) return ''
if (typeof value === 'string') return value
if (typeof value === 'number' || typeof value === 'boolean') return `${value}`
return ''
}
const toNullableString = (value: unknown): string | null => {
if (value === null || value === undefined || value === '') return null
if (typeof value === 'string') return value
if (typeof value === 'number' || typeof value === 'boolean') return `${value}`
return null
}
const formatSpeciesName = (species: SpeciesSource): string | null => {
const genusSpecies = [species.genus_name, species.species_name].filter(Boolean).join(' ').trim()
const uniqueIdentifier = species.unique_identifier?.trim()
const name =
uniqueIdentifier && uniqueIdentifier !== '-' ? `${genusSpecies} ${uniqueIdentifier}`.trim() : genusSpecies
if (name) return name
return species.species_id === null || species.species_id === undefined ? null : `species_id ${species.species_id}`
}
const addSpecies = (locality: MapExportLocality, species: SpeciesSource | null) => {
const speciesName = species ? formatSpeciesName(species) : null
if (!speciesName) return
locality.species ??= []
if (!locality.species.includes(speciesName)) {
locality.species.push(speciesName)
}
}
const getUniqueOccurrenceMapExportLocalities = (rows: OccurrenceMapExportRow[]): MapExportLocality[] => {
const localitiesById = new Map<number, MapExportLocality>()
rows.forEach(row => {
const locality = toMapExportLocality(row.locality)
if (!locality) return
const exportLocality = localitiesById.get(locality.lid) ?? locality
addSpecies(exportLocality, row.species)
localitiesById.set(locality.lid, exportLocality)
})
return [...localitiesById.values()]
}
export const toMapExportLocality = (locality: MapExportLocalitySource | null | undefined): MapExportLocality | null => {
if (!locality) return null
const decLat = toFiniteNumber(locality.dec_lat)
const decLong = toFiniteNumber(locality.dec_long)
if (decLat === null || decLong === null) return null
return {
...locality,
lid: Number(locality.lid),
loc_name: toStringValue(locality.loc_name),
country: toStringValue(locality.country),
dms_lat: toStringValue(locality.dms_lat),
dms_long: toStringValue(locality.dms_long),
dec_lat: decLat,
dec_long: decLong,
bfa_max: toNullableString(locality.bfa_max),
bfa_min: toNullableString(locality.bfa_min),
max_age: toFiniteNumber(locality.max_age) ?? 0,
min_age: toFiniteNumber(locality.min_age) ?? 0,
altitude: toFiniteNumber(locality.altitude) ?? 0,
appr_num_spm: toFiniteNumber(locality.appr_num_spm) ?? 0,
} as unknown as MapExportLocality
}
export const getUniqueSpeciesLocalityMapExportLocalities = (
species: SpeciesSource,
rows: SpeciesLocality[]
): MapExportLocality[] =>
getUniqueOccurrenceMapExportLocalities(
rows.map(row => ({
locality: { ...row.now_loc, lid: row.now_loc?.lid ?? row.lid },
species,
}))
)
export const getUniqueLocalityOccurrenceMapExportLocalities = (
locality: LocalityDetailsType,
rows: LocalitySpecies[]
): MapExportLocality[] =>
getUniqueOccurrenceMapExportLocalities(rows.map(row => ({ locality, species: row.com_species })))
export const getUniqueCrossSearchMapExportLocalities = (rows: CrossSearch[]): MapExportLocality[] =>
getUniqueOccurrenceMapExportLocalities(
rows.map(row => ({
locality: {
lid: row.lid_now_loc,
loc_name: row.loc_name,
country: row.country,
dms_lat: row.dms_lat,
dms_long: row.dms_long,
dec_lat: row.dec_lat,
dec_long: row.dec_long,
bfa_max: row.bfa_max,
bfa_min: row.bfa_min,
max_age: row.max_age,
min_age: row.min_age,
altitude: row.altitude,
appr_num_spm: row.appr_num_spm,
},
species: {
species_id: row.species_id_com_species,
genus_name: row.genus_name,
species_name: row.species_name,
unique_identifier: row.unique_identifier,
},
}))
)
const downloadTextFile = (dataString: string, type: string, filename: string) => {
const blob = new Blob([dataString], { type })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
}
export const exportOccurrenceMapKml = <T extends MRT_RowData>(
table: MRT_TableInstance<T>,
filenamePrefix: string,
getLocalities: (table: MRT_TableInstance<T>) => MapExportLocality[]
) => {
downloadTextFile(generateKml(getLocalities(table)), 'text/kml', `${filenamePrefix}-${currentDateAsString()}.kml`)
}
export const exportOccurrenceMapSvg = async <T extends MRT_RowData>(
table: MRT_TableInstance<T>,
filenamePrefix: string,
getLocalities: (table: MRT_TableInstance<T>) => MapExportLocality[]
) => {
const { generateSvg } = await import('@/components/Map/generateSvg')
downloadTextFile(generateSvg(getLocalities(table)), 'image/svg+xml', `${filenamePrefix}-${currentDateAsString()}.svg`)
}
@@ -0,0 +1,163 @@
import { describe, expect, it } from '@jest/globals'
import {
getUniqueCrossSearchMapExportLocalities,
getUniqueLocalityOccurrenceMapExportLocalities,
getUniqueSpeciesLocalityMapExportLocalities,
toMapExportLocality,
} from '@/components/Species/localitySpeciesMapExport'
import { generateKml } from '@/util/kml'
import type { CrossSearch, LocalityDetailsType, LocalitySpecies, SpeciesLocality } from '@/shared/types'
const crossSearch = (overrides: unknown): CrossSearch => overrides as CrossSearch
const speciesLocality = (overrides: unknown): SpeciesLocality => overrides as SpeciesLocality
const localitySpecies = (overrides: unknown): LocalitySpecies => overrides as LocalitySpecies
const locality = (overrides: unknown): LocalityDetailsType => overrides as LocalityDetailsType
describe('locality species map export helpers', () => {
it('maps a species locality row to a locality export row', () => {
const result = toMapExportLocality({
lid: 123,
loc_name: 'Test locality',
country: 'Finland',
dms_lat: '60 00 N',
dms_long: '25 00 E',
dec_lat: '60.5',
dec_long: '25.25',
max_age: '12.5',
min_age: '10.2',
altitude: null,
appr_num_spm: null,
})
expect(result).toMatchObject({
lid: 123,
loc_name: 'Test locality',
country: 'Finland',
dec_lat: 60.5,
dec_long: 25.25,
max_age: 12.5,
min_age: 10.2,
altitude: 0,
appr_num_spm: 0,
})
})
it('deduplicates localities and skips rows without valid coordinates', () => {
const rows = [
speciesLocality({ lid: 1, now_loc: { lid: 1, loc_name: 'A', dec_lat: 1, dec_long: 2 } }),
speciesLocality({ lid: 1, now_loc: { lid: 1, loc_name: 'A duplicate', dec_lat: 1, dec_long: 2 } }),
speciesLocality({ lid: 2, now_loc: { lid: 2, loc_name: 'B', dec_lat: null, dec_long: 2 } }),
speciesLocality({ lid: 3, now_loc: { lid: 3, loc_name: 'C', dec_lat: 3, dec_long: 4 } }),
]
expect(
getUniqueSpeciesLocalityMapExportLocalities(
{ species_id: 100, genus_name: 'Panthera', species_name: 'leo' },
rows
).map(locality => locality.lid)
).toEqual([1, 3])
})
it('uses the parent species name for species locality exports', () => {
const rows = [
speciesLocality({
species_id: 100,
lid: 1,
now_loc: { lid: 1, loc_name: 'A', dec_lat: 1, dec_long: 2 },
}),
speciesLocality({
species_id: 100,
lid: 1,
now_loc: { lid: 1, loc_name: 'A', dec_lat: 1, dec_long: 2 },
}),
]
expect(
getUniqueSpeciesLocalityMapExportLocalities(
{ species_id: 100, genus_name: 'Panthera', species_name: 'leo', unique_identifier: '-' },
rows
)[0]?.species
).toEqual(['Panthera leo'])
})
it('adds species names from occurrence rows for a locality tab export', () => {
const result = getUniqueLocalityOccurrenceMapExportLocalities(
locality({ lid: 1, loc_name: 'A', dec_lat: 1, dec_long: 2 }),
[
localitySpecies({
com_species: { species_id: 100, genus_name: 'Ursus', species_name: 'arctos', unique_identifier: '-' },
}),
localitySpecies({
com_species: { species_id: 101, genus_name: 'Vulpes', species_name: 'vulpes', unique_identifier: null },
}),
]
)
expect(result).toHaveLength(1)
expect(result[0]?.species).toEqual(['Ursus arctos', 'Vulpes vulpes'])
})
it('groups cross-search occurrence rows by locality with species names', () => {
const result = getUniqueCrossSearchMapExportLocalities([
crossSearch({
lid_now_loc: 1,
loc_name: 'A',
country: 'Finland',
dec_lat: 1,
dec_long: 2,
species_id_com_species: 100,
genus_name: 'Ursus',
species_name: 'arctos',
unique_identifier: '-',
}),
crossSearch({
lid_now_loc: 1,
loc_name: 'A',
country: 'Finland',
dec_lat: 1,
dec_long: 2,
species_id_com_species: 101,
genus_name: 'Vulpes',
species_name: 'vulpes',
unique_identifier: null,
}),
crossSearch({
lid_now_loc: 2,
loc_name: 'B',
country: 'Sweden',
dec_lat: null,
dec_long: 2,
species_id_com_species: 102,
genus_name: 'Alces',
species_name: 'alces',
}),
])
expect(result).toHaveLength(1)
expect(result[0]).toMatchObject({ lid: 1, loc_name: 'A', country: 'Finland' })
expect(result[0]?.species).toEqual(['Ursus arctos', 'Vulpes vulpes'])
})
it('writes species lists into KML descriptions when provided', () => {
const exportLocality = toMapExportLocality({
lid: 1,
loc_name: 'A',
country: 'Finland',
dms_lat: '60 00 N',
dms_long: '25 00 E',
dec_lat: 1,
dec_long: 2,
species: ['Ursus arctos', 'Vulpes vulpes'],
})
expect(exportLocality).not.toBeNull()
if (!exportLocality) return
const kml = generateKml([exportLocality])
expect(kml).toContain('<b>Species:</b>')
expect(kml).toContain('<li>Ursus arctos</li>')
expect(kml).toContain('<li>Vulpes vulpes</li>')
})
})
+16 -4
View File
@@ -1,9 +1,13 @@
import { Locality } from '@/shared/types'
type KmlLocality = Locality & {
species?: string[]
}
// Replaces special characters with html entities.
const htmlEncode = (str: string) => {
const el = document.createElement('div')
el.innerText = str
el.textContent = str
return el.innerHTML
}
@@ -26,8 +30,15 @@ const tableRow = (title: string, value: string, indentLevel: number) => {
return tableRowString
}
const speciesTableRow = (species: string[], indentLevel: number) => {
if (species.length === 0) return ''
const speciesListItems = species.map(speciesName => `<li>${htmlEncode(speciesName)}</li>`).join('')
return tableRow('Species:', `<ul>${speciesListItems}</ul>`, indentLevel)
}
// Locality info table that shows up when clicking map marker in Google Earth for example.
const localityInfoTable = (locality: Locality, indentLevel: number = 3) => {
const localityInfoTable = (locality: KmlLocality, indentLevel: number = 3) => {
const indent = '\t'.repeat(indentLevel)
let tableString = `${indent}<![CDATA[\n`
@@ -41,6 +52,7 @@ const localityInfoTable = (locality: Locality, indentLevel: number = 3) => {
`${locality.max_age} Ma (${locality.bfa_max}) - ${locality.min_age} Ma (${locality.bfa_min})`,
indentLevel + 1
)
tableString += speciesTableRow(locality.species ?? [], indentLevel + 1)
tableString += `${indent}</table>\n`
tableString += `${indent}]]>\n`
@@ -48,7 +60,7 @@ const localityInfoTable = (locality: Locality, indentLevel: number = 3) => {
return tableString
}
const generatePlacemarkKml = (locality: Locality) => {
const generatePlacemarkKml = (locality: KmlLocality) => {
let kmlString = `\t<Placemark>\n`
kmlString += `\t\t<name>${htmlEncode(locality.loc_name)}</name>\n`
@@ -66,7 +78,7 @@ const generatePlacemarkKml = (locality: Locality) => {
}
// Returns an XML / KML string containing locality information as map markers, openable in Google Earth for example.
export const generateKml = (localities: Locality[]) => {
export const generateKml = (localities: KmlLocality[]) => {
let kmlString = `<?xml version="1.0" encoding="UTF-8"?>\n`
kmlString +=
`<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" ` +