├── .github └── workflows │ └── main.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── README.md ├── example_test.go ├── examples ├── go.mod ├── go.sum ├── hike-and-fly-route-kml │ ├── README.md │ └── main.go └── route-to-kml │ └── main.go ├── go.mod ├── go.sum ├── icon ├── icon.go └── icon_test.go ├── internal └── generate │ ├── README.md │ ├── main.go │ ├── output.go.tmpl │ └── xsd.go ├── kml.go ├── kml22gx.gen.go ├── kml22gx.go ├── kml_test.go ├── kmz.go ├── kmz_test.go ├── ogckml22.gen.go ├── ogckml22.go ├── sphere ├── circle_test.go ├── example_test.go ├── sphere.go └── sphere_test.go └── xsd ├── kml22gx.xsd └── ogckml22.xsd /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - v* 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 14 | - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b 15 | - run: go build ./... 16 | - run: go test ./... 17 | - id: generate 18 | run: | 19 | go generate 20 | git diff --exit-code 21 | lint: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 25 | - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b 26 | - uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd 27 | with: 28 | version: v2.0.2 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /coverage.out 3 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | go: "1.22" 4 | linters: 5 | enable: 6 | - asasalint 7 | - asciicheck 8 | - bidichk 9 | - bodyclose 10 | - canonicalheader 11 | - containedctx 12 | - contextcheck 13 | - copyloopvar 14 | - decorder 15 | - dogsled 16 | - dupl 17 | - dupword 18 | - durationcheck 19 | - err113 20 | - errchkjson 21 | - errname 22 | - errorlint 23 | - exhaustive 24 | - fatcontext 25 | - forbidigo 26 | - forcetypeassert 27 | - ginkgolinter 28 | - gocheckcompilerdirectives 29 | - gochecknoinits 30 | - gochecksumtype 31 | - gocognit 32 | - goconst 33 | - gocritic 34 | - gocyclo 35 | - godot 36 | - goheader 37 | - gomoddirectives 38 | - gomodguard 39 | - goprintffuncname 40 | - gosec 41 | - gosmopolitan 42 | - grouper 43 | - iface 44 | - importas 45 | - inamedparam 46 | - interfacebloat 47 | - intrange 48 | - ireturn 49 | - loggercheck 50 | - makezero 51 | - mirror 52 | - misspell 53 | - musttag 54 | - nakedret 55 | - nestif 56 | - nilerr 57 | - nilnil 58 | - noctx 59 | - nolintlint 60 | - nonamedreturns 61 | - nosprintfhostport 62 | - perfsprint 63 | - prealloc 64 | - predeclared 65 | - promlinter 66 | - protogetter 67 | - reassign 68 | - recvcheck 69 | - revive 70 | - rowserrcheck 71 | - sloglint 72 | - spancheck 73 | - sqlclosecheck 74 | - staticcheck 75 | - tagalign 76 | - tagliatelle 77 | - testableexamples 78 | - testifylint 79 | - testpackage 80 | - thelper 81 | - tparallel 82 | - unconvert 83 | - unparam 84 | - usestdlibvars 85 | - wastedassign 86 | - whitespace 87 | - zerologlint 88 | disable: 89 | - cyclop 90 | - depguard 91 | - exhaustruct 92 | - funlen 93 | - gochecknoglobals 94 | - godox 95 | - lll 96 | - maintidx 97 | - nlreturn 98 | - paralleltest 99 | - varnamelen 100 | - wrapcheck 101 | - wsl 102 | settings: 103 | govet: 104 | disable: 105 | - fieldalignment 106 | - shadow 107 | enable-all: true 108 | ireturn: 109 | allow: 110 | - error 111 | - github.com/twpayne/go-kml/v3.ParentElement 112 | misspell: 113 | locale: US 114 | staticcheck: 115 | checks: 116 | - all 117 | exclusions: 118 | generated: lax 119 | presets: 120 | - comments 121 | - common-false-positives 122 | - legacy 123 | - std-error-handling 124 | rules: 125 | - linters: 126 | - dupl 127 | - scopelint 128 | path: _test\.go 129 | - linters: 130 | - forbidigo 131 | - gosec 132 | path: internal/ 133 | - linters: 134 | - err113 135 | text: do not define dynamic errors, use wrapped static errors instead 136 | paths: 137 | - third_party$ 138 | - builtin$ 139 | - examples$ 140 | formatters: 141 | enable: 142 | - gci 143 | - gofmt 144 | - gofumpt 145 | - goimports 146 | settings: 147 | gci: 148 | sections: 149 | - standard 150 | - default 151 | - prefix(github.com/twpayne/go-kml) 152 | gofumpt: 153 | extra-rules: true 154 | goimports: 155 | local-prefixes: 156 | - github.com/twpayne/go-kml 157 | exclusions: 158 | generated: lax 159 | paths: 160 | - third_party$ 161 | - builtin$ 162 | - examples$ 163 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Tom Payne 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-kml 2 | 3 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/twpayne/go-kml/v3)](https://pkg.go.dev/github.com/twpayne/go-kml/v3) 4 | 5 | Package `kml` provides convenience methods for creating and writing KML documents. 6 | 7 | ## Key Features 8 | 9 | * Simple API for building arbitrarily complex KML documents. 10 | * Support for all KML elements, including Google Earth `gx:` extensions. 11 | * Compatibility with the standard library [`encoding/xml`](https://pkg.go.dev/encoding/xml) package. 12 | * Pretty (neatly indented) and compact (minimum size) output formats. 13 | * Support for shared `Style` and `StyleMap` elements. 14 | * Simple mapping between functions and KML elements. 15 | * Convenience functions for using standard KML icons. 16 | * Convenience functions for spherical geometry. 17 | 18 | ## Example 19 | 20 | ```go 21 | func ExampleKML() { 22 | k := kml.KML( 23 | kml.Placemark( 24 | kml.Name("Simple placemark"), 25 | kml.Description("Attached to the ground. Intelligently places itself at the height of the underlying terrain."), 26 | kml.Point( 27 | kml.Coordinates(kml.Coordinate{Lon: -122.0822035425683, Lat: 37.42228990140251}), 28 | ), 29 | ), 30 | ) 31 | if err := k.WriteIndent(os.Stdout, "", " "); err != nil { 32 | log.Fatal(err) 33 | } 34 | } 35 | ``` 36 | 37 | Output: 38 | 39 | ```xml 40 | 41 | 42 | 43 | Simple placemark 44 | Attached to the ground. Intelligently places itself at the height of the underlying terrain. 45 | 46 | -122.0822035425683,37.42228990140251 47 | 48 | 49 | 50 | ``` 51 | 52 | There are more [examples in the 53 | documentation](https://pkg.go.dev/github.com/twpayne/go-kml/v3#pkg-examples) 54 | corresponding to the [examples in the KML 55 | tutorial](https://developers.google.com/kml/documentation/kml_tut). 56 | 57 | ## Subpackages 58 | 59 | * [`icon`](https://pkg.go.dev/github.com/twpayne/go-kml/v3/icon) Convenience functions for using standard KML icons. 60 | * [`sphere`](https://pkg.go.dev/github.com/twpayne/go-kml/v3/sphere) Convenience functions for spherical geometry. 61 | 62 | ## License 63 | 64 | MIT 65 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package kml_test 2 | 3 | import ( 4 | "image/color" 5 | "log" 6 | "os" 7 | 8 | "github.com/twpayne/go-kml/v3" 9 | ) 10 | 11 | func ExamplePlacemark() { 12 | k := kml.KML( 13 | kml.Placemark( 14 | kml.Name("Simple placemark"), 15 | kml.Description("Attached to the ground. Intelligently places itself at the height of the underlying terrain."), 16 | kml.Point( 17 | kml.Coordinates(kml.Coordinate{Lon: -122.0822035425683, Lat: 37.42228990140251}), 18 | ), 19 | ), 20 | ) 21 | if err := k.WriteIndent(os.Stdout, "", " "); err != nil { 22 | log.Fatal(err) 23 | } 24 | // Output: 25 | // 26 | // 27 | // 28 | // Simple placemark 29 | // Attached to the ground. Intelligently places itself at the height of the underlying terrain. 30 | // 31 | // -122.0822035425683,37.42228990140251 32 | // 33 | // 34 | // 35 | } 36 | 37 | func ExampleDescription() { 38 | k := kml.KML( 39 | kml.Document( 40 | kml.Placemark( 41 | kml.Name("CDATA example"), 42 | kml.Description(`

CDATA Tags are useful!

Text is more readable and easier to write when you can avoid using entity references.

`), 43 | kml.Point( 44 | kml.Coordinates(kml.Coordinate{Lon: 102.595626, Lat: 14.996729}), 45 | ), 46 | ), 47 | ), 48 | ) 49 | if err := k.WriteIndent(os.Stdout, "", " "); err != nil { 50 | log.Fatal(err) 51 | } 52 | // Output: 53 | // 54 | // 55 | // 56 | // 57 | // CDATA example 58 | // <h1>CDATA Tags are useful!</h1> <p><font color="red">Text is <i>more readable</i> and <b>easier to write</b> when you can avoid using entity references.</font></p> 59 | // 60 | // 102.595626,14.996729 61 | // 62 | // 63 | // 64 | // 65 | } 66 | 67 | func ExampleGroundOverlay() { 68 | k := kml.KML( 69 | kml.Folder( 70 | kml.Name("Ground Overlays"), 71 | kml.Description("Examples of ground overlays"), 72 | kml.GroundOverlay( 73 | kml.Name("Large-scale overlay on terrain"), 74 | kml.Description("Overlay shows Mount Etna erupting on July 13th, 2001."), 75 | kml.Icon( 76 | kml.Href("https://developers.google.com/kml/documentation/images/etna.jpg"), 77 | ), 78 | kml.LatLonBox( 79 | kml.North(37.91904192681665), 80 | kml.South(37.46543388598137), 81 | kml.East(15.35832653742206), 82 | kml.West(14.60128369746704), 83 | kml.Rotation(-0.1556640799496235), 84 | ), 85 | ), 86 | ), 87 | ) 88 | if err := k.WriteIndent(os.Stdout, "", " "); err != nil { 89 | log.Fatal(err) 90 | } 91 | // Output: 92 | // 93 | // 94 | // 95 | // Ground Overlays 96 | // Examples of ground overlays 97 | // 98 | // Large-scale overlay on terrain 99 | // Overlay shows Mount Etna erupting on July 13th, 2001. 100 | // 101 | // https://developers.google.com/kml/documentation/images/etna.jpg 102 | // 103 | // 104 | // 37.91904192681665 105 | // 37.46543388598137 106 | // 15.35832653742206 107 | // 14.60128369746704 108 | // -0.1556640799496235 109 | // 110 | // 111 | // 112 | // 113 | } 114 | 115 | func ExampleLineString() { 116 | k := kml.KML( 117 | kml.Document( 118 | kml.Name("Paths"), 119 | kml.Description("Examples of paths. Note that the tessellate tag is by default set to 0. If you want to create tessellated lines, they must be authored (or edited) directly in KML."), 120 | kml.SharedStyle( 121 | "yellowLineGreenPoly", 122 | kml.LineStyle( 123 | kml.Color(color.RGBA{R: 255, G: 255, B: 0, A: 127}), 124 | kml.Width(4), 125 | ), 126 | kml.PolyStyle( 127 | kml.Color(color.RGBA{R: 0, G: 255, B: 0, A: 127}), 128 | ), 129 | ), 130 | kml.Placemark( 131 | kml.Name("Absolute Extruded"), 132 | kml.Description("Transparent green wall with yellow outlines"), 133 | kml.StyleURL("#yellowLineGreenPoly"), 134 | kml.LineString( 135 | kml.Extrude(true), 136 | kml.Tessellate(true), 137 | kml.AltitudeMode(kml.AltitudeModeAbsolute), 138 | kml.Coordinates([]kml.Coordinate{ 139 | {Lon: -112.2550785337791, Lat: 36.07954952145647, Alt: 2357}, 140 | {Lon: -112.2549277039738, Lat: 36.08117083492122, Alt: 2357}, 141 | {Lon: -112.2552505069063, Lat: 36.08260761307279, Alt: 2357}, 142 | {Lon: -112.2564540158376, Lat: 36.08395660588506, Alt: 2357}, 143 | {Lon: -112.2580238976449, Lat: 36.08511401044813, Alt: 2357}, 144 | {Lon: -112.2595218489022, Lat: 36.08584355239394, Alt: 2357}, 145 | {Lon: -112.2608216347552, Lat: 36.08612634548589, Alt: 2357}, 146 | {Lon: -112.262073428656, Lat: 36.08626019085147, Alt: 2357}, 147 | {Lon: -112.2633204928495, Lat: 36.08621519860091, Alt: 2357}, 148 | {Lon: -112.2644963846444, Lat: 36.08627897945274, Alt: 2357}, 149 | {Lon: -112.2656969554589, Lat: 36.08649599090644, Alt: 2357}, 150 | }...), 151 | ), 152 | ), 153 | ), 154 | ) 155 | if err := k.WriteIndent(os.Stdout, "", " "); err != nil { 156 | log.Fatal(err) 157 | } 158 | // Output: 159 | // 160 | // 161 | // 162 | // Paths 163 | // Examples of paths. Note that the tessellate tag is by default set to 0. If you want to create tessellated lines, they must be authored (or edited) directly in KML. 164 | // 173 | // 174 | // Absolute Extruded 175 | // Transparent green wall with yellow outlines 176 | // #yellowLineGreenPoly 177 | // 178 | // 1 179 | // 1 180 | // absolute 181 | // -112.2550785337791,36.07954952145647,2357 -112.2549277039738,36.08117083492122,2357 -112.2552505069063,36.08260761307279,2357 -112.2564540158376,36.08395660588506,2357 -112.2580238976449,36.08511401044813,2357 -112.2595218489022,36.08584355239394,2357 -112.2608216347552,36.08612634548589,2357 -112.262073428656,36.08626019085147,2357 -112.2633204928495,36.08621519860091,2357 -112.2644963846444,36.08627897945274,2357 -112.2656969554589,36.08649599090644,2357 182 | // 183 | // 184 | // 185 | // 186 | } 187 | 188 | func ExamplePolygon() { 189 | k := kml.KML( 190 | kml.Placemark( 191 | kml.Name("The Pentagon"), 192 | kml.Polygon( 193 | kml.Extrude(true), 194 | kml.AltitudeMode(kml.AltitudeModeRelativeToGround), 195 | kml.OuterBoundaryIs( 196 | kml.LinearRing( 197 | kml.Coordinates([]kml.Coordinate{ 198 | {Lon: -77.05788457660967, Lat: 38.87253259892824, Alt: 100}, 199 | {Lon: -77.05465973756702, Lat: 38.87291016281703, Alt: 100}, 200 | {Lon: -77.0531553685479, Lat: 38.87053267794386, Alt: 100}, 201 | {Lon: -77.05552622493516, Lat: 38.868757801256, Alt: 100}, 202 | {Lon: -77.05844056290393, Lat: 38.86996206506943, Alt: 100}, 203 | {Lon: -77.05788457660967, Lat: 38.87253259892824, Alt: 100}, 204 | }...), 205 | ), 206 | ), 207 | kml.InnerBoundaryIs( 208 | kml.LinearRing( 209 | kml.Coordinates([]kml.Coordinate{ 210 | {Lon: -77.05668055019126, Lat: 38.87154239798456, Alt: 100}, 211 | {Lon: -77.05542625960818, Lat: 38.87167890344077, Alt: 100}, 212 | {Lon: -77.05485125901023, Lat: 38.87076535397792, Alt: 100}, 213 | {Lon: -77.05577677433152, Lat: 38.87008686581446, Alt: 100}, 214 | {Lon: -77.05691162017543, Lat: 38.87054446963351, Alt: 100}, 215 | {Lon: -77.05668055019126, Lat: 38.87154239798456, Alt: 100}, 216 | }...), 217 | ), 218 | ), 219 | ), 220 | ), 221 | ) 222 | if err := k.WriteIndent(os.Stdout, "", " "); err != nil { 223 | log.Fatal(err) 224 | } 225 | // Output: 226 | // 227 | // 228 | // 229 | // The Pentagon 230 | // 231 | // 1 232 | // relativeToGround 233 | // 234 | // 235 | // -77.05788457660967,38.87253259892824,100 -77.05465973756702,38.87291016281703,100 -77.0531553685479,38.87053267794386,100 -77.05552622493516,38.868757801256,100 -77.05844056290393,38.86996206506943,100 -77.05788457660967,38.87253259892824,100 236 | // 237 | // 238 | // 239 | // 240 | // -77.05668055019126,38.87154239798456,100 -77.05542625960818,38.87167890344077,100 -77.05485125901023,38.87076535397792,100 -77.05577677433152,38.87008686581446,100 -77.05691162017543,38.87054446963351,100 -77.05668055019126,38.87154239798456,100 241 | // 242 | // 243 | // 244 | // 245 | // 246 | } 247 | 248 | func ExampleStyle() { 249 | k := kml.KML( 250 | kml.Document( 251 | kml.SharedStyle( 252 | "transBluePoly", 253 | kml.LineStyle( 254 | kml.Width(1.5), 255 | ), 256 | kml.PolyStyle( 257 | kml.Color(color.RGBA{R: 0, G: 0, B: 255, A: 125}), 258 | ), 259 | ), 260 | kml.Placemark( 261 | kml.Name("Building 41"), 262 | kml.StyleURL("#transBluePoly"), 263 | kml.Polygon( 264 | kml.Extrude(true), 265 | kml.AltitudeMode(kml.AltitudeModeRelativeToGround), 266 | kml.OuterBoundaryIs( 267 | kml.LinearRing( 268 | kml.Coordinates([]kml.Coordinate{ 269 | {Lon: -122.0857412771483, Lat: 37.42227033155257, Alt: 17}, 270 | {Lon: -122.0858169768481, Lat: 37.42231408832346, Alt: 17}, 271 | {Lon: -122.085852582875, Lat: 37.42230337469744, Alt: 17}, 272 | {Lon: -122.0858799945639, Lat: 37.42225686138789, Alt: 17}, 273 | {Lon: -122.0858860101409, Lat: 37.4222311076138, Alt: 17}, 274 | {Lon: -122.0858069157288, Lat: 37.42220250173855, Alt: 17}, 275 | {Lon: -122.0858379542653, Lat: 37.42214027058678, Alt: 17}, 276 | {Lon: -122.0856732640519, Lat: 37.42208690214408, Alt: 17}, 277 | {Lon: -122.0856022926407, Lat: 37.42214885429042, Alt: 17}, 278 | {Lon: -122.0855902778436, Lat: 37.422128290487, Alt: 17}, 279 | {Lon: -122.0855841672237, Lat: 37.42208171967246, Alt: 17}, 280 | {Lon: -122.0854852065741, Lat: 37.42210455874995, Alt: 17}, 281 | {Lon: -122.0855067264352, Lat: 37.42214267949824, Alt: 17}, 282 | {Lon: -122.0854430712915, Lat: 37.42212783846172, Alt: 17}, 283 | {Lon: -122.0850990714904, Lat: 37.42251282407603, Alt: 17}, 284 | {Lon: -122.0856769818632, Lat: 37.42281815323651, Alt: 17}, 285 | {Lon: -122.0860162273783, Lat: 37.42244918858722, Alt: 17}, 286 | {Lon: -122.0857260327004, Lat: 37.42229239604253, Alt: 17}, 287 | {Lon: -122.0857412771483, Lat: 37.42227033155257, Alt: 17}, 288 | }...), 289 | ), 290 | ), 291 | ), 292 | ), 293 | ), 294 | ) 295 | if err := k.WriteIndent(os.Stdout, "", " "); err != nil { 296 | log.Fatal(err) 297 | } 298 | // Output: 299 | // 300 | // 301 | // 302 | // 310 | // 311 | // Building 41 312 | // #transBluePoly 313 | // 314 | // 1 315 | // relativeToGround 316 | // 317 | // 318 | // -122.0857412771483,37.42227033155257,17 -122.0858169768481,37.42231408832346,17 -122.085852582875,37.42230337469744,17 -122.0858799945639,37.42225686138789,17 -122.0858860101409,37.4222311076138,17 -122.0858069157288,37.42220250173855,17 -122.0858379542653,37.42214027058678,17 -122.0856732640519,37.42208690214408,17 -122.0856022926407,37.42214885429042,17 -122.0855902778436,37.422128290487,17 -122.0855841672237,37.42208171967246,17 -122.0854852065741,37.42210455874995,17 -122.0855067264352,37.42214267949824,17 -122.0854430712915,37.42212783846172,17 -122.0850990714904,37.42251282407603,17 -122.0856769818632,37.42281815323651,17 -122.0860162273783,37.42244918858722,17 -122.0857260327004,37.42229239604253,17 -122.0857412771483,37.42227033155257,17 319 | // 320 | // 321 | // 322 | // 323 | // 324 | // 325 | } 326 | 327 | func ExampleSharedStyleMap() { 328 | k := kml.KML( 329 | kml.Document( 330 | kml.Name("Highlighted Icon"), 331 | kml.Description("Place your mouse over the icon to see it display the new icon"), 332 | kml.SharedStyle( 333 | "highlightPlacemark", 334 | kml.IconStyle( 335 | kml.Icon( 336 | kml.Href("http://maps.google.com/mapfiles/kml/paddle/red-stars.png"), 337 | ), 338 | ), 339 | ), 340 | kml.SharedStyle( 341 | "normalPlacemark", 342 | kml.IconStyle( 343 | kml.Icon( 344 | kml.Href("http://maps.google.com/mapfiles/kml/paddle/wht-blank.png"), 345 | ), 346 | ), 347 | ), 348 | kml.SharedStyleMap( 349 | "exampleStyleMap", 350 | kml.Pair( 351 | kml.Key(kml.StyleStateNormal), 352 | kml.StyleURL("#normalPlacemark"), 353 | ), 354 | kml.Pair( 355 | kml.Key(kml.StyleStateHighlight), 356 | kml.StyleURL("#highlightPlacemark"), 357 | ), 358 | ), 359 | kml.Placemark( 360 | kml.Name("Roll over this icon"), 361 | kml.StyleURL("#exampleStyleMap"), 362 | kml.Point( 363 | kml.Coordinates(kml.Coordinate{Lon: -122.0856545755255, Lat: 37.42243077405461}), 364 | ), 365 | ), 366 | ), 367 | ) 368 | if err := k.WriteIndent(os.Stdout, "", " "); err != nil { 369 | log.Fatal(err) 370 | } 371 | // Output: 372 | // 373 | // 374 | // 375 | // Highlighted Icon 376 | // Place your mouse over the icon to see it display the new icon 377 | // 384 | // 391 | // 392 | // 393 | // normal 394 | // #normalPlacemark 395 | // 396 | // 397 | // highlight 398 | // #highlightPlacemark 399 | // 400 | // 401 | // 402 | // Roll over this icon 403 | // #exampleStyleMap 404 | // 405 | // -122.0856545755255,37.42243077405461 406 | // 407 | // 408 | // 409 | // 410 | } 411 | 412 | func ExampleScreenOverlay() { 413 | k := kml.KML( 414 | kml.ScreenOverlay( 415 | kml.Name("Absolute Positioning: Top left"), 416 | kml.Icon( 417 | kml.Href("http://developers.google.com/kml/documentation/images/top_left.jpg"), 418 | ), 419 | kml.OverlayXY(kml.Vec2{X: 0, Y: 1, XUnits: kml.UnitsFraction, YUnits: kml.UnitsFraction}), 420 | kml.ScreenXY(kml.Vec2{X: 0, Y: 1, XUnits: kml.UnitsFraction, YUnits: kml.UnitsFraction}), 421 | kml.RotationXY(kml.Vec2{X: 0, Y: 0, XUnits: kml.UnitsFraction, YUnits: kml.UnitsFraction}), 422 | kml.Size(kml.Vec2{X: 0, Y: 0, XUnits: kml.UnitsFraction, YUnits: kml.UnitsFraction}), 423 | ), 424 | ) 425 | if err := k.WriteIndent(os.Stdout, "", " "); err != nil { 426 | log.Fatal(err) 427 | } 428 | // Output: 429 | // 430 | // 431 | // 432 | // Absolute Positioning: Top left 433 | // 434 | // http://developers.google.com/kml/documentation/images/top_left.jpg 435 | // 436 | // 437 | // 438 | // 439 | // 440 | // 441 | // 442 | } 443 | 444 | func ExampleNetworkLink() { 445 | k := kml.KML( 446 | kml.Folder( 447 | kml.Name("Network Links"), 448 | kml.Visibility(false), 449 | kml.Open(false), 450 | kml.Description("Network link example 1"), 451 | kml.NetworkLink( 452 | kml.Name("Random Placemark"), 453 | kml.Visibility(false), 454 | kml.Open(false), 455 | kml.Description("A simple server-side script that generates a new random placemark on each call"), 456 | kml.RefreshVisibility(false), 457 | kml.FlyToView(false), 458 | kml.Link( 459 | kml.Href("http://yourserver.com/cgi-bin/randomPlacemark.py"), 460 | ), 461 | ), 462 | ), 463 | ) 464 | if err := k.WriteIndent(os.Stdout, "", " "); err != nil { 465 | log.Fatal(err) 466 | } 467 | // Output: 468 | // 469 | // 470 | // 471 | // Network Links 472 | // 0 473 | // 0 474 | // Network link example 1 475 | // 476 | // Random Placemark 477 | // 0 478 | // 0 479 | // A simple server-side script that generates a new random placemark on each call 480 | // 0 481 | // 0 482 | // 483 | // http://yourserver.com/cgi-bin/randomPlacemark.py 484 | // 485 | // 486 | // 487 | // 488 | } 489 | -------------------------------------------------------------------------------- /examples/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/twpayne/go-kml/examples 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | github.com/twpayne/go-gpx v1.4.1 9 | github.com/twpayne/go-kml/v3 v3.3.0 10 | github.com/twpayne/go-polyline v1.1.1 11 | github.com/twpayne/go-waypoint v0.1.0 12 | ) 13 | 14 | require ( 15 | github.com/twpayne/go-geom v1.6.0 // indirect 16 | golang.org/x/net v0.38.0 // indirect 17 | golang.org/x/text v0.23.0 // indirect 18 | ) 19 | 20 | replace github.com/twpayne/go-kml/v3 => .. 21 | -------------------------------------------------------------------------------- /examples/go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY= 2 | github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 3 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 4 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= 9 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 10 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 14 | github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 15 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 16 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 17 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 18 | github.com/twpayne/go-geom v1.6.0 h1:WPOJLCdd8OdcnHvKQepLKwOZrn5BzVlNxtQB59IDHRE= 19 | github.com/twpayne/go-geom v1.6.0/go.mod h1:Kr+Nly6BswFsKM5sd31YaoWS5PeDDH2NftJTK7Gd028= 20 | github.com/twpayne/go-gpx v1.4.1 h1:Y41EDC/r49OH6pTAQUk4Qpcp9z96fOvVLchRq/P4iys= 21 | github.com/twpayne/go-gpx v1.4.1/go.mod h1:6bVeKyVqzHRZ25UdFOWxv0f6SMW0P9lO7GO1aNNznEU= 22 | github.com/twpayne/go-polyline v1.1.1 h1:/tSF1BR7rN4HWj4XKqvRUNrCiYVMCvywxTFVofvDV0w= 23 | github.com/twpayne/go-polyline v1.1.1/go.mod h1:ybd9IWWivW/rlXPXuuckeKUyF3yrIim+iqA7kSl4NFY= 24 | github.com/twpayne/go-waypoint v0.1.0 h1:Ds5iK1+ag9MM9Y7Uiig1nzgD39Z402vYkpp1j+c/Y3Q= 25 | github.com/twpayne/go-waypoint v0.1.0/go.mod h1:iLAdRKZJUaMhj2nzYl9cLV3hbxol5vnI2VPmWLCDRuU= 26 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 27 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 28 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 29 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 31 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 32 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 33 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 34 | -------------------------------------------------------------------------------- /examples/hike-and-fly-route-kml/README.md: -------------------------------------------------------------------------------- 1 | # hike-and-fly-route-kml 2 | 3 | This directory contains an example of using `go-kml` to generate a rich KML 4 | file. It includes the use of the `icon` and `sphere` libraries. To run the 5 | example, run: 6 | 7 | $ go run $GOPATH/src/github.com/twpayne/go-kml/v3/examples/hike-and-fly-route-kml/main.go > route.kml 8 | 9 | Then open the `route.kml` file in Google Earth. 10 | -------------------------------------------------------------------------------- /examples/hike-and-fly-route-kml/main.go: -------------------------------------------------------------------------------- 1 | // hike-and-fly-route prints a KML file of the route of popular races. 2 | package main 3 | 4 | import ( 5 | "encoding/xml" 6 | "flag" 7 | "fmt" 8 | "image/color" 9 | "log" 10 | "net/url" 11 | "os" 12 | 13 | "github.com/twpayne/go-gpx" 14 | kml "github.com/twpayne/go-kml/v3" 15 | "github.com/twpayne/go-kml/v3/icon" 16 | "github.com/twpayne/go-kml/v3/sphere" 17 | polyline "github.com/twpayne/go-polyline" 18 | ) 19 | 20 | var ( 21 | formatFlag = flag.String("format", "kml", "format") 22 | raceFlag = flag.String("race", "red-bull-x-alps-2023", "race") 23 | ) 24 | 25 | var blockBearings = map[string]int{ 26 | "S": 0, 27 | "SW": 45, 28 | "W": 90, 29 | "NW": 135, 30 | "N": 180, 31 | "NE": 225, 32 | "E": 270, 33 | "SE": 315, 34 | } 35 | 36 | type turnpoint struct { 37 | name string 38 | lat float64 39 | lon float64 40 | ele float64 41 | radius int 42 | paddle string 43 | signboard bool 44 | pass string 45 | offRoute bool 46 | notes string 47 | } 48 | 49 | type race struct { 50 | name string 51 | snippet string 52 | turnpoints []turnpoint 53 | } 54 | 55 | var ( 56 | berghausBaregg = kml.Coordinate{Lat: 46.60046, Lon: 8.060011} 57 | berghausBareggNW = sphere.FAI.Offset(berghausBaregg, 100, 315) 58 | berghausBareggSE = sphere.FAI.Offset(berghausBaregg, 100, 135) 59 | 60 | lobhornhutte = kml.Coordinate{Lat: 46.618514, Lon: 7.868981} 61 | lobhornhutteNW = sphere.FAI.Offset(lobhornhutte, 100, 315) 62 | lobhornhutteSE = sphere.FAI.Offset(lobhornhutte, 100, 135) 63 | ) 64 | 65 | var races = map[string]race{ 66 | "red-bull-x-alps-2023": { 67 | name: "Red Bull X-Alps 2023", 68 | snippet: "Created by twpayne@gmail.com", 69 | turnpoints: []turnpoint{ 70 | { 71 | name: "KITZBÜHEL", 72 | lat: 47.446654, 73 | lon: 12.390899, 74 | ele: 760, 75 | paddle: "go", 76 | }, 77 | { 78 | name: "HAHNENKAM", 79 | lat: 47.426461, 80 | lon: 12.371147, 81 | ele: 1660, 82 | paddle: "1", 83 | signboard: true, 84 | }, 85 | { 86 | name: "WAGRAIN - KLEINARL", 87 | lat: 47.331859, 88 | lon: 13.303494, 89 | ele: 900, 90 | paddle: "2", 91 | signboard: true, 92 | }, 93 | { 94 | name: "CHIEMGAU ACHENTAL", 95 | lat: 47.767503, 96 | lon: 12.457437, 97 | ele: 540, 98 | paddle: "3", 99 | signboard: true, 100 | }, 101 | { 102 | name: "LERMOOS - TIROLER ZUGSPITZ ARENA", 103 | lat: 47.399948, 104 | lon: 10.879854, 105 | ele: 1000, 106 | paddle: "4", 107 | signboard: true, 108 | }, 109 | { 110 | name: "PIZ BUIN", 111 | lat: 46.844200, 112 | lon: 10.118800, 113 | ele: 3300, 114 | paddle: "5", 115 | radius: 3000, 116 | }, 117 | { 118 | name: "FIESCH - ALETSCH ARENA", 119 | lat: 46.409400, 120 | lon: 8.136880, 121 | ele: 1060, 122 | paddle: "6", 123 | signboard: true, 124 | }, 125 | { 126 | name: "FRUTIGEN", 127 | lat: 46.593319, 128 | lon: 7.654066, 129 | ele: 770, 130 | paddle: "7", 131 | signboard: true, 132 | }, 133 | { 134 | name: "NIESEN", 135 | lat: 46.645057, 136 | lon: 7.651374, 137 | ele: 2340, 138 | paddle: "8", 139 | signboard: true, 140 | }, 141 | { 142 | name: "MONT BLANC", 143 | lat: 45.832778, 144 | lon: 6.865000, 145 | ele: 4800, 146 | paddle: "9", 147 | }, 148 | { 149 | name: "COL DU PETIT SAINT-BERNARD", 150 | lat: 45.680474, 151 | lon: 6.883831, 152 | ele: 2190, 153 | paddle: "A", 154 | notes: "selfie", 155 | }, 156 | { 157 | name: "DUFOURSPITZE", 158 | lat: 45.936833, 159 | lon: 7.867056, 160 | ele: 4630, 161 | paddle: "B", 162 | radius: 5000, 163 | }, 164 | { 165 | name: "CIMA TOSA", 166 | lat: 46.175365, 167 | lon: 10.876155, 168 | ele: 2180, 169 | paddle: "C", 170 | notes: "signboard and selfie", 171 | }, 172 | { 173 | name: "3 ZINNEN", 174 | lat: 46.630400, 175 | lon: 12.315200, 176 | ele: 2740, 177 | paddle: "D", 178 | notes: "via ferrata", 179 | }, 180 | { 181 | name: "SEXTEN", 182 | lat: 46.696944, 183 | lon: 12.356500, 184 | ele: 1319, 185 | paddle: "E", 186 | signboard: true, 187 | }, 188 | { 189 | name: "SCHMITTENHÖHE", 190 | lat: 47.328744, 191 | lon: 12.737518, 192 | ele: 1960, 193 | paddle: "F", 194 | signboard: true, 195 | }, 196 | { 197 | name: "ZELL AM SEE", 198 | lat: 47.326821, 199 | lon: 12.800403, 200 | ele: 750, 201 | paddle: "stop", 202 | }, 203 | }, 204 | }, 205 | "red-bull-x-alps-2021": { 206 | name: "Red Bull X-Alps 2021", 207 | snippet: "Created by twpayne@gmail.com", 208 | turnpoints: []turnpoint{ 209 | { 210 | name: "Mozartplatz", 211 | lat: 47.798873, 212 | lon: 13.047720, 213 | ele: 432, 214 | paddle: "go", 215 | }, 216 | { 217 | name: "Gaisberg", 218 | lat: 47.804398, 219 | lon: 13.110690, 220 | ele: 1275, 221 | paddle: "1", 222 | signboard: true, 223 | }, 224 | { 225 | name: "Kleinarl Fußballplatz", 226 | lat: 47.274628, 227 | lon: 13.318581, 228 | ele: 1009, 229 | paddle: "2", 230 | signboard: true, 231 | }, 232 | { 233 | name: "Kitzbühl Streif Mausefalle", 234 | lat: 47.426461, 235 | lon: 12.371147, 236 | ele: 1633, 237 | paddle: "3", 238 | signboard: true, 239 | }, 240 | { 241 | name: "Chiemsee", 242 | lat: 47.858077, 243 | lon: 12.500269, 244 | ele: 521, 245 | radius: 3000, 246 | paddle: "4", 247 | }, 248 | { 249 | name: "Marquartstein", 250 | lat: 47.767503, 251 | lon: 12.457437, 252 | ele: 542, 253 | paddle: "red-circle", 254 | signboard: true, 255 | }, 256 | { 257 | name: "Zugspitze", 258 | lat: 47.421063, 259 | lon: 10.985517, 260 | ele: 2873, 261 | offRoute: true, 262 | pass: "N", 263 | }, 264 | { 265 | name: "Lermoos", 266 | lat: 47.401283, 267 | lon: 10.879767, 268 | ele: 990, 269 | paddle: "5", 270 | signboard: true, 271 | }, 272 | { 273 | name: "Säntis", 274 | lat: 47.249365, 275 | lon: 9.343238, 276 | ele: 2500, 277 | paddle: "6", 278 | radius: 2000, 279 | }, 280 | { 281 | name: "Fiesch", 282 | lat: 46.40940, 283 | lon: 8.13688, 284 | ele: 1057, 285 | paddle: "7", 286 | signboard: true, 287 | }, 288 | { 289 | name: "Dent d’Oche", 290 | lat: 46.352357, 291 | lon: 6.731626, 292 | ele: 2079, 293 | paddle: "8", 294 | pass: "NW", 295 | }, 296 | { 297 | name: "Mont Blanc", 298 | lat: 45.830359, 299 | lon: 6.867674, 300 | ele: 4714, 301 | paddle: "9", 302 | pass: "SW", 303 | }, 304 | { 305 | name: "Piz Palü", 306 | lat: 46.378200, 307 | lon: 9.958730, 308 | ele: 3901, 309 | paddle: "A", 310 | radius: 3500, 311 | }, 312 | { 313 | name: "Kronplatz", 314 | lat: 46.737598, 315 | lon: 11.954900, 316 | ele: 2258, 317 | paddle: "B", 318 | signboard: true, 319 | }, 320 | { 321 | name: "Schmittenhöhe", 322 | lat: 47.328744, 323 | lon: 12.737518, 324 | ele: 1950, 325 | paddle: "C", 326 | signboard: true, 327 | }, 328 | { 329 | name: "Zell am See", 330 | lat: 47.325290, 331 | lon: 12.801694, 332 | ele: 751, 333 | paddle: "stop", 334 | }, 335 | }, 336 | }, 337 | "eigertour-2019-challenge": { 338 | name: "Eigertour 2019 Challenge", 339 | snippet: "https://eigertour.rocks/ Created by twpayne@gmail.com", 340 | turnpoints: []turnpoint{ 341 | { 342 | name: "Eigerplatz", 343 | lat: dms(46, 37, 25.4), 344 | lon: dms(8, 2, 6.7), 345 | paddle: "ylw-stars", 346 | }, 347 | { 348 | name: "First", 349 | lat: dms(46, 39, 31.5), 350 | lon: dms(8, 3, 15.4), 351 | paddle: "1", 352 | }, 353 | { 354 | name: "Tierberglihütte", 355 | lat: 46.702018, 356 | lon: 8.41421, 357 | paddle: "2", 358 | }, 359 | { 360 | name: "Lobhornhütte", 361 | lat: lobhornhutteNW.Lat, 362 | lon: lobhornhutteNW.Lon, 363 | paddle: "3", 364 | }, 365 | { 366 | name: "Niesen", 367 | lat: 46.644999, 368 | lon: 7.651387, 369 | paddle: "4", 370 | }, 371 | { 372 | name: "Doldehornhütte", 373 | lat: 46.486806, 374 | lon: 7.697366, 375 | paddle: "5", 376 | }, 377 | { 378 | name: "Schmadrihütte", 379 | lat: 46.499159, 380 | lon: 7.892225, 381 | paddle: "6", 382 | }, 383 | { 384 | name: "Berghaus Bäregg", 385 | lat: berghausBareggSE.Lat, 386 | lon: berghausBareggSE.Lon, 387 | paddle: "7", 388 | }, 389 | { 390 | name: "Glecksteinhütte", 391 | lat: 46.625129, 392 | lon: 8.096503, 393 | paddle: "8", 394 | }, 395 | { 396 | name: "Lobhornhütte", 397 | lat: lobhornhutteSE.Lat, 398 | lon: lobhornhutteSE.Lon, 399 | paddle: "9", 400 | }, 401 | { 402 | name: "Berghaus Bäregg", 403 | lat: berghausBareggNW.Lat, 404 | lon: berghausBareggNW.Lon, 405 | paddle: "10", 406 | }, 407 | { 408 | name: "Eigerplatz", 409 | lat: dms(46, 37, 25.4), 410 | lon: dms(8, 2, 6.7), 411 | }, 412 | }, 413 | }, 414 | "red-bull-x-alps-2019": { 415 | name: "Red Bull X-Alps 2019", 416 | snippet: "https://www.redbullxalps.com/ Created by twpayne@gmail.com", 417 | turnpoints: []turnpoint{ 418 | { 419 | name: "Salzburg", 420 | lat: 47.79885, 421 | lon: 13.0484, 422 | paddle: "go", 423 | }, 424 | { 425 | name: "Gaisberg", 426 | lat: 47.804133, 427 | lon: 13.110917, 428 | paddle: "1", 429 | signboard: true, 430 | }, 431 | { 432 | name: "Wagrain-Kleinarl", 433 | lat: 47.332295, 434 | lon: 13.305787, 435 | paddle: "2", 436 | signboard: true, 437 | }, 438 | { 439 | name: "Aschau-Chiemsee", 440 | lat: 47.784362, 441 | lon: 12.33277, 442 | paddle: "3", 443 | signboard: true, 444 | }, 445 | { 446 | name: "Kronplatz", 447 | lat: 46.737598, 448 | lon: 11.9549, 449 | paddle: "4", 450 | signboard: true, 451 | }, 452 | { 453 | name: "Zugspitz", 454 | lat: 47.4211, 455 | lon: 10.98526, 456 | pass: "N", 457 | }, 458 | { 459 | name: "Lermoos-Tiroler Zugspitz Arena", 460 | lat: 47.401283, 461 | lon: 10.879767, 462 | paddle: "5", 463 | signboard: true, 464 | }, 465 | { 466 | name: "Davos", 467 | lat: 46.815225, 468 | lon: 9.851879, 469 | paddle: "6", 470 | signboard: true, 471 | }, 472 | { 473 | name: "Titlis", 474 | lat: 46.770918, 475 | lon: 8.424457, 476 | paddle: "7", 477 | signboard: true, 478 | }, 479 | { 480 | name: "Eiger", 481 | lat: 46.577621, 482 | lon: 8.005393, 483 | paddle: "8", 484 | radius: 1500, 485 | }, 486 | { 487 | name: "Mont Blanc", 488 | lat: 45.830359, 489 | lon: 6.867674, 490 | paddle: "9", 491 | pass: "N", 492 | offRoute: true, 493 | }, 494 | { 495 | name: "St. Hilare", 496 | lat: 45.306816, 497 | lon: 5.887857, 498 | paddle: "10", 499 | signboard: true, 500 | }, 501 | { 502 | name: "Monte Viso", 503 | lat: 44.667312, 504 | lon: 7.090381, 505 | paddle: "A", 506 | radius: 2250, 507 | }, 508 | { 509 | name: "Cheval Blanc", 510 | lat: 44.120985, 511 | lon: 6.422229, 512 | paddle: "B", 513 | pass: "W", 514 | }, 515 | { 516 | name: "Peille", 517 | lat: 43.755956, 518 | lon: 7.410751, 519 | paddle: "stop", 520 | signboard: true, 521 | }, 522 | { 523 | name: "Monaco", 524 | lat: 43.75875, 525 | lon: 7.454787, 526 | paddle: "ylw-stars", 527 | }, 528 | }, 529 | }, 530 | "x-pyr-2016": { 531 | name: "X-Pyr 2016", 532 | snippet: "http://www.x-pyr.com/", 533 | turnpoints: []turnpoint{ 534 | { 535 | name: "Start: Hondarribia", 536 | lat: 43.378709, 537 | lon: -1.796020, 538 | paddle: "go", 539 | notes: "Beginning of timed section.", 540 | }, 541 | { 542 | name: "TP1: Larun", 543 | lat: 43.309111, 544 | lon: -1.635409, 545 | paddle: "1", 546 | notes: "Pilots must walk across the gate on the summit. If it is flyable, the organisation will mark the take off.", 547 | }, 548 | { 549 | name: "TP2: Orhi", 550 | lat: 42.988113, 551 | lon: -1.005943, 552 | radius: 100, 553 | paddle: "2", 554 | }, 555 | { 556 | name: "TP3: Anayet", 557 | lat: 42.781424, 558 | lon: -0.455415, 559 | radius: 400, 560 | paddle: "3", 561 | }, 562 | { 563 | name: "TP4: Peña Montañesa", 564 | lat: 42.490226, 565 | lon: 0.199227, 566 | radius: 1000, 567 | paddle: "4", 568 | }, 569 | { 570 | name: "TP5: Ceciré", 571 | lat: 42.757425, 572 | lon: 0.537662, 573 | radius: 400, 574 | paddle: "5", 575 | }, 576 | { 577 | name: "TP6: Berguedà", 578 | lat: 42.248419, 579 | lon: 1.885515, 580 | paddle: "6", 581 | notes: "Pilots must walk across the gate on the waypoint.", 582 | }, 583 | { 584 | name: "TP7: Canigó", 585 | lat: 42.519159, 586 | lon: 2.456149, 587 | radius: 3000, 588 | paddle: "7", 589 | }, 590 | { 591 | name: "TP8: Santa Helena de Rodes", 592 | lat: 42.326468, 593 | lon: 3.16018, 594 | paddle: "stop", 595 | notes: "End of timed section. Pilots must cross a signposted area on foot.", 596 | }, 597 | { 598 | name: "Finish: El Port de la Selva", 599 | lat: 42.336152, 600 | lon: 3.201039, 601 | paddle: "ylw-stars", 602 | }, 603 | }, 604 | }, 605 | "x-pyr-2018": { 606 | name: "X-Pyr 2018", 607 | snippet: "http://www.x-pyr.com/", 608 | turnpoints: []turnpoint{ 609 | { 610 | name: "Hondarribia", 611 | lat: 43.379469, 612 | lon: -1.796731, 613 | paddle: "go", 614 | }, 615 | { 616 | name: "La Rhune", 617 | lat: 43.309039, 618 | lon: -1.635419, 619 | paddle: "1", 620 | }, 621 | { 622 | name: "Orhi", 623 | lat: 42.988111, 624 | lon: -1.005939, 625 | paddle: "2", 626 | }, 627 | { 628 | name: "Midi d-Ossau", 629 | lat: 42.843250, 630 | lon: -0.438069, 631 | paddle: "3", 632 | }, 633 | { 634 | name: "Turbon", 635 | lat: 42.416931, 636 | lon: 0.505181, 637 | paddle: "4", 638 | }, 639 | { 640 | name: "Midi de Bigorre", 641 | lat: 42.937019, 642 | lon: 0.140761, 643 | paddle: "5", 644 | }, 645 | { 646 | name: "Pedraforca", 647 | lat: 42.239869, 648 | lon: 1.702950, 649 | paddle: "6", 650 | }, 651 | { 652 | name: "Canigo", 653 | lat: 42.519161, 654 | lon: 2.456150, 655 | paddle: "7", 656 | }, 657 | { 658 | name: "Santa Helena de Rodes", 659 | lat: 42.326469, 660 | lon: 3.160181, 661 | paddle: "stop", 662 | }, 663 | { 664 | name: "El Port de la Selva", 665 | lat: 42.336150, 666 | lon: 3.201039, 667 | paddle: "ylw-stars", 668 | }, 669 | }, 670 | }, 671 | } 672 | 673 | func (tp turnpoint) desc() string { 674 | switch { 675 | case tp.signboard: 676 | return "signboard" 677 | case tp.notes != "": 678 | return tp.notes 679 | case tp.pass != "": 680 | return fmt.Sprintf("pass %s", tp.pass) 681 | case tp.radius != 0: 682 | return fmt.Sprintf("%dm radius", tp.radius) 683 | default: 684 | return "" 685 | } 686 | } 687 | 688 | func (tp turnpoint) kmlFolder() kml.Element { 689 | center := kml.Coordinate{Lon: tp.lon, Lat: tp.lat} 690 | var radiusPlacemark kml.Element 691 | if tp.radius != 0 { 692 | radiusPlacemark = kml.Placemark( 693 | kml.LineString( 694 | kml.Coordinates(sphere.FAI.Circle(center, float64(tp.radius), 1)...), 695 | kml.Tessellate(true), 696 | ), 697 | kml.Style( 698 | kml.LineStyle( 699 | kml.Color(color.RGBA{R: 0, G: 192, B: 0, A: 192}), 700 | kml.Width(3), 701 | ), 702 | ), 703 | ) 704 | } 705 | var blockPlacemark kml.Element 706 | if blockBearing, ok := blockBearings[tp.pass]; ok { 707 | blockPlacemark = kml.Folder( 708 | kml.Placemark( 709 | kml.LineString( 710 | kml.Coordinates( 711 | center, 712 | sphere.FAI.Offset(center, 25000, float64(blockBearing)), 713 | ), 714 | kml.Tessellate(true), 715 | ), 716 | kml.Style( 717 | kml.LineStyle( 718 | kml.Color(color.RGBA{R: 192, G: 0, B: 0, A: 192}), 719 | kml.Width(3), 720 | ), 721 | ), 722 | ), 723 | ) 724 | } 725 | var snippet kml.Element 726 | if desc := tp.desc(); desc != "" { 727 | snippet = kml.Snippet(desc) 728 | } 729 | var iconStyle kml.Element 730 | switch { 731 | case tp.paddle != "": 732 | iconStyle = icon.PaddleIconStyle(tp.paddle) 733 | default: 734 | iconStyle = kml.IconStyle( 735 | kml.Icon( 736 | kml.Href(icon.NoneHref()), 737 | ), 738 | ) 739 | } 740 | return kml.Folder( 741 | kml.Name(tp.name), 742 | snippet, 743 | kml.Placemark( 744 | kml.Point( 745 | kml.Coordinates(center), 746 | ), 747 | kml.Style( 748 | iconStyle, 749 | ), 750 | ), 751 | radiusPlacemark, 752 | blockPlacemark, 753 | kml.Style( 754 | kml.ListStyle( 755 | kml.ListItemType(kml.ListItemTypeCheckHideChildren), 756 | ), 757 | ), 758 | ) 759 | } 760 | 761 | func (r race) gpx() *gpx.GPX { 762 | var wpts []*gpx.WptType 763 | rte := &gpx.RteType{ 764 | Name: r.name, 765 | Desc: r.snippet, 766 | } 767 | for _, tp := range r.turnpoints { 768 | if tp.offRoute { 769 | continue 770 | } 771 | wpt := &gpx.WptType{ 772 | Lat: tp.lat, 773 | Lon: tp.lon, 774 | Ele: tp.ele, 775 | Name: tp.name, 776 | Desc: tp.desc(), 777 | } 778 | wpts = append(wpts, wpt) 779 | rte.RtePt = append(rte.RtePt, wpt) 780 | } 781 | return &gpx.GPX{ 782 | Version: "1.0", 783 | Creator: "ExpertGPS 1.1 - http://www.topografix.com", 784 | Wpt: wpts, 785 | Rte: []*gpx.RteType{rte}, 786 | } 787 | } 788 | 789 | func (r race) kmlRouteFolder() kml.Element { 790 | var coordinates []kml.Coordinate 791 | for _, tp := range r.turnpoints { 792 | if !tp.offRoute { 793 | coordinates = append(coordinates, kml.Coordinate{Lon: tp.lon, Lat: tp.lat}) 794 | } 795 | } 796 | return kml.Folder( 797 | kml.Name("Route"), 798 | kml.Placemark( 799 | kml.LineString( 800 | kml.Coordinates(coordinates...), 801 | kml.Tessellate(true), 802 | ), 803 | kml.Style( 804 | kml.LineStyle( 805 | kml.Color(color.RGBA{R: 144, G: 144, B: 0, A: 192}), 806 | kml.Width(4), 807 | ), 808 | ), 809 | ), 810 | kml.Style( 811 | kml.ListStyle( 812 | kml.ListItemType(kml.ListItemTypeCheckHideChildren), 813 | ), 814 | ), 815 | ) 816 | } 817 | 818 | func (r race) kmlTurnpointsFolder() kml.Element { 819 | var turnpointFolders []kml.Element 820 | for _, tp := range r.turnpoints { 821 | turnpointFolders = append(turnpointFolders, tp.kmlFolder()) 822 | } 823 | return kml.Folder(append([]kml.Element{ 824 | kml.Name("Turnpoints"), 825 | kml.Open(true), 826 | }, turnpointFolders...)..., 827 | ) 828 | } 829 | 830 | func (r race) kmlDocument() *kml.KMLElement { 831 | return kml.KML( 832 | kml.Document( 833 | kml.Name(fmt.Sprintf("%s Route", r.name)), 834 | kml.Snippet(r.snippet), 835 | kml.Open(true), 836 | r.kmlRouteFolder(), 837 | r.kmlTurnpointsFolder(), 838 | ), 839 | ) 840 | } 841 | 842 | func (r race) flyXCURL() *url.URL { 843 | var coords [][]float64 844 | for _, tp := range r.turnpoints { 845 | coords = append(coords, []float64{tp.lat, tp.lon}) 846 | } 847 | vs := url.Values{} 848 | vs.Set("p", string(polyline.EncodeCoords(coords))) 849 | return &url.URL{ 850 | Scheme: "https", 851 | Host: "flyxc.app", 852 | RawQuery: vs.Encode(), 853 | } 854 | } 855 | 856 | func dms(d, m, s float64) float64 { 857 | return d + m/60 + s/3600 858 | } 859 | 860 | func run() error { 861 | flag.Parse() 862 | r, ok := races[*raceFlag] 863 | if !ok { 864 | return fmt.Errorf("unknown race: %q", *raceFlag) 865 | } 866 | switch *formatFlag { 867 | case "flyxc": 868 | _, err := os.Stdout.WriteString(r.flyXCURL().String() + "\n") 869 | return err 870 | case "gpx": 871 | os.Stdout.WriteString(xml.Header) 872 | return r.gpx().WriteIndent(os.Stdout, "", " ") 873 | case "kml": 874 | return r.kmlDocument().WriteIndent(os.Stdout, "", " ") 875 | default: 876 | return fmt.Errorf("unknown format: %q", *formatFlag) 877 | } 878 | } 879 | 880 | func main() { 881 | if err := run(); err != nil { 882 | log.Fatal(err) 883 | } 884 | } 885 | -------------------------------------------------------------------------------- /examples/route-to-kml/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "image/color" 7 | "os" 8 | 9 | "github.com/twpayne/go-kml/v3" 10 | "github.com/twpayne/go-kml/v3/icon" 11 | "github.com/twpayne/go-waypoint" 12 | ) 13 | 14 | var name = flag.String("name", "Route", "name") 15 | 16 | func readWaypoints(filename string) (waypoint.Collection, error) { 17 | f, err := os.Open(filename) 18 | if err != nil { 19 | return nil, err 20 | } 21 | defer f.Close() 22 | 23 | wc, _, err := waypoint.Read(f) 24 | return wc, err 25 | } 26 | 27 | func run() error { 28 | flag.Parse() 29 | 30 | if flag.NArg() < 1 { 31 | return fmt.Errorf("syntax: %s waypoint-file [waypoints...]", os.Args[0]) 32 | } 33 | 34 | waypoints, err := readWaypoints(flag.Arg(0)) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | waypointsByID := make(map[string]*waypoint.T) 40 | for _, w := range waypoints { 41 | if _, ok := waypointsByID[w.ID]; ok { 42 | return fmt.Errorf("duplicate waypoint ID: %s", w.ID) 43 | } 44 | waypointsByID[w.ID] = w 45 | } 46 | 47 | var turnpoints []*waypoint.T 48 | turnpointIDs := make(map[string]bool) 49 | for _, arg := range flag.Args()[1:] { 50 | turnpoint, ok := waypointsByID[arg] 51 | if !ok { 52 | return fmt.Errorf("unknown waypoint: %s", arg) 53 | } 54 | turnpoints = append(turnpoints, turnpoint) 55 | turnpointIDs[arg] = true 56 | } 57 | 58 | var routeCoordinates []kml.Coordinate 59 | for _, turnpoint := range turnpoints { 60 | coordinate := kml.Coordinate{ 61 | Lon: turnpoint.Longitude, 62 | Lat: turnpoint.Latitude, 63 | Alt: turnpoint.Altitude, 64 | } 65 | routeCoordinates = append(routeCoordinates, coordinate) 66 | } 67 | 68 | routeFolder := kml.Folder( 69 | kml.Name("Route"), 70 | kml.Placemark( 71 | kml.LineString( 72 | kml.Coordinates(routeCoordinates...), 73 | kml.Tessellate(true), 74 | ), 75 | kml.Style( 76 | kml.LineStyle( 77 | kml.Color(color.RGBA{R: 192, G: 0, B: 0, A: 192}), 78 | kml.Width(3), 79 | ), 80 | ), 81 | ), 82 | kml.Style( 83 | kml.ListStyle( 84 | kml.ListItemType(kml.ListItemTypeCheckHideChildren), 85 | ), 86 | ), 87 | ) 88 | 89 | var turnpointFolders []kml.Element 90 | for i := 0; i < len(turnpoints)-1; i++ { 91 | turnpoint := turnpoints[i] 92 | var name string 93 | var paddleID string 94 | if i == 0 { 95 | name = "START" 96 | paddleID = "go" 97 | } else { 98 | name = fmt.Sprintf("TP%02d", i) 99 | paddleID = string([]byte{byte('A' + i - 1)}) 100 | } 101 | turnpointFolder := kml.Folder( 102 | kml.Name(fmt.Sprintf("%s %s", name, turnpoint.Description)), 103 | kml.Placemark( 104 | kml.Point( 105 | kml.Coordinates(kml.Coordinate{ 106 | Lon: turnpoint.Longitude, 107 | Lat: turnpoint.Latitude, 108 | Alt: turnpoint.Altitude, 109 | }), 110 | ), 111 | kml.Style( 112 | kml.IconStyle( 113 | kml.HotSpot(kml.Vec2{X: 0.5, Y: 0, XUnits: kml.UnitsFraction, YUnits: kml.UnitsFraction}), 114 | kml.Icon( 115 | kml.Href(icon.PaddleHref(paddleID)), 116 | ), 117 | kml.Scale(0.5), 118 | ), 119 | ), 120 | ), 121 | kml.Style( 122 | kml.ListStyle( 123 | kml.ListItemType(kml.ListItemTypeCheckHideChildren), 124 | ), 125 | ), 126 | ) 127 | turnpointFolders = append(turnpointFolders, turnpointFolder) 128 | } 129 | 130 | turnpointsFolder := kml.Folder( 131 | append([]kml.Element{ 132 | kml.Name("Turnpoints"), 133 | kml.Open(true), 134 | }, 135 | turnpointFolders..., 136 | )..., 137 | ) 138 | 139 | var waypointFolders []kml.Element 140 | for _, waypoint := range waypoints { 141 | if _, ok := turnpointIDs[waypoint.ID]; ok { 142 | continue 143 | } 144 | waypointFolder := kml.Folder( 145 | kml.Name(waypoint.Description), 146 | kml.Placemark( 147 | kml.Point( 148 | kml.Coordinates(kml.Coordinate{ 149 | Lon: waypoint.Longitude, 150 | Lat: waypoint.Latitude, 151 | Alt: waypoint.Latitude, 152 | }), 153 | ), 154 | kml.Style( 155 | kml.IconStyle( 156 | kml.Icon( 157 | kml.Href( 158 | icon.PaletteHref(2, 13), 159 | ), 160 | ), 161 | kml.Scale(0.5), 162 | ), 163 | ), 164 | ), 165 | ) 166 | waypointFolders = append(waypointFolders, waypointFolder) 167 | } 168 | 169 | waypointsFolder := kml.Folder( 170 | append([]kml.Element{ 171 | kml.Name("Waypoints"), 172 | kml.Open(false), 173 | }, 174 | waypointFolders..., 175 | )..., 176 | ) 177 | 178 | result := kml.KML( 179 | kml.Document( 180 | kml.Name(*name), 181 | kml.Open(true), 182 | routeFolder, 183 | turnpointsFolder, 184 | waypointsFolder, 185 | ), 186 | ) 187 | 188 | return result.WriteIndent(os.Stdout, "", " ") 189 | } 190 | 191 | func main() { 192 | if err := run(); err != nil { 193 | fmt.Println(err) 194 | os.Exit(1) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/twpayne/go-kml/v3 2 | 3 | go 1.24.0 4 | 5 | tool github.com/twpayne/go-kml/v3/internal/generate 6 | 7 | require github.com/alecthomas/assert/v2 v2.10.0 8 | 9 | require ( 10 | github.com/alecthomas/repr v0.4.0 // indirect 11 | github.com/hexops/gotextdiff v1.0.3 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY= 2 | github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 3 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 4 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 5 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 6 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 7 | -------------------------------------------------------------------------------- /icon/icon.go: -------------------------------------------------------------------------------- 1 | // Package icon provides helper functions for standard Google Earth icons. 2 | // See http://kml4earth.appspot.com/icons.html. 3 | package icon 4 | 5 | import ( 6 | "strconv" 7 | 8 | "github.com/twpayne/go-kml/v3" 9 | ) 10 | 11 | // CharacterHref returns the href of the icon wirh the specificed character, or 12 | // an empty string if no such icon exists. See 13 | // http://kml4earth.appspot.com/icons.html#pal3 and 14 | // http://kml4earth.appspot.com/icons.html#pal5. 15 | func CharacterHref(c rune) string { 16 | switch { 17 | case '1' <= c && c <= '9': 18 | return PaletteHref(3, int(c-'1')) 19 | case 'A' <= c && c <= 'Z': 20 | return PaletteHref(5, int((c-'A')%8+16*((31-c+'A')/8))) 21 | default: 22 | return "" 23 | } 24 | } 25 | 26 | // DefaultHref returns the href of the default icon. 27 | func DefaultHref() string { 28 | return PushpinHref("ylw") 29 | } 30 | 31 | // NoneHref returns the icon of the empty icon. 32 | func NoneHref() string { 33 | return PaletteHref(2, 15) 34 | } 35 | 36 | // NumberHref returns the href of the icon with number n. See 37 | // http://kml4earth.appspot.com/icons.html#pal3. 38 | func NumberHref(n int) string { 39 | if 1 <= n && n <= 10 { 40 | return PaletteHref(3, (n-1)%8+16*((n-1)/8)) 41 | } 42 | return "" 43 | } 44 | 45 | // PaddleHref returns the href of the paddle icon with id. See 46 | // http://kml4earth.appspot.com/icons.html#paddle. 47 | func PaddleHref(id string) string { 48 | return "https://maps.google.com/mapfiles/kml/paddle/" + id + ".png" 49 | } 50 | 51 | // PaddleIconStyle returns an IconStyle for the paddle icon with id and the 52 | // hotspot set. See http://kml4earth.appspot.com/icons.html#paddle. 53 | func PaddleIconStyle(id string) *kml.IconStyleElement { 54 | return kml.IconStyle( 55 | kml.HotSpot(kml.Vec2{X: 0.5, Y: 0, XUnits: kml.UnitsFraction, YUnits: kml.UnitsFraction}), 56 | kml.Icon( 57 | kml.Href(PaddleHref(id)), 58 | ), 59 | ) 60 | } 61 | 62 | // PaletteHref returns the href of icon in pal. 63 | func PaletteHref(pal, icon int) string { 64 | return "https://maps.google.com/mapfiles/kml/pal" + strconv.Itoa(pal) + "/icon" + strconv.Itoa(icon) + ".png" 65 | } 66 | 67 | // PushpinHref returns the href of pushpin of color. Valid colors are blue, 68 | // green, ltblu, pink, purple, red, wht, and ylw. See 69 | // http://kml4earth.appspot.com/icons.html#pushpin. 70 | func PushpinHref(color string) string { 71 | return "https://maps.google.com/mapfiles/kml/pushpin/" + color + "-pushpin.png" 72 | } 73 | 74 | // ShapeHref returns the href of the icon with the specified shape. See 75 | // http://kml4earth.appspot.com/icons.html#shapes. 76 | func ShapeHref(shape string) string { 77 | return "http://maps.google.com/mapfiles/kml/shapes/" + shape + ".png" 78 | } 79 | 80 | // TrackHref returns the href of the ith track icon. See 81 | // http://kml4earth.appspot.com/icons.html#kml-icons. 82 | func TrackHref(i int) string { 83 | return "https://earth.google.com/images/kml-icons/track-directional/track-" + strconv.Itoa(i) + ".png" 84 | } 85 | 86 | // TrackNoneHref returns the href of the track icon when there is no heading. 87 | // See http://kml4earth.appspot.com/icons.html#kml-icons. 88 | func TrackNoneHref() string { 89 | return "https://earth.google.com/images/kml-icons/track-directional/track-none.png" 90 | } 91 | -------------------------------------------------------------------------------- /icon/icon_test.go: -------------------------------------------------------------------------------- 1 | package icon_test 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/alecthomas/assert/v2" 8 | 9 | "github.com/twpayne/go-kml/v3/icon" 10 | ) 11 | 12 | func TestHref(t *testing.T) { 13 | for i, tc := range []struct { 14 | actual string 15 | expected string 16 | }{ 17 | { 18 | actual: icon.CharacterHref('9'), 19 | expected: "https://maps.google.com/mapfiles/kml/pal3/icon8.png", 20 | }, 21 | { 22 | actual: icon.CharacterHref('A'), 23 | expected: "https://maps.google.com/mapfiles/kml/pal5/icon48.png", 24 | }, 25 | { 26 | actual: icon.CharacterHref('M'), 27 | expected: "https://maps.google.com/mapfiles/kml/pal5/icon36.png", 28 | }, 29 | { 30 | actual: icon.CharacterHref('Z'), 31 | expected: "https://maps.google.com/mapfiles/kml/pal5/icon1.png", 32 | }, 33 | { 34 | actual: icon.DefaultHref(), 35 | expected: "https://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png", 36 | }, 37 | { 38 | actual: icon.NoneHref(), 39 | expected: "https://maps.google.com/mapfiles/kml/pal2/icon15.png", 40 | }, 41 | { 42 | actual: icon.NumberHref(1), 43 | expected: "https://maps.google.com/mapfiles/kml/pal3/icon0.png", 44 | }, 45 | { 46 | actual: icon.NumberHref(10), 47 | expected: "https://maps.google.com/mapfiles/kml/pal3/icon17.png", 48 | }, 49 | { 50 | actual: icon.PaddleHref("A"), 51 | expected: "https://maps.google.com/mapfiles/kml/paddle/A.png", 52 | }, 53 | { 54 | actual: icon.TrackHref(0), 55 | expected: "https://earth.google.com/images/kml-icons/track-directional/track-0.png", 56 | }, 57 | } { 58 | t.Run(strconv.Itoa(i), func(t *testing.T) { 59 | assert.Equal(t, tc.expected, tc.actual) 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /internal/generate/README.md: -------------------------------------------------------------------------------- 1 | # generate 2 | 3 | To re-generate `xsd.go`, run: 4 | 5 | goxmlstruct -named-types -use-pointers-for-optional-fields=false xsd/*.xsd > internal/generate/xsd.go -------------------------------------------------------------------------------- /internal/generate/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "encoding/xml" 6 | "flag" 7 | "fmt" 8 | "go/format" 9 | "os" 10 | "regexp" 11 | "strings" 12 | "text/template" 13 | "unicode" 14 | ) 15 | 16 | var ( 17 | output = flag.String("o", "/dev/stdout", "output") 18 | namespace = flag.String("n", "", "namespace") 19 | 20 | aOrAnRegexp = regexp.MustCompile(`(?i)\A[aeio]`) 21 | abbreviationsRegexp = regexp.MustCompile(`Fov|Http|Id|Kml|Lod|Url`) 22 | gxEnumTypeRegexp = regexp.MustCompile(`\Agx:(.*Enum)Type\z`) 23 | kmlEnumTypeRegexp = regexp.MustCompile(`\Akml:(.*Enum)Type\z`) 24 | 25 | xsdTypeToGoType = map[string]string{ 26 | "anyURI": "string", 27 | "boolean": "bool", 28 | "double": "float64", 29 | "float": "float64", 30 | "gx:outerWidthType": "float64", 31 | "int": "int", 32 | "integer": "int", 33 | "kml:angle180Type": "float64", 34 | "kml:angle360Type": "float64", 35 | "kml:angle90Type": "float64", 36 | "kml:anglepos180Type": "float64", 37 | "kml:colorType": "color.Color", 38 | "kml:dateTimeType": "time.Time", 39 | "kml:itemIconStateType": "ItemIconStateEnum", 40 | "kml:SchemaDataType": "string", 41 | "kml:SimpleDataType": "string", 42 | "kml:vec2Type": "Vec2", 43 | "string": "string", 44 | } 45 | ) 46 | 47 | //go:embed output.go.tmpl 48 | var outputGoTemplateText string 49 | 50 | func run() error { 51 | flag.Parse() 52 | 53 | file, err := os.Open(flag.Arg(0)) 54 | if err != nil { 55 | return err 56 | } 57 | defer file.Close() 58 | 59 | var schema Schema 60 | if err := xml.NewDecoder(file).Decode(&schema); err != nil { 61 | return err 62 | } 63 | 64 | funcMap := template.FuncMap{ 65 | "aOrAn": func(s string) string { 66 | if aOrAnRegexp.MatchString(s) { 67 | return "an " + s 68 | } 69 | return "a " + s 70 | }, 71 | "hasSuffix": func(suffix, s string) bool { 72 | return strings.HasSuffix(s, suffix) 73 | }, 74 | "nameToGoName": func(name string) string { 75 | return abbreviationsRegexp.ReplaceAllStringFunc(name, strings.ToUpper) 76 | }, 77 | "titleFirst": titleFirst, 78 | "trimSuffix": func(suffix, s string) string { 79 | return strings.TrimSuffix(s, suffix) 80 | }, 81 | "xsdTypeToGoType": func(typeStr string) string { 82 | if goType, ok := xsdTypeToGoType[typeStr]; ok { 83 | return goType 84 | } 85 | if match := kmlEnumTypeRegexp.FindStringSubmatch(typeStr); match != nil { 86 | return titleFirst(match[1]) 87 | } 88 | if match := gxEnumTypeRegexp.FindStringSubmatch(typeStr); match != nil { 89 | return "Gx" + titleFirst(match[1]) 90 | } 91 | return "" 92 | }, 93 | } 94 | 95 | outputGoTemplate, err := template.New("output.go.tmpl"). 96 | Funcs(funcMap). 97 | Parse(outputGoTemplateText) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | source := &strings.Builder{} 103 | if err := outputGoTemplate.Execute(source, struct { 104 | Namespace string 105 | Schema Schema 106 | }{ 107 | Namespace: *namespace, 108 | Schema: schema, 109 | }); err != nil { 110 | return err 111 | } 112 | 113 | formattedSource, err := format.Source([]byte(source.String())) 114 | if err != nil { 115 | formattedSource = []byte(source.String()) 116 | } 117 | 118 | return os.WriteFile(*output, formattedSource, 0o666) 119 | } 120 | 121 | func main() { 122 | if err := run(); err != nil { 123 | fmt.Println(err) 124 | os.Exit(1) 125 | } 126 | } 127 | 128 | func titleFirst(s string) string { 129 | if s == "" { 130 | return "" 131 | } 132 | runes := []rune(s) 133 | runes[0] = unicode.ToTitle(runes[0]) 134 | return string(runes) 135 | } 136 | -------------------------------------------------------------------------------- /internal/generate/output.go.tmpl: -------------------------------------------------------------------------------- 1 | {{- $namespace := .Namespace -}} 2 | {{- $gxPrefix := "" -}} 3 | {{- if eq $namespace "gx:" -}} 4 | {{- $gxPrefix = "Gx" -}} 5 | {{- end -}} 6 | // Code generated by github.com/twpayne/go-kml/v3/internal/generate. DO NOT EDIT. 7 | 8 | package kml 9 | 10 | import ( 11 | "encoding/xml" 12 | "fmt" 13 | "image/color" 14 | "strconv" 15 | "time" 16 | ) 17 | 18 | {{- range .Schema.SimpleType }} 19 | {{- if or (ne .Restriction.Base "string") (or (not (hasSuffix "EnumType" .Name))) }} 20 | {{- continue }} 21 | {{- end }} 22 | {{- $typeName := printf "%s%s" $gxPrefix (.Name | trimSuffix "Type" | titleFirst) }} 23 | 24 | // {{ aOrAn $typeName | titleFirst }} is {{ aOrAn .Name }}. 25 | type {{ $typeName }} string 26 | 27 | func (e {{ $typeName}}) String() string { return string(e) } 28 | 29 | // {{ $typeName }}s. 30 | const ( 31 | {{- if and (eq $namespace "gx:") (eq .Name "altitudeModeEnumType") }} 32 | GxAltitudeModeClampToGround GxAltitudeModeEnum = "clampToGround" 33 | GxAltitudeModeRelativeToGround GxAltitudeModeEnum = "relativeToGround" 34 | GxAltitudeModeAbsolute GxAltitudeModeEnum = "absolute" 35 | {{- end }} 36 | {{- range .Restriction.Enumeration }} 37 | {{ $typeName | trimSuffix "Enum" }}{{ .Value | titleFirst }} {{ $typeName }} = "{{ .Value }}" 38 | {{- end }} 39 | ) 40 | {{- end }} 41 | 42 | {{- range .Schema.Element }} 43 | {{- if or .Abstract (eq .Name "Data" "Scale" "Schema" "SchemaData" "SimpleArrayField" "SimpleData" "SimpleField" "Snippet" "Style" "StyleMap" "angles" "coord" "coordinates" "kml" "linkSnippet" "maxSnippetLines" "option" "snippet" "value") }} 44 | {{- continue }} 45 | {{- else if eq .Name (.Name | titleFirst) "innerBoundaryIs" "outerBoundaryIs" }} 46 | {{- $name := printf "%s%s" $gxPrefix (.Name | nameToGoName | titleFirst ) }} 47 | {{- $elementTypeName := printf "%sElement" $name }} 48 | 49 | // {{ aOrAn $elementTypeName | titleFirst }} is {{ aOrAn .Name }} element. 50 | type {{ $elementTypeName }} struct { 51 | Children []Element 52 | } 53 | 54 | // {{ $name }} returns a new {{ $elementTypeName }}. 55 | func {{ $name }}(children ...Element) *{{ $elementTypeName }} { 56 | return &{{ $elementTypeName }}{ 57 | Children: children, 58 | } 59 | } 60 | 61 | // Add appends children to e and returns e as a ParentElement. 62 | func (e *{{ $elementTypeName }}) Add(children ...Element) ParentElement { 63 | return e.Append(children...) 64 | } 65 | 66 | // Append appends children to e and returns e. 67 | func (e *{{ $elementTypeName }}) Append(children ...Element) *{{ $elementTypeName }} { 68 | e.Children = append(e.Children, children...) 69 | return e 70 | } 71 | 72 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 73 | func (e *{{ $elementTypeName }}) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 74 | startElement := xml.StartElement{Name: xml.Name{Local: "{{ $namespace }}{{ .Name }}"}} 75 | return encodeElementWithChildren(encoder, startElement, e.Children) 76 | } 77 | {{- else }} 78 | 79 | {{- $name := printf "%s%s" $gxPrefix (.Name | nameToGoName | titleFirst ) }} 80 | {{- $elementTypeName := printf "%sElement" $name }} 81 | {{- $valueTypeName := .Type | xsdTypeToGoType }} 82 | {{- if eq .Name "delayedStart" "duration" "maxSessionLength" "minRefreshPeriod" "refreshInterval" "viewRefreshTime" }} 83 | {{- $valueTypeName = "time.Duration" }} 84 | {{- end }} 85 | 86 | // {{ aOrAn $elementTypeName | titleFirst }} is {{ aOrAn .Name }} element. 87 | type {{ $elementTypeName }} struct { 88 | Value {{ $valueTypeName }} 89 | } 90 | 91 | // {{ $name }} returns a new {{ $elementTypeName }}. 92 | func {{ $name }}(value {{ $valueTypeName }}) *{{ $elementTypeName }} { 93 | return &{{ $elementTypeName }}{ 94 | Value: value, 95 | } 96 | } 97 | 98 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 99 | func (e *{{ $elementTypeName }}) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 100 | {{- if eq $valueTypeName "Vec2" }} 101 | startElement := xml.StartElement{ 102 | Name: xml.Name{Local: "{{ $namespace }}{{ .Name }}"}, 103 | Attr: e.Value.attr(), 104 | } 105 | return encodeElement(encoder, startElement) 106 | {{- else }} 107 | startElement := xml.StartElement{Name: xml.Name{Local: "{{ $namespace }}{{ .Name }}"}} 108 | {{- if eq $valueTypeName "bool" }} 109 | var charData xml.CharData 110 | if e.Value { 111 | charData = xml.CharData("1") 112 | } else { 113 | charData = xml.CharData("0") 114 | } 115 | {{- else if eq $valueTypeName "color.Color" }} 116 | red, green, blue, alpha := e.Value.RGBA() 117 | charData := xml.CharData(fmt.Sprintf("%02x%02x%02x%02x", alpha/256, blue/256, green/256, red/256)) 118 | {{- else if eq $valueTypeName "float64" }} 119 | charData := xml.CharData(strconv.FormatFloat(e.Value, 'f', -1, 64)) 120 | {{- else if eq $valueTypeName "int" }} 121 | charData := xml.CharData(strconv.Itoa(e.Value)) 122 | {{- else if eq $valueTypeName "string" }} 123 | charData := xml.CharData(e.Value) 124 | {{- else if eq $valueTypeName "time.Duration" }} 125 | seconds := float64(e.Value) / float64(time.Second) 126 | charData := xml.CharData(strconv.FormatFloat(seconds, 'f', -1, 64)) 127 | {{- else if eq $valueTypeName "time.Time" }} 128 | charData := xml.CharData(e.Value.Format(time.RFC3339)) 129 | {{- else if hasSuffix "Enum" $valueTypeName }} 130 | charData := xml.CharData(e.Value) 131 | {{- end }} 132 | return encodeElementWithCharData(encoder, startElement, charData) 133 | {{- end }} 134 | } 135 | {{- end }} 136 | {{- end }} -------------------------------------------------------------------------------- /internal/generate/xsd.go: -------------------------------------------------------------------------------- 1 | // This file is automatically generated. DO NOT EDIT. 2 | 3 | package main 4 | 5 | type Annotation struct { 6 | Appinfo string `xml:"appinfo"` 7 | Documentation string `xml:"documentation"` 8 | } 9 | 10 | type Any struct { 11 | MaxOccurs string `xml:"maxOccurs,attr"` 12 | MinOccurs bool `xml:"minOccurs,attr"` 13 | Namespace string `xml:"namespace,attr"` 14 | ProcessContents string `xml:"processContents,attr"` 15 | } 16 | 17 | type Attribute struct { 18 | Default string `xml:"default,attr"` 19 | Name string `xml:"name,attr"` 20 | Type string `xml:"type,attr"` 21 | Use string `xml:"use,attr"` 22 | } 23 | 24 | type AttributeGroup struct { 25 | Name string `xml:"name,attr"` 26 | Ref string `xml:"ref,attr"` 27 | Attribute []Attribute `xml:"attribute"` 28 | } 29 | 30 | type Choice struct { 31 | MaxOccurs string `xml:"maxOccurs,attr"` 32 | Annotation Annotation `xml:"annotation"` 33 | Element []Element `xml:"element"` 34 | } 35 | 36 | type ComplexContent struct { 37 | Extension Extension `xml:"extension"` 38 | } 39 | 40 | type ComplexType struct { 41 | Abstract bool `xml:"abstract,attr"` 42 | Final string `xml:"final,attr"` 43 | Name string `xml:"name,attr"` 44 | Annotation Annotation `xml:"annotation"` 45 | Attribute []Attribute `xml:"attribute"` 46 | AttributeGroup AttributeGroup `xml:"attributeGroup"` 47 | ComplexContent ComplexContent `xml:"complexContent"` 48 | Sequence Sequence `xml:"sequence"` 49 | SimpleContent SimpleContent `xml:"simpleContent"` 50 | } 51 | 52 | type Element struct { 53 | Abstract bool `xml:"abstract,attr"` 54 | Default string `xml:"default,attr"` 55 | MaxOccurs string `xml:"maxOccurs,attr"` 56 | MinOccurs bool `xml:"minOccurs,attr"` 57 | Name string `xml:"name,attr"` 58 | Ref string `xml:"ref,attr"` 59 | SubstitutionGroup string `xml:"substitutionGroup,attr"` 60 | Type string `xml:"type,attr"` 61 | Annotation Annotation `xml:"annotation"` 62 | } 63 | 64 | type Enumeration struct { 65 | Value string `xml:"value,attr"` 66 | } 67 | 68 | type Extension struct { 69 | Base string `xml:"base,attr"` 70 | Attribute Attribute `xml:"attribute"` 71 | Sequence Sequence `xml:"sequence"` 72 | } 73 | 74 | type Import struct { 75 | Namespace string `xml:"namespace,attr"` 76 | SchemaLocation string `xml:"schemaLocation,attr"` 77 | } 78 | 79 | type Length struct { 80 | Value int `xml:"value,attr"` 81 | } 82 | 83 | type List struct { 84 | ItemType string `xml:"itemType,attr"` 85 | } 86 | 87 | type MaxInclusive struct { 88 | Value float64 `xml:"value,attr"` 89 | } 90 | 91 | type MinInclusive struct { 92 | Value float64 `xml:"value,attr"` 93 | } 94 | 95 | type Restriction struct { 96 | Base string `xml:"base,attr"` 97 | Enumeration []Enumeration `xml:"enumeration"` 98 | Length Length `xml:"length"` 99 | MaxInclusive MaxInclusive `xml:"maxInclusive"` 100 | MinInclusive MinInclusive `xml:"minInclusive"` 101 | } 102 | 103 | type Schema struct { 104 | Annotation Annotation `xml:"annotation"` 105 | AttributeGroup AttributeGroup `xml:"attributeGroup"` 106 | ComplexType []ComplexType `xml:"complexType"` 107 | Element []Element `xml:"element"` 108 | Import []Import `xml:"import"` 109 | SimpleType []SimpleType `xml:"simpleType"` 110 | } 111 | 112 | type Sequence struct { 113 | Any Any `xml:"any"` 114 | Choice []Choice `xml:"choice"` 115 | Element []Element `xml:"element"` 116 | } 117 | 118 | type SimpleContent struct { 119 | Extension Extension `xml:"extension"` 120 | } 121 | 122 | type SimpleType struct { 123 | Name string `xml:"name,attr"` 124 | Annotation Annotation `xml:"annotation"` 125 | List List `xml:"list"` 126 | Restriction Restriction `xml:"restriction"` 127 | Union Union `xml:"union"` 128 | } 129 | 130 | type Union struct { 131 | MemberTypes string `xml:"memberTypes,attr"` 132 | } 133 | -------------------------------------------------------------------------------- /kml.go: -------------------------------------------------------------------------------- 1 | //go:generate go tool generate -o kml22gx.gen.go -n gx: xsd/kml22gx.xsd 2 | //go:generate go tool generate -o ogckml22.gen.go xsd/ogckml22.xsd 3 | 4 | // Package kml provides convenience methods for creating and writing KML documents. 5 | // 6 | // See https://developers.google.com/kml/. 7 | // 8 | // Goals 9 | // 10 | // - Convenient API for creating both simple and complex KML documents. 11 | // - 1:1 mapping between functions and KML elements. 12 | // 13 | // Non-goals 14 | // 15 | // - Protection against generating invalid documents. 16 | // - Concealment of KML complexity. 17 | // - Fine-grained control over generated XML. 18 | package kml 19 | 20 | import ( 21 | "encoding/xml" 22 | "fmt" 23 | "io" 24 | ) 25 | 26 | const float64StringSize = 16 27 | 28 | type unsupportedTypeError struct { 29 | value any 30 | } 31 | 32 | func (e unsupportedTypeError) Error() string { 33 | return fmt.Sprintf("%T: unsupported type", e.value) 34 | } 35 | 36 | // An Element is a KML element. 37 | type Element interface { 38 | xml.Marshaler 39 | } 40 | 41 | // A ParentElement is a KML element with children. 42 | type ParentElement interface { 43 | Element 44 | Add(children ...Element) ParentElement 45 | } 46 | 47 | // A TopLevelElement is a top level KML element. 48 | type TopLevelElement interface { 49 | Element 50 | Write(w io.Writer) error 51 | WriteIndent(w io.Writer, prefix, indent string) error 52 | } 53 | 54 | func encodeElement(encoder *xml.Encoder, startElement xml.StartElement) error { 55 | if err := encoder.EncodeToken(startElement); err != nil { 56 | return err 57 | } 58 | return encoder.EncodeToken(startElement.End()) 59 | } 60 | 61 | func encodeElementWithCharData(encoder *xml.Encoder, startElement xml.StartElement, charData xml.CharData) error { 62 | if err := encoder.EncodeToken(startElement); err != nil { 63 | return err 64 | } 65 | if charData != nil { 66 | if err := encoder.EncodeToken(charData); err != nil { 67 | return err 68 | } 69 | } 70 | return encoder.EncodeToken(startElement.End()) 71 | } 72 | 73 | func encodeElementWithChild(encoder *xml.Encoder, startElement xml.StartElement, child Element) error { 74 | if err := encoder.EncodeToken(startElement); err != nil { 75 | return err 76 | } 77 | if child != nil { 78 | if err := child.MarshalXML(encoder, xml.StartElement{}); err != nil { 79 | return err 80 | } 81 | } 82 | return encoder.EncodeToken(startElement.End()) 83 | } 84 | 85 | func encodeElementWithChildren(encoder *xml.Encoder, startElement xml.StartElement, children []Element) error { 86 | if err := encoder.EncodeToken(startElement); err != nil { 87 | return err 88 | } 89 | for _, child := range children { 90 | if child == nil { 91 | continue 92 | } 93 | if err := child.MarshalXML(encoder, xml.StartElement{}); err != nil { 94 | return err 95 | } 96 | } 97 | return encoder.EncodeToken(startElement.End()) 98 | } 99 | 100 | func write(w io.Writer, e Element) error { 101 | if _, err := w.Write([]byte(xml.Header)); err != nil { 102 | return err 103 | } 104 | return xml.NewEncoder(w).Encode(e) 105 | } 106 | 107 | func writeIndent(w io.Writer, e Element, prefix, indent string) error { 108 | if _, err := w.Write([]byte(xml.Header)); err != nil { 109 | return err 110 | } 111 | encoder := xml.NewEncoder(w) 112 | encoder.Indent(prefix, indent) 113 | return encoder.Encode(e) 114 | } 115 | -------------------------------------------------------------------------------- /kml22gx.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/twpayne/go-kml/v3/internal/generate. DO NOT EDIT. 2 | 3 | package kml 4 | 5 | import ( 6 | "encoding/xml" 7 | "fmt" 8 | "image/color" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | // A GxAltitudeModeEnum is an altitudeModeEnumType. 14 | type GxAltitudeModeEnum string 15 | 16 | func (e GxAltitudeModeEnum) String() string { return string(e) } 17 | 18 | // GxAltitudeModeEnums. 19 | const ( 20 | GxAltitudeModeClampToGround GxAltitudeModeEnum = "clampToGround" 21 | GxAltitudeModeRelativeToGround GxAltitudeModeEnum = "relativeToGround" 22 | GxAltitudeModeAbsolute GxAltitudeModeEnum = "absolute" 23 | GxAltitudeModeClampToSeaFloor GxAltitudeModeEnum = "clampToSeaFloor" 24 | GxAltitudeModeRelativeToSeaFloor GxAltitudeModeEnum = "relativeToSeaFloor" 25 | ) 26 | 27 | // A GxFlyToModeEnum is a flyToModeEnumType. 28 | type GxFlyToModeEnum string 29 | 30 | func (e GxFlyToModeEnum) String() string { return string(e) } 31 | 32 | // GxFlyToModeEnums. 33 | const ( 34 | GxFlyToModeBounce GxFlyToModeEnum = "bounce" 35 | GxFlyToModeSmooth GxFlyToModeEnum = "smooth" 36 | ) 37 | 38 | // A GxPlayModeEnum is a playModeEnumType. 39 | type GxPlayModeEnum string 40 | 41 | func (e GxPlayModeEnum) String() string { return string(e) } 42 | 43 | // GxPlayModeEnums. 44 | const ( 45 | GxPlayModePause GxPlayModeEnum = "pause" 46 | ) 47 | 48 | // A GxAltitudeModeElement is an altitudeMode element. 49 | type GxAltitudeModeElement struct { 50 | Value GxAltitudeModeEnum 51 | } 52 | 53 | // GxAltitudeMode returns a new GxAltitudeModeElement. 54 | func GxAltitudeMode(value GxAltitudeModeEnum) *GxAltitudeModeElement { 55 | return &GxAltitudeModeElement{ 56 | Value: value, 57 | } 58 | } 59 | 60 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 61 | func (e *GxAltitudeModeElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 62 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:altitudeMode"}} 63 | charData := xml.CharData(e.Value) 64 | return encodeElementWithCharData(encoder, startElement, charData) 65 | } 66 | 67 | // A GxAltitudeOffsetElement is an altitudeOffset element. 68 | type GxAltitudeOffsetElement struct { 69 | Value float64 70 | } 71 | 72 | // GxAltitudeOffset returns a new GxAltitudeOffsetElement. 73 | func GxAltitudeOffset(value float64) *GxAltitudeOffsetElement { 74 | return &GxAltitudeOffsetElement{ 75 | Value: value, 76 | } 77 | } 78 | 79 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 80 | func (e *GxAltitudeOffsetElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 81 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:altitudeOffset"}} 82 | charData := xml.CharData(strconv.FormatFloat(e.Value, 'f', -1, 64)) 83 | return encodeElementWithCharData(encoder, startElement, charData) 84 | } 85 | 86 | // A GxBalloonVisibilityElement is a balloonVisibility element. 87 | type GxBalloonVisibilityElement struct { 88 | Value bool 89 | } 90 | 91 | // GxBalloonVisibility returns a new GxBalloonVisibilityElement. 92 | func GxBalloonVisibility(value bool) *GxBalloonVisibilityElement { 93 | return &GxBalloonVisibilityElement{ 94 | Value: value, 95 | } 96 | } 97 | 98 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 99 | func (e *GxBalloonVisibilityElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 100 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:balloonVisibility"}} 101 | var charData xml.CharData 102 | if e.Value { 103 | charData = xml.CharData("1") 104 | } else { 105 | charData = xml.CharData("0") 106 | } 107 | return encodeElementWithCharData(encoder, startElement, charData) 108 | } 109 | 110 | // A GxDelayedStartElement is a delayedStart element. 111 | type GxDelayedStartElement struct { 112 | Value time.Duration 113 | } 114 | 115 | // GxDelayedStart returns a new GxDelayedStartElement. 116 | func GxDelayedStart(value time.Duration) *GxDelayedStartElement { 117 | return &GxDelayedStartElement{ 118 | Value: value, 119 | } 120 | } 121 | 122 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 123 | func (e *GxDelayedStartElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 124 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:delayedStart"}} 125 | seconds := float64(e.Value) / float64(time.Second) 126 | charData := xml.CharData(strconv.FormatFloat(seconds, 'f', -1, 64)) 127 | return encodeElementWithCharData(encoder, startElement, charData) 128 | } 129 | 130 | // A GxDrawOrderElement is a drawOrder element. 131 | type GxDrawOrderElement struct { 132 | Value int 133 | } 134 | 135 | // GxDrawOrder returns a new GxDrawOrderElement. 136 | func GxDrawOrder(value int) *GxDrawOrderElement { 137 | return &GxDrawOrderElement{ 138 | Value: value, 139 | } 140 | } 141 | 142 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 143 | func (e *GxDrawOrderElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 144 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:drawOrder"}} 145 | charData := xml.CharData(strconv.Itoa(e.Value)) 146 | return encodeElementWithCharData(encoder, startElement, charData) 147 | } 148 | 149 | // A GxDurationElement is a duration element. 150 | type GxDurationElement struct { 151 | Value time.Duration 152 | } 153 | 154 | // GxDuration returns a new GxDurationElement. 155 | func GxDuration(value time.Duration) *GxDurationElement { 156 | return &GxDurationElement{ 157 | Value: value, 158 | } 159 | } 160 | 161 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 162 | func (e *GxDurationElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 163 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:duration"}} 164 | seconds := float64(e.Value) / float64(time.Second) 165 | charData := xml.CharData(strconv.FormatFloat(seconds, 'f', -1, 64)) 166 | return encodeElementWithCharData(encoder, startElement, charData) 167 | } 168 | 169 | // A GxFlyToModeElement is a flyToMode element. 170 | type GxFlyToModeElement struct { 171 | Value GxFlyToModeEnum 172 | } 173 | 174 | // GxFlyToMode returns a new GxFlyToModeElement. 175 | func GxFlyToMode(value GxFlyToModeEnum) *GxFlyToModeElement { 176 | return &GxFlyToModeElement{ 177 | Value: value, 178 | } 179 | } 180 | 181 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 182 | func (e *GxFlyToModeElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 183 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:flyToMode"}} 184 | charData := xml.CharData(e.Value) 185 | return encodeElementWithCharData(encoder, startElement, charData) 186 | } 187 | 188 | // A GxHorizFOVElement is a horizFov element. 189 | type GxHorizFOVElement struct { 190 | Value float64 191 | } 192 | 193 | // GxHorizFOV returns a new GxHorizFOVElement. 194 | func GxHorizFOV(value float64) *GxHorizFOVElement { 195 | return &GxHorizFOVElement{ 196 | Value: value, 197 | } 198 | } 199 | 200 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 201 | func (e *GxHorizFOVElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 202 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:horizFov"}} 203 | charData := xml.CharData(strconv.FormatFloat(e.Value, 'f', -1, 64)) 204 | return encodeElementWithCharData(encoder, startElement, charData) 205 | } 206 | 207 | // A GxInterpolateElement is an interpolate element. 208 | type GxInterpolateElement struct { 209 | Value bool 210 | } 211 | 212 | // GxInterpolate returns a new GxInterpolateElement. 213 | func GxInterpolate(value bool) *GxInterpolateElement { 214 | return &GxInterpolateElement{ 215 | Value: value, 216 | } 217 | } 218 | 219 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 220 | func (e *GxInterpolateElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 221 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:interpolate"}} 222 | var charData xml.CharData 223 | if e.Value { 224 | charData = xml.CharData("1") 225 | } else { 226 | charData = xml.CharData("0") 227 | } 228 | return encodeElementWithCharData(encoder, startElement, charData) 229 | } 230 | 231 | // A GxLabelVisibilityElement is a labelVisibility element. 232 | type GxLabelVisibilityElement struct { 233 | Value bool 234 | } 235 | 236 | // GxLabelVisibility returns a new GxLabelVisibilityElement. 237 | func GxLabelVisibility(value bool) *GxLabelVisibilityElement { 238 | return &GxLabelVisibilityElement{ 239 | Value: value, 240 | } 241 | } 242 | 243 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 244 | func (e *GxLabelVisibilityElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 245 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:labelVisibility"}} 246 | var charData xml.CharData 247 | if e.Value { 248 | charData = xml.CharData("1") 249 | } else { 250 | charData = xml.CharData("0") 251 | } 252 | return encodeElementWithCharData(encoder, startElement, charData) 253 | } 254 | 255 | // A GxOuterColorElement is an outerColor element. 256 | type GxOuterColorElement struct { 257 | Value color.Color 258 | } 259 | 260 | // GxOuterColor returns a new GxOuterColorElement. 261 | func GxOuterColor(value color.Color) *GxOuterColorElement { 262 | return &GxOuterColorElement{ 263 | Value: value, 264 | } 265 | } 266 | 267 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 268 | func (e *GxOuterColorElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 269 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:outerColor"}} 270 | red, green, blue, alpha := e.Value.RGBA() 271 | charData := xml.CharData(fmt.Sprintf("%02x%02x%02x%02x", alpha/256, blue/256, green/256, red/256)) 272 | return encodeElementWithCharData(encoder, startElement, charData) 273 | } 274 | 275 | // A GxOuterWidthElement is an outerWidth element. 276 | type GxOuterWidthElement struct { 277 | Value float64 278 | } 279 | 280 | // GxOuterWidth returns a new GxOuterWidthElement. 281 | func GxOuterWidth(value float64) *GxOuterWidthElement { 282 | return &GxOuterWidthElement{ 283 | Value: value, 284 | } 285 | } 286 | 287 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 288 | func (e *GxOuterWidthElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 289 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:outerWidth"}} 290 | charData := xml.CharData(strconv.FormatFloat(e.Value, 'f', -1, 64)) 291 | return encodeElementWithCharData(encoder, startElement, charData) 292 | } 293 | 294 | // A GxPhysicalWidthElement is a physicalWidth element. 295 | type GxPhysicalWidthElement struct { 296 | Value float64 297 | } 298 | 299 | // GxPhysicalWidth returns a new GxPhysicalWidthElement. 300 | func GxPhysicalWidth(value float64) *GxPhysicalWidthElement { 301 | return &GxPhysicalWidthElement{ 302 | Value: value, 303 | } 304 | } 305 | 306 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 307 | func (e *GxPhysicalWidthElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 308 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:physicalWidth"}} 309 | charData := xml.CharData(strconv.FormatFloat(e.Value, 'f', -1, 64)) 310 | return encodeElementWithCharData(encoder, startElement, charData) 311 | } 312 | 313 | // A GxPlayModeElement is a playMode element. 314 | type GxPlayModeElement struct { 315 | Value GxPlayModeEnum 316 | } 317 | 318 | // GxPlayMode returns a new GxPlayModeElement. 319 | func GxPlayMode(value GxPlayModeEnum) *GxPlayModeElement { 320 | return &GxPlayModeElement{ 321 | Value: value, 322 | } 323 | } 324 | 325 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 326 | func (e *GxPlayModeElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 327 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:playMode"}} 328 | charData := xml.CharData(e.Value) 329 | return encodeElementWithCharData(encoder, startElement, charData) 330 | } 331 | 332 | // A GxRankElement is a rank element. 333 | type GxRankElement struct { 334 | Value float64 335 | } 336 | 337 | // GxRank returns a new GxRankElement. 338 | func GxRank(value float64) *GxRankElement { 339 | return &GxRankElement{ 340 | Value: value, 341 | } 342 | } 343 | 344 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 345 | func (e *GxRankElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 346 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:rank"}} 347 | charData := xml.CharData(strconv.FormatFloat(e.Value, 'f', -1, 64)) 348 | return encodeElementWithCharData(encoder, startElement, charData) 349 | } 350 | 351 | // A GxXElement is a x element. 352 | type GxXElement struct { 353 | Value int 354 | } 355 | 356 | // GxX returns a new GxXElement. 357 | func GxX(value int) *GxXElement { 358 | return &GxXElement{ 359 | Value: value, 360 | } 361 | } 362 | 363 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 364 | func (e *GxXElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 365 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:x"}} 366 | charData := xml.CharData(strconv.Itoa(e.Value)) 367 | return encodeElementWithCharData(encoder, startElement, charData) 368 | } 369 | 370 | // A GxYElement is a y element. 371 | type GxYElement struct { 372 | Value int 373 | } 374 | 375 | // GxY returns a new GxYElement. 376 | func GxY(value int) *GxYElement { 377 | return &GxYElement{ 378 | Value: value, 379 | } 380 | } 381 | 382 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 383 | func (e *GxYElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 384 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:y"}} 385 | charData := xml.CharData(strconv.Itoa(e.Value)) 386 | return encodeElementWithCharData(encoder, startElement, charData) 387 | } 388 | 389 | // A GxWElement is a w element. 390 | type GxWElement struct { 391 | Value int 392 | } 393 | 394 | // GxW returns a new GxWElement. 395 | func GxW(value int) *GxWElement { 396 | return &GxWElement{ 397 | Value: value, 398 | } 399 | } 400 | 401 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 402 | func (e *GxWElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 403 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:w"}} 404 | charData := xml.CharData(strconv.Itoa(e.Value)) 405 | return encodeElementWithCharData(encoder, startElement, charData) 406 | } 407 | 408 | // A GxHElement is a h element. 409 | type GxHElement struct { 410 | Value int 411 | } 412 | 413 | // GxH returns a new GxHElement. 414 | func GxH(value int) *GxHElement { 415 | return &GxHElement{ 416 | Value: value, 417 | } 418 | } 419 | 420 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 421 | func (e *GxHElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 422 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:h"}} 423 | charData := xml.CharData(strconv.Itoa(e.Value)) 424 | return encodeElementWithCharData(encoder, startElement, charData) 425 | } 426 | 427 | // A GxAbstractTourPrimitiveElement is an AbstractTourPrimitive element. 428 | type GxAbstractTourPrimitiveElement struct { 429 | Children []Element 430 | } 431 | 432 | // GxAbstractTourPrimitive returns a new GxAbstractTourPrimitiveElement. 433 | func GxAbstractTourPrimitive(children ...Element) *GxAbstractTourPrimitiveElement { 434 | return &GxAbstractTourPrimitiveElement{ 435 | Children: children, 436 | } 437 | } 438 | 439 | // Add appends children to e and returns e as a ParentElement. 440 | func (e *GxAbstractTourPrimitiveElement) Add(children ...Element) ParentElement { 441 | return e.Append(children...) 442 | } 443 | 444 | // Append appends children to e and returns e. 445 | func (e *GxAbstractTourPrimitiveElement) Append(children ...Element) *GxAbstractTourPrimitiveElement { 446 | e.Children = append(e.Children, children...) 447 | return e 448 | } 449 | 450 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 451 | func (e *GxAbstractTourPrimitiveElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 452 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:AbstractTourPrimitive"}} 453 | return encodeElementWithChildren(encoder, startElement, e.Children) 454 | } 455 | 456 | // A GxAnimatedUpdateElement is an AnimatedUpdate element. 457 | type GxAnimatedUpdateElement struct { 458 | Children []Element 459 | } 460 | 461 | // GxAnimatedUpdate returns a new GxAnimatedUpdateElement. 462 | func GxAnimatedUpdate(children ...Element) *GxAnimatedUpdateElement { 463 | return &GxAnimatedUpdateElement{ 464 | Children: children, 465 | } 466 | } 467 | 468 | // Add appends children to e and returns e as a ParentElement. 469 | func (e *GxAnimatedUpdateElement) Add(children ...Element) ParentElement { 470 | return e.Append(children...) 471 | } 472 | 473 | // Append appends children to e and returns e. 474 | func (e *GxAnimatedUpdateElement) Append(children ...Element) *GxAnimatedUpdateElement { 475 | e.Children = append(e.Children, children...) 476 | return e 477 | } 478 | 479 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 480 | func (e *GxAnimatedUpdateElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 481 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:AnimatedUpdate"}} 482 | return encodeElementWithChildren(encoder, startElement, e.Children) 483 | } 484 | 485 | // A GxFlyToElement is a FlyTo element. 486 | type GxFlyToElement struct { 487 | Children []Element 488 | } 489 | 490 | // GxFlyTo returns a new GxFlyToElement. 491 | func GxFlyTo(children ...Element) *GxFlyToElement { 492 | return &GxFlyToElement{ 493 | Children: children, 494 | } 495 | } 496 | 497 | // Add appends children to e and returns e as a ParentElement. 498 | func (e *GxFlyToElement) Add(children ...Element) ParentElement { 499 | return e.Append(children...) 500 | } 501 | 502 | // Append appends children to e and returns e. 503 | func (e *GxFlyToElement) Append(children ...Element) *GxFlyToElement { 504 | e.Children = append(e.Children, children...) 505 | return e 506 | } 507 | 508 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 509 | func (e *GxFlyToElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 510 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:FlyTo"}} 511 | return encodeElementWithChildren(encoder, startElement, e.Children) 512 | } 513 | 514 | // A GxPlaylistElement is a Playlist element. 515 | type GxPlaylistElement struct { 516 | Children []Element 517 | } 518 | 519 | // GxPlaylist returns a new GxPlaylistElement. 520 | func GxPlaylist(children ...Element) *GxPlaylistElement { 521 | return &GxPlaylistElement{ 522 | Children: children, 523 | } 524 | } 525 | 526 | // Add appends children to e and returns e as a ParentElement. 527 | func (e *GxPlaylistElement) Add(children ...Element) ParentElement { 528 | return e.Append(children...) 529 | } 530 | 531 | // Append appends children to e and returns e. 532 | func (e *GxPlaylistElement) Append(children ...Element) *GxPlaylistElement { 533 | e.Children = append(e.Children, children...) 534 | return e 535 | } 536 | 537 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 538 | func (e *GxPlaylistElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 539 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:Playlist"}} 540 | return encodeElementWithChildren(encoder, startElement, e.Children) 541 | } 542 | 543 | // A GxSoundCueElement is a SoundCue element. 544 | type GxSoundCueElement struct { 545 | Children []Element 546 | } 547 | 548 | // GxSoundCue returns a new GxSoundCueElement. 549 | func GxSoundCue(children ...Element) *GxSoundCueElement { 550 | return &GxSoundCueElement{ 551 | Children: children, 552 | } 553 | } 554 | 555 | // Add appends children to e and returns e as a ParentElement. 556 | func (e *GxSoundCueElement) Add(children ...Element) ParentElement { 557 | return e.Append(children...) 558 | } 559 | 560 | // Append appends children to e and returns e. 561 | func (e *GxSoundCueElement) Append(children ...Element) *GxSoundCueElement { 562 | e.Children = append(e.Children, children...) 563 | return e 564 | } 565 | 566 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 567 | func (e *GxSoundCueElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 568 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:SoundCue"}} 569 | return encodeElementWithChildren(encoder, startElement, e.Children) 570 | } 571 | 572 | // A GxTourElement is a Tour element. 573 | type GxTourElement struct { 574 | Children []Element 575 | } 576 | 577 | // GxTour returns a new GxTourElement. 578 | func GxTour(children ...Element) *GxTourElement { 579 | return &GxTourElement{ 580 | Children: children, 581 | } 582 | } 583 | 584 | // Add appends children to e and returns e as a ParentElement. 585 | func (e *GxTourElement) Add(children ...Element) ParentElement { 586 | return e.Append(children...) 587 | } 588 | 589 | // Append appends children to e and returns e. 590 | func (e *GxTourElement) Append(children ...Element) *GxTourElement { 591 | e.Children = append(e.Children, children...) 592 | return e 593 | } 594 | 595 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 596 | func (e *GxTourElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 597 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:Tour"}} 598 | return encodeElementWithChildren(encoder, startElement, e.Children) 599 | } 600 | 601 | // A GxTimeStampElement is a TimeStamp element. 602 | type GxTimeStampElement struct { 603 | Children []Element 604 | } 605 | 606 | // GxTimeStamp returns a new GxTimeStampElement. 607 | func GxTimeStamp(children ...Element) *GxTimeStampElement { 608 | return &GxTimeStampElement{ 609 | Children: children, 610 | } 611 | } 612 | 613 | // Add appends children to e and returns e as a ParentElement. 614 | func (e *GxTimeStampElement) Add(children ...Element) ParentElement { 615 | return e.Append(children...) 616 | } 617 | 618 | // Append appends children to e and returns e. 619 | func (e *GxTimeStampElement) Append(children ...Element) *GxTimeStampElement { 620 | e.Children = append(e.Children, children...) 621 | return e 622 | } 623 | 624 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 625 | func (e *GxTimeStampElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 626 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:TimeStamp"}} 627 | return encodeElementWithChildren(encoder, startElement, e.Children) 628 | } 629 | 630 | // A GxTimeSpanElement is a TimeSpan element. 631 | type GxTimeSpanElement struct { 632 | Children []Element 633 | } 634 | 635 | // GxTimeSpan returns a new GxTimeSpanElement. 636 | func GxTimeSpan(children ...Element) *GxTimeSpanElement { 637 | return &GxTimeSpanElement{ 638 | Children: children, 639 | } 640 | } 641 | 642 | // Add appends children to e and returns e as a ParentElement. 643 | func (e *GxTimeSpanElement) Add(children ...Element) ParentElement { 644 | return e.Append(children...) 645 | } 646 | 647 | // Append appends children to e and returns e. 648 | func (e *GxTimeSpanElement) Append(children ...Element) *GxTimeSpanElement { 649 | e.Children = append(e.Children, children...) 650 | return e 651 | } 652 | 653 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 654 | func (e *GxTimeSpanElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 655 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:TimeSpan"}} 656 | return encodeElementWithChildren(encoder, startElement, e.Children) 657 | } 658 | 659 | // A GxTourControlElement is a TourControl element. 660 | type GxTourControlElement struct { 661 | Children []Element 662 | } 663 | 664 | // GxTourControl returns a new GxTourControlElement. 665 | func GxTourControl(children ...Element) *GxTourControlElement { 666 | return &GxTourControlElement{ 667 | Children: children, 668 | } 669 | } 670 | 671 | // Add appends children to e and returns e as a ParentElement. 672 | func (e *GxTourControlElement) Add(children ...Element) ParentElement { 673 | return e.Append(children...) 674 | } 675 | 676 | // Append appends children to e and returns e. 677 | func (e *GxTourControlElement) Append(children ...Element) *GxTourControlElement { 678 | e.Children = append(e.Children, children...) 679 | return e 680 | } 681 | 682 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 683 | func (e *GxTourControlElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 684 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:TourControl"}} 685 | return encodeElementWithChildren(encoder, startElement, e.Children) 686 | } 687 | 688 | // A GxWaitElement is a Wait element. 689 | type GxWaitElement struct { 690 | Children []Element 691 | } 692 | 693 | // GxWait returns a new GxWaitElement. 694 | func GxWait(children ...Element) *GxWaitElement { 695 | return &GxWaitElement{ 696 | Children: children, 697 | } 698 | } 699 | 700 | // Add appends children to e and returns e as a ParentElement. 701 | func (e *GxWaitElement) Add(children ...Element) ParentElement { 702 | return e.Append(children...) 703 | } 704 | 705 | // Append appends children to e and returns e. 706 | func (e *GxWaitElement) Append(children ...Element) *GxWaitElement { 707 | e.Children = append(e.Children, children...) 708 | return e 709 | } 710 | 711 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 712 | func (e *GxWaitElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 713 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:Wait"}} 714 | return encodeElementWithChildren(encoder, startElement, e.Children) 715 | } 716 | 717 | // A GxLatLonQuadElement is a LatLonQuad element. 718 | type GxLatLonQuadElement struct { 719 | Children []Element 720 | } 721 | 722 | // GxLatLonQuad returns a new GxLatLonQuadElement. 723 | func GxLatLonQuad(children ...Element) *GxLatLonQuadElement { 724 | return &GxLatLonQuadElement{ 725 | Children: children, 726 | } 727 | } 728 | 729 | // Add appends children to e and returns e as a ParentElement. 730 | func (e *GxLatLonQuadElement) Add(children ...Element) ParentElement { 731 | return e.Append(children...) 732 | } 733 | 734 | // Append appends children to e and returns e. 735 | func (e *GxLatLonQuadElement) Append(children ...Element) *GxLatLonQuadElement { 736 | e.Children = append(e.Children, children...) 737 | return e 738 | } 739 | 740 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 741 | func (e *GxLatLonQuadElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 742 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:LatLonQuad"}} 743 | return encodeElementWithChildren(encoder, startElement, e.Children) 744 | } 745 | 746 | // A GxTrackElement is a Track element. 747 | type GxTrackElement struct { 748 | Children []Element 749 | } 750 | 751 | // GxTrack returns a new GxTrackElement. 752 | func GxTrack(children ...Element) *GxTrackElement { 753 | return &GxTrackElement{ 754 | Children: children, 755 | } 756 | } 757 | 758 | // Add appends children to e and returns e as a ParentElement. 759 | func (e *GxTrackElement) Add(children ...Element) ParentElement { 760 | return e.Append(children...) 761 | } 762 | 763 | // Append appends children to e and returns e. 764 | func (e *GxTrackElement) Append(children ...Element) *GxTrackElement { 765 | e.Children = append(e.Children, children...) 766 | return e 767 | } 768 | 769 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 770 | func (e *GxTrackElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 771 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:Track"}} 772 | return encodeElementWithChildren(encoder, startElement, e.Children) 773 | } 774 | 775 | // A GxMultiTrackElement is a MultiTrack element. 776 | type GxMultiTrackElement struct { 777 | Children []Element 778 | } 779 | 780 | // GxMultiTrack returns a new GxMultiTrackElement. 781 | func GxMultiTrack(children ...Element) *GxMultiTrackElement { 782 | return &GxMultiTrackElement{ 783 | Children: children, 784 | } 785 | } 786 | 787 | // Add appends children to e and returns e as a ParentElement. 788 | func (e *GxMultiTrackElement) Add(children ...Element) ParentElement { 789 | return e.Append(children...) 790 | } 791 | 792 | // Append appends children to e and returns e. 793 | func (e *GxMultiTrackElement) Append(children ...Element) *GxMultiTrackElement { 794 | e.Children = append(e.Children, children...) 795 | return e 796 | } 797 | 798 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 799 | func (e *GxMultiTrackElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 800 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:MultiTrack"}} 801 | return encodeElementWithChildren(encoder, startElement, e.Children) 802 | } 803 | 804 | // A GxSimpleArrayDataElement is a SimpleArrayData element. 805 | type GxSimpleArrayDataElement struct { 806 | Children []Element 807 | } 808 | 809 | // GxSimpleArrayData returns a new GxSimpleArrayDataElement. 810 | func GxSimpleArrayData(children ...Element) *GxSimpleArrayDataElement { 811 | return &GxSimpleArrayDataElement{ 812 | Children: children, 813 | } 814 | } 815 | 816 | // Add appends children to e and returns e as a ParentElement. 817 | func (e *GxSimpleArrayDataElement) Add(children ...Element) ParentElement { 818 | return e.Append(children...) 819 | } 820 | 821 | // Append appends children to e and returns e. 822 | func (e *GxSimpleArrayDataElement) Append(children ...Element) *GxSimpleArrayDataElement { 823 | e.Children = append(e.Children, children...) 824 | return e 825 | } 826 | 827 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 828 | func (e *GxSimpleArrayDataElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 829 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:SimpleArrayData"}} 830 | return encodeElementWithChildren(encoder, startElement, e.Children) 831 | } 832 | 833 | // A GxViewerOptionsElement is a ViewerOptions element. 834 | type GxViewerOptionsElement struct { 835 | Children []Element 836 | } 837 | 838 | // GxViewerOptions returns a new GxViewerOptionsElement. 839 | func GxViewerOptions(children ...Element) *GxViewerOptionsElement { 840 | return &GxViewerOptionsElement{ 841 | Children: children, 842 | } 843 | } 844 | 845 | // Add appends children to e and returns e as a ParentElement. 846 | func (e *GxViewerOptionsElement) Add(children ...Element) ParentElement { 847 | return e.Append(children...) 848 | } 849 | 850 | // Append appends children to e and returns e. 851 | func (e *GxViewerOptionsElement) Append(children ...Element) *GxViewerOptionsElement { 852 | e.Children = append(e.Children, children...) 853 | return e 854 | } 855 | 856 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 857 | func (e *GxViewerOptionsElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 858 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:ViewerOptions"}} 859 | return encodeElementWithChildren(encoder, startElement, e.Children) 860 | } 861 | -------------------------------------------------------------------------------- /kml22gx.go: -------------------------------------------------------------------------------- 1 | package kml 2 | 3 | import ( 4 | "encoding/xml" 5 | "io" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // GxNamespace is the default namespace for Google Earth extensions. 11 | const GxNamespace = "http://www.google.com/kml/ext/2.2" 12 | 13 | // A GxOptionName is a gx:option name. 14 | type GxOptionName string 15 | 16 | func (e GxOptionName) String() string { return string(e) } 17 | 18 | // GxOptionNames. 19 | const ( 20 | GxOptionNameHistoricalImagery GxOptionName = "historicalimagery" 21 | GxOptionNameStreetView GxOptionName = "streetview" 22 | GxOptionNameSunlight GxOptionName = "sunlight" 23 | ) 24 | 25 | // A GxAnglesElement is a gx:angles element. 26 | type GxAnglesElement struct { 27 | Heading float64 28 | Tilt float64 29 | Roll float64 30 | } 31 | 32 | // GxAngles returns a new GxAnglesElement. 33 | func GxAngles(heading, tilt, roll float64) *GxAnglesElement { 34 | return &GxAnglesElement{ 35 | Heading: heading, 36 | Tilt: tilt, 37 | Roll: roll, 38 | } 39 | } 40 | 41 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 42 | func (e *GxAnglesElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 43 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:angles"}} 44 | var builder strings.Builder 45 | builder.Grow(3 * float64StringSize) 46 | builder.WriteString(strconv.FormatFloat(e.Heading, 'f', -1, 64)) 47 | builder.WriteByte(' ') 48 | builder.WriteString(strconv.FormatFloat(e.Tilt, 'f', -1, 64)) 49 | builder.WriteByte(' ') 50 | builder.WriteString(strconv.FormatFloat(e.Roll, 'f', -1, 64)) 51 | charData := xml.CharData(builder.String()) 52 | return encodeElementWithCharData(encoder, startElement, charData) 53 | } 54 | 55 | // A GxCoordElement is a gx:coord element. 56 | type GxCoordElement Coordinate 57 | 58 | // GxCoord returns a new GxCoordElement. 59 | func GxCoord(coordinate Coordinate) GxCoordElement { 60 | return GxCoordElement(coordinate) 61 | } 62 | 63 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 64 | func (e GxCoordElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 65 | startElement := xml.StartElement{Name: xml.Name{Local: "gx:coord"}} 66 | var builder strings.Builder 67 | builder.Grow(3 * float64StringSize) 68 | builder.WriteString(strconv.FormatFloat(e.Lon, 'f', -1, 64)) 69 | builder.WriteByte(' ') 70 | builder.WriteString(strconv.FormatFloat(e.Lat, 'f', -1, 64)) 71 | builder.WriteByte(' ') 72 | builder.WriteString(strconv.FormatFloat(e.Alt, 'f', -1, 64)) 73 | charData := xml.CharData(builder.String()) 74 | return encodeElementWithCharData(encoder, startElement, charData) 75 | } 76 | 77 | // A GxKMLElement is a kml element with gx: extensions. 78 | type GxKMLElement struct { 79 | Child Element 80 | } 81 | 82 | // GxKML returns a new GxKMLElement. 83 | func GxKML(child Element) *GxKMLElement { 84 | return &GxKMLElement{ 85 | Child: child, 86 | } 87 | } 88 | 89 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 90 | func (e *GxKMLElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 91 | startElement := xml.StartElement{ 92 | Name: xml.Name{Space: Namespace, Local: "kml"}, 93 | Attr: []xml.Attr{ 94 | { 95 | Name: xml.Name{Local: "xmlns:gx"}, 96 | Value: GxNamespace, 97 | }, 98 | }, 99 | } 100 | return encodeElementWithChild(encoder, startElement, e.Child) 101 | } 102 | 103 | // Write writes e to w. 104 | func (e *GxKMLElement) Write(w io.Writer) error { 105 | return write(w, e) 106 | } 107 | 108 | // WriteIndent writes e to w with the given prefix and indent. 109 | func (e *GxKMLElement) WriteIndent(w io.Writer, prefix, indent string) error { 110 | return writeIndent(w, e, prefix, indent) 111 | } 112 | 113 | // A GxOptionElement is a gx:option element. 114 | type GxOptionElement struct { 115 | Name GxOptionName 116 | Enabled bool 117 | } 118 | 119 | // GxOption returns a new gx:option element. 120 | func GxOption(name GxOptionName, enabled bool) *GxOptionElement { 121 | return &GxOptionElement{ 122 | Name: name, 123 | Enabled: enabled, 124 | } 125 | } 126 | 127 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 128 | func (e *GxOptionElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 129 | startElement := xml.StartElement{ 130 | Name: xml.Name{Local: "gx:option"}, 131 | Attr: []xml.Attr{ 132 | {Name: xml.Name{Local: "name"}, Value: string(e.Name)}, 133 | {Name: xml.Name{Local: "enabled"}, Value: strconv.FormatBool(e.Enabled)}, 134 | }, 135 | } 136 | return encodeElement(encoder, startElement) 137 | } 138 | 139 | // A GxSimpleArrayFieldElement is a gx:SimpleArrayField element. 140 | type GxSimpleArrayFieldElement struct { 141 | Name string 142 | Type string 143 | Children []Element 144 | } 145 | 146 | // GxSimpleArrayField returns a new GxSimpleArrayFieldElement. 147 | func GxSimpleArrayField(name, _type string, children ...Element) *GxSimpleArrayFieldElement { 148 | return &GxSimpleArrayFieldElement{ 149 | Name: name, 150 | Type: _type, 151 | Children: children, 152 | } 153 | } 154 | 155 | // Add appends children to e and returns e as a ParentElement. 156 | func (e *GxSimpleArrayFieldElement) Add(children ...Element) ParentElement { 157 | return e.Append(children...) 158 | } 159 | 160 | // Append appends children to e and returns e. 161 | func (e *GxSimpleArrayFieldElement) Append(children ...Element) *GxSimpleArrayFieldElement { 162 | e.Children = append(e.Children, children...) 163 | return e 164 | } 165 | 166 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 167 | func (e *GxSimpleArrayFieldElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 168 | startElement := xml.StartElement{ 169 | Name: xml.Name{Local: "gx:SimpleArrayField"}, 170 | Attr: []xml.Attr{ 171 | {Name: xml.Name{Local: "name"}, Value: e.Name}, 172 | {Name: xml.Name{Local: "type"}, Value: e.Type}, 173 | }, 174 | } 175 | return encodeElementWithChildren(encoder, startElement, e.Children) 176 | } 177 | -------------------------------------------------------------------------------- /kml_test.go: -------------------------------------------------------------------------------- 1 | package kml_test 2 | 3 | import ( 4 | "encoding/xml" 5 | "image/color" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/alecthomas/assert/v2" 11 | 12 | kml "github.com/twpayne/go-kml/v3" 13 | ) 14 | 15 | var ( 16 | _ kml.TopLevelElement = &kml.GxKMLElement{} 17 | _ kml.TopLevelElement = &kml.KMLElement{} 18 | ) 19 | 20 | func TestSimpleElements(t *testing.T) { 21 | for _, tc := range []struct { 22 | name string 23 | element kml.Element 24 | expected string 25 | expectedErr string 26 | }{ 27 | { 28 | name: "altitude", 29 | element: kml.Altitude(0), 30 | expected: `0`, 31 | }, 32 | { 33 | name: "altitudeMode", 34 | element: kml.AltitudeMode(kml.AltitudeModeAbsolute), 35 | expected: `absolute`, 36 | }, 37 | { 38 | name: "begin", 39 | element: kml.Begin(time.Date(1876, 8, 1, 0, 0, 0, 0, time.UTC)), 40 | expected: `1876-08-01T00:00:00Z`, 41 | }, 42 | { 43 | name: "bgColor", 44 | element: kml.BgColor(color.Black), 45 | expected: `ff000000`, 46 | }, 47 | { 48 | name: "color", 49 | element: kml.Color(color.White), 50 | expected: `ffffffff`, 51 | }, 52 | { 53 | name: "coordinates", 54 | element: kml.Coordinates(kml.Coordinate{Lon: 1.23, Lat: 4.56, Alt: 7.89}), 55 | expected: `1.23,4.56,7.89`, 56 | }, 57 | { 58 | name: "coordinatesFlat0", 59 | element: kml.CoordinatesFlat([]float64{1.23, 4.56, 7.89, 0.12}, 0, 4, 2, 2), 60 | expected: `1.23,4.56 7.89,0.12`, 61 | }, 62 | { 63 | name: "coordinatesFlat1", 64 | element: kml.CoordinatesFlat([]float64{1.23, 4.56, 0, 7.89, 0.12, 0}, 0, 6, 3, 3), 65 | expected: `1.23,4.56 7.89,0.12`, 66 | }, 67 | { 68 | name: "coordinatesFlat2", 69 | element: kml.CoordinatesFlat([]float64{1.23, 4.56, 7.89, 0.12, 3.45, 6.78}, 0, 6, 3, 3), 70 | expected: `1.23,4.56,7.89 0.12,3.45,6.78`, 71 | }, 72 | { 73 | name: "coordinatesSlice0", 74 | element: kml.CoordinatesSlice([]float64{1.23, 4.56}), 75 | expected: `1.23,4.56`, 76 | }, 77 | { 78 | name: "coordinatesSlice1", 79 | element: kml.CoordinatesSlice([]float64{1.23, 4.56, 7.89}), 80 | expected: `1.23,4.56,7.89`, 81 | }, 82 | { 83 | name: "coordinatesSlice2", 84 | element: kml.CoordinatesSlice([][]float64{{1.23, 4.56}, {7.89, 0.12}}...), 85 | expected: `1.23,4.56 7.89,0.12`, 86 | }, 87 | { 88 | name: "description", 89 | element: kml.Description("text"), 90 | expected: `text`, 91 | }, 92 | { 93 | name: "end", 94 | element: kml.End(time.Date(2015, 12, 31, 23, 59, 59, 0, time.UTC)), 95 | expected: `2015-12-31T23:59:59Z`, 96 | }, 97 | { 98 | name: "extrude", 99 | element: kml.Extrude(false), 100 | expected: `0`, 101 | }, 102 | { 103 | name: "Folder", 104 | element: kml.Folder(), 105 | expected: ``, 106 | }, 107 | { 108 | name: "gx:angles", 109 | element: kml.GxAngles(1.23, 4.56, 7.89), 110 | expected: `1.23 4.56 7.89`, 111 | }, 112 | { 113 | name: "gx:coord", 114 | element: kml.GxCoord(kml.Coordinate{1.23, 4.56, 7.89}), 115 | expected: `1.23 4.56 7.89`, 116 | }, 117 | { 118 | name: "gx:option", 119 | element: kml.GxOption(kml.GxOptionNameStreetView, true), 120 | expected: ``, 121 | }, 122 | { 123 | name: "heading", 124 | element: kml.Heading(0), 125 | expected: `0`, 126 | }, 127 | { 128 | name: "hotSpot", 129 | element: kml.HotSpot(kml.Vec2{X: 0.5, Y: 0.5, XUnits: kml.UnitsPixels, YUnits: kml.UnitsPixels}), 130 | expected: ``, 131 | }, 132 | { 133 | name: "href", 134 | element: kml.Href("https://www.google.com/"), 135 | expected: `https://www.google.com/`, 136 | }, 137 | { 138 | name: "latitude", 139 | element: kml.Latitude(0), 140 | expected: `0`, 141 | }, 142 | { 143 | name: "linkSnippet0", 144 | element: kml.LinkSnippet("snippet"), 145 | expected: `snippet`, 146 | }, 147 | { 148 | name: "linkSnippet1", 149 | element: kml.LinkSnippet("snippet").WithMaxLines(1), 150 | expected: `snippet`, 151 | }, 152 | { 153 | name: "listItemType", 154 | element: kml.ListItemType(kml.ListItemTypeCheck), 155 | expected: `check`, 156 | }, 157 | { 158 | name: "name", 159 | element: kml.Name("value"), 160 | expected: "value", 161 | }, 162 | { 163 | name: "overlayXY", 164 | element: kml.OverlayXY(kml.Vec2{X: 0, Y: 0, XUnits: kml.UnitsFraction, YUnits: kml.UnitsFraction}), 165 | expected: ``, 166 | }, 167 | { 168 | name: "Snippet", 169 | element: kml.Snippet("snippet").WithMaxLines(1), 170 | expected: `snippet`, 171 | }, 172 | { 173 | name: "value_charData", 174 | element: kml.Value(xml.CharData("<>")), 175 | expected: "<>", 176 | }, 177 | { 178 | name: "value_stringer", 179 | element: kml.Value(kml.AltitudeModeAbsolute), 180 | expected: "absolute", 181 | }, 182 | { 183 | name: "value_byte_slice", 184 | element: kml.Value([]byte("&")), 185 | expected: "&", 186 | }, 187 | { 188 | name: "value_bool", 189 | element: kml.Value(true), 190 | expected: "true", 191 | }, 192 | { 193 | name: "value_complex64", 194 | element: kml.Value(complex64(1 + 2i)), 195 | expected: "(1+2i)", 196 | }, 197 | { 198 | name: "value_complex128", 199 | element: kml.Value(1 + 2i), 200 | expected: "(1+2i)", 201 | }, 202 | { 203 | name: "value_float32", 204 | element: kml.Value(float32(1.25)), 205 | expected: "1.25", 206 | }, 207 | { 208 | name: "value_float64", 209 | element: kml.Value(1.2), 210 | expected: "1.2", 211 | }, 212 | { 213 | name: "value_int", 214 | element: kml.Value(1), 215 | expected: "1", 216 | }, 217 | { 218 | name: "value_int8", 219 | element: kml.Value(int8(-8)), 220 | expected: "-8", 221 | }, 222 | { 223 | name: "value_int16", 224 | element: kml.Value(int16(-16)), 225 | expected: "-16", 226 | }, 227 | { 228 | name: "value_int32", 229 | element: kml.Value(int32(-32)), 230 | expected: "-32", 231 | }, 232 | { 233 | name: "value_int64", 234 | element: kml.Value(int64(-64)), 235 | expected: "-64", 236 | }, 237 | { 238 | name: "value_nil", 239 | element: kml.Value(nil), 240 | expected: "", 241 | }, 242 | { 243 | name: "value_string", 244 | element: kml.Value("<>"), 245 | expected: "<>", 246 | }, 247 | { 248 | name: "value_uint", 249 | element: kml.Value(uint(1)), 250 | expected: "1", 251 | }, 252 | { 253 | name: "value_uint8", 254 | element: kml.Value(uint8(8)), 255 | expected: "8", 256 | }, 257 | { 258 | name: "value_uint16", 259 | element: kml.Value(uint16(16)), 260 | expected: "16", 261 | }, 262 | { 263 | name: "value_uint32", 264 | element: kml.Value(uint32(32)), 265 | expected: "32", 266 | }, 267 | { 268 | name: "value_uint64", 269 | element: kml.Value(uint64(64)), 270 | expected: "64", 271 | }, 272 | { 273 | name: "value_nil", 274 | element: kml.Value(nil), 275 | expected: "", 276 | }, 277 | { 278 | name: "value_unsupported", 279 | element: kml.Value(kml.Value(nil)), 280 | expectedErr: "*kml.ValueElement: unsupported type", 281 | }, 282 | } { 283 | t.Run(tc.name, func(t *testing.T) { 284 | var builder strings.Builder 285 | err := xml.NewEncoder(&builder).Encode(tc.element) 286 | if tc.expectedErr != "" { 287 | assert.Error(t, err) 288 | assert.Equal(t, tc.expectedErr, err.Error()) 289 | } else { 290 | assert.NoError(t, err) 291 | assert.Equal(t, tc.expected, builder.String()) 292 | } 293 | }) 294 | } 295 | } 296 | 297 | func TestParentElements(t *testing.T) { 298 | for _, tc := range []struct { 299 | name string 300 | element kml.ParentElement 301 | expected string 302 | }{ 303 | { 304 | name: "easy_trail", 305 | element: kml.Placemark( 306 | kml.Name("Easy trail"), 307 | kml.ExtendedData( 308 | kml.SchemaData("#TrailHeadTypeId", 309 | kml.SimpleData("TrailHeadName", "Pi in the sky"), 310 | kml.SimpleData("TrailLength", "3.14159"), 311 | kml.SimpleData("ElevationGain", "10"), 312 | ), 313 | ), 314 | kml.Point( 315 | kml.Coordinates(kml.Coordinate{Lon: -122.000, Lat: 37.002}), 316 | ), 317 | ), 318 | expected: `` + 319 | `` + 320 | `Easy trail` + 321 | `` + 322 | `` + 323 | `Pi in the sky` + 324 | `3.14159` + 325 | `10` + 326 | `` + 327 | `` + 328 | `` + 329 | `-122,37.002` + 330 | `` + 331 | ``, 332 | }, 333 | { 334 | name: "simple_crosshairs", 335 | element: kml.ScreenOverlay( 336 | kml.Name("Simple crosshairs"), 337 | kml.Description("This screen overlay uses fractional positioning to put the image in the exact center of the screen"), 338 | kml.Icon( 339 | kml.Href("http://myserver/myimage.jpg"), 340 | ), 341 | kml.OverlayXY(kml.Vec2{X: 0.5, Y: 0.5, XUnits: kml.UnitsFraction, YUnits: kml.UnitsFraction}), 342 | kml.ScreenXY(kml.Vec2{X: 0.5, Y: 0.5, XUnits: kml.UnitsFraction, YUnits: kml.UnitsFraction}), 343 | kml.Rotation(39.37878630116985), 344 | kml.Size(kml.Vec2{X: 0, Y: 0, XUnits: kml.UnitsPixels, YUnits: kml.UnitsPixels}), 345 | ), 346 | expected: `` + 347 | `` + 348 | `Simple crosshairs` + 349 | `This screen overlay uses fractional positioning to put the image in the exact center of the screen` + 350 | `` + 351 | `http://myserver/myimage.jpg` + 352 | `` + 353 | `` + 354 | `` + 355 | `39.37878630116985` + 356 | `` + 357 | ``, 358 | }, 359 | { 360 | name: "extended_data", 361 | element: kml.Placemark( 362 | kml.Name("Club house"), 363 | kml.ExtendedData( 364 | kml.Data("holeNumber", kml.Value(1)), 365 | kml.Data("holeYardage", kml.Value(234)), 366 | kml.Data("holePar", kml.Value(4)), 367 | ), 368 | ), 369 | expected: `` + 370 | `` + 371 | `Club house` + 372 | `` + 373 | `` + 374 | `1` + 375 | `` + 376 | `` + 377 | `234` + 378 | `` + 379 | `` + 380 | `4` + 381 | `` + 382 | `` + 383 | ``, 384 | }, 385 | { 386 | name: "Schema", 387 | element: kml.Schema("schema", 388 | kml.GxSimpleArrayField("heartrate", "int", kml.DisplayName("Heart Rate")), 389 | kml.GxSimpleArrayField("cadence", "int", kml.DisplayName("Cadence")), 390 | kml.GxSimpleArrayField("power", "float", kml.DisplayName("Power")), 391 | ), 392 | expected: `` + 393 | `` + 394 | `` + 395 | `Heart Rate` + 396 | `` + 397 | `` + 398 | `Cadence` + 399 | `` + 400 | `` + 401 | `Power` + 402 | `` + 403 | ``, 404 | }, 405 | { 406 | name: "gx:Wait", 407 | element: kml.GxWait( 408 | kml.GxDuration(2500 * time.Millisecond), 409 | ), 410 | expected: `` + 411 | `` + 412 | `2.5` + 413 | ``, 414 | }, 415 | } { 416 | t.Run(tc.name, func(t *testing.T) { 417 | var builder strings.Builder 418 | encoder := xml.NewEncoder(&builder) 419 | assert.NoError(t, encoder.Encode(tc.element)) 420 | assert.Equal(t, tc.expected, builder.String()) 421 | }) 422 | } 423 | } 424 | 425 | func TestModel(t *testing.T) { 426 | k := kml.KML( 427 | kml.Placemark( 428 | kml.Name("SketchUp Model of Macky Auditorium"), 429 | kml.Description("University of Colorado, Boulder; model created by Noël Nemcik."), 430 | kml.LookAt( 431 | kml.Longitude(-105.2727379358738), 432 | kml.Latitude(40.01000594412381), 433 | kml.Altitude(0), 434 | kml.Range(127.2393107680517), 435 | kml.Tilt(65.74454495876547), 436 | kml.Heading(-27.70337734057933), 437 | ), 438 | kml.Model( 439 | kml.AltitudeMode(kml.AltitudeModeRelativeToGround), 440 | kml.Location( 441 | kml.Longitude(-105.272774533734), 442 | kml.Latitude(40.009993372683), 443 | kml.Altitude(0), 444 | ), 445 | kml.Orientation( 446 | kml.Heading(0), 447 | kml.Tilt(0), 448 | kml.Roll(0), 449 | ), 450 | kml.ModelScale( 451 | kml.X(1), 452 | kml.Y(1), 453 | kml.Z(1), 454 | ), 455 | ), 456 | ), 457 | ) 458 | expected := `` + "\n" + 459 | `` + 460 | `` + 461 | `SketchUp Model of Macky Auditorium` + 462 | `University of Colorado, Boulder; model created by Noël Nemcik.` + 463 | `` + 464 | `-105.2727379358738` + 465 | `40.01000594412381` + 466 | `0` + 467 | `127.2393107680517` + 468 | `65.74454495876547` + 469 | `-27.70337734057933` + 470 | `` + 471 | `` + 472 | `relativeToGround` + 473 | `` + 474 | `-105.272774533734` + 475 | `40.009993372683` + 476 | `0` + 477 | `` + 478 | `` + 479 | `0` + 480 | `0` + 481 | `0` + 482 | `` + 483 | `` + 484 | `1` + 485 | `1` + 486 | `1` + 487 | `` + 488 | `` + 489 | `` + 490 | `` 491 | var builder strings.Builder 492 | assert.NoError(t, k.Write(&builder)) 493 | assert.Equal(t, expected, builder.String()) 494 | } 495 | 496 | func TestSharedStyles(t *testing.T) { 497 | style0 := kml.SharedStyle("0") 498 | highlightPlacemarkStyle := kml.SharedStyle( 499 | "highlightPlacemark", 500 | kml.IconStyle( 501 | kml.Icon( 502 | kml.Href("http://maps.google.com/mapfiles/kml/paddle/red-stars.png"), 503 | ), 504 | ), 505 | ) 506 | normalPlacemarkStyle := kml.SharedStyle( 507 | "normalPlacemark", 508 | kml.IconStyle( 509 | kml.Icon( 510 | kml.Href("http://maps.google.com/mapfiles/kml/paddle/wht-blank.png"), 511 | ), 512 | ), 513 | ) 514 | exampleStyleMap := kml.SharedStyleMap( 515 | "exampleStyleMap", 516 | kml.Pair( 517 | kml.Key(kml.StyleStateNormal), 518 | kml.StyleURL(normalPlacemarkStyle.URL()), 519 | ), 520 | kml.Pair( 521 | kml.Key(kml.StyleStateHighlight), 522 | kml.StyleURL(highlightPlacemarkStyle.URL()), 523 | ), 524 | ) 525 | for _, tc := range []struct { 526 | name string 527 | element kml.Element 528 | expected string 529 | }{ 530 | { 531 | name: "folder", 532 | element: kml.Folder( 533 | style0, 534 | kml.Placemark( 535 | kml.StyleURL(style0.URL()), 536 | ), 537 | ), 538 | expected: `` + 539 | `` + 541 | `` + 542 | `#0` + 543 | `` + 544 | ``, 545 | }, 546 | { 547 | name: "highlighted_icon", 548 | element: kml.KML( 549 | kml.Document( 550 | kml.Name("Highlighted Icon"), 551 | kml.Description("Place your mouse over the icon to see it display the new icon"), 552 | highlightPlacemarkStyle, 553 | normalPlacemarkStyle, 554 | exampleStyleMap, 555 | kml.Placemark( 556 | kml.Name("Roll over this icon"), 557 | kml.StyleURL(exampleStyleMap.URL()), 558 | kml.Point( 559 | kml.Coordinates(kml.Coordinate{Lon: -122.0856545755255, Lat: 37.42243077405461}), 560 | ), 561 | ), 562 | ), 563 | ), 564 | expected: `` + 565 | `` + 566 | `Highlighted Icon` + 567 | `Place your mouse over the icon to see it display the new icon` + 568 | `` + 575 | `` + 582 | `` + 583 | `` + 584 | `normal` + 585 | `#normalPlacemark` + 586 | `` + 587 | `` + 588 | `highlight` + 589 | `#highlightPlacemark` + 590 | `` + 591 | `` + 592 | `` + 593 | `Roll over this icon` + 594 | `#exampleStyleMap` + 595 | `` + 596 | `-122.0856545755255,37.42243077405461` + 597 | `` + 598 | `` + 599 | `` + 600 | ``, 601 | }, 602 | { 603 | name: "trail_head_type", 604 | element: kml.KML( 605 | kml.Document( 606 | kml.NamedSchema("TrailHeadTypeId", "TrailHeadType", 607 | kml.SimpleField("TrailHeadName", "string", 608 | kml.DisplayName("Trail Head Name"), 609 | ), 610 | kml.SimpleField("TrailLength", "double", 611 | kml.DisplayName("The length in miles"), 612 | ), 613 | kml.SimpleField("ElevationGain", "int", 614 | kml.DisplayName("change in altitude"), 615 | ), 616 | ), 617 | ), 618 | ), 619 | expected: `` + 620 | `` + 621 | `` + 622 | `` + 623 | `<b>Trail Head Name</b>` + 624 | `` + 625 | `` + 626 | `<i>The length in miles</i>` + 627 | `` + 628 | `` + 629 | `<i>change in altitude</i>` + 630 | `` + 631 | `` + 632 | `` + 633 | ``, 634 | }, 635 | } { 636 | t.Run(tc.name, func(t *testing.T) { 637 | var builder strings.Builder 638 | encoder := xml.NewEncoder(&builder) 639 | assert.NoError(t, encoder.Encode(tc.element)) 640 | assert.Equal(t, tc.expected, builder.String()) 641 | }) 642 | } 643 | } 644 | 645 | func TestWrite(t *testing.T) { 646 | for _, tc := range []struct { 647 | name string 648 | element kml.TopLevelElement 649 | expected string 650 | }{ 651 | { 652 | name: "placemark", 653 | element: kml.KML(kml.Placemark()), 654 | expected: `` + "\n" + 655 | `` + 656 | `` + 657 | `` + 658 | ``, 659 | }, 660 | { 661 | name: "simple_placemark", 662 | element: kml.KML( 663 | kml.Placemark( 664 | kml.Name("Simple placemark"), 665 | kml.Description("Attached to the ground. Intelligently places itself at the height of the underlying terrain."), 666 | kml.Point( 667 | kml.Coordinates(kml.Coordinate{Lon: -122.0822035425683, Lat: 37.42228990140251}), 668 | ), 669 | ), 670 | ), 671 | expected: `` + "\n" + 672 | `` + 673 | `` + 674 | `Simple placemark` + 675 | `Attached to the ground. Intelligently places itself at the height of the underlying terrain.` + 676 | `` + 677 | `-122.0822035425683,37.42228990140251` + 678 | `` + 679 | `` + 680 | ``, 681 | }, 682 | { 683 | name: "entity_references_example", 684 | element: kml.KML( 685 | kml.Document( 686 | kml.Placemark( 687 | kml.Name("Entity references example"), 688 | kml.Description( 689 | `

Entity references are hard to type!

`+ 690 | `

Text is more readable and `+ 691 | `easier to write when you can avoid using entity `+ 692 | `references.

`, 693 | ), 694 | kml.Point( 695 | kml.Coordinates(kml.Coordinate{Lon: 102.594411, Lat: 14.998518}), 696 | ), 697 | ), 698 | ), 699 | ), 700 | expected: `` + "\n" + 701 | `` + 702 | `` + 703 | `` + 704 | `Entity references example` + 705 | `` + 706 | `<h1>Entity references are hard to type!</h1>` + 707 | `<p><font color="red">Text is ` + 708 | `<i>more readable</i> ` + 709 | `and <b>easier to write</b> ` + 710 | `when you can avoid using entity references.</font></p>` + 711 | `` + 712 | `` + 713 | `102.594411,14.998518` + 714 | `` + 715 | `` + 716 | `` + 717 | ``, 718 | }, 719 | { 720 | name: "ground_overlays", 721 | element: kml.KML( 722 | kml.Folder( 723 | kml.Name("Ground Overlays"), 724 | kml.Description("Examples of ground overlays"), 725 | kml.GroundOverlay( 726 | kml.Name("Large-scale overlay on terrain"), 727 | kml.Description("Overlay shows Mount Etna erupting on July 13th, 2001."), 728 | kml.Icon( 729 | kml.Href("http://developers.google.com/kml/documentation/images/etna.jpg"), 730 | ), 731 | kml.LatLonBox( 732 | kml.North(37.91904192681665), 733 | kml.South(37.46543388598137), 734 | kml.East(15.35832653742206), 735 | kml.West(14.60128369746704), 736 | kml.Rotation(-0.1556640799496235), 737 | ), 738 | ), 739 | ), 740 | ), 741 | expected: `` + "\n" + 742 | `` + 743 | `` + 744 | `Ground Overlays` + 745 | `Examples of ground overlays` + 746 | `` + 747 | `Large-scale overlay on terrain` + 748 | `Overlay shows Mount Etna erupting on July 13th, 2001.` + 749 | `` + 750 | `http://developers.google.com/kml/documentation/images/etna.jpg` + 751 | `` + 752 | `` + 753 | `37.91904192681665` + 754 | `37.46543388598137` + 755 | `15.35832653742206` + 756 | `14.60128369746704` + 757 | `-0.1556640799496235` + 758 | `` + 759 | `` + 760 | `` + 761 | ``, 762 | }, 763 | { 764 | name: "the_pentagon", 765 | element: kml.KML( 766 | kml.Placemark( 767 | kml.Name("The Pentagon"), 768 | kml.Polygon( 769 | kml.Extrude(true), 770 | kml.AltitudeMode(kml.AltitudeModeRelativeToGround), 771 | kml.OuterBoundaryIs( 772 | kml.LinearRing( 773 | kml.Coordinates([]kml.Coordinate{ 774 | {-77.05788457660967, 38.87253259892824, 100}, 775 | {-77.05465973756702, 38.87291016281703, 100}, 776 | {-77.05315536854791, 38.87053267794386, 100}, 777 | {-77.05552622493516, 38.868757801256, 100}, 778 | {-77.05844056290393, 38.86996206506943, 100}, 779 | {-77.05788457660967, 38.87253259892824, 100}, 780 | }...), 781 | ), 782 | ), 783 | kml.InnerBoundaryIs( 784 | kml.LinearRing( 785 | kml.Coordinates([]kml.Coordinate{ 786 | {-77.05668055019126, 38.87154239798456, 100}, 787 | {-77.05542625960818, 38.87167890344077, 100}, 788 | {-77.05485125901024, 38.87076535397792, 100}, 789 | {-77.05577677433152, 38.87008686581446, 100}, 790 | {-77.05691162017543, 38.87054446963351, 100}, 791 | {-77.05668055019126, 38.87154239798456, 100}, 792 | }...), 793 | ), 794 | ), 795 | ), 796 | ), 797 | ), 798 | expected: `` + "\n" + 799 | `` + 800 | `` + 801 | `The Pentagon` + 802 | `` + 803 | `1` + 804 | `relativeToGround` + 805 | `` + 806 | `` + 807 | `` + 808 | `-77.05788457660967,38.87253259892824,100 ` + 809 | `-77.05465973756702,38.87291016281703,100 ` + 810 | `-77.0531553685479,38.87053267794386,100 ` + 811 | `-77.05552622493516,38.868757801256,100 ` + 812 | `-77.05844056290393,38.86996206506943,100 ` + 813 | `-77.05788457660967,38.87253259892824,100` + 814 | `` + 815 | `` + 816 | `` + 817 | `` + 818 | `` + 819 | `` + 820 | `-77.05668055019126,38.87154239798456,100 ` + 821 | `-77.05542625960818,38.87167890344077,100 ` + 822 | `-77.05485125901023,38.87076535397792,100 ` + 823 | `-77.05577677433152,38.87008686581446,100 ` + 824 | `-77.05691162017543,38.87054446963351,100 ` + 825 | `-77.05668055019126,38.87154239798456,100` + 826 | `` + 827 | `` + 828 | `` + 829 | `` + 830 | `` + 831 | ``, 832 | }, 833 | { 834 | name: "gx_placemark", 835 | element: kml.GxKML(kml.Placemark()), 836 | expected: `` + "\n" + 837 | `` + 838 | `` + 839 | `` + 840 | ``, 841 | }, 842 | { 843 | name: "gx_track", 844 | element: kml.GxKML( 845 | kml.Folder( 846 | kml.Placemark( 847 | kml.GxTrack( 848 | kml.When(time.Date(2010, 5, 28, 2, 2, 9, 0, time.UTC)), 849 | kml.When(time.Date(2010, 5, 28, 2, 2, 35, 0, time.UTC)), 850 | kml.When(time.Date(2010, 5, 28, 2, 2, 44, 0, time.UTC)), 851 | kml.When(time.Date(2010, 5, 28, 2, 2, 53, 0, time.UTC)), 852 | kml.When(time.Date(2010, 5, 28, 2, 2, 54, 0, time.UTC)), 853 | kml.When(time.Date(2010, 5, 28, 2, 2, 55, 0, time.UTC)), 854 | kml.When(time.Date(2010, 5, 28, 2, 2, 56, 0, time.UTC)), 855 | kml.GxCoord(kml.Coordinate{-122.207881, 37.371915, 156.000000}), 856 | kml.GxCoord(kml.Coordinate{-122.205712, 37.373288, 152.000000}), 857 | kml.GxCoord(kml.Coordinate{-122.204678, 37.373939, 147.000000}), 858 | kml.GxCoord(kml.Coordinate{-122.203572, 37.374630, 142.199997}), 859 | kml.GxCoord(kml.Coordinate{-122.203451, 37.374706, 141.800003}), 860 | kml.GxCoord(kml.Coordinate{-122.203329, 37.374780, 141.199997}), 861 | kml.GxCoord(kml.Coordinate{-122.203207, 37.374857, 140.199997}), 862 | ), 863 | ), 864 | ), 865 | ), 866 | expected: `` + "\n" + 867 | `` + 868 | `` + 869 | `` + 870 | `` + 871 | `2010-05-28T02:02:09Z` + 872 | `2010-05-28T02:02:35Z` + 873 | `2010-05-28T02:02:44Z` + 874 | `2010-05-28T02:02:53Z` + 875 | `2010-05-28T02:02:54Z` + 876 | `2010-05-28T02:02:55Z` + 877 | `2010-05-28T02:02:56Z` + 878 | `-122.207881 37.371915 156` + 879 | `-122.205712 37.373288 152` + 880 | `-122.204678 37.373939 147` + 881 | `-122.203572 37.37463 142.199997` + 882 | `-122.203451 37.374706 141.800003` + 883 | `-122.203329 37.37478 141.199997` + 884 | `-122.203207 37.374857 140.199997` + 885 | `` + 886 | `` + 887 | `` + 888 | ``, 889 | }, 890 | } { 891 | t.Run(tc.name, func(t *testing.T) { 892 | var builder strings.Builder 893 | assert.NoError(t, tc.element.Write(&builder)) 894 | assert.Equal(t, tc.expected, builder.String()) 895 | }) 896 | } 897 | } 898 | -------------------------------------------------------------------------------- /kmz.go: -------------------------------------------------------------------------------- 1 | package kml 2 | 3 | import ( 4 | "archive/zip" 5 | "fmt" 6 | "io" 7 | "sort" 8 | ) 9 | 10 | // WriteKMZ writes a KMZ file containing files to w. The values of the files map 11 | // can be []bytes, strings, *KMLElements, *GxKMLElements, Elements, or 12 | // io.Readers. 13 | func WriteKMZ(w io.Writer, files map[string]any) error { 14 | names := make([]string, 0, len(files)) 15 | for name := range files { 16 | names = append(names, name) 17 | } 18 | sort.Strings(names) 19 | 20 | zipWriter := zip.NewWriter(w) 21 | for _, filename := range names { 22 | zipFileWriter, err := zipWriter.Create(filename) 23 | if err != nil { 24 | return err 25 | } 26 | switch value := files[filename].(type) { 27 | case []byte: 28 | if _, err := zipFileWriter.Write(value); err != nil { 29 | return err 30 | } 31 | case string: 32 | if _, err := zipFileWriter.Write([]byte(value)); err != nil { 33 | return err 34 | } 35 | case *KMLElement: 36 | if err := value.Write(zipFileWriter); err != nil { 37 | return err 38 | } 39 | case *GxKMLElement: 40 | if err := value.Write(zipFileWriter); err != nil { 41 | return err 42 | } 43 | case Element: 44 | if err := KML(value).Write(zipFileWriter); err != nil { 45 | return err 46 | } 47 | case io.Reader: 48 | if _, err := io.Copy(zipFileWriter, value); err != nil { 49 | return err 50 | } 51 | default: 52 | return fmt.Errorf("%T: unsupported type", value) 53 | } 54 | } 55 | return zipWriter.Close() 56 | } 57 | -------------------------------------------------------------------------------- /kmz_test.go: -------------------------------------------------------------------------------- 1 | package kml_test 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "os" 9 | 10 | "github.com/twpayne/go-kml/v3" 11 | ) 12 | 13 | func ExampleWriteKMZ() { 14 | doc := kml.KML( 15 | kml.Placemark( 16 | kml.Name("Zürich"), 17 | kml.Point( 18 | kml.Coordinates( 19 | kml.Coordinate{Lat: 47.374444, Lon: 8.541111}, 20 | ), 21 | ), 22 | ), 23 | ) 24 | 25 | var buffer bytes.Buffer 26 | buffer.Grow(512) 27 | if err := kml.WriteKMZ(&buffer, map[string]any{ 28 | "doc.kml": doc, 29 | }); err != nil { 30 | panic(err) 31 | } 32 | 33 | zipReader, err := zip.NewReader(bytes.NewReader(buffer.Bytes()), int64(buffer.Len())) 34 | if err != nil { 35 | panic(err) 36 | } 37 | for _, zipFile := range zipReader.File { 38 | fmt.Println(zipFile.Name + ":") 39 | file, err := zipFile.Open() 40 | if err != nil { 41 | panic(err) 42 | } 43 | if _, err := io.Copy(os.Stdout, file); err != nil { //nolint:gosec 44 | panic(err) 45 | } 46 | file.Close() 47 | } 48 | 49 | // Output: 50 | // doc.kml: 51 | // 52 | // Zürich8.541111,47.374444 53 | } 54 | -------------------------------------------------------------------------------- /ogckml22.go: -------------------------------------------------------------------------------- 1 | package kml 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // Namespace is the default namespace. 12 | const Namespace = "http://www.opengis.net/kml/2.2" 13 | 14 | const defaultLinkSnippetMaxLines = 2 15 | 16 | // A Coordinate is a single geographical coordinate. 17 | type Coordinate struct { 18 | Lon float64 // Longitude in degrees. 19 | Lat float64 // Latitude in degrees. 20 | Alt float64 // Altitude in meters. 21 | } 22 | 23 | // CoordinatesElement is a coordinates element composed of Coordinates. 24 | type CoordinatesElement []Coordinate 25 | 26 | // Coordinates returns a new CoordinatesElement. 27 | func Coordinates(value ...Coordinate) CoordinatesElement { 28 | return CoordinatesElement(value) 29 | } 30 | 31 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 32 | func (e CoordinatesElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 33 | startElement := xml.StartElement{Name: xml.Name{Local: "coordinates"}} 34 | var builder strings.Builder 35 | builder.Grow(3 * float64StringSize * len(e)) 36 | for i, c := range e { 37 | if i != 0 { 38 | builder.WriteByte(' ') 39 | } 40 | builder.WriteString(strconv.FormatFloat(c.Lon, 'f', -1, 64)) 41 | builder.WriteByte(',') 42 | builder.WriteString(strconv.FormatFloat(c.Lat, 'f', -1, 64)) 43 | if c.Alt != 0 { 44 | builder.WriteByte(',') 45 | builder.WriteString(strconv.FormatFloat(c.Alt, 'f', -1, 64)) 46 | } 47 | } 48 | charData := xml.CharData(builder.String()) 49 | return encodeElementWithCharData(encoder, startElement, charData) 50 | } 51 | 52 | // CoordinatesFlatElement is a coordinates element composed of flat coordinates. 53 | type CoordinatesFlatElement struct { 54 | FlatCoords []float64 55 | Offset int 56 | End int 57 | Stride int 58 | Dim int 59 | } 60 | 61 | // CoordinatesFlat returns a new Coordinates element from flat coordinates. 62 | func CoordinatesFlat(flatCoords []float64, offset, end, stride, dim int) *CoordinatesFlatElement { 63 | return &CoordinatesFlatElement{ 64 | FlatCoords: flatCoords, 65 | Offset: offset, 66 | End: end, 67 | Stride: stride, 68 | Dim: dim, 69 | } 70 | } 71 | 72 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 73 | func (e *CoordinatesFlatElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 74 | startElement := xml.StartElement{Name: xml.Name{Local: "coordinates"}} 75 | var builder strings.Builder 76 | builder.Grow(3 * float64StringSize * (e.End - e.Offset) / e.Stride) 77 | for i := e.Offset; i < e.End; i += e.Stride { 78 | if i != e.Offset { 79 | builder.WriteByte(' ') 80 | } 81 | builder.WriteString(strconv.FormatFloat(e.FlatCoords[i], 'f', -1, 64)) 82 | builder.WriteByte(',') 83 | builder.WriteString(strconv.FormatFloat(e.FlatCoords[i+1], 'f', -1, 64)) 84 | if e.Dim > 2 && e.FlatCoords[i+2] != 0 { 85 | builder.WriteByte(',') 86 | builder.WriteString(strconv.FormatFloat(e.FlatCoords[i+2], 'f', -1, 64)) 87 | } 88 | } 89 | charData := xml.CharData(builder.String()) 90 | return encodeElementWithCharData(encoder, startElement, charData) 91 | } 92 | 93 | // CoordinatesSliceElement is a coordinates element composed of a slice of []float64s. 94 | type CoordinatesSliceElement [][]float64 95 | 96 | // CoordinatesSlice returns a new CoordinatesArrayElement. 97 | func CoordinatesSlice(value ...[]float64) CoordinatesSliceElement { 98 | return CoordinatesSliceElement(value) 99 | } 100 | 101 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 102 | func (e CoordinatesSliceElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 103 | startElement := xml.StartElement{Name: xml.Name{Local: "coordinates"}} 104 | var builder strings.Builder 105 | builder.Grow(3 * float64StringSize * len(e)) 106 | for i, c := range e { 107 | if i != 0 { 108 | builder.WriteByte(' ') 109 | } 110 | builder.WriteString(strconv.FormatFloat(c[0], 'f', -1, 64)) 111 | builder.WriteByte(',') 112 | builder.WriteString(strconv.FormatFloat(c[1], 'f', -1, 64)) 113 | if len(c) > 2 && c[2] != 0 { 114 | builder.WriteByte(',') 115 | builder.WriteString(strconv.FormatFloat(c[2], 'f', -1, 64)) 116 | } 117 | } 118 | charData := xml.CharData(builder.String()) 119 | return encodeElementWithCharData(encoder, startElement, charData) 120 | } 121 | 122 | // A DataElement is a Data element. 123 | type DataElement struct { 124 | Name string 125 | Children []Element 126 | } 127 | 128 | // Data returns a new DataElement. 129 | func Data(name string, children ...Element) *DataElement { 130 | return &DataElement{ 131 | Name: name, 132 | Children: children, 133 | } 134 | } 135 | 136 | // Add appends children to e and returns e as a ParentElement. 137 | func (e *DataElement) Add(children ...Element) ParentElement { 138 | return e.Append(children...) 139 | } 140 | 141 | // Append appends children to e and returns e. 142 | func (e *DataElement) Append(children ...Element) *DataElement { 143 | e.Children = append(e.Children, children...) 144 | return e 145 | } 146 | 147 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 148 | func (e *DataElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 149 | startElement := xml.StartElement{ 150 | Name: xml.Name{Local: "Data"}, 151 | Attr: []xml.Attr{ 152 | {Name: xml.Name{Local: "name"}, Value: e.Name}, 153 | }, 154 | } 155 | return encodeElementWithChildren(encoder, startElement, e.Children) 156 | } 157 | 158 | // A KMLElement is a kml element. 159 | type KMLElement struct { //nolint:revive 160 | Child Element 161 | } 162 | 163 | // KML returns a new KMLElement. 164 | func KML(child Element) *KMLElement { 165 | return &KMLElement{ 166 | Child: child, 167 | } 168 | } 169 | 170 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 171 | func (e *KMLElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 172 | startElement := xml.StartElement{ 173 | Name: xml.Name{Space: Namespace, Local: "kml"}, 174 | } 175 | return encodeElementWithChild(encoder, startElement, e.Child) 176 | } 177 | 178 | // Write writes e to w. 179 | func (e *KMLElement) Write(w io.Writer) error { 180 | return write(w, e) 181 | } 182 | 183 | // WriteIndent writes e to w with the given prefix and indent. 184 | func (e *KMLElement) WriteIndent(w io.Writer, prefix, indent string) error { 185 | return writeIndent(w, e, prefix, indent) 186 | } 187 | 188 | // A LinkSnippetElement is a LinkSnippet element. 189 | type LinkSnippetElement struct { 190 | MaxLines int 191 | Value string 192 | } 193 | 194 | // LinkSnippet returns a new LinkSnippetElement. 195 | func LinkSnippet(value string) *LinkSnippetElement { 196 | return &LinkSnippetElement{ 197 | MaxLines: defaultLinkSnippetMaxLines, 198 | Value: value, 199 | } 200 | } 201 | 202 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 203 | func (e *LinkSnippetElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 204 | startElement := xml.StartElement{Name: xml.Name{Local: "linkSnippet"}} 205 | if e.MaxLines != defaultLinkSnippetMaxLines { 206 | startElement.Attr = []xml.Attr{ 207 | {Name: xml.Name{Local: "maxLines"}, Value: strconv.Itoa(e.MaxLines)}, 208 | } 209 | } 210 | charData := xml.CharData(e.Value) 211 | return encodeElementWithCharData(encoder, startElement, charData) 212 | } 213 | 214 | // WithMaxLines sets e's maxLines attribute. 215 | func (e *LinkSnippetElement) WithMaxLines(maxLines int) *LinkSnippetElement { 216 | e.MaxLines = maxLines 217 | return e 218 | } 219 | 220 | // A ModelScaleElement is a Scale element. 221 | type ModelScaleElement struct { 222 | Children []Element 223 | } 224 | 225 | // ModelScale returns a new ModelScaleElement. 226 | func ModelScale(children ...Element) *ModelScaleElement { 227 | return &ModelScaleElement{ 228 | Children: children, 229 | } 230 | } 231 | 232 | // Add appends children to e and returns e as a ParentElement. 233 | func (e *ModelScaleElement) Add(children ...Element) ParentElement { 234 | return e.Append(children...) 235 | } 236 | 237 | // Append appends children to e and returns e. 238 | func (e *ModelScaleElement) Append(children ...Element) *ModelScaleElement { 239 | e.Children = append(e.Children, children...) 240 | return e 241 | } 242 | 243 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 244 | func (e *ModelScaleElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 245 | startElement := xml.StartElement{Name: xml.Name{Local: "Scale"}} 246 | return encodeElementWithChildren(encoder, startElement, e.Children) 247 | } 248 | 249 | // A SchemaElement is a Schema element. 250 | type SchemaElement struct { 251 | ID string 252 | Name string 253 | Children []Element 254 | } 255 | 256 | // NamedSchema returns a new SchemaElement with the given name. 257 | func NamedSchema(id, name string, children ...Element) *SchemaElement { 258 | return &SchemaElement{ 259 | ID: id, 260 | Name: name, 261 | Children: children, 262 | } 263 | } 264 | 265 | // Schema returns a new SchemaElement. 266 | func Schema(id string, children ...Element) *SchemaElement { 267 | return &SchemaElement{ 268 | ID: id, 269 | Children: children, 270 | } 271 | } 272 | 273 | // Add appends children to e and returns e as a ParentElement. 274 | func (e *SchemaElement) Add(children ...Element) ParentElement { 275 | return e.Append(children...) 276 | } 277 | 278 | // Append appends children to e and returns e. 279 | func (e *SchemaElement) Append(children ...Element) *SchemaElement { 280 | e.Children = append(e.Children, children...) 281 | return e 282 | } 283 | 284 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 285 | func (e *SchemaElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 286 | startElement := xml.StartElement{ 287 | Name: xml.Name{Local: "Schema"}, 288 | Attr: []xml.Attr{ 289 | {Name: xml.Name{Local: "id"}, Value: e.ID}, 290 | }, 291 | } 292 | if e.Name != "" { 293 | startElement.Attr = append(startElement.Attr, 294 | xml.Attr{Name: xml.Name{Local: "name"}, Value: e.Name}, 295 | ) 296 | } 297 | return encodeElementWithChildren(encoder, startElement, e.Children) 298 | } 299 | 300 | // WithName sets e's name. 301 | func (e *SchemaElement) WithName(name string) *SchemaElement { 302 | e.Name = name 303 | return e 304 | } 305 | 306 | // URL return e's URL. 307 | func (e *SchemaElement) URL() string { 308 | if e.ID == "" { 309 | return "" 310 | } 311 | return "#" + e.ID 312 | } 313 | 314 | // A SchemaDataElement is a SchemaData element. 315 | type SchemaDataElement struct { 316 | SchemaURL string 317 | Children []Element 318 | } 319 | 320 | // SchemaData returns a new SchemaDataElement. 321 | func SchemaData(schemaURL string, children ...Element) *SchemaDataElement { 322 | return &SchemaDataElement{ 323 | SchemaURL: schemaURL, 324 | Children: children, 325 | } 326 | } 327 | 328 | // Add appends children to e and returns e as a ParentElement. 329 | func (e *SchemaDataElement) Add(children ...Element) ParentElement { 330 | return e.Append(children...) 331 | } 332 | 333 | // Append appends children to e and returns e. 334 | func (e *SchemaDataElement) Append(children ...Element) *SchemaDataElement { 335 | e.Children = append(e.Children, children...) 336 | return e 337 | } 338 | 339 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 340 | func (e *SchemaDataElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 341 | startElement := xml.StartElement{ 342 | Name: xml.Name{Local: "SchemaData"}, 343 | Attr: []xml.Attr{ 344 | {Name: xml.Name{Local: "schemaUrl"}, Value: e.SchemaURL}, 345 | }, 346 | } 347 | return encodeElementWithChildren(encoder, startElement, e.Children) 348 | } 349 | 350 | // A SimpleDataElement is a SimpleData element. 351 | type SimpleDataElement struct { 352 | Name string 353 | Value string 354 | } 355 | 356 | // SimpleData returns a new SimpleDataElement. 357 | func SimpleData(name, value string) *SimpleDataElement { 358 | return &SimpleDataElement{ 359 | Name: name, 360 | Value: value, 361 | } 362 | } 363 | 364 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 365 | func (e *SimpleDataElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 366 | startElement := xml.StartElement{ 367 | Name: xml.Name{Local: "SimpleData"}, 368 | Attr: []xml.Attr{ 369 | {Name: xml.Name{Local: "name"}, Value: e.Name}, 370 | }, 371 | } 372 | charData := xml.CharData(e.Value) 373 | return encodeElementWithCharData(encoder, startElement, charData) 374 | } 375 | 376 | // A SimpleFieldElement is a SimpleField element. 377 | type SimpleFieldElement struct { 378 | Name string 379 | Type string 380 | Children []Element 381 | } 382 | 383 | // SimpleField returns a new SimpleFieldElement. 384 | func SimpleField(name, _type string, children ...Element) *SimpleFieldElement { 385 | return &SimpleFieldElement{ 386 | Name: name, 387 | Type: _type, 388 | Children: children, 389 | } 390 | } 391 | 392 | // Add appends children to e and returns e as a ParentElement. 393 | func (e *SimpleFieldElement) Add(children ...Element) ParentElement { 394 | return e.Append(children...) 395 | } 396 | 397 | // Append appends children to e and returns e. 398 | func (e *SimpleFieldElement) Append(children ...Element) *SimpleFieldElement { 399 | e.Children = append(e.Children, children...) 400 | return e 401 | } 402 | 403 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 404 | func (e *SimpleFieldElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 405 | startElement := xml.StartElement{ 406 | Name: xml.Name{Local: "SimpleField"}, 407 | Attr: []xml.Attr{ 408 | {Name: xml.Name{Local: "name"}, Value: e.Name}, 409 | {Name: xml.Name{Local: "type"}, Value: e.Type}, 410 | }, 411 | } 412 | return encodeElementWithChildren(encoder, startElement, e.Children) 413 | } 414 | 415 | // A SnippetElement is a snippet element. 416 | type SnippetElement struct { 417 | MaxLines int 418 | Value string 419 | } 420 | 421 | // Snippet returns a new SnippetElement. 422 | func Snippet(value string) *SnippetElement { 423 | return &SnippetElement{ 424 | MaxLines: defaultLinkSnippetMaxLines, 425 | Value: value, 426 | } 427 | } 428 | 429 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 430 | func (e *SnippetElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 431 | startElement := xml.StartElement{Name: xml.Name{Local: "Snippet"}} 432 | if e.MaxLines != defaultLinkSnippetMaxLines { 433 | startElement.Attr = []xml.Attr{ 434 | {Name: xml.Name{Local: "maxLines"}, Value: strconv.Itoa(e.MaxLines)}, 435 | } 436 | } 437 | charData := xml.CharData(e.Value) 438 | return encodeElementWithCharData(encoder, startElement, charData) 439 | } 440 | 441 | // WithMaxLines sets e's maxLines attribute. 442 | func (e *SnippetElement) WithMaxLines(maxLines int) *SnippetElement { 443 | e.MaxLines = maxLines 444 | return e 445 | } 446 | 447 | // A StyleElement is a Style element. 448 | type StyleElement struct { 449 | ID string 450 | Children []Element 451 | } 452 | 453 | // SharedStyle returns a new StyleElement with the given id. 454 | func SharedStyle(id string, children ...Element) *StyleElement { 455 | return &StyleElement{ 456 | ID: id, 457 | Children: children, 458 | } 459 | } 460 | 461 | // Style returns a new StyleElement. 462 | func Style(children ...Element) *StyleElement { 463 | return &StyleElement{ 464 | Children: children, 465 | } 466 | } 467 | 468 | // Add appends children to e and returns e as a ParentElement. 469 | func (e *StyleElement) Add(children ...Element) ParentElement { 470 | return e.Append(children...) 471 | } 472 | 473 | // Append appends children to e and returns e. 474 | func (e *StyleElement) Append(children ...Element) *StyleElement { 475 | e.Children = append(e.Children, children...) 476 | return e 477 | } 478 | 479 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 480 | func (e *StyleElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 481 | startElement := xml.StartElement{Name: xml.Name{Local: "Style"}} 482 | if e.ID != "" { 483 | startElement.Attr = []xml.Attr{ 484 | {Name: xml.Name{Local: "id"}, Value: e.ID}, 485 | } 486 | } 487 | return encodeElementWithChildren(encoder, startElement, e.Children) 488 | } 489 | 490 | // URL return e's URL. 491 | func (e *StyleElement) URL() string { 492 | if e.ID == "" { 493 | return "" 494 | } 495 | return "#" + e.ID 496 | } 497 | 498 | // WithID sets e's ID. 499 | func (e *StyleElement) WithID(id string) *StyleElement { 500 | e.ID = id 501 | return e 502 | } 503 | 504 | // A StyleMapElement is a StyleMap element. 505 | type StyleMapElement struct { 506 | ID string 507 | Children []Element 508 | } 509 | 510 | // SharedStyleMap returns a new StyleMapElement with the given id. 511 | func SharedStyleMap(id string, children ...Element) *StyleMapElement { 512 | return &StyleMapElement{ 513 | ID: id, 514 | Children: children, 515 | } 516 | } 517 | 518 | // StyleMap returns a new StyleMapElement. 519 | func StyleMap(children ...Element) *StyleMapElement { 520 | return &StyleMapElement{ 521 | Children: children, 522 | } 523 | } 524 | 525 | // Add appends children to e and returns e as a ParentElement. 526 | func (e *StyleMapElement) Add(children ...Element) ParentElement { 527 | return e.Append(children...) 528 | } 529 | 530 | // Append appends children to e and returns e. 531 | func (e *StyleMapElement) Append(children ...Element) *StyleMapElement { 532 | e.Children = append(e.Children, children...) 533 | return e 534 | } 535 | 536 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 537 | func (e *StyleMapElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 538 | startElement := xml.StartElement{Name: xml.Name{Local: "StyleMap"}} 539 | if e.ID != "" { 540 | startElement.Attr = []xml.Attr{ 541 | {Name: xml.Name{Local: "id"}, Value: e.ID}, 542 | } 543 | } 544 | return encodeElementWithChildren(encoder, startElement, e.Children) 545 | } 546 | 547 | // URL return e's URL. 548 | func (e *StyleMapElement) URL() string { 549 | if e.ID == "" { 550 | return "" 551 | } 552 | return "#" + e.ID 553 | } 554 | 555 | // WithID sets e's ID. 556 | func (e *StyleMapElement) WithID(id string) *StyleMapElement { 557 | e.ID = id 558 | return e 559 | } 560 | 561 | // A ValueElement is a value element. 562 | type ValueElement struct { 563 | Value any 564 | } 565 | 566 | // Value returns a new ValueElement. 567 | func Value(value any) *ValueElement { 568 | return &ValueElement{ 569 | Value: value, 570 | } 571 | } 572 | 573 | // MarshalXML implements encoding/xml.Marshaler.MarshalXML. 574 | func (e *ValueElement) MarshalXML(encoder *xml.Encoder, _ xml.StartElement) error { 575 | startElement := xml.StartElement{Name: xml.Name{Local: "value"}} 576 | charData, err := charData(e.Value) 577 | if err != nil { 578 | return err 579 | } 580 | return encodeElementWithCharData(encoder, startElement, charData) 581 | } 582 | 583 | // A Vec2 is a vec2. 584 | type Vec2 struct { 585 | X float64 586 | Y float64 587 | XUnits UnitsEnum 588 | YUnits UnitsEnum 589 | } 590 | 591 | // attr returns a slice of attributes populated with v's values. 592 | func (v *Vec2) attr() []xml.Attr { 593 | return []xml.Attr{ 594 | {Name: xml.Name{Local: "x"}, Value: strconv.FormatFloat(v.X, 'f', -1, 64)}, 595 | {Name: xml.Name{Local: "y"}, Value: strconv.FormatFloat(v.Y, 'f', -1, 64)}, 596 | {Name: xml.Name{Local: "xunits"}, Value: string(v.XUnits)}, 597 | {Name: xml.Name{Local: "yunits"}, Value: string(v.YUnits)}, 598 | } 599 | } 600 | 601 | func charData(value any) (xml.CharData, error) { 602 | switch value := value.(type) { 603 | case nil: 604 | return nil, nil 605 | case xml.CharData: 606 | return value, nil 607 | case fmt.Stringer: 608 | return xml.CharData(value.String()), nil 609 | case []byte: 610 | return xml.CharData(value), nil 611 | case bool: 612 | return xml.CharData(strconv.FormatBool(value)), nil 613 | case complex64: 614 | return xml.CharData(strconv.FormatComplex(complex128(value), 'f', -1, 64)), nil 615 | case complex128: 616 | return xml.CharData(strconv.FormatComplex(value, 'f', -1, 128)), nil 617 | case float32: 618 | return xml.CharData(strconv.FormatFloat(float64(value), 'f', -1, 64)), nil 619 | case float64: 620 | return xml.CharData(strconv.FormatFloat(value, 'f', -1, 64)), nil 621 | case int: 622 | return xml.CharData(strconv.Itoa(value)), nil 623 | case int8: 624 | return xml.CharData(strconv.FormatInt(int64(value), 10)), nil 625 | case int16: 626 | return xml.CharData(strconv.FormatInt(int64(value), 10)), nil 627 | case int32: 628 | return xml.CharData(strconv.FormatInt(int64(value), 10)), nil 629 | case int64: 630 | return xml.CharData(strconv.FormatInt(value, 10)), nil 631 | case string: 632 | return xml.CharData(value), nil 633 | case uint: 634 | return xml.CharData(strconv.FormatUint(uint64(value), 10)), nil 635 | case uint8: 636 | return xml.CharData(strconv.FormatUint(uint64(value), 10)), nil 637 | case uint16: 638 | return xml.CharData(strconv.FormatUint(uint64(value), 10)), nil 639 | case uint32: 640 | return xml.CharData(strconv.FormatUint(uint64(value), 10)), nil 641 | case uint64: 642 | return xml.CharData(strconv.FormatUint(value, 10)), nil 643 | default: 644 | return nil, &unsupportedTypeError{value: value} 645 | } 646 | } 647 | -------------------------------------------------------------------------------- /sphere/circle_test.go: -------------------------------------------------------------------------------- 1 | package sphere_test 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/alecthomas/assert/v2" 9 | 10 | "github.com/twpayne/go-kml/v3" 11 | "github.com/twpayne/go-kml/v3/sphere" 12 | ) 13 | 14 | func TestCircle(t *testing.T) { 15 | for i, tc := range []struct { 16 | center kml.Coordinate 17 | radius float64 18 | maxErr float64 19 | expected []kml.Coordinate 20 | }{ 21 | { 22 | center: kml.Coordinate{Lon: 0, Lat: 0, Alt: 100}, 23 | radius: 1000, 24 | maxErr: 1, 25 | expected: []kml.Coordinate{ 26 | {Lon: 0, Lat: 0.008983152841195214, Alt: 100}, 27 | {Lon: 0.0011258876022698656, Lat: 0.008912317997331125, Alt: 100}, 28 | {Lon: 0.002234019283634706, Lat: 0.008700930573621838, Alt: 100}, 29 | {Lon: 0.0033069191447876595, Lat: 0.008352324276246322, Alt: 100}, 30 | {Lon: 0.004327666913493653, Lat: 0.007871996835109306, Alt: 100}, 31 | {Lon: 0.005280164787461206, Lat: 0.007267523301307612, Alt: 100}, 32 | {Lon: 0.006149391306330964, Lat: 0.0065484365839908, Alt: 100}, 33 | {Lon: 0.006921638249063812, Lat: 0.005726077110629261, Alt: 100}, 34 | {Lon: 0.007584726820717381, Lat: 0.004813413981645677, Alt: 100}, 35 | {Lon: 0.008128199719224507, Lat: 0.003824840439915492, Alt: 100}, 36 | {Lon: 0.00854348605317887, Lat: 0.0027759468807100067, Alt: 100}, 37 | {Lon: 0.008824036509791956, Lat: 0.001683274981853487, Alt: 100}, 38 | {Lon: 0.008965426641358826, Lat: 0.0005640568316080706, Alt: 100}, 39 | {Lon: 0.008965426641358826, Lat: -0.0005640568316080696, Alt: 100}, 40 | {Lon: 0.008824036509791956, Lat: -0.0016832749818534878, Alt: 100}, 41 | {Lon: 0.00854348605317887, Lat: -0.002775946880710006, Alt: 100}, 42 | {Lon: 0.008128199719224507, Lat: -0.003824840439915493, Alt: 100}, 43 | {Lon: 0.007584726820717381, Lat: -0.004813413981645679, Alt: 100}, 44 | {Lon: 0.006921638249063812, Lat: -0.00572607711062926, Alt: 100}, 45 | {Lon: 0.006149391306330961, Lat: -0.006548436583990801, Alt: 100}, 46 | {Lon: 0.005280164787461206, Lat: -0.007267523301307612, Alt: 100}, 47 | {Lon: 0.004327666913493655, Lat: -0.007871996835109304, Alt: 100}, 48 | {Lon: 0.0033069191447876573, Lat: -0.008352324276246324, Alt: 100}, 49 | {Lon: 0.002234019283634706, Lat: -0.008700930573621838, Alt: 100}, 50 | {Lon: 0.001125887602269864, Lat: -0.008912317997331125, Alt: 100}, 51 | {Lon: 1.1001189463363886e-18, Lat: -0.008983152841195214, Alt: 100}, 52 | {Lon: -0.0011258876022698621, Lat: -0.008912317997331125, Alt: 100}, 53 | {Lon: -0.002234019283634708, Lat: -0.008700930573621838, Alt: 100}, 54 | {Lon: -0.003306919144787659, Lat: -0.008352324276246324, Alt: 100}, 55 | {Lon: -0.004327666913493654, Lat: -0.007871996835109304, Alt: 100}, 56 | {Lon: -0.005280164787461205, Lat: -0.007267523301307612, Alt: 100}, 57 | {Lon: -0.00614939130633096, Lat: -0.006548436583990801, Alt: 100}, 58 | {Lon: -0.006921638249063812, Lat: -0.00572607711062926, Alt: 100}, 59 | {Lon: -0.007584726820717378, Lat: -0.004813413981645681, Alt: 100}, 60 | {Lon: -0.00812819971922451, Lat: -0.0038248404399154876, Alt: 100}, 61 | {Lon: -0.00854348605317887, Lat: -0.002775946880710008, Alt: 100}, 62 | {Lon: -0.008824036509791956, Lat: -0.001683274981853488, Alt: 100}, 63 | {Lon: -0.008965426641358826, Lat: -0.0005640568316080777, Alt: 100}, 64 | {Lon: -0.008965426641358826, Lat: 0.0005640568316080745, Alt: 100}, 65 | {Lon: -0.008824036509791954, Lat: 0.0016832749818534924, Alt: 100}, 66 | {Lon: -0.00854348605317887, Lat: 0.002775946880710005, Alt: 100}, 67 | {Lon: -0.008128199719224507, Lat: 0.003824840439915492, Alt: 100}, 68 | {Lon: -0.0075847268207173855, Lat: 0.004813413981645672, Alt: 100}, 69 | {Lon: -0.006921638249063809, Lat: 0.005726077110629263, Alt: 100}, 70 | {Lon: -0.0061493913063309594, Lat: 0.006548436583990802, Alt: 100}, 71 | {Lon: -0.005280164787461206, Lat: 0.007267523301307612, Alt: 100}, 72 | {Lon: -0.004327666913493653, Lat: 0.007871996835109306, Alt: 100}, 73 | {Lon: -0.0033069191447876655, Lat: 0.008352324276246322, Alt: 100}, 74 | {Lon: -0.002234019283634703, Lat: 0.00870093057362184, Alt: 100}, 75 | {Lon: -0.0011258876022698613, Lat: 0.008912317997331125, Alt: 100}, 76 | {Lon: 0, Lat: 0.008983152841195214, Alt: 100}, 77 | }, 78 | }, 79 | { 80 | center: kml.Coordinate{Lon: 13.631333, Lat: 46.438500}, 81 | radius: 50, 82 | maxErr: 1, 83 | expected: []kml.Coordinate{ 84 | {Lon: 13.631333, Lat: 46.43894915764205}, 85 | {Lon: 13.631658888465811, Lat: 46.43888898146551}, 86 | {Lon: 13.631897453677293, Lat: 46.43872457743259}, 87 | {Lon: 13.631984772278717, Lat: 46.438499998148764}, 88 | {Lon: 13.631897449024441, Lat: 46.43827541979054}, 89 | {Lon: 13.63165888381296, Lat: 46.43811101760886}, 90 | {Lon: 13.631333, Lat: 46.43805084235793}, 91 | {Lon: 13.63100711618704, Lat: 46.43811101760886}, 92 | {Lon: 13.630768550975558, Lat: 46.43827541979054}, 93 | {Lon: 13.630681227721285, Lat: 46.438499998148764}, 94 | {Lon: 13.630768546322708, Lat: 46.43872457743259}, 95 | {Lon: 13.63100711153419, Lat: 46.43888898146551}, 96 | {Lon: 13.631333, Lat: 46.43894915764205}, 97 | }, 98 | }, 99 | } { 100 | t.Run(strconv.Itoa(i), func(t *testing.T) { 101 | actual := sphere.WGS84.Circle(tc.center, tc.radius, tc.maxErr) 102 | assert.Equal(t, len(tc.expected), len(actual)) 103 | for i, actualCoordinate := range actual { 104 | assertInDelta(t, tc.expected[i].Lon, actualCoordinate.Lon, 1e-14) 105 | assertInDelta(t, tc.expected[i].Lat, actualCoordinate.Lat, 1e-14) 106 | assert.Equal(t, tc.center.Alt, actualCoordinate.Alt) 107 | assertInDelta(t, tc.radius, sphere.WGS84.HaversineDistance(tc.center, actualCoordinate), 1e-9) 108 | } 109 | for _, expectedCoordinate := range tc.expected { 110 | assertInDelta(t, tc.radius, sphere.WGS84.HaversineDistance(tc.center, expectedCoordinate), 1e-9) 111 | } 112 | }) 113 | } 114 | } 115 | 116 | func assertInDelta(tb testing.TB, expected, actual, delta float64) { 117 | tb.Helper() 118 | if math.Abs(expected-actual) <= delta { 119 | return 120 | } 121 | tb.Fatalf("Expected %f to be within %f of %f", actual, delta, expected) 122 | } 123 | -------------------------------------------------------------------------------- /sphere/example_test.go: -------------------------------------------------------------------------------- 1 | // Disable test on arm64 as it tests generated floating point values exactly and 2 | // arm64 differs from amd64 in the last place of one of the generated values. 3 | 4 | //go:build !arm64 5 | 6 | package sphere_test 7 | 8 | import ( 9 | "log" 10 | "os" 11 | 12 | "github.com/twpayne/go-kml/v3" 13 | "github.com/twpayne/go-kml/v3/sphere" 14 | ) 15 | 16 | func ExampleT_Circle() { 17 | k := kml.KML( 18 | kml.Placemark( 19 | kml.LineString( 20 | kml.Coordinates(sphere.WGS84.Circle(kml.Coordinate{Lon: 7.658320, Lat: 45.97651, Alt: 0}, 5500, 1)...), 21 | ), 22 | ), 23 | ) 24 | if err := k.WriteIndent(os.Stdout, "", " "); err != nil { 25 | log.Fatal(err) 26 | } 27 | // Output: 28 | // 29 | // 30 | // 31 | // 32 | // 7.658320000000001,46.02591734062657 7.662139516168806,46.02584604997079 7.665947990217271,46.02563238410356 7.669734412203183,46.02527696072462 7.6734878364438375,46.024780807333265 7.677197413404282,46.024145358234385 7.680852421296631,46.02337245035928 7.684442297295684,46.02246431791424 7.68795666827733,46.02142358587259 7.691385380987812,46.02025326232997 7.694718531553885,46.01895672974565 7.697946494246017,46.01753773509616 7.701059949409304,46.016000378970816 7.704049910479545,46.01434910364147 7.706907750004893,46.01258868014259 7.70962522459679,46.010724194399664 7.712194498737385,46.00876103244779 7.71460816737436,46.00670486478404 7.7168592772379965,46.004561629900266 7.7189413468194505,46.00233751704516 7.720848384953432,46.00003894826663 7.722574907953002,45.99767255978747 7.72411595524867,45.995245182769374 7.7254671034887545,45.99276382352193 7.726624479062679,45.99023564321468 7.727584769013816,45.987667937151684 7.728345230313356,45.98506811366952 7.728903697471718,45.982443672719974 7.729258588468999,45.97980218420011 7.729408908990993,45.97715126609252 7.729354254962322,45.97449856247933 7.7290948133732424,45.971851721493394 7.728631361401627,45.96921837327061 7.727965263836586,45.9666061079666 7.727098468814982,45.964022453901094 7.726033501886931,45.961474855892696 7.724773458431034,45.958970653845924 7.723321994444677,45.95651706165177 7.72168331573922,45.95412114646181 7.719862165574228,45.95178980839473 7.717863810769134,45.94952976073298 7.715694026334803,45.947347510665324 7.713359078671356,45.94524934063003 7.710865707382437,45.94324129031095 7.708221105759674,45.941329139337306 7.705432899994549,45.93951839073578 7.702509127178112,45.9378142551811 7.69945821215212,45.93622163608952 7.696288943278037,45.934745115596385 7.693010447193027,45.93338894145731 7.689632162624654,45.932157014909166 7.686163813338272,45.93105287952481 7.682615380293243,45.93007971109241 7.678997073086062,45.92924030854741 7.675319300760165,45.92853708598244 7.671592642063786,45.9279720657571 7.667827815238496,45.92754687272685 7.66403564742224,45.927262729606845 7.660227043751559,45.92712045348346 7.656412956248442,45.92712045348346 7.652604352577761,45.927262729606845 7.648812184761505,45.92754687272685 7.645047357936216,45.9279720657571 7.641320699239836,45.92853708598244 7.637642926913938,45.92924030854741 7.634024619706757,45.93007971109241 7.630476186661729,45.93105287952481 7.627007837375347,45.932157014909166 7.623629552806974,45.93338894145731 7.620351056721964,45.934745115596385 7.617181787847882,45.93622163608952 7.61413087282189,45.9378142551811 7.611207100005451,45.93951839073578 7.608418894240327,45.941329139337306 7.605774292617563,45.94324129031095 7.603280921328645,45.94524934063003 7.600945973665198,45.947347510665324 7.598776189230867,45.94952976073298 7.596777834425772,45.95178980839473 7.59495668426078,45.95412114646181 7.593318005555323,45.95651706165177 7.591866541568967,45.958970653845924 7.590606498113069,45.961474855892696 7.589541531185019,45.964022453901094 7.588674736163415,45.9666061079666 7.588008638598374,45.96921837327061 7.587545186626759,45.971851721493394 7.587285745037679,45.97449856247933 7.5872310910090075,45.97715126609252 7.587381411531002,45.97980218420011 7.587736302528283,45.982443672719974 7.5882947696866445,45.98506811366952 7.589055230986185,45.987667937151684 7.590015520937322,45.99023564321468 7.591172896511246,45.99276382352193 7.592524044751331,45.995245182769374 7.594065092046999,45.99767255978747 7.5957916150465685,46.00003894826663 7.597698653180551,46.00233751704516 7.599780722762004,46.004561629900266 7.6020318326256415,46.00670486478404 7.604445501262616,46.00876103244779 7.607014775403211,46.010724194399664 7.6097322499951074,46.01258868014259 7.612590089520457,46.01434910364147 7.6155800505906965,46.016000378970816 7.618693505753984,46.01753773509616 7.621921468446115,46.01895672974565 7.625254619012189,46.02025326232997 7.628683331722671,46.02142358587259 7.632197702704317,46.02246431791424 7.63578757870337,46.02337245035928 7.63944258659572,46.024145358234385 7.643152163556164,46.024780807333265 7.646905587796819,46.02527696072462 7.65069200978273,46.02563238410356 7.654500483831195,46.02584604997079 7.658320000000001,46.02591734062657 33 | // 34 | // 35 | // 36 | } 37 | -------------------------------------------------------------------------------- /sphere/sphere.go: -------------------------------------------------------------------------------- 1 | // Package sphere contains convenience methods for generating coordinates on 2 | // a sphere. All angles are measured in degrees. 3 | package sphere 4 | 5 | import ( 6 | "math" 7 | 8 | "github.com/twpayne/go-kml/v3" 9 | ) 10 | 11 | const ( 12 | degrees = 180 / math.Pi 13 | radians = math.Pi / 180 14 | ) 15 | 16 | // A T is a sphere of radius R. 17 | type T struct { 18 | R float64 19 | } 20 | 21 | var ( 22 | // Unit is the unit sphere. 23 | Unit = T{R: 1} 24 | 25 | // FAI is the FAI sphere, measured in meters. 26 | FAI = T{R: 6371000} 27 | 28 | // WGS84 is a sphere whose radius is equal to the semi-major axis of the 29 | // WGS84 ellipsoid, measured in meters. 30 | WGS84 = T{R: 6378137} 31 | ) 32 | 33 | // Offset returns the coordinate at distance from origin in direction bearing. 34 | func (t T) Offset(origin kml.Coordinate, distance, bearing float64) kml.Coordinate { 35 | lat := math.Asin(math.Sin(origin.Lat*radians)*math.Cos(distance/t.R) + math.Cos(origin.Lat*radians)*math.Sin(distance/t.R)*math.Cos(bearing*radians)) 36 | lon := origin.Lon*radians + math.Atan2(math.Sin(bearing*radians)*math.Sin(distance/t.R)*math.Cos(origin.Lat*radians), math.Cos(distance/t.R)-math.Sin(origin.Lat*radians)*math.Sin(lat)) 37 | return kml.Coordinate{ 38 | Lon: lon * degrees, 39 | Lat: lat * degrees, 40 | Alt: origin.Alt, 41 | } 42 | } 43 | 44 | // Circle returns an array of kml.Coordinates that approximate a circle of the 45 | // given radius centered on center with a maximum error of maxErr. 46 | func (t T) Circle(center kml.Coordinate, radius, maxErr float64) []kml.Coordinate { 47 | numVertices := int(math.Ceil(math.Pi / math.Acos((radius-maxErr)/(radius+maxErr)))) 48 | cs := make([]kml.Coordinate, numVertices+1) 49 | for i := range numVertices { 50 | cs[i] = t.Offset(center, radius, 360*float64(i)/float64(numVertices)) 51 | } 52 | cs[numVertices] = cs[0] 53 | return cs 54 | } 55 | 56 | // HaversineDistance returns the great circle distance between c1 and c2 using 57 | // the Haversine formula. Altitude is ignored. 58 | func (t T) HaversineDistance(c1, c2 kml.Coordinate) float64 { 59 | lat1 := c1.Lat * radians 60 | lat2 := c2.Lat * radians 61 | deltaLat := lat2 - lat1 62 | deltaLon := (c1.Lon - c2.Lon) * radians 63 | a := math.Sin(deltaLat/2)*math.Sin(deltaLat/2) + math.Cos(lat1)*math.Cos(lat2)*math.Sin(deltaLon/2)*math.Sin(deltaLon/2) 64 | c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a)) 65 | return t.R * c 66 | } 67 | 68 | // InitialBearingTo returns the initial bearing from c1 to c2. Altitude is 69 | // ignored. 70 | func (t T) InitialBearingTo(c1, c2 kml.Coordinate) float64 { 71 | lat1 := c1.Lat * radians 72 | lat2 := c2.Lat * radians 73 | deltaLon := (c2.Lon - c1.Lon) * radians 74 | y := math.Sin(deltaLon) * math.Cos(lat2) 75 | x := math.Cos(lat1)*math.Sin(lat2) - math.Sin(lat1)*math.Cos(lat2)*math.Cos(deltaLon) 76 | return math.Atan2(y, x) * degrees 77 | } 78 | -------------------------------------------------------------------------------- /sphere/sphere_test.go: -------------------------------------------------------------------------------- 1 | package sphere_test 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/alecthomas/assert/v2" 8 | 9 | "github.com/twpayne/go-kml/v3" 10 | "github.com/twpayne/go-kml/v3/sphere" 11 | ) 12 | 13 | func TestSphereHaversineDistance(t *testing.T) { 14 | for i, tc := range []struct { 15 | sphere sphere.T 16 | c1 kml.Coordinate 17 | c2 kml.Coordinate 18 | expected float64 19 | delta float64 20 | }{ 21 | { 22 | sphere: sphere.FAI, 23 | c1: kml.Coordinate{Lon: -108.6180554, Lat: 35.4325002}, 24 | c2: kml.Coordinate{Lon: -108.61, Lat: 35.43}, 25 | expected: 781, 26 | delta: 1e-3, 27 | }, 28 | } { 29 | t.Run(strconv.Itoa(i), func(t *testing.T) { 30 | assert.True(t, tc.sphere.HaversineDistance(tc.c1, tc.c2)-tc.expected < tc.delta) 31 | }) 32 | } 33 | } 34 | 35 | func TestInitialBearingTo(t *testing.T) { 36 | for i, tc := range []struct { 37 | sphere sphere.T 38 | c1 kml.Coordinate 39 | c2 kml.Coordinate 40 | expected float64 41 | }{ 42 | { 43 | sphere: sphere.FAI, 44 | c1: kml.Coordinate{Lon: 0, Lat: 0}, 45 | c2: kml.Coordinate{Lon: 0, Lat: 1}, 46 | expected: 0, 47 | }, 48 | { 49 | sphere: sphere.FAI, 50 | c1: kml.Coordinate{Lon: 0, Lat: 0}, 51 | c2: kml.Coordinate{Lon: 1, Lat: 0}, 52 | expected: 90, 53 | }, 54 | { 55 | sphere: sphere.FAI, 56 | c1: kml.Coordinate{Lon: 0, Lat: 0}, 57 | c2: kml.Coordinate{Lon: 0, Lat: -1}, 58 | expected: 180, 59 | }, 60 | { 61 | sphere: sphere.FAI, 62 | c1: kml.Coordinate{Lon: 0, Lat: 0}, 63 | c2: kml.Coordinate{Lon: -1, Lat: 0}, 64 | expected: -90, 65 | }, 66 | } { 67 | t.Run(strconv.Itoa(i), func(t *testing.T) { 68 | assert.Equal(t, tc.expected, tc.sphere.InitialBearingTo(tc.c1, tc.c2)) 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /xsd/kml22gx.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 65 | 67 | 69 | 72 | 74 | 75 | 77 | 78 | 79 | 80 | 82 | 83 | 84 | 86 | 88 | 89 | 91 | 93 | 95 | 97 | 99 | 100 | 103 | 104 | 105 | 107 | 109 | 111 | 113 | 114 | 115 | 116 | 117 | 118 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 157 | 158 | 159 | 160 | 161 | 163 | 164 | 165 | 166 | 167 | 168 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 195 | 197 | 199 | 200 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 253 | 254 | 255 | 256 | 257 | 258 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 275 | 276 | 277 | 278 | 279 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 325 | 326 | 327 | 328 | 329 | 330 | --------------------------------------------------------------------------------