From 2a17c5c1333d9fafbd5e1b3e34b846e6bf7018ad Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Tue, 17 Feb 2026 16:23:56 +0000 Subject: [PATCH] Add initial work on TomTom integration for routing --- tomtom/.air.toml | 52 ++++++ tomtom/example/route/main.go | 53 ++++++ tomtom/route.go | 150 +++++++++++++++++ tomtom/routing.go | 1 + tomtom/tomtom.go | 31 ++++ tomtom/types.go | 307 +++++++++++++++++++++++++++++++++++ 6 files changed, 594 insertions(+) create mode 100644 tomtom/.air.toml create mode 100644 tomtom/example/route/main.go create mode 100644 tomtom/route.go create mode 100644 tomtom/routing.go create mode 100644 tomtom/tomtom.go create mode 100644 tomtom/types.go diff --git a/tomtom/.air.toml b/tomtom/.air.toml new file mode 100644 index 00000000..ae997d6c --- /dev/null +++ b/tomtom/.air.toml @@ -0,0 +1,52 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./tmp/main" + cmd = "go build -o ./tmp/main ./example/route" + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = true + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + silent = false + time = false + +[misc] + clean_on_exit = false + +[proxy] + app_port = 0 + enabled = false + proxy_port = 0 + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/tomtom/example/route/main.go b/tomtom/example/route/main.go new file mode 100644 index 00000000..b1aab762 --- /dev/null +++ b/tomtom/example/route/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "fmt" + "log" + + "github.com/Gleipnir-Technology/nidus-sync/tomtom" +) + +func main() { + client := tomtom.NewClient() + + // Example 1: Calculate a simple route + traffic := false + routeRequest := &tomtom.CalculateRouteRequest{ + Params: tomtom.CalculateRouteParams{ + VersionNumber: 1, + Locations: "52.50931,13.42936:52.50274,13.43872", + ContentType: "json", + Traffic: &traffic, + TravelMode: "car", + RouteType: "fastest", + }, + } + + routeResp, err := client.CalculateRoute(routeRequest) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Route distance: %d meters\n", routeResp.Routes[0].Summary.LengthInMeters) + + // Example 2: Calculate reachable range + /* + energyBudget := 43.0 + rangeRequest := &tomtom.CalculateReachableRangeRequest{ + Params: tomtom.CalculateReachableRangeParams{ + VersionNumber: 1, + Origin: "52.50931,13.42936", + ContentType: "json", + EnergyBudgetInkWh: &energyBudget, + VehicleEngineType: "electric", + }, + } + + rangeResp, err := client.CalculateReachableRange(rangeRequest) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Reachable range includes %d points in the polygon\n", len(rangeResp.Polygon.Exterior)) + */ +} diff --git a/tomtom/route.go b/tomtom/route.go new file mode 100644 index 00000000..6b1a0f3e --- /dev/null +++ b/tomtom/route.go @@ -0,0 +1,150 @@ +package tomtom + +import ( + "fmt" + "net/url" + "strconv" + + "github.com/google/go-querystring/query" +) + +// CalculateRouteRequest combines both path parameters and POST body +type CalculateRouteRequest struct { + Params CalculateRouteParams + PostData *CalculateRoutePostData +} + +func (sgr CalculateRouteRequest) toQueryParams() (url.Values, error) { + return query.Values(sgr) +} + +// BuildURL builds the URL for the Calculate Route request +func (req *CalculateRouteRequest) BuildURL(apiKey string) (string, error) { + baseURL := fmt.Sprintf("%s/routing/%d/calculateRoute/%s/%s", + BaseURL, + req.Params.VersionNumber, + req.Params.Locations, + req.Params.ContentType) + + // Add query parameters + query := url.Values{} + query.Add("key", apiKey) + + // Add all the query parameters if they're set + if req.Params.MaxAlternatives != nil { + query.Add("maxAlternatives", strconv.Itoa(*req.Params.MaxAlternatives)) + } + + if req.Params.AlternativeType != "" { + query.Add("alternativeType", req.Params.AlternativeType) + } + + if req.Params.MinDeviationDistance != nil { + query.Add("minDeviationDistance", strconv.Itoa(*req.Params.MinDeviationDistance)) + } + + if req.Params.MinDeviationTime != nil { + query.Add("minDeviationTime", strconv.Itoa(*req.Params.MinDeviationTime)) + } + + if req.Params.InstructionsType != "" { + query.Add("instructionsType", req.Params.InstructionsType) + } + + if req.Params.Language != "" { + query.Add("language", req.Params.Language) + } + + if req.Params.ComputeBestOrder != nil { + query.Add("computeBestOrder", strconv.FormatBool(*req.Params.ComputeBestOrder)) + } + + if req.Params.RouteRepresentation != "" { + query.Add("routeRepresentation", req.Params.RouteRepresentation) + } + + if req.Params.ComputeTravelTimeFor != "" { + query.Add("computeTravelTimeFor", req.Params.ComputeTravelTimeFor) + } + + if req.Params.VehicleHeading != nil { + query.Add("vehicleHeading", strconv.Itoa(*req.Params.VehicleHeading)) + } + + for _, sectionType := range req.Params.SectionType { + query.Add("sectionType", sectionType) + } + + if req.Params.IncludeTollPaymentTypes != "" { + query.Add("includeTollPaymentTypes", req.Params.IncludeTollPaymentTypes) + } + + if req.Params.Callback != "" { + query.Add("callback", req.Params.Callback) + } + + if req.Params.Report != "" { + query.Add("report", req.Params.Report) + } + + if req.Params.DepartAt != "" { + query.Add("departAt", req.Params.DepartAt) + } + + if req.Params.ArriveAt != "" { + query.Add("arriveAt", req.Params.ArriveAt) + } + + if req.Params.RouteType != "" { + query.Add("routeType", req.Params.RouteType) + } + + if req.Params.Traffic != nil { + query.Add("traffic", strconv.FormatBool(*req.Params.Traffic)) + } + + for _, avoid := range req.Params.Avoid { + query.Add("avoid", avoid) + } + + if req.Params.TravelMode != "" { + query.Add("travelMode", req.Params.TravelMode) + } + + // Add other parameters similarly... + // Too many to list all here, but the pattern is the same + + return baseURL + "?" + query.Encode(), nil +} + +// CalculateRoute sends a route calculation request to TomTom API +func (c *TomTom) CalculateRoute(req *CalculateRouteRequest) (*CalculateRouteResponse, error) { + /*url, err := req.BuildURL(c.APIKey) + if err != nil { + return nil, err + }*/ + + var result CalculateRouteResponse + /* + query, err := req.toQueryParams() + if err != nil { + return nil, fmt.Errorf("structured geocode query: %w", err) + } + */ + + resp, err := c.client.R(). + //SetQueryParamsFromValues(query). + SetResult(&result). + SetPathParam("locations", req.Params.Locations). + SetPathParam("urlBase", c.urlBase). + SetQueryParam("key", c.APIKey). + Get("https://{urlBase}/routing/1/calculateRoute/{locations}/json") + if err != nil { + return nil, fmt.Errorf("calculate route get: %w", err) + } + if !resp.IsSuccess() { + return nil, fmt.Errorf("calculate route status: %d", resp.Status) + } + + return &result, nil +} diff --git a/tomtom/routing.go b/tomtom/routing.go new file mode 100644 index 00000000..ef66c0bb --- /dev/null +++ b/tomtom/routing.go @@ -0,0 +1 @@ +package tomtom diff --git a/tomtom/tomtom.go b/tomtom/tomtom.go new file mode 100644 index 00000000..8c1b43ce --- /dev/null +++ b/tomtom/tomtom.go @@ -0,0 +1,31 @@ +package tomtom + +import ( + "os" + + "resty.dev/v3" +) + +type TomTom struct { + APIKey string + + client *resty.Client + urlBase string +} + +func NewClient() *TomTom { + //logger := NewLogger(log.Logger) + //r := resty.New().SetLogger(logger).SetDebug(true) + r := resty.New().SetDebug(true) + api_key := os.Getenv("TOMTOM_API_KEY") + //r := resty.New() + return &TomTom{ + APIKey: api_key, + client: r, + urlBase: "api.tomtom.com", + } +} + +func (s *TomTom) Close() { + s.client.Close() +} diff --git a/tomtom/types.go b/tomtom/types.go new file mode 100644 index 00000000..c4ee008e --- /dev/null +++ b/tomtom/types.go @@ -0,0 +1,307 @@ +package tomtom + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" +) + +// Base URLs and API constants +const ( + BaseURL = "https://api.tomtom.com" +) + +// Common structs + +// Coordinates represents latitude and longitude values +type Coordinates struct { + Latitude string `json:"latitude" xml:"latitude,attr"` + Longitude string `json:"longitude" xml:"longitude,attr"` +} + +// Rectangle represents a geographic rectangle +type Rectangle struct { + SouthWestCorner Coordinates `json:"southWestCorner" xml:"southWestCorner"` + NorthEastCorner Coordinates `json:"northEastCorner" xml:"northEastCorner"` +} + +// AvoidAreas represents areas to avoid in routing +type AvoidAreas struct { + Rectangles []Rectangle `json:"rectangles" xml:"rectangles>rectangle"` +} + +// SupportingPoint represents a supporting point in the route calculation +type SupportingPoint struct { + Latitude string `json:"latitude" xml:"latitude,attr"` + Longitude string `json:"longitude" xml:"longitude,attr"` +} + +// Client represents a TomTom API client +type Client struct { + APIKey string + HTTPClient *http.Client +} + +// CalculateRoute API structures + +// CalculateRouteParams holds the parameters for the Calculate Route API +type CalculateRouteParams struct { + // Path parameters + VersionNumber int + Locations string + ContentType string // "json" or "jsonp" + + // Query parameters + MaxAlternatives *int + AlternativeType string + MinDeviationDistance *int + MinDeviationTime *int + InstructionsType string + Language string + ComputeBestOrder *bool + RouteRepresentation string + ComputeTravelTimeFor string + VehicleHeading *int + SectionType []string + IncludeTollPaymentTypes string + Callback string + Report string + DepartAt string + ArriveAt string + RouteType string + Traffic *bool + Avoid []string + TravelMode string + Hilliness string + Windingness string + VehicleMaxSpeed *int + VehicleWeight *int + VehicleAxleWeight *int + VehicleNumberOfAxles *int + VehicleLength *float64 + VehicleWidth *float64 + VehicleHeight *float64 + VehicleCommercial *bool + VehicleLoadType []string + VehicleAdrTunnelRestrictionCode string + VehicleHasElectricTollCollectionTransponder string + VehicleEngineType string + ConstantSpeedConsumptionInLitersPerHundredkm string + CurrentFuelInLiters *float64 + AuxiliaryPowerInLitersPerHour *float64 + FuelEnergyDensityInMJoulesPerLiter *float64 + AccelerationEfficiency *float64 + DecelerationEfficiency *float64 + UphillEfficiency *float64 + DownhillEfficiency *float64 + ConsumptionInkWhPerkmAltitudeGain *float64 + RecuperationInkWhPerkmAltitudeLoss *float64 + ConstantSpeedConsumptionInkWhPerHundredkm string + CurrentChargeInkWh *float64 + MaxChargeInkWh *float64 + AuxiliaryPowerInkW *float64 +} + +// CalculateRoutePostData represents the POST body for Calculate Route API +type CalculateRoutePostData struct { + SupportingPoints []SupportingPoint `json:"supportingPoints,omitempty" xml:"supportingPoints>supportingPoint,omitempty"` + AvoidVignette []string `json:"avoidVignette,omitempty" xml:"avoidVignette,omitempty"` + AllowVignette []string `json:"allowVignette,omitempty" xml:"allowVignette,omitempty"` + AvoidAreas *AvoidAreas `json:"avoidAreas,omitempty" xml:"avoidAreas,omitempty"` +} + +// Route response structures - These would need to be completed based on actual API response +type Summary struct { + LengthInMeters int `json:"lengthInMeters"` + TravelTimeInSeconds int `json:"travelTimeInSeconds"` + TrafficDelayInSeconds int `json:"trafficDelayInSeconds"` + DepartureTime string `json:"departureTime"` + ArrivalTime string `json:"arrivalTime"` + FuelConsumptionInLiters float64 `json:"fuelConsumptionInLiters,omitempty"` + ElectricEnergyConsumptionInkWh float64 `json:"electricEnergyConsumptionInkWh,omitempty"` +} + +type Point struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` +} + +type Polyline struct { + Points []Point `json:"points"` +} + +type Leg struct { + Summary Summary `json:"summary"` + Points []Point `json:"points,omitempty"` + Polyline Polyline `json:"polyline,omitempty"` +} + +type Route struct { + Summary Summary `json:"summary"` + Legs []Leg `json:"legs,omitempty"` + Polyline Polyline `json:"polyline,omitempty"` +} + +type CalculateRouteResponse struct { + Routes []Route `json:"routes"` +} + +// CalculateReachableRange API structures + +// CalculateReachableRangeParams holds the parameters for the Calculate Reachable Range API +type CalculateReachableRangeParams struct { + // Path parameters + VersionNumber int + Origin string + ContentType string // "json" or "jsonp" + + // Query parameters + FuelBudgetInLiters *float64 + EnergyBudgetInkWh *float64 + TimeBudgetInSec *float64 + Callback string + Report string + DepartAt string + ArriveAt string + RouteType string + Traffic *bool + Avoid []string + TravelMode string + Hilliness string + Windingness string + VehicleMaxSpeed *int + VehicleWeight *int + VehicleAxleWeight *int + VehicleNumberOfAxles *int + VehicleLength *float64 + VehicleWidth *float64 + VehicleHeight *float64 + VehicleCommercial *bool + VehicleLoadType []string + VehicleAdrTunnelRestrictionCode string + VehicleHasElectricTollCollectionTransponder string + VehicleEngineType string + ConstantSpeedConsumptionInLitersPerHundredkm string + CurrentFuelInLiters *float64 + AuxiliaryPowerInLitersPerHour *float64 + FuelEnergyDensityInMJoulesPerLiter *float64 + AccelerationEfficiency *float64 + DecelerationEfficiency *float64 + UphillEfficiency *float64 + DownhillEfficiency *float64 + ConsumptionInkWhPerkmAltitudeGain *float64 + RecuperationInkWhPerkmAltitudeLoss *float64 + ConstantSpeedConsumptionInkWhPerHundredkm string + CurrentChargeInkWh *float64 + MaxChargeInkWh *float64 + AuxiliaryPowerInkW *float64 +} + +// CalculateReachableRangePostData represents the POST body for Calculate Reachable Range API +type CalculateReachableRangePostData struct { + AvoidVignette []string `json:"avoidVignette,omitempty" xml:"avoidVignette,omitempty"` + AllowVignette []string `json:"allowVignette,omitempty" xml:"allowVignette,omitempty"` + AvoidAreas *AvoidAreas `json:"avoidAreas,omitempty" xml:"avoidAreas,omitempty"` +} + +// CalculateReachableRangeRequest combines both path parameters and POST body +type CalculateReachableRangeRequest struct { + Params CalculateReachableRangeParams + PostData *CalculateReachableRangePostData +} + +// Reachable Range response structures +type Polygon struct { + Exterior []Point `json:"exterior"` + Interior [][]Point `json:"interior,omitempty"` +} + +type CalculateReachableRangeResponse struct { + Polygon Polygon `json:"polygon"` + Summary struct { + DistanceLimit float64 `json:"distanceLimit,omitempty"` + TimeLimit int `json:"timeLimit,omitempty"` + FuelConsumptionLimit float64 `json:"fuelConsumptionLimit,omitempty"` + EnergyConsumptionLimit float64 `json:"energyConsumptionLimit,omitempty"` + } `json:"summary"` +} + +// BuildURL builds the URL for the Calculate Reachable Range request +func (req *CalculateReachableRangeRequest) BuildURL(apiKey string) (string, error) { + baseURL := fmt.Sprintf("%s/routing/%d/calculateReachableRange/%s/%s", + BaseURL, + req.Params.VersionNumber, + req.Params.Origin, + req.Params.ContentType) + + // Add query parameters + query := url.Values{} + query.Add("key", apiKey) + + if req.Params.FuelBudgetInLiters != nil { + query.Add("fuelBudgetInLiters", fmt.Sprintf("%f", *req.Params.FuelBudgetInLiters)) + } + + if req.Params.EnergyBudgetInkWh != nil { + query.Add("energyBudgetInkWh", fmt.Sprintf("%f", *req.Params.EnergyBudgetInkWh)) + } + + if req.Params.TimeBudgetInSec != nil { + query.Add("timeBudgetInSec", fmt.Sprintf("%f", *req.Params.TimeBudgetInSec)) + } + + // Add other parameters similarly... + + return baseURL + "?" + query.Encode(), nil +} + +// Client methods for executing requests + +// CalculateReachableRange sends a reachable range calculation request to TomTom API +func (c *Client) CalculateReachableRange(req *CalculateReachableRangeRequest) (*CalculateReachableRangeResponse, error) { + url, err := req.BuildURL(c.APIKey) + if err != nil { + return nil, err + } + + var response CalculateReachableRangeResponse + var httpReq *http.Request + + if req.PostData != nil { + // POST request + jsonData, err := json.Marshal(req.PostData) + if err != nil { + return nil, err + } + httpReq, err = http.NewRequest("POST", url, strings.NewReader(string(jsonData))) + if err != nil { + return nil, err + } + httpReq.Header.Set("Content-Type", "application/json") + } else { + // GET request + httpReq, err = http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + } + + resp, err := c.HTTPClient.Do(httpReq) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("API error: %s", resp.Status) + } + + err = json.NewDecoder(resp.Body).Decode(&response) + if err != nil { + return nil, err + } + + return &response, nil +}