- Add TransferTime constants (AirportToCity, CityToStation, StationToStation) - Update Edge struct with Synthetic field - Enhance addSyntheticEdgesForNode to use constants and mark synthetic edges - Update BuildGraphFromStations to set Synthetic field - Mark synthetic edges in GeoJSON output as dashed lines - Write TestSyntheticAirportCityEdges and TestRouteWithSyntheticAirportCityEdges tests
231 lines
7.0 KiB
Go
231 lines
7.0 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/go-redis/redis/v8"
|
|
|
|
"trip-planner/internal/cache"
|
|
"trip-planner/internal/routing"
|
|
"trip-planner/internal/yandex"
|
|
)
|
|
|
|
// HandlerContext holds the dependencies for API handlers.
|
|
type HandlerContext struct {
|
|
Cache cache.Cache
|
|
Redis *redis.Client
|
|
Router *routing.Graph
|
|
Yandex *yandex.Client
|
|
SearchCache *routing.SearchCacheService
|
|
}
|
|
|
|
// stationStatusResponse represents the response for station status.
|
|
type stationStatusResponse struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Status string `json:"status"` // "active" or "closed"
|
|
Transport string `json:"transport"` // e.g., "train", "plane", "bus"
|
|
}
|
|
|
|
// cityResponse represents the response for city autocomplete.
|
|
type cityResponse []string
|
|
|
|
// routeSearchResponse represents the response for route search.
|
|
type routeSearchResponse struct {
|
|
Routes []interface{} `json:"routes"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
// routeGeoJSONResponse represents the response for route GeoJSON.
|
|
type routeGeoJSONResponse struct {
|
|
Type string `json:"type"`
|
|
Features []map[string]interface{} `json:"features"`
|
|
SyntheticEdgeStyle map[string]string `json:"synthetic_edge_style,omitempty"`
|
|
}
|
|
|
|
// CityAutocomplete handles GET /v1/cities?query=.
|
|
func CityAutocomplete(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
|
query := r.URL.Query().Get("query")
|
|
if query == "" {
|
|
http.Error(w, "missing query parameter", http.StatusBadRequest)
|
|
return
|
|
}
|
|
// In a full implementation, would query Postgres for city matches
|
|
// For now, return a simple JSON response
|
|
resp := []cityResponse{{query + "-result1", query + "-result2"}}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
// CityStations handles GET /v1/cities/{id}/stations.
|
|
func CityStations(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
|
parts := strings.Split(r.URL.Path, "/")
|
|
if len(parts) < 4 {
|
|
http.Error(w, "invalid city ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
_ = parts[3] // city ID captured for future use
|
|
// In a full implementation, would look up city and its stations from Postgres
|
|
// For now, return a simple JSON response
|
|
resp := cityResponse{"station1", "station2"}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
// RouteSearch handles POST /v1/routes/search.
|
|
func RouteSearch(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
FromCityID string `json:"from_city_id"`
|
|
ToCityID string `json:"to_city_id"`
|
|
Date string `json:"date"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
// In a full implementation, would search routes using the graph and Yandex API
|
|
// For now, return a simple JSON response
|
|
resp := routeSearchResponse{
|
|
Routes: []interface{}{},
|
|
Count: 0,
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
// RouteGeoJSON handles GET /v1/routes/{search_id}/{route_id}/geojson.
|
|
func RouteGeoJSON(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
|
parts := strings.Split(r.URL.Path, "/")
|
|
if len(parts) < 4 {
|
|
http.Error(w, "invalid route ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Generate GeoJSON from the graph's edges, distinguishing synthetic vs real
|
|
// Synthetic edges (e.g., city↔airport transfers) are marked with dashed lines
|
|
// Real edges (actual scheduled trips) are solid lines
|
|
features := make([]map[string]interface{}, 0)
|
|
|
|
for _, edge := range hc.Router.Edges() {
|
|
// Determine line style based on edge type
|
|
strokeColor := "#1976d2" // default blue for train
|
|
strokeDasharray := "" // solid for real edges
|
|
|
|
if edge.Synthetic {
|
|
strokeDasharray = "5, 5" // dashed line for synthetic edges
|
|
}
|
|
|
|
// Color by transport type
|
|
switch edge.TransportType {
|
|
case routing.TransportTypePlane:
|
|
strokeColor = "#ff9800" // orange for plane
|
|
case routing.TransportTypeBus:
|
|
strokeColor = "#cddc39" // lime for bus
|
|
case routing.TransportTypeTrain:
|
|
strokeColor = "#1976d2" // blue for train (default)
|
|
}
|
|
|
|
// Create LineString geometry
|
|
// Use edge endpoints as coordinate placeholders
|
|
fromCoord := []float64{0, 0} // placeholder
|
|
toCoord := []float64{0, 0} // placeholder
|
|
|
|
// In a full implementation, would use actual node coordinates from PostGIS
|
|
// For now, use fixed placeholder coordinates
|
|
geoJsonLine := map[string]interface{}{
|
|
"type": "LineString",
|
|
"coordinates": []interface{}{
|
|
fromCoord, toCoord,
|
|
},
|
|
"properties": map[string]interface{}{
|
|
"transport": edge.Transport,
|
|
"transport_type": string(edge.TransportType),
|
|
"kind": "real",
|
|
"synthetic": edge.Synthetic,
|
|
"duration": edge.Duration,
|
|
"cost": edge.Cost,
|
|
"is_transfer": edge.IsTransfer,
|
|
"stroke_color": strokeColor,
|
|
"stroke_width": 2,
|
|
"stroke_dasharray": strokeDasharray,
|
|
},
|
|
}
|
|
|
|
features = append(features, map[string]interface{}{
|
|
"type": "Feature",
|
|
"geometry": geoJsonLine,
|
|
"properties": map[string]interface{}{},
|
|
})
|
|
}
|
|
|
|
resp := routeGeoJSONResponse{
|
|
Type: "FeatureCollection",
|
|
Features: features,
|
|
SyntheticEdgeStyle: map[string]string{"stroke_dasharray": "5, 5", "stroke_color": "#ff9800"},
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
// StationStatus handles GET /v1/stations/{id}/status.
|
|
func StationStatus(hc *HandlerContext, w http.ResponseWriter, r *http.Request) {
|
|
// Extract station ID from path: /v1/stations/{id}/status
|
|
parts := strings.Split(r.URL.Path, "/")
|
|
// Expected: /v1/stations/{id}/status
|
|
if len(parts) < 4 {
|
|
http.Error(w, "invalid station ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
stationID := parts[3]
|
|
|
|
// Find the station node in the graph
|
|
station := hc.Router.NodesByID(stationID)
|
|
|
|
// Check if the station has real edges (scheduled trips)
|
|
hasRealEdges := false
|
|
if station != nil {
|
|
for _, edge := range hc.Router.Edges() {
|
|
if edge.From.ID == stationID || edge.To.ID == stationID {
|
|
if edge.Kind == routing.EdgeKindReal {
|
|
hasRealEdges = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
status := "active"
|
|
if !hasRealEdges {
|
|
status = "closed"
|
|
}
|
|
|
|
name := ""
|
|
if station != nil {
|
|
name = station.Name
|
|
}
|
|
|
|
resp := stationStatusResponse{
|
|
ID: stationID,
|
|
Name: name,
|
|
Status: status,
|
|
Transport: "unknown",
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
// NewHandlerContext creates a new HandlerContext with initialized services.
|
|
func NewHandlerContext(redisClient *redis.Client, router *routing.Graph, yandex *yandex.Client) *HandlerContext {
|
|
cacheStore := cache.NewCacheStore(redisClient)
|
|
return &HandlerContext{
|
|
Cache: cacheStore,
|
|
Redis: redisClient,
|
|
Router: router,
|
|
Yandex: yandex,
|
|
SearchCache: routing.NewSearchCacheService(cache.NewCacheAside(cacheStore), yandex),
|
|
}
|
|
}
|