-
Notifications
You must be signed in to change notification settings - Fork 3
/
source.go
255 lines (221 loc) · 6.35 KB
/
source.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
package buoy
import (
"encoding/xml"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"sort"
"strings"
)
const (
ALL = 0
INTERNATIONAL_PARTNERS = 1
IOOS_PARTNERS = 2
MARINE_METAR = 3
NDBC_METEOROLOGICAL_OCEAN = 4
NERRS = 5
NOS_CO_OPS = 6
SHIPS = 7
TAO = 8
TSUNAMI = 9
// NOAASourcesURL is the URL where all of the source data is supplied as KML
NOAASourcesURL = "https://www.ndbc.noaa.gov/kml/marineobs_as_kml.php?sort=pgm"
expectedFile = "marineobs_as_kml.php"
)
var (
SourceTypeMap = map[int]string{
INTERNATIONAL_PARTNERS: "International Partners",
IOOS_PARTNERS: "IOOS Partners",
MARINE_METAR: "Marine METAR",
NDBC_METEOROLOGICAL_OCEAN: "NDBC Meteorological/Ocean",
NERRS: "NERRS",
NOS_CO_OPS: "NOS/CO-OPS",
SHIPS: "Ships",
TAO: "TAO",
TSUNAMI: "Tsunami",
}
)
// XMLDoc assists in the xml/kml parsing. The struct represents the entry point of the document.
type XMLDoc struct {
Sources []Source `xml:"Document>Folder>Folder"`
}
// Source represents a group of buoys. The Source also contains details about
// the grouping such as a description of why/how the buoys are grouped.
type Source struct {
// Name of the Source corresponding to Source Type
Name string `json:"name" xml:"name"`
// Description for the Source
Description string `json:"description,omitempty" xml:"description"`
// Map of Buoys that are contained or associated with[in] this Source
Buoys map[uint64]*Buoy `json:"buoys,omitempty"`
// Placemarks are the xml version of the Buoy information. These
// require additional parsing, so they are NOT interchangeable with
// the Buoy Structs
Placemarks []Placemark `xml:"Placemark"`
}
// SourceTypeAsString will convert the source type to a readable string.
// In the event that ALL is passed, then all strings are returned as a single
// string with a comma as a delimiter
func SourceTypeAsString(sourceType int) (string, error) {
if typeStr, ok := SourceTypeMap[sourceType]; ok {
return typeStr, nil
} else {
if sourceType == ALL {
typeStrs := make([]string, 0, len(SourceTypeMap)-1)
for key, value := range SourceTypeMap {
if key == sourceType {
continue
} else {
typeStrs = append(typeStrs, value)
}
}
sort.Strings(typeStrs)
return strings.Join(typeStrs, ", "), nil
}
}
return "", fmt.Errorf("failed to find SourceType: %d", sourceType)
}
// String returns the string representation of the Source
func (s *Source) String() string {
return s.Name
}
// Contains determines if the hash of the buoy is contained in this struct.
func (s *Source) Contains(hash uint64) bool {
_, ok := s.Buoys[hash]
return ok
}
// GetBuoys will return all Buoy structs. This is not the same as the Map, as it
// returns a list of Buoy Structs without the keys in the map
func (s *Source) GetBuoys() []*Buoy {
buoys := make([]*Buoy, 0, len(s.Buoys)-1)
for _, value := range s.Buoys {
buoys = append(buoys, value)
}
return buoys
}
// AddBuoy will add a buoy to the struct
func (s *Source) AddBuoy(buoy *Buoy) error {
hash := buoy.Hash()
if s.Buoys == nil {
s.Buoys = make(map[uint64]*Buoy)
}
if s.Contains(hash) {
return fmt.Errorf("buoy already exists: %s", buoy.Station)
}
s.Buoys[hash] = buoy
return nil
}
// GetBuoy will get a buoy from the station Id of the Buoy
func (s *Source) GetBuoy(station string) (*Buoy, error) {
for _, value := range s.Buoys {
if value.Station == station {
return value, nil
}
}
return nil, fmt.Errorf("failed to find buoy with station: %s", station)
}
// GetBuoySources will parse and create sources from the NOAA link:
// https://www.ndbc.noaa.gov/kml/marineobs_by_pgm.kml
func GetBuoySources() ([]*Source, error) {
filename, err := downloadSourcesFile()
if err != nil {
return nil, err
}
xmlFile, err := os.Open(filename)
if err != nil {
return nil, err
}
defer xmlFile.Close()
byteValue, _ := ioutil.ReadAll(xmlFile)
var myXMLDoc XMLDoc
xml.Unmarshal(byteValue, &myXMLDoc)
sources := []*Source{}
for _, source := range myXMLDoc.Sources {
s := &source
for _, pm := range s.Placemarks {
buoy, err := ParseCData(pm.Description)
if err != nil {
continue
}
buoy.Station = pm.Name
buoy.Description = fmt.Sprintf("KML Parsed Buoy information for %s", pm.Description)
if err := s.AddBuoy(buoy); err != nil {
continue
}
}
}
if err := removeSourcesFile(filename); err != nil {
return sources, err
}
return sources, nil
}
// FilterSourcesByType filters out the unwanted source types
func FilterSourcesByType(sources []*Source, sourceType int) ([]*Source, error) {
sourceTypeMap := map[string]bool{}
if sourceType == ALL {
for _, value := range SourceTypeMap {
sourceTypeMap[value] = true
}
} else {
strType, err := SourceTypeAsString(sourceType)
if err != nil {
return nil, err
}
sourceTypeMap[strType] = true
}
newSources := []*Source{}
for _, source := range sources {
if _, ok := sourceTypeMap[source.Name]; ok {
newSources = append(newSources, source)
}
}
return newSources, nil
}
// downloadSourcesFile downloads the KML file from the url source. The file contains
// all NOAA Source information as KML.
func downloadSourcesFile() (string, error) {
// Build filename from the path
fileURL, err := url.Parse(NOAASourcesURL)
if err != nil {
return "", err
}
path := fileURL.Path
segments := strings.Split(path, "/")
filename := segments[len(segments)-1]
// Create a blank file where the data will be stored
file, err := os.Create(filename)
if err != nil {
return "", err
}
client := http.Client{
CheckRedirect: func(r *http.Request, via []*http.Request) error {
r.URL.Opaque = r.URL.Path
return nil
},
}
// Grab the data from the URL
resp, err := client.Get(NOAASourcesURL)
if err != nil {
return "", err
}
defer resp.Body.Close()
// Copy the contents from the website to the file
_, err = io.Copy(file, resp.Body)
if err != nil {
return "", err
}
defer file.Close()
return filename, nil
}
// removeSourcesFile removes the file if it exists
func removeSourcesFile(filename string) error {
if _, err := os.Stat(filename); err == nil {
if err := os.Remove(filename); err != nil {
return err
}
}
return nil
}