package travel import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" ) type OpenRouteClient struct { apiKey string baseURL string httpClient *http.Client } func (c *OpenRouteClient) HasAPIKey() bool { return c.apiKey != "" } type OpenRouteConfig struct { APIKey string BaseURL string } func NewOpenRouteClient(cfg OpenRouteConfig) *OpenRouteClient { baseURL := cfg.BaseURL if baseURL == "" { baseURL = "https://api.openrouteservice.org" } return &OpenRouteClient{ apiKey: cfg.APIKey, baseURL: baseURL, httpClient: &http.Client{ Timeout: 30 * time.Second, }, } } func (c *OpenRouteClient) doRequest(ctx context.Context, method, path string, query url.Values) ([]byte, error) { fullURL := c.baseURL + path if len(query) > 0 { fullURL += "?" + query.Encode() } req, err := http.NewRequestWithContext(ctx, method, fullURL, nil) if err != nil { return nil, fmt.Errorf("create request: %w", err) } req.Header.Set("Authorization", c.apiKey) req.Header.Set("Accept", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("execute request: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read response: %w", err) } if resp.StatusCode >= 400 { return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(body)) } return body, nil } func (c *OpenRouteClient) GetDirections(ctx context.Context, points []GeoLocation, profile string) (*RouteDirection, error) { if len(points) < 2 { return nil, fmt.Errorf("at least 2 points required") } if profile == "" { profile = "driving-car" } coords := make([]string, len(points)) for i, p := range points { coords[i] = fmt.Sprintf("%f,%f", p.Lng, p.Lat) } query := url.Values{} query.Set("start", coords[0]) query.Set("end", coords[len(coords)-1]) body, err := c.doRequest(ctx, "GET", "/v2/directions/"+profile, query) if err != nil { return nil, err } var response struct { Features []struct { Geometry struct { Coordinates [][2]float64 `json:"coordinates"` Type string `json:"type"` } `json:"geometry"` Properties struct { Summary struct { Distance float64 `json:"distance"` Duration float64 `json:"duration"` } `json:"summary"` Segments []struct { Distance float64 `json:"distance"` Duration float64 `json:"duration"` Steps []struct { Instruction string `json:"instruction"` Distance float64 `json:"distance"` Duration float64 `json:"duration"` Type int `json:"type"` } `json:"steps"` } `json:"segments"` } `json:"properties"` } `json:"features"` } if err := json.Unmarshal(body, &response); err != nil { return nil, fmt.Errorf("unmarshal directions: %w", err) } if len(response.Features) == 0 { return nil, fmt.Errorf("no route found") } feature := response.Features[0] direction := &RouteDirection{ Geometry: RouteGeometry{ Coordinates: feature.Geometry.Coordinates, Type: feature.Geometry.Type, }, Distance: feature.Properties.Summary.Distance, Duration: feature.Properties.Summary.Duration, } for _, seg := range feature.Properties.Segments { for _, step := range seg.Steps { direction.Steps = append(direction.Steps, RouteStep{ Instruction: step.Instruction, Distance: step.Distance, Duration: step.Duration, Type: fmt.Sprintf("%d", step.Type), }) } } return direction, nil } func (c *OpenRouteClient) Geocode(ctx context.Context, query string) (*GeoLocation, error) { params := url.Values{} params.Set("api_key", c.apiKey) params.Set("text", query) params.Set("size", "1") body, err := c.doRequest(ctx, "GET", "/geocode/search", params) if err != nil { return nil, err } var response struct { Features []struct { Geometry struct { Coordinates [2]float64 `json:"coordinates"` } `json:"geometry"` Properties struct { Name string `json:"name"` Country string `json:"country"` Label string `json:"label"` } `json:"properties"` } `json:"features"` } if err := json.Unmarshal(body, &response); err != nil { return nil, fmt.Errorf("unmarshal geocode: %w", err) } if len(response.Features) == 0 { return nil, fmt.Errorf("location not found: %s", query) } feature := response.Features[0] return &GeoLocation{ Lng: feature.Geometry.Coordinates[0], Lat: feature.Geometry.Coordinates[1], Name: feature.Properties.Name, Country: feature.Properties.Country, }, nil } func (c *OpenRouteClient) ReverseGeocode(ctx context.Context, lat, lng float64) (*GeoLocation, error) { params := url.Values{} params.Set("api_key", c.apiKey) params.Set("point.lat", fmt.Sprintf("%f", lat)) params.Set("point.lon", fmt.Sprintf("%f", lng)) params.Set("size", "1") body, err := c.doRequest(ctx, "GET", "/geocode/reverse", params) if err != nil { return nil, err } var response struct { Features []struct { Properties struct { Name string `json:"name"` Country string `json:"country"` Label string `json:"label"` } `json:"properties"` } `json:"features"` } if err := json.Unmarshal(body, &response); err != nil { return nil, fmt.Errorf("unmarshal reverse geocode: %w", err) } if len(response.Features) == 0 { return nil, fmt.Errorf("location not found at %f,%f", lat, lng) } feature := response.Features[0] return &GeoLocation{ Lat: lat, Lng: lng, Name: feature.Properties.Name, Country: feature.Properties.Country, }, nil } func (c *OpenRouteClient) SearchPOI(ctx context.Context, req POISearchRequest) ([]POI, error) { params := url.Values{} params.Set("api_key", c.apiKey) params.Set("request", "pois") params.Set("geometry", fmt.Sprintf(`{"geojson":{"type":"Point","coordinates":[%f,%f]},"buffer":%d}`, req.Lng, req.Lat, req.Radius)) if len(req.Categories) > 0 { params.Set("filters", fmt.Sprintf(`{"category_ids":[%s]}`, strings.Join(req.Categories, ","))) } limit := req.Limit if limit == 0 { limit = 20 } params.Set("limit", fmt.Sprintf("%d", limit)) body, err := c.doRequest(ctx, "POST", "/pois", params) if err != nil { return nil, err } var response struct { Features []struct { Geometry struct { Coordinates [2]float64 `json:"coordinates"` } `json:"geometry"` Properties struct { OSMId int64 `json:"osm_id"` Name string `json:"osm_tags.name"` Category struct { ID int `json:"category_id"` Name string `json:"category_name"` } `json:"category"` Distance float64 `json:"distance"` } `json:"properties"` } `json:"features"` } if err := json.Unmarshal(body, &response); err != nil { return nil, fmt.Errorf("unmarshal POI: %w", err) } pois := make([]POI, 0, len(response.Features)) for _, f := range response.Features { poi := POI{ ID: fmt.Sprintf("%d", f.Properties.OSMId), Name: f.Properties.Name, Lng: f.Geometry.Coordinates[0], Lat: f.Geometry.Coordinates[1], Category: f.Properties.Category.Name, Distance: f.Properties.Distance, } pois = append(pois, poi) } return pois, nil } func (c *OpenRouteClient) GetIsochrone(ctx context.Context, lat, lng float64, timeMinutes int, profile string) (*RouteGeometry, error) { if profile == "" { profile = "driving-car" } params := url.Values{} params.Set("api_key", c.apiKey) params.Set("locations", fmt.Sprintf("%f,%f", lng, lat)) params.Set("range", fmt.Sprintf("%d", timeMinutes*60)) body, err := c.doRequest(ctx, "GET", "/v2/isochrones/"+profile, params) if err != nil { return nil, err } var response struct { Features []struct { Geometry struct { Coordinates [][][2]float64 `json:"coordinates"` Type string `json:"type"` } `json:"geometry"` } `json:"features"` } if err := json.Unmarshal(body, &response); err != nil { return nil, fmt.Errorf("unmarshal isochrone: %w", err) } if len(response.Features) == 0 { return nil, fmt.Errorf("no isochrone found") } coords := make([][2]float64, 0) if len(response.Features[0].Geometry.Coordinates) > 0 { coords = response.Features[0].Geometry.Coordinates[0] } return &RouteGeometry{ Coordinates: coords, Type: response.Features[0].Geometry.Type, }, nil }