-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathvaporwair.go
307 lines (266 loc) · 9.87 KB
/
vaporwair.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
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
package main
import (
"flag"
"fmt"
"github.com/jeff-bruemmer/vaporwair/src/air"
"github.com/jeff-bruemmer/vaporwair/src/geolocation"
"github.com/jeff-bruemmer/vaporwair/src/report"
"github.com/jeff-bruemmer/vaporwair/src/storage"
"github.com/jeff-bruemmer/vaporwair/src/weather"
"log"
"strings"
"time"
)
// Timeout, an int representing minutes, determines how long a forecast is valid.
const Timeout = 5
// Flags
var weatherHourly bool
var weatherWeek bool
var airQuality bool
// Globals
var weatherForecast weather.Forecast
var airForecast []air.Forecast
var config storage.Config
// Variables used to sync spinner.
var reportsReady = false
var spinnerChan = make(chan time.Time)
// Is the forecast still valid? Specify a timeout duration (in minutes) that determines whether or not
// the forecast is still valid.
func isValid(t time.Time, timeout float64) bool {
return time.Since(t).Minutes() < timeout
}
// Spinner creates a basic loading bar with zero connection to reality.
// Its purpose is to show the user the program is running.
// It returns a time once it finds the the reports are ready to print.
// That time is put in a channel used to terminate the spinner.
func Spinner(t time.Time) time.Time {
meterInit := "\r[=> ]"
meter := meterInit
for i := 0; i <= len(meterInit)-1; i++ {
if reportsReady {
break
}
// Set the interval to add another =.
time.Sleep(100 * time.Millisecond)
meter = strings.Replace(meter, "> ", "=>", 1)
fmt.Printf(meter)
// Loop spinner if it completes before the reports have returned
if i == len(meterInit)-1 {
log.Fatal("There was a problem getting your forecast. Please check your internet connection.")
}
}
// Clear the line.
fmt.Printf("\r ")
return t
}
// Prints the time it took to download (or retrieve from disk) the forecasts.
func PrintElapsedTime(t time.Time) {
fmt.Printf("\rForecasts fetched in %v seconds.\n", time.Since(t).Seconds())
}
// RunReports determines which report to run based on flags.
// Only one report can be run at a time.
func RunReports(f weather.Forecast, a []air.Forecast) {
switch {
case weatherHourly:
report.WeatherHourly(f, a)
case weatherWeek:
report.WeatherWeek(f, a)
case airQuality:
report.AirQuality(f, a)
default:
report.Summary(f, a)
}
}
// GetCoordinates retrieves user's current coordinates via IP address
// and the IP-API.
func GetCoordinates() geolocation.Coordinates {
// Get geolocation data.
geoData := geolocation.GetGeoData(geolocation.IPAPIAddress)
// Format coordinates and compose URLs for API calls.
return geolocation.FormatCoordinates(geoData)
}
func PrintSpaceTime(t, t1 time.Time, c geolocation.Coordinates) {
PrintElapsedTime(t1)
fmt.Println(t.Format("Mon Jan 2 15:04:05 MST 2006"))
fmt.Println(c.City, c.Zip, "|", c.Latitude, ",", c.Longitude)
}
func RunReportsForFirstTime(c geolocation.Coordinates, t time.Time) (weather.Forecast, []air.Forecast) {
dsURL := weather.BuildDarkSkyURL(weather.DarkSkyAddress, config.DarkSkyAPIKey, c, weather.DarkSkyUnits)
// build AirNowURL
anURL := air.BuildAirNowURL(air.AirNowAddress, c, t.Format("2006-01-02"), config.AirNowAPIKey)
weatherChan := make(chan weather.Forecast)
airChan := make(chan []air.Forecast)
go func() {
weatherChan <- weather.GetForecast(dsURL)
}()
go func() {
airChan <- air.GetForecast(anURL)
}()
// Wait for API calls to return and run reports.
weatherForecast = <-weatherChan
close(weatherChan)
airForecast = <-airChan
close(airChan)
reportsReady = true
t1 := <-spinnerChan
close(spinnerChan)
PrintSpaceTime(t, t1, c)
RunReports(weatherForecast, airForecast)
report.TW.Flush()
return weatherForecast, airForecast
}
// Saves forecast in Vaporwair home directory for caching.
func SaveForecasts(homeDir string, coordinates geolocation.Coordinates, weather weather.Forecast, air []air.Forecast) {
// Update last api call
storage.UpdateLastCall(coordinates, homeDir+storage.SavedCallFileName)
// Save forecasts for next call
storage.SaveWeatherForecast(homeDir+storage.SavedWeatherFileName, weatherForecast)
storage.SaveAirForecast(homeDir+storage.SavedAirFileName, airForecast)
}
// CaptureAPIKeys prompts users for Dark Sky and Air Now API keys
// and saves thems in a config file.
func CaptureAPIKeys(homeDir string) {
DSAPIKey := storage.Capture("Enter Dark Sky API key: ")
ANAPIKey := storage.Capture("Enter Air Now API key: ")
err := storage.CreateConfig(homeDir, DSAPIKey, ANAPIKey)
if err != nil {
log.Fatal("There was a problem saving your APIkeys. Try again.")
}
}
// CleanPrintSave closes open air, weather, and spinner channels; prints reports;
// and saves the forecasts for future reporting.
func CleanPrintSave(weatherChan chan weather.Forecast, airChan chan []air.Forecast, t time.Time, c geolocation.Coordinates, homeDir string) {
// Wait for forecasts, then close channels.
weatherForecast = <-weatherChan
close(weatherChan)
airForecast = <-airChan
close(airChan)
// Stop Spinner
reportsReady = true
t1 := <-spinnerChan
close(spinnerChan)
// Print time and geodata, then print reports.
PrintSpaceTime(t, t1, c)
RunReports(weatherForecast, airForecast)
report.TW.Flush()
// Save forecasts
SaveForecasts(homeDir, c, weatherForecast, airForecast)
return
}
// Assign commandline flags.
func init() {
flag.BoolVar(&weatherHourly, "h", false, "Prints weather forecast hour by hour.")
flag.BoolVar(&weatherWeek, "w", false, "Prints daily weather forecast for the next week.")
flag.BoolVar(&airQuality, "a", false, "Prints air quality forecast.")
}
// The main function is large for a Go program, but it provides a good
// overview of the program's execution.
func main() {
t := time.Now()
// Start call to IP-API in case previously used coordinates either
// do not exist or are invalid.
geoChan := make(chan geolocation.GeoData)
go func() {
geoChan <- geolocation.GetGeoData(geolocation.IPAPIAddress)
}()
// Start Spinner
go func() {
spinnerChan <- Spinner(t)
}()
// Parse flags to determine which report to run.
flag.Parse()
// First get home directory for user.
homeDir, err := storage.GetHomeDir()
// If the home directory could not be determined, bail.
if err != nil {
log.Fatal("Unable to determine home directory.")
}
// Identify or create vaporwair directory.
storage.CreateVaporwairDir(homeDir + storage.VaporwairDir)
// Check if configuration file with API keys exists.
cf := homeDir + storage.ConfigFileName
configExists, _ := storage.Exists(cf)
// If not, prompt user for API keys and create configuration file.
if !configExists {
CaptureAPIKeys(homeDir)
}
// Load API keys
config = storage.GetConfig(cf)
// Channels to store calls with newly confirmed coordinates
airChan := make(chan []air.Forecast)
weatherChan := make(chan weather.Forecast)
// Load previous call metadata to determine if call is still valid.
pc, err := storage.LoadCallInfo(homeDir + storage.SavedCallFileName)
if err != nil {
// If not, run the reports for the first time.
coordinates := GetCoordinates()
weatherForecast, airForecast = RunReportsForFirstTime(coordinates, t)
SaveForecasts(homeDir, coordinates, weatherForecast, airForecast)
return
}
// If saved forecasts are found, check if the call has expired.
valid := isValid(pc.Time, Timeout)
// If the previous air and weather forecasts are still valid
// i.e. they were made within the timeout period suppplied to the isValid function
// (presumably from the same location), print forecast report and return.
if valid {
// Load previous weather forecast from disk.
pwf, err := storage.LoadSavedWeather(homeDir + storage.SavedWeatherFileName)
if err != nil {
fmt.Println("No previous weather forecast found.")
}
// Load previous air forecast from disk.
paf, err := storage.LoadSavedAir(homeDir + storage.SavedAirFileName)
if err != nil {
fmt.Println("No previous air forecast found.")
paf = <-airChan
}
reportsReady = true
t1 := <-spinnerChan
PrintSpaceTime(t, t1, pc.Coordinates)
RunReports(pwf, paf)
report.TW.Flush()
return
}
// While waiting for the coordinates to return form the IP-API,
// assume user has not changed coordinates since last weather check
// and make optimistic call to APIs using saved coordinates.
odsURL := weather.BuildDarkSkyURL(weather.DarkSkyAddress, config.DarkSkyAPIKey, pc.Coordinates, weather.DarkSkyUnits)
oanURL := air.BuildAirNowURL(air.AirNowAddress, pc.Coordinates, t.Format("2006-01-02"), config.AirNowAPIKey)
// optimistic channels
ow := make(chan weather.Forecast)
oa := make(chan []air.Forecast)
go func() {
ow <- weather.GetForecast(odsURL)
}()
go func() {
oa <- air.GetForecast(oanURL)
}()
// Get geolocation data from channel and extract coordinates.
geoData := <-geoChan
coordinates := geolocation.FormatCoordinates(geoData)
// If current coordinates match previous coordinates, the optimistic API calls
// are valid, no need to make new calls. Clean up, print reports, save forecasts,
// and return.
if coordinates.Latitude == pc.Coordinates.Latitude &&
coordinates.Longitude == pc.Coordinates.Longitude {
CleanPrintSave(ow, oa, t, coordinates, homeDir)
return
} else {
// If coordinates returned by IP-API call differ from coordinates in saved forecast,
// user is in a new location, and calls with the updated coordinates need to be made.
// Build URLs.
dsURL := weather.BuildDarkSkyURL(weather.DarkSkyAddress, config.DarkSkyAPIKey, coordinates, weather.DarkSkyUnits)
anURL := air.BuildAirNowURL(air.AirNowAddress, coordinates, t.Format("2006-01-02"), config.AirNowAPIKey)
// Asynchronously make calls to Dark Sky and Airnow with confirmed coordinates
go func() {
weatherChan <- weather.GetForecast(dsURL)
}()
go func() {
airChan <- air.GetForecast(anURL)
}()
// Wait for forecasts to return, then clean up, print reports, save forecasts, and return.
CleanPrintSave(weatherChan, airChan, t, coordinates, homeDir)
return
}
}