/
client.go
126 lines (103 loc) · 3.21 KB
/
client.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
package stations
import (
"encoding/xml"
"fmt"
"io"
"math"
"net/http"
"sync"
"github.com/webercoder/gocean/src/lib"
"golang.org/x/net/html/charset"
)
// DefaultStationsEndpoint is the default stations endpoint from NOAA.
const DefaultStationsEndpoint = "https://opendap.co-ops.nos.noaa.gov/stations/stationsXML.jsp"
// MaxStationsSearchChunkSize is used when searching for nearest station with goroutines
const MaxStationsSearchChunkSize = 10
// Client interacts with NOAA.
type Client struct {
URL string
HTTPClient lib.HTTPGetter
StationsCache []Station
}
// NewClient creates a new NOAAStationClient with the default URL.
func NewClient() *Client {
return &Client{URL: DefaultStationsEndpoint, HTTPClient: &http.Client{}}
}
func decodeStationsXMLStream(httpResponseBody io.ReadCloser) (GetStationResponse, error) {
var stations GetStationResponse
decoder := xml.NewDecoder(httpResponseBody)
decoder.CharsetReader = charset.NewReaderLabel
err := decoder.Decode(&stations)
return stations, err
}
// GetStations .
func (s *Client) GetStations(skipCache bool) []Station {
var stationResponse GetStationResponse
if !skipCache && len(s.StationsCache) > 0 {
return s.StationsCache
}
resp, err := s.HTTPClient.Get(s.URL)
if err != nil {
fmt.Println("Error retrieving station data", err)
return stationResponse.Stations
}
if resp.Body == nil {
fmt.Println("Error retrieving HTTP request body for station data", err)
return stationResponse.Stations
}
defer resp.Body.Close()
stationResponse, err = decodeStationsXMLStream(resp.Body)
if err != nil {
fmt.Println(err)
}
s.StationsCache = stationResponse.Stations
return s.StationsCache
}
// GetNearestStation gets the nearest station to a given set of coordinates.
func (s *Client) GetNearestStation(coords lib.GeoCoordinates) *StationDistance {
result := &StationDistance{Distance: -1.0, From: coords}
stations := s.GetStations(false)
var wg sync.WaitGroup
if len(stations) == 0 {
return result
}
// Either MaxStationsSearchChunkSize or half the size of the stations list rounded up
chunkSize := math.Min(MaxStationsSearchChunkSize, math.Ceil(float64(len(stations))/2))
// Length is the number of goroutines that will be spawned
routineCount := int(math.Ceil(float64(len(stations)) / chunkSize))
c := make(chan *StationDistance, routineCount)
for i := 0; i < routineCount; i++ {
start := int(float64(i) * chunkSize)
end := int(math.Min(float64(len(stations)), float64(i+1)*chunkSize))
wg.Add(1)
go s.findNearestStation(&wg, c, coords, stations[start:end])
}
wg.Wait()
close(c)
for item := range c {
if result.Distance == -1 || result.Distance > item.Distance {
result = item
}
}
return result
}
func (s *Client) findNearestStation(
wg *sync.WaitGroup,
c chan *StationDistance,
coords lib.GeoCoordinates,
stations []Station,
) {
defer wg.Done()
result := &StationDistance{Distance: -1.0, From: coords}
for _, station := range stations {
distance := lib.HarvesineDistance(coords, lib.GeoCoordinates{
Lat: station.Metadata.Location.Lat,
Long: station.Metadata.Location.Long,
})
if result.Distance < 0 || result.Distance > distance {
result.Station = station
result.Distance = distance
}
}
c <- result
}