diff --git a/libs/transit/transitous/client.go b/libs/transit/transitous/client.go new file mode 100644 index 0000000..a6dceae --- /dev/null +++ b/libs/transit/transitous/client.go @@ -0,0 +1,105 @@ +package transitous + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/rs/zerolog/log" +) + +// Client represents a Transitous API client +type Client struct { + baseURL string + httpClient *http.Client +} + +// NewClient creates a new Transitous client +func NewClient(baseURL string) *Client { + return &Client{ + baseURL: baseURL, + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + } +} + +// PlanParams represents the parameters for route planning +type PlanParams struct { + FromPlace string + ToPlace string + Time *time.Time +} + +// PlanResponse represents the response from the Transitous API +type PlanResponse struct { + Itineraries []Itinerary `json:"itineraries"` +} + +// PlanWithResponse plans a route and returns the response +func (c *Client) PlanWithResponse(ctx context.Context, params *PlanParams) (*TransitousResponse, error) { + // Build URL with query parameters + u, err := url.Parse(fmt.Sprintf("%s/api/v4/plan", c.baseURL)) + if err != nil { + return nil, fmt.Errorf("failed to parse base URL: %w", err) + } + + query := u.Query() + query.Set("fromPlace", params.FromPlace) + query.Set("toPlace", params.ToPlace) + + if params.Time != nil { + // Use ISO 8601 format with timezone like in the example + query.Set("time", params.Time.Format(time.RFC3339)) + } + + // Additional parameters matching the example + query.Set("withFares", "true") + query.Set("fastestDirectFactor", "1.5") + query.Set("joinInterlinedLegs", "false") + query.Set("maxMatchingDistance", "250") + query.Set("arriveBy", "true") + + u.RawQuery = query.Encode() + + // Create HTTP request + req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", "COOPGO-Platform/1.0") + + log.Debug(). + Str("url", u.String()). + Msg("Making Transitous API request") + + // Execute request + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("API request failed with status %d", resp.StatusCode) + } + + // Parse response + var transitousResponse TransitousResponse + if err := json.NewDecoder(resp.Body).Decode(&transitousResponse); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + log.Debug(). + Str("from", params.FromPlace). + Str("to", params.ToPlace). + Int("itineraries", len(transitousResponse.Itineraries)). + Msg("Transitous API response received") + + return &transitousResponse, nil +} \ No newline at end of file diff --git a/libs/transit/transitous/types.go b/libs/transit/transitous/types.go new file mode 100644 index 0000000..eecc94e --- /dev/null +++ b/libs/transit/transitous/types.go @@ -0,0 +1,86 @@ +package transitous + +import "time" + +// TransitousResponse represents the top-level response from Transitous API +type TransitousResponse struct { + RequestParameters map[string]interface{} `json:"requestParameters"` + DebugOutput map[string]interface{} `json:"debugOutput"` + From Place `json:"from"` + To Place `json:"to"` + Direct []interface{} `json:"direct"` + Itineraries []Itinerary `json:"itineraries"` + PreviousPageCursor string `json:"previousPageCursor,omitempty"` + NextPageCursor string `json:"nextPageCursor,omitempty"` +} + +// Itinerary represents a complete journey from origin to destination +type Itinerary struct { + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` + Duration int `json:"duration"` // Duration in seconds + Legs []Leg `json:"legs"` +} + +// Leg represents a single segment of a journey +type Leg struct { + Mode string `json:"mode"` // WALK, BUS, TRAIN, etc. + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` + Duration int `json:"duration"` // Duration in seconds + Distance float64 `json:"distance"` // Distance in meters + From Place `json:"from"` + To Place `json:"to"` + Route *Route `json:"route,omitempty"` + AgencyName string `json:"agencyName,omitempty"` + Headsign string `json:"headsign,omitempty"` + RouteShortName string `json:"routeShortName,omitempty"` + RouteColor string `json:"routeColor,omitempty"` + RouteTextColor string `json:"routeTextColor,omitempty"` +} + +// Place represents a location (stop, station, or coordinate) +type Place struct { + Name string `json:"name"` + Lat float64 `json:"lat"` + Lon float64 `json:"lon"` + StopId string `json:"stopId,omitempty"` + StopCode string `json:"stopCode,omitempty"` +} + +// Route represents a transit route +type Route struct { + AgencyName string `json:"agencyName"` + ShortName string `json:"shortName"` + LongName string `json:"longName"` + Type int `json:"type"` + Color string `json:"color"` + TextColor string `json:"textColor"` + Url string `json:"url,omitempty"` +} + +// Agency represents a transit agency +type Agency struct { + Id string `json:"id"` + Name string `json:"name"` + Url string `json:"url"` + Timezone string `json:"timezone"` + Lang string `json:"lang,omitempty"` + Phone string `json:"phone,omitempty"` +} + +// Stop represents a transit stop or station +type Stop struct { + Id string `json:"id"` + Code string `json:"code,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Lat float64 `json:"lat"` + Lon float64 `json:"lon"` + ZoneId string `json:"zoneId,omitempty"` + Url string `json:"url,omitempty"` + LocationType int `json:"locationType,omitempty"` + ParentStation string `json:"parentStation,omitempty"` + Timezone string `json:"timezone,omitempty"` + WheelchairBoarding int `json:"wheelchairBoarding,omitempty"` +} \ No newline at end of file