From d03c12ffb63ec40f90f2aaf8f4133e0cd41d2215 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Thu, 16 Apr 2026 20:37:49 +0000 Subject: [PATCH] Add working ability to get stadia tiles directly --- stadia/bulk.go | 2 +- stadia/cmd/tile-raster/main.go | 55 +++++++++++++++++++++++ stadia/geocode_autocomplete.go | 2 +- stadia/geocode_bygid.go | 2 +- stadia/geocode_raw.go | 2 +- stadia/geocode_structured.go | 2 +- stadia/map_tile_raster.go | 81 ++++++++++++++++++++++++++++++++++ stadia/reverse_geocode.go | 2 +- stadia/stadia.go | 12 ++--- 9 files changed, 149 insertions(+), 11 deletions(-) create mode 100644 stadia/cmd/tile-raster/main.go create mode 100644 stadia/map_tile_raster.go diff --git a/stadia/bulk.go b/stadia/bulk.go index 4c1c303a..863a13d4 100644 --- a/stadia/bulk.go +++ b/stadia/bulk.go @@ -37,7 +37,7 @@ func (s *StadiaMaps) BulkGeocode(requests []BulkGeocodeQuery) ([]BulkGeocodeResp resp, err := s.client.R(). SetBody(body). SetContentType("application/json"). - SetPathParam("urlBase", s.urlBase). + SetPathParam("urlBase", s.urlBaseApi). SetQueryParam("api_key", s.APIKey). SetError(&api_error). SetResult(&results). diff --git a/stadia/cmd/tile-raster/main.go b/stadia/cmd/tile-raster/main.go new file mode 100644 index 00000000..80db25c2 --- /dev/null +++ b/stadia/cmd/tile-raster/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "context" + "flag" + "log" + "os" + + "github.com/Gleipnir-Technology/nidus-sync/stadia" +) + +func main() { + // Define command-line flags + lat := flag.Float64("lat", 0, "The latitude of the tile") + lng := flag.Float64("lng", 0, "The longitude of the tile") + zoom := flag.Uint("zoom", 16, "The zoom level") + + // Parse the flags + flag.Parse() + + if *lat == 0 { + log.Println("Error: you must specify -lat") + flag.Usage() + os.Exit(1) + } + if *lng == 0 { + log.Println("Error: you must specify -lng") + flag.Usage() + os.Exit(1) + } + key := os.Getenv("STADIA_MAPS_API_KEY") + if key == "" { + log.Println("STADIA_MAPS_API_KEY is empty") + os.Exit(1) + } + + client := stadia.NewStadiaMaps(key) + ctx := context.Background() + req := stadia.RequestTileRaster{ + Latitude: *lat, + Longitude: *lng, + Zoom: *zoom, + } + data, err := client.TileRaster(ctx, req) + if err != nil { + log.Printf("err: %v\n", err) + os.Exit(2) + } + err = os.WriteFile("tile.raw", data, 0666) + if err != nil { + log.Printf("err: %v\n", err) + os.Exit(2) + } + log.Printf("wrote tile.raw") +} diff --git a/stadia/geocode_autocomplete.go b/stadia/geocode_autocomplete.go index a00acf48..33bc50d3 100644 --- a/stadia/geocode_autocomplete.go +++ b/stadia/geocode_autocomplete.go @@ -58,7 +58,7 @@ func (s *StadiaMaps) GeocodeAutocomplete(ctx context.Context, req RequestGeocode SetQueryParamsFromValues(query). SetContext(ctx). SetResult(&result). - SetPathParam("urlBase", s.urlBase). + SetPathParam("urlBase", s.urlBaseApi). SetQueryParam("api_key", s.APIKey). Get("https://{urlBase}/geocoding/v2/autocomplete") if err != nil { diff --git a/stadia/geocode_bygid.go b/stadia/geocode_bygid.go index 49943ae1..0a07d3f0 100644 --- a/stadia/geocode_bygid.go +++ b/stadia/geocode_bygid.go @@ -27,7 +27,7 @@ func (s *StadiaMaps) GeocodeByGID(ctx context.Context, req RequestGeocodeByGID) SetQueryParamsFromValues(query). SetContext(ctx). SetResult(&result). - SetPathParam("urlBase", s.urlBase). + SetPathParam("urlBase", s.urlBaseApi). SetQueryParam("api_key", s.APIKey). Get("https://{urlBase}/geocoding/v2/place_details") if err != nil { diff --git a/stadia/geocode_raw.go b/stadia/geocode_raw.go index 2c1a5d75..0356292d 100644 --- a/stadia/geocode_raw.go +++ b/stadia/geocode_raw.go @@ -58,7 +58,7 @@ func (s *StadiaMaps) GeocodeRaw(ctx context.Context, req RequestGeocodeRaw) (*Ge SetQueryParamsFromValues(query). SetContext(ctx). SetResult(&result). - SetPathParam("urlBase", s.urlBase). + SetPathParam("urlBase", s.urlBaseApi). SetQueryParam("api_key", s.APIKey). Get("https://{urlBase}/geocoding/v1/search") if err != nil { diff --git a/stadia/geocode_structured.go b/stadia/geocode_structured.go index 935dbd10..5fba4575 100644 --- a/stadia/geocode_structured.go +++ b/stadia/geocode_structured.go @@ -71,7 +71,7 @@ func (s *StadiaMaps) GeocodeStructured(ctx context.Context, req RequestGeocodeSt SetQueryParamsFromValues(query). SetContext(ctx). SetResult(&result). - SetPathParam("urlBase", s.urlBase). + SetPathParam("urlBase", s.urlBaseApi). SetQueryParam("api_key", s.APIKey). Get("https://{urlBase}/geocoding/v1/search/structured") if err != nil { diff --git a/stadia/map_tile_raster.go b/stadia/map_tile_raster.go new file mode 100644 index 00000000..3c17bf04 --- /dev/null +++ b/stadia/map_tile_raster.go @@ -0,0 +1,81 @@ +package stadia + +import ( + "context" + "fmt" + "math" + "strconv" + + "github.com/rs/zerolog/log" +) + +type RequestTileRaster struct { + Latitude float64 + Longitude float64 + //Style string + Zoom uint +} + +func (s *StadiaMaps) TileRaster(ctx context.Context, req RequestTileRaster) ([]byte, error) { + // https://docs.stadiamaps.com/raster/ + //url := "https://{urlBase}/tiles/{style}/{z}/{x}/{y}{r}.png" + //url := "https://{urlBase}/data/imagery/{z}/{x}/{y}{r}.png" + url := "https://{urlBase}/tiles/alidade_satellite/{z}/{x}/{y}.jpg" + + y, x := LatLngToTile(req.Zoom, req.Latitude, req.Longitude) + //var api_error Error + resp, err := s.client.R(). + SetContext(ctx). + //SetPathParam("style", req.Style). + //SetPathParam("r", ""). + SetPathParam("x", strconv.Itoa(int(x))). + SetPathParam("y", strconv.Itoa(int(y))). + SetPathParam("z", strconv.Itoa(int(req.Zoom))). + SetPathParam("urlBase", s.urlBaseTiles). + SetQueryParam("api_key", s.APIKey). + Get(url) + if err != nil { + return nil, fmt.Errorf("autocomplete get: %w", err) + } + + if !resp.IsSuccess() { + return nil, parseError(resp) + } + content_type := resp.Header().Get("Content-Type") + log.Debug().Str("content_type", content_type).Send() + return resp.Bytes(), nil +} + +// LatLngToTile converts GPS coordinates to ArcGIS tile coordinates +func LatLngToTile(level uint, lat, lng float64) (row, column uint) { + // Get number of tiles per dimension at this zoom level + numTiles := math.Pow(2, float64(level)) + + // Convert longitude to tile column + // Range: -180 to 180 degrees maps to 0 to numTiles + column = uint(math.Floor((lng + 180.0) / 360.0 * numTiles)) + + // Convert latitude to tile row using Mercator projection + // First convert lat to radians + latRad := lat * math.Pi / 180.0 + + // Apply Mercator projection formula + // This maps latitude from -85.0511 to 85.0511 degrees to 0 to numTiles + mercatorY := 0.5 - math.Log(math.Tan(latRad)+1/math.Cos(latRad))/(2*math.Pi) + row = uint(math.Floor(mercatorY * numTiles)) + + // Ensure values are within valid range + if column < 0 { + column = 0 + } else if column >= uint(numTiles) { + column = uint(numTiles) - 1 + } + + if row < 0 { + row = 0 + } else if row >= uint(numTiles) { + row = uint(numTiles) - 1 + } + + return row, column +} diff --git a/stadia/reverse_geocode.go b/stadia/reverse_geocode.go index c611d3ce..3b5d7fa6 100644 --- a/stadia/reverse_geocode.go +++ b/stadia/reverse_geocode.go @@ -35,7 +35,7 @@ func (s *StadiaMaps) ReverseGeocode(ctx context.Context, req RequestReverseGeoco SetQueryParamsFromValues(query). SetContext(ctx). SetResult(&result). - SetPathParam("urlBase", s.urlBase). + SetPathParam("urlBase", s.urlBaseApi). SetQueryParam("api_key", s.APIKey). Get("https://{urlBase}/geocoding/v2/reverse") if err != nil { diff --git a/stadia/stadia.go b/stadia/stadia.go index 38e15d1c..7c24982d 100644 --- a/stadia/stadia.go +++ b/stadia/stadia.go @@ -10,8 +10,9 @@ import ( type StadiaMaps struct { APIKey string - client *resty.Client - urlBase string + client *resty.Client + urlBaseApi string + urlBaseTiles string } func NewStadiaMaps(api_key string) *StadiaMaps { @@ -26,9 +27,10 @@ func NewStadiaMaps(api_key string) *StadiaMaps { }) } return &StadiaMaps{ - APIKey: api_key, - client: r, - urlBase: "api.stadiamaps.com", + APIKey: api_key, + client: r, + urlBaseApi: "api.stadiamaps.com", + urlBaseTiles: "tiles.stadiamaps.com", } }