├── .gitignore
├── .travis.yml
├── LICENSE.txt
├── README.md
├── go.mod
├── go.sum
├── gpx
├── converters.go
├── fixedpoint_float64.go
├── geo.go
├── geo_test.go
├── gpx.go
├── gpx10.go
├── gpx11.go
├── gpx11_extensions.go
├── gpx_extensions_test.go
├── gpx_test.go
├── nullable_float64.go
├── nullable_int.go
├── nullable_string.go
├── nullable_time.go
├── xml.go
└── xml_test.go
├── gpxinfo.go
├── makefile
└── test_files
├── Mojstrovka.gpx
├── cerknicko-without-times.gpx
├── file.gpx
├── gpx-without-root-attributes.gpx
├── gpx-without-xml-declaration.gpx
├── gpx1.0_with_all_fields.gpx
├── gpx1.1_with_all_fields.gpx
├── gpx1.1_with_extensions.gpx
├── gpx1.1_with_extensions_without_namespaces.gpx
├── gpx_with_garmin_extension.gpx
├── graphhopper.gpx.gz
├── iso8859-1encoded.gpx
├── korita-zbevnica.gpx
└── visnjan.gpx
/.gitignore:
--------------------------------------------------------------------------------
1 | *.sw*
2 | tags
3 | gpxgo
4 | pkg/*
5 | .vscode/*
6 | vendor/*
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | go:
3 | - "1.10"
4 |
5 | script:
6 | - go test -v ./gpx
7 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [2016-] [Tomo Krajina]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Go GPX library
2 |
3 | gpxgo is a golang library for parsing and manipulating GPX files. GPX (GPS eXchange Format) is a XML based file format for GPS track logs.
4 |
5 | ## Example:
6 |
7 | import (
8 | ...
9 | "github.com/tkrajina/gpxgo/gpx"
10 | ...
11 | )
12 |
13 | gpxBytes := ...
14 | gpxFile, err := gpx.ParseBytes(gpxBytes)
15 | if err != nil {
16 | ...
17 | }
18 |
19 | // Analyize/manipulate your track data here...
20 | for _, track := range gpxFile.Tracks {
21 | for _, segment := range track.Segments {
22 | for _, point := range segment.Points {
23 | fmt.Print(point)
24 | }
25 | }
26 | }
27 |
28 | // (Check the API for GPX manipulation and analyzing utility methods)
29 |
30 | // When ready, you can write the resulting GPX file:
31 | xmlBytes, err := gpxFile.ToXml(gpx.ToXmlParams{Version: "1.1", Indent: true})
32 | ...
33 |
34 | ## GPX Compatibility
35 |
36 | Gpxgo can read/write both GPX 1.0 and GPX 1.1 files.
37 |
38 | GPX extensions support is experimental from v1.1.0 on.
39 |
40 | ## gpxinfo
41 |
42 | `gpxinfo` is a command line utility for writing basic stats from gpx files:
43 |
44 | $ go run gpxinfo.go test_files/Mojstrovka.gpx
45 | File: /Users/puzz/golang/src/github.com/tkrajina/gpxgo/test_files/Mojstrovka.gpx
46 | GPX name:
47 | GPX desctiption:
48 | GPX version: 1.0
49 | Author:
50 | Email:
51 |
52 |
53 | Global stats:
54 | Points: 184
55 | Length 2D: 2.6958067369682577
56 | Length 3D: 3.00439590990862
57 | Bounds: 46.430350, 46.435641, 13.738842, 13.748333
58 | Moving time: 0
59 | Stopped time: 0
60 | Max speed: 0.000000m/s = 0.000000km/h
61 | Total uphill: 446.4893280000001
62 | Total downhill: 417.65524800000026
63 | Started: 1901-12-13 20:45:52 +0000 UTC
64 | Ended: 1901-12-13 20:45:52 +0000 UTC
65 |
66 |
67 | Track #1:
68 | Points: 184
69 | Length 2D: 2.6958067369682577
70 | Length 3D: 3.00439590990862
71 | Bounds: 46.430350, 46.435641, 13.738842, 13.748333
72 | Moving time: 0
73 | Stopped time: 0
74 | Max speed: 0.000000m/s = 0.000000km/h
75 | Total uphill: 446.4893280000001
76 | ...etc...
77 |
78 | ## History
79 |
80 | Gpxgo is based on:
81 |
82 | * https://github.com/tkrajina/gpxpy (python gpx library)
83 | * https://github.com/ptrv/go-gpx (an earlier port of gpxpy)
84 |
85 | # License
86 |
87 | gpxgo is licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
88 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/tkrajina/gpxgo
2 |
3 | go 1.16
4 |
5 | require (
6 | github.com/stretchr/testify v1.7.0
7 | golang.org/x/net v0.0.0-20210614182718-04defd469f4e
8 | )
9 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
6 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
7 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
8 | golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
9 | golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
10 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
11 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
12 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
13 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
14 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
15 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
18 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
19 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
20 |
--------------------------------------------------------------------------------
/gpx/converters.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013, 2014 Peter Vasil, Tomo Krajina. All
2 | // rights reserved. Use of this source code is governed
3 | // by a BSD-style license that can be found in the
4 | // LICENSE file.
5 |
6 | package gpx
7 |
8 | import (
9 | "encoding/xml"
10 | "fmt"
11 | "math/rand"
12 | "sort"
13 | "strings"
14 | )
15 |
16 | // defaultCreator contains the original repo path
17 | const defaultCreator = "https://github.com/tkrajina/gpxgo"
18 |
19 | // ----------------------------------------------------------------------------------------------------
20 | // Gpx 1.0 Stuff
21 | // ----------------------------------------------------------------------------------------------------
22 |
23 | func convertToGpx10Models(gpxDoc *GPX) *gpx10Gpx {
24 | gpx10Doc := &gpx10Gpx{}
25 | //gpx10Doc.Attrs = namespacesMapToAttrs(gpxDoc.Namespaces)
26 |
27 | //gpx10Doc.XMLNs = gpxDoc.XMLNs
28 | gpx10Doc.XMLNs = "http://www.topografix.com/GPX/1/0"
29 | gpx10Doc.XmlNsXsi = gpxDoc.XmlNsXsi
30 | gpx10Doc.XmlSchemaLoc = gpxDoc.XmlSchemaLoc
31 |
32 | gpx10Doc.Version = "1.0"
33 | if len(gpxDoc.Creator) == 0 {
34 | gpx10Doc.Creator = defaultCreator
35 | } else {
36 | gpx10Doc.Creator = gpxDoc.Creator
37 | }
38 | gpx10Doc.Name = gpxDoc.Name
39 | gpx10Doc.Desc = gpxDoc.Description
40 | gpx10Doc.Author = gpxDoc.AuthorName
41 | gpx10Doc.Email = gpxDoc.AuthorEmail
42 |
43 | if len(gpxDoc.AuthorLink) > 0 || len(gpxDoc.AuthorLinkText) > 0 {
44 | // TODO
45 | }
46 |
47 | if len(gpxDoc.Link) > 0 || len(gpxDoc.LinkText) > 0 {
48 | gpx10Doc.Url = gpxDoc.Link
49 | gpx10Doc.UrlName = gpxDoc.LinkText
50 | }
51 |
52 | if gpxDoc.Time != nil {
53 | gpx10Doc.Time = formatGPXTime(gpxDoc.Time)
54 | }
55 |
56 | gpx10Doc.Keywords = gpxDoc.Keywords
57 |
58 | if gpxDoc.Waypoints != nil {
59 | gpx10Doc.Waypoints = make([]*gpx10GpxPoint, len(gpxDoc.Waypoints))
60 | for waypointNo, waypoint := range gpxDoc.Waypoints {
61 | gpx10Doc.Waypoints[waypointNo] = convertPointToGpx10(&waypoint)
62 | }
63 | }
64 |
65 | if gpxDoc.Routes != nil {
66 | gpx10Doc.Routes = make([]*gpx10GpxRte, len(gpxDoc.Routes))
67 | for routeNo, route := range gpxDoc.Routes {
68 | r := new(gpx10GpxRte)
69 | r.Name = route.Name
70 | r.Cmt = route.Comment
71 | r.Desc = route.Description
72 | r.Src = route.Source
73 | // TODO
74 | //r.Links = route.Links
75 | r.Number = route.Number
76 | r.Type = route.Type
77 | // TODO
78 | //r.RoutePoints = route.RoutePoints
79 |
80 | gpx10Doc.Routes[routeNo] = r
81 |
82 | if route.Points != nil {
83 | r.Points = make([]*gpx10GpxPoint, len(route.Points))
84 | for pointNo, point := range route.Points {
85 | r.Points[pointNo] = convertPointToGpx10(&point)
86 | }
87 | }
88 | }
89 | }
90 |
91 | if gpxDoc.Tracks != nil {
92 | gpx10Doc.Tracks = make([]*gpx10GpxTrk, len(gpxDoc.Tracks))
93 | for trackNo, track := range gpxDoc.Tracks {
94 | gpx10Track := new(gpx10GpxTrk)
95 | gpx10Track.Name = track.Name
96 | gpx10Track.Cmt = track.Comment
97 | gpx10Track.Desc = track.Description
98 | gpx10Track.Src = track.Source
99 | gpx10Track.Number = track.Number
100 | gpx10Track.Type = track.Type
101 |
102 | if track.Segments != nil {
103 | gpx10Track.Segments = make([]*gpx10GpxTrkSeg, len(track.Segments))
104 | for segmentNo, segment := range track.Segments {
105 | gpx10Segment := new(gpx10GpxTrkSeg)
106 | if segment.Points != nil {
107 | gpx10Segment.Points = make([]*gpx10GpxPoint, len(segment.Points))
108 | for pointNo, point := range segment.Points {
109 | gpx10Point := convertPointToGpx10(&point)
110 | // TODO
111 | //gpx10Point.Speed = point.Speed
112 | //gpx10Point.Speed = point.Speed
113 | gpx10Segment.Points[pointNo] = gpx10Point
114 | }
115 | }
116 | gpx10Track.Segments[segmentNo] = gpx10Segment
117 | }
118 | }
119 | gpx10Doc.Tracks[trackNo] = gpx10Track
120 | }
121 | }
122 |
123 | return gpx10Doc
124 | }
125 |
126 | func convertFromGpx10Models(gpx10Doc *gpx10Gpx) *GPX {
127 | gpxDoc := new(GPX)
128 | gpxDoc.Attrs = NewGPXAttributes(gpx10Doc.Attrs)
129 |
130 | gpxDoc.XMLNs = gpx10Doc.XMLNs
131 | gpxDoc.XmlNsXsi = gpx10Doc.XmlNsXsi
132 | gpxDoc.XmlSchemaLoc = gpx10Doc.XmlSchemaLoc
133 |
134 | gpxDoc.Creator = gpx10Doc.Creator
135 | gpxDoc.Version = gpx10Doc.Version
136 | gpxDoc.Name = gpx10Doc.Name
137 | gpxDoc.Description = gpx10Doc.Desc
138 | gpxDoc.AuthorName = gpx10Doc.Author
139 | gpxDoc.AuthorEmail = gpx10Doc.Email
140 |
141 | if len(gpx10Doc.Url) > 0 || len(gpx10Doc.UrlName) > 0 {
142 | gpxDoc.Link = gpx10Doc.Url
143 | gpxDoc.LinkText = gpx10Doc.UrlName
144 | }
145 |
146 | if len(gpx10Doc.Time) > 0 {
147 | gpxDoc.Time, _ = parseGPXTime(gpx10Doc.Time)
148 | }
149 |
150 | gpxDoc.Keywords = gpx10Doc.Keywords
151 |
152 | if gpx10Doc.Waypoints != nil {
153 | waypoints := make([]GPXPoint, len(gpx10Doc.Waypoints))
154 | for waypointNo, waypoint := range gpx10Doc.Waypoints {
155 | waypoints[waypointNo] = *convertPointFromGpx10(waypoint)
156 | }
157 | gpxDoc.Waypoints = waypoints
158 | }
159 |
160 | if gpx10Doc.Routes != nil {
161 | gpxDoc.Routes = make([]GPXRoute, len(gpx10Doc.Routes))
162 | for routeNo, route := range gpx10Doc.Routes {
163 | r := new(GPXRoute)
164 |
165 | r.Name = route.Name
166 | r.Comment = route.Cmt
167 | r.Description = route.Desc
168 | r.Source = route.Src
169 | // TODO
170 | //r.Links = route.Links
171 | r.Number = route.Number
172 | r.Type = route.Type
173 | // TODO
174 | //r.RoutePoints = route.RoutePoints
175 |
176 | if route.Points != nil {
177 | r.Points = make([]GPXPoint, len(route.Points))
178 | for pointNo, point := range route.Points {
179 | r.Points[pointNo] = *convertPointFromGpx10(point)
180 | }
181 | }
182 |
183 | gpxDoc.Routes[routeNo] = *r
184 | }
185 | }
186 |
187 | if gpx10Doc.Tracks != nil {
188 | gpxDoc.Tracks = make([]GPXTrack, len(gpx10Doc.Tracks))
189 | for trackNo, track := range gpx10Doc.Tracks {
190 | gpxTrack := new(GPXTrack)
191 | gpxTrack.Name = track.Name
192 | gpxTrack.Comment = track.Cmt
193 | gpxTrack.Description = track.Desc
194 | gpxTrack.Source = track.Src
195 | gpxTrack.Number = track.Number
196 | gpxTrack.Type = track.Type
197 |
198 | if track.Segments != nil {
199 | gpxTrack.Segments = make([]GPXTrackSegment, len(track.Segments))
200 | for segmentNo, segment := range track.Segments {
201 | gpxSegment := GPXTrackSegment{}
202 | if segment.Points != nil {
203 | gpxSegment.Points = make([]GPXPoint, len(segment.Points))
204 | for pointNo, point := range segment.Points {
205 | gpxSegment.Points[pointNo] = *convertPointFromGpx10(point)
206 | }
207 | }
208 | gpxTrack.Segments[segmentNo] = gpxSegment
209 | }
210 | }
211 | gpxDoc.Tracks[trackNo] = *gpxTrack
212 | }
213 | }
214 |
215 | return gpxDoc
216 | }
217 |
218 | func convertPointToGpx10(original *GPXPoint) *gpx10GpxPoint {
219 | result := new(gpx10GpxPoint)
220 | result.Lat = formattedFloat(original.Latitude)
221 | result.Lon = formattedFloat(original.Longitude)
222 | result.Ele = original.Elevation
223 | result.Timestamp = formatGPXTime(&original.Timestamp)
224 | result.MagVar = original.MagneticVariation
225 | result.GeoIdHeight = original.GeoidHeight
226 | result.Name = original.Name
227 | result.Cmt = original.Comment
228 | result.Desc = original.Description
229 | result.Src = original.Source
230 | // TODO
231 | //w.Links = original.Links
232 | result.Sym = original.Symbol
233 | result.Type = original.Type
234 | result.Fix = original.TypeOfGpsFix
235 | if original.Satellites.NotNull() {
236 | value := original.Satellites.Value()
237 | result.Sat = &value
238 | }
239 | if original.HorizontalDilution.NotNull() {
240 | value := original.HorizontalDilution.Value()
241 | result.Hdop = &value
242 | }
243 | if original.VerticalDilution.NotNull() {
244 | value := original.VerticalDilution.Value()
245 | result.Vdop = &value
246 | }
247 | if original.PositionalDilution.NotNull() {
248 | value := original.PositionalDilution.Value()
249 | result.Pdop = &value
250 | }
251 | if original.AgeOfDGpsData.NotNull() {
252 | value := original.AgeOfDGpsData.Value()
253 | result.AgeOfDGpsData = &value
254 | }
255 | if original.DGpsId.NotNull() {
256 | value := original.DGpsId.Value()
257 | result.DGpsId = &value
258 | }
259 | return result
260 | }
261 |
262 | func convertPointFromGpx10(original *gpx10GpxPoint) *GPXPoint {
263 | result := new(GPXPoint)
264 | result.Latitude = float64(original.Lat)
265 | result.Longitude = float64(original.Lon)
266 | result.Elevation = original.Ele
267 | time, _ := parseGPXTime(original.Timestamp)
268 | if time != nil {
269 | result.Timestamp = *time
270 | }
271 | result.MagneticVariation = original.MagVar
272 | result.GeoidHeight = original.GeoIdHeight
273 | result.Name = original.Name
274 | result.Comment = original.Cmt
275 | result.Description = original.Desc
276 | result.Source = original.Src
277 | // TODO
278 | //w.Links = original.Links
279 | result.Symbol = original.Sym
280 | result.Type = original.Type
281 | result.TypeOfGpsFix = original.Fix
282 | if original.Sat != nil {
283 | result.Satellites = *NewNullableInt(*original.Sat)
284 | }
285 | if original.Hdop != nil {
286 | result.HorizontalDilution = *NewNullableFloat64(*original.Hdop)
287 | }
288 | if original.Vdop != nil {
289 | result.VerticalDilution = *NewNullableFloat64(*original.Vdop)
290 | }
291 | if original.Pdop != nil {
292 | result.PositionalDilution = *NewNullableFloat64(*original.Pdop)
293 | }
294 | if original.AgeOfDGpsData != nil {
295 | result.AgeOfDGpsData = *NewNullableFloat64(*original.AgeOfDGpsData)
296 | }
297 | if original.DGpsId != nil {
298 | result.DGpsId = *NewNullableInt(*original.DGpsId)
299 | }
300 | return result
301 | }
302 |
303 | // ----------------------------------------------------------------------------------------------------
304 | // Gpx 1.1 Stuff
305 | // ----------------------------------------------------------------------------------------------------
306 |
307 | type NamespaceAttribute struct {
308 | xml.Attr
309 | replacement string
310 | }
311 |
312 | type GPXAttributes struct {
313 | // NamespaceAttributes by namespace and local name
314 | NamespaceAttributes map[string]map[string]NamespaceAttribute
315 | }
316 |
317 | func NewGPXAttributes(attrs []xml.Attr) GPXAttributes {
318 | namespacesByUrls := map[string]string{}
319 |
320 | for _, attr := range attrs {
321 | if attr.Name.Space == "xmlns" {
322 | namespacesByUrls[attr.Value] = attr.Name.Local
323 | }
324 | }
325 |
326 | res := map[string]map[string]NamespaceAttribute{}
327 | for _, attr := range attrs {
328 | space := attr.Name.Space
329 | if ns, found := namespacesByUrls[attr.Name.Space]; found {
330 | space = ns
331 | }
332 | if _, found := res[space]; !found {
333 | res[space] = map[string]NamespaceAttribute{}
334 | }
335 | res[space][attr.Name.Local] = NamespaceAttribute{
336 | Attr: attr,
337 | replacement: strings.Replace(fmt.Sprint("xmlns_prefix_", rand.Float64()), ".", "", -1),
338 | }
339 | }
340 | return GPXAttributes{
341 | NamespaceAttributes: res,
342 | }
343 | }
344 |
345 | func (ga *GPXAttributes) RegisterNamespace(ns, url string) {
346 | ga.GetNamespaceAttrs()[ns] = NamespaceAttribute{
347 | Attr: xml.Attr{
348 | Name: xml.Name{
349 | Space: "xmlns",
350 | Local: ns,
351 | },
352 | Value: url,
353 | },
354 | replacement: strings.Replace(fmt.Sprint("xmlns_registered_prefix_", rand.Float64()), ".", "", -1),
355 | }
356 | }
357 |
358 | func (ga *GPXAttributes) GetNamespaceAttrs() map[string]NamespaceAttribute {
359 | if ga.NamespaceAttributes == nil {
360 | ga.NamespaceAttributes = make(map[string]map[string]NamespaceAttribute)
361 | }
362 | if _, found := ga.NamespaceAttributes["xmlns"]; !found {
363 | ga.NamespaceAttributes["xmlns"] = make(map[string]NamespaceAttribute)
364 | }
365 | return ga.NamespaceAttributes["xmlns"]
366 | }
367 |
368 | func (ga GPXAttributes) ToXMLAttrs() (namespacesReplacement string, replacements map[string]string) {
369 | var keys []string
370 | for k := range ga.NamespaceAttributes {
371 | keys = append(keys, k)
372 | }
373 | sort.Strings(keys)
374 |
375 | replacements = map[string]string{}
376 |
377 | var attrsList []string
378 | for space := range ga.NamespaceAttributes {
379 | for local, nsInfo := range ga.NamespaceAttributes[space] {
380 | var key string
381 | if space == "" {
382 | key = local
383 | } else {
384 | key = space + ":" + local
385 | }
386 | attrsList = append(attrsList, fmt.Sprint(key, `="`, nsInfo.Value, `"`))
387 | if space == "xmlns" {
388 | replacements[nsInfo.replacement] = local + ":"
389 | }
390 | }
391 | }
392 |
393 | namespacesReplacement = strings.Replace(fmt.Sprint("xmlns_", rand.Float64()), ".", "", -1)
394 | sort.Strings(attrsList)
395 | replacements[namespacesReplacement+`=""`] = strings.Join(attrsList, " ")
396 | return
397 | }
398 |
399 | func convertToGpx11Models(gpxDoc *GPX) (*gpx11Gpx, map[string]string) {
400 | namespacesReplacement, replacements := gpxDoc.Attrs.ToXMLAttrs()
401 |
402 | gpx11Doc := &gpx11Gpx{}
403 | gpx11Doc.Attrs = append(gpx11Doc.Attrs, xml.Attr{Name: xml.Name{Local: namespacesReplacement}, Value: ""})
404 |
405 | gpx11Doc.Version = "1.1"
406 |
407 | gpx11Doc.XMLNs = "http://www.topografix.com/GPX/1/1"
408 | gpx11Doc.XmlNsXsi = gpxDoc.XmlNsXsi
409 | gpx11Doc.XmlSchemaLoc = gpxDoc.XmlSchemaLoc
410 |
411 | gpx11Doc.Extensions = gpxDoc.Extensions
412 | gpx11Doc.Extensions.globalNsAttrs = gpxDoc.Attrs.GetNamespaceAttrs()
413 |
414 | gpx11Doc.MetadataExtensions = gpxDoc.MetadataExtensions
415 | gpx11Doc.MetadataExtensions.globalNsAttrs = gpxDoc.Attrs.GetNamespaceAttrs()
416 |
417 | if len(gpxDoc.Creator) == 0 {
418 | gpx11Doc.Creator = defaultCreator
419 | } else {
420 | gpx11Doc.Creator = gpxDoc.Creator
421 | }
422 | gpx11Doc.Name = gpxDoc.Name
423 | gpx11Doc.Desc = gpxDoc.Description
424 | gpx11Doc.AuthorName = gpxDoc.AuthorName
425 |
426 | if len(gpxDoc.AuthorEmail) > 0 {
427 | parts := strings.Split(gpxDoc.AuthorEmail, "@")
428 | if len(parts) == 1 {
429 | gpx11Doc.AuthorEmail = new(gpx11GpxEmail)
430 | gpx11Doc.AuthorEmail.Id = parts[0]
431 | } else if len(parts) > 1 {
432 | gpx11Doc.AuthorEmail = new(gpx11GpxEmail)
433 | gpx11Doc.AuthorEmail.Id = parts[0]
434 | gpx11Doc.AuthorEmail.Domain = parts[1]
435 | }
436 | }
437 |
438 | if len(gpxDoc.AuthorLink) > 0 || len(gpxDoc.AuthorLinkText) > 0 || len(gpxDoc.AuthorLinkType) > 0 {
439 | gpx11Doc.AuthorLink = new(gpx11GpxLink)
440 | gpx11Doc.AuthorLink.Href = gpxDoc.AuthorLink
441 | gpx11Doc.AuthorLink.Text = gpxDoc.AuthorLinkText
442 | gpx11Doc.AuthorLink.Type = gpxDoc.AuthorLinkType
443 | }
444 |
445 | if len(gpxDoc.Copyright) > 0 || len(gpxDoc.CopyrightYear) > 0 || len(gpxDoc.CopyrightLicense) > 0 {
446 | gpx11Doc.Copyright = new(gpx11GpxCopyright)
447 | gpx11Doc.Copyright.Author = gpxDoc.Copyright
448 | gpx11Doc.Copyright.Year = gpxDoc.CopyrightYear
449 | gpx11Doc.Copyright.License = gpxDoc.CopyrightLicense
450 | }
451 |
452 | if len(gpxDoc.Link) > 0 || len(gpxDoc.LinkText) > 0 || len(gpxDoc.LinkType) > 0 {
453 | gpx11Doc.Link = new(gpx11GpxLink)
454 | gpx11Doc.Link.Href = gpxDoc.Link
455 | gpx11Doc.Link.Text = gpxDoc.LinkText
456 | gpx11Doc.Link.Type = gpxDoc.LinkType
457 | }
458 |
459 | if gpxDoc.Time != nil {
460 | gpx11Doc.Timestamp = formatGPXTime(gpxDoc.Time)
461 | }
462 |
463 | gpx11Doc.Keywords = gpxDoc.Keywords
464 |
465 | if gpxDoc.Waypoints != nil {
466 | gpx11Doc.Waypoints = make([]*gpx11GpxPoint, len(gpxDoc.Waypoints))
467 | for waypointNo, waypoint := range gpxDoc.Waypoints {
468 | gpx11Doc.Waypoints[waypointNo] = convertPointToGpx11(&waypoint)
469 | gpx11Doc.Waypoints[waypointNo].Extensions.globalNsAttrs = gpxDoc.Attrs.GetNamespaceAttrs()
470 | }
471 | }
472 |
473 | if gpxDoc.Routes != nil {
474 | gpx11Doc.Routes = make([]*gpx11GpxRte, len(gpxDoc.Routes))
475 | for routeNo, route := range gpxDoc.Routes {
476 | r := new(gpx11GpxRte)
477 | r.Name = route.Name
478 | r.Cmt = route.Comment
479 | r.Desc = route.Description
480 | r.Src = route.Source
481 | // TODO
482 | //r.Links = route.Links
483 | r.Number = route.Number
484 | r.Type = route.Type
485 | r.Extensions.globalNsAttrs = gpxDoc.Attrs.GetNamespaceAttrs()
486 |
487 | gpx11Doc.Routes[routeNo] = r
488 |
489 | if route.Points != nil {
490 | r.Points = make([]*gpx11GpxPoint, len(route.Points))
491 | for pointNo, point := range route.Points {
492 | r.Points[pointNo] = convertPointToGpx11(&point)
493 | r.Points[pointNo].Extensions.globalNsAttrs = gpxDoc.Attrs.GetNamespaceAttrs()
494 | }
495 | }
496 | }
497 | }
498 |
499 | if gpxDoc.Tracks != nil {
500 | gpx11Doc.Tracks = make([]*gpx11GpxTrk, len(gpxDoc.Tracks))
501 | for trackNo, track := range gpxDoc.Tracks {
502 | gpx11Track := new(gpx11GpxTrk)
503 | gpx11Track.Name = track.Name
504 | gpx11Track.Cmt = track.Comment
505 | gpx11Track.Desc = track.Description
506 | gpx11Track.Src = track.Source
507 | gpx11Track.Number = track.Number
508 | gpx11Track.Type = track.Type
509 | gpx11Track.Extensions.globalNsAttrs = gpxDoc.Attrs.GetNamespaceAttrs()
510 |
511 | if track.Segments != nil {
512 | gpx11Track.Segments = make([]*gpx11GpxTrkSeg, len(track.Segments))
513 | for segmentNo, segment := range track.Segments {
514 | gpx11Segment := new(gpx11GpxTrkSeg)
515 | gpx11Segment.Extensions.globalNsAttrs = gpxDoc.Attrs.GetNamespaceAttrs()
516 | if segment.Points != nil {
517 | gpx11Segment.Points = make([]*gpx11GpxPoint, len(segment.Points))
518 | for pointNo, point := range segment.Points {
519 | gpx11Segment.Points[pointNo] = convertPointToGpx11(&point)
520 | gpx11Segment.Points[pointNo].Extensions.globalNsAttrs = gpxDoc.Attrs.GetNamespaceAttrs()
521 | }
522 | }
523 | gpx11Track.Segments[segmentNo] = gpx11Segment
524 | }
525 | }
526 | gpx11Doc.Tracks[trackNo] = gpx11Track
527 | }
528 | }
529 |
530 | return gpx11Doc, replacements
531 | }
532 |
533 | func convertFromGpx11Models(gpx11Doc *gpx11Gpx) *GPX {
534 | gpxDoc := new(GPX)
535 |
536 | gpxDoc.Attrs = NewGPXAttributes(gpx11Doc.Attrs)
537 |
538 | gpxDoc.XMLNs = gpx11Doc.XMLNs
539 | gpxDoc.XmlNsXsi = gpx11Doc.XmlNsXsi
540 | gpxDoc.XmlSchemaLoc = gpx11Doc.XmlSchemaLoc
541 |
542 | gpxDoc.Creator = gpx11Doc.Creator
543 | gpxDoc.Version = gpx11Doc.Version
544 | gpxDoc.Name = gpx11Doc.Name
545 | gpxDoc.Description = gpx11Doc.Desc
546 | gpxDoc.AuthorName = gpx11Doc.AuthorName
547 | gpxDoc.Extensions = gpx11Doc.Extensions
548 | gpxDoc.MetadataExtensions = gpx11Doc.MetadataExtensions
549 |
550 | if gpx11Doc.AuthorEmail != nil {
551 | gpxDoc.AuthorEmail = gpx11Doc.AuthorEmail.Id + "@" + gpx11Doc.AuthorEmail.Domain
552 | }
553 | if gpx11Doc.AuthorLink != nil {
554 | gpxDoc.AuthorLink = gpx11Doc.AuthorLink.Href
555 | gpxDoc.AuthorLinkText = gpx11Doc.AuthorLink.Text
556 | gpxDoc.AuthorLinkType = gpx11Doc.AuthorLink.Type
557 | }
558 |
559 | /* TODO
560 | if gpx11Doc.Extensions != nil {
561 | gpxDoc.Extensions = &gpx11Doc.Extensions.Bytes
562 | }
563 | */
564 |
565 | if len(gpx11Doc.Timestamp) > 0 {
566 | gpxDoc.Time, _ = parseGPXTime(gpx11Doc.Timestamp)
567 | }
568 |
569 | if gpx11Doc.Copyright != nil {
570 | gpxDoc.Copyright = gpx11Doc.Copyright.Author
571 | gpxDoc.CopyrightYear = gpx11Doc.Copyright.Year
572 | gpxDoc.CopyrightLicense = gpx11Doc.Copyright.License
573 | }
574 |
575 | if gpx11Doc.Link != nil {
576 | gpxDoc.Link = gpx11Doc.Link.Href
577 | gpxDoc.LinkText = gpx11Doc.Link.Text
578 | gpxDoc.LinkType = gpx11Doc.Link.Type
579 | }
580 |
581 | gpxDoc.Keywords = gpx11Doc.Keywords
582 |
583 | if gpx11Doc.Waypoints != nil {
584 | waypoints := make([]GPXPoint, len(gpx11Doc.Waypoints))
585 | for waypointNo, waypoint := range gpx11Doc.Waypoints {
586 | waypoints[waypointNo] = *convertPointFromGpx11(waypoint)
587 | }
588 | gpxDoc.Waypoints = waypoints
589 | }
590 |
591 | if gpx11Doc.Routes != nil {
592 | gpxDoc.Routes = make([]GPXRoute, len(gpx11Doc.Routes))
593 | for routeNo, route := range gpx11Doc.Routes {
594 | r := new(GPXRoute)
595 |
596 | r.Name = route.Name
597 | r.Comment = route.Cmt
598 | r.Description = route.Desc
599 | r.Source = route.Src
600 | // TODO
601 | //r.Links = route.Links
602 | r.Number = route.Number
603 | r.Type = route.Type
604 | // TODO
605 | //r.RoutePoints = route.RoutePoints
606 | r.Extensions = route.Extensions
607 |
608 | if route.Points != nil {
609 | r.Points = make([]GPXPoint, len(route.Points))
610 | for pointNo, point := range route.Points {
611 | r.Points[pointNo] = *convertPointFromGpx11(point)
612 | }
613 | }
614 |
615 | gpxDoc.Routes[routeNo] = *r
616 | }
617 | }
618 |
619 | if gpx11Doc.Tracks != nil {
620 | gpxDoc.Tracks = make([]GPXTrack, len(gpx11Doc.Tracks))
621 | for trackNo, track := range gpx11Doc.Tracks {
622 | gpxTrack := new(GPXTrack)
623 | gpxTrack.Name = track.Name
624 | gpxTrack.Comment = track.Cmt
625 | gpxTrack.Description = track.Desc
626 | gpxTrack.Source = track.Src
627 | gpxTrack.Number = track.Number
628 | gpxTrack.Type = track.Type
629 | gpxTrack.Extensions = track.Extensions
630 |
631 | if track.Segments != nil {
632 | gpxTrack.Segments = make([]GPXTrackSegment, len(track.Segments))
633 | gpxTrack.Extensions = track.Extensions
634 | for segmentNo, segment := range track.Segments {
635 | gpxSegment := GPXTrackSegment{}
636 | gpxSegment.Extensions = segment.Extensions
637 | if segment.Points != nil {
638 | gpxSegment.Points = make([]GPXPoint, len(segment.Points))
639 | for pointNo, point := range segment.Points {
640 | gpxSegment.Points[pointNo] = *convertPointFromGpx11(point)
641 | }
642 | }
643 | gpxTrack.Segments[segmentNo] = gpxSegment
644 | }
645 | }
646 | gpxDoc.Tracks[trackNo] = *gpxTrack
647 | }
648 | }
649 |
650 | return gpxDoc
651 | }
652 |
653 | func convertPointToGpx11(original *GPXPoint) *gpx11GpxPoint {
654 | result := new(gpx11GpxPoint)
655 | result.Lat = formattedFloat(original.Latitude)
656 | result.Lon = formattedFloat(original.Longitude)
657 | result.Ele = original.Elevation
658 | result.Timestamp = formatGPXTime(&original.Timestamp)
659 | result.MagVar = original.MagneticVariation
660 | result.GeoIdHeight = original.GeoidHeight
661 | result.Name = original.Name
662 | result.Cmt = original.Comment
663 | result.Desc = original.Description
664 | result.Src = original.Source
665 | // TODO
666 | //w.Links = original.Links
667 | result.Sym = original.Symbol
668 | result.Type = original.Type
669 | result.Fix = original.TypeOfGpsFix
670 | result.Extensions = original.Extensions
671 | if original.Satellites.NotNull() {
672 | value := original.Satellites.Value()
673 | result.Sat = &value
674 | }
675 | if original.HorizontalDilution.NotNull() {
676 | value := original.HorizontalDilution.Value()
677 | result.Hdop = &value
678 | }
679 | if original.VerticalDilution.NotNull() {
680 | value := original.VerticalDilution.Value()
681 | result.Vdop = &value
682 | }
683 | if original.PositionalDilution.NotNull() {
684 | value := original.PositionalDilution.Value()
685 | result.Pdop = &value
686 | }
687 | if original.AgeOfDGpsData.NotNull() {
688 | value := original.AgeOfDGpsData.Value()
689 | result.AgeOfDGpsData = &value
690 | }
691 | if original.DGpsId.NotNull() {
692 | value := original.DGpsId.Value()
693 | result.DGpsId = &value
694 | }
695 | return result
696 | }
697 |
698 | func convertPointFromGpx11(original *gpx11GpxPoint) *GPXPoint {
699 | result := new(GPXPoint)
700 | result.Latitude = float64(original.Lat)
701 | result.Longitude = float64(original.Lon)
702 | result.Elevation = original.Ele
703 | time, _ := parseGPXTime(original.Timestamp)
704 | if time != nil {
705 | result.Timestamp = *time
706 | }
707 | result.MagneticVariation = original.MagVar
708 | result.GeoidHeight = original.GeoIdHeight
709 | result.Name = original.Name
710 | result.Comment = original.Cmt
711 | result.Description = original.Desc
712 | result.Source = original.Src
713 | // TODO
714 | //w.Links = original.Links
715 | result.Symbol = original.Sym
716 | result.Type = original.Type
717 | result.TypeOfGpsFix = original.Fix
718 | result.Extensions = original.Extensions
719 | if original.Sat != nil {
720 | result.Satellites = *NewNullableInt(*original.Sat)
721 | }
722 | if original.Hdop != nil {
723 | result.HorizontalDilution = *NewNullableFloat64(*original.Hdop)
724 | }
725 | if original.Vdop != nil {
726 | result.VerticalDilution = *NewNullableFloat64(*original.Vdop)
727 | }
728 | if original.Pdop != nil {
729 | result.PositionalDilution = *NewNullableFloat64(*original.Pdop)
730 | }
731 | if original.AgeOfDGpsData != nil {
732 | result.AgeOfDGpsData = *NewNullableFloat64(*original.AgeOfDGpsData)
733 | }
734 | if original.DGpsId != nil {
735 | result.DGpsId = *NewNullableInt(*original.DGpsId)
736 | }
737 | return result
738 | }
739 |
--------------------------------------------------------------------------------
/gpx/fixedpoint_float64.go:
--------------------------------------------------------------------------------
1 | package gpx
2 |
3 | import (
4 | "encoding/xml"
5 | "strconv"
6 | "strings"
7 | )
8 |
9 | // formattedFloat forces XML attributes to be marshalled as a fixed point decimal with 10 decimal places.
10 | type formattedFloat float64
11 |
12 | func (f formattedFloat) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
13 | s := strings.TrimRight(strconv.FormatFloat(float64(f), 'f', 10, 64), "0")
14 | if strings.HasSuffix(s, ".") {
15 | s += "0"
16 | }
17 | return xml.Attr{
18 | Name: xml.Name{Local: name.Local},
19 | Value: s,
20 | }, nil
21 | }
22 |
--------------------------------------------------------------------------------
/gpx/geo.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013, 2014 Peter Vasil, Tomo Krajina. All
2 | // rights reserved. Use of this source code is governed
3 | // by a BSD-style license that can be found in the
4 | // LICENSE file.
5 |
6 | package gpx
7 |
8 | import (
9 | "math"
10 | "sort"
11 | )
12 |
13 | const oneDegree = 1000.0 * 10000.8 / 90.0
14 | const earthRadius = 6371 * 1000
15 |
16 | //ToRad converts to radial coordinates
17 | func ToRad(x float64) float64 {
18 | return x / 180. * math.Pi
19 | }
20 |
21 | //Location implements an interface for all kinds of lat/long/elevation information
22 | type Location interface {
23 | GetLatitude() float64
24 | GetLongitude() float64
25 | GetElevation() NullableFloat64
26 | }
27 |
28 | //MovingData contains moving data
29 | type MovingData struct {
30 | MovingTime float64
31 | StoppedTime float64
32 | MovingDistance float64
33 | StoppedDistance float64
34 | MaxSpeed float64
35 | }
36 |
37 | //Equals compares to another MovingData struct
38 | func (md MovingData) Equals(md2 MovingData) bool {
39 | return md.MovingTime == md2.MovingTime &&
40 | md.MovingDistance == md2.MovingDistance &&
41 | md.StoppedTime == md2.StoppedTime &&
42 | md.StoppedDistance == md2.StoppedDistance &&
43 | md.MaxSpeed == md.MaxSpeed
44 | }
45 |
46 | //SpeedsAndDistances contaings speed/distance information
47 | type SpeedsAndDistances struct {
48 | Speed float64
49 | Distance float64
50 | }
51 |
52 | // HaversineDistance returns the haversine distance between two points.
53 | //
54 | // Implemented from http://www.movable-type.co.uk/scripts/latlong.html
55 | func HaversineDistance(lat1, lon1, lat2, lon2 float64) float64 {
56 | dLat := ToRad(lat1 - lat2)
57 | dLon := ToRad(lon1 - lon2)
58 | thisLat1 := ToRad(lat1)
59 | thisLat2 := ToRad(lat2)
60 |
61 | a := math.Sin(dLat/2)*math.Sin(dLat/2) + math.Sin(dLon/2)*math.Sin(dLon/2)*math.Cos(thisLat1)*math.Cos(thisLat2)
62 | c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
63 | d := earthRadius * c
64 |
65 | return d
66 | }
67 |
68 | func length(locs []Point, threeD bool) float64 {
69 | var previousLoc Point
70 | var res float64
71 | for k, v := range locs {
72 | if k > 0 {
73 | previousLoc = locs[k-1]
74 | var d float64
75 | if threeD {
76 | d = v.Distance3D(&previousLoc)
77 | } else {
78 | d = v.Distance2D(&previousLoc)
79 | }
80 | res += d
81 | }
82 | }
83 | return res
84 | }
85 |
86 | //Length2D calculates the lenght of given points list disregarding elevation
87 | func Length2D(locs []Point) float64 {
88 | return length(locs, false)
89 | }
90 |
91 | //Length3D calculates the lenght of given points list including elevation distance
92 | func Length3D(locs []Point) float64 {
93 | return length(locs, true)
94 | }
95 |
96 | //CalcMaxSpeed returns the maximum speed
97 | func CalcMaxSpeed(speedsDistances []SpeedsAndDistances) float64 {
98 | lenArrs := len(speedsDistances)
99 |
100 | if len(speedsDistances) < 3 {
101 | //log.Println("Segment too small to compute speed, size: ", lenArrs)
102 | return 0.0
103 | }
104 |
105 | var sumDists float64
106 | for _, d := range speedsDistances {
107 | sumDists += d.Distance
108 | }
109 | avgDist := sumDists / float64(lenArrs)
110 |
111 | var variance float64
112 | for i := 0; i < len(speedsDistances); i++ {
113 | variance += math.Pow(speedsDistances[i].Distance-avgDist, 2)
114 | }
115 | stdDeviation := math.Sqrt(variance)
116 |
117 | // ignore items with distance too long
118 | filteredSD := make([]SpeedsAndDistances, 0)
119 | for i := 0; i < len(speedsDistances); i++ {
120 | dist := math.Abs(speedsDistances[i].Distance - avgDist)
121 | if dist <= stdDeviation*1.5 {
122 | filteredSD = append(filteredSD, speedsDistances[i])
123 | }
124 | }
125 |
126 | speeds := make([]float64, len(filteredSD))
127 | for i, sd := range filteredSD {
128 | speeds[i] = sd.Speed
129 | }
130 |
131 | speedsSorted := sort.Float64Slice(speeds)
132 | sort.Sort(speedsSorted)
133 |
134 | if len(speedsSorted) == 0 {
135 | return 0
136 | }
137 |
138 | maxIdx := int(float64(len(speedsSorted)) * 0.95)
139 | if maxIdx >= len(speedsSorted) {
140 | maxIdx = len(speedsSorted) - 1
141 | }
142 | if maxIdx < 0 {
143 | maxIdx = 0
144 | }
145 | return speedsSorted[maxIdx]
146 | }
147 |
148 | //CalcUphillDownhill calculates uphill and downhill from given elevations
149 | func CalcUphillDownhill(elevations []NullableFloat64) (float64, float64) {
150 | elevsLen := len(elevations)
151 | if elevsLen == 0 {
152 | return 0.0, 0.0
153 | }
154 |
155 | smoothElevations := make([]NullableFloat64, elevsLen)
156 |
157 | for i, elev := range elevations {
158 | currEle := elev
159 | if 0 < i && i < elevsLen-1 {
160 | prevEle := elevations[i-1]
161 | nextEle := elevations[i+1]
162 | if prevEle.NotNull() && nextEle.NotNull() && elev.NotNull() {
163 | currEle = *NewNullableFloat64(prevEle.Value()*0.3 + elev.Value()*0.4 + nextEle.Value()*0.3)
164 | }
165 | }
166 | smoothElevations[i] = currEle
167 | }
168 |
169 | var uphill float64
170 | var downhill float64
171 |
172 | for i := 1; i < len(smoothElevations); i++ {
173 | if smoothElevations[i].NotNull() && smoothElevations[i-1].NotNull() {
174 | d := smoothElevations[i].Value() - smoothElevations[i-1].Value()
175 | if d > 0.0 {
176 | uphill += d
177 | } else {
178 | downhill -= d
179 | }
180 | }
181 | }
182 |
183 | return uphill, downhill
184 | }
185 |
186 | func distance(lat1, lon1 float64, ele1 NullableFloat64, lat2, lon2 float64, ele2 NullableFloat64, threeD, haversine bool) float64 {
187 | absLat := math.Abs(lat1 - lat2)
188 | absLon := math.Abs(lon1 - lon2)
189 | if haversine || absLat > 0.2 || absLon > 0.2 {
190 | return HaversineDistance(lat1, lon1, lat2, lon2)
191 | }
192 |
193 | coef := math.Cos(ToRad(lat1))
194 | x := lat1 - lat2
195 | y := (lon1 - lon2) * coef
196 |
197 | distance2d := math.Sqrt(x*x+y*y) * oneDegree
198 |
199 | if !threeD || ele1 == ele2 {
200 | return distance2d
201 | }
202 |
203 | eleDiff := 0.0
204 | if ele1.NotNull() && ele2.NotNull() {
205 | eleDiff = ele1.Value() - ele2.Value()
206 | }
207 |
208 | return math.Sqrt(math.Pow(distance2d, 2) + math.Pow(eleDiff, 2))
209 | }
210 |
211 | ////not used currently
212 | //func distanceBetweenLocations(loc1, loc2 Location, threeD, haversine bool) float64 {
213 | // lat1 := loc1.GetLatitude()
214 | // lon1 := loc1.GetLongitude()
215 | // ele1 := loc1.GetElevation()
216 | //
217 | // lat2 := loc2.GetLatitude()
218 | // lon2 := loc2.GetLongitude()
219 | // ele2 := loc2.GetElevation()
220 | //
221 | // return distance(lat1, lon1, ele1, lat2, lon2, ele2, threeD, haversine)
222 | //}
223 |
224 | //Distance2D calculates the distance of 2 geo coordinates
225 | func Distance2D(lat1, lon1, lat2, lon2 float64, haversine bool) float64 {
226 | return distance(lat1, lon1, *new(NullableFloat64), lat2, lon2, *new(NullableFloat64), false, haversine)
227 | }
228 |
229 | //Distance3D calculates the distance of 2 geo coordinates including elevation distance
230 | func Distance3D(lat1, lon1 float64, ele1 NullableFloat64, lat2, lon2 float64, ele2 NullableFloat64, haversine bool) float64 {
231 | return distance(lat1, lon1, ele1, lat2, lon2, ele2, true, haversine)
232 | }
233 |
234 | func AngleFromNorth(loc1, loc2 Point, radians bool) float64 {
235 | coef := math.Cos(ToRad(loc1.Latitude))
236 |
237 | b := (loc2.Longitude - loc1.Longitude) * coef
238 | a := loc2.Latitude - loc1.Latitude
239 |
240 | angle := math.Atan(b / a)
241 |
242 | if a < 0 {
243 | angle += math.Pi
244 | }
245 |
246 | if angle < 0 {
247 | angle += 2 * math.Pi
248 | }
249 |
250 | if radians {
251 | return angle
252 | }
253 |
254 | return 180 * angle / math.Pi
255 | }
256 |
257 | //ElevationAngle calculates the elevation angle (steepness) between to points
258 | func ElevationAngle(loc1, loc2 Point, radians bool) float64 {
259 | if loc1.Elevation.Null() || loc2.Elevation.Null() {
260 | return 0.0
261 | }
262 |
263 | b := loc2.Elevation.Value() - loc1.Elevation.Value()
264 | a := loc2.Distance2D(&loc1)
265 |
266 | if a == 0.0 {
267 | return 0.0
268 | }
269 |
270 | angle := math.Atan(b / a)
271 |
272 | if radians {
273 | return angle
274 | }
275 |
276 | return 180 * angle / math.Pi
277 | }
278 |
279 | // Distance of point from a line given with two points.
280 | func distanceFromLine(point Point, linePoint1, linePoint2 GPXPoint) float64 {
281 | a := linePoint1.Distance2D(&linePoint2)
282 |
283 | if a == 0 {
284 | return linePoint1.Distance2D(&point)
285 | }
286 |
287 | b := linePoint1.Distance2D(&point)
288 | c := linePoint2.Distance2D(&point)
289 |
290 | s := (a + b + c) / 2.
291 |
292 | return 2.0 * math.Sqrt(math.Abs(s*(s-a)*(s-b)*(s-c))) / a
293 | }
294 |
295 | func getLineEquationCoefficients(location1, location2 Point) (float64, float64, float64) {
296 | if location1.Longitude == location2.Longitude {
297 | // Vertical line:
298 | return 0.0, 1.0, -location1.Longitude
299 | } else {
300 | a := (location1.Latitude - location2.Latitude) / (location1.Longitude - location2.Longitude)
301 | b := location1.Latitude - location1.Longitude*a
302 | return 1.0, -a, -b
303 | }
304 | }
305 |
306 | func simplifyPoints(points []GPXPoint, maxDistance float64) []GPXPoint {
307 | if len(points) < 3 {
308 | return points
309 | }
310 |
311 | begin, end := points[0], points[len(points)-1]
312 |
313 | /*
314 | Use a "normal" line just to detect the most distant point (not its real distance)
315 | this is because this is faster to compute than calling distance_from_line() for
316 | every point.
317 |
318 | This is an approximation and may have some errors near the poles and if
319 | the points are too distant, but it should be good enough for most use
320 | cases...
321 | */
322 | a, b, c := getLineEquationCoefficients(begin.Point, end.Point)
323 |
324 | tmpMaxDistance := -1000000000.0
325 | tmpMaxDistancePosition := 0
326 | for pointNo, point := range points {
327 | d := math.Abs(a*point.Latitude + b*point.Longitude + c)
328 | if d > tmpMaxDistance {
329 | tmpMaxDistance = d
330 | tmpMaxDistancePosition = pointNo
331 | }
332 | }
333 |
334 | //fmt.Println()
335 |
336 | //fmt.Println("tmpMaxDistancePosition=", tmpMaxDistancePosition, " len(points)=", len(points))
337 |
338 | realMaxDistance := distanceFromLine(points[tmpMaxDistancePosition].Point, begin, end)
339 | //fmt.Println("realMaxDistance=", realMaxDistance, " len(points)=", len(points))
340 |
341 | if realMaxDistance < maxDistance {
342 | return []GPXPoint{begin, end}
343 | }
344 |
345 | points1 := points[:tmpMaxDistancePosition]
346 | point := points[tmpMaxDistancePosition]
347 | points2 := points[tmpMaxDistancePosition+1:]
348 |
349 | //fmt.Println("before simplify: len_points=", len(points), " l_points1=", len(points1), " l_points2=", len(points2))
350 |
351 | points1 = simplifyPoints(points1, maxDistance)
352 | points2 = simplifyPoints(points2, maxDistance)
353 |
354 | //fmt.Println("after simplify: len_points=", len(points), " l_points1=", len(points1), " l_points2=", len(points2))
355 |
356 | result := append(points1, point)
357 | return append(result, points2...)
358 | }
359 |
360 | func smoothHorizontal(originalPoints []GPXPoint) []GPXPoint {
361 | result := make([]GPXPoint, len(originalPoints))
362 |
363 | for pointNo, point := range originalPoints {
364 | result[pointNo] = point
365 | if 1 <= pointNo && pointNo <= len(originalPoints)-2 {
366 | previousPoint := originalPoints[pointNo-1]
367 | nextPoint := originalPoints[pointNo+1]
368 | result[pointNo] = point
369 | result[pointNo].Latitude = previousPoint.Latitude*0.4 + point.Latitude*0.2 + nextPoint.Latitude*0.4
370 | result[pointNo].Longitude = previousPoint.Longitude*0.4 + point.Longitude*0.2 + nextPoint.Longitude*0.4
371 | //log.Println("->(%f, %f)", seg.Points[pointNo].Latitude, seg.Points[pointNo].Longitude)
372 | }
373 | }
374 |
375 | return result
376 | }
377 |
378 | func smoothVertical(originalPoints []GPXPoint) []GPXPoint {
379 | result := make([]GPXPoint, len(originalPoints))
380 |
381 | for pointNo, point := range originalPoints {
382 | result[pointNo] = point
383 | if 1 <= pointNo && pointNo <= len(originalPoints)-2 {
384 | previousPointElevation := originalPoints[pointNo-1].Elevation
385 | nextPointElevation := originalPoints[pointNo+1].Elevation
386 | if previousPointElevation.NotNull() && point.Elevation.NotNull() && nextPointElevation.NotNull() {
387 | result[pointNo].Elevation = *NewNullableFloat64(previousPointElevation.Value()*0.4 + point.Elevation.Value()*0.2 + nextPointElevation.Value()*0.4)
388 | //log.Println("->%f", seg.Points[pointNo].Elevation.Value())
389 | }
390 | }
391 | }
392 |
393 | return result
394 | }
395 |
--------------------------------------------------------------------------------
/gpx/geo_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013, 2014 Peter Vasil, Tomo Krajina. All
2 | // rights reserved. Use of this source code is governed
3 | // by a BSD-style license that can be found in the
4 | // LICENSE file.
5 |
6 | package gpx
7 |
8 | import (
9 | "math"
10 | "testing"
11 |
12 | "github.com/stretchr/testify/assert"
13 | )
14 |
15 | func TestToRad(t *testing.T) {
16 | radVal := ToRad(360)
17 | if radVal != math.Pi*2 {
18 | t.Errorf("Test failed: %f", radVal)
19 | }
20 | }
21 |
22 | func TestElevationAngle(t *testing.T) {
23 | loc1 := Point{Latitude: 52.5113534275, Longitude: 13.4571944922, Elevation: *NewNullableFloat64(59.26)}
24 | loc2 := Point{Latitude: 52.5113568641, Longitude: 13.4571697656, Elevation: *NewNullableFloat64(65.51)}
25 |
26 | elevAngleA := ElevationAngle(loc1, loc2, false)
27 | elevAngleE := 74.65347905197362
28 |
29 | if elevAngleE != elevAngleA {
30 | t.Errorf("Elevation angle expected: %f, actual: %f", elevAngleE, elevAngleA)
31 | }
32 | }
33 |
34 | func TestMaxSpeed(t *testing.T) {
35 | t.Parallel()
36 |
37 | maxSpeed := CalcMaxSpeed([]SpeedsAndDistances{
38 | {Speed: 5.0, Distance: 508.674260463},
39 | {Speed: 4.0, Distance: 593.443625286},
40 | {Speed: 6.0, Distance: 523.841129461},
41 | {Speed: 1.0, Distance: 489.306355103},
42 | })
43 | assert.Equal(t, 6.0, maxSpeed)
44 | }
45 |
--------------------------------------------------------------------------------
/gpx/gpx10.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013, 2014 Peter Vasil, Tomo Krajina. All
2 | // rights reserved. Use of this source code is governed
3 | // by a BSD-style license that can be found in the
4 | // LICENSE file.
5 |
6 | package gpx
7 |
8 | import (
9 | "encoding/xml"
10 | )
11 |
12 | /*
13 |
14 | The GPX XML hierarchy:
15 |
16 | gpx
17 | - attr: version (xsd:string) required
18 | - attr: creator (xsd:string) required
19 | name
20 | desc
21 | author
22 | email
23 | url
24 | urlname
25 | time
26 | keywords
27 | bounds
28 | wpt
29 | - attr: lat (gpx:latitudeType) required
30 | - attr: lon (gpx:longitudeType) required
31 | ele
32 | time
33 | magvar
34 | geoidheight
35 | name
36 | cmt
37 | desc
38 | src
39 | url
40 | urlname
41 | sym
42 | type
43 | fix
44 | sat
45 | hdop
46 | vdop
47 | pdop
48 | ageofdgpsdata
49 | dgpsid
50 | rte
51 | name
52 | cmt
53 | desc
54 | src
55 | url
56 | urlname
57 | number
58 | rtept
59 | - attr: lat (gpx:latitudeType) required
60 | - attr: lon (gpx:longitudeType) required
61 | ele
62 | time
63 | magvar
64 | geoidheight
65 | name
66 | cmt
67 | desc
68 | src
69 | url
70 | urlname
71 | sym
72 | type
73 | fix
74 | sat
75 | hdop
76 | vdop
77 | pdop
78 | ageofdgpsdata
79 | dgpsid
80 | trk
81 | name
82 | cmt
83 | desc
84 | src
85 | url
86 | urlname
87 | number
88 | trkseg
89 | trkpt
90 | - attr: lat (gpx:latitudeType) required
91 | - attr: lon (gpx:longitudeType) required
92 | ele
93 | time
94 | course
95 | speed
96 | magvar
97 | geoidheight
98 | name
99 | cmt
100 | desc
101 | src
102 | url
103 | urlname
104 | sym
105 | type
106 | fix
107 | sat
108 | hdop
109 | vdop
110 | pdop
111 | ageofdgpsdata
112 | dgpsid
113 | */
114 |
115 | type gpx10Gpx struct {
116 | XMLName xml.Name `xml:"gpx"`
117 | Attrs []xml.Attr `xml:",any,attr"`
118 | XMLNs string `xml:"xmlns,attr,omitempty"`
119 | XmlNsXsi string `xml:"xmlns:xsi,attr,omitempty"`
120 | XmlSchemaLoc string `xml:"xsi:schemaLocation,attr,omitempty"`
121 |
122 | Version string `xml:"version,attr"`
123 | Creator string `xml:"creator,attr"`
124 | Name string `xml:"name,omitempty"`
125 | Desc string `xml:"desc,omitempty"`
126 | Author string `xml:"author,omitempty"`
127 | Email string `xml:"email,omitempty"`
128 | Url string `xml:"url,omitempty"`
129 | UrlName string `xml:"urlname,omitempty"`
130 | Time string `xml:"time,omitempty"`
131 | Keywords string `xml:"keywords,omitempty"`
132 | Bounds *GpxBounds `xml:"bounds"`
133 | Waypoints []*gpx10GpxPoint `xml:"wpt"`
134 | Routes []*gpx10GpxRte `xml:"rte"`
135 | Tracks []*gpx10GpxTrk `xml:"trk"`
136 | }
137 |
138 | //type gpx10GpxBounds struct {
139 | // //XMLName xml.Name `xml:"bounds"`
140 | // MinLat float64 `xml:"minlat,attr"`
141 | // MaxLat float64 `xml:"maxlat,attr"`
142 | // MinLon float64 `xml:"minlon,attr"`
143 | // MaxLon float64 `xml:"maxlon,attr"`
144 | //}
145 |
146 | //type gpx10GpxAuthor struct {
147 | // Name string `xml:"name,omitempty"`
148 | // Email string `xml:"email,omitempty"`
149 | // Link *gpx10GpxLink `xml:"link"`
150 | //}
151 |
152 | //type gpx10GpxEmail struct {
153 | // Id string `xml:"id,attr"`
154 | // Domain string `xml:"domain,attr"`
155 | //}
156 |
157 | type gpx10GpxLink struct {
158 | Href string `xml:"href,attr"`
159 | Text string `xml:"text,omitempty"`
160 | Type string `xml:"type,omitempty"`
161 | }
162 |
163 | //type gpx10GpxMetadata struct {
164 | // XMLName xml.Name `xml:"metadata"`
165 | // Name string `xml:"name,omitempty"`
166 | // Desc string `xml:"desc,omitempty"`
167 | // Author *gpx10GpxAuthor `xml:"author,omitempty"`
168 | // // Links []GpxLink `xml:"link"`
169 | // Timestamp string `xml:"time,omitempty"`
170 | // Keywords string `xml:"keywords,omitempty"`
171 | // // Bounds *GpxBounds `xml:"bounds"`
172 | //}
173 |
174 | //type gpx10GpxExtensions struct {
175 | // Bytes []byte `xml:",innerxml"`
176 | //}
177 |
178 | /**
179 | * Common struct fields for all points
180 | */
181 | type gpx10GpxPoint struct {
182 | Lat formattedFloat `xml:"lat,attr"`
183 | Lon formattedFloat `xml:"lon,attr"`
184 | // Position info
185 | Ele NullableFloat64 `xml:"ele,omitempty"`
186 | Timestamp string `xml:"time,omitempty"`
187 | MagVar string `xml:"magvar,omitempty"`
188 | GeoIdHeight string `xml:"geoidheight,omitempty"`
189 | // Description info
190 | Name string `xml:"name,omitempty"`
191 | Cmt string `xml:"cmt,omitempty"`
192 | Desc string `xml:"desc,omitempty"`
193 | Src string `xml:"src,omitempty"`
194 | Links []gpx10GpxLink `xml:"link"`
195 | Sym string `xml:"sym,omitempty"`
196 | Type string `xml:"type,omitempty"`
197 | // Accuracy info
198 | Fix string `xml:"fix,omitempty"`
199 | Sat *int `xml:"sat,omitempty"`
200 | Hdop *float64 `xml:"hdop,omitempty"`
201 | Vdop *float64 `xml:"vdop,omitempty"`
202 | Pdop *float64 `xml:"pdop,omitempty"`
203 | AgeOfDGpsData *float64 `xml:"ageofdgpsdata,omitempty"`
204 | DGpsId *int `xml:"dgpsid,omitempty"`
205 |
206 | // Those two values are here for simplicity, but they are available only when this is part of a track segment (not route or waypoint)!
207 | Course string `xml:"course,omitempty"`
208 | Speed string `speed:"speed,omitempty"`
209 | }
210 |
211 | type gpx10GpxRte struct {
212 | XMLName xml.Name `xml:"rte"`
213 | Name string `xml:"name,omitempty"`
214 | Cmt string `xml:"cmt,omitempty"`
215 | Desc string `xml:"desc,omitempty"`
216 | Src string `xml:"src,omitempty"`
217 | // TODO
218 | //Links []Link `xml:"link"`
219 | Number NullableInt `xml:"number,omitempty"`
220 | Type string `xml:"type,omitempty"`
221 | Points []*gpx10GpxPoint `xml:"rtept"`
222 | }
223 |
224 | type gpx10GpxTrkSeg struct {
225 | XMLName xml.Name `xml:"trkseg"`
226 | Points []*gpx10GpxPoint `xml:"trkpt"`
227 | }
228 |
229 | // Trk is a GPX track
230 | type gpx10GpxTrk struct {
231 | XMLName xml.Name `xml:"trk"`
232 | Name string `xml:"name,omitempty"`
233 | Cmt string `xml:"cmt,omitempty"`
234 | Desc string `xml:"desc,omitempty"`
235 | Src string `xml:"src,omitempty"`
236 | // TODO
237 | //Links []Link `xml:"link"`
238 | Number NullableInt `xml:"number,omitempty"`
239 | Type string `xml:"type,omitempty"`
240 | Segments []*gpx10GpxTrkSeg `xml:"trkseg,omitempty"`
241 | }
242 |
--------------------------------------------------------------------------------
/gpx/gpx11.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013, 2014 Peter Vasil, Tomo Krajina. All
2 | // rights reserved. Use of this source code is governed
3 | // by a BSD-style license that can be found in the
4 | // LICENSE file.
5 |
6 | package gpx
7 |
8 | import (
9 | "encoding/xml"
10 | )
11 |
12 | /*
13 |
14 | The GPX XML hierarchy:
15 |
16 | gpx (gpxType)
17 | - attr: version (xsd:string) None
18 | - attr: creator (xsd:string) None
19 | metadata (metadataType)
20 | name (xsd:string)
21 | desc (xsd:string)
22 | author (personType)
23 | name (xsd:string)
24 | email (emailType)
25 | - attr: id (xsd:string) None
26 | - attr: domain (xsd:string) None
27 | link (linkType)
28 | - attr: href (xsd:anyURI) None
29 | text (xsd:string)
30 | type (xsd:string)
31 | copyright (copyrightType)
32 | - attr: author (xsd:string) None
33 | year (xsd:gYear)
34 | license (xsd:anyURI)
35 | link (linkType)
36 | - attr: href (xsd:anyURI) None
37 | text (xsd:string)
38 | type (xsd:string)
39 | time (xsd:dateTime)
40 | keywords (xsd:string)
41 | bounds (boundsType)
42 | - attr: minlat (latitudeType) None
43 | - attr: minlon (longitudeType) None
44 | - attr: maxlat (latitudeType) None
45 | - attr: maxlon (longitudeType) None
46 | extensions (extensionsType)
47 | wpt (wptType)
48 | - attr: lat (latitudeType) None
49 | - attr: lon (longitudeType) None
50 | ele (xsd:decimal)
51 | time (xsd:dateTime)
52 | magvar (degreesType)
53 | geoidheight (xsd:decimal)
54 | name (xsd:string)
55 | cmt (xsd:string)
56 | desc (xsd:string)
57 | src (xsd:string)
58 | link (linkType)
59 | - attr: href (xsd:anyURI) None
60 | text (xsd:string)
61 | type (xsd:string)
62 | sym (xsd:string)
63 | type (xsd:string)
64 | fix (fixType)
65 | sat (xsd:nonNegativeInteger)
66 | hdop (xsd:decimal)
67 | vdop (xsd:decimal)
68 | pdop (xsd:decimal)
69 | ageofdgpsdata (xsd:decimal)
70 | dgpsid (dgpsStationType)
71 | extensions (extensionsType)
72 | rte (rteType)
73 | name (xsd:string)
74 | cmt (xsd:string)
75 | desc (xsd:string)
76 | src (xsd:string)
77 | link (linkType)
78 | - attr: href (xsd:anyURI) None
79 | text (xsd:string)
80 | type (xsd:string)
81 | number (xsd:nonNegativeInteger)
82 | type (xsd:string)
83 | extensions (extensionsType)
84 | rtept (wptType)
85 | - attr: lat (latitudeType) None
86 | - attr: lon (longitudeType) None
87 | ele (xsd:decimal)
88 | time (xsd:dateTime)
89 | magvar (degreesType)
90 | geoidheight (xsd:decimal)
91 | name (xsd:string)
92 | cmt (xsd:string)
93 | desc (xsd:string)
94 | src (xsd:string)
95 | link (linkType)
96 | - attr: href (xsd:anyURI) None
97 | text (xsd:string)
98 | type (xsd:string)
99 | sym (xsd:string)
100 | type (xsd:string)
101 | fix (fixType)
102 | sat (xsd:nonNegativeInteger)
103 | hdop (xsd:decimal)
104 | vdop (xsd:decimal)
105 | pdop (xsd:decimal)
106 | ageofdgpsdata (xsd:decimal)
107 | dgpsid (dgpsStationType)
108 | extensions (extensionsType)
109 | trk (trkType)
110 | name (xsd:string)
111 | cmt (xsd:string)
112 | desc (xsd:string)
113 | src (xsd:string)
114 | link (linkType)
115 | - attr: href (xsd:anyURI) None
116 | text (xsd:string)
117 | type (xsd:string)
118 | number (xsd:nonNegativeInteger)
119 | type (xsd:string)
120 | extensions (extensionsType)
121 | trkseg (trksegType)
122 | trkpt (wptType)
123 | - attr: lat (latitudeType) None
124 | - attr: lon (longitudeType) None
125 | ele (xsd:decimal)
126 | time (xsd:dateTime)
127 | magvar (degreesType)
128 | geoidheight (xsd:decimal)
129 | name (xsd:string)
130 | cmt (xsd:string)
131 | desc (xsd:string)
132 | src (xsd:string)
133 | link (linkType)
134 | - attr: href (xsd:anyURI) None
135 | text (xsd:string)
136 | type (xsd:string)
137 | sym (xsd:string)
138 | type (xsd:string)
139 | fix (fixType)
140 | sat (xsd:nonNegativeInteger)
141 | hdop (xsd:decimal)
142 | vdop (xsd:decimal)
143 | pdop (xsd:decimal)
144 | ageofdgpsdata (xsd:decimal)
145 | dgpsid (dgpsStationType)
146 | extensions (extensionsType)
147 | extensions (extensionsType)
148 | extensions (extensionsType)
149 | */
150 |
151 | type gpx11Gpx struct {
152 | XMLName xml.Name `xml:"gpx"`
153 | Attrs []xml.Attr `xml:",any,attr"`
154 | XMLNs string `xml:"xmlns,attr,omitempty"`
155 | XmlNsXsi string `xml:"xmlns:xsi,attr,omitempty"`
156 | XmlSchemaLoc string `xml:"xsi:schemaLocation,attr,omitempty"`
157 |
158 | Version string `xml:"version,attr"`
159 | Creator string `xml:"creator,attr"`
160 | Name string `xml:"metadata>name,omitempty"`
161 | Desc string `xml:"metadata>desc,omitempty"`
162 | AuthorName string `xml:"metadata>author>name,omitempty"`
163 | AuthorEmail *gpx11GpxEmail `xml:"metadata>author>email,omitempty"`
164 | // TODO: There can be more than one link?
165 | AuthorLink *gpx11GpxLink `xml:"metadata>author>link,omitempty"`
166 | Copyright *gpx11GpxCopyright `xml:"metadata>copyright,omitempty"`
167 | Link *gpx11GpxLink `xml:"metadata>link,omitempty"`
168 | Timestamp string `xml:"metadata>time,omitempty"`
169 | Keywords string `xml:"metadata>keywords,omitempty"`
170 | MetadataExtensions Extension `xml:"metadata>extensions"`
171 | Bounds *gpx11GpxBounds `xml:"bounds"`
172 | Waypoints []*gpx11GpxPoint `xml:"wpt"`
173 | Routes []*gpx11GpxRte `xml:"rte"`
174 | Tracks []*gpx11GpxTrk `xml:"trk"`
175 | Extensions Extension `xml:"extensions"`
176 | }
177 |
178 | type gpx11GpxBounds struct {
179 | //XMLName xml.Name `xml:"bounds"`
180 | MinLat formattedFloat `xml:"minlat,attr"`
181 | MaxLat formattedFloat `xml:"maxlat,attr"`
182 | MinLon formattedFloat `xml:"minlon,attr"`
183 | MaxLon formattedFloat `xml:"maxlon,attr"`
184 | }
185 |
186 | type gpx11GpxCopyright struct {
187 | XMLName xml.Name `xml:"copyright"`
188 | Author string `xml:"author,attr"`
189 | Year string `xml:"year,omitempty"`
190 | License string `xml:"license,omitempty"`
191 | }
192 |
193 | //type gpx11GpxAuthor struct {
194 | // Name string `xml:"name,omitempty"`
195 | // Email string `xml:"email,omitempty"`
196 | // Link *gpx11GpxLink `xml:"link"`
197 | //}
198 |
199 | type gpx11GpxEmail struct {
200 | Id string `xml:"id,attr"`
201 | Domain string `xml:"domain,attr"`
202 | }
203 |
204 | type gpx11GpxLink struct {
205 | Href string `xml:"href,attr"`
206 | Text string `xml:"text,omitempty"`
207 | Type string `xml:"type,omitempty"`
208 | }
209 |
210 | //type gpx11GpxMetadata struct {
211 | // XMLName xml.Name `xml:"metadata"`
212 | // Name string `xml:"name,omitempty"`
213 | // Desc string `xml:"desc,omitempty"`
214 | // Author *gpx11GpxAuthor `xml:"author,omitempty"`
215 | // // Copyright *GpxCopyright `xml:"copyright,omitempty"`
216 | // // Links []GpxLink `xml:"link"`
217 | // Timestamp string `xml:"time,omitempty"`
218 | // Keywords string `xml:"keywords,omitempty"`
219 | // // Bounds *GpxBounds `xml:"bounds"`
220 | //}
221 |
222 | /**
223 | * Common struct fields for all points
224 | */
225 | type gpx11GpxPoint struct {
226 | Lat formattedFloat `xml:"lat,attr"`
227 | Lon formattedFloat `xml:"lon,attr"`
228 | // Position info
229 | Ele NullableFloat64 `xml:"ele,omitempty"`
230 | Timestamp string `xml:"time,omitempty"`
231 | MagVar string `xml:"magvar,omitempty"`
232 | GeoIdHeight string `xml:"geoidheight,omitempty"`
233 | // Description info
234 | Name string `xml:"name,omitempty"`
235 | Cmt string `xml:"cmt,omitempty"`
236 | Desc string `xml:"desc,omitempty"`
237 | Src string `xml:"src,omitempty"`
238 | Links []gpx11GpxLink `xml:"link"`
239 | Sym string `xml:"sym,omitempty"`
240 | Type string `xml:"type,omitempty"`
241 | // Accuracy info
242 | Fix string `xml:"fix,omitempty"`
243 | Sat *int `xml:"sat,omitempty"`
244 | Hdop *float64 `xml:"hdop,omitempty"`
245 | Vdop *float64 `xml:"vdop,omitempty"`
246 | Pdop *float64 `xml:"pdop,omitempty"`
247 | AgeOfDGpsData *float64 `xml:"ageofdgpsdata,omitempty"`
248 | DGpsId *int `xml:"dgpsid,omitempty"`
249 | Extensions Extension `xml:"extensions"`
250 | }
251 |
252 | type gpx11GpxRte struct {
253 | XMLName xml.Name `xml:"rte"`
254 | Name string `xml:"name,omitempty"`
255 | Cmt string `xml:"cmt,omitempty"`
256 | Desc string `xml:"desc,omitempty"`
257 | Src string `xml:"src,omitempty"`
258 | // TODO
259 | //Links []Link `xml:"link"`
260 | Number NullableInt `xml:"number,omitempty"`
261 | Type string `xml:"type,omitempty"`
262 | Points []*gpx11GpxPoint `xml:"rtept"`
263 | Extensions Extension `xml:"extensions"`
264 | }
265 |
266 | type gpx11GpxTrkSeg struct {
267 | XMLName xml.Name `xml:"trkseg"`
268 | Points []*gpx11GpxPoint `xml:"trkpt"`
269 | Extensions Extension `xml:"extensions"`
270 | }
271 |
272 | // Trk is a GPX track
273 | type gpx11GpxTrk struct {
274 | XMLName xml.Name `xml:"trk"`
275 | Name string `xml:"name,omitempty"`
276 | Cmt string `xml:"cmt,omitempty"`
277 | Desc string `xml:"desc,omitempty"`
278 | Src string `xml:"src,omitempty"`
279 | // TODO
280 | //Links []Link `xml:"link"`
281 | Number NullableInt `xml:"number,omitempty"`
282 | Type string `xml:"type,omitempty"`
283 | Segments []*gpx11GpxTrkSeg `xml:"trkseg,omitempty"`
284 | Extensions Extension `xml:"extensions"`
285 | }
286 |
--------------------------------------------------------------------------------
/gpx/gpx11_extensions.go:
--------------------------------------------------------------------------------
1 | package gpx
2 |
3 | import (
4 | "encoding/xml"
5 | "strings"
6 | )
7 |
8 | type ExtensionNode struct {
9 | XMLName xml.Name
10 | Attrs []xml.Attr `xml:",any,attr"`
11 | Data string `xml:",chardata"`
12 | Nodes []ExtensionNode `xml:",any"`
13 | }
14 |
15 | func (n ExtensionNode) debugXMLChunk() []byte {
16 | byts, err := xml.MarshalIndent(n, "", " ")
17 | if err != nil {
18 | return []byte("???")
19 | }
20 | return byts
21 | }
22 |
23 | func (n ExtensionNode) toTokens(prefix string) (tokens []xml.Token) {
24 | var attrs []xml.Attr
25 | for _, a := range n.Attrs {
26 | attrs = append(attrs, xml.Attr{Name: xml.Name{Local: prefix + a.Name.Local}, Value: a.Value})
27 | }
28 |
29 | start := xml.StartElement{Name: xml.Name{Local: prefix + n.XMLName.Local, Space: ""}, Attr: attrs}
30 | tokens = append(tokens, start)
31 | data := strings.TrimSpace(n.Data)
32 | if len(n.Nodes) > 0 {
33 | for _, node := range n.Nodes {
34 | tokens = append(tokens, node.toTokens(prefix)...)
35 | }
36 | } else if data != "" {
37 | tokens = append(tokens, xml.CharData(data))
38 | } else {
39 | return nil
40 | }
41 | tokens = append(tokens, xml.EndElement{start.Name})
42 | return
43 | }
44 |
45 | func (n *ExtensionNode) GetAttr(key string) (value string, found bool) {
46 | for i := range n.Attrs {
47 | if n.Attrs[i].Name.Local == key {
48 | value = n.Attrs[i].Value
49 | found = true
50 | return
51 | }
52 | }
53 | return
54 | }
55 |
56 | func (n *ExtensionNode) SetAttr(key, value string) {
57 | for i := range n.Attrs {
58 | if n.Attrs[i].Name.Local == key {
59 | n.Attrs[i].Value = value
60 | return
61 | }
62 | }
63 | n.Attrs = append(n.Attrs, xml.Attr{
64 | Name: xml.Name{
65 | Space: n.SpaceNameURL(),
66 | Local: key,
67 | },
68 | Value: value,
69 | })
70 | }
71 |
72 | func (n *ExtensionNode) GetNode(path0 string) (node *ExtensionNode, found bool) {
73 | for subn := range n.Nodes {
74 | if n.Nodes[subn].LocalName() == path0 {
75 | node = &n.Nodes[subn]
76 | found = true
77 | return
78 | }
79 | }
80 | return
81 | }
82 |
83 | func (n *ExtensionNode) GetOrCreateNode(path ...string) *ExtensionNode {
84 | if len(path) == 0 {
85 | return n
86 | }
87 |
88 | path0, rest := path[0], path[1:]
89 |
90 | subNode, found := n.GetNode(path0)
91 | if !found {
92 | n.Nodes = append(n.Nodes, ExtensionNode{
93 | XMLName: xml.Name{
94 | Space: n.XMLName.Space,
95 | Local: path0,
96 | },
97 | Attrs: nil,
98 | })
99 | subNode = &(n.Nodes[len(n.Nodes)-1])
100 | }
101 |
102 | return subNode.GetOrCreateNode(rest...)
103 | }
104 |
105 | func (n ExtensionNode) IsEmpty() bool {
106 | return len(n.Nodes) == 0 && len(n.Attrs) == 0 && len(n.Data) == 0
107 | }
108 | func (n ExtensionNode) LocalName() string { return n.XMLName.Local }
109 | func (n ExtensionNode) SpaceNameURL() string { return n.XMLName.Space }
110 | func (n ExtensionNode) GetAttrOrEmpty(attr string) string {
111 | val, _ := n.GetAttr(attr)
112 | return val
113 | }
114 |
115 | type Extension struct {
116 | // XMLName xml.Name
117 | // Attrs []xml.Attr `xml:",any,attr"`
118 | Nodes []ExtensionNode `xml:",any"`
119 |
120 | // Filled before deserializing:
121 | globalNsAttrs map[string]NamespaceAttribute
122 | }
123 |
124 | var _ xml.Marshaler = Extension{}
125 |
126 | func (ex Extension) debugXMLChunk() []byte {
127 | byts, err := xml.MarshalIndent(ex, "", " ")
128 | if err != nil {
129 | return []byte("???")
130 | }
131 | return byts
132 | }
133 |
134 | func (ex Extension) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
135 | if len(ex.Nodes) == 0 {
136 | return nil
137 | }
138 |
139 | start = xml.StartElement{Name: xml.Name{Local: start.Name.Local}, Attr: nil}
140 | tokens := []xml.Token{start}
141 | for _, node := range ex.Nodes {
142 | prefix := ""
143 | for _, v := range ex.globalNsAttrs {
144 | if node.SpaceNameURL() == v.Value || node.SpaceNameURL() == v.Name.Local {
145 | prefix = v.replacement
146 | }
147 | }
148 | tokens = append(tokens, node.toTokens(prefix)...)
149 | }
150 |
151 | tokens = append(tokens, xml.EndElement{Name: start.Name})
152 |
153 | for _, t := range tokens {
154 | err := e.EncodeToken(t)
155 | if err != nil {
156 | return err
157 | }
158 | }
159 |
160 | // flush to ensure tokens are written
161 | err := e.Flush()
162 | if err != nil {
163 | return err
164 | }
165 |
166 | return nil
167 | }
168 |
169 | type NamespaceURL string
170 |
171 | const (
172 | // NoNamespace is used for extension nodes without namespace
173 | NoNamespace NamespaceURL = ""
174 | // AnyNamespace is an invalid namespace used for searching for nodes by name (regardless of namespace)
175 | AnyNamespace NamespaceURL = "-1"
176 | )
177 |
178 | func (ex *Extension) GetOrCreateNode(namespaceURL NamespaceURL, path ...string) *ExtensionNode {
179 | // TODO: Check is len(nodes) == 0
180 | var subNode *ExtensionNode
181 | for n := range ex.Nodes {
182 | if ex.Nodes[n].SpaceNameURL() == string(namespaceURL) && ex.Nodes[n].LocalName() == path[0] {
183 | subNode = &ex.Nodes[n]
184 | break
185 | }
186 | }
187 | if subNode == nil {
188 | ex.Nodes = append(ex.Nodes, ExtensionNode{
189 | XMLName: xml.Name{
190 | Space: string(namespaceURL),
191 | Local: path[0],
192 | },
193 | })
194 | subNode = &ex.Nodes[len(ex.Nodes)-1]
195 | }
196 | return subNode.GetOrCreateNode(path[1:]...)
197 | }
198 |
199 | func (ex *Extension) GetNode(namespaceURL NamespaceURL, path0 string) (node *ExtensionNode, found bool) {
200 | for subn := range ex.Nodes {
201 | if ex.Nodes[subn].LocalName() == path0 {
202 | if ex.Nodes[subn].SpaceNameURL() == string(namespaceURL) || namespaceURL == AnyNamespace {
203 | node = &ex.Nodes[subn]
204 | found = true
205 | return
206 | }
207 | }
208 | }
209 | return
210 | }
211 |
--------------------------------------------------------------------------------
/gpx/gpx_extensions_test.go:
--------------------------------------------------------------------------------
1 | package gpx
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestReadExtension(t *testing.T) {
11 | t.Parallel()
12 |
13 | original, reparsed := loadAndReparseFile(t, "../test_files/gpx1.1_with_extensions.gpx")
14 |
15 | byts, err := reparsed.ToXml(ToXmlParams{Indent: true})
16 | assert.Nil(t, err)
17 | fmt.Println(string(byts))
18 |
19 | /*
20 |
21 | bbbhhh
22 |
23 |
24 | ggg
25 |
26 |
27 |
28 | */
29 |
30 | for n, g := range []GPX{*original, *reparsed} {
31 | fmt.Printf("gpx #%d\n", n)
32 |
33 | exts := []Extension{
34 | g.MetadataExtensions,
35 | g.Routes[0].Points[0].Extensions,
36 | g.Waypoints[0].Extensions,
37 | g.Tracks[0].Segments[0].Points[0].Extensions,
38 | }
39 |
40 | for _, ext := range exts {
41 | assert.Equal(t, 2, len(ext.Nodes))
42 | assert.Equal(t, "bbb", ext.Nodes[0].Data)
43 | assert.Equal(t, 1, len(ext.Nodes[0].Attrs), "%#v", ext.Nodes[0].Attrs)
44 | assert.Equal(t, "kkk", ext.Nodes[0].GetAttrOrEmpty("jjj"))
45 | assert.Equal(t, "aaa", ext.Nodes[0].LocalName())
46 | //assert.Equal(t, "gpx.py", ext.Nodes[0].SpaceName())
47 | assert.Equal(t, 1, len(ext.Nodes[1].Nodes))
48 | assert.Equal(t, 0, len(ext.Nodes[1].Attrs))
49 | assert.Equal(t, "mmm", ext.Nodes[1].Nodes[0].GetAttrOrEmpty("lll"))
50 | assert.Equal(t, "ooo", ext.Nodes[1].Nodes[0].GetAttrOrEmpty("nnn"))
51 | assert.Equal(t, "ggg", ext.Nodes[1].Nodes[0].Nodes[0].Data)
52 | }
53 | }
54 | }
55 |
56 | func TestExtensionWithoutNamespace(t *testing.T) {
57 | t.Parallel()
58 |
59 | original, err := ParseString(`
60 |
61 |
62 | bbbhhh
63 |
64 |
65 | ggg
66 |
67 |
68 |
69 |
70 | `)
71 | assert.Nil(t, err)
72 | assert.NotNil(t, original)
73 |
74 | if t.Failed() {
75 | t.FailNow()
76 | }
77 |
78 | reparsed, err := reparse(*original)
79 | assert.Nil(t, err)
80 |
81 | for _, g := range []GPX{*original, *reparsed} {
82 | ext := g.MetadataExtensions
83 | assert.Equal(t, 2, len(ext.Nodes))
84 | assert.Equal(t, "bbb", ext.Nodes[0].Data)
85 | assert.Equal(t, 1, len(ext.Nodes[0].Attrs), "%#v", ext.Nodes[0].Attrs)
86 | assert.Equal(t, "kkk", ext.Nodes[0].GetAttrOrEmpty("jjj"))
87 | assert.Equal(t, "aaa", ext.Nodes[0].LocalName())
88 | //assert.Equal(t, "gpx.py", ext.Nodes[0].SpaceName())
89 | assert.Equal(t, 1, len(ext.Nodes[1].Nodes))
90 | assert.Equal(t, 0, len(ext.Nodes[1].Attrs))
91 | assert.Equal(t, "mmm", ext.Nodes[1].Nodes[0].GetAttrOrEmpty("lll"))
92 | assert.Equal(t, "ooo", ext.Nodes[1].Nodes[0].GetAttrOrEmpty("nnn"))
93 | assert.Equal(t, "ggg", ext.Nodes[1].Nodes[0].Nodes[0].Data)
94 | }
95 | }
96 |
97 | func TestNodesSubnodesAndAttrs(t *testing.T) {
98 | t.Parallel()
99 |
100 | var node ExtensionNode
101 |
102 | assert.Equal(t, 0, len(node.Attrs))
103 | node.SetAttr("xxx", "yyy")
104 | assert.Equal(t, 1, len(node.Attrs))
105 | {
106 | val, found := node.GetAttr("xxx")
107 | assert.True(t, found)
108 | assert.Equal(t, "yyy", val)
109 | }
110 |
111 | assert.Equal(t, 0, len(node.Nodes))
112 | node.GetOrCreateNode("aaa").Data = "aaa data"
113 | assert.Equal(t, 1, len(node.Nodes))
114 | assert.Equal(t, 0, len(node.Nodes[0].Attrs))
115 |
116 | assert.Equal(t, &node.Nodes[0], node.GetOrCreateNode("aaa"))
117 |
118 | fmt.Println(string(node.debugXMLChunk()))
119 | node.GetOrCreateNode("aaa").SetAttr("aaa", "bbb")
120 | fmt.Println(string(node.debugXMLChunk()))
121 | assert.Equal(t, 1, len(node.Nodes[0].Attrs))
122 | assert.Equal(t, "aaa", node.Nodes[0].Attrs[0].Name.Local)
123 | assert.Equal(t, "bbb", node.Nodes[0].Attrs[0].Value)
124 |
125 | fmt.Println(string(node.debugXMLChunk()))
126 | node.GetOrCreateNode("aaa", "bbb").SetAttr("aaa", "bbb")
127 | fmt.Println(string(node.debugXMLChunk()))
128 | assert.Equal(t, 1, len(node.Nodes))
129 | assert.Equal(t, 1, len(node.Nodes[0].Nodes))
130 | assert.Equal(t, "aaa", node.Nodes[0].Nodes[0].Attrs[0].Name.Local)
131 | assert.Equal(t, "bbb", node.Nodes[0].Nodes[0].Attrs[0].Value)
132 | }
133 |
134 | func TestExtensionNodesAndAttrs(t *testing.T) {
135 | t.Parallel()
136 |
137 | var ext Extension
138 | assert.Equal(t, 0, len(ext.Nodes))
139 | ext.GetOrCreateNode(NoNamespace, "aaa").Data = "aaa data"
140 | assert.Equal(t, 1, len(ext.Nodes))
141 | assert.Equal(t, 0, len(ext.Nodes[0].Attrs))
142 | ext.GetOrCreateNode(NoNamespace, "aaa").SetAttr("aaa", "bbb")
143 | assert.Equal(t, 1, len(ext.Nodes[0].Attrs))
144 | assert.Equal(t, "aaa", ext.Nodes[0].Attrs[0].Name.Local)
145 | assert.Equal(t, "bbb", ext.Nodes[0].Attrs[0].Value)
146 |
147 | fmt.Println(string(ext.debugXMLChunk()))
148 | ext.GetOrCreateNode(NoNamespace, "aaa", "bbb").SetAttr("aaa", "bbb")
149 | fmt.Println(string(ext.debugXMLChunk()))
150 |
151 | {
152 | fmt.Println("a", string(ext.debugXMLChunk()))
153 | n1 := ext.GetOrCreateNode(NoNamespace, "aaa", "bbb")
154 | fmt.Println("b", string(ext.debugXMLChunk()))
155 | n2 := &ext.Nodes[0].Nodes[0]
156 | fmt.Println("c", string(ext.debugXMLChunk()))
157 | assert.Equal(t, fmt.Sprintf("%p", n1), fmt.Sprintf("%p", n2))
158 | }
159 |
160 | assert.Equal(t, 1, len(ext.Nodes))
161 | assert.Equal(t, 1, len(ext.Nodes[0].Nodes))
162 | assert.Equal(t, "aaa", ext.Nodes[0].Nodes[0].Attrs[0].Name.Local)
163 | assert.Equal(t, "bbb", ext.Nodes[0].Nodes[0].Attrs[0].Value)
164 | }
165 |
166 | func TestCreateExtensionWithoutNamespace(t *testing.T) {
167 | t.Parallel()
168 |
169 | var original GPX
170 | fmt.Println("1:", string(original.MetadataExtensions.debugXMLChunk()))
171 | original.MetadataExtensions.GetOrCreateNode(NoNamespace, "aaa", "bbb", "ccc").Data = "ccc data"
172 | fmt.Println("2:", string(original.MetadataExtensions.debugXMLChunk()))
173 | assert.Equal(t, 1, len(original.MetadataExtensions.Nodes))
174 | assert.Equal(t, "aaa", original.MetadataExtensions.Nodes[0].XMLName.Local)
175 | assert.Equal(t, "bbb", original.MetadataExtensions.Nodes[0].Nodes[0].XMLName.Local)
176 | assert.Equal(t, 0, len(original.MetadataExtensions.Nodes[0].Nodes[0].Attrs), "attrs=%#v", original.MetadataExtensions.Nodes[0].Nodes[0].Attrs)
177 | original.MetadataExtensions.GetOrCreateNode(NoNamespace, "aaa", "bbb").SetAttr("key", "value")
178 | fmt.Println("3:", string(original.MetadataExtensions.debugXMLChunk()))
179 | assert.Equal(t, 1, len(original.MetadataExtensions.Nodes[0].Nodes[0].Attrs), "attrs=%#v", original.MetadataExtensions.Nodes[0].Nodes[0].Attrs)
180 | if t.Failed() {
181 | t.FailNow()
182 | }
183 |
184 | assert.Equal(t, "aaa", original.MetadataExtensions.Nodes[0].XMLName.Local)
185 | assert.Equal(t, "bbb", original.MetadataExtensions.Nodes[0].Nodes[0].XMLName.Local)
186 | assert.Equal(t, 1, len(original.MetadataExtensions.Nodes[0].Nodes[0].Attrs), "attrs=%#v", original.MetadataExtensions.Nodes[0].Nodes[0].Attrs)
187 | assert.Equal(t, "key", original.MetadataExtensions.Nodes[0].Nodes[0].Attrs[0].Name.Local)
188 | assert.Equal(t, "value", original.MetadataExtensions.Nodes[0].Nodes[0].Attrs[0].Value)
189 |
190 | val, found := original.MetadataExtensions.GetOrCreateNode(NoNamespace, "aaa", "bbb").GetAttr("key")
191 | assert.True(t, found)
192 | assert.Equal(t, "value", val)
193 |
194 | reparsed, err := reparse(original)
195 | assert.Nil(t, err)
196 |
197 | for _, g := range []GPX{original, *reparsed} {
198 | byts, err := g.ToXml(ToXmlParams{Indent: true})
199 | assert.Nil(t, err)
200 | expected := `
201 |
202 |
203 |
204 |
205 |
206 |
207 | ccc data
208 |
209 |
210 |
211 |
212 | `
213 | assertLinesEquals(t, expected, string(byts))
214 | }
215 | }
216 |
217 | func TestCreateMetadataExtensionWithNamespace(t *testing.T) {
218 | t.Parallel()
219 |
220 | var original GPX
221 | original.RegisterNamespace("ext", "http://trla.baba.lan")
222 | original.MetadataExtensions.GetOrCreateNode("http://trla.baba.lan", "aaa", "bbb", "ccc").Data = "ccc data"
223 |
224 | assert.Equal(t, "http://trla.baba.lan", original.Attrs.NamespaceAttributes["xmlns"]["ext"].Value)
225 | assert.NotEmpty(t, original.Attrs.NamespaceAttributes["xmlns"]["ext"].replacement)
226 |
227 | original.MetadataExtensions.GetOrCreateNode("http://trla.baba.lan", "aaa", "bbb").SetAttr("key", "value")
228 | val, found := original.MetadataExtensions.GetOrCreateNode("http://trla.baba.lan", "aaa", "bbb").GetAttr("key")
229 | assert.True(t, found)
230 | assert.Equal(t, "value", val)
231 |
232 | reparsed, err := reparse(original)
233 | assert.Nil(t, err)
234 |
235 | rereparsed, err := reparse(*reparsed)
236 | assert.Nil(t, err)
237 |
238 | fmt.Println(string(original.MetadataExtensions.debugXMLChunk()))
239 | fmt.Println(string(reparsed.MetadataExtensions.debugXMLChunk()))
240 | assert.Equal(t, original.MetadataExtensions.debugXMLChunk(), reparsed.MetadataExtensions.debugXMLChunk())
241 | assert.Equal(t, original.MetadataExtensions, reparsed.MetadataExtensions)
242 |
243 | assert.Equal(t, 1, len(original.Attrs.NamespaceAttributes))
244 | assert.Equal(t, len(original.Attrs.NamespaceAttributes), len(reparsed.Attrs.NamespaceAttributes))
245 | assert.Equal(t, original.Attrs.NamespaceAttributes["xmlns"]["ext"].Attr, reparsed.Attrs.NamespaceAttributes["xmlns"]["ext"].Attr)
246 |
247 | assert.Equal(t, 1, len(reparsed.MetadataExtensions.Nodes))
248 | assert.Equal(t, len(original.MetadataExtensions.Nodes), len(reparsed.MetadataExtensions.Nodes))
249 | // assert.Equal(t, original.MetadataExtensions.XMLName, reparsed.MetadataExtensions.XMLName)
250 | assert.Equal(t, original.MetadataExtensions.Nodes[0], reparsed.MetadataExtensions.Nodes[0])
251 | // assert.Equal(t, original.MetadataExtensions.Attrs, reparsed.MetadataExtensions.Attrs)
252 | // assert.Equal(t, original.MetadataExtensions.Data, reparsed.MetadataExtensions.Data)
253 | assert.Equal(t, original.MetadataExtensions, reparsed.MetadataExtensions)
254 |
255 | if t.Failed() {
256 | t.FailNow()
257 | }
258 |
259 | for n, g := range []GPX{original, *reparsed, *rereparsed} {
260 | fmt.Printf("Test %d\n", n)
261 |
262 | node, found := g.MetadataExtensions.GetNode(AnyNamespace, "aaa")
263 | assert.True(t, found)
264 | assert.NotNil(t, node)
265 |
266 | node, found = g.MetadataExtensions.GetNode(NamespaceURL("http://trla.baba.lan"), "aaa")
267 | assert.True(t, found)
268 | assert.NotNil(t, node)
269 | assert.Equal(t, "http://trla.baba.lan", node.SpaceNameURL())
270 |
271 | node, found = node.GetNode("bbb")
272 | assert.True(t, found)
273 | assert.NotNil(t, node)
274 |
275 | assert.Equal(t, "http://trla.baba.lan", node.SpaceNameURL())
276 |
277 | byts, err := g.ToXml(ToXmlParams{Indent: true})
278 | assert.Nil(t, err)
279 | expected := `
280 |
281 |
282 |
283 |
284 |
285 |
286 | ccc data
287 |
288 |
289 |
290 |
291 | `
292 | assertLinesEquals(t, expected, string(byts))
293 | }
294 | }
295 |
296 | func TestCreateExtensionWithNamespace(t *testing.T) {
297 | t.Parallel()
298 |
299 | var original GPX
300 | original.RegisterNamespace("ext", "http://trla.baba.lan")
301 | original.Extensions.GetOrCreateNode("http://trla.baba.lan", "aaa", "bbb", "ccc").Data = "ccc data"
302 |
303 | assert.Equal(t, "http://trla.baba.lan", original.Attrs.NamespaceAttributes["xmlns"]["ext"].Value)
304 | assert.NotEmpty(t, original.Attrs.NamespaceAttributes["xmlns"]["ext"].replacement)
305 |
306 | original.Extensions.GetOrCreateNode("http://trla.baba.lan", "aaa", "bbb").SetAttr("key", "value")
307 | val, found := original.Extensions.GetOrCreateNode("http://trla.baba.lan", "aaa", "bbb").GetAttr("key")
308 | assert.True(t, found)
309 | assert.Equal(t, "value", val)
310 |
311 | reparsed, err := reparse(original)
312 | assert.Nil(t, err)
313 |
314 | rereparsed, err := reparse(*reparsed)
315 | assert.Nil(t, err)
316 |
317 | fmt.Println(string(original.Extensions.debugXMLChunk()))
318 | fmt.Println(string(reparsed.Extensions.debugXMLChunk()))
319 | assert.Equal(t, original.Extensions.debugXMLChunk(), reparsed.Extensions.debugXMLChunk())
320 | assert.Equal(t, original.Extensions, reparsed.Extensions)
321 |
322 | assert.Equal(t, 1, len(original.Attrs.NamespaceAttributes))
323 | assert.Equal(t, len(original.Attrs.NamespaceAttributes), len(reparsed.Attrs.NamespaceAttributes))
324 | assert.Equal(t, original.Attrs.NamespaceAttributes["xmlns"]["ext"].Attr, reparsed.Attrs.NamespaceAttributes["xmlns"]["ext"].Attr)
325 |
326 | assert.Equal(t, 1, len(reparsed.Extensions.Nodes))
327 | assert.Equal(t, len(original.Extensions.Nodes), len(reparsed.Extensions.Nodes))
328 | // assert.Equal(t, original.Extensions.XMLName, reparsed.Extensions.XMLName)
329 | assert.Equal(t, original.Extensions.Nodes[0], reparsed.Extensions.Nodes[0])
330 | // assert.Equal(t, original.Extensions.Attrs, reparsed.Extensions.Attrs)
331 | // assert.Equal(t, original.Extensions.Data, reparsed.Extensions.Data)
332 | assert.Equal(t, original.Extensions, reparsed.Extensions)
333 |
334 | if t.Failed() {
335 | t.FailNow()
336 | }
337 |
338 | for n, g := range []GPX{original, *reparsed, *rereparsed} {
339 | fmt.Printf("Test %d\n", n)
340 |
341 | node, found := g.Extensions.GetNode(AnyNamespace, "aaa")
342 | assert.True(t, found)
343 | assert.NotNil(t, node)
344 |
345 | node, found = g.Extensions.GetNode(NamespaceURL("http://trla.baba.lan"), "aaa")
346 | assert.True(t, found)
347 | assert.NotNil(t, node)
348 | assert.Equal(t, "http://trla.baba.lan", node.SpaceNameURL())
349 |
350 | node, found = node.GetNode("bbb")
351 | assert.True(t, found)
352 | assert.NotNil(t, node)
353 |
354 | assert.Equal(t, "http://trla.baba.lan", node.SpaceNameURL())
355 |
356 | byts, err := g.ToXml(ToXmlParams{Indent: true})
357 | assert.Nil(t, err)
358 | expected := `
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 | ccc data
367 |
368 |
369 |
370 | `
371 | assertLinesEquals(t, expected, string(byts))
372 | }
373 | }
374 |
375 | func TestGarminExtensions(t *testing.T) {
376 | t.Parallel()
377 |
378 | original, reparsed := loadAndReparseFile(t, "../test_files/gpx_with_garmin_extension.gpx")
379 | if t.Failed() {
380 | t.FailNow()
381 | }
382 |
383 | for n, g := range []GPX{*original, *reparsed} {
384 | fmt.Printf("Test %d\n", n)
385 | xml, err := g.ToXml(ToXmlParams{})
386 | assert.Nil(t, err)
387 | assert.Contains(t, string(xml), "")
388 | assert.Contains(t, string(xml), "171")
389 | }
390 | }
391 |
--------------------------------------------------------------------------------
/gpx/nullable_float64.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013, 2014 Peter Vasil, Tomo Krajina. All
2 | // rights reserved. Use of this source code is governed
3 | // by a BSD-style license that can be found in the
4 | // LICENSE file.
5 |
6 | package gpx
7 |
8 | import (
9 | "encoding/xml"
10 | "fmt"
11 | "strconv"
12 | "strings"
13 | )
14 |
15 | // NullableFloat64 implements a nullable float64
16 | type NullableFloat64 struct {
17 | data float64
18 | notNull bool
19 | }
20 |
21 | // Null checks if value is null
22 | func (n *NullableFloat64) Null() bool {
23 | return !n.notNull
24 | }
25 |
26 | // NotNull checks if value is not null
27 | func (n *NullableFloat64) NotNull() bool {
28 | return n.notNull
29 | }
30 |
31 | // Value returns the value
32 | func (n *NullableFloat64) Value() float64 {
33 | return n.data
34 | }
35 |
36 | // SetValue sets the value
37 | func (n *NullableFloat64) SetValue(data float64) {
38 | n.data = data
39 | n.notNull = true
40 | }
41 |
42 | // SetNull sets the value to null
43 | func (n *NullableFloat64) SetNull() {
44 | var defaultValue float64
45 | n.data = defaultValue
46 | n.notNull = false
47 | }
48 |
49 | // NewNullableFloat64 creates a new NullableFloat64
50 | func NewNullableFloat64(data float64) *NullableFloat64 {
51 | result := new(NullableFloat64)
52 | result.data = data
53 | result.notNull = true
54 | return result
55 | }
56 |
57 | // UnmarshalXML implements xml unmarshalling
58 | func (n *NullableFloat64) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
59 | t, err := d.Token()
60 | if err != nil {
61 | n.SetNull()
62 | return nil
63 | }
64 | if charData, ok := t.(xml.CharData); ok {
65 | strData := strings.Trim(string(charData), " ")
66 | value, err := strconv.ParseFloat(strData, 64)
67 | if err != nil {
68 | n.SetNull()
69 | return nil
70 | }
71 | n.SetValue(value)
72 | }
73 | d.Skip()
74 | return nil
75 | }
76 |
77 | // UnmarshalXMLAttr implements xml attribute unmarshalling
78 | func (n *NullableFloat64) UnmarshalXMLAttr(attr xml.Attr) error {
79 | strData := strings.Trim(string(attr.Value), " ")
80 | value, err := strconv.ParseFloat(strData, 64)
81 | if err != nil {
82 | n.SetNull()
83 | return nil
84 | }
85 | n.SetValue(value)
86 | return nil
87 | }
88 |
89 | // MarshalXML implements xml marshalling
90 | func (n NullableFloat64) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
91 | if n.Null() {
92 | return nil
93 | }
94 | xmlName := xml.Name{Local: start.Name.Local}
95 | if err := e.EncodeToken(xml.StartElement{Name: xmlName}); err != nil {
96 | return err
97 | }
98 | if err := e.EncodeToken(xml.CharData([]byte(fmt.Sprintf("%g", n.Value())))); err != nil {
99 | return err
100 | }
101 | if err := e.EncodeToken(xml.EndElement{Name: xmlName}); err != nil {
102 | return err
103 | }
104 | return nil
105 | }
106 |
107 | // MarshalXMLAttr implements xml attribute marshalling
108 | func (n NullableFloat64) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
109 | var result xml.Attr
110 | if n.Null() {
111 | return result, nil
112 | }
113 | return formattedFloat(n.Value()).MarshalXMLAttr(name)
114 | }
115 |
--------------------------------------------------------------------------------
/gpx/nullable_int.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013, 2014 Peter Vasil, Tomo Krajina. All
2 | // rights reserved. Use of this source code is governed
3 | // by a BSD-style license that can be found in the
4 | // LICENSE file.
5 |
6 | package gpx
7 |
8 | import (
9 | "encoding/xml"
10 | "fmt"
11 | "strconv"
12 | "strings"
13 | )
14 |
15 | //NullableInt implements a nullable int
16 | type NullableInt struct {
17 | data int
18 | notNull bool
19 | }
20 |
21 | //Null checks if value is null
22 | func (n *NullableInt) Null() bool {
23 | return !n.notNull
24 | }
25 |
26 | //NotNull checks if value is not null
27 | func (n *NullableInt) NotNull() bool {
28 | return n.notNull
29 | }
30 |
31 | //Value returns the value
32 | func (n *NullableInt) Value() int {
33 | return n.data
34 | }
35 |
36 | //SetValue sets the value
37 | func (n *NullableInt) SetValue(data int) {
38 | n.data = data
39 | n.notNull = true
40 | }
41 |
42 | //SetNull sets the value to null
43 | func (n *NullableInt) SetNull() {
44 | var defaultValue int
45 | n.data = defaultValue
46 | n.notNull = false
47 | }
48 |
49 | //NewNullableInt creates a new NullableInt
50 | func NewNullableInt(data int) *NullableInt {
51 | result := new(NullableInt)
52 | result.data = data
53 | result.notNull = true
54 | return result
55 | }
56 |
57 | //UnmarshalXML implements xml unmarshalling
58 | func (n *NullableInt) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
59 | t, err := d.Token()
60 | if err != nil {
61 | n.SetNull()
62 | return nil
63 | }
64 | if charData, ok := t.(xml.CharData); ok {
65 | strData := strings.Trim(string(charData), " ")
66 | value, err := strconv.ParseFloat(strData, 64)
67 | if err != nil {
68 | n.SetNull()
69 | return nil
70 | }
71 | n.SetValue(int(value))
72 | }
73 | d.Skip()
74 | return nil
75 | }
76 |
77 | //UnmarshalXMLAttr implements xml attribute unmarshalling
78 | func (n *NullableInt) UnmarshalXMLAttr(attr xml.Attr) error {
79 | strData := strings.Trim(string(attr.Value), " ")
80 | value, err := strconv.ParseFloat(strData, 64)
81 | if err != nil {
82 | n.SetNull()
83 | return nil
84 | }
85 | n.SetValue(int(value))
86 | return nil
87 | }
88 |
89 | //MarshalXML implements xml marshalling
90 | func (n NullableInt) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
91 | if n.Null() {
92 | return nil
93 | }
94 | xmlName := xml.Name{Local: start.Name.Local}
95 | if err := e.EncodeToken(xml.StartElement{Name: xmlName}); err != nil {
96 | return err
97 | }
98 | e.EncodeToken(xml.CharData([]byte(fmt.Sprintf("%d", n.Value()))))
99 | e.EncodeToken(xml.EndElement{Name: xmlName})
100 | return nil
101 | }
102 |
103 | //MarshalXMLAttr implements xml attribute marshalling
104 | func (n NullableInt) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
105 | var result xml.Attr
106 | if n.Null() {
107 | return result, nil
108 | }
109 | return xml.Attr{
110 | Name: xml.Name{Local: name.Local},
111 | Value: fmt.Sprintf("%d", n.Value())},
112 | nil
113 | }
114 |
--------------------------------------------------------------------------------
/gpx/nullable_string.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013, 2014 Peter Vasil, Tomo Krajina. All
2 | // rights reserved. Use of this source code is governed
3 | // by a BSD-style license that can be found in the
4 | // LICENSE file.
5 |
6 | package gpx
7 |
8 | //NullableString implements a nullable string
9 | type NullableString struct {
10 | data string
11 | notNull bool
12 | }
13 |
14 | //Null checks if value is null
15 | func (n *NullableString) Null() bool {
16 | return !n.notNull
17 | }
18 |
19 | //NotNull checks if value is not null
20 | func (n *NullableString) NotNull() bool {
21 | return n.notNull
22 | }
23 |
24 | //Value returns the value
25 | func (n *NullableString) Value() string {
26 | return n.data
27 | }
28 |
29 | //SetValue sets the value
30 | func (n *NullableString) SetValue(data string) {
31 | n.data = data
32 | n.notNull = true
33 | }
34 |
35 | //SetNull sets the value to null
36 | func (n *NullableString) SetNull() {
37 | var defaultValue string
38 | n.data = defaultValue
39 | n.notNull = false
40 | }
41 |
42 | //NewNullableString creates a new NullableString
43 | func NewNullableString(data string) *NullableString {
44 | result := new(NullableString)
45 | result.data = data
46 | result.notNull = true
47 | return result
48 | }
49 |
--------------------------------------------------------------------------------
/gpx/nullable_time.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013, 2014 Peter Vasil, Tomo Krajina. All
2 | // rights reserved. Use of this source code is governed
3 | // by a BSD-style license that can be found in the
4 | // LICENSE file.
5 |
6 | package gpx
7 |
8 | import "time"
9 |
10 | //NullableTime implements a nullable time
11 | type NullableTime struct {
12 | data time.Time
13 | notNull bool
14 | }
15 |
16 | //Null checks if value is null
17 | func (n *NullableTime) Null() bool {
18 | return !n.notNull
19 | }
20 |
21 | //NotNull checks if value is not null
22 | func (n *NullableTime) NotNull() bool {
23 | return n.notNull
24 | }
25 |
26 | //Value returns the value
27 | func (n *NullableTime) Value() time.Time {
28 | return n.data
29 | }
30 |
31 | //SetValue sets the value
32 | func (n *NullableTime) SetValue(data time.Time) {
33 | n.data = data
34 | n.notNull = true
35 | }
36 |
37 | //SetNull sets the value to null
38 | func (n *NullableTime) SetNull() {
39 | var defaultValue time.Time
40 | n.data = defaultValue
41 | n.notNull = false
42 | }
43 |
44 | //NewNullableTime creates a new NullableTime
45 | func NewNullableTime(data time.Time) *NullableTime {
46 | result := new(NullableTime)
47 | result.data = data
48 | result.notNull = true
49 | return result
50 | }
51 |
--------------------------------------------------------------------------------
/gpx/xml.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013, 2014 Peter Vasil, Tomo Krajina. All
2 | // rights reserved. Use of this source code is governed
3 | // by a BSD-style license that can be found in the
4 | // LICENSE file.
5 |
6 | package gpx
7 |
8 | import (
9 | "bytes"
10 | "encoding/xml"
11 | "errors"
12 | "io"
13 | "os"
14 | "strings"
15 | "time"
16 |
17 | "golang.org/x/net/html/charset"
18 | )
19 |
20 | const formattingTimelayout = "2006-01-02T15:04:05Z"
21 | const formattingTimeLayoutWithMillis = "2006-01-02T15:04:05.000Z"
22 |
23 | // parsingTimelayouts defines a list of possible time formats
24 | var parsingTimelayouts = []string{
25 | formattingTimeLayoutWithMillis,
26 | formattingTimelayout,
27 | "2006-01-02T15:04:05+00:00",
28 | "2006-01-02T15:04:05",
29 | "2006-01-02 15:04:05Z",
30 | "2006-01-02 15:04:05",
31 | }
32 |
33 | func init() {
34 | /*
35 | fmt.Println("----------------------------------------------------------------------------------------------------")
36 | fmt.Println("This API is experimental, it *will* change")
37 | fmt.Println("----------------------------------------------------------------------------------------------------")
38 | */
39 | }
40 |
41 | // ToXmlParams contains settings for xml transformation
42 | type ToXmlParams struct {
43 | Version string
44 | Indent bool
45 | }
46 |
47 | // ToXml returns the xml representation of the GPX object.
48 | // Params are optional, you can set null to use GPXs Version and no indentation.
49 | func ToXml(g *GPX, params ToXmlParams) ([]byte, error) {
50 | version := g.Version
51 | if len(params.Version) > 0 {
52 | version = params.Version
53 | }
54 | indentation := params.Indent
55 |
56 | var replacemends map[string]string
57 | var gpxDoc interface{}
58 | if version == "1.0" {
59 | gpxDoc = convertToGpx10Models(g)
60 | } else if version == "1.1" {
61 | gpxDoc, replacemends = convertToGpx11Models(g)
62 | } else {
63 | g.Version = "1.1"
64 | gpxDoc, replacemends = convertToGpx11Models(g)
65 | }
66 |
67 | var buffer bytes.Buffer
68 | buffer.WriteString(xml.Header)
69 | if indentation {
70 | b, err := xml.MarshalIndent(gpxDoc, "", " ")
71 | if err != nil {
72 | return nil, err
73 | }
74 | buffer.Write(b)
75 | } else {
76 | b, err := xml.Marshal(gpxDoc)
77 | if err != nil {
78 | return nil, err
79 | }
80 | buffer.Write(b)
81 | }
82 |
83 | byts := buffer.Bytes()
84 |
85 | for replKey, replVal := range replacemends {
86 | byts = bytes.Replace(byts, []byte(replKey), []byte(replVal), -1)
87 | }
88 |
89 | return byts, nil
90 | }
91 |
92 | func guessGPXVersion(bytes []byte) (string, error) {
93 | bytesCount := 1000
94 | if len(bytes) < 1000 {
95 | bytesCount = len(bytes)
96 | }
97 |
98 | startOfDocument := string(bytes[:bytesCount])
99 |
100 | parts := strings.Split(startOfDocument, "")
103 | }
104 | parts = strings.Split(parts[1], "version=")
105 |
106 | if len(parts) <= 1 {
107 | return "", errors.New("invalid GPX file, cannot find version in ")
108 | }
109 |
110 | version := strings.TrimLeft(parts[1], `'" `)
111 | if strings.HasPrefix(version, "1.0") {
112 | return "1.0", nil
113 | } else if strings.HasPrefix(version, "1.1") {
114 | return "1.1", nil
115 | }
116 |
117 | return "", errors.New("invalid GPX file, cannot find version")
118 | }
119 |
120 | func parseGPXTime(timestr string) (*time.Time, error) {
121 | if strings.Contains(timestr, ".") {
122 | // Probably seconds with milliseconds
123 | timestr = strings.Split(timestr, ".")[0]
124 | }
125 | timestr = strings.Trim(timestr, " \t\n\r")
126 | for _, timeLayout := range parsingTimelayouts {
127 | t, err := time.Parse(timeLayout, timestr)
128 |
129 | if err == nil {
130 | return &t, nil
131 | }
132 | }
133 |
134 | return nil, errors.New("Cannot parse " + timestr)
135 | }
136 |
137 | func formatGPXTime(time *time.Time) string {
138 | if time == nil {
139 | return ""
140 | }
141 | if time.Year() <= 1 {
142 | // Invalid date:
143 | return ""
144 | }
145 | if time.Nanosecond() > 0 {
146 | return time.Format(formattingTimeLayoutWithMillis)
147 | }
148 | return time.Format(formattingTimelayout)
149 | }
150 |
151 | // ParseFile parses a gpx file and returns a GPX object
152 | func ParseFile(fileName string) (*GPX, error) {
153 | f, err := os.Open(fileName)
154 | if err != nil {
155 | return nil, err
156 | }
157 |
158 | defer f.Close()
159 |
160 | return Parse(f)
161 | }
162 |
163 | // ParseBytes parses GPX from bytes
164 | func ParseBytes(buf []byte) (*GPX, error) {
165 | return Parse(bytes.NewReader(buf))
166 | }
167 |
168 | // ParseDecoder parses a gpx from a predefined decoder.
169 | //
170 | // That way the decoder can have parameters you need, for example `decoder.Strict = false`
171 | //
172 | // `initialBytes` are used to "guess" the gpx version. It can be nil, but in that case the parses will assume the GPX version is 1.1
173 | func ParseDecoder(decoder *xml.Decoder, initialBytes []byte) (*GPX, error) {
174 | version, err := guessGPXVersion(initialBytes)
175 | if err != nil {
176 | // Unknown version, try with 1.1
177 | version = "1.1"
178 | }
179 |
180 | switch version {
181 | case "1.0":
182 | g := &gpx10Gpx{}
183 | err = decoder.Decode(&g)
184 | if err != nil {
185 | return nil, err
186 | }
187 | return convertFromGpx10Models(g), nil
188 | case "1.1":
189 | g := &gpx11Gpx{}
190 | err = decoder.Decode(&g)
191 | if err != nil {
192 | return nil, err
193 | }
194 | return convertFromGpx11Models(g), nil
195 | default:
196 | return nil, errors.New("Invalid version:" + version)
197 | }
198 | }
199 |
200 | // Parse parses GPX from io.Reader
201 | func Parse(inReader io.Reader) (*GPX, error) {
202 | // at most 1000 bytes will make guessGPXVersion happy
203 | buf := make([]byte, 1000)
204 |
205 | n, err := inReader.Read(buf)
206 | if err != nil {
207 | return nil, err
208 | }
209 | buf = buf[:n]
210 |
211 | reader := io.MultiReader(bytes.NewReader(buf), inReader)
212 | decoder := xml.NewDecoder(reader)
213 | decoder.CharsetReader = charset.NewReaderLabel
214 |
215 | return ParseDecoder(decoder, buf)
216 | }
217 |
218 | // ParseString parses GPX from string
219 | func ParseString(str string) (*GPX, error) {
220 | return Parse(strings.NewReader(str))
221 | }
222 |
--------------------------------------------------------------------------------
/gpx/xml_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013, 2014 Peter Vasil, Tomo Krajina. All
2 | // rights reserved. Use of this source code is governed
3 | // by a BSD-style license that can be found in the
4 | // LICENSE file.
5 |
6 | package gpx
7 |
8 | import (
9 | "encoding/xml"
10 | "fmt"
11 | "testing"
12 | "time"
13 |
14 | "github.com/stretchr/testify/assert"
15 | )
16 |
17 | func TestParseTime2(t *testing.T) {
18 | tm, err := parseGPXTime("2021-06-19T17:28:22+00:00")
19 | assert.Nil(t, err)
20 | assert.Equal(t, "2021-06-19T17:28:22Z", tm.Format(time.RFC3339))
21 | }
22 |
23 | func TestParseTime(t *testing.T) {
24 | time, err := parseGPXTime("")
25 | assert.NotNil(t, err)
26 | assert.Nil(t, time)
27 | }
28 |
29 | type testXml struct {
30 | XMLName xml.Name `xml:"gpx"`
31 | Float NullableFloat64 `xml:"float"`
32 | Int NullableInt `xml:"int"`
33 | FloatAttr NullableFloat64 `xml:"floatattr,attr"`
34 | IntAttr NullableInt `xml:"intattr,attr"`
35 | }
36 |
37 | func TestInvalidFloat(t *testing.T) {
38 | xmlStr := `...a`
39 | testXmlDoc := testXml{}
40 | xml.Unmarshal([]byte(xmlStr), &testXmlDoc)
41 | if testXmlDoc.Float.NotNull() {
42 | t.Error("Float is invalid in ", xmlStr)
43 | }
44 | }
45 |
46 | func TestValidFloat(t *testing.T) {
47 | xmlStr := `120`
48 | testFloat(xmlStr, 120, 13, `120`, t)
49 | }
50 |
51 | func TestValidFloat2(t *testing.T) {
52 | xmlStr := ` 12.3`
53 | testFloat(xmlStr, 12.3, 13.4, `12.3`, t)
54 | }
55 |
56 | func TestValidFloat3(t *testing.T) {
57 | xmlStr := `12.3 `
58 | testFloat(xmlStr, 12.3, 13.5, `12.3`, t)
59 | }
60 |
61 | func testFloat(xmlStr string, expectedFloat float64, expectedFloatAttribute float64, expectedXml string, t *testing.T) {
62 | testXmlDoc := testXml{}
63 | err := xml.Unmarshal([]byte(xmlStr), &testXmlDoc)
64 | assert.Nil(t, err)
65 | assert.False(t, testXmlDoc.Float.Null())
66 | assert.Equal(t, testXmlDoc.Float.Value(), expectedFloat)
67 | if testXmlDoc.FloatAttr.Null() || testXmlDoc.FloatAttr.Value() != expectedFloatAttribute {
68 | t.Error("Float attribute invalid ", xmlStr)
69 | }
70 | bytes, err := xml.Marshal(testXmlDoc)
71 | if err != nil {
72 | t.Error("Error marshalling:", err.Error())
73 | }
74 |
75 | if string(bytes) != expectedXml {
76 | t.Error("Invalid marshalled xml:", string(bytes), "expected:", expectedXml)
77 | }
78 | }
79 |
80 | func TestValidInt(t *testing.T) {
81 | xmlStr := `12`
82 | testInt(xmlStr, 12, 15, `12`, t)
83 | }
84 |
85 | func TestValidInt2(t *testing.T) {
86 | xmlStr := ` 12.3`
87 | testInt(xmlStr, 12, 17, `12`, t)
88 | }
89 |
90 | func TestValidInt3(t *testing.T) {
91 | xmlStr := `12.3 `
92 | testInt(xmlStr, 12, 18, `12`, t)
93 | }
94 |
95 | func testInt(xmlStr string, expectedInt int, expectedIntAttribute int, expectedXml string, t *testing.T) {
96 | testXmlDoc := testXml{}
97 | xml.Unmarshal([]byte(xmlStr), &testXmlDoc)
98 | if testXmlDoc.Int.Null() || testXmlDoc.Int.Value() != expectedInt {
99 | t.Error("Int invalid ", xmlStr)
100 | }
101 | if testXmlDoc.IntAttr.Null() || testXmlDoc.IntAttr.Value() != expectedIntAttribute {
102 | t.Error("Int attribute valid ", xmlStr)
103 | }
104 | bytes, err := xml.Marshal(testXmlDoc)
105 | if err != nil {
106 | t.Error("Error marshalling:", err.Error())
107 | }
108 |
109 | if string(bytes) != expectedXml {
110 | t.Error("Invalid marshalled xml:", string(bytes), "expected:", expectedXml)
111 | }
112 | }
113 |
114 | func TestGuessVersion(t *testing.T) {
115 | t.Parallel()
116 |
117 | for _, testData := range []struct {
118 | str string
119 | expected string
120 | shouldErr bool
121 | }{
122 | {"
148 |
149 | `)
150 | assert.Nil(t, err)
151 | assert.Equal(t, g.Version, "7.7.5-play")
152 |
153 | _, err = g.ToXml(ToXmlParams{})
154 | assert.Nil(t, err)
155 | }
156 |
157 | func Test(t *testing.T) {
158 | t.Parallel()
159 |
160 | {
161 | tm := time.Date(2021, time.Month(6), 19, 17, 28, 22, 0, time.UTC)
162 | assert.Equal(t, "2021-06-19T17:28:22Z", formatGPXTime(&tm))
163 | }
164 | {
165 | tm := time.Date(2021, time.Month(6), 19, 17, 28, 22, 999999999, time.UTC)
166 | assert.Equal(t, "2021-06-19T17:28:22.999Z", formatGPXTime(&tm))
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/gpxinfo.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013, 2014 Peter Vasil, Tomo Krajina. All
2 | // rights reserved. Use of this source code is governed
3 | // by a BSD-style license that can be found in the
4 | // LICENSE file.
5 |
6 | package main
7 |
8 | import (
9 | "flag"
10 | "fmt"
11 | "path/filepath"
12 |
13 | "github.com/tkrajina/gpxgo/gpx"
14 | )
15 |
16 | func main() {
17 | flag.Parse()
18 |
19 | args := flag.Args()
20 | if len(args) != 1 {
21 | fmt.Println("Please provide a GPX file path!")
22 | return
23 | }
24 |
25 | gpxFileArg := args[0]
26 | gpxFile, err := gpx.ParseFile(gpxFileArg)
27 |
28 | if err != nil {
29 | fmt.Println("Error opening gpx file: ", err)
30 | return
31 | }
32 |
33 | gpxPath, _ := filepath.Abs(gpxFileArg)
34 |
35 | fmt.Print("File: ", gpxPath, "\n")
36 |
37 | fmt.Println(gpxFile.GetGpxInfo())
38 | }
39 |
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | test:
2 | go test ./gpx
3 | gofmt:
4 | gofmt -w ./gpx
5 | goimports:
6 | goimports -w ./gpx
7 | build-generics:
8 | gengen generic/nullable.go string \
9 | | gofmt -r 'NullableGeneric -> NullableString' \
10 | | gofmt -r 'NewNullableGeneric -> NewNullableString' \
11 | > gpx/nullable_string.go
12 | gengen generic/nullable.go int \
13 | | gofmt -r 'NullableGeneric -> NullableInt' \
14 | | gofmt -r 'NewNullableGeneric -> NewNullableInt' \
15 | > gpx/nullable_int.go
16 | gengen generic/nullable.go float64 \
17 | | gofmt -r 'NullableGeneric -> NullableFloat64' \
18 | | gofmt -r 'NewNullableGeneric -> NewNullableFloat64' \
19 | > gpx/nullable_float64.go
20 | gengen generic/nullable.go time.Time \
21 | | gofmt -r 'NullableGeneric -> NullableTime' \
22 | | gofmt -r 'NewNullableGeneric -> NewNullableTime' \
23 | > gpx/nullable_time.go
24 | install:
25 | go install ./gpx
26 | prepare:
27 | go get
28 | clean:
29 | echo "TODO"
30 | ctags:
31 | ctags -R .
32 | lint:
33 | golongfuncs
34 | gometalinter --deadline=60s --disable=interfacer gpx
35 |
36 | install-tools:
37 | go get -u github.com/tkrajina/golongfuncs/...
38 | go get -u gopkg.in/alecthomas/gometalinter.v2
39 | gometalinter --install
--------------------------------------------------------------------------------
/test_files/Mojstrovka.gpx:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 |
13 | 1614.678000
14 |
15 |
16 |
17 | 1636.776000
18 |
19 |
20 |
21 | 1632.935520
22 |
23 |
24 |
25 | 1631.502960
26 |
27 |
28 |
29 | 1629.095040
30 |
31 |
32 |
33 | 1628.119680
34 |
35 |
36 |
37 | 1626.199440
38 |
39 |
40 |
41 | 1627.174800
42 |
43 |
44 |
45 | 1652.168400
46 |
47 |
48 |
49 | 1684.842960
50 |
51 |
52 |
53 | 1688.226240
54 |
55 |
56 |
57 | 1688.226240
58 |
59 |
60 |
61 | 1694.931840
62 |
63 |
64 |
65 | 1699.747680
66 |
67 |
68 |
69 | 1701.180240
70 |
71 |
72 |
73 | 1702.155600
74 |
75 |
76 |
77 | 1716.084960
78 |
79 |
80 |
81 | 1714.652400
82 |
83 |
84 |
85 | 1718.980560
86 |
87 |
88 |
89 | 1725.716640
90 |
91 |
92 |
93 | 1726.173840
94 |
95 |
96 |
97 | 1727.149200
98 |
99 |
100 |
101 | 1728.094080
102 |
103 |
104 |
105 | 1738.670640
106 |
107 |
108 |
109 | 1874.215200
110 |
111 |
112 |
113 | 1883.359200
114 |
115 |
116 |
117 | 1887.687360
118 |
119 |
120 |
121 | 1886.254800
122 |
123 |
124 |
125 | 1921.337280
126 |
127 |
128 |
129 | 1959.315360
130 |
131 |
132 |
133 | 1945.843200
134 |
135 |
136 |
137 | 1946.330880
138 |
139 |
140 |
141 | 1951.603920
142 |
143 |
144 |
145 | 1954.987200
146 |
147 |
148 |
149 | 1956.419760
150 |
151 |
152 |
153 | 1961.235600
154 |
155 |
156 |
157 | 1968.428880
158 |
159 |
160 |
161 | 1972.757040
162 |
163 |
164 |
165 | 1973.244720
166 |
167 |
168 |
169 | 1975.652640
170 |
171 |
172 |
173 | 1981.413360
174 |
175 |
176 |
177 | 1992.965280
178 |
179 |
180 |
181 | 1999.670880
182 |
183 |
184 |
185 | 2000.646240
186 |
187 |
188 |
189 | 2015.063280
190 |
191 |
192 |
193 | 2019.391440
194 |
195 |
196 |
197 | 2014.575600
198 |
199 |
200 |
201 | 2019.879120
202 |
203 |
204 |
205 | 2024.207280
206 |
207 |
208 |
209 | 2025.152160
210 |
211 |
212 |
213 | 2023.719600
214 |
215 |
216 |
217 | 2028.992640
218 |
219 |
220 |
221 | 2032.375920
222 |
223 |
224 |
225 | 2057.369520
226 |
227 |
228 |
229 | 2050.145760
230 |
231 |
232 |
233 | 2048.713200
234 |
235 |
236 |
237 | 2046.792960
238 |
239 |
240 |
241 | 2042.464800
242 |
243 |
244 |
245 | 2038.624320
246 |
247 |
248 |
249 | 2034.753360
250 |
251 |
252 |
253 | 2033.320800
254 |
255 |
256 |
257 | 2029.968000
258 |
259 |
260 |
261 | 2027.072400
262 |
263 |
264 |
265 | 2022.744240
266 |
267 |
268 |
269 | 2021.799360
270 |
271 |
272 |
273 | 2018.416080
274 |
275 |
276 |
277 | 2016.983520
278 |
279 |
280 |
281 | 2015.550960
282 |
283 |
284 |
285 | 2008.814880
286 |
287 |
288 |
289 | 2003.054160
290 |
291 |
292 |
293 | 1997.750640
294 |
295 |
296 |
297 | 1999.213680
298 |
299 |
300 |
301 | 1991.014560
302 |
303 |
304 |
305 | 1983.333600
306 |
307 |
308 |
309 | 1981.901040
310 |
311 |
312 |
313 | 1982.845920
314 |
315 |
316 |
317 | 1979.005440
318 |
319 |
320 |
321 | 1976.140320
322 |
323 |
324 |
325 | 1972.757040
326 |
327 |
328 |
329 | 1970.836800
330 |
331 |
332 |
333 | 1963.643520
334 |
335 |
336 |
337 | 1959.772560
338 |
339 |
340 |
341 | 1956.907440
342 |
343 |
344 |
345 | 1955.444400
346 |
347 |
348 |
349 | 1954.011840
350 |
351 |
352 |
353 | 1948.738800
354 |
355 |
356 |
357 | 1946.818560
358 |
359 |
360 |
361 | 1943.922960
362 |
363 |
364 |
365 | 1942.490400
366 |
367 |
368 |
369 | 1942.490400
370 |
371 |
372 |
373 | 1942.490400
374 |
375 |
376 |
377 | 1942.490400
378 |
379 |
380 |
381 | 1942.490400
382 |
383 |
384 |
385 | 1942.490400
386 |
387 |
388 |
389 | 1942.490400
390 |
391 |
392 |
393 | 1873.270320
394 |
395 |
396 |
397 | 1867.021920
398 |
399 |
400 |
401 | 1869.429840
402 |
403 |
404 |
405 | 1863.181440
406 |
407 |
408 |
409 | 1864.614000
410 |
411 |
412 |
413 | 1864.614000
414 |
415 |
416 |
417 | 1864.614000
418 |
419 |
420 |
421 | 1864.614000
422 |
423 |
424 |
425 | 1864.614000
426 |
427 |
428 |
429 | 1852.604880
430 |
431 |
432 |
433 | 1846.356480
434 |
435 |
436 |
437 | 1837.700160
438 |
439 |
440 |
441 | 1833.859680
442 |
443 |
444 |
445 | 1829.531520
446 |
447 |
448 |
449 | 1827.123600
450 |
451 |
452 |
453 | 1822.307760
454 |
455 |
456 |
457 | 1819.899840
458 |
459 |
460 |
461 | 1816.547040
462 |
463 |
464 |
465 | 1813.651440
466 |
467 |
468 |
469 | 1813.651440
470 |
471 |
472 |
473 | 1811.274000
474 |
475 |
476 |
477 | 1809.810960
478 |
479 |
480 |
481 | 1807.890720
482 |
483 |
484 |
485 | 1805.482800
486 |
487 |
488 |
489 | 1800.697440
490 |
491 |
492 |
493 | 1794.906240
494 |
495 |
496 |
497 | 1792.041120
498 |
499 |
500 |
501 | 1788.200640
502 |
503 |
504 |
505 | 1786.737600
506 |
507 |
508 |
509 | 1782.897120
510 |
511 |
512 |
513 | 1779.056640
514 |
515 |
516 |
517 | 1779.544320
518 |
519 |
520 |
521 | 1772.320560
522 |
523 |
524 |
525 | 1767.535200
526 |
527 |
528 |
529 | 1760.799120
530 |
531 |
532 |
533 | 1759.823760
534 |
535 |
536 |
537 | 1759.823760
538 |
539 |
540 |
541 | 1757.903520
542 |
543 |
544 |
545 | 1757.415840
546 |
547 |
548 |
549 | 1755.495600
550 |
551 |
552 |
553 | 1753.087680
554 |
555 |
556 |
557 | 1751.655120
558 |
559 |
560 |
561 | 1748.302320
562 |
563 |
564 |
565 | 1747.326960
566 |
567 |
568 |
569 | 1742.053920
570 |
571 |
572 |
573 | 1737.238080
574 |
575 |
576 |
577 | 1736.293200
578 |
579 |
580 |
581 | 1734.342480
582 |
583 |
584 |
585 | 1732.909920
586 |
587 |
588 |
589 | 1730.989680
590 |
591 |
592 |
593 | 1726.173840
594 |
595 |
596 |
597 | 1723.765920
598 |
599 |
600 |
601 | 1719.468240
602 |
603 |
604 |
605 | 1715.140080
606 |
607 |
608 |
609 | 1714.164720
610 |
611 |
612 |
613 | 1711.756800
614 |
615 |
616 |
617 | 1710.811920
618 |
619 |
620 |
621 | 1708.891680
622 |
623 |
624 |
625 | 1708.404000
626 |
627 |
628 |
629 | 1704.563520
630 |
631 |
632 |
633 | 1702.643280
634 |
635 |
636 |
637 | 1700.235360
638 |
639 |
640 |
641 | 1696.852080
642 |
643 |
644 |
645 | 1695.907200
646 |
647 |
648 |
649 | 1693.499280
650 |
651 |
652 |
653 | 1692.523920
654 |
655 |
656 |
657 | 1689.658800
658 |
659 |
660 |
661 | 1686.763200
662 |
663 |
664 |
665 | 1686.763200
666 |
667 |
668 |
669 | 1686.763200
670 |
671 |
672 |
673 | 1686.763200
674 |
675 |
676 |
677 | 1686.275520
678 |
679 |
680 |
681 | 1686.275520
682 |
683 |
684 |
685 | 1686.275520
686 |
687 |
688 |
689 | 1685.818320
690 |
691 |
692 |
693 | 1685.818320
694 |
695 |
696 |
697 | 1685.330640
698 |
699 |
700 |
701 | 1685.330640
702 |
703 |
704 |
705 | 1685.330640
706 |
707 |
708 |
709 | 1685.330640
710 |
711 |
712 |
713 | 1684.842960
714 |
715 |
716 |
717 | 1677.162000
718 |
719 |
720 |
721 | 1677.162000
722 |
723 |
724 |
725 | 1677.162000
726 |
727 |
728 |
729 | 1659.849360
730 |
731 |
732 |
733 | 1652.168400
734 |
735 |
736 |
737 | 1649.272800
738 |
739 |
740 |
741 | 1644.944640
742 |
743 |
744 |
745 | 1643.512080
746 |
747 |
748 |
749 |
750 |
751 |
--------------------------------------------------------------------------------
/test_files/cerknicko-without-times.gpx:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 | 001
12 | 05-AUG-10 16:58:37
13 | 05-AUG-10 16:58:37
14 | Flag, Blue
15 |
16 |
17 | -0.114380
18 | BACK T TH
19 | BACK TO THE ROOTS
20 | BACK TO THE ROOTS
21 | City (Small)
22 |
23 |
24 | -0.114380
25 | BIRDS NEST
26 | BIRDS NEST
27 | BIRDS NEST
28 | City (Small)
29 |
30 |
31 | -0.114380
32 | FAGGIO
33 | FAGGIO
34 | FAGGIO
35 | City (Small)
36 |
37 |
38 | -0.114380
39 | RAKOV12
40 | RAKOV12
41 | RAKOV12
42 | City (Small)
43 |
44 |
45 | -0.114380
46 | RAKV SKCJN
47 | RAKOV SKOCJAN
48 | RAKOV SKOCJAN
49 | City (Small)
50 |
51 |
52 | -0.114380
53 | VANSHNG LK
54 | VANISHING LAKE
55 | VANISHING LAKE
56 | City (Small)
57 |
58 |
59 | ACTIVE LOG
60 |
61 |
62 |
63 |
64 | ACTIVE LOG #2
65 | 1
66 |
67 |
68 | 542.320923
69 |
70 |
71 | 550.972656
72 |
73 |
74 | 553.856689
75 |
76 |
77 | 555.779297
78 |
79 |
80 | 555.779297
81 |
82 |
83 | 555.298584
84 |
85 |
86 | 553.856689
87 |
88 |
89 | 552.414551
90 |
91 |
92 | 551.934082
93 |
94 |
95 | 552.895264
96 |
97 |
98 | 552.414551
99 |
100 |
101 | 552.895264
102 |
103 |
104 | 552.895264
105 |
106 |
107 | 552.895264
108 |
109 |
110 | 552.414551
111 |
112 |
113 | 552.414551
114 |
115 |
116 | 551.453369
117 |
118 |
119 | 550.972656
120 |
121 |
122 | 551.453369
123 |
124 |
125 | 551.934082
126 |
127 |
128 | 551.934082
129 |
130 |
131 | 550.492188
132 |
133 |
134 | 550.011475
135 |
136 |
137 | 550.011475
138 |
139 |
140 | 549.530762
141 |
142 |
143 | 548.088867
144 |
145 |
146 | 547.608154
147 |
148 |
149 | 547.608154
150 |
151 |
152 | 548.088867
153 |
154 |
155 | 548.569336
156 |
157 |
158 | 548.569336
159 |
160 |
161 | 548.088867
162 |
163 |
164 | 547.608154
165 |
166 |
167 | 548.088867
168 |
169 |
170 | 549.530762
171 |
172 |
173 | 550.972656
174 |
175 |
176 | 550.972656
177 |
178 |
179 | 551.453369
180 |
181 |
182 | 550.972656
183 |
184 |
185 | 551.453369
186 |
187 |
188 | 551.453369
189 |
190 |
191 | 551.453369
192 |
193 |
194 | 551.453369
195 |
196 |
197 | 551.453369
198 |
199 |
200 | 549.050049
201 |
202 |
203 | 546.646851
204 |
205 |
206 | 548.088867
207 |
208 |
209 | 546.166260
210 |
211 |
212 | 548.569336
213 |
214 |
215 | 550.011475
216 |
217 |
218 | 552.895264
219 |
220 |
221 | 553.856689
222 |
223 |
224 | 553.375977
225 |
226 |
227 | 552.895264
228 |
229 |
230 | 553.375977
231 |
232 |
233 | 553.856689
234 |
235 |
236 | 553.856689
237 |
238 |
239 | 551.453369
240 |
241 |
242 | 552.895264
243 |
244 |
245 | 553.856689
246 |
247 |
248 | 553.375977
249 |
250 |
251 | 553.375977
252 |
253 |
254 | 553.856689
255 |
256 |
257 | 553.856689
258 |
259 |
260 | 554.337402
261 |
262 |
263 | 554.337402
264 |
265 |
266 | 553.856689
267 |
268 |
269 | 552.895264
270 |
271 |
272 | 553.375977
273 |
274 |
275 | 552.895264
276 |
277 |
278 | 553.375977
279 |
280 |
281 | 553.375977
282 |
283 |
284 | 553.375977
285 |
286 |
287 | 553.375977
288 |
289 |
290 | 553.375977
291 |
292 |
293 | 553.856689
294 |
295 |
296 | 554.337402
297 |
298 |
299 | 553.856689
300 |
301 |
302 | 553.375977
303 |
304 |
305 | 552.895264
306 |
307 |
308 | 552.895264
309 |
310 |
311 | 552.414551
312 |
313 |
314 | 551.453369
315 |
316 |
317 | 550.492188
318 |
319 |
320 | 550.492188
321 |
322 |
323 | 550.972656
324 |
325 |
326 | 550.492188
327 |
328 |
329 | 550.492188
330 |
331 |
332 | 549.530762
333 |
334 |
335 | 549.050049
336 |
337 |
338 | 550.011475
339 |
340 |
341 | 550.972656
342 |
343 |
344 | 551.934082
345 |
346 |
347 | 550.972656
348 |
349 |
350 | 549.530762
351 |
352 |
353 | 548.088867
354 |
355 |
356 | 546.646851
357 |
358 |
359 | 546.166260
360 |
361 |
362 | 545.685547
363 |
364 |
365 | 545.685547
366 |
367 |
368 | 547.127441
369 |
370 |
371 | 548.088867
372 |
373 |
374 | 548.088867
375 |
376 |
377 | 548.569336
378 |
379 |
380 | 547.127441
381 |
382 |
383 | 546.646851
384 |
385 |
386 | 546.166260
387 |
388 |
389 | 546.166260
390 |
391 |
392 | 545.685547
393 |
394 |
395 | 542.801514
396 |
397 |
398 | 543.762817
399 |
400 |
401 | 543.762817
402 |
403 |
404 | 543.762817
405 |
406 |
407 | 544.724243
408 |
409 |
410 | 544.724243
411 |
412 |
413 | 545.204834
414 |
415 |
416 | 545.685547
417 |
418 |
419 | 545.204834
420 |
421 |
422 | 545.685547
423 |
424 |
425 | 545.685547
426 |
427 |
428 | 545.204834
429 |
430 |
431 | 544.243652
432 |
433 |
434 | 543.282104
435 |
436 |
437 | 544.243652
438 |
439 |
440 | 545.685547
441 |
442 |
443 | 546.646851
444 |
445 |
446 | 546.646851
447 |
448 |
449 | 550.011475
450 |
451 |
452 | 550.492188
453 |
454 |
455 | 550.492188
456 |
457 |
458 | 550.972656
459 |
460 |
461 | 550.972656
462 |
463 |
464 | 550.972656
465 |
466 |
467 | 550.492188
468 |
469 |
470 | 550.492188
471 |
472 |
473 | 550.972656
474 |
475 |
476 | 551.934082
477 |
478 |
479 | 551.934082
480 |
481 |
482 | 551.934082
483 |
484 |
485 | 551.934082
486 |
487 |
488 | 552.414551
489 |
490 |
491 | 552.895264
492 |
493 |
494 | 551.453369
495 |
496 |
497 | 550.972656
498 |
499 |
500 | 550.492188
501 |
502 |
503 | 550.972656
504 |
505 |
506 | 550.492188
507 |
508 |
509 | 550.492188
510 |
511 |
512 | 550.011475
513 |
514 |
515 | 549.530762
516 |
517 |
518 | 550.492188
519 |
520 |
521 | 550.972656
522 |
523 |
524 | 550.492188
525 |
526 |
527 | 551.453369
528 |
529 |
530 | 556.260010
531 |
532 |
533 | 553.856689
534 |
535 |
536 | 554.337402
537 |
538 |
539 | 553.856689
540 |
541 |
542 | 552.414551
543 |
544 |
545 | 551.453369
546 |
547 |
548 | 550.492188
549 |
550 |
551 | 549.530762
552 |
553 |
554 | 546.166260
555 |
556 |
557 | 545.685547
558 |
559 |
560 | 543.282104
561 |
562 |
563 | 544.243652
564 |
565 |
566 | 546.166260
567 |
568 |
569 | 546.646851
570 |
571 |
572 | 545.685547
573 |
574 |
575 | 546.646851
576 |
577 |
578 | 545.685547
579 |
580 |
581 | 544.243652
582 |
583 |
584 | 543.282104
585 |
586 |
587 |
588 |
589 | ACTIVE LOG #3
590 | 2
591 |
592 |
593 | 546.646851
594 |
595 |
596 | 549.050049
597 |
598 |
599 | 548.088867
600 |
601 |
602 | 548.569336
603 |
604 |
605 | 548.569336
606 |
607 |
608 | 548.569336
609 |
610 |
611 | 548.569336
612 |
613 |
614 | 549.050049
615 |
616 |
617 | 549.530762
618 |
619 |
620 | 549.530762
621 |
622 |
623 | 549.530762
624 |
625 |
626 | 549.530762
627 |
628 |
629 | 549.530762
630 |
631 |
632 | 549.530762
633 |
634 |
635 | 549.530762
636 |
637 |
638 | 549.530762
639 |
640 |
641 | 550.011475
642 |
643 |
644 | 550.492188
645 |
646 |
647 | 550.972656
648 |
649 |
650 | 551.453369
651 |
652 |
653 | 550.972656
654 |
655 |
656 | 550.492188
657 |
658 |
659 | 550.011475
660 |
661 |
662 | 550.011475
663 |
664 |
665 | 550.011475
666 |
667 |
668 | 550.011475
669 |
670 |
671 | 550.011475
672 |
673 |
674 | 550.492188
675 |
676 |
677 | 550.492188
678 |
679 |
680 | 550.492188
681 |
682 |
683 | 550.972656
684 |
685 |
686 | 550.492188
687 |
688 |
689 | 550.972656
690 |
691 |
692 | 550.972656
693 |
694 |
695 | 550.972656
696 |
697 |
698 | 551.453369
699 |
700 |
701 | 551.453369
702 |
703 |
704 | 551.453369
705 |
706 |
707 | 551.934082
708 |
709 |
710 | 551.453369
711 |
712 |
713 | 551.934082
714 |
715 |
716 | 551.934082
717 |
718 |
719 | 551.934082
720 |
721 |
722 | 551.453369
723 |
724 |
725 | 551.934082
726 |
727 |
728 | 551.934082
729 |
730 |
731 | 551.934082
732 |
733 |
734 | 551.453369
735 |
736 |
737 | 551.453369
738 |
739 |
740 | 551.453369
741 |
742 |
743 | 551.453369
744 |
745 |
746 | 550.972656
747 |
748 |
749 |
750 |
751 | ACTIVE LOG #4
752 | 3
753 |
754 |
755 | 506.752075
756 |
757 |
758 | 544.243652
759 |
760 |
761 |
762 |
763 | ACTIVE LOG #5
764 | 4
765 |
766 |
767 | 545.685547
768 |
769 |
770 | 551.934082
771 |
772 |
773 | 552.414551
774 |
775 |
776 | 553.375977
777 |
778 |
779 | 553.375977
780 |
781 |
782 | 552.414551
783 |
784 |
785 | 552.895264
786 |
787 |
788 | 553.375977
789 |
790 |
791 | 552.414551
792 |
793 |
794 | 552.895264
795 |
796 |
797 | 553.375977
798 |
799 |
800 | 553.375977
801 |
802 |
803 | 553.375977
804 |
805 |
806 | 553.375977
807 |
808 |
809 | 553.375977
810 |
811 |
812 | 553.375977
813 |
814 |
815 | 553.375977
816 |
817 |
818 | 553.375977
819 |
820 |
821 | 553.375977
822 |
823 |
824 | 553.375977
825 |
826 |
827 | 553.856689
828 |
829 |
830 | 554.337402
831 |
832 |
833 | 553.856689
834 |
835 |
836 | 553.856689
837 |
838 |
839 | 553.375977
840 |
841 |
842 | 552.895264
843 |
844 |
845 | 552.895264
846 |
847 |
848 | 552.895264
849 |
850 |
851 | 552.895264
852 |
853 |
854 | 552.414551
855 |
856 |
857 | 552.895264
858 |
859 |
860 | 552.895264
861 |
862 |
863 | 551.934082
864 |
865 |
866 | 551.934082
867 |
868 |
869 | 551.934082
870 |
871 |
872 | 551.453369
873 |
874 |
875 | 551.934082
876 |
877 |
878 | 551.934082
879 |
880 |
881 | 552.414551
882 |
883 |
884 | 552.414551
885 |
886 |
887 | 552.414551
888 |
889 |
890 | 552.414551
891 |
892 |
893 | 552.414551
894 |
895 |
896 | 555.779297
897 |
898 |
899 |
900 |
901 | ACTIVE LOG #6
902 | 5
903 |
904 |
905 | 511.558716
906 |
907 |
908 | 542.320923
909 |
910 |
911 |
912 |
913 | ACTIVE LOG #7
914 | 6
915 |
916 |
917 | 544.243652
918 |
919 |
920 | 543.282104
921 |
922 |
923 |
924 |
925 | ACTIVE LOG #8
926 | 7
927 |
928 |
929 | 511.078003
930 |
931 |
932 | 556.260010
933 |
934 |
935 | 555.298584
936 |
937 |
938 | 556.740723
939 |
940 |
941 | 561.066650
942 |
943 |
944 | 579.331543
945 |
946 |
947 | 578.370361
948 |
949 |
950 | 565.392578
951 |
952 |
953 | 549.530762
954 |
955 |
956 | 540.398315
957 |
958 |
959 | 540.398315
960 |
961 |
962 | 540.878906
963 |
964 |
965 | 546.646851
966 |
967 |
968 | 547.608154
969 |
970 |
971 | 551.934082
972 |
973 |
974 | 554.337402
975 |
976 |
977 | 556.740723
978 |
979 |
980 |
981 | 559.144043
982 |
983 |
984 |
985 | 561.066650
986 |
987 |
988 |
989 | 561.066650
990 |
991 |
992 |
993 | 562.508545
994 |
995 |
996 |
997 |
998 |
999 |
--------------------------------------------------------------------------------
/test_files/file.gpx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Garmin International
6 |
7 |
8 |
9 |
10 | 195.440933
11 |
12 | 001
13 | Flag, Blue
14 |
15 |
16 | 195.438324
17 |
18 | 002
19 | Flag, Blue
20 |
21 |
22 | 17-MRZ-12 16:44:12
23 |
24 |
25 | Cyan
26 |
27 |
28 |
29 |
30 | 59.26
31 |
32 |
33 |
34 | 0
35 |
36 |
37 |
38 |
39 | 65.51
40 |
41 |
42 |
43 | 0
44 |
45 |
46 |
47 |
48 | 65.99
49 |
50 |
51 |
52 | 0
53 |
54 |
55 |
56 |
57 | 63.58
58 |
59 |
60 |
61 | 0
62 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/test_files/gpx-without-root-attributes.gpx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 1614.678000
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/test_files/gpx-without-xml-declaration.gpx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 1614.678000
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/test_files/gpx1.0_with_all_fields.gpx:
--------------------------------------------------------------------------------
1 |
2 | example name
3 | example description
4 | example author
5 | example@email.com
6 | http://example.url
7 | example urlname
8 |
9 | example keywords
10 |
11 |
12 | 75.1
13 |
14 | 1.1
15 | 2.0
16 | example name
17 | example cmt
18 | example desc
19 | example src
20 | example url
21 | example urlname
22 | example sym
23 | example type
24 | 2d
25 | 5
26 | 6
27 | 7
28 | 8
29 | 9
30 | 45
31 |
32 |
33 |
34 |
35 | example name
36 | example cmt
37 | example desc
38 | example src
39 | example url
40 | example urlname
41 | 7
42 |
43 | 75.1
44 |
45 | 1.2
46 | 2.1
47 | example name r
48 | example cmt r
49 | example desc r
50 | example src r
51 | example url r
52 | example urlname r
53 | example sym r
54 | example type r
55 | 3d
56 | 6
57 | 7
58 | 8
59 | 9
60 | 10
61 | 99
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | second route
70 | example desc 2
71 |
72 |
73 |
74 |
75 |
76 |
77 | example name t
78 | example cmt t
79 | example desc t
80 | example src t
81 | example url t
82 | example urlname t
83 | 1
84 |
85 |
86 | 11.1
87 |
88 | 12
89 | 13
90 | example name t
91 | example cmt t
92 | example desc t
93 | example src t
94 | example url t
95 | example urlname t
96 | example sym t
97 | example type t
98 | 3d
99 | 100
100 | 101
101 | 102
102 | 103
103 | 104
104 | 99
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/test_files/gpx1.1_with_all_fields.gpx:
--------------------------------------------------------------------------------
1 |
2 |
3 | example name
4 | example description
5 |
6 | author name
7 |
8 |
9 | link text
10 | link type
11 |
12 |
13 |
14 |
15 | 2013
16 | lic
17 |
18 |
19 | link text2
20 | link type2
21 |
22 |
23 | example keywords
24 |
25 |
26 | bbb
27 | ccc
28 | ddd
29 |
30 |
31 |
32 | 75.1
33 |
34 | 1.1
35 | 2.0
36 | example name
37 | example cmt
38 | example desc
39 | example src
40 |
41 | link text3
42 | link type3
43 |
44 | example sym
45 | example type
46 | 2d
47 | 5
48 | 6
49 | 7
50 | 8
51 | 9
52 | 45
53 |
54 | bbb
55 | ddd
56 |
57 |
58 |
59 |
60 |
61 | example name
62 | example cmt
63 | example desc
64 | example src
65 |
66 | link text3
67 | link type3
68 |
69 | 7
70 | rte type
71 |
72 | 1
73 | 2
74 |
75 |
76 | 75.1
77 |
78 | 1.2
79 | 2.1
80 | example name r
81 | example cmt r
82 | example desc r
83 | example src r
84 |
85 | rtept link
86 | rtept link type
87 |
88 | example sym r
89 | example type r
90 | 3d
91 | 6
92 | 7
93 | 8
94 | 9
95 | 10
96 | 99
97 |
98 | rtept
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | second route
108 | example desc 2
109 |
110 |
111 |
112 |
113 |
114 |
115 | example name t
116 | example cmt t
117 | example desc t
118 | example src t
119 |
120 | trk link
121 | trk link type
122 |
123 | 1
124 | t
125 |
126 | 2
127 |
128 |
129 |
130 | 11.1
131 |
132 | 12
133 | 13
134 | example name t
135 | example cmt t
136 | example desc t
137 | example src t
138 |
139 | trkpt link
140 | trkpt link type
141 |
142 | example sym t
143 | example type t
144 | 3d
145 | 100
146 | 101
147 | 102
148 | 103
149 | 104
150 | 99
151 |
152 | true
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 | ...
163 |
164 |
165 |
166 |
167 |
--------------------------------------------------------------------------------
/test_files/gpx1.1_with_extensions.gpx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | bbbhhh
5 |
6 |
7 | ggg
8 |
9 |
10 |
11 |
12 |
13 |
14 | bbbhhh
15 |
16 |
17 | ggg
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | bbbhhh
26 |
27 |
28 | ggg
29 |
30 |
31 |
32 |
33 |
34 |
35 | 17-MRZ-12 16:44:12
36 |
37 |
38 | 59.26
39 |
40 |
41 | bbbhhh
42 |
43 |
44 | ggg
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/test_files/gpx1.1_with_extensions_without_namespaces.gpx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | bbbhhh
5 |
6 | eee
7 | gggiii
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/test_files/gpx_with_garmin_extension.gpx:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 | 3.4171
11 |
12 |
--------------------------------------------------------------------------------
/test_files/graphhopper.gpx.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tkrajina/gpxgo/0a078e11c1cccf5466ffb087efcecff0246235c0/test_files/graphhopper.gpx.gz
--------------------------------------------------------------------------------
/test_files/iso8859-1encoded.gpx:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 | 1
13 |
14 |
15 |
16 | 2
17 |
18 |
19 |
20 | 3
21 |
22 |
23 |
24 | 4
25 |
26 |
27 |
28 | 5
29 |
30 |
31 |
32 | 6
33 |
34 |
35 |
36 | 7
37 |
38 |
39 |
40 | 8
41 |
42 |
43 |
--------------------------------------------------------------------------------