-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #156 from Geoportail-Luxembourg/GSLUX-737-export-feat
GSLUX-737: Export drawn feature as gpx, kml and shapefile
- Loading branch information
Showing
16 changed files
with
759 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import { Feature, Map } from 'ol' | ||
import { Point, LineString, Polygon, MultiLineString } from 'ol/geom' | ||
import { ExportFeatureGpx } from './export-feature-gpx' | ||
|
||
describe('ExportFeatureGpx', () => { | ||
let exportFeatureGpx: ExportFeatureGpx | ||
let features: Feature[] | ||
|
||
beforeEach(() => { | ||
exportFeatureGpx = new ExportFeatureGpx(new Map({})) | ||
features = [ | ||
new Feature({ | ||
geometry: new Point([0, 0]), | ||
name: 'Point 1', | ||
}), | ||
new Feature({ | ||
geometry: new LineString([ | ||
[0, 0], | ||
[1, 1], | ||
]), | ||
name: 'Line 1', | ||
}), | ||
new Feature({ | ||
geometry: new Polygon([ | ||
[ | ||
[0, 0], | ||
[1, 1], | ||
[1, 0], | ||
[0, 0], | ||
], | ||
]), | ||
name: 'Polygon 1', | ||
}), | ||
] | ||
|
||
global.URL.createObjectURL = vi.fn(() => 'blob:http://localhost/test') | ||
global.URL.revokeObjectURL = vi.fn() | ||
}) | ||
|
||
describe('#export', () => { | ||
it('should call download with correct parameters', () => { | ||
const downloadSpy = vi.spyOn(exportFeatureGpx, 'download') | ||
const fileName = 'testFile' | ||
|
||
exportFeatureGpx.export(features, fileName, true) | ||
|
||
expect(downloadSpy).toHaveBeenCalledWith( | ||
fileName, | ||
expect.any(String), | ||
'gpx', | ||
'application/gpx' | ||
) | ||
}) | ||
}) | ||
|
||
describe('#prepareFeatures', () => { | ||
it('should prepare features correctly', () => { | ||
const preparedFeatures = exportFeatureGpx.prepareFeatures(features, true) | ||
|
||
expect(preparedFeatures.length).toBeGreaterThan(0) | ||
}) | ||
}) | ||
|
||
describe('#generateContent', () => { | ||
it('should generate GPX content', () => { | ||
const content = exportFeatureGpx.generateContent(features, 'testFile') | ||
expect(content).toBe( | ||
'<gpx xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" version="1.1" creator="OpenLayers"><metadata><name>testFile</name></metadata><wpt lat="0" lon="0"><name>Point 1</name></wpt><rte><name>Line 1</name><rtept lat="0" lon="0"/><rtept lat="0.000008983152838482056" lon="0.000008983152841195214"/></rte></gpx>' | ||
) | ||
}) | ||
}) | ||
|
||
describe('#changePolygonToLine', () => { | ||
it('should convert polygon to line', () => { | ||
const changedFeatures = exportFeatureGpx['changePolygonToLine'](features) | ||
expect(changedFeatures[2].getGeometry()?.getType()).toBe('LineString') | ||
}) | ||
}) | ||
|
||
describe('#changeMultilineToLine', () => { | ||
it('should convert multiline to line', () => { | ||
const multiLineFeature = new Feature({ | ||
geometry: new MultiLineString([ | ||
[ | ||
[0, 0], | ||
[1, 1], | ||
], | ||
[ | ||
[2, 2], | ||
[3, 3], | ||
], | ||
]), | ||
name: 'MultiLine', | ||
}) | ||
|
||
const changedFeatures = exportFeatureGpx['changeMultilineToLine']([ | ||
multiLineFeature, | ||
]) | ||
expect(changedFeatures.length).toBe(2) | ||
expect(changedFeatures[0].getGeometry()?.getType()).toBe('LineString') | ||
}) | ||
}) | ||
|
||
describe('#changeLineToMultiline', () => { | ||
it('should convert line to multiline', () => { | ||
const changedFeatures = | ||
exportFeatureGpx['changeLineToMultiline'](features) | ||
expect(changedFeatures[1].getGeometry()?.getType()).toBe( | ||
'MultiLineString' | ||
) | ||
}) | ||
}) | ||
|
||
describe('#orderFeaturesForGpx', () => { | ||
it('should order features correctly', () => { | ||
const orderedFeatures = exportFeatureGpx['orderFeaturesForGpx'](features) | ||
|
||
expect(orderedFeatures[0].getGeometry()?.getType()).toBe('Point') | ||
expect(orderedFeatures[1].getGeometry()?.getType()).toBe('LineString') | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import { Feature } from 'ol' | ||
import { Geometry, LineString, Polygon } from 'ol/geom' | ||
import MultiLineString from 'ol/geom/MultiLineString' | ||
|
||
import { ExportFeature } from './export-feature' | ||
import { GPX } from './ol-format-gpx' | ||
|
||
export class ExportFeatureGpx extends ExportFeature { | ||
/** | ||
* Export a Gpx file | ||
* @param features The features to export | ||
* @param fileName The file name without the file extension (file extension ".gpx" will be added here) | ||
* @param isTrack True if gpx should export tracks instead of routes (eg. use true for feature export, false for routing export) | ||
*/ | ||
export(features: Feature<Geometry>[], fileName: string, isTrack = false) { | ||
const explodedFeatures = this.prepareFeatures(features, isTrack) | ||
const content = this.generateContent( | ||
explodedFeatures as Feature<Geometry>[], | ||
fileName | ||
) | ||
|
||
this.download(fileName, content, 'gpx', 'application/gpx') | ||
} | ||
|
||
prepareFeatures(features: Feature<Geometry>[], isTrack = false) { | ||
features.forEach(feature => { | ||
const properties = feature.getProperties() | ||
|
||
// NB. not sure if this is used in Lux | ||
// LineString geometries, and tracks from MultiLineString | ||
if ('feature_name' in properties) { | ||
feature.set('name', properties['feature_name'], true) | ||
} | ||
}) | ||
|
||
let explodedFeatures = this.exploseFeatures(features) | ||
if (isTrack) { | ||
explodedFeatures = this.changeLineToMultiline(explodedFeatures) | ||
} else { | ||
explodedFeatures = this.changeMultilineToLine(explodedFeatures) | ||
} | ||
|
||
explodedFeatures = this.changePolygonToLine(explodedFeatures) | ||
|
||
return explodedFeatures | ||
} | ||
|
||
generateContent(features: Feature<Geometry>[], fileName: string) { | ||
return new GPX().writeFeatures(this.orderFeaturesForGpx(features), { | ||
...this.encodeOptions, | ||
...{ metadata: { name: fileName } }, | ||
}) | ||
} | ||
|
||
/** | ||
* Change polygon to lines | ||
*/ | ||
private changePolygonToLine(features: Feature<Geometry>[]) { | ||
return features.map(feature => { | ||
const geometry = feature.getGeometry() | ||
|
||
if (geometry?.getType() === 'Polygon') { | ||
const polygon = feature.getGeometry() | ||
const exteriorRing = | ||
(<Polygon>polygon).getLinearRing(0)?.getCoordinates() || [] | ||
const lineString = new LineString(exteriorRing) | ||
|
||
return this.cloneFeatureWithGeom(feature, lineString) | ||
} | ||
|
||
return feature | ||
}) | ||
} | ||
|
||
/** | ||
* Change each multiline contained in the array into line geometry | ||
*/ | ||
private changeMultilineToLine(features: Feature<Geometry>[]) { | ||
return features.reduce((acc, feature) => { | ||
const geometry = feature.getGeometry() | ||
|
||
if (geometry?.getType() === 'MultiLineString') { | ||
const lines = (<MultiLineString>geometry).getLineStrings() | ||
lines.forEach(line => | ||
acc.push(this.cloneFeatureWithGeom(feature, line)) | ||
) | ||
} else { | ||
acc.push(feature) | ||
} | ||
|
||
return acc | ||
}, [] as Feature<Geometry>[]) | ||
} | ||
|
||
/** | ||
* Change each line contained in the array into multiline geometry | ||
*/ | ||
private changeLineToMultiline(features: Feature<Geometry>[]) { | ||
return features.map(feature => { | ||
const geometry = feature.getGeometry() | ||
|
||
if (geometry?.getType() === 'LineString') { | ||
return this.cloneFeatureWithGeom( | ||
feature, | ||
new MultiLineString([(<LineString>geometry).getCoordinates()]) | ||
) | ||
} | ||
|
||
return feature | ||
}) | ||
} | ||
|
||
/** | ||
* Order the feature to have the right GPX order. | ||
* An optional instance of <meta /> | ||
* An arbitrary number of instances of <wpt /> | ||
* An arbitrary number of instances of <rte /> | ||
* An arbitrary number of instances of <trk /> | ||
* An optional instance of <extensions /> | ||
* @param features The features to order | ||
*/ | ||
private orderFeaturesForGpx(features: Feature<Geometry>[]) { | ||
const points: Feature<Geometry>[] = [] | ||
const lines: Feature<Geometry>[] = [] | ||
const others: Feature<Geometry>[] = [] | ||
|
||
features.forEach(feature => { | ||
const geomType = feature.getGeometry()?.getType() | ||
|
||
switch (geomType) { | ||
case 'Point': | ||
points.push(feature) | ||
break | ||
case 'LineString': | ||
lines.push(feature) | ||
break | ||
default: | ||
others.push(feature) | ||
break | ||
} | ||
}) | ||
|
||
return [...points, ...lines, ...others] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { Feature, Map } from 'ol' | ||
import { Point } from 'ol/geom' | ||
import { ExportFeatureKml } from './export-feature-kml' | ||
|
||
describe('ExportFeatureKml', () => { | ||
let exportFeatureKml: ExportFeatureKml | ||
let features: Feature<Point>[] | ||
|
||
beforeEach(() => { | ||
global.URL.createObjectURL = vi.fn(() => 'blob:http://localhost/test') | ||
global.URL.revokeObjectURL = vi.fn() | ||
|
||
exportFeatureKml = new ExportFeatureKml(new Map({})) | ||
features = [ | ||
new Feature({ | ||
geometry: new Point([0, 0]), | ||
name: 'Point 1', | ||
}), | ||
new Feature({ | ||
geometry: new Point([1, 1]), | ||
name: 'Point 2', | ||
}), | ||
] | ||
}) | ||
|
||
describe('#generateContent', () => { | ||
it('should generate KML content from features', () => { | ||
const content = exportFeatureKml.generateContent(features) | ||
expect(content).toBe( | ||
'<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/kml/2.2 https://developers.google.com/kml/schema/kml22gx.xsd"><Document><Placemark><name>Point 1</name><Point><coordinates>0,0</coordinates></Point></Placemark><Placemark><name>Point 2</name><Point><coordinates>0.000008983152841195214,0.000008983152838482056</coordinates></Point></Placemark></Document></kml>' | ||
) | ||
}) | ||
}) | ||
|
||
describe('#export', () => { | ||
it('should call download method with correct parameters', () => { | ||
const downloadSpy = vi.spyOn(exportFeatureKml, 'download') | ||
const fileName = 'testFile' | ||
|
||
exportFeatureKml.export(features, fileName) | ||
|
||
expect(downloadSpy).toHaveBeenCalledWith( | ||
fileName, | ||
expect.stringContaining('<kml'), | ||
'kml', | ||
'application/vnd.google-earth.kml+xml' | ||
) | ||
}) | ||
}) | ||
}) |
Oops, something went wrong.