File : core/lib/GpxParser.js

1
/*
2
Copyright - 2017 2023 - wwwouaiebe - Contact: http//www.ouaie.be/
3
4
This  program is free software;
5
you can redistribute it and/or modify it under the terms of the
6
GNU General Public License as published by the Free Software Foundation;
7
either version 3 of the License, or any later version.
8
9
This program is distributed in the hope that it will be useful,
10
but WITHOUT ANY WARRANTY; without even the implied warranty of
11
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
GNU General Public License for more details.
13
14
You should have received a copy of the GNU General Public License
15
along with this program; if not, write to the Free Software
16
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17
*/
18
/*
19
Changes:
20
    - v4.0.0:
21
        - created from v3.6.0
22
Doc reviewed 202208
23
 */
24
25
import Travel from '../../data/Travel.js';
26
import Route from '../../data/Route.js';
27
import ItineraryPoint from '../../data/ItineraryPoint.js';
28
import Maneuver from '../../data/Maneuver.js';
29
import WayPoint from '../../data/WayPoint.js';
30
import Note from '../../data/Note.js';
31
import theSphericalTrigonometry from './SphericalTrigonometry.js';
32
import theGeometry from './Geometry.js';
33
import theTranslator from '../../core/uiLib/Translator.js';
34
import { INVALID_OBJ_ID, ZERO, ONE, DISTANCE } from '../../main/Constants.js';
35
36
/* ------------------------------------------------------------------------------------------------------------------------- */
37
/**
38
Parser to transform a gpx file into a Travel object
39
*/
40
/* ------------------------------------------------------------------------------------------------------------------------- */
41
42
class GpxParser {
43
44
    /**
45
    The gpx document
46
    @type {XMLDocument}
47
    */
48
49
    #gpxDocument;
50
51
    /**
52
    The destination Travel
53
    @type {Travel}
54
    */
55
56
    #travel;
57
58
    /**
59
    The currently loaded Route
60
    @type {Route}
61
    */
62
63
    #route;
64
65
    /**
66
    A boolean set to true when the gpx file comes from a node network
67
    @type {Boolean}
68
    */
69
70
    #isNodeNetwork;
71
72
    /**
73
    Parse a trkpt xml node
74
    @param {Node} trkPtNode A Node with a trkpt tag
75
    */
76
77
    #parseTrkPtNode ( trkPtNode ) {
78
        let itineraryPoint = new ItineraryPoint ( );
79
        itineraryPoint.lat = Number.parseFloat ( trkPtNode.getAttributeNS ( null, 'lat' ) );
80
        itineraryPoint.lng = Number.parseFloat ( trkPtNode.getAttributeNS ( null, 'lon' ) );
81
        const childs = trkPtNode.childNodes;
82
        for ( let nodeCounter = ZERO; nodeCounter < childs.length; nodeCounter ++ ) {
83
            switch ( childs [ nodeCounter ].nodeName ) {
84
            case 'ele' :
85
                itineraryPoint.elev = Number.parseFloat ( childs [ nodeCounter ].textContent );
86
                if ( ZERO !== itineraryPoint.elev ) {
87
                    this.#route.itinerary.hasProfile = true;
88
                }
89
                break;
90
            default :
91
                break;
92
            }
93
        }
94
        this.#route.itinerary.itineraryPoints.add ( itineraryPoint );
95
    }
96
97
    /**
98
    Parse a trkseg xml node
99
    @param {Node} trkSegNode A Node with a trkseg tag
100
    */
101
102
    #parseTrkSegNode ( trkSegNode ) {
103
        const childs = trkSegNode.childNodes;
104
        for ( let nodeCounter = ZERO; nodeCounter < childs.length; nodeCounter ++ ) {
105
            switch ( childs [ nodeCounter ].nodeName ) {
106
            case 'trkpt' :
107
                this.#parseTrkPtNode ( childs [ nodeCounter ] );
108
                break;
109
            default :
110
                break;
111
            }
112
        }
113
    }
114
115
    /**
116
    Compute the ascent and descent of the currently parsed Route
117
    */
118
119
    #computeAscentAndDescent ( ) {
120
        let ascent = ZERO;
121
        let descent = ZERO;
122
        let itineraryPointsIterator = this.#route.itinerary.itineraryPoints.iterator;
123
        itineraryPointsIterator.done;
124
        while ( ! itineraryPointsIterator.done ) {
125
            let deltaElev = itineraryPointsIterator.value.elev - itineraryPointsIterator.previous.elev;
126
            if ( ZERO > deltaElev ) {
127
                descent -= deltaElev;
128
            }
129
            else {
130
                ascent += deltaElev;
131
            }
132
        }
133
        this.#route.itinerary.ascent = ascent;
134
        this.#route.itinerary.descent = descent;
135
    }
136
137
    /**
138
    Parse a trk xml node
139
    @param {Node} trkNode A Node with a trk tag
140
    */
141
142
    #parseTrkNode ( trkNode ) {
143
        this.#route = new Route ( );
144
        const childs = trkNode.childNodes;
145
        for ( let nodeCounter = ZERO; nodeCounter < childs.length; nodeCounter ++ ) {
146
            switch ( childs [ nodeCounter ].nodeName ) {
147
            case 'name' :
148
                this.#route.name = childs [ nodeCounter ].textContent;
149
                break;
150
            case 'trkseg' :
151
                this.#parseTrkSegNode ( childs [ nodeCounter ] );
152
                break;
153
            default :
154
                break;
155
            }
156
        }
157
        if ( this.#route.itinerary.hasProfile ) {
158
            this.#computeAscentAndDescent ( );
159
        }
160
        this.#travel.routes.add ( this.#route );
161
    }
162
163
    /**
164
    Search the nearest ItineraryPoint objId of the currently parsed route from a given point
165
    @param {Array.<Number>} latLng The lat and lng of the given point
166
    */
167
168
    #nearestItineraryPointObjId ( latLng ) {
169
        let distance = Number.MAX_VALUE;
170
        let itineraryPointObjId = INVALID_OBJ_ID;
171
        this.#route.itinerary.itineraryPoints.forEach (
172
            itineraryPoint => {
173
                const pointDistance = theSphericalTrigonometry.pointsDistance ( latLng, itineraryPoint.latLng );
174
                if ( pointDistance < distance ) {
175
                    distance = pointDistance;
176
                    itineraryPointObjId = itineraryPoint.objId;
177
                }
178
            }
179
        );
180
        return itineraryPointObjId;
181
    }
182
183
    /**
184
    Parse a rtept xml node
185
    @param {Node} rtePtNode A Node with a rtept tag
186
    */
187
188
    #parseRtePtNode ( rtePtNode ) {
189
        const maneuver = new Maneuver ( );
190
        maneuver.iconName = 'kUndefined';
191
        const maneuverLat = Number.parseFloat ( rtePtNode.getAttributeNS ( null, 'lat' ) );
192
        const maneuverLng = Number.parseFloat ( rtePtNode.getAttributeNS ( null, 'lon' ) );
193
        maneuver.itineraryPointObjId = this.#nearestItineraryPointObjId ( [ maneuverLat, maneuverLng ] );
194
        const childs = rtePtNode.childNodes;
195
        for ( let nodeCounter = ZERO; nodeCounter < childs.length; nodeCounter ++ ) {
196
            switch ( childs [ nodeCounter ].nodeName ) {
197
            case 'desc' :
198
                maneuver.instruction = childs [ nodeCounter ].textContent;
199
                break;
200
            default :
201
                break;
202
            }
203
        }
204
        this.#route.itinerary.maneuvers.add ( maneuver );
205
    }
206
207
    /**
208
    Parse a rte xml node
209
    @param {Node} rteNode A Node with a rte tag
210
    */
211
212
    #parseRteNode ( rteNode ) {
213
        const childs = rteNode.childNodes;
214
        for ( let nodeCounter = ZERO; nodeCounter < childs.length; nodeCounter ++ ) {
215
            switch ( childs [ nodeCounter ].nodeName ) {
216
            case 'name' :
217
                this.#route.name = childs [ nodeCounter ].textContent;
218
                break;
219
            case 'rtept' :
220
                this.#parseRtePtNode ( childs [ nodeCounter ] );
221
                break;
222
            default :
223
                break;
224
            }
225
        }
226
    }
227
228
    /**
229
    Add a note at the same position than a way node (only for node networks gpx files)
230
    @param {WayPoint} wayPoint The wayPoint for witch a Note must be created
231
    */
232
233
    #addWptNote ( wayPoint ) {
234
        const note = new Note ( );
235
        note.latLng = wayPoint.latLng;
236
        note.iconLatLng = wayPoint.latLng;
237
        const names = wayPoint.name.split ( '+' );
238
        note.iconContent =
239
            '<div class="TravelNotes-MapNote TravelNotes-MapNoteCategory-0073">' +
240
            '<svg viewBox="0 0 20 20"><text x="10" y="14">' +
241
            names [ ZERO ] + '</text></svg></div>';
242
243
        note.tooltipContent = theTranslator.getText ( 'GpxParser - Network node' ) + names [ ZERO ];
244
        if ( names [ ONE ] ) {
245
            note.tooltipContent += theTranslator.getText ( 'GpxParser - Go to network node' ) + names [ ONE ];
246
        }
247
248
        note.distance = theGeometry.getClosestLatLngDistance ( this.#route, note.latLng ).distance;
249
        this.#route.notes.add ( note );
250
    }
251
252
    /**
253
    Parse a wpt xml node
254
    @param {Node} wptNode A Node with a wpt tag
255
    */
256
257
    #parseWptNode ( wptNode ) {
258
        let wayPoint = new WayPoint ( );
259
        wayPoint.lat = Number.parseFloat ( wptNode.getAttributeNS ( null, 'lat' ) );
260
        wayPoint.lng = Number.parseFloat ( wptNode.getAttributeNS ( null, 'lon' ) );
261
262
        const childs = wptNode.childNodes;
263
        for ( let nodeCounter = ZERO; nodeCounter < childs.length; nodeCounter ++ ) {
264
            switch ( childs [ nodeCounter ].nodeName ) {
265
            case 'name' :
266
                wayPoint.name = childs [ nodeCounter ].textContent;
267
                break;
268
            default :
269
                break;
270
            }
271
        }
272
        this.#route.wayPoints.add ( wayPoint );
273
        if ( this.#isNodeNetwork ) {
274
            this.#addWptNote ( wayPoint );
275
        }
276
    }
277
278
    /**
279
    Parse a NodeList of wpt xml node
280
    @param {NodeList} wptNodes A NodeList of Nodes with a wpt tag
281
    */
282
283
    #parseWptNodes ( wptNodes ) {
284
        for ( let wptNodeCounter = 0; wptNodeCounter < wptNodes.length; wptNodeCounter ++ ) {
285
            this.#parseWptNode ( wptNodes [ wptNodeCounter ] );
286
        }
287
    }
288
289
    /**
290
    Add a starting and an ending waypoint on each route of the parsed Travel
291
    */
292
293
    #createWayPoints ( ) {
294
        this.#travel.routes.forEach (
295
            route => {
296
                route.wayPoints.remove ( route.wayPoints.first.objId );
297
                route.wayPoints.remove ( route.wayPoints.last.objId );
298
                const startWayPoint = new WayPoint ( );
299
                startWayPoint.latLng = route.itinerary.itineraryPoints.first.latLng;
300
                route.wayPoints.add ( startWayPoint );
301
                const endWayPoint = new WayPoint ( );
302
                endWayPoint.latLng = route.itinerary.itineraryPoints.last.latLng;
303
                route.wayPoints.add ( endWayPoint );
304
            }
305
        );
306
    }
307
308
    /**
309
    This method compute the route, itineraryPoints and maneuvers distances
310
    @param {Route} route The route for witch the distances are computed
311
    */
312
313
    #computeRouteDistances ( route ) {
314
315
        // Computing the distance between itineraryPoints
316
        const itineraryPointsIterator = route.itinerary.itineraryPoints.iterator;
317
        const maneuverIterator = route.itinerary.maneuvers.iterator;
318
319
        itineraryPointsIterator.done;
320
        let maneuverDone = maneuverIterator.done;
321
322
        if ( ! maneuverDone ) {
323
            maneuverIterator.value.distance = DISTANCE.defaultValue;
324
            maneuverDone = maneuverIterator.done;
325
        }
326
        route.distance = DISTANCE.defaultValue;
327
        route.duration = DISTANCE.defaultValue;
328
329
        while ( ! itineraryPointsIterator.done ) {
330
            itineraryPointsIterator.previous.distance = theSphericalTrigonometry.pointsDistance (
331
                itineraryPointsIterator.previous.latLng,
332
                itineraryPointsIterator.value.latLng
333
            );
334
            route.distance += itineraryPointsIterator.previous.distance;
335
            if ( ! maneuverDone ) {
336
                maneuverIterator.previous.distance += itineraryPointsIterator.previous.distance;
337
                if ( maneuverIterator.value.itineraryPointObjId === itineraryPointsIterator.value.objId ) {
338
                    route.duration += maneuverIterator.previous.duration;
339
                    maneuverIterator.value.distance = DISTANCE.defaultValue;
340
                    if (
341
                        maneuverIterator.next
342
                        &&
343
                        maneuverIterator.value.itineraryPointObjId === maneuverIterator.next.itineraryPointObjId
344
                    ) {
345
346
                        // 2 maneuvers on the same itineraryPoint. We skip the first maneuver
347
                        maneuverDone = maneuverIterator.done;
348
                        maneuverIterator.value.distance = DISTANCE.defaultValue;
349
                    }
350
                    maneuverDone = maneuverIterator.done;
351
352
                }
353
            }
354
        }
355
    }
356
357
    /**
358
    The constructor
359
    */
360
361
    constructor ( ) {
362
        Object.freeze ( this );
363
    }
364
365
    /**
366
    Parse a gpx string and load the data found in the gpx in a Travel Object
367
    @param {String} gpxString The string to parse
368
    @return {Travel} A travel cpmpleted with Routes and Notes found in the gpx string
369
    */
370
371
    parse ( gpxString ) {
372
        this.#gpxDocument = new DOMParser ( ).parseFromString ( gpxString, 'text/xml' );
373
374
        this.#isNodeNetwork =
375
            Boolean (
376
                this.#gpxDocument.querySelector ( 'gpx' )
377
                    .getAttributeNS ( null, 'creator' )
378
                    .match ( /fietsnet|knooppuntnet/g )
379
            );
380
        this.#travel = new Travel ( );
381
        this.#travel.routes.remove ( this.#travel.routes.first.objId );
382
        const trkNodes = this.#gpxDocument.querySelectorAll ( 'trk' );
383
        for ( let trkNodeCounter = 0; trkNodeCounter < trkNodes.length; trkNodeCounter ++ ) {
384
            this.#parseTrkNode ( trkNodes [ trkNodeCounter ] );
385
        }
386
        const rteNodes = this.#gpxDocument.querySelectorAll ( 'rte' );
387
388
        if (
389
            ONE === rteNodes.length
390
            &&
391
            ONE === this.#travel.routes.length
392
        ) {
393
            this.#parseRteNode ( rteNodes [ ZERO ] );
394
        }
395
396
        this.#travel.routes.forEach (
397
            route => this.#computeRouteDistances ( route )
398
        );
399
400
        const wptNodes = this.#gpxDocument.querySelectorAll ( 'wpt' );
401
402
        if (
403
            ZERO < wptNodes.length
404
            &&
405
            ONE === this.#travel.routes.length
406
        ) {
407
            this.#route.wayPoints.remove ( this.#route.wayPoints.first.objId );
408
            this.#route.wayPoints.remove ( this.#route.wayPoints.last.objId );
409
            this.#parseWptNodes ( wptNodes );
410
        }
411
        else {
412
            this.#createWayPoints ( );
413
        }
414
415
        return this.#travel;
416
    }
417
}
418
419
export default GpxParser;
420
421
/* --- End of file --------------------------------------------------------------------------------------------------------- */
422