Files
David Straub a3b3123c46 Show KML and GeoJSON files of the selected place on the map (#1524)
* Show KML and GeoJSON files of the selected place on the map

When a place is selected on the map, the KML and GeoJSON media objects
attached to it are drawn as shapes, lines and points, using the styles
in the file where it has them. A place without coordinates stays
selected and the map fits its files.

The files are listed in the layer switcher while the place is selected
and can be switched off; the setting is saved like that of image
overlays. A hidden file is not downloaded.

* Move place map file loading into a controller

PlaceVectorsController owns the KML and GeoJSON files of the selected
place. Each load aborts the previous one and its downloads, so an older
refresh can no longer overwrite newer files, and a refresh keeps files
whose checksum is unchanged.

Only a 404 closes the selection; other errors keep it. Switching on a
file of a place without coordinates fits the map to the files shown.
A file without description is listed by its Gramps ID.
2026-10-12 21:21:43 +02:00

109 lines
3.4 KiB
JavaScript

import {describe, it, expect} from 'vitest'
import {parseVector, vectorBounds, vectorFormat} from '../../src/mapVector.js'
const KML = `<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<Style id="red">
<LineStyle><color>ff0000ff</color><width>3</width></LineStyle>
</Style>
<Placemark>
<name>Parish</name>
<styleUrl>#red</styleUrl>
<Polygon><outerBoundaryIs><LinearRing>
<coordinates>8,49 9,49 9,50 8,49</coordinates>
</LinearRing></outerBoundaryIs></Polygon>
</Placemark>
<Placemark>
<name>Church</name>
<Point><coordinates>8.5,49.5</coordinates></Point>
</Placemark>
</Document>
</kml>`
describe('vectorFormat', () => {
it('recognises KML and GeoJSON mime types', () => {
expect(
vectorFormat({mime: 'application/vnd.google-earth.kml+xml'})
).to.equal('kml')
expect(vectorFormat({mime: 'application/geo+json'})).to.equal('geojson')
})
it('falls back to the file extension', () => {
expect(vectorFormat({mime: 'text/xml', path: 'a/Parish.KML'})).to.equal(
'kml'
)
expect(
vectorFormat({mime: 'application/json', path: 'b/parish.geojson'})
).to.equal('geojson')
})
it('rejects other files, including plain JSON', () => {
expect(vectorFormat({mime: 'application/json', path: 'x.json'})).to.equal(
null
)
expect(vectorFormat({mime: 'image/png', path: 'map.png'})).to.equal(null)
expect(vectorFormat({})).to.equal(null)
})
})
describe('parseVector', () => {
it('converts KML with its styles', () => {
const fc = parseVector(KML, 'kml')
expect(fc.type).to.equal('FeatureCollection')
expect(fc.features.map(f => f.properties.name)).to.deep.equal([
'Parish',
'Church',
])
expect(fc.features[0].geometry.type).to.equal('Polygon')
expect(fc.features[0].properties.stroke).to.equal('#ff0000')
expect(fc.features[1].geometry.coordinates).to.deep.equal([8.5, 49.5])
})
it('wraps a bare feature or geometry', () => {
const geometry = {type: 'Point', coordinates: [1, 2]}
const fromGeometry = parseVector(JSON.stringify(geometry), 'geojson')
expect(fromGeometry.features[0].geometry).to.deep.equal(geometry)
const feature = {type: 'Feature', geometry, properties: {name: 'A'}}
const fromFeature = parseVector(JSON.stringify(feature), 'geojson')
expect(fromFeature.features).to.deep.equal([feature])
})
it('returns null for unreadable files', () => {
expect(parseVector('{not json', 'geojson')).to.equal(null)
expect(parseVector('{"type": "Topology"}', 'geojson')).to.equal(null)
expect(parseVector('<kml><Document>', 'kml')).to.equal(null)
expect(parseVector('', 'gpx')).to.equal(null)
})
})
describe('vectorBounds', () => {
it('spans all features and geometry types', () => {
const fc = parseVector(KML, 'kml')
const other = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'GeometryCollection',
geometries: [{type: 'Point', coordinates: [10, 48]}],
},
properties: {},
},
],
}
expect(vectorBounds([fc, other])).to.deep.equal([
[8, 48],
[10, 50],
])
})
it('returns null without coordinates', () => {
const empty = {type: 'FeatureCollection', features: []}
expect(vectorBounds([empty])).to.equal(null)
expect(vectorBounds([])).to.equal(null)
})
})