-
Notifications
You must be signed in to change notification settings - Fork 0
/
graphics.go
201 lines (166 loc) · 6.99 KB
/
graphics.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
package main
import (
"fmt"
"image/color"
"math"
"github.com/fogleman/gg"
"github.com/kaosfere/aptdata"
"github.com/kellydunn/golang-geo"
"github.com/spf13/viper"
)
const sideLength = 640
const outerMargin = 100
const chartSideLength = sideLength - outerMargin
func kmToFeet(km float64) (feet float64) {
return km * 3280.4
}
func round(raw float64) (rounded int) {
return int(math.Floor(raw + .5))
}
// This math is super-duper ugly and was iteratively developed. I do not
// profess strength in geometry, and there could totally be a better way
// of doing this. There probably is. I'm st00pid. But this works.
func calcPixels(runways []*aptdata.Runway) (pxEndpoints [][2][2]float64, xDimension int, yDimension int) {
degEndpoints := make([][2]*geo.Point, len(runways))
pxEndpoints = make([][2][2]float64, len(runways))
var minLatitude, maxLatitude, minLongitude, maxLongitude,
xyLengthRatio, xLengthDegrees, yLengthDegrees, lngAdjFactor,
latAdjFactor float64
// Set some values we know will be reset by our data to ensure we properly
// capture all the extremes. (Setting, say, minLatitude to 0 won't work,
// because we may have negative latitudes -- instead we'll set it as high
// as it could possibly be and let our data drag it down.)
minLatitude = 90
maxLatitude = -90
minLongitude = 180
maxLongitude = -180
// Find our minimum and maximum lats and longs so we know the
// dimensions of our bounding box. Then create a slice of pairs of
// points representing the ends of each runway.
for i, r := range runways {
minLatitude = math.Min(minLatitude,
math.Min(r.End1Latitude, r.End2Latitude))
maxLatitude = math.Max(maxLatitude,
math.Max(r.End1Latitude, r.End2Latitude))
minLongitude = math.Min(minLongitude,
math.Min(r.End1Longitude, r.End2Longitude))
maxLongitude = math.Max(maxLongitude,
math.Max(r.End1Longitude, r.End2Longitude))
degEndpoints[i] = [2]*geo.Point{geo.NewPoint(r.End1Latitude, r.End1Longitude),
geo.NewPoint(r.End2Latitude, r.End2Longitude)}
}
// Create a point for each corner of the bounding box
nwPoint := geo.NewPoint(maxLatitude, minLongitude)
nePoint := geo.NewPoint(maxLatitude, maxLongitude)
swPoint := geo.NewPoint(minLatitude, minLongitude)
sePoint := geo.NewPoint(minLatitude, maxLongitude)
// Find the lat/long deltas for the X and Y sides. TODO: Correct this for
// the slight difference in distance betweenlines like we do below? Meh.
xLengthDegrees = maxLongitude - minLongitude
yLengthDegrees = maxLatitude - minLatitude
// Determine what the length of each side is in feet. We look for the max
// here to offset the fact that one degree will have slightly different
// lengths depending on where in the world you are. This is probably overly
// picky.
xLengthFeet := round(kmToFeet(math.Max(nwPoint.GreatCircleDistance(nePoint),
swPoint.GreatCircleDistance(sePoint))))
yLengthFeet := round(kmToFeet(math.Max(nwPoint.GreatCircleDistance(swPoint),
nePoint.GreatCircleDistance(sePoint))))
// Determine the ratio of the LengthDegreess of the north/south and
// east/west sides. This will be used to scale the actual pixel-counts of
// the sides.
xyLengthRatio = float64(xLengthFeet) / float64(yLengthFeet)
// Set the dimension of each side in pixels witn the longest side being
// defined by chartSideLength and the shorter by the xyLengthRatio.
if xLengthFeet > yLengthFeet {
xDimension = chartSideLength
yDimension = round(chartSideLength / xyLengthRatio)
} else {
yDimension = chartSideLength
xDimension = round(chartSideLength * xyLengthRatio)
}
// Eacn axis needs to have its ration of distance in degrees to distance
// in pixels adjusted seperately, due to the difference in the actual
// size of a "degree" as mentioned above. This will create a (faily large)
// float that we multiply each degree delta by to convert it into pixels.
lngAdjFactor = float64(xDimension) / xLengthDegrees
latAdjFactor = float64(yDimension) / yLengthDegrees
// Now we create a new array, witn the delta degrees converted into pixel
// coordinates as descibed above.
for i, r := range degEndpoints {
pxEndpoints[i] = [2][2]float64{{float64(round((r[0].Lat() - minLatitude) * latAdjFactor)),
float64(round((r[0].Lng() - minLongitude) * lngAdjFactor))},
{float64(round((r[1].Lat() - minLatitude) * latAdjFactor)),
float64(round((r[1].Lng() - minLongitude) * lngAdjFactor))}}
}
return pxEndpoints, xDimension, yDimension
}
func drawAirport(runways []*aptdata.Runway, code string, name string, city string, region string, country string) error {
// Get our runway endpoints and image size as pixels. The math is ugly,
// so it's contained in a seperate function.
pxEndpoints, xDimension, yDimension := calcPixels(runways)
// define the colors for easier setting
blueprint := color.RGBA{4, 63, 140, 255}
white := color.RGBA{255, 255, 255, 255}
// Start with canvas shrink-wrapped to the airport size
canvas := gg.NewContext(xDimension, yDimension)
canvas.InvertY()
// Fill the entire box with blueprint blue
canvas.SetColor(blueprint)
canvas.Clear()
// Prepare for line drawing
canvas.SetColor(white)
canvas.SetLineWidth(3)
// Draw a line for each runway
for _, p := range pxEndpoints {
canvas.DrawLine(p[0][1], p[0][0],
p[1][1], p[1][0])
}
// Now stroke the lot of them
canvas.Stroke()
// Render the chart to an Image
chart := canvas.Image()
// Switch to a new context the full size of our picture
canvas = gg.NewContext(sideLength, sideLength)
// Fill it with blueprint blue
canvas.SetColor(blueprint)
canvas.Clear()
// Now render the chart image centered in the box
canvas.DrawImage(chart, (sideLength-xDimension)/2, (sideLength-yDimension)/2)
// Time to do some labelling!
err := canvas.LoadFontFace(viper.GetString("font"), 12)
if err != nil {
return err
}
canvas.SetLineWidth(3)
// Set strings for our name and location. Sometimes the city is empty
// in our data. Handle that neatly.
nameCode := fmt.Sprintf("%s (%s)", name, code)
location := fmt.Sprintf("%s, %s", region, country)
if city != "" {
location = fmt.Sprintf("%s, %s", city, location)
}
// Get the dimensions of our text
nWidth, nHeight := canvas.MeasureString(nameCode)
lWidth, lHeight := canvas.MeasureString(location)
// And calculate dimensions of its box
textMargin := float64(10)
lineSpacing := float64(8)
textWidth := math.Max(nWidth, lWidth)
boxWidth := textWidth + textMargin*2
boxHeight := nHeight + lHeight + textMargin*2 + lineSpacing
// Clear out a box for the text to fit into
boxX := sideLength - boxWidth
canvas.DrawRectangle(boxX, 0, boxWidth, boxHeight)
canvas.SetColor(blueprint)
canvas.Fill()
// Add the text
canvas.SetColor(white)
canvas.DrawString(nameCode, sideLength-textMargin-nWidth, nHeight+textMargin)
canvas.DrawString(location, sideLength-textMargin-lWidth, nHeight+lHeight+textMargin+lineSpacing)
// Finally, draw a border all around it
canvas.DrawRectangle(0, 0, sideLength, sideLength)
canvas.Stroke()
err = canvas.SavePNG(fmt.Sprintf("%s/%s", viper.GetString("outdir"), "out.png"))
return err
}