Skip to content

Commit

Permalink
Merge pull request #156 from Geoportail-Luxembourg/GSLUX-737-export-feat
Browse files Browse the repository at this point in the history
GSLUX-737: Export drawn feature as gpx, kml and shapefile
  • Loading branch information
AlitaBernachot authored Oct 15, 2024
2 parents 79a8686 + 9e20a7c commit a76b429
Show file tree
Hide file tree
Showing 16 changed files with 759 additions and 5 deletions.
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ VITE_MVTSTYLES_PATH_GET="/getpermalinkstyle"
VITE_MVTSTYLES_PATH_UPLOAD="/uploadpermalinkstyle"
VITE_MVTSTYLES_PATH_DELETE="/deletepermalinkstyle"

VITE_URL_MYMAPS_EXPORT_FILE="/mymaps/exportgpxkml"

# Offline
VITE_OFFLINE_GUTTER=96

Expand Down
2 changes: 2 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ VITE_MVTSTYLES_PATH_GET="/getpermalinkstyle"
VITE_MVTSTYLES_PATH_UPLOAD="/uploadpermalinkstyle"
VITE_MVTSTYLES_PATH_DELETE="/deletepermalinkstyle"

VITE_URL_MYMAPS_EXPORT_FILE="http://localhost:8080/mymaps/exportgpxkml"

# Offline
VITE_OFFLINE_GUTTER=96

Expand Down
2 changes: 2 additions & 0 deletions .env.e2e
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ VITE_MVTSTYLES_PATH_GET="/getpermalinkstyle"
VITE_MVTSTYLES_PATH_UPLOAD="/uploadpermalinkstyle"
VITE_MVTSTYLES_PATH_DELETE="/deletepermalinkstyle"

VITE_URL_MYMAPS_EXPORT_FILE="https://migration.geoportail.lu/mymaps/exportgpxkml"

# Offline
VITE_OFFLINE_GUTTER=96

Expand Down
2 changes: 2 additions & 0 deletions .env.staging
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ VITE_MVTSTYLES_PATH_GET="https://migration.geoportail.lu/getvtstyle"
VITE_MVTSTYLES_PATH_UPLOAD="https://migration.geoportail.lu/uploadvtstyle"
VITE_MVTSTYLES_PATH_DELETE="https://migration.geoportail.lu/deletevtstyle"

VITE_URL_MYMAPS_EXPORT_FILE="https://migration.geoportail.lu/mymaps/exportgpxkml"

# Offline
VITE_OFFLINE_GUTTER=96

Expand Down
22 changes: 18 additions & 4 deletions src/components/draw/feature-menu-popup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,36 @@ import { type MenuPopupItem as MenuPopupItemType } from '@/components/common/men
import MenuPopup from '@/components/common/menu-popup/menu-popup.vue'
import MenuPopupItem from '@/components/common/menu-popup/menu-popup-item.vue'
import { DrawnFeature } from '@/services/draw/drawn-feature'
import {
exportFeatureService,
type exportFormat,
} from '@/services/export-feature/export-feature.service'
const { t } = useTranslation()
const feature: DrawnFeature | undefined = inject('feature')
const feature: DrawnFeature = inject('feature')!
function download(format: exportFormat) {
exportFeatureService.export(
feature.map,
format,
[feature],
feature.label,
true
)
}
let drawingMenuOptions = <MenuPopupItemType[]>[
{
label: 'Exporter un GPX',
action: () => alert('TODO: Draw feature click drawingMenuOptions'),
action: () => download('gpx'),
},
{
label: 'Exporter un KML',
action: () => alert('TODO: Draw feature click drawingMenuOptions'),
action: () => download('kml'),
},
{
label: 'Exporter un Shapefile',
action: () => alert('TODO: Draw feature click drawingMenuOptions'),
action: () => download('shapefile'),
},
]
Expand Down
122 changes: 122 additions & 0 deletions src/services/export-feature/export-feature-gpx.spec.ts
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')
})
})
})
145 changes: 145 additions & 0 deletions src/services/export-feature/export-feature-gpx.ts
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]
}
}
50 changes: 50 additions & 0 deletions src/services/export-feature/export-feature-kml.spec.ts
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'
)
})
})
})
Loading

0 comments on commit a76b429

Please sign in to comment.