This repository has been archived by the owner on Nov 21, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
darksky.go
324 lines (271 loc) · 8.99 KB
/
darksky.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
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
/*
Package darksky provides a Go API for accessing the DarkSky HTTP API.
For Dark Sky API documentation refer to:
https://darksky.net/dev/docs
Requires an API Key to use. To register go to:
https://darksky.net/dev/register
*/
package darksky
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
)
// Forecast is the top level representation of the weather forecast for a location.
type Forecast struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Timezone string `json:"timezone"`
Offset int `json:"offset"`
Currently DataPoint `json:"currently,omitempty"`
Minutely DataBlock `json:"minutely,omitempty"`
Hourly DataBlock `json:"hourly,omitempty"`
Daily DataBlock `json:"daily,omitempty"`
Alerts []Alert `json:"alerts,omitempty"`
Flags Flags `json:"flags,omitempty"`
}
// DataPoint is the current weather data for a single point in time.
type DataPoint struct {
Time int64 `json:"time"`
Summary string `json:"summary"`
Icon string `json:"icon"`
SunriseTime int64 `json:"sunriseTime"`
SunsetTime int64 `json:"sunsetTime"`
PrecipIntensity float64 `json:"precipIntensity"`
PrecipIntensityMax float64 `json:"precipIntensityMax"`
PrecipIntensityMaxTime int64 `json:"precipIntensityMaxTime"`
PrecipProbability float64 `json:"precipProbability"`
PrecipType string `json:"precipType"`
PrecipAccumulation float64 `json:"precipAccumulation"`
Temperature float64 `json:"temperature"`
TemperatureMin float64 `json:"temperatureMin"`
TemperatureMinTime int64 `json:"temperatureMinTime"`
TemperatureMax float64 `json:"temperatureMax"`
TemperatureMaxTime int64 `json:"temperatureMaxTime"`
ApparentTemperature float64 `json:"apparentTemperature"`
DewPoint float64 `json:"dewPoint"`
WindSpeed float64 `json:"windSpeed"`
WindBearing float64 `json:"windBearing"`
CloudCover float64 `json:"cloudCover"`
Humidity float64 `json:"humidity"`
Pressure float64 `json:"pressure"`
Visibility float64 `json:"visibility"`
Ozone float64 `json:"ozone"`
MoonPhase float64 `json:"moonPhase"`
}
// WindDirection converts the numerical WindBearing value in degrees to directional text. (ex: 200 => "SW")
func (dp DataPoint) WindDirection() string {
direction := ""
if dp.WindBearing > 293 || dp.WindBearing < 67 {
direction += "N"
}
if dp.WindBearing < 247 && dp.WindBearing > 113 {
direction += "S"
}
if dp.WindBearing > 22 && dp.WindBearing < 157 {
direction += "E"
}
if dp.WindBearing < 337 && dp.WindBearing > 203 {
direction += "W"
}
return direction
}
// DataBlock is a collection of data points over a period of time.
type DataBlock struct {
Summary string `json:"summary"`
Icon string `json:"icon"`
Data []DataPoint `json:"data"`
}
// Alert is a potentially serious weather condition.
type Alert struct {
Title string `json:"title"`
Description string `json:"description"`
Expires int64 `json:"expires"`
URI string `json:"uri"`
}
// Flags contains meta data about the Forecast.
type Flags struct {
DarkSkyUnavailable string `json:"darksky-unavailable"`
DarkSkyStations []string `json:"darksky-stations"`
DataPointStations []string `json:"datapoint-stations"`
ISDStations []string `json:"isds-stations"`
LAMPStations []string `json:"lamp-stations"`
METARStations []string `json:"metars-stations"`
METNOLicense string `json:"metnol-license"`
Sources []string `json:"sources"`
Units string `json:"units"`
}
// ForecastRequest is the data needed to retrieve a forecast from the Dark Sky API.
// Key, Lat, and Lng are required to make a basic request. All other fields are optional,
// and have sensible defaults if created using MakeRequest.
type ForecastRequest struct {
Key string
Lat float64
Lng float64
Time int64
Lang Lang
Units Units
ExtendHourly bool
Exclude []string
baseURL string
}
// ForecastResponse is a wrapper struct for a response from the DarkSky API.
// Errors are included to make it easier to pass single values via channel from a goroutine.
type ForecastResponse struct {
Forecast Forecast
APICallCount int
Error error
}
// MakeRequest creates a new ForecastRequest with defaults for the optional fields. If
// used as-is the current forecast for the given lat/lng position will be retrieved in
// imperial units with english language text.
func MakeRequest(key string, latitude float64, longitude float64) *ForecastRequest {
return &ForecastRequest{
Key: key,
Lat: latitude,
Lng: longitude,
Time: -1,
Lang: English,
Units: US,
ExtendHourly: false,
Exclude: []string{},
baseURL: "https://api.darksky.net/forecast",
}
}
// Get makes an outbound call to the Dark Sky API, using the provided fields in the ForecastRequest.
func (f *ForecastRequest) Get() ForecastResponse {
if len(f.Key) == 0 {
return ForecastResponse{Error: errors.New(KeyRequired)}
}
if f.Lat < -90.0 || f.Lat > 90.0 {
return ForecastResponse{Error: errors.New(LatitudeInvalid)}
}
if f.Lng < -180.0 || f.Lng > 180.0 {
return ForecastResponse{Error: errors.New(LongitudeInvalid)}
}
fr := ForecastResponse{}
reqURL, err := f.URL()
if err != nil {
fr.Error = err
return fr
}
res, err := http.Get(reqURL)
if err != nil {
fr.Error = err
return fr
}
body, err := ioutil.ReadAll(res.Body)
defer res.Body.Close()
if err != nil {
fr.Error = err
return fr
}
if res.StatusCode >= 400 {
fr.Error = errors.New(string(body))
return fr
}
callCount, err := strconv.Atoi(res.Header.Get(APICallsHeader))
if err == nil {
fr.APICallCount = callCount
}
forecast, err := fromJSON(body)
if err != nil {
fr.Error = err
return fr
}
fr.Forecast = *forecast
return fr
}
// URL constructs and returns the valid url to request a forecast from the Dark Sky API.
func (f *ForecastRequest) URL() (string, error) {
reqURL, err := url.Parse(f.baseURL)
if err != nil {
return "", err
}
v := reqURL.Query()
v.Add("lang", string(f.Lang))
v.Add("units", string(f.Units))
reqURL.Path = fmt.Sprintf("%v/%v/%v,%v", reqURL.Path, f.Key, f.Lat, f.Lng)
if f.Time > 0 {
reqURL.Path = reqURL.Path + "," + strconv.FormatInt(f.Time, 10)
}
reqURL.RawQuery = v.Encode()
return reqURL.String(), nil
}
// WithBaseURL will cause a request to be made to the provided baseURL. The expected format is
// scheme://host:port/path. Useful for testing or hitting an internal proxy server.
func (f *ForecastRequest) WithBaseURL(baseURL string) *ForecastRequest {
f.baseURL = baseURL
return f
}
// WithTime will cause a Forecast to be retrieved for the given time, specified as seconds
// since unix epoch. This provides access to the "Time Machine" functionality of the Dark Sky API.
func (f *ForecastRequest) WithTime(t int64) *ForecastRequest {
f.Time = t
return f
}
// WithLang allows forecast text to be returned in the given language.
func (f *ForecastRequest) WithLang(l Lang) *ForecastRequest {
f.Lang = l
return f
}
// WithUnits allows the forecast values to be returned in the given units.
func (f *ForecastRequest) WithUnits(u Units) *ForecastRequest {
f.Units = u
return f
}
// ForecastRequest validation errors
const (
KeyRequired = "key is required"
LatitudeInvalid = "latitude is not valid, must between -90 and +90 degrees"
LongitudeInvalid = "longitude is not valid, must between -180 and 180 degrees"
)
// Units defines the possible options for measurement units used in the response.
type Units string
const (
US Units = "us"
SI Units = "si"
CA Units = "ca"
UK Units = "uk"
UK2 Units = "uk2"
AUTO Units = "auto"
)
// Lang defines the possible options for the text summary language.
type Lang string
const (
Arabic Lang = "ar"
Bosnian Lang = "bs"
German Lang = "de"
Greek Lang = "el"
English Lang = "en"
Spanish Lang = "es"
French Lang = "fr"
Croatian Lang = "hr"
Italian Lang = "it"
Dutch Lang = "nl"
Polish Lang = "pl"
Portuguese Lang = "pt"
Russian Lang = "ru"
Slovak Lang = "sk"
Swedish Lang = "sv"
Tetum Lang = "tet"
Turkish Lang = "tr"
Ukranian Lang = "uk"
PigLatin Lang = "x-pig-latin"
Chinese Lang = "zh"
TraditionalChinese Lang = "zh-tw"
)
// APICallsHeader is the HTTP Header that contains the number of API calls made by the given key for the current 24 period.
const APICallsHeader = "X-Forecast-API-Calls"
func fromJSON(jsonBlob []byte) (*Forecast, error) {
var f Forecast
err := json.Unmarshal(jsonBlob, &f)
if err != nil {
return nil, err
}
return &f, nil
}