diff --git a/README.md b/README.md index 3f33241..7fa394f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,103 @@ -# go-lib -A template repository for Go libraries. +# WSDOT Go Client + +A Go client library for accessing Washington State Department of Transportation (WSDOT) APIs. This library provides convenient access to traffic, vessel, and camera data from WSDOT's public APIs. + +## Installation + +```bash +go get alpineworks.io/wsdot +``` + +## Quick Start + +```go +package main + +import ( + "fmt" + "os" + + "alpineworks.io/wsdot" + "alpineworks.io/wsdot/vessels" + "alpineworks.io/wsdot/traffic" +) + +func main() { + // Create client with API key + client, err := wsdot.NewWSDOTClient( + wsdot.WithAPIKey(os.Getenv("WSDOT_API_KEY")), + ) + if err != nil { + panic(err) + } + + // Use vessels API + vesselsClient, _ := vessels.NewVesselsClient(client) + stats, err := vesselsClient.GetVesselStats() + if err != nil { + panic(err) + } + fmt.Printf("Found %d vessels\n", len(stats)) + + // Use traffic API + trafficClient, _ := traffic.NewTrafficClient(client) + alerts, err := trafficClient.GetHighwayAlerts() + if err != nil { + panic(err) + } + fmt.Printf("Found %d highway alerts\n", len(alerts)) +} +``` + +## API Modules + +### Vessels +Ferry system data including schedules, vessel information, and real-time locations: +- Vessel statistics and accommodations +- Terminal locations and information +- Route schedules and sailing times +- Historical vessel positions +- Wait times and fares + +### Traffic +Road and highway information: +- Highway alerts and incidents +- Travel times between points +- Traffic Cameras +- Traffic flow data +- Weather stations and conditions +- Mountain pass conditions +- Bridge clearances +- Border crossing wait times +- Commercial vehicle restrictions +- Toll rates + +## Configuration + +The client requires a WSDOT API key, which can be obtained from the [WSDOT website](https://wsdot.wa.gov/traffic/api/). + +```go +client, err := wsdot.NewWSDOTClient( + wsdot.WithAPIKey("your-api-key"), + wsdot.WithHTTPClient(customHTTPClient), // optional +) +``` + +## Examples + +See the `example/` directory for complete working examples: +- `example/vessels/` - Ferry and vessel data examples +- `example/traffic/` - Traffic and road condition examples +- `example/fares/` - Ferry fare information examples + +Run examples with: +```bash +export WSDOT_API_KEY="your-api-key" +go run example/vessels/main.go +go run example/traffic/main.go -verbose +``` + +## Requirements + +- Go 1.22 or higher +- Valid WSDOT API key diff --git a/client.go b/client.go index f00b746..a842163 100644 --- a/client.go +++ b/client.go @@ -22,6 +22,7 @@ var ( const ( ParamCamerasAccessCodeKey = "AccessCode" ParamFerriesAccessCodeKey = "apiaccesscode" + ParamTrafficAccessCodeKey = "AccessCode" ) func NewWSDOTClient(options ...WSDOTClientOption) (*WSDOTClient, error) { diff --git a/example/cameras/main.go b/example/cameras/main.go index d48873e..5cf03a4 100644 --- a/example/cameras/main.go +++ b/example/cameras/main.go @@ -5,7 +5,7 @@ import ( "os" "alpineworks.io/wsdot" - "alpineworks.io/wsdot/cameras" + "alpineworks.io/wsdot/traffic" ) func main() { @@ -23,14 +23,14 @@ func main() { panic(err) } - // Create a new Cameras client - camerasClient, err := cameras.NewCamerasClient(wsdotClient) + // Create a new Traffic client + trafficClient, err := traffic.NewTrafficClient(wsdotClient) if err != nil { panic(err) } // Get the cameras - cameras, err := camerasClient.GetCameras() + cameras, err := trafficClient.GetCameras() if err != nil { panic(err) } @@ -42,7 +42,7 @@ func main() { } // Get a specific camera - camera, err := camerasClient.GetCamera(cameras[0].CameraID) + camera, err := trafficClient.GetCamera(cameras[0].CameraID) if err != nil { panic(err) } diff --git a/example/fares/main.go b/example/fares/main.go new file mode 100644 index 0000000..3b1a5d0 --- /dev/null +++ b/example/fares/main.go @@ -0,0 +1,230 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + "strconv" + "time" + + "alpineworks.io/wsdot" + "alpineworks.io/wsdot/vessels" +) + +// prettyPrint outputs a struct as indented JSON if verbose is true +func prettyPrint(v interface{}, verbose bool, label string) { + if verbose { + fmt.Printf("\n--- %s (Full JSON) ---\n", label) + jsonBytes, err := json.MarshalIndent(v, "", " ") + if err != nil { + fmt.Printf("Error marshaling JSON: %v\n", err) + return + } + fmt.Println(string(jsonBytes)) + fmt.Println("--- End JSON ---") + } +} + +func main() { + // Parse command line flags + verbose := flag.Bool("verbose", false, "Print full JSON structures for all responses") + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: %s [options]\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "Options:\n") + flag.PrintDefaults() + fmt.Fprintf(os.Stderr, "\nExample:\n") + fmt.Fprintf(os.Stderr, " %s # Run with summary output\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " %s -verbose # Run with full JSON output\n", os.Args[0]) + } + flag.Parse() + + apiKey := os.Getenv("API_KEY") + if apiKey == "" { + panic("API_KEY environment variable is required") + } + + // Create a new WSDOT client + wsdotClient, err := wsdot.NewWSDOTClient( + wsdot.WithAPIKey(apiKey), + ) + + if err != nil { + panic(err) + } + + // Create a single unified vessels client that handles vessels, terminals, and fares + vesselsClient, err := vessels.NewVesselsClient(wsdotClient) + if err != nil { + panic(err) + } + + fmt.Println("=== Testing Unified Vessels Client (Terminals and Fares) ===") + + // 1. Test Cache Flush Date + fmt.Println("\n1. Cache Flush Date:") + cacheDate, err := vesselsClient.GetCacheFlushDate() + if err != nil { + fmt.Printf("Error getting cache flush date: %v\n", err) + } else { + fmt.Printf("Cache flushed at: %v\n", cacheDate) + prettyPrint(cacheDate, *verbose, "Cache Flush Date") + } + + // 2. Test Terminal Basics + fmt.Println("\n2. Terminal Basics:") + terminalBasics, err := vesselsClient.GetTerminalBasics() + if err != nil { + fmt.Printf("Error getting terminal basics: %v\n", err) + } else { + fmt.Printf("Found %d terminals\n", len(terminalBasics)) + if len(terminalBasics) > 0 { + fmt.Printf("First terminal: %s (%s) - Reservable: %t\n", + terminalBasics[0].TerminalName, terminalBasics[0].TerminalAbbrev, + terminalBasics[0].Reservable) + } + prettyPrint(terminalBasics, *verbose, "Terminal Basics") + } + + // 3. Test Terminal Wait Times + fmt.Println("\n3. Terminal Wait Times:") + waitTimes, err := vesselsClient.GetTerminalWaitTimes() + if err != nil { + fmt.Printf("Error getting terminal wait times: %v\n", err) + } else { + fmt.Printf("Found %d terminal wait times\n", len(waitTimes)) + for i, waitTime := range waitTimes { + if i >= 3 { // Show only first 3 to avoid too much output + fmt.Printf("... and %d more terminals\n", len(waitTimes)-3) + break + } + fmt.Printf("- %s (%s): %d minutes\n", + waitTime.TerminalName, waitTime.TerminalAbbrev, waitTime.WaitTime) + } + prettyPrint(waitTimes, *verbose, "Terminal Wait Times") + } + + // 4. Test Terminal Locations + fmt.Println("\n4. Terminal Locations:") + locations, err := vesselsClient.GetTerminalLocations() + if err != nil { + fmt.Printf("Error getting terminal locations: %v\n", err) + } else { + fmt.Printf("Found %d terminal locations\n", len(locations)) + if len(locations) > 0 { + fmt.Printf("Example: %s at %.4f°N, %.4f°W\n", + locations[0].TerminalName, locations[0].Latitude, locations[0].Longitude) + } + prettyPrint(locations, *verbose, "Terminal Locations") + } + + // 5. Test Fare Terminals (using tomorrow's date) + fmt.Println("\n5. Fare Terminals:") + tomorrow := time.Now().AddDate(0, 0, 1).Format("2006-01-02") + fareTerminals, err := vesselsClient.GetTerminals(tomorrow) + if err != nil { + fmt.Printf("Error getting fare terminals: %v\n", err) + } else { + fmt.Printf("Found %d fare terminals for %s\n", len(fareTerminals), tomorrow) + if len(fareTerminals) > 0 { + fmt.Printf("Example: %s (%s) - Region: %d\n", + fareTerminals[0].TerminalName, fareTerminals[0].TerminalAbbrev, + fareTerminals[0].Region) + } + prettyPrint(fareTerminals, *verbose, "Fare Terminals") + } + + // 7. Test Terminal Mates (if we have terminals) + if len(fareTerminals) > 0 { + fmt.Println("\n7. Terminal Mates:") + firstTerminalID := fareTerminals[0].TerminalID + mates, err := vesselsClient.GetTerminalMates(tomorrow, firstTerminalID) + if err != nil { + fmt.Printf("Error getting terminal mates for terminal %d: %v\n", firstTerminalID, err) + } else { + fmt.Printf("Terminal %s can reach %d destinations\n", + fareTerminals[0].TerminalName, len(mates)) + for i, mate := range mates { + if i >= 3 { // Show only first 3 + fmt.Printf("... and %d more destinations\n", len(mates)-3) + break + } + fmt.Printf("- %s (%s)\n", mate.TerminalName, mate.TerminalAbbrev) + } + prettyPrint(mates, *verbose, "Terminal Mates") + } + + // 8. Test Fare Line Items (if we have terminal mates) + if len(mates) > 0 { + fmt.Println("\n8. Fare Line Items:") + departingID := strconv.Itoa(firstTerminalID) + arrivingID := strconv.Itoa(mates[0].TerminalID) + + fareItems, err := vesselsClient.GetFareLineItemsBasic(tomorrow, departingID, arrivingID, false) + if err != nil { + fmt.Printf("Error getting fare line items: %v\n", err) + } else { + fmt.Printf("Found %d fare options from %s to %s\n", + len(fareItems), fareTerminals[0].TerminalName, mates[0].TerminalName) + for i, item := range fareItems { + if i >= 5 { // Show only first 5 + fmt.Printf("... and %d more fare options\n", len(fareItems)-5) + break + } + fmt.Printf("- %s: $%.2f\n", item.FareLineItemDescription, item.Cost) + } + prettyPrint(fareItems, *verbose, "Fare Line Items") + + // 9. Test Fare Total (for first fare item) + if len(fareItems) > 0 { + fmt.Println("\n9. Fare Total Calculation:") + fareTotal, err := vesselsClient.GetFareTotal(tomorrow, departingID, arrivingID, false, + fareItems[0].FareLineItemID, 1) + if err != nil { + fmt.Printf("Error getting fare total: %v\n", err) + } else { + fmt.Printf("Total cost for 1x %s: $%.2f\n", + fareTotal.Description, fareTotal.TotalCost) + prettyPrint(fareTotal, *verbose, "Fare Total") + } + } + } + } + } + + // 10. Test specific terminal by ID (if we have terminals) + if len(terminalBasics) > 0 { + fmt.Println("\n10. Specific Terminal Details:") + terminalID := terminalBasics[0].TerminalID + + // Get specific terminal basic info + terminalDetail, err := vesselsClient.GetTerminalBasicByID(terminalID) + if err != nil { + fmt.Printf("Error getting terminal details for ID %d: %v\n", terminalID, err) + } else { + fmt.Printf("Terminal %s details: %s, %s %s\n", + terminalDetail.TerminalName, terminalDetail.City, + terminalDetail.State, terminalDetail.ZipCode) + } + + // Get specific terminal wait time + terminalWaitTime, err := vesselsClient.GetTerminalWaitTimeByID(terminalID) + if err != nil { + fmt.Printf("Error getting terminal wait time for ID %d: %v\n", terminalID, err) + } else { + fmt.Printf("Current wait time at %s: %d minutes\n", + terminalWaitTime.TerminalName, terminalWaitTime.WaitTime) + } + + // Get specific terminal location + terminalLocation, err := vesselsClient.GetTerminalLocationByID(terminalID) + if err != nil { + fmt.Printf("Error getting terminal location for ID %d: %v\n", terminalID, err) + } else { + fmt.Printf("Terminal %s coordinates: %.4f°N, %.4f°W\n", + terminalLocation.TerminalName, terminalLocation.Latitude, terminalLocation.Longitude) + } + } + + fmt.Println("\n=== All unified vessels client tests completed! ===") +} \ No newline at end of file diff --git a/example/traffic/main.go b/example/traffic/main.go new file mode 100644 index 0000000..dd8d15f --- /dev/null +++ b/example/traffic/main.go @@ -0,0 +1,208 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + + "alpineworks.io/wsdot" + "alpineworks.io/wsdot/traffic" +) + +// prettyPrint outputs a struct as indented JSON if verbose is true +func prettyPrint(v interface{}, verbose bool, label string) { + if verbose { + fmt.Printf("\n--- %s (Full JSON) ---\n", label) + jsonBytes, err := json.MarshalIndent(v, "", " ") + if err != nil { + fmt.Printf("Error marshaling JSON: %v\n", err) + return + } + fmt.Println(string(jsonBytes)) + fmt.Println("--- End JSON ---") + } +} + +func main() { + // Parse command line flags + verbose := flag.Bool("verbose", false, "Print full JSON structures for all responses") + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: %s [options]\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "Options:\n") + flag.PrintDefaults() + fmt.Fprintf(os.Stderr, "\nExample:\n") + fmt.Fprintf(os.Stderr, " %s # Run with summary output\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " %s -verbose # Run with full JSON output\n", os.Args[0]) + } + flag.Parse() + + apiKey := os.Getenv("API_KEY") + if apiKey == "" { + panic("API_KEY environment variable is required") + } + + // Create a new WSDOT client + wsdotClient, err := wsdot.NewWSDOTClient( + wsdot.WithAPIKey(apiKey), + ) + + if err != nil { + panic(err) + } + + // Create a new Traffic client + trafficClient, err := traffic.NewTrafficClient(wsdotClient) + if err != nil { + panic(err) + } + + fmt.Println("=== Testing All Traffic API Endpoints ===") + + // 1. Test Border Crossings + fmt.Println("\n1. Border Crossings:") + borderCrossings, err := trafficClient.GetBorderCrossings() + if err != nil { + fmt.Printf("Error getting border crossings: %v\n", err) + } else { + fmt.Printf("Found %d border crossings\n", len(borderCrossings)) + if len(borderCrossings) > 0 { + fmt.Printf("Example: %s - Wait time: %d minutes\n", + borderCrossings[0].CrossingName, borderCrossings[0].WaitTime) + } + prettyPrint(borderCrossings, *verbose, "Border Crossings") + } + + // 2. Test Bridge Clearances + fmt.Println("\n2. Bridge Clearances:") + bridgeClearances, err := trafficClient.GetBridgeClearances() + if err != nil { + fmt.Printf("Error getting bridge clearances: %v\n", err) + } else { + fmt.Printf("Found %d bridge clearances\n", len(bridgeClearances)) + if len(bridgeClearances) > 0 { + fmt.Printf("Example: %s - Min clearance: %d inches\n", + bridgeClearances[0].CrossingDescription, + bridgeClearances[0].VerticalClearanceMinimumInches) + } + prettyPrint(bridgeClearances, *verbose, "Bridge Clearances") + } + + // 3. Test Commercial Vehicle Restrictions + fmt.Println("\n3. Commercial Vehicle Restrictions:") + cvRestrictions, err := trafficClient.GetCommercialVehicleRestrictions() + if err != nil { + fmt.Printf("Error getting CV restrictions: %v\n", err) + } else { + fmt.Printf("Found %d commercial vehicle restrictions\n", len(cvRestrictions)) + if len(cvRestrictions) > 0 { + fmt.Printf("Example: %s - %s\n", + cvRestrictions[0].LocationName, cvRestrictions[0].RestrictionType) + } + prettyPrint(cvRestrictions, *verbose, "Commercial Vehicle Restrictions") + } + + // 4. Test Highway Alerts + fmt.Println("\n4. Highway Alerts:") + alerts, err := trafficClient.GetHighwayAlerts() + if err != nil { + fmt.Printf("Error getting highway alerts: %v\n", err) + } else { + fmt.Printf("Found %d highway alerts\n", len(alerts)) + if len(alerts) > 0 { + fmt.Printf("Example: %s - %s\n", + alerts[0].HeadlineDescription, alerts[0].EventCategory) + } + prettyPrint(alerts, *verbose, "Highway Alerts") + } + + // 5. Test Mountain Pass Conditions + fmt.Println("\n5. Mountain Pass Conditions:") + passConditions, err := trafficClient.GetMountainPassConditions() + if err != nil { + fmt.Printf("Error getting mountain pass conditions: %v\n", err) + } else { + fmt.Printf("Found %d mountain pass conditions\n", len(passConditions)) + if len(passConditions) > 0 { + fmt.Printf("Example: %s - %s, %d°F\n", + passConditions[0].MountainPassName, + passConditions[0].WeatherCondition, + passConditions[0].TemperatureInFahrenheit) + } + prettyPrint(passConditions, *verbose, "Mountain Pass Conditions") + } + + // 6. Test Toll Rates + fmt.Println("\n6. Toll Rates:") + tollRates, err := trafficClient.GetTollRates() + if err != nil { + fmt.Printf("Error getting toll rates: %v\n", err) + } else { + fmt.Printf("Found %d toll rates\n", len(tollRates)) + if len(tollRates) > 0 { + fmt.Printf("Example: %s - $%.2f\n", + tollRates[0].TripName, tollRates[0].CurrentToll) + } + prettyPrint(tollRates, *verbose, "Toll Rates") + } + + // 7. Test Traffic Flow + fmt.Println("\n7. Traffic Flow:") + trafficFlow, err := trafficClient.GetTrafficFlow() + if err != nil { + fmt.Printf("Error getting traffic flow: %v\n", err) + } else { + fmt.Printf("Found %d traffic flow readings\n", len(trafficFlow)) + if len(trafficFlow) > 0 { + fmt.Printf("Example: %s - Flow: %v\n", + trafficFlow[0].StationName, trafficFlow[0].FlowReadingValue) + } + prettyPrint(trafficFlow, *verbose, "Traffic Flow") + } + + // 8. Test Travel Times + fmt.Println("\n8. Travel Times:") + travelTimes, err := trafficClient.GetTravelTimes() + if err != nil { + fmt.Printf("Error getting travel times: %v\n", err) + } else { + fmt.Printf("Found %d travel time routes\n", len(travelTimes)) + if len(travelTimes) > 0 { + fmt.Printf("Example: %s - Current: %d min, Average: %d min\n", + travelTimes[0].Name, travelTimes[0].CurrentTime, travelTimes[0].AverageTime) + } + prettyPrint(travelTimes, *verbose, "Travel Times") + } + + // 9. Test Weather Information + fmt.Println("\n9. Weather Information:") + weatherInfo, err := trafficClient.GetCurrentWeatherInformation() + if err != nil { + fmt.Printf("Error getting weather information: %v\n", err) + } else { + fmt.Printf("Found %d weather stations\n", len(weatherInfo)) + if len(weatherInfo) > 0 { + fmt.Printf("Example: %s - %.1f°F, Wind: %.1f mph %s\n", + weatherInfo[0].StationName, weatherInfo[0].TemperatureInFahrenheit, + weatherInfo[0].WindSpeedInMPH, weatherInfo[0].WindDirectionCardinal) + } + prettyPrint(weatherInfo, *verbose, "Weather Information") + } + + // 10. Test Weather Stations + fmt.Println("\n10. Weather Stations:") + weatherStations, err := trafficClient.GetWeatherStations() + if err != nil { + fmt.Printf("Error getting weather stations: %v\n", err) + } else { + fmt.Printf("Found %d weather stations\n", len(weatherStations)) + if len(weatherStations) > 0 { + fmt.Printf("Example: %s at %.4f°N, %.4f°W\n", + weatherStations[0].StationName, + weatherStations[0].Latitude, weatherStations[0].Longitude) + } + prettyPrint(weatherStations, *verbose, "Weather Stations") + } + + fmt.Println("\n=== All Traffic endpoint tests completed! ===") +} \ No newline at end of file diff --git a/example/vessels/main.go b/example/vessels/main.go index b936891..8f2cb71 100644 --- a/example/vessels/main.go +++ b/example/vessels/main.go @@ -1,14 +1,43 @@ package main import ( + "encoding/json" + "flag" "fmt" "os" + "time" "alpineworks.io/wsdot" - "alpineworks.io/wsdot/ferries" + "alpineworks.io/wsdot/vessels" ) +// prettyPrint outputs a struct as indented JSON if verbose is true +func prettyPrint(v interface{}, verbose bool, label string) { + if verbose { + fmt.Printf("\n--- %s (Full JSON) ---\n", label) + jsonBytes, err := json.MarshalIndent(v, "", " ") + if err != nil { + fmt.Printf("Error marshaling JSON: %v\n", err) + return + } + fmt.Println(string(jsonBytes)) + fmt.Println("--- End JSON ---") + } +} + func main() { + // Parse command line flags + verbose := flag.Bool("verbose", false, "Print full JSON structures for all responses") + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: %s [options]\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "Options:\n") + flag.PrintDefaults() + fmt.Fprintf(os.Stderr, "\nExample:\n") + fmt.Fprintf(os.Stderr, " %s # Run with summary output\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " %s -verbose # Run with full JSON output\n", os.Args[0]) + } + flag.Parse() + apiKey := os.Getenv("API_KEY") if apiKey == "" { panic("API_KEY environment variable is required") @@ -23,48 +52,151 @@ func main() { panic(err) } - // Create a new Ferries client - ferriesClient, err := ferries.NewFerriesClient(wsdotClient) + // Create a new Vessels client + vesselsClient, err := vessels.NewVesselsClient(wsdotClient) if err != nil { panic(err) } - // Get the vessel basics - vessels, err := ferriesClient.GetVesselBasics() + fmt.Println("=== Testing All Vessels API Endpoints ===") + + // 1. Test Cache Flush Date + fmt.Println("\n1. Cache Flush Date:") + cacheDate, err := vesselsClient.GetCacheFlushDate() if err != nil { - panic(err) + fmt.Printf("Error getting cache flush date: %v\n", err) + } else { + fmt.Printf("Cache flushed at: %v\n", cacheDate) + prettyPrint(cacheDate, *verbose, "Cache Flush Date") } - if len(vessels) > 0 { - fmt.Println(vessels[0].VesselName) - fmt.Println(vessels[0].VesselID) - fmt.Println(vessels[0].Class.ClassName) + // 2. Test Vessel Stats (all vessels) + fmt.Println("\n2. Vessel Stats (all):") + vesselStats, err := vesselsClient.GetVesselStats() + if err != nil { + fmt.Printf("Error getting vessel stats: %v\n", err) + } else if len(vesselStats) > 0 { + fmt.Printf("Found %d vessels\n", len(vesselStats)) + fmt.Printf("First vessel: %s (ID: %d, Class: %s)\n", + vesselStats[0].VesselName, vesselStats[0].VesselID, vesselStats[0].Class.ClassName) + prettyPrint(vesselStats, *verbose, "All Vessel Stats") + + // 3. Test Vessel Stats by ID (using first vessel) + fmt.Println("\n3. Vessel Stats by ID:") + vesselStat, err := vesselsClient.GetVesselStatsByID(vesselStats[0].VesselID) + if err != nil { + fmt.Printf("Error getting vessel stats by ID: %v\n", err) + } else { + fmt.Printf("Vessel: %s (Built: %d, Length: %s)\n", vesselStat.VesselName, vesselStat.YearBuilt, vesselStat.Length) + prettyPrint(vesselStat, *verbose, "Single Vessel Stats") + } } - // Get the vessel locations - vesselLocations, err := ferriesClient.GetVesselLocations() + // 4. Test Terminal Locations (all terminals) + fmt.Println("\n4. Terminal Locations (all):") + terminalLocations, err := vesselsClient.GetTerminalLocations() if err != nil { - panic(err) + fmt.Printf("Error getting terminal locations: %v\n", err) + } else if len(terminalLocations) > 0 { + fmt.Printf("Found %d terminal locations\n", len(terminalLocations)) + for i, loc := range terminalLocations { + if i >= 3 { // Show only first 3 to avoid too much output + fmt.Printf("... and %d more terminals\n", len(terminalLocations)-3) + break + } + fmt.Printf("- %s: %.4f°N, %.4f°W\n", + loc.TerminalName, loc.Latitude, loc.Longitude) + } + + // 5. Test Terminal Location by ID (using first terminal) + if len(terminalLocations) > 0 { + fmt.Println("\n5. Terminal Location by ID:") + terminalLocation, err := vesselsClient.GetTerminalLocationByID(terminalLocations[0].TerminalID) + if err != nil { + fmt.Printf("Error getting terminal location by ID: %v\n", err) + } else { + fmt.Printf("%s: %s\n", + terminalLocation.TerminalName, terminalLocation.TerminalAbbrev) + } + } } - if len(vesselLocations) > 0 { - fmt.Println(vesselLocations[1].VesselName) - fmt.Printf("%f°N, %f°W\n", vesselLocations[1].Latitude, vesselLocations[1].Longitude) + // 6. Test Vessel Accommodations (all vessels) + fmt.Println("\n6. Vessel Accommodations (all):") + accommodations, err := vesselsClient.GetVesselAccommodations() + if err != nil { + fmt.Printf("Error getting vessel accommodations: %v\n", err) + } else if len(accommodations) > 0 { + fmt.Printf("Found %d vessel accommodations\n", len(accommodations)) + fmt.Printf("Example - %s: Car deck restroom: %t, Public WiFi: %t, ADA: %t\n", + accommodations[0].VesselName, accommodations[0].CarDeckRestroom, + accommodations[0].PublicWifi, accommodations[0].ADAAccessible) + prettyPrint(accommodations, *verbose, "All Vessel Accommodations") + + // 7. Test Vessel Accommodation by ID + fmt.Println("\n7. Vessel Accommodation by ID:") + accommodation, err := vesselsClient.GetVesselAccommodationByID(accommodations[0].VesselID) + if err != nil { + fmt.Printf("Error getting accommodation by ID: %v\n", err) + } else { + fmt.Printf("%s: Main cabin galley: %t, Car deck shelter: %t, Elevator: %t\n", + accommodation.VesselName, accommodation.MainCabinGalley, + accommodation.CarDeckShelter, accommodation.Elevator) + } } - // get the route schedule - schedules, err := ferriesClient.GetRouteSchedules() + // 8. Test Vessel History (all vessels) + fmt.Println("\n8. Vessel History (all):") + history, err := vesselsClient.GetVesselHistory() if err != nil { - panic(err) + fmt.Printf("Error getting vessel history: %v\n", err) + } else { + fmt.Printf("Found %d historical records\n", len(history)) + if len(history) > 0 { + fmt.Printf("Latest record: %s at %.4f°N, %.4f°W\n", + history[0].VesselName, history[0].Latitude, history[0].Longitude) + } } - for _, schedule := range schedules { - fmt.Printf("Schedule ID: %d, Route ID: %d, Description: %s\n", schedule.ScheduleID, schedule.RouteID, schedule.Description) - allSailings, err := ferriesClient.GetSchedulesTodayByRouteID(schedule.RouteID, false) + // 9. Test Vessel History by Name and Date Range + fmt.Println("\n9. Vessel History by Name and Date Range:") + if len(vesselStats) > 0 { + // Get history for the first vessel from yesterday to today + startDate := time.Now().AddDate(0, 0, -1) // Yesterday + endDate := time.Now() // Today + vesselName := vesselStats[0].VesselName + + historyRange, err := vesselsClient.GetVesselHistoryByNameAndDateRange(vesselName, startDate, endDate) + if err != nil { + fmt.Printf("Error getting vessel history by date range: %v\n", err) + } else { + fmt.Printf("Found %d records for %s from %s to %s\n", + len(historyRange), vesselName, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")) + } + } + + // 10. Test Route Schedules (Schedule API) + fmt.Println("\n10. Route Schedules:") + schedules, err := vesselsClient.GetRouteSchedules() + if err != nil { + fmt.Printf("Error getting route schedules: %v\n", err) + } else if len(schedules) > 0 { + fmt.Printf("Found %d route schedules\n", len(schedules)) + fmt.Printf("Example - Route %d: %s\n", schedules[0].RouteID, schedules[0].Description) + + // 11. Test Schedule Today by Route ID + fmt.Println("\n11. Today's Schedule for Route:") + allSailings, err := vesselsClient.GetSchedulesTodayByRouteID(schedules[0].RouteID, false) if err != nil { - panic(err) + fmt.Printf("Error getting today's schedule: %v\n", err) + } else { + fmt.Printf("Schedule: %s, Season: %d\n", allSailings.ScheduleName, allSailings.ScheduleSeason) + if len(allSailings.TerminalCombos) > 0 && len(allSailings.TerminalCombos[0].Times) > 0 { + lastSailing := allSailings.TerminalCombos[0].Times[len(allSailings.TerminalCombos[0].Times)-1] + fmt.Printf("Last sailing departing time: %v\n", lastSailing.DepartingTime) + } } - fmt.Printf("last sailing: %s\n", allSailings.TerminalCombos[0].Times[len(allSailings.TerminalCombos[0].Times)-1].DepartingTime) } + fmt.Println("\n=== All endpoint tests completed! ===") } diff --git a/ferries/ferries.go b/ferries/ferries.go deleted file mode 100644 index 574cc82..0000000 --- a/ferries/ferries.go +++ /dev/null @@ -1,19 +0,0 @@ -package ferries - -import ( - "alpineworks.io/wsdot" -) - -type FerriesClient struct { - wsdot *wsdot.WSDOTClient -} - -func NewFerriesClient(wsdotClient *wsdot.WSDOTClient) (*FerriesClient, error) { - if wsdotClient == nil { - return nil, wsdot.ErrNoClient - } - - return &FerriesClient{ - wsdot: wsdotClient, - }, nil -} diff --git a/traffic/border_crossings.go b/traffic/border_crossings.go new file mode 100644 index 0000000..8007eee --- /dev/null +++ b/traffic/border_crossings.go @@ -0,0 +1,61 @@ +package traffic + +import ( + "encoding/json" + "fmt" + "net/http" + + "alpineworks.io/wsdot" +) + +const ( + getBorderCrossingsAsJsonURL = "http://www.wsdot.wa.gov/Traffic/api/BorderCrossings/BorderCrossingsREST.svc/GetBorderCrossingsAsJson" +) + +// BorderCrossingLocation represents the location details of a border crossing +type BorderCrossingLocation struct { + Description string `json:"Description"` + Direction string `json:"Direction"` + Latitude float64 `json:"Latitude"` + Longitude float64 `json:"Longitude"` + MilePost int `json:"MilePost"` + RoadName string `json:"RoadName"` +} + +// BorderCrossing represents a border crossing wait time +type BorderCrossing struct { + BorderCrossingLocation BorderCrossingLocation `json:"BorderCrossingLocation"` + CrossingName string `json:"CrossingName"` + Time string `json:"Time"` + WaitTime int `json:"WaitTime"` +} + +// GetBorderCrossings retrieves all border crossing wait times +func (t *TrafficClient) GetBorderCrossings() ([]BorderCrossing, error) { + req, err := http.NewRequest(http.MethodGet, getBorderCrossingsAsJsonURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamTrafficAccessCodeKey, t.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := t.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var crossings []BorderCrossing + if err := json.NewDecoder(resp.Body).Decode(&crossings); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return crossings, nil +} \ No newline at end of file diff --git a/traffic/bridge_clearances.go b/traffic/bridge_clearances.go new file mode 100644 index 0000000..e2a0c8d --- /dev/null +++ b/traffic/bridge_clearances.go @@ -0,0 +1,66 @@ +package traffic + +import ( + "encoding/json" + "fmt" + "net/http" + + "alpineworks.io/wsdot" +) + +const ( + getBridgeClearancesAsJsonURL = "http://wsdot.wa.gov/Traffic/api/Bridges/ClearanceREST.svc/GetClearancesAsJson" +) + +// BridgeClearance represents a bridge clearance measurement from WSDOT +type BridgeClearance struct { + APILastUpdate string `json:"APILastUpdate"` + BridgeNumber string `json:"BridgeNumber"` + ControlEntityGuid string `json:"ControlEntityGuid"` + CrossingDescription string `json:"CrossingDescription"` + CrossingLocationId int `json:"CrossingLocationId"` + CrossingRecordGuid string `json:"CrossingRecordGuid"` + InventoryDirection string `json:"InventoryDirection"` + Latitude float64 `json:"Latitude"` + LocationGuid string `json:"LocationGuid"` + Longitude float64 `json:"Longitude"` + RouteDate string `json:"RouteDate"` + SRMP float64 `json:"SRMP"` + SRMPAheadBackIndicator string `json:"SRMPAheadBackIndicator"` + StateRouteID string `json:"StateRouteID"` + StateStructureId string `json:"StateStructureId"` + VerticalClearanceMaximumFeetInch string `json:"VerticalClearanceMaximumFeetInch"` + VerticalClearanceMaximumInches int `json:"VerticalClearanceMaximumInches"` + VerticalClearanceMinimumFeetInch string `json:"VerticalClearanceMinimumFeetInch"` + VerticalClearanceMinimumInches int `json:"VerticalClearanceMinimumInches"` +} + +// GetBridgeClearances retrieves all bridge clearance information +func (t *TrafficClient) GetBridgeClearances() ([]BridgeClearance, error) { + req, err := http.NewRequest(http.MethodGet, getBridgeClearancesAsJsonURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamTrafficAccessCodeKey, t.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := t.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var clearances []BridgeClearance + if err := json.NewDecoder(resp.Body).Decode(&clearances); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return clearances, nil +} \ No newline at end of file diff --git a/cameras/cameras.go b/traffic/cameras.go similarity index 82% rename from cameras/cameras.go rename to traffic/cameras.go index 8ce8e28..171aa8b 100644 --- a/cameras/cameras.go +++ b/traffic/cameras.go @@ -1,4 +1,4 @@ -package cameras +package traffic import ( "encoding/json" @@ -16,19 +16,6 @@ const ( ParamCameraID = "CameraID" ) -type CamerasClient struct { - wsdot *wsdot.WSDOTClient -} - -func NewCamerasClient(wsdotClient *wsdot.WSDOTClient) (*CamerasClient, error) { - if wsdotClient == nil { - return nil, wsdot.ErrNoClient - } - - return &CamerasClient{ - wsdot: wsdotClient, - }, nil -} type CameraLocation struct { Description any `json:"Description"` @@ -56,18 +43,18 @@ type Camera struct { Title string `json:"Title"` } -func (c *CamerasClient) GetCameras() ([]Camera, error) { +func (t *TrafficClient) GetCameras() ([]Camera, error) { req, err := http.NewRequest(http.MethodGet, getCamerasAsJsonURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %v", err) } q := req.URL.Query() - q.Add(wsdot.ParamCamerasAccessCodeKey, c.wsdot.ApiKey) + q.Add(wsdot.ParamCamerasAccessCodeKey, t.wsdot.ApiKey) req.URL.RawQuery = q.Encode() req.Header.Set("Content-Type", "application/json") - resp, err := c.wsdot.Client.Do(req) + resp, err := t.wsdot.Client.Do(req) if err != nil { return nil, fmt.Errorf("error making request: %v", err) } @@ -85,19 +72,19 @@ func (c *CamerasClient) GetCameras() ([]Camera, error) { return cameras, nil } -func (c *CamerasClient) GetCamera(cameraID int) (*Camera, error) { +func (t *TrafficClient) GetCamera(cameraID int) (*Camera, error) { req, err := http.NewRequest(http.MethodGet, getCameraAsJsonURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %v", err) } q := req.URL.Query() - q.Add(wsdot.ParamCamerasAccessCodeKey, c.wsdot.ApiKey) + q.Add(wsdot.ParamCamerasAccessCodeKey, t.wsdot.ApiKey) q.Add(ParamCameraID, strconv.Itoa(cameraID)) req.URL.RawQuery = q.Encode() req.Header.Set("Content-Type", "application/json") - resp, err := c.wsdot.Client.Do(req) + resp, err := t.wsdot.Client.Do(req) if err != nil { return nil, fmt.Errorf("error making request: %v", err) } diff --git a/traffic/commercial_vehicle_restrictions.go b/traffic/commercial_vehicle_restrictions.go new file mode 100644 index 0000000..18e7d19 --- /dev/null +++ b/traffic/commercial_vehicle_restrictions.go @@ -0,0 +1,117 @@ +package traffic + +import ( + "encoding/json" + "fmt" + "net/http" + + "alpineworks.io/wsdot" +) + +const ( + getCommercialVehicleRestrictionsAsJsonURL = "http://www.wsdot.wa.gov/Traffic/api/CVRestrictions/CVRestrictionsREST.svc/GetCommercialVehicleRestrictionsAsJson" + getCommercialVehicleRestrictionsWithIdAsJsonURL = "http://www.wsdot.wa.gov/Traffic/api/CVRestrictions/CVRestrictionsREST.svc/GetCommercialVehicleRestrictionsWithIdAsJson" +) + +// RoadwayLocation represents a location on a roadway +type RoadwayLocation struct { + Description string `json:"Description"` + Direction string `json:"Direction"` + Latitude float64 `json:"Latitude"` + Longitude float64 `json:"Longitude"` + MilePost float64 `json:"MilePost"` + RoadName string `json:"RoadName"` +} + +// CommercialVehicleRestriction represents a commercial vehicle restriction +type CommercialVehicleRestriction struct { + BLMaxAxle int `json:"BLMaxAxle"` + BridgeName string `json:"BridgeName"` + BridgeNumber string `json:"BridgeNumber"` + CL8MaxAxle int `json:"CL8MaxAxle"` + DateEffective string `json:"DateEffective"` + DateExpires string `json:"DateExpires"` + DatePosted string `json:"DatePosted"` + EndRoadwayLocation RoadwayLocation `json:"EndRoadwayLocation"` + IsDetourAvailable bool `json:"IsDetourAvailable"` + IsExceptionsAllowed bool `json:"IsExceptionsAllowed"` + IsPermanentRestriction bool `json:"IsPermanentRestriction"` + IsWarning bool `json:"IsWarning"` + Latitude float64 `json:"Latitude"` + LocationDescription string `json:"LocationDescription"` + LocationName string `json:"LocationName"` + Longitude float64 `json:"Longitude"` + MaximumGrossVehicleWeightInPounds int `json:"MaximumGrossVehicleWeightInPounds"` + RestrictionComment string `json:"RestrictionComment"` + RestrictionHeightInInches int `json:"RestrictionHeightInInches"` + RestrictionLengthInInches int `json:"RestrictionLengthInInches"` + RestrictionType string `json:"RestrictionType"` + RestrictionWeightInPounds int `json:"RestrictionWeightInPounds"` + RestrictionWidthInInches int `json:"RestrictionWidthInInches"` + SAMaxAxle int `json:"SAMaxAxle"` + StartRoadwayLocation RoadwayLocation `json:"StartRoadwayLocation"` + State string `json:"State"` + StateRouteID string `json:"StateRouteID"` + TDMaxAxle int `json:"TDMaxAxle"` + VehicleType string `json:"VehicleType"` +} + +// GetCommercialVehicleRestrictions retrieves all commercial vehicle restrictions +func (t *TrafficClient) GetCommercialVehicleRestrictions() ([]CommercialVehicleRestriction, error) { + req, err := http.NewRequest(http.MethodGet, getCommercialVehicleRestrictionsAsJsonURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamTrafficAccessCodeKey, t.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := t.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var restrictions []CommercialVehicleRestriction + if err := json.NewDecoder(resp.Body).Decode(&restrictions); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return restrictions, nil +} + +// GetCommercialVehicleRestrictionsWithID retrieves all commercial vehicle restrictions with unique IDs +func (t *TrafficClient) GetCommercialVehicleRestrictionsWithID() ([]CommercialVehicleRestriction, error) { + req, err := http.NewRequest(http.MethodGet, getCommercialVehicleRestrictionsWithIdAsJsonURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamTrafficAccessCodeKey, t.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := t.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var restrictions []CommercialVehicleRestriction + if err := json.NewDecoder(resp.Body).Decode(&restrictions); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return restrictions, nil +} \ No newline at end of file diff --git a/traffic/crossings.go b/traffic/crossings.go new file mode 100644 index 0000000..a9af224 --- /dev/null +++ b/traffic/crossings.go @@ -0,0 +1,21 @@ +package traffic + +import ( + "alpineworks.io/wsdot" +) + +// CrossingsClient provides access to WSDOT Border Crossings API endpoints +type CrossingsClient struct { + wsdot *wsdot.WSDOTClient +} + +// NewCrossingsClient creates a new Border Crossings client +func NewCrossingsClient(wsdotClient *wsdot.WSDOTClient) (*CrossingsClient, error) { + if wsdotClient == nil { + return nil, wsdot.ErrNoClient + } + + return &CrossingsClient{ + wsdot: wsdotClient, + }, nil +} \ No newline at end of file diff --git a/traffic/highway_alerts.go b/traffic/highway_alerts.go new file mode 100644 index 0000000..21f8dd7 --- /dev/null +++ b/traffic/highway_alerts.go @@ -0,0 +1,70 @@ +package traffic + +import ( + "encoding/json" + "fmt" + "net/http" + + "alpineworks.io/wsdot" +) + +const ( + getHighwayAlertsAsJsonURL = "http://www.wsdot.wa.gov/Traffic/api/HighwayAlerts/HighwayAlertsREST.svc/GetAlertsAsJson" +) + +// AlertLocation represents a location for highway alerts (reusing RoadwayLocation) +type AlertLocation struct { + Description string `json:"Description"` + Direction string `json:"Direction"` + Latitude float64 `json:"Latitude"` + Longitude float64 `json:"Longitude"` + MilePost float64 `json:"MilePost"` + RoadName string `json:"RoadName"` +} + +// HighwayAlert represents a highway alert from WSDOT +type HighwayAlert struct { + AlertID int `json:"AlertID"` + County string `json:"County"` + EndRoadwayLocation AlertLocation `json:"EndRoadwayLocation"` + EndTime string `json:"EndTime"` + EventCategory string `json:"EventCategory"` + EventStatus string `json:"EventStatus"` + ExtendedDescription string `json:"ExtendedDescription"` + HeadlineDescription string `json:"HeadlineDescription"` + LastUpdatedTime string `json:"LastUpdatedTime"` + Priority string `json:"Priority"` + Region string `json:"Region"` + StartRoadwayLocation AlertLocation `json:"StartRoadwayLocation"` + StartTime string `json:"StartTime"` +} + +// GetHighwayAlerts retrieves all highway alerts +func (t *TrafficClient) GetHighwayAlerts() ([]HighwayAlert, error) { + req, err := http.NewRequest(http.MethodGet, getHighwayAlertsAsJsonURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamTrafficAccessCodeKey, t.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := t.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var alerts []HighwayAlert + if err := json.NewDecoder(resp.Body).Decode(&alerts); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return alerts, nil +} \ No newline at end of file diff --git a/traffic/mountain_pass_conditions.go b/traffic/mountain_pass_conditions.go new file mode 100644 index 0000000..d0491f2 --- /dev/null +++ b/traffic/mountain_pass_conditions.go @@ -0,0 +1,65 @@ +package traffic + +import ( + "encoding/json" + "fmt" + "net/http" + + "alpineworks.io/wsdot" +) + +const ( + getMountainPassConditionsAsJsonURL = "http://www.wsdot.wa.gov/Traffic/api/MountainPassConditions/MountainPassConditionsREST.svc/GetMountainPassConditionsAsJson" +) + +// MountainPassRestriction represents a travel restriction for a mountain pass +type MountainPassRestriction struct { + RestrictionText string `json:"RestrictionText"` + TravelDirection string `json:"TravelDirection"` +} + +// MountainPassCondition represents conditions for a mountain pass +type MountainPassCondition struct { + DateUpdated string `json:"DateUpdated"` + ElevationInFeet int `json:"ElevationInFeet"` + Latitude float64 `json:"Latitude"` + Longitude float64 `json:"Longitude"` + MountainPassId int `json:"MountainPassId"` + MountainPassName string `json:"MountainPassName"` + RestrictionOne MountainPassRestriction `json:"RestrictionOne"` + RestrictionTwo MountainPassRestriction `json:"RestrictionTwo"` + RoadCondition string `json:"RoadCondition"` + TemperatureInFahrenheit int `json:"TemperatureInFahrenheit"` + TravelAdvisoryActive bool `json:"TravelAdvisoryActive"` + WeatherCondition string `json:"WeatherCondition"` +} + +// GetMountainPassConditions retrieves all mountain pass conditions +func (t *TrafficClient) GetMountainPassConditions() ([]MountainPassCondition, error) { + req, err := http.NewRequest(http.MethodGet, getMountainPassConditionsAsJsonURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamTrafficAccessCodeKey, t.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := t.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var conditions []MountainPassCondition + if err := json.NewDecoder(resp.Body).Decode(&conditions); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return conditions, nil +} \ No newline at end of file diff --git a/traffic/toll_rates.go b/traffic/toll_rates.go new file mode 100644 index 0000000..161df24 --- /dev/null +++ b/traffic/toll_rates.go @@ -0,0 +1,61 @@ +package traffic + +import ( + "encoding/json" + "fmt" + "net/http" + + "alpineworks.io/wsdot" +) + +const ( + getTollRatesAsJsonURL = "http://www.wsdot.wa.gov/Traffic/api/TollRates/TollRatesREST.svc/GetTollRatesAsJson" +) + +// TollRate represents toll rate information for a route segment +type TollRate struct { + CurrentMessage string `json:"CurrentMessage"` + CurrentToll float64 `json:"CurrentToll"` + EndLatitude float64 `json:"EndLatitude"` + EndLocationName string `json:"EndLocationName"` + EndLongitude float64 `json:"EndLongitude"` + EndMilepost float64 `json:"EndMilepost"` + StartLatitude float64 `json:"StartLatitude"` + StartLocationName string `json:"StartLocationName"` + StartLongitude float64 `json:"StartLongitude"` + StartMilepost float64 `json:"StartMilepost"` + StateRoute string `json:"StateRoute"` + TimeUpdated string `json:"TimeUpdated"` + TravelDirection string `json:"TravelDirection"` + TripName string `json:"TripName"` +} + +// GetTollRates retrieves all toll rates +func (t *TrafficClient) GetTollRates() ([]TollRate, error) { + req, err := http.NewRequest(http.MethodGet, getTollRatesAsJsonURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamTrafficAccessCodeKey, t.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := t.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var tollRates []TollRate + if err := json.NewDecoder(resp.Body).Decode(&tollRates); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return tollRates, nil +} \ No newline at end of file diff --git a/traffic/traffic.go b/traffic/traffic.go new file mode 100644 index 0000000..94db3e0 --- /dev/null +++ b/traffic/traffic.go @@ -0,0 +1,26 @@ +package traffic + +import ( + "alpineworks.io/wsdot" +) + +// TrafficClient provides access to WSDOT Traffic API endpoints +type TrafficClient struct { + wsdot *wsdot.WSDOTClient +} + +// NewTrafficClient creates a new Traffic client +func NewTrafficClient(wsdotClient *wsdot.WSDOTClient) (*TrafficClient, error) { + if wsdotClient == nil { + return nil, wsdot.ErrNoClient + } + + return &TrafficClient{ + wsdot: wsdotClient, + }, nil +} + +// GetWSDOTClient returns the underlying WSDOT client +func (t *TrafficClient) GetWSDOTClient() *wsdot.WSDOTClient { + return t.wsdot +} \ No newline at end of file diff --git a/traffic/traffic_flow.go b/traffic/traffic_flow.go new file mode 100644 index 0000000..ae0bae4 --- /dev/null +++ b/traffic/traffic_flow.go @@ -0,0 +1,63 @@ +package traffic + +import ( + "encoding/json" + "fmt" + "net/http" + + "alpineworks.io/wsdot" +) + +const ( + getTrafficFlowAsJsonURL = "http://www.wsdot.wa.gov/Traffic/api/TrafficFlow/TrafficFlowREST.svc/GetTrafficFlowAsJson" +) + +// TrafficFlowStationLocation represents the location of a traffic flow station +type TrafficFlowStationLocation struct { + Description string `json:"Description"` + Direction string `json:"Direction"` + Latitude float64 `json:"Latitude"` + Longitude float64 `json:"Longitude"` + MilePost float64 `json:"MilePost"` + RoadName string `json:"RoadName"` +} + +// TrafficFlow represents traffic flow data from a monitoring station +type TrafficFlow struct { + FlowDataID int `json:"FlowDataID"` + FlowReadingValue interface{} `json:"FlowReadingValue"` // Can be int or string (Unknown, WideOpen, Moderate, Heavy, StopAndGo, NoData) + FlowStationLocation TrafficFlowStationLocation `json:"FlowStationLocation"` + Region string `json:"Region"` + StationName string `json:"StationName"` + Time string `json:"Time"` +} + +// GetTrafficFlow retrieves all traffic flow data +func (t *TrafficClient) GetTrafficFlow() ([]TrafficFlow, error) { + req, err := http.NewRequest(http.MethodGet, getTrafficFlowAsJsonURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamTrafficAccessCodeKey, t.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := t.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var trafficFlow []TrafficFlow + if err := json.NewDecoder(resp.Body).Decode(&trafficFlow); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return trafficFlow, nil +} \ No newline at end of file diff --git a/traffic/travel_times.go b/traffic/travel_times.go new file mode 100644 index 0000000..1de3667 --- /dev/null +++ b/traffic/travel_times.go @@ -0,0 +1,66 @@ +package traffic + +import ( + "encoding/json" + "fmt" + "net/http" + + "alpineworks.io/wsdot" +) + +const ( + getTravelTimesAsJsonURL = "http://www.wsdot.wa.gov/Traffic/api/TravelTimes/TravelTimesREST.svc/GetTravelTimesAsJson" +) + +// TravelTimePoint represents a travel time route endpoint +type TravelTimePoint struct { + Description string `json:"Description"` + Direction string `json:"Direction"` + Latitude float64 `json:"Latitude"` + Longitude float64 `json:"Longitude"` + MilePost float64 `json:"MilePost"` + RoadName string `json:"RoadName"` +} + +// TravelTime represents travel time data for a route segment +type TravelTime struct { + AverageTime int `json:"AverageTime"` + CurrentTime int `json:"CurrentTime"` + Description string `json:"Description"` + Distance float64 `json:"Distance"` + EndPoint TravelTimePoint `json:"EndPoint"` + Name string `json:"Name"` + StartPoint TravelTimePoint `json:"StartPoint"` + TimeUpdated string `json:"TimeUpdated"` + TravelTimeID int `json:"TravelTimeID"` +} + +// GetTravelTimes retrieves all travel times +func (t *TrafficClient) GetTravelTimes() ([]TravelTime, error) { + req, err := http.NewRequest(http.MethodGet, getTravelTimesAsJsonURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamTrafficAccessCodeKey, t.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := t.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var travelTimes []TravelTime + if err := json.NewDecoder(resp.Body).Decode(&travelTimes); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return travelTimes, nil +} \ No newline at end of file diff --git a/traffic/weather_information.go b/traffic/weather_information.go new file mode 100644 index 0000000..b98d3df --- /dev/null +++ b/traffic/weather_information.go @@ -0,0 +1,62 @@ +package traffic + +import ( + "encoding/json" + "fmt" + "net/http" + + "alpineworks.io/wsdot" +) + +const ( + getCurrentWeatherInformationAsJsonURL = "http://www.wsdot.wa.gov/Traffic/api/WeatherInformation/WeatherInformationREST.svc/GetCurrentWeatherInformationAsJson" +) + +// WeatherInformation represents current weather information from a station +type WeatherInformation struct { + BarometricPressure float64 `json:"BarometricPressure"` + Latitude float64 `json:"Latitude"` + Longitude float64 `json:"Longitude"` + PrecipitationInInches float64 `json:"PrecipitationInInches"` + ReadingTime string `json:"ReadingTime"` + RelativeHumidity float64 `json:"RelativeHumidity"` + SkyCoverage string `json:"SkyCoverage"` + StationID int `json:"StationID"` + StationName string `json:"StationName"` + TemperatureInFahrenheit float64 `json:"TemperatureInFahrenheit"` + Visibility int `json:"Visibility"` + WindDirection float64 `json:"WindDirection"` + WindDirectionCardinal string `json:"WindDirectionCardinal"` + WindGustSpeedInMPH float64 `json:"WindGustSpeedInMPH"` + WindSpeedInMPH float64 `json:"WindSpeedInMPH"` +} + +// GetCurrentWeatherInformation retrieves current weather information from all stations +func (t *TrafficClient) GetCurrentWeatherInformation() ([]WeatherInformation, error) { + req, err := http.NewRequest(http.MethodGet, getCurrentWeatherInformationAsJsonURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamTrafficAccessCodeKey, t.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := t.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var weatherInfo []WeatherInformation + if err := json.NewDecoder(resp.Body).Decode(&weatherInfo); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return weatherInfo, nil +} \ No newline at end of file diff --git a/traffic/weather_stations.go b/traffic/weather_stations.go new file mode 100644 index 0000000..43be9e8 --- /dev/null +++ b/traffic/weather_stations.go @@ -0,0 +1,52 @@ +package traffic + +import ( + "encoding/json" + "fmt" + "net/http" + + "alpineworks.io/wsdot" +) + +const ( + getWeatherStationsAsJsonURL = "http://www.wsdot.wa.gov/Traffic/api/WeatherStations/WeatherStationsREST.svc/GetWeatherStationsAsJson" +) + +// WeatherStation represents a weather monitoring station +type WeatherStation struct { + Description string `json:"Description"` + Latitude float64 `json:"Latitude"` + Longitude float64 `json:"Longitude"` + StationID int `json:"StationID"` + StationName string `json:"StationName"` +} + +// GetWeatherStations retrieves all weather station information +func (t *TrafficClient) GetWeatherStations() ([]WeatherStation, error) { + req, err := http.NewRequest(http.MethodGet, getWeatherStationsAsJsonURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamTrafficAccessCodeKey, t.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := t.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var stations []WeatherStation + if err := json.NewDecoder(resp.Body).Decode(&stations); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return stations, nil +} \ No newline at end of file diff --git a/vessels/accommodations.go b/vessels/accommodations.go new file mode 100644 index 0000000..3a810e2 --- /dev/null +++ b/vessels/accommodations.go @@ -0,0 +1,90 @@ +package vessels + +import ( + "encoding/json" + "fmt" + "net/http" + + "alpineworks.io/wsdot" +) + +const ( + getVesselAccommodationsAsJsonURL = "https://www.wsdot.wa.gov/Ferries/API/Vessels/rest/vesselaccommodations" +) + +type VesselAccommodation struct { + VesselID int `json:"VesselID"` + VesselSubjectID int `json:"VesselSubjectID"` + VesselName string `json:"VesselName"` + VesselAbbrev string `json:"VesselAbbrev"` + Class VesselClass `json:"Class"` + CarDeckRestroom bool `json:"CarDeckRestroom"` + CarDeckShelter bool `json:"CarDeckShelter"` + Elevator bool `json:"Elevator"` + ADAAccessible bool `json:"ADAAccessible"` + MainCabinGalley bool `json:"MainCabinGalley"` + MainCabinRestroom bool `json:"MainCabinRestroom"` + PublicWifi bool `json:"PublicWifi"` + ADAInfo string `json:"ADAInfo"` + AdditionalInfo string `json:"AdditionalInfo"` +} + +func (v *VesselsClient) GetVesselAccommodations() ([]VesselAccommodation, error) { + req, err := http.NewRequest(http.MethodGet, getVesselAccommodationsAsJsonURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamFerriesAccessCodeKey, v.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := v.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var accommodations []VesselAccommodation + if err := json.NewDecoder(resp.Body).Decode(&accommodations); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return accommodations, nil +} + +func (v *VesselsClient) GetVesselAccommodationByID(vesselID int) (*VesselAccommodation, error) { + url := fmt.Sprintf("%s/%d", getVesselAccommodationsAsJsonURL, vesselID) + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamFerriesAccessCodeKey, v.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := v.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var accommodation VesselAccommodation + if err := json.NewDecoder(resp.Body).Decode(&accommodation); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return &accommodation, nil +} \ No newline at end of file diff --git a/vessels/basics.go b/vessels/basics.go new file mode 100644 index 0000000..5dce757 --- /dev/null +++ b/vessels/basics.go @@ -0,0 +1,99 @@ +package vessels + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + + "alpineworks.io/wsdot" +) + +const ( + getTerminalBasicsURL = "http://www.wsdot.wa.gov/ferries/api/terminals/rest/terminalbasics" +) + +// TerminalBasic represents basic terminal information +type TerminalBasic struct { + TerminalID int `json:"TerminalID"` + TerminalName string `json:"TerminalName"` + TerminalAbbrev string `json:"TerminalAbbrev"` + Region int `json:"Region"` + SortSeq int `json:"SortSeq"` + Latitude float64 `json:"Latitude"` + Longitude float64 `json:"Longitude"` + AddressLineOne string `json:"AddressLineOne"` + AddressLineTwo string `json:"AddressLineTwo"` + City string `json:"City"` + State string `json:"State"` + ZipCode string `json:"ZipCode"` + Country string `json:"Country"` + MapLink string `json:"MapLink"` + DispGISLat float64 `json:"DispGISLat"` + DispGISLong float64 `json:"DispGISLong"` + Reservable bool `json:"Reservable"` + CheckTravelAdvisory bool `json:"CheckTravelAdvisory"` + WaitTimesEnabled bool `json:"WaitTimesEnabled"` +} + +// GetTerminalBasics retrieves all terminal basic information +func (v *VesselsClient) GetTerminalBasics() ([]TerminalBasic, error) { + req, err := http.NewRequest(http.MethodGet, getTerminalBasicsURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamFerriesAccessCodeKey, v.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := v.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var terminals []TerminalBasic + if err := json.NewDecoder(resp.Body).Decode(&terminals); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return terminals, nil +} + +// GetTerminalBasicByID retrieves basic information for a specific terminal +func (v *VesselsClient) GetTerminalBasicByID(terminalID int) (*TerminalBasic, error) { + url := fmt.Sprintf("%s/%s", getTerminalBasicsURL, strconv.Itoa(terminalID)) + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamFerriesAccessCodeKey, v.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := v.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var terminal TerminalBasic + if err := json.NewDecoder(resp.Body).Decode(&terminal); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return &terminal, nil +} diff --git a/vessels/cache.go b/vessels/cache.go new file mode 100644 index 0000000..dd5e049 --- /dev/null +++ b/vessels/cache.go @@ -0,0 +1,49 @@ +package vessels + +import ( + "encoding/json" + "fmt" + "net/http" + "time" +) + +const ( + getFaresCacheFlushDateURL = "http://www.wsdot.wa.gov/ferries/api/fares/rest/cacheflushdate" +) + +// GetCacheFlushDate retrieves the fares cache flush date +func (v *VesselsClient) GetCacheFlushDate() (*time.Time, error) { + req, err := http.NewRequest(http.MethodGet, getFaresCacheFlushDateURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + req.Header.Set("Content-Type", "application/json") + + resp, err := v.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var dateStr string + if err := json.NewDecoder(resp.Body).Decode(&dateStr); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + // Parse the date string - assuming standard ISO format or custom format + parsedTime, err := time.Parse(time.RFC3339, dateStr) + if err != nil { + // Try alternative format if RFC3339 fails + parsedTime, err = time.Parse("2006-01-02T15:04:05", dateStr) + if err != nil { + return nil, fmt.Errorf("error parsing date: %v", err) + } + } + + return &parsedTime, nil +} \ No newline at end of file diff --git a/vessels/fare_line_items.go b/vessels/fare_line_items.go new file mode 100644 index 0000000..4ec0401 --- /dev/null +++ b/vessels/fare_line_items.go @@ -0,0 +1,164 @@ +package vessels + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + + "alpineworks.io/wsdot" +) + +const ( + getFareLineItemsURL = "http://www.wsdot.wa.gov/ferries/api/fares/rest/farelineitems" + getFareLineItemsBasicURL = "http://www.wsdot.wa.gov/ferries/api/fares/rest/farelineitemsbasic" +) + +// FareLineItem represents a fare line item for a specific route and trip +type FareLineItem struct { + FareLineItemID int `json:"FareLineItemID"` + FareTypeID int `json:"FareTypeID"` + FareTypeDescription string `json:"FareTypeDescription"` + FareLineItemDescription string `json:"FareLineItemDescription"` + Cost float64 `json:"Cost"` + SortOrder int `json:"SortOrder"` + MaxVehicleLength int `json:"MaxVehicleLength"` + MaxVehicleHeight int `json:"MaxVehicleHeight"` + VehicleClass string `json:"VehicleClass"` + IsVehicle bool `json:"IsVehicle"` + RestrictionText string `json:"RestrictionText"` + Discountable bool `json:"Discountable"` +} + +// BasicFareLineItem represents a simplified fare line item +type BasicFareLineItem struct { + FareLineItemID int `json:"FareLineItemID"` + FareLineItemDescription string `json:"FareLineItemDescription"` + Cost float64 `json:"Cost"` + SortOrder int `json:"SortOrder"` + IsVehicle bool `json:"IsVehicle"` +} + +// GetFareLineItems retrieves detailed fare line items for a specific route and trip +func (v *VesselsClient) GetFareLineItems(tripDate, departingTerminalID, arrivingTerminalID string, roundTrip bool) ([]FareLineItem, error) { + roundTripStr := "false" + if roundTrip { + roundTripStr = "true" + } + + url := fmt.Sprintf("%s/%s/%s/%s/%s", getFareLineItemsURL, tripDate, departingTerminalID, arrivingTerminalID, roundTripStr) + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamFerriesAccessCodeKey, v.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := v.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var fareItems []FareLineItem + if err := json.NewDecoder(resp.Body).Decode(&fareItems); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return fareItems, nil +} + +// GetFareLineItemsBasic retrieves basic fare line items for a specific route and trip +func (v *VesselsClient) GetFareLineItemsBasic(tripDate, departingTerminalID, arrivingTerminalID string, roundTrip bool) ([]BasicFareLineItem, error) { + roundTripStr := "false" + if roundTrip { + roundTripStr = "true" + } + + url := fmt.Sprintf("%s/%s/%s/%s/%s", getFareLineItemsBasicURL, tripDate, departingTerminalID, arrivingTerminalID, roundTripStr) + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamFerriesAccessCodeKey, v.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := v.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var fareItems []BasicFareLineItem + if err := json.NewDecoder(resp.Body).Decode(&fareItems); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return fareItems, nil +} + +// GetFareTotal calculates the total fare for specified items and quantities +func (v *VesselsClient) GetFareTotal(tripDate, departingTerminalID, arrivingTerminalID string, roundTrip bool, fareLineItemID int, quantity int) (*FareTotalResult, error) { + roundTripStr := "false" + if roundTrip { + roundTripStr = "true" + } + + url := fmt.Sprintf("http://www.wsdot.wa.gov/ferries/api/fares/rest/faretotals/%s/%s/%s/%s/%s/%s", + tripDate, departingTerminalID, arrivingTerminalID, roundTripStr, + strconv.Itoa(fareLineItemID), strconv.Itoa(quantity)) + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamFerriesAccessCodeKey, v.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := v.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var result FareTotalResult + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return &result, nil +} + +// FareTotalResult represents the result of a fare total calculation +type FareTotalResult struct { + TotalCost float64 `json:"TotalCost"` + Currency string `json:"Currency"` + TaxAmount float64 `json:"TaxAmount"` + FareLineItemID int `json:"FareLineItemID"` + Quantity int `json:"Quantity"` + UnitCost float64 `json:"UnitCost"` + Description string `json:"Description"` +} \ No newline at end of file diff --git a/vessels/fares.go b/vessels/fares.go new file mode 100644 index 0000000..d10d03c --- /dev/null +++ b/vessels/fares.go @@ -0,0 +1,21 @@ +package vessels + +import ( + "alpineworks.io/wsdot" +) + +// FaresClient provides access to WSDOT Fares API endpoints +type FaresClient struct { + wsdot *wsdot.WSDOTClient +} + +// NewFaresClient creates a new Fares client +func NewFaresClient(wsdotClient *wsdot.WSDOTClient) (*FaresClient, error) { + if wsdotClient == nil { + return nil, wsdot.ErrNoClient + } + + return &FaresClient{ + wsdot: wsdotClient, + }, nil +} \ No newline at end of file diff --git a/ferries/locations.go b/vessels/history.go similarity index 54% rename from ferries/locations.go rename to vessels/history.go index a41695f..081281c 100644 --- a/ferries/locations.go +++ b/vessels/history.go @@ -1,69 +1,21 @@ -package ferries +package vessels import ( "encoding/json" "fmt" "net/http" + "time" "alpineworks.io/wsdot" ) const ( - getVesselBasicsAsJsonURL = "https://www.wsdot.wa.gov/Ferries/API/Vessels/rest/vesselbasics" - getVesselLocationsAsJsonURL = "https://www.wsdot.wa.gov/Ferries/API/Vessels/rest/vessellocations" + getVesselHistoryAsJsonURL = "https://www.wsdot.wa.gov/Ferries/API/Vessels/rest/vesselhistory" ) -type VesselBasic struct { - VesselID int `json:"VesselID"` - VesselSubjectID int `json:"VesselSubjectID"` - VesselName string `json:"VesselName"` - VesselAbbrev string `json:"VesselAbbrev"` - Class struct { - ClassID int `json:"ClassID"` - ClassSubjectID int `json:"ClassSubjectID"` - ClassName string `json:"ClassName"` - SortSeq int `json:"SortSeq"` - DrawingImg string `json:"DrawingImg"` - SilhouetteImg string `json:"SilhouetteImg"` - PublicDisplayName string `json:"PublicDisplayName"` - } `json:"Class"` - Status int `json:"Status"` - OwnedByWSF bool `json:"OwnedByWSF"` -} - -func (f *FerriesClient) GetVesselBasics() ([]VesselBasic, error) { - req, err := http.NewRequest(http.MethodGet, getVesselBasicsAsJsonURL, nil) - if err != nil { - return nil, fmt.Errorf("error creating request: %v", err) - } - - q := req.URL.Query() - q.Add(wsdot.ParamFerriesAccessCodeKey, f.wsdot.ApiKey) - req.URL.RawQuery = q.Encode() - req.Header.Set("Content-Type", "application/json") - - resp, err := f.wsdot.Client.Do(req) - if err != nil { - return nil, fmt.Errorf("error making request: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } - - var vessels []VesselBasic - if err := json.NewDecoder(resp.Body).Decode(&vessels); err != nil { - return nil, fmt.Errorf("error decoding response: %v", err) - } - - return vessels, nil -} - -type VesselLocation struct { +type VesselHistory struct { VesselID int `json:"VesselID"` VesselName string `json:"VesselName"` - Mmsi int `json:"Mmsi"` DepartingTerminalID int `json:"DepartingTerminalID"` DepartingTerminalName string `json:"DepartingTerminalName"` DepartingTerminalAbbrev string `json:"DepartingTerminalAbbrev"` @@ -85,25 +37,20 @@ type VesselLocation struct { SortSeq int `json:"SortSeq"` ManagedBy int `json:"ManagedBy"` TimeStamp string `json:"TimeStamp"` - VesselWatchShutID int `json:"VesselWatchShutID"` - VesselWatchShutMsg string `json:"VesselWatchShutMsg"` - VesselWatchShutFlag string `json:"VesselWatchShutFlag"` - VesselWatchStatus string `json:"VesselWatchStatus"` - VesselWatchMsg string `json:"VesselWatchMsg"` } -func (f *FerriesClient) GetVesselLocations() ([]VesselLocation, error) { - req, err := http.NewRequest(http.MethodGet, getVesselLocationsAsJsonURL, nil) +func (v *VesselsClient) GetVesselHistory() ([]VesselHistory, error) { + req, err := http.NewRequest(http.MethodGet, getVesselHistoryAsJsonURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %v", err) } q := req.URL.Query() - q.Add(wsdot.ParamFerriesAccessCodeKey, f.wsdot.ApiKey) + q.Add(wsdot.ParamFerriesAccessCodeKey, v.wsdot.ApiKey) req.URL.RawQuery = q.Encode() req.Header.Set("Content-Type", "application/json") - resp, err := f.wsdot.Client.Do(req) + resp, err := v.wsdot.Client.Do(req) if err != nil { return nil, fmt.Errorf("error making request: %v", err) } @@ -113,10 +60,45 @@ func (f *FerriesClient) GetVesselLocations() ([]VesselLocation, error) { return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) } - var vessels []VesselLocation - if err := json.NewDecoder(resp.Body).Decode(&vessels); err != nil { + var history []VesselHistory + if err := json.NewDecoder(resp.Body).Decode(&history); err != nil { return nil, fmt.Errorf("error decoding response: %v", err) } - return vessels, nil + return history, nil } + +func (v *VesselsClient) GetVesselHistoryByNameAndDateRange(vesselName string, dateStart, dateEnd time.Time) ([]VesselHistory, error) { + // Format dates as YYYY-MM-DD + startStr := dateStart.Format("2006-01-02") + endStr := dateEnd.Format("2006-01-02") + + url := fmt.Sprintf("%s/%s/%s/%s", getVesselHistoryAsJsonURL, vesselName, startStr, endStr) + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamFerriesAccessCodeKey, v.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := v.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var history []VesselHistory + if err := json.NewDecoder(resp.Body).Decode(&history); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return history, nil +} \ No newline at end of file diff --git a/vessels/locations.go b/vessels/locations.go new file mode 100644 index 0000000..7a54a90 --- /dev/null +++ b/vessels/locations.go @@ -0,0 +1,94 @@ +package vessels + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + + "alpineworks.io/wsdot" +) + +const ( + getTerminalLocationsURL = "http://www.wsdot.wa.gov/ferries/api/terminals/rest/terminallocations" +) + +// TerminalLocation represents location information for a terminal +type TerminalLocation struct { + TerminalID int `json:"TerminalID"` + TerminalName string `json:"TerminalName"` + TerminalAbbrev string `json:"TerminalAbbrev"` + Latitude float64 `json:"Latitude"` + Longitude float64 `json:"Longitude"` + AddressLineOne string `json:"AddressLineOne"` + AddressLineTwo string `json:"AddressLineTwo"` + City string `json:"City"` + State string `json:"State"` + ZipCode string `json:"ZipCode"` + Country string `json:"Country"` + MapLink string `json:"MapLink"` + DispGISLat float64 `json:"DispGISLat"` + DispGISLong float64 `json:"DispGISLong"` +} + +// GetTerminalLocations retrieves location information for all terminals +func (v *VesselsClient) GetTerminalLocations() ([]TerminalLocation, error) { + req, err := http.NewRequest(http.MethodGet, getTerminalLocationsURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamFerriesAccessCodeKey, v.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := v.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var locations []TerminalLocation + if err := json.NewDecoder(resp.Body).Decode(&locations); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return locations, nil +} + +// GetTerminalLocationByID retrieves location information for a specific terminal +func (v *VesselsClient) GetTerminalLocationByID(terminalID int) (*TerminalLocation, error) { + url := fmt.Sprintf("%s/%s", getTerminalLocationsURL, strconv.Itoa(terminalID)) + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamFerriesAccessCodeKey, v.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := v.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var location TerminalLocation + if err := json.NewDecoder(resp.Body).Decode(&location); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return &location, nil +} \ No newline at end of file diff --git a/ferries/schedule.go b/vessels/schedule.go similarity index 96% rename from ferries/schedule.go rename to vessels/schedule.go index 7dd6c54..85f6f70 100644 --- a/ferries/schedule.go +++ b/vessels/schedule.go @@ -1,4 +1,4 @@ -package ferries +package vessels import ( "encoding/json" @@ -46,18 +46,18 @@ type ContingencyAdjustment struct { ReplacedBySchedRouteID *int `json:"ReplacedBySchedRouteID"` } -func (f *FerriesClient) GetRouteSchedules() ([]RouteSchedule, error) { +func (v *VesselsClient) GetRouteSchedules() ([]RouteSchedule, error) { req, err := http.NewRequest(http.MethodGet, getRouteSchedulesAsJsonURL, nil) if err != nil { return nil, fmt.Errorf("error creating request: %v", err) } q := req.URL.Query() - q.Add(wsdot.ParamFerriesAccessCodeKey, f.wsdot.ApiKey) + q.Add(wsdot.ParamFerriesAccessCodeKey, v.wsdot.ApiKey) req.URL.RawQuery = q.Encode() req.Header.Set("Content-Type", "application/json") - resp, err := f.wsdot.Client.Do(req) + resp, err := v.wsdot.Client.Do(req) if err != nil { return nil, fmt.Errorf("error making request: %v", err) } @@ -143,7 +143,7 @@ type Time struct { AnnotationIndexes []int `json:"AnnotationIndexes"` } -func (f *FerriesClient) GetSchedulesTodayByRouteID(routeID int, onlyRemainingTimes bool) (*Schedule, error) { +func (v *VesselsClient) GetSchedulesTodayByRouteID(routeID int, onlyRemainingTimes bool) (*Schedule, error) { url := fmt.Sprintf(getScheduleTodayByRouteIDAsJsonURL, routeID, onlyRemainingTimes) req, err := http.NewRequest(http.MethodGet, url, nil) @@ -152,11 +152,11 @@ func (f *FerriesClient) GetSchedulesTodayByRouteID(routeID int, onlyRemainingTim } q := req.URL.Query() - q.Add(wsdot.ParamFerriesAccessCodeKey, f.wsdot.ApiKey) + q.Add(wsdot.ParamFerriesAccessCodeKey, v.wsdot.ApiKey) req.URL.RawQuery = q.Encode() req.Header.Set("Content-Type", "application/json") - resp, err := f.wsdot.Client.Do(req) + resp, err := v.wsdot.Client.Do(req) if err != nil { return nil, fmt.Errorf("error making request: %v", err) } diff --git a/ferries/schedule_test.go b/vessels/schedule_test.go similarity index 98% rename from ferries/schedule_test.go rename to vessels/schedule_test.go index 43f3b36..4e92dfe 100644 --- a/ferries/schedule_test.go +++ b/vessels/schedule_test.go @@ -1,4 +1,4 @@ -package ferries +package vessels import ( "testing" diff --git a/vessels/stats.go b/vessels/stats.go new file mode 100644 index 0000000..b3af0bf --- /dev/null +++ b/vessels/stats.go @@ -0,0 +1,120 @@ +package vessels + +import ( + "encoding/json" + "fmt" + "net/http" + + "alpineworks.io/wsdot" +) + +const ( + getVesselStatsAsJsonURL = "https://www.wsdot.wa.gov/Ferries/API/Vessels/rest/vesselstats" +) + +type VesselStats struct { + VesselID int `json:"VesselID"` + VesselSubjectID int `json:"VesselSubjectID"` + VesselName string `json:"VesselName"` + VesselAbbrev string `json:"VesselAbbrev"` + Class VesselClass `json:"Class"` + VesselNameDesc string `json:"VesselNameDesc"` + VesselHistory string `json:"VesselHistory"` + Beam string `json:"Beam"` + CityBuilt string `json:"CityBuilt"` + SpeedInKnots int `json:"SpeedInKnots"` + Draft string `json:"Draft"` + EngineCount int `json:"EngineCount"` + Horsepower int `json:"Horsepower"` + Length string `json:"Length"` + MaxPassengerCount int `json:"MaxPassengerCount"` + PassengerOnly bool `json:"PassengerOnly"` + FastFerry bool `json:"FastFerry"` + PropulsionInfo string `json:"PropulsionInfo"` + TallDeckClearance int `json:"TallDeckClearance"` + RegDeckSpace int `json:"RegDeckSpace"` + TallDeckSpace int `json:"TallDeckSpace"` + Tonnage int `json:"Tonnage"` + Displacement int `json:"Displacement"` + YearBuilt int `json:"YearBuilt"` + YearRebuilt int `json:"YearRebuilt"` + VesselDrawingImg string `json:"VesselDrawingImg"` + SolasCertified bool `json:"SolasCertified"` + PicnicTables bool `json:"PicnicTables"` + Restroom bool `json:"Restroom"` + UploadVesselArrivingAnnouncement bool `json:"UploadVesselArrivingAnnouncement"` + Galley bool `json:"Galley"` + VesselWatchUrl string `json:"VesselWatchUrl"` + Status int `json:"Status"` + OwnedByWSF bool `json:"OwnedByWSF"` +} + +type VesselClass struct { + ClassID int `json:"ClassID"` + ClassSubjectID int `json:"ClassSubjectID"` + ClassName string `json:"ClassName"` + SortSeq int `json:"SortSeq"` + DrawingImg string `json:"DrawingImg"` + SilhouetteImg string `json:"SilhouetteImg"` + PublicDisplayName string `json:"PublicDisplayName"` +} + +func (v *VesselsClient) GetVesselStats() ([]VesselStats, error) { + req, err := http.NewRequest(http.MethodGet, getVesselStatsAsJsonURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamFerriesAccessCodeKey, v.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := v.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var stats []VesselStats + if err := json.NewDecoder(resp.Body).Decode(&stats); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return stats, nil +} + +func (v *VesselsClient) GetVesselStatsByID(vesselID int) (*VesselStats, error) { + url := fmt.Sprintf("%s/%d", getVesselStatsAsJsonURL, vesselID) + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamFerriesAccessCodeKey, v.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := v.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var stats VesselStats + if err := json.NewDecoder(resp.Body).Decode(&stats); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return &stats, nil +} \ No newline at end of file diff --git a/vessels/terminals.go b/vessels/terminals.go new file mode 100644 index 0000000..125d164 --- /dev/null +++ b/vessels/terminals.go @@ -0,0 +1,101 @@ +package vessels + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + + "alpineworks.io/wsdot" +) + +const ( + getFareTerminalsURL = "http://www.wsdot.wa.gov/ferries/api/fares/rest/terminals" +) + +// FareTerminal represents a terminal in the fares system +type FareTerminal struct { + TerminalID int `json:"TerminalID"` + TerminalName string `json:"TerminalName"` + TerminalAbbrev string `json:"TerminalAbbrev"` + Region int `json:"Region"` + SortSeq int `json:"SortSeq"` + Latitude float64 `json:"Latitude"` + Longitude float64 `json:"Longitude"` + AddressLineOne string `json:"AddressLineOne"` + AddressLineTwo string `json:"AddressLineTwo"` + City string `json:"City"` + State string `json:"State"` + ZipCode string `json:"ZipCode"` + Country string `json:"Country"` + MapLink string `json:"MapLink"` + DispGISLat float64 `json:"DispGISLat"` + DispGISLong float64 `json:"DispGISLong"` + Reservable bool `json:"Reservable"` + CheckTravelAdvisory bool `json:"CheckTravelAdvisory"` +} + +// GetTerminals retrieves all terminals available for fare calculations +func (v *VesselsClient) GetTerminals(tripDate string) ([]FareTerminal, error) { + url := fmt.Sprintf("%s/%s", getFareTerminalsURL, tripDate) + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamFerriesAccessCodeKey, v.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := v.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var terminals []FareTerminal + if err := json.NewDecoder(resp.Body).Decode(&terminals); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return terminals, nil +} + +// GetTerminalMates retrieves terminals that can be reached from a specific terminal +func (v *VesselsClient) GetTerminalMates(tripDate string, terminalID int) ([]FareTerminal, error) { + url := fmt.Sprintf("http://www.wsdot.wa.gov/ferries/api/fares/rest/terminalmates/%s/%s", + tripDate, strconv.Itoa(terminalID)) + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamFerriesAccessCodeKey, v.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := v.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var terminals []FareTerminal + if err := json.NewDecoder(resp.Body).Decode(&terminals); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return terminals, nil +} \ No newline at end of file diff --git a/vessels/vessels.go b/vessels/vessels.go new file mode 100644 index 0000000..4bff5b4 --- /dev/null +++ b/vessels/vessels.go @@ -0,0 +1,26 @@ +package vessels + +import ( + "alpineworks.io/wsdot" +) + +// VesselsClient provides access to all WSDOT Vessels, Terminals, and Fares API endpoints +type VesselsClient struct { + wsdot *wsdot.WSDOTClient +} + +// NewVesselsClient creates a new unified Vessels client +func NewVesselsClient(wsdotClient *wsdot.WSDOTClient) (*VesselsClient, error) { + if wsdotClient == nil { + return nil, wsdot.ErrNoClient + } + + return &VesselsClient{ + wsdot: wsdotClient, + }, nil +} + +// GetWSDOTClient returns the underlying WSDOT client +func (v *VesselsClient) GetWSDOTClient() *wsdot.WSDOTClient { + return v.wsdot +} \ No newline at end of file diff --git a/vessels/wait_times.go b/vessels/wait_times.go new file mode 100644 index 0000000..d454f56 --- /dev/null +++ b/vessels/wait_times.go @@ -0,0 +1,87 @@ +package vessels + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + + "alpineworks.io/wsdot" +) + +const ( + getTerminalWaitTimesURL = "http://www.wsdot.wa.gov/ferries/api/terminals/rest/terminalwaittimes" +) + +// TerminalWaitTime represents wait time information for a terminal +type TerminalWaitTime struct { + TerminalID int `json:"TerminalID"` + TerminalName string `json:"TerminalName"` + TerminalAbbrev string `json:"TerminalAbbrev"` + WaitTimeNotes string `json:"WaitTimeNotes"` + WaitTime int `json:"WaitTime"` + LastUpdated string `json:"LastUpdated"` + DisplayOrder int `json:"DisplayOrder"` +} + +// GetTerminalWaitTimes retrieves wait times for all terminals +func (v *VesselsClient) GetTerminalWaitTimes() ([]TerminalWaitTime, error) { + req, err := http.NewRequest(http.MethodGet, getTerminalWaitTimesURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamFerriesAccessCodeKey, v.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := v.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var waitTimes []TerminalWaitTime + if err := json.NewDecoder(resp.Body).Decode(&waitTimes); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return waitTimes, nil +} + +// GetTerminalWaitTimeByID retrieves wait time for a specific terminal +func (v *VesselsClient) GetTerminalWaitTimeByID(terminalID int) (*TerminalWaitTime, error) { + url := fmt.Sprintf("%s/%s", getTerminalWaitTimesURL, strconv.Itoa(terminalID)) + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + q := req.URL.Query() + q.Add(wsdot.ParamFerriesAccessCodeKey, v.wsdot.ApiKey) + req.URL.RawQuery = q.Encode() + req.Header.Set("Content-Type", "application/json") + + resp, err := v.wsdot.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var waitTime TerminalWaitTime + if err := json.NewDecoder(resp.Body).Decode(&waitTime); err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } + + return &waitTime, nil +} \ No newline at end of file