Files
gooseek/backend/internal/travel/openroute.go
home 08bd41e75c feat: travel service with 2GIS routing, POI, hotels + finance providers + UI overhaul
- Add travel-svc microservice (Amadeus, TravelPayouts, 2GIS, OpenRouteService)
- Add travel orchestrator with parallel collectors (events, POI, hotels, flights)
- Add 2GIS road routing with transport cost calculation (car/bus/taxi)
- Add TravelMap (2GIS MapGL) and TravelWidgets components
- Add useTravelChat hook for streaming travel agent responses
- Add finance heatmap providers refactor
- Add SearXNG settings, API proxy routes, Docker compose updates
- Update Dockerfiles, config, types, and all UI pages for consistency

Made-with: Cursor
2026-03-01 21:58:32 +03:00

336 lines
8.2 KiB
Go

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
}