-
Notifications
You must be signed in to change notification settings - Fork 51
/
globe.go
277 lines (240 loc) · 8.5 KB
/
globe.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
// Package globe builds 3D visualizations on the earth.
package globe
import (
"image"
"image/color"
"math"
"github.com/tidwall/pinhole"
)
// Precision constants.
const (
// graticuleLineStep is the gap between nodes of a parallel or meridian line
// in degrees.
graticuleLineStep = 1.0
// linePointInterval is the max distance (in km) between line segments when
// drawing along a great circle.
linePointInterval = 500.0
)
// Style encapsulates globe display options.
type Style struct {
GraticuleColor color.Color
LineColor color.Color
DotColor color.Color
Background color.Color
LineWidth float64
Scale float64
}
// imageOptions builds the pinhole ImageOptions object for this Style.
func (s Style) imageOptions() *pinhole.ImageOptions {
return &pinhole.ImageOptions{
BGColor: s.Background,
LineWidth: s.LineWidth,
Scale: s.Scale,
}
}
// DefaultStyle specifies out-of-the box style options.
var DefaultStyle = Style{
GraticuleColor: color.Gray{192},
LineColor: color.Gray{32},
DotColor: color.NRGBA{255, 0, 0, 255},
Background: color.White,
LineWidth: 0.1,
Scale: 0.7,
}
// Globe is a globe visualization.
type Globe struct {
p *pinhole.Pinhole
style Style
}
// New constructs an empty globe with the default style.
func New() *Globe {
return &Globe{
p: pinhole.New(),
style: DefaultStyle,
}
}
// Option is a function that stylizes a globe.
type Option func(*Globe)
// Color uses the given color.
func Color(c color.Color) Option {
return func(g *Globe) {
g.p.Colorize(c)
}
}
// styled is an internal convenience for applying style Options within a pinhole
// Begin/End context.
func (g *Globe) styled(base Option, options ...Option) func() {
g.p.Begin()
return func() {
base(g)
for _, option := range options {
option(g)
}
g.p.End()
}
}
// DrawParallel draws the parallel of latitude lat.
// Uses the default GraticuleColor unless overridden by style Options.
func (g *Globe) DrawParallel(lat float64, style ...Option) {
defer g.styled(Color(g.style.GraticuleColor), style...)()
for lng := -180.0; lng < 180.0; lng += graticuleLineStep {
x1, y1, z1 := cartestian(lat, lng)
x2, y2, z2 := cartestian(lat, lng+graticuleLineStep)
g.p.DrawLine(x1, y1, z1, x2, y2, z2)
}
}
// DrawParallels draws parallels at the given interval.
// Uses the default GraticuleColor unless overridden by style Options.
func (g *Globe) DrawParallels(interval float64, style ...Option) {
g.DrawParallel(0, style...)
for lat := interval; lat < 90.0; lat += interval {
g.DrawParallel(lat, style...)
g.DrawParallel(-lat, style...)
}
}
// DrawMeridian draws the meridian at longitude lng.
// Uses the default GraticuleColor unless overridden by style Options.
func (g *Globe) DrawMeridian(lng float64, style ...Option) {
defer g.styled(Color(g.style.GraticuleColor), style...)()
for lat := -90.0; lat < 90.0; lat += graticuleLineStep {
x1, y1, z1 := cartestian(lat, lng)
x2, y2, z2 := cartestian(lat+graticuleLineStep, lng)
g.p.DrawLine(x1, y1, z1, x2, y2, z2)
}
}
// DrawMeridians draws meridians at the given interval.
// Uses the default GraticuleColor unless overridden by style Options.
func (g *Globe) DrawMeridians(interval float64, style ...Option) {
for lng := -180.0; lng < 180.0; lng += interval {
g.DrawMeridian(lng, style...)
}
}
// DrawGraticule draws a latitude/longitude grid at the given interval.
// Uses the default GraticuleColor unless overridden by style Options.
func (g *Globe) DrawGraticule(interval float64, style ...Option) {
g.DrawParallels(interval, style...)
g.DrawMeridians(interval, style...)
}
// DrawDot draws a dot at (lat, lng) with the given radius.
// Uses the default DotColor unless overridden by style Options.
func (g *Globe) DrawDot(lat, lng float64, radius float64, style ...Option) {
defer g.styled(Color(g.style.DotColor), style...)()
x, y, z := cartestian(lat, lng)
g.p.DrawDot(x, y, z, radius)
}
// DrawLine draws a line between (lat1, lng1) and (lat2, lng2) along the great
// circle.
// Uses the default LineColor unless overridden by style Options.
func (g *Globe) DrawLine(lat1, lng1, lat2, lng2 float64, style ...Option) {
defer g.styled(Color(g.style.LineColor), style...)()
d := haversine(lat1, lng1, lat2, lng2)
step := d / math.Ceil(d/linePointInterval)
fx, fy, fz := cartestian(lat1, lng1)
for p := step; p < d-step/2; p += step {
tlat, tlng := intermediate(lat1, lng1, lat2, lng2, p/d)
tx, ty, tz := cartestian(tlat, tlng)
g.p.DrawLine(fx, fy, fz, tx, ty, tz)
fx, fy, fz = tx, ty, tz
}
tx, ty, tz := cartestian(lat2, lng2)
g.p.DrawLine(fx, fy, fz, tx, ty, tz)
}
// DrawRect draws the rectangle with the given corners. Sides are drawn along
// great circles, as in DrawLine.
// Uses the default LineColor unless overridden by style Options.
func (g *Globe) DrawRect(minlat, minlng, maxlat, maxlng float64, style ...Option) {
g.DrawLine(minlat, minlng, maxlat, minlng, style...)
g.DrawLine(maxlat, minlng, maxlat, maxlng, style...)
g.DrawLine(maxlat, maxlng, minlat, maxlng, style...)
g.DrawLine(minlat, maxlng, minlat, minlng, style...)
}
// DrawLandBoundaries draws land boundaries on the globe.
// Uses the default LineColor unless overridden by style Options.
func (g *Globe) DrawLandBoundaries(style ...Option) {
g.drawPreparedPaths(land, style...)
}
// DrawCountryBoundaries draws country boundaries on the globe.
// Uses the default LineColor unless overridden by style Options.
func (g *Globe) DrawCountryBoundaries(style ...Option) {
g.drawPreparedPaths(countries, style...)
}
func (g *Globe) drawPreparedPaths(paths [][]struct{ lat, lng float32 }, style ...Option) {
defer g.styled(Color(g.style.LineColor), style...)()
for _, path := range paths {
n := len(path)
for i := 0; i+1 < n; i++ {
p1, p2 := path[i], path[i+1]
x1, y1, z1 := cartestian(float64(p1.lat), float64(p1.lng))
x2, y2, z2 := cartestian(float64(p2.lat), float64(p2.lng))
g.p.DrawLine(x1, y1, z1, x2, y2, z2)
}
}
}
// CenterOn rotates the globe to center on (lat, lng).
func (g *Globe) CenterOn(lat, lng float64) {
g.p.Rotate(0, 0, -degToRad(lng)-math.Pi/2)
g.p.Rotate(math.Pi/2-degToRad(lat), 0, 0)
}
// Image renders an image object for the visualization with dimensions
// (side, side).
func (g *Globe) Image(side int) *image.RGBA {
opts := g.style.imageOptions()
return g.p.Image(side, side, opts)
}
// SavePNG writes the visualization to filename in PNG format with dimensions
// (side, side).
func (g *Globe) SavePNG(filename string, side int) error {
opts := g.style.imageOptions()
return g.p.SavePNG(filename, side, side, opts)
}
// cartestian maps (lat, lng) to pinhole cartestian space.
func cartestian(lat, lng float64) (x, y, z float64) {
x = cos(lat) * cos(lng)
y = cos(lat) * sin(lng)
z = -sin(lat)
return
}
// earthRadius is the radius of the earth.
const earthRadius = 6371.0
// haversine returns the distance (in km) between the points (lat1, lng1) and
// (lat2, lng2).
func haversine(lat1, lng1, lat2, lng2 float64) float64 {
dlat := lat2 - lat1
dlng := lng2 - lng1
a := sin(dlat/2)*sin(dlat/2) + cos(lat1)*cos(lat2)*sin(dlng/2)*sin(dlng/2)
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
return earthRadius * c
}
// intermediate returns the point that is fraction f between (lat1, lng1) and
// (lat2, lng2).
func intermediate(lat1, lng1, lat2, lng2, f float64) (float64, float64) {
dr := haversine(lat1, lng1, lat2, lng2) / earthRadius
a := math.Sin((1-f)*dr) / math.Sin(dr)
b := math.Sin(f*dr) / math.Sin(dr)
x := a*cos(lat1)*cos(lng1) + b*cos(lat2)*cos(lng2)
y := a*cos(lat1)*sin(lng1) + b*cos(lat2)*sin(lng2)
z := a*sin(lat1) + b*sin(lat2)
phi := math.Atan2(z, math.Sqrt(x*x+y*y))
lambda := math.Atan2(y, x)
return radToDeg(phi), radToDeg(lambda)
}
// destination computes the destination point reached when travelling distance d
// from (lat, lng) at bearing brng.
func destination(lat, lng, d, brng float64) (float64, float64) {
dr := d / earthRadius
phi := math.Asin(sin(lat)*math.Cos(dr) + cos(lat)*math.Sin(dr)*cos(brng))
lambda := degToRad(lng) + math.Atan2(sin(brng)*math.Sin(dr)*cos(lat), math.Cos(dr)-sin(lat)*math.Sin(phi))
return radToDeg(phi), math.Mod(radToDeg(lambda)+540, 360) - 180
}
// sin is math.Sin for degrees.
func sin(d float64) float64 { return math.Sin(degToRad(d)) }
// cos is math.Cos for degrees
func cos(d float64) float64 { return math.Cos(degToRad(d)) }
// degToRad converts d degrees to radians.
func degToRad(d float64) float64 {
return math.Pi * d / 180.0
}
// radToDeg converts r radians to degrees.
func radToDeg(r float64) float64 {
return 180.0 * r / math.Pi
}