diff --git a/CLEANUP.md b/CLEANUP.md index e331ec58..fb9f1de9 100644 --- a/CLEANUP.md +++ b/CLEANUP.md @@ -115,9 +115,15 @@ Once all routes are ported or confirmed dead, remove the entire `html/template/` --- -## 3. esbuild (`build.js`) — Removed ✅ +## 3. esbuild (`build.js`) — Dead Build Tool -*(Completed 2026-05-09: `build.js` removed and `pkgs.esbuild` dropped from flake.nix devShell — Vite is the build tool)* +**Status:** `build.js` is an esbuild-based build script that predates the Vite migration. It is not referenced by `package.json` scripts (those use `vite build`). It is also not referenced by any CI or Nix config. + +### 3a. Remove esbuild-related files + +- `build.js` — the esbuild build script +- Remove `pkgs.esbuild` from `flake.nix` devShell dependencies (if not used elsewhere) +- Remove esbuild-related dependencies from `package.json` if present (currently no esbuild deps are in `package.json` — they may have been already cleaned) --- @@ -163,9 +169,14 @@ The map-locator, address-suggestion, and photo-upload functionality has Vue equi --- -## 5. TomTom Integration — Removed ✅ +## 5. TomTom Integration — Unused -*(Completed 2026-05-09: `tomtom/` directory removed — zero imports outside itself, Stadia Maps is now the geocoding/tile provider)* +**Status:** The `tomtom/` directory contains a TomTom routing/geocoding client. It is not imported by any file outside the `tomtom/` directory. Stadia Maps is now used for geocoding and tiles. + +### 5a. Remove TomTom entirely + +- `tomtom/` — entire directory +- `tomtom/example/` — example code --- @@ -236,7 +247,9 @@ Verify that all code references use the external package, not a local path. ## 10. Old Generated Files & Artifacts -### 10a. `query.go` at project root — Removed ✅ +### 10a. `query.go` at project root + +Contains a commented-out Bob query interface and an unused `QueryWriter` interface. The `insertQueryToString` function is entirely commented out. Either repurpose or remove. ### 10b. `db/sql/` directory @@ -282,10 +295,10 @@ Empty placeholder file. Remove. ## Priority Summary 1. **High impact, low effort:** - - ~~Remove `tomtom/` (unused, no imports)~~ ✅ - - ~~Remove `build.js` (dead, replaced by Vite)~~ ✅ + - Remove `tomtom/` (unused, no imports) + - Remove `build.js` (dead, replaced by Vite) - Remove commented-out routes in `sync/routes.go` and `rmo/routes.go` - - ~~Remove `query.go` commented-out code~~ ✅ + - Remove `query.go` commented-out code - Remove `static/gen/main.js` stale artifact - Remove `static/css/placeholder` diff --git a/build.js b/build.js new file mode 100644 index 00000000..8c94ffed --- /dev/null +++ b/build.js @@ -0,0 +1,92 @@ +import esbuild from "esbuild"; +import vue from "esbuild-plugin-vue3"; +import { sassPlugin } from "esbuild-sass-plugin"; + +const args = process.argv.slice(2); +const watch = args.includes("--watch"); +const minify = args.includes("--minify"); + +// Plugin to show build status +const buildStatusPlugin = { + name: "build-status", + setup(build) { + let buildStart; + + build.onStart(() => { + buildStart = Date.now(); + // Clear console and move cursor to top + console.clear(); + console.log( + "\x1b[36m%s\x1b[0m", + `🔨 Building... [${new Date().toLocaleTimeString()}]`, + ); + }); + + build.onEnd((result) => { + const buildTime = Date.now() - buildStart; + if (result.errors.length > 0) { + console.log( + "\x1b[31m%s\x1b[0m", + `❌ Build failed (${buildTime}ms) [${new Date().toLocaleTimeString()}]`, + ); + } else { + console.log( + "\x1b[32m%s\x1b[0m", + `✅ Build complete (${buildTime}ms) [${new Date().toLocaleTimeString()}]`, + ); + } + console.log("\x1b[33m%s\x1b[0m", "\n👀 Watching for changes..."); + }); + }, +}; + +const config = { + entryPoints: ["ts/main.ts"], + bundle: true, + format: "esm", + plugins: [ + buildStatusPlugin, // Add this first + sassPlugin({ + quietDeps: true, + precompile(source, pathname) { + // Only inject variables into Vue component styles + // (not the main scss files to avoid circular imports) + if (pathname.endsWith(".vue")) { + return `@import "./ts/style/variables.scss";\n${source}`; + } + return source; + }, + silenceDeprecations: ["import"], + type: "css", + }), + vue({ + sourceMap: true, + }), + ], + sourcemap: true, + sourcesContent: true, + define: { + __VUE_OPTIONS_API__: "true", + __VUE_PROD_DEVTOOLS__: "false", + __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: "false", + }, + minify, + loader: { + ".css": "css", + ".woff": "file", + ".woff2": "file", + ".ttf": "file", + ".eot": "file", + }, + outdir: "static/gen", + outbase: "ts", + assetNames: "fonts/[name]", +}; + +if (watch) { + const ctx = await esbuild.context(config); + await ctx.watch(); + console.log("\x1b[33m%s\x1b[0m", "👀 Watching for changes...\n"); +} else { + await esbuild.build(config); +} diff --git a/flake.nix b/flake.nix index e937e801..486d275f 100644 --- a/flake.nix +++ b/flake.nix @@ -31,6 +31,7 @@ pkgs.air pkgs.autoprefixer pkgs.dart-sass + pkgs.esbuild pkgs.go pkgs.go-jet pkgs.golangci-lint diff --git a/query.go b/query.go new file mode 100644 index 00000000..0b50022e --- /dev/null +++ b/query.go @@ -0,0 +1,34 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "io" + //"github.com/Gleipnir-Technology/bob" + //"github.com/Gleipnir-Technology/bob/dialect/psql" +) + +type QueryWriter interface { + WriteQuery(ctx context.Context, w io.Writer, start int) ([]any, error) +} + +func queryToString(query QueryWriter) string { + buf := new(bytes.Buffer) + _, err := query.WriteQuery(context.TODO(), buf, 0) + if err != nil { + return fmt.Sprintf("Failed to write query to buffer: %v", err) + } + return buf.String() +} + +/* +func insertQueryToString(query bob.BaseQuery[*dialect.InsertQuery]) string { + buf := new(bytes.Buffer) + _, err := query.WriteQuery(context.TODO(), buf, 0) + if err != nil { + return fmt.Sprintf("Failed to write query: %v", err) + } + return buf.String() +} +*/ diff --git a/tomtom/.air.toml b/tomtom/.air.toml new file mode 100644 index 00000000..7c5738f8 --- /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/geocode-and-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/geocode-and-route/main.go b/tomtom/example/geocode-and-route/main.go new file mode 100644 index 00000000..0780d493 --- /dev/null +++ b/tomtom/example/geocode-and-route/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "log" + + "github.com/Gleipnir-Technology/nidus-sync/tomtom" +) + +func main() { + client := tomtom.NewClient() + + // Example 1: Geocode a series of addresses + waypoints := []string{ + "1737 W Houston Ave, Visalia, CA 93291", + "1138 W Prescott Ave, Visalia, CA 93291", + "3228 W Clinton Ct, Visalia, CA 93291", + "3800 N Mendonca St, Visalia, CA 93291", + } + coords := make([]tomtom.Point, 0) + for _, wp := range waypoints { + geocode, err := client.Geocode(wp) + if err != nil { + log.Fatal("Failed to geocode '%s': %w", wp, err) + } + if len(geocode.Results) == 0 { + log.Fatal("Failed to get any results for '%s'", wp) + } + result := geocode.Results[0] + coords = append(coords, result.Position.AsPoint()) + log.Printf("Geocoded %s to %f, %f", wp, result.Position.Longitude, result.Position.Latitude) + } + // Example 2: Calculate a simple route through them + traffic := false + routeRequest := &tomtom.CalculateRouteRequest{ + Locations: coords, + Traffic: &traffic, + TravelMode: tomtom.TravelModeCar, + RouteType: tomtom.RouteTypeFastest, + } + + //client.SetDebug(true) + routeResp, err := client.CalculateRoute(routeRequest) + if err != nil { + log.Fatal(err) + } + + all_points := make([]tomtom.Point, 0) + all_stops := make([]tomtom.Point, 0) + for i, route := range routeResp.Routes { + s := route.Summary + log.Printf("%d: %d meters, %d seconds, %s traffic delay", i, s.LengthInMeters, s.TravelTimeInSeconds, s.TrafficDelayInSeconds) + for _, leg := range route.Legs { + all_stops = append(all_stops, leg.Points[0]) + all_points = append(all_points, leg.Points...) + } + } + lines := tomtom.PolylineToGeoJSON(all_points) + log.Printf("%s", lines) + stops := tomtom.PolylineToGeoJSON(all_stops) + log.Printf("%s", stops) +} diff --git a/tomtom/example/route-gps/main.go b/tomtom/example/route-gps/main.go new file mode 100644 index 00000000..de222f36 --- /dev/null +++ b/tomtom/example/route-gps/main.go @@ -0,0 +1,31 @@ +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{ + Locations: []tomtom.Point{ + tomtom.P(52.50931, 13.42936), + tomtom.P(52.50274, 13.43872), + }, + Traffic: &traffic, + TravelMode: tomtom.TravelModeCar, + RouteType: tomtom.RouteTypeFastest, + } + + routeResp, err := client.CalculateRoute(routeRequest) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Route distance: %d meters\n", routeResp.Routes[0].Summary.LengthInMeters) +} diff --git a/tomtom/geocode.go b/tomtom/geocode.go new file mode 100644 index 00000000..17b7947e --- /dev/null +++ b/tomtom/geocode.go @@ -0,0 +1,96 @@ +package tomtom + +import ( + "fmt" +) + +type PointShort struct { + Latitude float64 `json:"lat"` + Longitude float64 `json:"lon"` +} + +func (ps PointShort) AsPoint() Point { + return Point(ps) +} + +type GeocodeResult struct { + Type string `json:"type"` + ID string `json:"id"` + Score float64 `json:"score"` + Dist float64 `json:"dist"` + MatchConfidence MatchConfidence `json:"matchConfidence"` + Address Address `json:"address"` + Position PointShort `json:"position"` + Viewport Viewport `json:"viewport"` + EntryPoints []EntryPoint `json:"entryPoints"` +} + +// MatchConfidence represents the confidence score for a match +type MatchConfidence struct { + Score float64 `json:"score"` +} + +// Address contains detailed address information +type Address struct { + StreetNumber string `json:"streetNumber"` + StreetName string `json:"streetName"` + Municipality string `json:"municipality"` + CountrySecondarySubdivision string `json:"countrySecondarySubdivision"` + CountrySubdivision string `json:"countrySubdivision"` + CountrySubdivisionName string `json:"countrySubdivisionName"` + CountrySubdivisionCode string `json:"countrySubdivisionCode"` + PostalCode string `json:"postalCode"` + ExtendedPostalCode string `json:"extendedPostalCode"` + CountryCode string `json:"countryCode"` + Country string `json:"country"` + CountryCodeISO3 string `json:"countryCodeISO3"` + FreeformAddress string `json:"freeformAddress"` + LocalName string `json:"localName"` +} + +// Viewport defines a geographic bounding box +type Viewport struct { + TopLeftPoint PointShort `json:"topLeftPoint"` + BtmRightPoint PointShort `json:"btmRightPoint"` +} + +// EntryPoint contains information about a point of entry to a location +type EntryPoint struct { + Type string `json:"type"` + Position PointShort `json:"position"` +} +type GeocodeSummary struct { + Query string `json:"query"` + QueryType string `json:"queryType"` + QueryTime uint `json:"queryTime"` + NumResults uint `json:"numResults"` + Offset uint `json:"offset"` + TotalResults uint `json:"totalResults"` + FuzzyLevel uint `json:"fuzzyLevel"` + GeoBias PointShort `json:"geoBias"` +} +type GeocodeResponse struct { + Summary GeocodeSummary `json:"summary"` + Results []GeocodeResult `json:"results"` +} + +// CalculateRoute sends a route calculation request to TomTom API +func (c *TomTom) Geocode(address string) (*GeocodeResponse, error) { + var result GeocodeResponse + + resp, err := c.client.R(). + SetResult(&result). + SetPathParam("address", address). + SetPathParam("urlBase", c.urlBase). + SetQueryParam("key", c.APIKey). + SetQueryParam("storeResult", "false"). + Get("https://{urlBase}/search/2/geocode/{address}.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/geojson.go b/tomtom/geojson.go new file mode 100644 index 00000000..0879acae --- /dev/null +++ b/tomtom/geojson.go @@ -0,0 +1,27 @@ +package tomtom + +import ( + "fmt" + "strings" +) + +// Convert a slice of points to GeoJSON +func PolylineToGeoJSON(polyline []Point) string { + var sb strings.Builder + + sb.WriteString(`{"type":"LineString","coordinates":[`) + + for i, point := range polyline { + // IMPORTANT: GeoJSON uses [longitude, latitude] order! + sb.WriteString(fmt.Sprintf("[%g,%g]", point.Longitude, point.Latitude)) + + // Add comma if not the last point + if i < len(polyline)-1 { + sb.WriteString(",") + } + } + + sb.WriteString("]}") + + return sb.String() +} diff --git a/tomtom/route.go b/tomtom/route.go new file mode 100644 index 00000000..ae0534bc --- /dev/null +++ b/tomtom/route.go @@ -0,0 +1,126 @@ +package tomtom + +import ( + "fmt" + "net/url" + "strings" + + "github.com/google/go-querystring/query" +) + +// CalculateRouteRequest combines both path parameters and POST body +type CalculateRouteRequest struct { + // Path parameters + Locations Locations + + // 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 +} + +func (sgr CalculateRouteRequest) toQueryParams() (url.Values, error) { + return query.Values(sgr) +} + +type CalculateRouteResponse struct { + Routes []Route `json:"routes"` +} + +// 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", locationString(req.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 +} + +func P(lat, lng float64) Point { + return Point{ + Latitude: lat, + Longitude: lng, + } +} + +type Locations = []Point + +func locationString(locations Locations) string { + var sb strings.Builder + for i, p := range locations { + if i == 0 { + sb.WriteString(fmt.Sprintf("%f,%f", p.Latitude, p.Longitude)) + } else { + sb.WriteString(fmt.Sprintf(":%f,%f", p.Latitude, p.Longitude)) + } + } + return sb.String() +} 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..0d529bb0 --- /dev/null +++ b/tomtom/tomtom.go @@ -0,0 +1,37 @@ +package tomtom + +import ( + "os" + + "resty.dev/v3" +) + +type TomTom struct { + APIKey string + + client *resty.Client + urlBase string +} + +func NewClient() *TomTom { + 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() +} + +func (s *TomTom) SetDebug(enabled bool) { + s.client.Close() + if enabled { + s.client = resty.New().SetDebug(true) + } else { + s.client = resty.New().SetDebug(false) + } +} diff --git a/tomtom/types.go b/tomtom/types.go new file mode 100644 index 00000000..29d48bdb --- /dev/null +++ b/tomtom/types.go @@ -0,0 +1,235 @@ +package tomtom + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" +) + +// Base URLs and API constants +const ( + BaseURL = "https://api.tomtom.com" + RouteTypeFastest = "fastest" + TravelModeCar = "car" +) + +// 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 +} + +// 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 Leg struct { + Summary Summary `json:"summary"` + Points []Point `json:"points,omitempty"` +} + +type Route struct { + Summary Summary `json:"summary"` + Legs []Leg `json:"legs,omitempty"` +} + +// CalculateReachableRange API structures + +// CalculateReachableRangeParams holds the parameters for the Calculate Reachable Range API +type CalculateReachableRangeParams struct { + // Path parameters + 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.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 +}