-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathforecast.go
391 lines (358 loc) · 13.2 KB
/
forecast.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
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
package meteologix
import (
"encoding/json"
"fmt"
"net/url"
"strconv"
"time"
)
const (
// ForecastDetailStandard represents a standard level of detail for weather forecasts retrieved from the API.
ForecastDetailStandard ForecastDetails = "standard"
// ForecastDetailAdvanced represents an advanced level of detail for weather forecasts retrieved from the API.
ForecastDetailAdvanced ForecastDetails = "advanced"
)
// WeatherForecast represents the weather forecast API response
type WeatherForecast struct {
// Altitude represents the altitude of the location that has been queried
Altitude int `json:"alt"`
// Data holds the different APICurrentWeatherData points
Data []APIWeatherForecastData `json:"data"`
// Latitude represents the GeoLocation latitude coordinates for the weather data
Latitude float64 `json:"lat"`
// Longitude represents the GeoLocation longitude coordinates for the weather data
Longitude float64 `json:"lon"`
// Precision represents the weather models resolution
Precision Precision `json:"resolution"`
// Run represents the time when the weather forecast was generated.
Run time.Time `json:"run"`
// Timezone represents the timezone at the location
Timezone string `json:"timeZone"`
// UnitSystem is the unit system that is used for the results (we default to metric)
UnitSystem string `json:"systemOfUnits"`
}
// ForecastTimeSteps represents a time step used in a weather forecast. It is an alias type for a string type
type ForecastTimeSteps string
// ForecastDetails represents a type of detail for weather forecasts retrieved from the API
type ForecastDetails string
// APIWeatherForecastData holds the different data points of the WeatherForecast as returned by the
// weather forecast API endpoints.
type APIWeatherForecastData struct {
// CloudCoverage represents the effective cloud coverage within the preceding timespan
// in % (e.g. low clouds have more priority than high clouds)
CloudCoverage NilFloat64 `json:"cloudCoverage,omitempty"`
// DateTime represents the date and time for the forecast values
DateTime time.Time `json:"dateTime"`
// Humidity represents the relative humidity value of a weather forecast
Humidity NilFloat64 `json:"humidityRelative"`
// IsDay is true when it is date and time of forecast is at daytime
IsDay bool `json:"isDay"`
// Dewpoint represents the predicted dewpoint (at current timestamp)
Dewpoint NilFloat64 `json:"dewpoint,omitempty"`
// PressureMSL represents barometric air pressure at mean sea level (at current timestamp)
PressureMSL NilFloat64 `json:"pressureMsl,omitempty"`
// SunHours represents the most probable amount of hours the sun will be visible
SunHours NilFloat64 `json:"sunHours,omitempty"`
// Temperature represents the predicted temperature at 2m height (at current timestamp)
Temperature float64 `json:"temp"`
// WeatherSymbol is a text representation of the current weather conditions
WeatherSymbol NilString `json:"weatherSymbol,omitempty"`
// WindDirection represents the average direction from which the wind originates in degree
WindDirection NilFloat64 `json:"windDirection,omitempty"`
// WindGust represents the wind gust speed in m/s (for a timespan)
WindGust NilFloat64 `json:"windGust,omitempty"`
// WindGust3h represents the wind gust speed in m/s over the last 3 hours
WindGust3h NilFloat64 `json:"windGust3h,omitempty"`
// WindSpeed represents the average wind speed (for a timespan) in m/s
WindSpeed NilFloat64 `json:"windspeed,omitempty"`
}
// WeatherForecastDatapoint represents a single data point in a weather forecast.
type WeatherForecastDatapoint struct {
cloudCoverage NilFloat64
dateTime time.Time
dewpoint NilFloat64
humidity NilFloat64
isDay bool
pressureMSL NilFloat64
sunhours NilFloat64
temperature float64
weatherSymbol NilString
winddirection NilFloat64
windgust NilFloat64
windgust3h NilFloat64
windspeed NilFloat64
}
// ForecastByCoordinates returns the WeatherForecast values for the given coordinates
func (c *Client) ForecastByCoordinates(latitude, longitude float64, timespan Timespan,
details ForecastDetails,
) (WeatherForecast, error) {
var forecast WeatherForecast
var steps string
switch timespan {
case Timespan1Hour, Timespan3Hours, Timespan6Hours:
steps = timespan.String()
default:
return forecast, fmt.Errorf("unsupported timespan for weather forecasts: %s", timespan)
}
latitudeFormat := strconv.FormatFloat(latitude, 'f', -1, 64)
longitudeFormat := strconv.FormatFloat(longitude, 'f', -1, 64)
apiURL, err := url.Parse(fmt.Sprintf("%s/forecast/%s/%s/%s/%s", c.config.apiURL, latitudeFormat,
longitudeFormat, details, steps))
if err != nil {
return forecast, fmt.Errorf("failed to parse weather forecast URL: %w", err)
}
queryString := apiURL.Query()
queryString.Add("units", "metric")
apiURL.RawQuery = queryString.Encode()
response, err := c.httpClient.Get(apiURL.String())
if err != nil {
return forecast, fmt.Errorf("API request failed: %w", err)
}
if err = json.Unmarshal(response, &forecast); err != nil {
return forecast, fmt.Errorf("failed to unmarshal API response JSON: %w", err)
}
return forecast, nil
}
// ForecastByLocation returns the WeatherForecast values for the given location
func (c *Client) ForecastByLocation(location string, timesteps Timespan,
details ForecastDetails,
) (WeatherForecast, error) {
geoLocation, err := c.GetGeoLocationByName(location)
if err != nil {
return WeatherForecast{}, fmt.Errorf("failed too look up geolocation: %w", err)
}
return c.ForecastByCoordinates(geoLocation.Latitude, geoLocation.Longitude, timesteps, details)
}
// At returns the WeatherForecastDatapoint for the specified timestamp. It will try to find the closest datapoint
// in the forecast that matches the given timestamp. If no matching datapoint is found, an empty
// WeatherForecastDatapoint is returned.
func (wf WeatherForecast) At(timestamp time.Time) WeatherForecastDatapoint {
datapoint := findClosestForecast(wf.Data, timestamp)
if datapoint == nil {
return WeatherForecastDatapoint{}
}
return newWeatherForecastDataPoint(*datapoint)
}
// All returns a slice of WeatherForecastDatapoint representing all forecasted data points.
func (wf WeatherForecast) All() []WeatherForecastDatapoint {
datapoints := make([]WeatherForecastDatapoint, 0)
for _, data := range wf.Data {
datapoint := newWeatherForecastDataPoint(data)
datapoints = append(datapoints, datapoint)
}
return datapoints
}
// CloudCoverage returns the cloud coverage data point as Coverage.
//
// If the data point is not available in the WeatherForecast it will return Coverage in which
// the "not available" field will be true.
func (dp WeatherForecastDatapoint) CloudCoverage() Coverage {
if dp.cloudCoverage.IsNil() {
return Coverage{notAvailable: true}
}
coverage := Coverage{
dateTime: dp.dateTime,
name: FieldCloudCoverage,
source: SourceForecast,
floatVal: dp.cloudCoverage.value,
}
return coverage
}
// DateTime returns the date and time of the WeatherForecastDatapoint.
func (dp WeatherForecastDatapoint) DateTime() time.Time {
return dp.dateTime
}
// Dewpoint returns the dewpoint data point as Temperature.
//
// If the data point is not available in the WeatherForecast it will return Temperature in which the
// "not available" field will be true.
func (dp WeatherForecastDatapoint) Dewpoint() Temperature {
if dp.dewpoint.IsNil() {
return Temperature{notAvailable: true}
}
temperature := Temperature{
dateTime: dp.dateTime,
name: FieldDewpoint,
source: SourceForecast,
floatVal: dp.dewpoint.Get(),
}
return temperature
}
// HumidityRelative returns the relative humidity data point as Humidity.
//
// If the data point is not available in the WeatherForecast it will return Humidity in which the
// "not available" field will be true.
func (dp WeatherForecastDatapoint) HumidityRelative() Humidity {
if dp.humidity.IsNil() {
return Humidity{notAvailable: true}
}
humidity := Humidity{
dateTime: dp.dateTime,
name: FieldHumidityRelative,
source: SourceForecast,
floatVal: dp.humidity.Get(),
}
return humidity
}
// PressureMSL returns the pressure at mean sea level data point as Pressure.
//
// If the data point is not available in the WeatherForecast it will return Pressure in which the
// "not available" field will be true.
func (dp WeatherForecastDatapoint) PressureMSL() Pressure {
if dp.pressureMSL.IsNil() {
return Pressure{notAvailable: true}
}
pressure := Pressure{
dateTime: dp.dateTime,
name: FieldPressureMSL,
source: SourceForecast,
floatVal: dp.pressureMSL.Get(),
}
return pressure
}
// SunHours returns the sun hours data point as Duration.
//
// If the data point is not available in the WeatherForecast it will return Duration in which the
// "not available" field will be true.
func (dp WeatherForecastDatapoint) SunHours() Duration {
if dp.winddirection.IsNil() {
return Duration{notAvailable: true}
}
duration := Duration{
dateTime: dp.dateTime,
name: FieldSunhours,
source: SourceForecast,
floatVal: dp.sunhours.Get(),
}
return duration
}
// Temperature returns the temperature data point as Temperature.
func (dp WeatherForecastDatapoint) Temperature() Temperature {
return Temperature{
dateTime: dp.DateTime(),
name: FieldTemperature,
source: SourceForecast,
floatVal: dp.temperature,
}
}
// WeatherSymbol returns a text representation of the weather forecast as Condition.
//
// If the data point is not available in the WeatherForecast, it will return Condition in which
// the "not available" field will be true.
func (dp WeatherForecastDatapoint) WeatherSymbol() Condition {
if dp.weatherSymbol.IsNil() {
return Condition{notAvailable: true}
}
condition := Condition{
dateTime: dp.dateTime,
name: FieldWeatherSymbol,
source: SourceForecast,
stringVal: dp.weatherSymbol.value,
}
return condition
}
// WindDirection returns the wind direction data point as Direction.
//
// If the data point is not available in the WeatherForecast it will return Direction in which the
// "not available" field will be true.
func (dp WeatherForecastDatapoint) WindDirection() Direction {
if dp.winddirection.IsNil() {
return Direction{notAvailable: true}
}
direction := Direction{
dateTime: dp.dateTime,
name: FieldWindDirection,
source: SourceForecast,
floatVal: dp.winddirection.Get(),
}
return direction
}
// WindGust returns the wind gust data point as Speed.
//
// If the data point is not available in the WeatherForecast it will return Speed in which the
// "not available" field will be true.
func (dp WeatherForecastDatapoint) WindGust() Speed {
if dp.windgust.IsNil() {
return Speed{notAvailable: true}
}
speed := Speed{
dateTime: dp.dateTime,
name: FieldWindGust,
source: SourceForecast,
floatVal: dp.windgust.Get(),
}
return speed
}
// WindGust3h returns the wind gust over the last 3 hours data point as Speed.
//
// If the data point is not available in the WeatherForecast it will return Speed in which the
// "not available" field will be true.
func (dp WeatherForecastDatapoint) WindGust3h() Speed {
if dp.windgust3h.IsNil() {
return Speed{notAvailable: true}
}
speed := Speed{
dateTime: dp.dateTime,
name: FieldWindGust3h,
source: SourceForecast,
floatVal: dp.windgust3h.Get(),
}
return speed
}
// WindSpeed returns the average wind speed data point as Speed.
//
// If the data point is not available in the WeatherForecast it will return Speed in which the
// "not available" field will be true.
func (dp WeatherForecastDatapoint) WindSpeed() Speed {
if dp.windspeed.IsNil() {
return Speed{notAvailable: true}
}
speed := Speed{
dateTime: dp.dateTime,
name: FieldWindSpeed,
source: SourceForecast,
floatVal: dp.windspeed.Get(),
}
return speed
}
// findClosestForecast finds the APIWeatherForecastData item in the given items slice
// that has the closest DateTime value to the target time. It returns a pointer to
// the closest item. If the items slice is empty, it returns nil.
func findClosestForecast(items []APIWeatherForecastData, target time.Time) *APIWeatherForecastData {
if len(items) <= 0 {
return nil
}
closest := items[0]
minDiff := target.Sub(closest.DateTime).Abs()
for _, item := range items[1:] {
diff := target.Sub(item.DateTime).Abs()
if diff < minDiff {
minDiff = diff
closest = item
}
}
return &closest
}
// newWeatherForecastDataPoint creates a new WeatherForecastDatapoint from the provided APIWeatherForecastData.
// It extracts the necessary data from the APIWeatherForecastData and sets them in the WeatherForecastDatapoint
// structure. The new WeatherForecastDatapoint is then returned.
func newWeatherForecastDataPoint(data APIWeatherForecastData) WeatherForecastDatapoint {
return WeatherForecastDatapoint{
cloudCoverage: data.CloudCoverage,
dateTime: data.DateTime,
dewpoint: data.Dewpoint,
humidity: data.Humidity,
isDay: data.IsDay,
pressureMSL: data.PressureMSL,
sunhours: data.SunHours,
temperature: data.Temperature,
weatherSymbol: data.WeatherSymbol,
winddirection: data.WindDirection,
windgust: data.WindGust,
windgust3h: data.WindGust3h,
windspeed: data.WindSpeed,
}
}