Skip to content

PB-101 : zoom on GPX extent on file added #626

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@ivanv/vue-collapse-transition": "^1.0.2",
"@mapbox/togeojson": "^0.16.2",
"@popperjs/core": "^2.11.8",
"@turf/bbox": "^6.5.0",
"@turf/boolean-contains": "^6.5.0",
"@turf/boolean-point-in-polygon": "^6.5.0",
"@turf/centroid": "^6.5.0",
Expand Down
20 changes: 17 additions & 3 deletions src/modules/menu/components/advancedTools/ImportFile/utils.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { gpx as gpxToGeoJSON } from '@mapbox/togeojson'
import bbox from '@turf/bbox'

import GPXLayer from '@/api/layers/GPXLayer.class.js'
import KMLLayer from '@/api/layers/KMLLayer.class'
import { OutOfBoundsError } from '@/utils/coordinates/coordinateUtils'
import { getExtentForProjection } from '@/utils/extentUtils.js'
import GPX from '@/utils/GPX'
import { EmptyKMLError, getKmlExtent, getKmlExtentForProjection } from '@/utils/kmlUtils'
import { EmptyGPXError } from '@/utils/gpxUtils.js'
import { EmptyKMLError, getKmlExtent } from '@/utils/kmlUtils'

/**
* Checks if file is KML
Expand Down Expand Up @@ -40,7 +45,7 @@ export function handleFileContent(store, content, source) {
if (!extent) {
throw new EmptyKMLError()
}
const projectedExtent = getKmlExtentForProjection(store.state.position.projection, extent)
const projectedExtent = getExtentForProjection(store.state.position.projection, extent)

if (!projectedExtent) {
throw new OutOfBoundsError(`KML out of projection bounds: ${extent}`)
Expand All @@ -50,8 +55,17 @@ export function handleFileContent(store, content, source) {
} else if (isGpx(content)) {
const gpxParser = new GPX()
const metadata = gpxParser.readMetadata(content)
const parseGpx = new DOMParser().parseFromString(content, 'text/xml')
layer = new GPXLayer(source, true, 1.0, content, metadata)
// TODO : zoom to extent
const extent = bbox(gpxToGeoJSON(parseGpx))
if (!extent) {
throw new EmptyGPXError()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You also need to handle this error here

}
const projectedExtent = getExtentForProjection(store.state.position.projection, extent)
if (!projectedExtent) {
throw new OutOfBoundsError(`GPX out of projection bounds: ${extent}`)
}
store.dispatch('zoomToExtent', extent)
store.dispatch('addLayer', layer)
} else {
throw new Error(`Unsupported file ${source} content`)
Expand Down
5 changes: 3 additions & 2 deletions src/store/modules/layers.store.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import AbstractLayer from '@/api/layers/AbstractLayer.class'
import ExternalGroupOfLayers from '@/api/layers/ExternalGroupOfLayers.class'
import LayerTypes from '@/api/layers/LayerTypes.enum'
import { getKmlExtent, getKmlExtentForProjection, parseKmlName } from '@/utils/kmlUtils'
import { getExtentForProjection } from '@/utils/extentUtils.js'
import { getKmlExtent, parseKmlName } from '@/utils/kmlUtils'
import { ActiveLayerConfig } from '@/utils/layerUtils'
import log from '@/utils/logging'

Expand Down Expand Up @@ -465,7 +466,7 @@ const actions = {
if (!extent) {
updatedLayer.errorKey = 'kml_gpx_file_empty'
updatedLayer.hasError = true
} else if (!getKmlExtentForProjection(rootState.position.projection, extent)) {
} else if (!getExtentForProjection(rootState.position.projection, extent)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just seen that in your previous PR for GPX support it seems that you don't support GPX after a reload as the GPX data will not be set (no store subscriber to get the GPX data from url). This means that you would need to add that support and also need a similar logic in the layer store as here and have a updateGpxLayer() and test here again for the extent being in projection bound like for kml. So we would handle the case were an online GPX is updated and the new version is out of bound.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's something I wanted to do in another PR

updatedLayer.errorKey = 'kml_gpx_file_out_of_bounds'
updatedLayer.hasError = true
}
Expand Down
55 changes: 55 additions & 0 deletions src/utils/__tests__/extentUtils.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { expect } from 'chai'
import proj4 from 'proj4'
import { describe, it } from 'vitest'

import { LV95, WGS84 } from '@/utils/coordinates/coordinateSystems'
import { getExtentForProjection } from '@/utils/extentUtils'

describe('Test extent utils', () => {
describe('reproject and cut extent within projection bounds', () => {
it('handles well wrong inputs and returns null', () => {
expect(getExtentForProjection()).to.be.null
expect(getExtentForProjection(null, null)).to.be.null
expect(getExtentForProjection(0, 0)).to.be.null
expect(getExtentForProjection({}, [])).to.be.null
expect(getExtentForProjection(LV95, [1, 2, 3])).to.be.null
})
it('reproject extent of a single coordinate inside the bounds of the projection', () => {
const singleCoordinate = [8.2, 47.5]
const singleCoordinateInLV95 = proj4(WGS84.epsg, LV95.epsg, singleCoordinate).map(
LV95.roundCoordinateValue
)
const extent = [singleCoordinate, singleCoordinate].flat()
expect(getExtentForProjection(LV95, extent)).to.deep.equal([
singleCoordinateInLV95,
singleCoordinateInLV95,
])
})
it('returns null if a single coordinate outside of bounds is given', () => {
const singleCoordinateOutOfLV95Bounds = [8.2, 40]
const extent = [singleCoordinateOutOfLV95Bounds, singleCoordinateOutOfLV95Bounds].flat()
expect(getExtentForProjection(LV95, extent)).to.be.null
})
it('returns null if the extent given is completely outside of the projection bounds', () => {
const extent = [-5.0, -20.0, -25.0, -45.0]
expect(getExtentForProjection(LV95, extent)).to.be.null
})
it('reproject and cut an extent that is greater than LV95 extent on all sides', () => {
const projectedExtent = getExtentForProjection(LV95, [-2.4, 35, 21.3, 51.7])
expect(projectedExtent).to.deep.equal([LV95.bounds.bottomLeft, LV95.bounds.topRight])
})
it('only gives back the portion of an extent that is within LV95 bounds', () => {
const singleCoordinateInsideLV95 = [7.54, 48.12]
const singleCoordinateInLV95 = proj4(
WGS84.epsg,
LV95.epsg,
singleCoordinateInsideLV95
).map(LV95.roundCoordinateValue)
const overlappingExtent = [0, 0, ...singleCoordinateInsideLV95]
expect(getExtentForProjection(LV95, overlappingExtent)).to.deep.equal([
LV95.bounds.bottomLeft,
singleCoordinateInLV95,
])
})
})
})
19 changes: 19 additions & 0 deletions src/utils/__tests__/gpxUtils.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { expect } from 'chai'
import { describe, it } from 'vitest'

import { LV95 } from '@/utils/coordinates/coordinateSystems.js'
import { parseGpx } from '@/utils/gpxUtils.js'

describe('Test GPX utils', () => {
describe('parseGpx', () => {
it('handles correctly invalid inputs', () => {
expect(parseGpx()).to.be.null
expect(parseGpx(null, null)).to.be.null
expect(parseGpx(0, LV95)).to.be.null
expect(parseGpx([], LV95)).to.be.null
expect(parseGpx({}, LV95)).to.be.null
expect(parseGpx('', LV95)).to.be.null
})
// further testing isn't really necessary as it's using out-of-the-box OL functions
})
})
125 changes: 16 additions & 109 deletions src/utils/__tests__/kmlUtils.spec.js
Original file line number Diff line number Diff line change
@@ -1,106 +1,27 @@
import { expect } from 'chai'
import { describe, it } from 'vitest'

import { LV95 } from '@/utils/coordinates/coordinateSystems'
import { getKmlExtent, getKmlExtentForProjection } from '@/utils/kmlUtils'
import { getKmlExtent } from '@/utils/kmlUtils'

describe('Test KML utils', () => {
describe('get KML Extent', () => {
it('get extent of a single feature', () => {
const content = `<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<Placemark>
<name>Sample Placemark</name>
<description>This is a sample KML Placemark.</description>
<Point>
<coordinates>8.117189,46.852375</coordinates>
</Point>
</Placemark>
</Document>
</kml>
`
const extent = getKmlExtent(content)

expect(extent).to.deep.equal([8.117189, 46.852375, 8.117189, 46.852375])
it('handles correctly invalid inputs', () => {
expect(getKmlExtent()).to.be.null
expect(getKmlExtent(null)).to.be.null
expect(getKmlExtent(0)).to.be.null
expect(getKmlExtent([])).to.be.null
expect(getKmlExtent({})).to.be.null
expect(getKmlExtent('')).to.be.null
})
it('get extent of a single line feature crossing europe', () => {
const content = `<?xml version="1.0" encoding="UTF-8"?>
it('returns null if the KML has no feature', () => {
const emptyDocument = `<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<Placemark>
<name>Line accross europe and switzerland</name>
<description>This is a sample KML Placemark.</description>
<LineString>
<coordinates>
-0.771255570521181,47.49354012712542,0
2.274382135396515,47.72412908565185,0
4.437570870313552,46.75086073673278,0
6.524331142187565,46.85471653404525,0
7.977505534554298,46.45920177484218,0
8.377161172051387,46.87625449359594,0
10.28654975787117,46.54805193225931,0
13.85851000860247,47.33184853266135,0
15.69760034017345,47.60792210418662,0
</coordinates>
</LineString>
</Placemark>
</Document>
</kml>
`
const extent = getKmlExtent(content)

expect(extent).to.deep.equal([
-0.771255570521181, 46.45920177484218, 15.69760034017345, 47.72412908565185,
])
expect(getKmlExtent(emptyDocument)).to.be.null
})
it('get extent of multiples features (marker, line, polygone)', () => {
const content = `<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<Placemark>
<name>My Marker</name>
<Point>
<coordinates>7.659940678339698,46.95427014117109,843.3989330301123</coordinates>
</Point>
</Placemark>
<Placemark>
<name>No title</name>
<Point>
<coordinates>7.84495931692009,46.92430731160568,801.1875341365708</coordinates>
</Point>
</Placemark>
<Placemark>
<name>Polygone sans titre</name>
<Polygon>
<outerBoundaryIs>
<LinearRing>
<coordinates>
7.736857178846723,46.82670125209653,0 8.06124136917296,46.75405886506746,0 8.092263503513564,46.88254009447432,0 7.674984953448975,46.90741897412888,0 7.736857178846723,46.82670125209653,0
</coordinates>
</LinearRing>
</outerBoundaryIs>
</Polygon>
</Placemark>
<Placemark>
<name>This is a line</name>
<LineString>
<coordinates>
7.661326190900879,46.9229765613044,0 7.736317332421581,46.95310606597951,0 7.783350668405761,46.96964910688379,0 7.819080121407933,46.95554156911379,0 7.895270534105141,46.9409704077278,0
</coordinates>
</LineString>
</Placemark>
</Document>
</kml>
`
const extent = getKmlExtent(content)

expect(extent).to.deep.equal([
7.659940678339698, 46.75405886506746, 8.092263503513564, 46.96964910688379,
])
})
})
describe('get KML Extent within projection bounds', () => {
it('get extent of a single feature', () => {
const content = `<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
Expand All @@ -115,13 +36,7 @@ describe('Test KML utils', () => {
</Document>
</kml>
`
const extent = getKmlExtent(content)
const projectedExtent = getKmlExtentForProjection(LV95, extent)

expect(projectedExtent).to.deep.equal([
[2651749.9748876626, 1189249.9890369223],
[2651749.9748876626, 1189249.9890369223],
])
expect(getKmlExtent(content)).to.deep.equal([8.117189, 46.852375, 8.117189, 46.852375])
})
it('get extent of a single line feature crossing europe', () => {
const content = `<?xml version="1.0" encoding="UTF-8"?>
Expand All @@ -147,12 +62,8 @@ describe('Test KML utils', () => {
</Document>
</kml>
`
const extent = getKmlExtent(content)
const projectedExtent = getKmlExtentForProjection(LV95, extent)

expect(projectedExtent).to.deep.equal([
[2423458.972383674, 1147908.7345235168],
[2902899.0399873387, 1293747.491178336],
expect(getKmlExtent(content)).to.deep.equal([
-0.771255570521181, 46.45920177484218, 15.69760034017345, 47.72412908565185,
])
})
it('get extent of multiples features (marker, line, polygone)', () => {
Expand Down Expand Up @@ -194,12 +105,8 @@ describe('Test KML utils', () => {
</Document>
</kml>
`
const extent = getKmlExtent(content)
const projectedExtent = getKmlExtentForProjection(LV95, extent)

expect(projectedExtent).to.deep.equal([
[2616908.846006978, 1178120.7002258834],
[2649740.5472833975, +1202270.7737464283],
expect(getKmlExtent(content)).to.deep.equal([
7.659940678339698, 46.75405886506746, 8.092263503513564, 46.96964910688379,
])
})
})
Expand Down
2 changes: 1 addition & 1 deletion src/utils/coordinates/coordinateUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export function projExtent(fromProj, toProj, extent) {
if (extent.length === 4) {
const topLeft = proj4(fromProj.epsg, toProj.epsg, [extent[0], extent[1]])
const bottomRight = proj4(fromProj.epsg, toProj.epsg, [extent[2], extent[3]])
return [...topLeft, ...bottomRight]
return [...topLeft, ...bottomRight].map(toProj.roundCoordinateValue)
}
return null
}
Expand Down
35 changes: 35 additions & 0 deletions src/utils/extentUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { getIntersection as getExtentIntersection, isEmpty as isExtentEmpty } from 'ol/extent'

import CoordinateSystem from '@/utils/coordinates/CoordinateSystem.class.js'
import { WGS84 } from '@/utils/coordinates/coordinateSystems'
import { normalizeExtent, projExtent } from '@/utils/coordinates/coordinateUtils'
import log from '@/utils/logging'

/**
* Get a flattened extent for the projection bounds.
*
* @param {CoordinateSystem} projection Projection in which to get the extent
* @param {[number, number, number, number]} extent Extent in WGS84 such as `[minx, miny, maxx,
* maxy]`
* @returns {null | [[number, number], [number, number]]} Return null if the extent is out of
* projection bounds or the intersection between the extent and projection bounds. The return
* extent is re-projected to the projection. The output format will be `[[minx, miny], [maxx,
* maxy]]`.
*/
export function getExtentForProjection(projection, extent) {
if (!(projection instanceof CoordinateSystem) || extent?.length !== 4) {
return null
}
const projectionBounds = projection.getBoundsAs(WGS84).flatten
let intersectExtent = getExtentIntersection(projectionBounds, extent)
log.debug(
`Get extent for projection ${projection.epsg}`,
`extent=${extent}`,
`projectionBounds=${projectionBounds}`,
`intersectExtent=${intersectExtent}`
)
if (!isExtentEmpty(intersectExtent)) {
return normalizeExtent(projExtent(WGS84, projection, intersectExtent))
}
return null
}
9 changes: 8 additions & 1 deletion src/utils/gpxUtils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CoordinateSystem from '@/utils/coordinates/CoordinateSystem.class.js'
import { WGS84 } from '@/utils/coordinates/coordinateSystems'
import GPX from '@/utils/GPX'
import { gpxStyle } from '@/utils/styleUtils'
Expand All @@ -7,9 +8,13 @@ import { gpxStyle } from '@/utils/styleUtils'
*
* @param {String} gpxData KML content to parse
* @param {CoordinateSystem} projection Projection to use for the OL Feature
* @returns {ol/Feature[]} List of OL Features
* @returns {ol/Feature[]|null} List of OL Features, or null of the gpxData or projection is
* invalid/empty
*/
export function parseGpx(gpxData, projection) {
if (!gpxData?.length || !(projection instanceof CoordinateSystem)) {
return null
}
const features = new GPX().readFeatures(gpxData, {
dataProjection: WGS84.epsg, // GPX files should always be in WGS84
featureProjection: projection.epsg,
Expand All @@ -19,3 +24,5 @@ export function parseGpx(gpxData, projection) {
})
return features
}

export class EmptyGPXError extends Error {}
Loading