-
Notifications
You must be signed in to change notification settings - Fork 3
/
buoy.go
221 lines (185 loc) · 5.5 KB
/
buoy.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
package buoy
import (
"encoding/json"
"fmt"
"hash/fnv"
"regexp"
"strconv"
"strings"
"github.com/barbacbd/nautical/pkg/io"
"github.com/barbacbd/nautical/pkg/location"
"github.com/anaskhan96/soup"
)
var (
// NauticalRegex is a regular expression to find values between the parentheses
NauticalRegex = regexp.MustCompile(`\((.*?)\)`)
// aliasMap provides a faster lookup than a list of strings to compare
aliasMap = map[string]bool{
"gst": true,
"wvht": true,
"dpd": true,
"apd": true,
"pres": true,
"atmp": true,
"wtmp": true,
"dewp": true,
"sal": true,
"vis": true,
"tide": true,
"swd": true,
"swh": true,
"swp": true,
"wwh": true,
"wwp": true,
"wwd": true,
"wspd": true,
"steepness": true,
}
)
// Buoy represents a NOAA buoy.
type Buoy struct {
// Station is the station ID or ID of the buoy
Station string `json:"station"`
// Description is the string description for the Buoy
// +optional
Description string `json:"description,omitempty"`
// Location is the geographical location of the buoy
// +optional
Location location.Point `json:"location,omitempty"`
// Present is the current BuoyData associated with this Buoy
// +optional
Present *BuoyData `json:"data,omitempty"`
// Past is a list of BuoyData structs that are from the Past
// Deprecated: the past variable is still provided for user assistance, but
// the value is considered deprecated and abandoned
// +optional
Past []*BuoyData `json:"past,omitempty"`
// Determines whether the data stored in this stuct is considered valid
// +optional
Valid bool `json:"valid,omitempty"`
}
// Placemark is the xml structure for a Buoy. It is a very different
// format than the normal Buoy
type Placemark struct {
// Name is the name tag in the Placemark that should match the
// name of a Buoy
Name string `xml:"name"`
// Description is the description tag of the Placemark that should
// match the description of the Buoy
Description string `xml:"description"`
// Placement is the location of the buoy
Placement location.Point `xml:"LookAt"`
}
// Hash returns the hashed string as an integer from Buoy
func (b *Buoy) Hash() uint64 {
hash := fnv.New64a()
var hashString string
if b.Description != "" {
hashString = fmt.Sprintf("%s %s", b.Station, b.Description)
} else {
hashString = b.Station
}
hash.Write([]byte(hashString))
return hash.Sum64()
}
// SetData sets the current data and moves the current data to the past
func (b *Buoy) SetData(data *BuoyData) error {
if b.Present != nil {
dataEpoch, err := data.EpochTime()
if err != nil {
return nil
}
presentEpoch, err := b.Present.EpochTime()
if err != nil {
return nil
}
if dataEpoch <= presentEpoch {
return fmt.Errorf("failed to set data, epoch time is older than present data")
}
b.Past = append(b.Past, b.Present)
}
b.Present = data
return nil
}
// CreateBuoy will provide a full workup for a specific buoy. When the
// buoy with matching station is not found, an error is returned.
func CreateBuoy(stationID string) (*Buoy, error) {
station := &Buoy{Station: stationID}
err := station.FillBuoy()
if err != nil {
return nil, err
}
if !station.Valid {
return nil, fmt.Errorf("buoy %s is not valid", stationID)
}
return station, nil
}
// FillBuoy will fill a Buoy struct with the data parsed from the web
func (b *Buoy) FillBuoy() error {
url := io.GetNOAAForecastURL(b.Station)
root, err := io.GetURLSource(url)
if err != nil {
return err
}
search := []string{fmt.Sprintf("Conditions at %s", b.Station), "Detailed Wave Summary"}
if err := b.GetCurrentData(root, search); err != nil {
return err
}
return nil
}
// GetCurrentData parses the current data for a buoy from the NOAA website
func (b *Buoy) GetCurrentData(root *soup.Root, search []string) error {
buoyVariablesSet := false
tables := map[string]soup.Root{}
// Find all tables that have a caption that matches any of
// the search criteria
suspectTables := root.FindAll("table")
for _, suspect := range suspectTables {
captions := suspect.FindAll("caption")
captionText := []string{}
for _, caption := range captions {
captionText = append(captionText, caption.Text())
}
for _, searchText := range search {
for _, ct := range captionText {
if strings.Contains(ct, searchText) {
if _, ok := tables[ct]; !ok {
tables[ct] = suspect
}
}
}
}
}
for _, table := range tables {
allTR := table.FindAll("tr")
for i, row := range allTR {
if i >= 1 {
cells := row.FindAll("td")
if len(cells) > 2 {
// for idx, cell := range cells {
submatchall := NauticalRegex.FindAllString(cells[1].Text(), -1)
if len(submatchall) > 0 {
// Trim the () off of the value so that we can use the variable name
alias := strings.Trim(strings.Trim(strings.ToLower(submatchall[0]), "("), ")")
// Make sure that this is data that we are expecting
if _, found := aliasMap[alias]; found {
splitCell := RemoveEmpty(strings.Split(cells[2].Text(), " "))
val, err := strconv.ParseFloat(splitCell[0], 64)
if err != nil {
json.Unmarshal([]byte(fmt.Sprintf("{\"%s\": \"%s\"}", alias, splitCell[0])), b.Present)
} else {
json.Unmarshal([]byte(fmt.Sprintf("{\"%s\": %f}", alias, val)), b.Present)
}
buoyVariablesSet = true
}
}
}
}
}
}
b.Valid = buoyVariablesSet
if !buoyVariablesSet {
return fmt.Errorf("no buoy variables set")
}
return nil
}