├── .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 | {" 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 | --------------------------------------------------------------------------------