├── .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 | [](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 |
--------------------------------------------------------------------------------