diff --git a/package-lock.json b/package-lock.json index 9dcc7d1..6a19f08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,8 +32,8 @@ "@types/react-router": "^5.1.11", "@types/react-router-dom": "^5.1.7", "cordova-plugin-geolocation": "^4.1.0", - "gpx-parser-builder": "^1.0.2", "ionicons": "^6.0.3", + "isomorphic-xml2js": "^0.1.3", "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -9096,14 +9096,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gpx-parser-builder": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/gpx-parser-builder/-/gpx-parser-builder-1.0.2.tgz", - "integrity": "sha512-zCTGKANSytYLIicVYUUFTYhz3mbDEtIemWZvC3Vb0j8DhwPMbDSCIl9blMClxSLrr7gGbwLAk1nhj3Z41oC5sw==", - "dependencies": { - "isomorphic-xml2js": "~0.1" - } - }, "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -24639,14 +24631,6 @@ "slash": "^3.0.0" } }, - "gpx-parser-builder": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/gpx-parser-builder/-/gpx-parser-builder-1.0.2.tgz", - "integrity": "sha512-zCTGKANSytYLIicVYUUFTYhz3mbDEtIemWZvC3Vb0j8DhwPMbDSCIl9blMClxSLrr7gGbwLAk1nhj3Z41oC5sw==", - "requires": { - "isomorphic-xml2js": "~0.1" - } - }, "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", diff --git a/package.json b/package.json index dee2c0a..445ec00 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,8 @@ "@types/react-router": "^5.1.11", "@types/react-router-dom": "^5.1.7", "cordova-plugin-geolocation": "^4.1.0", - "gpx-parser-builder": "^1.0.2", "ionicons": "^6.0.3", + "isomorphic-xml2js": "^0.1.3", "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/components/map/gpx-import.tsx b/src/components/map/gpx-import.tsx index ea7792a..2b51513 100644 --- a/src/components/map/gpx-import.tsx +++ b/src/components/map/gpx-import.tsx @@ -1,13 +1,13 @@ import React from 'react'; -import { Geolocation } from '@awesome-cordova-plugins/geolocation'; import { useDispatch } from 'react-redux'; -import { mapActions } from '../../store/map'; +import GPX from '../../lib/gpx-parser-builder'; import '../../theme/get-location.css'; import { IonIcon, IonItem } from '@ionic/react'; import { downloadSharp } from 'ionicons/icons'; +import { tracksActions } from '../../store/tracks'; const GpxImport: React.FC<{}> = () => { const dispatch = useDispatch(); @@ -23,6 +23,8 @@ const GpxImport: React.FC<{}> = () => { () => { // this will then display a text file console.log(fileReader.result); + const track = GPX.parse(fileReader.result); + dispatch(tracksActions.push(JSON.parse(JSON.stringify(track)))); }, false ); diff --git a/src/lib/gpx-parser-builder/LICENSE b/src/lib/gpx-parser-builder/LICENSE new file mode 100644 index 0000000..f975d73 --- /dev/null +++ b/src/lib/gpx-parser-builder/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Zheng-Xiang Ke + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/lib/gpx-parser-builder/README.md b/src/lib/gpx-parser-builder/README.md new file mode 100644 index 0000000..88d2602 --- /dev/null +++ b/src/lib/gpx-parser-builder/README.md @@ -0,0 +1,91 @@ +# gpx-parser-builder +A simple gpx parser and builder between GPX string and JavaScript object. It is dependent on [isomorphic-xml2js](https://github.com/RikkiGibson/isomorphic-xml2js). + +[![npm](https://img.shields.io/npm/dt/gpx-parser-builder.svg)](https://www.npmjs.com/package/gpx-parser-builder) +[![GitHub stars](https://img.shields.io/github/stars/kf99916/gpx-parser-builder.svg)](https://github.com/kf99916/gpx-parser-builder/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/kf99916/gpx-parser-builder.svg)](https://github.com/kf99916/gpx-parser-builder/network) +[![npm](https://img.shields.io/npm/v/gpx-parser-builder.svg)](https://www.npmjs.com/package/gpx-parser-builder) +[![GitHub license](https://img.shields.io/github/license/kf99916/gpx-parser-builder.svg)](https://github.com/kf99916/gpx-parser-builder/blob/master/LICENSE) + +## Requirements + +gpx-parser-builder is written with ECMAScript 6. You can leverage [Babel](https://babeljs.io/) and [Webpack](https://webpack.js.org/) to make all browsers available. + +## Installation + +```bash +npm install gpx-parser-builder --save +``` + +## Version + +v1.0.0+ is a breaking change for v0.2.2-. v1.0.0+ fully supports gpx files including waypoints, routes, and tracks. Every gpx type is 1-1 corresponding to a JavaScript class. + +## Usage + +```javascript +import GPX from 'gpx-parser-builder'; + +// Parse gpx +const gpx = GPX.parse('GPX_STRING'); + +window.console.dir(gpx.metadata); +window.console.dir(gpx.wpt); +window.console.dir(gpx.trk); + +// Build gpx +window.console.log(gpx.toString()); +``` + +Get more details about usage with the unit tests. + +### GPX + +The GPX JavaScript object. + +`constructor(object)` + +```javascript +const gpx = new Gpx({$:{...}, metadat: {...}, wpt:[{...},{...}]}, trk: {...}, rte: {...}) +``` + +#### Member Variables + +`$` the attributes for the gpx element. Default value: +```javascript +{ + 'version': '1.1', + 'creator': 'gpx-parser-builder', + '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' +} +``` + +`metadata` the metadata for the gpx. + +`wpt` array of waypoints. It is corresponded to ``. The type of all elements in `wpt` is `Waypoint`; + +`rte` array of routes. It is corresponded to ``. The type of all elements in `rte` is `Route`; + +`trk` array of tracks. It is corresponded to ``. The type of all elements in `trk` is `Track`; + +#### Static Methods + +`parse(gpxString)` parse gpx string to Gpx object. return `null` if parsing failed. + +#### Member Methods + +`toString(options)` GPX object to gpx string. The options is for [isomorphic-xml2js](https://github.com/RikkiGibson/isomorphic-xml2js). + +## Save as GPX file in the frontend + +You can leverage [StreamSaver.js](https://github.com/jimmywarting/StreamSaver.js) or [FileSaver.js](https://github.com/eligrey/FileSaver.js) to save as GPX file. ⚠️Not all borwsers support the above file techniques. ⚠️️️ + +## Author + +Zheng-Xiang Ke, kf99916@gmail.com + +## License + +gpx-parser-builder is available under the MIT license. See the LICENSE file for more info. diff --git a/src/lib/gpx-parser-builder/package.json b/src/lib/gpx-parser-builder/package.json new file mode 100644 index 0000000..19b2cdc --- /dev/null +++ b/src/lib/gpx-parser-builder/package.json @@ -0,0 +1,36 @@ +{ + "name": "gpx-parser-builder", + "version": "1.0.2", + "description": "A simple gpx parser and builder between GPX string and JavaScript object", + "main": "./src/gpx.js", + "scripts": { + "test": "mocha --require @babel/register test/**/*.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/kf99916/gpx-parser-builder.git" + }, + "keywords": [ + "gpx", + "parser", + "builder" + ], + "author": "Zheng-Xiang Ke", + "license": "MIT", + "bugs": { + "url": "https://github.com/kf99916/gpx-parser-builder/issues" + }, + "homepage": "https://github.com/kf99916/gpx-parser-builder", + "files": [ + "src" + ], + "devDependencies": { + "@babel/core": "~7.7", + "@babel/preset-env": "~7.7", + "@babel/register": "~7.7", + "mocha": "~6.2" + }, + "dependencies": { + "isomorphic-xml2js": "~0.1" + } +} diff --git a/src/lib/gpx-parser-builder/src/bounds.js b/src/lib/gpx-parser-builder/src/bounds.js new file mode 100644 index 0000000..08f45b4 --- /dev/null +++ b/src/lib/gpx-parser-builder/src/bounds.js @@ -0,0 +1,8 @@ +export default class Bounds { + constructor(object) { + this.minlat = object.minlat; + this.minlon = object.minlon; + this.maxlat = object.maxlat; + this.maxlon = object.maxlon; + } +} diff --git a/src/lib/gpx-parser-builder/src/copyright.js b/src/lib/gpx-parser-builder/src/copyright.js new file mode 100644 index 0000000..5a9f77e --- /dev/null +++ b/src/lib/gpx-parser-builder/src/copyright.js @@ -0,0 +1,7 @@ +export default class Copyright { + constructor(object) { + this.author = object.author; + this.year = object.year; + this.license = object.license; + } +} \ No newline at end of file diff --git a/src/lib/gpx-parser-builder/src/gpx-parser-builder.d.ts b/src/lib/gpx-parser-builder/src/gpx-parser-builder.d.ts new file mode 100644 index 0000000..37665f0 --- /dev/null +++ b/src/lib/gpx-parser-builder/src/gpx-parser-builder.d.ts @@ -0,0 +1,13 @@ +declare module 'gpx-parser-builder' { + class GPX { + static parse(gpxString: any): any; + constructor(object: any); + $: any; + extensions: any; + metadata: any; + wpt: any; + rte: any; + trk: any; + toString(options: any): string; + } +} diff --git a/src/lib/gpx-parser-builder/src/gpx.js b/src/lib/gpx-parser-builder/src/gpx.js new file mode 100644 index 0000000..07be870 --- /dev/null +++ b/src/lib/gpx-parser-builder/src/gpx.js @@ -0,0 +1,79 @@ +import * as xml2js from 'isomorphic-xml2js'; +import Metadata from './metadata'; +import Waypoint from './waypoint'; +import Route from './route'; +import Track from './track'; +import {removeEmpty, allDatesToISOString} from './utils'; + +const defaultAttributes = { + version: '1.1', + creator: 'gpx-parser-builder', + 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' +} + +export default class GPX { + constructor(object) { + this.$ = Object.assign({}, defaultAttributes, object.$ || object.attributes || {}); + this.extensions = object.extensions; + + if (object.metadata) { + this.metadata = new Metadata(object.metadata); + } + if (object.wpt) { + if (!Array.isArray(object.wpt)) { + object.wpt = [object.wpt]; + } + this.wpt = object.wpt.map(wpt => new Waypoint(wpt)); + } + if (object.rte) { + if (!Array.isArray(object.rte)) { + object.rte = [object.rte]; + } + this.rte = object.rte.map(rte => new Route(rte)); + } + if (object.trk) { + if (!Array.isArray(object.trk)) { + object.trk = [object.trk]; + } + this.trk = object.trk.map(trk => new Track(trk)); + } + + removeEmpty(this); + } + + static parse(gpxString) { + let gpx; + xml2js.parseString(gpxString, { + explicitArray: false + }, (err, xml) => { + if (err) { + return; + } + if (!xml.gpx) { + return; + } + + gpx = new GPX({ + attributes: xml.gpx.$, + metadata: xml.gpx.metadata, + wpt: xml.gpx.wpt, + rte: xml.gpx.rte, + trk: xml.gpx.trk + }); + }); + + return gpx; + } + + toString(options) { + options = options || {}; + options.rootName = 'gpx'; + + const builder = new xml2js.Builder(options), gpx = new GPX(this); + allDatesToISOString(gpx); + return builder.buildObject(gpx); + } +} \ No newline at end of file diff --git a/src/lib/gpx-parser-builder/src/link.js b/src/lib/gpx-parser-builder/src/link.js new file mode 100644 index 0000000..f7e05af --- /dev/null +++ b/src/lib/gpx-parser-builder/src/link.js @@ -0,0 +1,8 @@ +export default class Link { + constructor(object) { + this.$ = {}; + this.$.href = object.$.href || object.href; + this.text = object.text; + this.type = object.type; + } +} \ No newline at end of file diff --git a/src/lib/gpx-parser-builder/src/metadata.js b/src/lib/gpx-parser-builder/src/metadata.js new file mode 100644 index 0000000..293bbec --- /dev/null +++ b/src/lib/gpx-parser-builder/src/metadata.js @@ -0,0 +1,29 @@ +import Copyright from './copyright'; +import Link from './link'; +import Person from './person'; +import Bounds from './bounds'; + +export default class Metadata { + constructor(object) { + this.name = object.name; + this.desc = object.desc; + this.time = object.time ? new Date(object.time) : new Date(); + this.keywords = object.keywords; + this.extensions = object.extensions; + if (object.author) { + this.author = new Person(object.author); + } + if (object.link) { + if (!Array.isArray(object.link)) { + object.link = [object.link]; + } + this.link = object.link.map(l => new Link(l)); + } + if (object.bounds) { + this.bounds = new Bounds(object.bounds); + } + if (object.copyright) { + this.copyright = new Copyright(object.copyright); + } + } +} \ No newline at end of file diff --git a/src/lib/gpx-parser-builder/src/person.js b/src/lib/gpx-parser-builder/src/person.js new file mode 100644 index 0000000..6382578 --- /dev/null +++ b/src/lib/gpx-parser-builder/src/person.js @@ -0,0 +1,9 @@ +import Link from './link'; + +export default class Person { + constructor(object) { + this.name = object.name; + this.email = object.emil; + this.link = new Link(object.link); + } +} \ No newline at end of file diff --git a/src/lib/gpx-parser-builder/src/route.js b/src/lib/gpx-parser-builder/src/route.js new file mode 100644 index 0000000..842e9f8 --- /dev/null +++ b/src/lib/gpx-parser-builder/src/route.js @@ -0,0 +1,27 @@ +import Waypoint from './waypoint'; +import Link from './link'; + +export default class Route { + constructor(object) { + this.name = object.name; + this.cmt = object.cmt; + this.desc = object.desc; + this.src = object.src; + this.number = object.number; + this.type = object.type; + this.extensions = object.extensions; + if (object.link) { + if (!Array.isArray(object.link)) { + this.link = [object.link]; + } + this.link = object.link.map(l => new Link(l)); + } + + if (object.rtept) { + if (!Array.isArray(object.rtept)) { + this.rtept = [object.rtept]; + } + this.rtept = object.rtept.map(pt => new Waypoint(pt)); + } + } +} \ No newline at end of file diff --git a/src/lib/gpx-parser-builder/src/track-segment.js b/src/lib/gpx-parser-builder/src/track-segment.js new file mode 100644 index 0000000..5fca2d5 --- /dev/null +++ b/src/lib/gpx-parser-builder/src/track-segment.js @@ -0,0 +1,13 @@ +import Waypoint from './waypoint'; + +export default class TrackSegment { + constructor(object) { + if (object.trkpt) { + if (!Array.isArray(object.trkpt)) { + object.trkpt = [object.trkpt]; + } + this.trkpt = object.trkpt.map(pt => new Waypoint(pt)); + } + this.extensions = object.extensions; + } +} \ No newline at end of file diff --git a/src/lib/gpx-parser-builder/src/track.js b/src/lib/gpx-parser-builder/src/track.js new file mode 100644 index 0000000..4e330aa --- /dev/null +++ b/src/lib/gpx-parser-builder/src/track.js @@ -0,0 +1,26 @@ +import TrackSegment from './track-segment'; +import Link from './link'; + +export default class Track { + constructor(object) { + this.name = object.name; + this.cmt = object.cmt; + this.desc = object.desc; + this.src = object.src; + this.number = object.number; + this.type = object.type; + this.extensions = object.extensions; + if (object.link) { + if (!Array.isArray(object.link)) { + object.link = [object.link]; + } + this.link = object.link.map(l => new Link(l)); + } + if (object.trkseg) { + if (!Array.isArray(object.trkseg)) { + object.trkseg = [object.trkseg]; + } + this.trkseg = object.trkseg.map(seg => new TrackSegment(seg)); + } + } +} \ No newline at end of file diff --git a/src/lib/gpx-parser-builder/src/utils.js b/src/lib/gpx-parser-builder/src/utils.js new file mode 100644 index 0000000..d769918 --- /dev/null +++ b/src/lib/gpx-parser-builder/src/utils.js @@ -0,0 +1,23 @@ +function removeEmpty(obj) { + Object.entries(obj).forEach(([key, val]) => { + if (val && val instanceof Object) { + removeEmpty(val); + } else if (val == null) { + delete obj[key]; + } + }); +} + +function allDatesToISOString(obj) { + Object.entries(obj).forEach(([key, val]) => { + if (val) { + if (val instanceof Date) { + obj[key] = val.toISOString().split('.')[0] + 'Z'; + } else if (val instanceof Object) { + allDatesToISOString(val); + } + } + }); +} + +export { removeEmpty, allDatesToISOString }; diff --git a/src/lib/gpx-parser-builder/src/waypoint.js b/src/lib/gpx-parser-builder/src/waypoint.js new file mode 100644 index 0000000..2910ff8 --- /dev/null +++ b/src/lib/gpx-parser-builder/src/waypoint.js @@ -0,0 +1,32 @@ +import Link from './link'; + +export default class Waypoint { + constructor(object) { + this.$ = {}; + this.$.lat = object.$.lat === 0 || object.lat === 0 ? 0 : object.$.lat || object.lat || -1; + this.$.lon = object.$.lon === 0 || object.lon === 0 ? 0 : object.$.lon || object.lon || -1; + this.ele = object.ele; + this.time = object.time ? new Date(object.time) : new Date(); + this.magvar = object.magvar; + this.geoidheight = object.geoidheight; + this.name = object.name; + this.cmt = object.cmt; + this.desc = object.desc; + this.src = object.src; + this.sym = object.sym; + this.type = object.type; + this.sat = object.sat; + this.hdop = object.hdop; + this.vdop = object.vdop; + this.pdop = object.pdop; + this.ageofdgpsdata = object.ageofdgpsdata; + this.dgpsid = object.dgpsid; + this.extensions = object.extensions; + if (object.link) { + if (!Array.isArray(object.link)) { + object.link = [object.link]; + } + this.link = object.link.map(l => new Link(l)); + } + } +} \ No newline at end of file diff --git a/src/store/index.ts b/src/store/index.ts index 5e67564..1ec1b0e 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,9 +1,10 @@ import { configureStore } from '@reduxjs/toolkit'; import mapReducer from './map'; +import tracksReducer from './tracks'; const store = configureStore({ - reducer: { map: mapReducer }, + reducer: { map: mapReducer, tracks: tracksReducer }, }); export default store; diff --git a/src/store/tracks.ts b/src/store/tracks.ts new file mode 100644 index 0000000..128627a --- /dev/null +++ b/src/store/tracks.ts @@ -0,0 +1,29 @@ +import { createSlice } from '@reduxjs/toolkit'; + +interface TracksState { + index: number; + tracks: { [index: string]: any }; +} + +const initialTracks: TracksState = { + index: 0, + tracks: {}, +}; + +const tracksSlice = createSlice({ + name: 'tracks', + initialState: initialTracks, + reducers: { + add: (state, action) => { + state.tracks[action.payload.id] = action.payload.track; + }, + push: (state, action) => { + state.tracks['$' + state.index] = action.payload; + state.index++; + }, + }, +}); + +export const tracksActions = tracksSlice.actions; + +export default tracksSlice.reducer;