diff --git a/package.json b/package.json
index 8aa04e2..9896588 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"@types/react-router": "^5.1.11",
"@types/react-router-dom": "^5.1.7",
"ionicons": "^6.0.3",
+ "isomorphic-xml2js": "^0.1.3",
"jotai": "^1.8.6",
"lodash": "^4.17.21",
"pouchdb": "^7.3.0",
diff --git a/src/App.tsx b/src/App.tsx
index 73751a3..65b20b0 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -40,6 +40,7 @@ import { geoPoint } from './components/map/types';
import Back from './components/buttons/Back';
import Forward from './components/buttons/Forward';
import CurrentLocation from './components/map/CurrentLocation';
+import GpxImport from './components/dialogs/GpxImport';
// import { initDb } from './db';
// import PouchDB from 'pouchdb';
// import PouchDBFind from 'pouchdb-find';
@@ -123,7 +124,9 @@ const App: React.FC = () => {
-
+
+
+
diff --git a/src/components/dialogs/GpxImport.module.css b/src/components/dialogs/GpxImport.module.css
new file mode 100644
index 0000000..440c9c1
--- /dev/null
+++ b/src/components/dialogs/GpxImport.module.css
@@ -0,0 +1,8 @@
+.inputFile {
+ width: 0.1px;
+ height: 0.1px;
+ opacity: 0;
+ overflow: hidden;
+ position: absolute;
+ z-index: -1;
+}
\ No newline at end of file
diff --git a/src/components/dialogs/GpxImport.tsx b/src/components/dialogs/GpxImport.tsx
new file mode 100644
index 0000000..5a7ca3a
--- /dev/null
+++ b/src/components/dialogs/GpxImport.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+
+import GPX from '../../lib/gpx-parser-builder/src/gpx';
+
+import css from './GpxImport.module.css';
+import { IonIcon, IonItem } from '@ionic/react';
+import { cloudUpload } from 'ionicons/icons';
+
+const GpxImport: React.FC<{}> = () => {
+ const onChangeHandler = (event: any) => {
+ console.log('On change handler');
+ const file: File = event.target.files[0];
+ const fileReader = new FileReader();
+ fileReader.readAsText(file);
+
+ fileReader.addEventListener(
+ 'load',
+ () => {
+ // this will then display a text file
+ console.log(fileReader.result);
+ const gpx = GPX.parse(fileReader.result);
+ console.log(`gpx: ${JSON.stringify(gpx)}`);
+ // pushGpx(db, {
+ // gpx,
+ // metadata: {
+ // lastModified: new Date(file.lastModified).toISOString(),
+ // importDate: new Date().toISOString(),
+ // name: file.name,
+ // size: file.size,
+ // type: file.type,
+ // },
+ // });
+ },
+ false
+ );
+ };
+
+ return (
+
+
+
+
+ );
+};
+
+export default GpxImport;
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..24b44c4
--- /dev/null
+++ b/src/lib/gpx-parser-builder/src/person.js
@@ -0,0 +1,11 @@
+import Link from './link';
+
+export default class Person {
+ constructor(object) {
+ this.name = object.name;
+ this.email = object.email;
+ if (object.link) {
+ this.link = new Link(object.link);
+ }
+ }
+}
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/missing-typedefs/gpx-parser-builder.d.ts b/src/missing-typedefs/gpx-parser-builder.d.ts
new file mode 100644
index 0000000..d50663c
--- /dev/null
+++ b/src/missing-typedefs/gpx-parser-builder.d.ts
@@ -0,0 +1 @@
+declare module 'gpx-parser-builder';
\ No newline at end of file