diff --git a/bbox.go b/bbox.go index 85657616..6898aeb3 100644 --- a/bbox.go +++ b/bbox.go @@ -9,6 +9,13 @@ type Extenter interface { Extent() (extent [4]float64) } +type PtMinMaxer interface { + // Min returns the minimum x and y values + Min() [2]float64 + // Max returns the maximum x and y values + Max() [2]float64 +} + // MinMaxer is a wrapper for an Extent that gets min/max of the extent type MinMaxer interface { MinX() float64 @@ -51,7 +58,7 @@ func (e *Extent) Edges(cwfn ClockwiseFunc) [][2][2]float64 { } } -// MaxX is the larger of the x values. +// MaxX is the largest of the x values. func (e *Extent) MaxX() float64 { if e == nil { return math.MaxFloat64 @@ -59,7 +66,7 @@ func (e *Extent) MaxX() float64 { return e[2] } -// MinX is the smaller of the x values. +// MinX is the smallest of the x values. func (e *Extent) MinX() float64 { if e == nil { return -math.MaxFloat64 @@ -67,7 +74,7 @@ func (e *Extent) MinX() float64 { return e[0] } -// MaxY is the larger of the y values. +// MaxY is the largest of the y values. func (e *Extent) MaxY() float64 { if e == nil { return math.MaxFloat64 @@ -75,7 +82,7 @@ func (e *Extent) MaxY() float64 { return e[3] } -// MinY is the smaller of the y values. +// MinY is the smallest of the y values. func (e *Extent) MinY() float64 { if e == nil { return -math.MaxFloat64 @@ -333,6 +340,7 @@ func (e *Extent) Clone() *Extent { // +--------------+----------+ | // | B | // +-----------------+ +// // For example the for the above Box A intersects Box B at the area surround by C. // // If the Boxes don't intersect does will be false, otherwise ibb will be the intersect. diff --git a/cmd/main.go b/cmd/main.go index dabe8ba3..ca7a84c4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -55,7 +55,7 @@ func readInputWKT(filename string) (geom.Geometry, error) { } type outfile struct { - tile *slippy.Tile + tile slippy.Tile format string } type outfilefile struct { @@ -83,7 +83,7 @@ func (off *outfilefile) WriteWKTGeom(geos ...geom.Geometry) *outfilefile { return off } -func newOutFile(tile *slippy.Tile, tag string) outfile { +func newOutFile(tile slippy.Tile, tag string) outfile { path := fmt.Sprintf("%v/%v/%v", tile.Z, tile.X, tile.Y) if tag != "" { path = fmt.Sprintf("%v/%v", path, tag) @@ -96,6 +96,17 @@ func newOutFile(tile *slippy.Tile, tag string) outfile { } } +// MvtTileDim is the number of pixels in a tile +const MvtTileDim = 4096.0 + +func PixelToNative(g slippy.TileGriddor, z slippy.Zoom) (float64, error) { + ext, err := slippy.Extent(g, slippy.Tile{Z: z}) + if err != nil { + return 0, err + } + return ext.XSpan() / MvtTileDim, nil +} + func main() { flag.Parse() if len(flag.Args()) < 2 || *help { @@ -116,17 +127,21 @@ func main() { fmt.Fprintf(os.Stderr, "Unabled to parse z: %v", err) usage() } - x, err := strconv.ParseUint(parts[1], 10, 64) + x, err := strconv.ParseInt(parts[1], 10, 64) if err != nil { fmt.Fprintf(os.Stderr, "Unabled to parse x: %v", err) usage() } - y, err := strconv.ParseUint(parts[2], 10, 64) + y, err := strconv.ParseInt(parts[2], 10, 64) if err != nil { fmt.Fprintf(os.Stderr, "Unabled to parse y: %v", err) usage() } - tile := slippy.NewTile(uint(z), uint(x), uint(y)) + tile := slippy.Tile{ + Z: slippy.Zoom(z), + X: int(x), + Y: int(y), + } fileTemplate := newOutFile(tile, *tag) geo, err := readInputWKT(flag.Args()[1]) if err != nil { @@ -142,21 +157,27 @@ func main() { fmt.Printf("Polygon:\n%v\n", plywkt) */ order := winding.Order{} - grid3857, _ := slippy.NewGrid(3857) + grid3857 := slippy.NewGrid(3857, 0) var clipRegion *geom.Extent { - webs := slippy.PixelsToNative(grid3857, tile.Z, uint(*buffer)) + webs, err := PixelToNative(grid3857, tile.Z) + if err != nil { + panic(err) + } ext, _ := slippy.Extent(grid3857, tile) clipRegion = ext.ExpandBy(webs) } if *simplifyGeo { + tol, err := PixelToNative(grid3857, tile.Z) + if err != nil { + panic(err) + } simp := simplify.DouglasPeucker{ - Tolerance: slippy.PixelsToNative(grid3857, tile.Z, 10.0), + Tolerance: tol, } - var err error geo, err = planar.Simplify(ctx, simp, geo) if err != nil { fmt.Fprintf(os.Stderr, "Unabled to simplify geo : %v", err) diff --git a/point.go b/point.go index ad845bf6..58368222 100644 --- a/point.go +++ b/point.go @@ -38,6 +38,12 @@ func (p Point) X() float64 { return p[0] } // Y is the y coordinate of a point in the projection func (p Point) Y() float64 { return p[1] } +// Lon is the lon coordinate of a point in the projection +func (p Point) Lon() float64 { return p[0] } + +// Lat is the lat coordinate of a point in the projection +func (p Point) Lat() float64 { return p[1] } + // MaxX is the same as X func (p Point) MaxX() float64 { return p[0] } diff --git a/slippy/maths.go b/slippy/maths.go new file mode 100644 index 00000000..0372075b --- /dev/null +++ b/slippy/maths.go @@ -0,0 +1,143 @@ +package slippy + +import ( + "math" + + "github.com/go-spatial/geom" +) + +/* + * + * This file should contain the basic math function for converting + * between coordinates that are internal to the system. + * + * Much of the math here is derived from two sources: + * ref: https://maplibre.org/maplibre-native/docs/book/design/coordinate-system.html#11 + * ref: https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#ECMAScript_(JavaScript/ActionScript,_etc.) + */ + +const ( + // DefaultTileSize is the tile size used if the given tile size is 0. + DefaultTileSize = 256 + // Lat4326Max is the maximum degree for latitude on an SRID 4326 map + Lat4326Max = 85.05112 + // Lon4326Max is the maximum degree for longitude on an SRID 4326 map + Lon4326Max = 180 + + // floatDrift is used to compare floating point numbers, and to deal with float drift + floatDrift = 0.000001 +) + +// Degree2Radians converts degrees to radians +func Degree2Radians(degree float64) float64 { + return degree * math.Pi / 180 +} + +// Radians2Degree converts radians to degrees +func Radians2Degree(radians float64) float64 { + return radians * 180 / math.Pi +} + +// lat2Num will return the Y coordinate for the tile at the given Z. +// +// Lat is assumed to be in degrees in SRID 3857 coordinates +// If tileSize == 0 then we will use a tileSize of DefaultTileSize +func lat2Num(tileSize uint32, z Zoom, lat float64) (y int) { + if tileSize == 0 { + tileSize = DefaultTileSize + } + // bound it because we have a top of the world problem + if lat < -Lat4326Max { + return int(z.N() - 1) + } + + if lat > Lat4326Max { + return 0 + } + tileY := lat2Px(tileSize, z, lat) + tileY = tileY / float64(tileSize) + // Truncate to get the tile + return int(tileY) +} + +// lat2Px will return the pixel coordinate for the lat. This can return +// a pixel that is outside the extents of the map, this just means +// the drawing is happening in the buffered area usually done for stitching +// purposes. +func lat2Px(tileSize uint32, z Zoom, lat float64) (yPx float64) { + if tileSize == 0 { + tileSize = DefaultTileSize + } + worldSize := float64(tileSize) * z.N() + + // Convert the Degree to radians as most of the math functions work in radians + radLat := Degree2Radians(45 + lat/2) + // normalize lat + latTan := math.Tan(radLat) + latNormalized := math.Log(latTan) + + // compute the pixel value for y: + yPxRaw := (180 - Radians2Degree(latNormalized)) / 360 + yPx = yPxRaw * worldSize + // instead of getting 7.0 we can end up with 6.9999999999, etc... use floatDrift to correct for such cases + return yPx + floatDrift +} + +// lon2Num will return the Y coordinate for the tile at the given Z. +// +// Lat is assumed to be in degrees in SRID 3857 coordinates +// If tileSize == 0 then we will use a tileSize of DefaultTileSize +func lon2Num(tileSize uint32, z Zoom, lon float64) (x int) { + if tileSize == 0 { + tileSize = DefaultTileSize + } + + if lon <= -Lon4326Max { + return 0 + } + + if lon >= Lon4326Max { + return int(z.N() - 1) + } + + tileX := lon2Px(tileSize, z, lon) + tileX = tileX / float64(tileSize) + // Truncate to get the tile + return int(tileX) + +} + +// lonPx will return the pixel coordinate for the lon. This can return +// a pixels that is outside the extents of the map, this just means +// the drawing is happening in the buffered area usually done for stitching +// purposes. +func lon2Px(tileSize uint32, z Zoom, lon float64) (xPx float64) { + if tileSize == 0 { + tileSize = DefaultTileSize + } + worldSize := float64(tileSize) * z.N() + lonNormalized := 180 + lon + // compute the pixel value for y: + xPxRaw := lonNormalized / 360 + xPx = xPxRaw * worldSize + // instead of getting 7.0 we can end up with 6.9999999999, etc... use floatDrift to correct for such cases + return xPx + floatDrift +} + +func PtFromLatLon(lat, lon float64) geom.Point { + return geom.Point{lon, lat} +} + +func x2deg(z Zoom, x int) float64 { + n := z.N() + long := float64(x) / n + long = long * 360.0 + long = long - 180.0 + return long +} + +func y2deg(z Zoom, y int) float64 { + n := math.Pi - 2.0*math.Pi*float64(y)/z.N() + lat := 180.0 / math.Pi * math.Atan(0.5*(math.Exp(n)-math.Exp(-n))) + return lat +} diff --git a/slippy/maths_test.go b/slippy/maths_test.go new file mode 100644 index 00000000..39b4bc0d --- /dev/null +++ b/slippy/maths_test.go @@ -0,0 +1,204 @@ +package slippy + +import ( + "errors" + "fmt" + "reflect" + "testing" +) + +func Test_lat2Num(t *testing.T) { + + type tcase struct { + TileSize uint32 + Z Zoom + Lat float64 + + y int + } + + fn := func(tc tcase) func(t *testing.T) { + return func(t *testing.T) { + // this is to help understand things. + lat := y2deg(tc.Z, tc.y) + t.Logf("z: %v y: %v lat = %v tc.Lat: %v", tc.Z, tc.y, lat, tc.Lat) + y := lat2Num(tc.TileSize, tc.Z, tc.Lat) + if y != tc.y { + t.Errorf("y got %d expected %d", y, tc.y) + } + } + } + tests := []tcase{ + { + Lat: 38.889814, + Z: 11, + y: 783, + }, + { + Lat: 38.889814, + y: 0, + }, + { + Lat: -86, + y: 0, + }, + { + Lat: -Lat4326Max, + Z: 0, + y: 0, + }, + { + Lat: -85.0511, + Z: 1, + y: 1, + }, + { + // example from orb/maptile + Lat: 41.850033, + Z: 28, + y: 99798110, + }, + { // example from open street maps slippy tile + // Lat: Radians2Degree(0.66693624687), + Lat: 35.6590699, // 4326 + Z: 18, + y: 103246, + }, + } + for i, tc := range tests { + t.Run(fmt.Sprintf("test_%d", i), fn(tc)) + } + +} + +func Test_lon2Num(t *testing.T) { + + type tcase struct { + TileSize uint32 + Z Zoom + Lon float64 + + x int + } + + fn := func(tc tcase) func(t *testing.T) { + return func(t *testing.T) { + x := lon2Num(tc.TileSize, tc.Z, tc.Lon) + if x != tc.x { + t.Errorf("x got %d expected %d", x, tc.x) + } + } + } + tests := []tcase{ + { + Lon: -77.035915, + Z: 11, + x: 585, + }, + { + Lon: 38.889814, + x: 0, + }, + { + Lon: Lon4326Max, + Z: 0, + x: 0, + }, + { + Lon: -Lon4326Max, + Z: 1, + x: 0, + }, + { + Lon: 139.7006793, + Z: 18, + x: 232798, + }, + } + for i, tc := range tests { + t.Run(fmt.Sprintf("test_%d", i), fn(tc)) + } + var tc tcase + for i := -Lon4326Max; i < Lon4326Max; i++ { + tc.Lon = float64(i) + tc.TileSize = 256 + t.Run(fmt.Sprintf("z0_test_%v", i), fn(tc)) + } + for z := Zoom(0); z <= 20; z++ { + tc.Lon = -Lon4326Max + tc.TileSize = 256 + tc.Z = z + t.Run(fmt.Sprintf("z%02d_test_neg_lon_max", z), fn(tc)) + } + for z := Zoom(0); z <= 20; z++ { + tc.Lon = Lon4326Max + tc.TileSize = 256 + tc.Z = z + tc.x = (1 << z) - 1 // last tile + t.Run(fmt.Sprintf("z%02d_test_lon_max", z), fn(tc)) + } + +} + +// Test_RoundTrip will go ToNative with a given tile, that use the native point to go FromNative, and verify that the +// Tile is the same as the starting tile. +func Test_RoundTrip(t *testing.T) { + + type tcase struct { + Tile Tile + ToErr error + FromErr error + } + fn := func(tc tcase) func(t *testing.T) { + g := Grid4326{} + return func(t *testing.T) { + pt, err := g.ToNative(tc.Tile) + if tc.ToErr != nil { + if err == nil { + t.Errorf("to err got nil, want %v", tc.ToErr) + return + } + if !errors.Is(err, tc.ToErr) { + t.Errorf("to err got %v, want %v", err, tc.ToErr) + return + } + return + } + if err != nil { + t.Errorf("to err got %v, want nil", err) + } + tile, err := g.FromNative(Zoom(tc.Tile.Z), pt) + if tc.FromErr != nil { + if err == nil { + t.Errorf("from err got nil, want %v", tc.FromErr) + return + } + if !errors.Is(err, tc.FromErr) { + t.Errorf("from err got %v, want %v", err, tc.FromErr) + return + } + return + } + if err != nil { + t.Errorf("from err got %v, want nil", err) + } + if !reflect.DeepEqual(tile, tc.Tile) { + t.Errorf("tile (pt: %v) got %v expected %v", pt, tile, tc.Tile) + return + } + } + } + // Test for all tiles in Z0, we want to make sure we can over extend the tile. + for z := Zoom(0); z <= 7; z++ { + for x := 0; x < (1 << z); x++ { + for y := 0; y < (1 << z); y++ { + t.Run(fmt.Sprintf("Tile(%v,%v,%v)", z, x, y), fn(tcase{Tile: Tile{ + Z: z, + X: x, + Y: y, + }})) + } + } + } + +} diff --git a/slippy/projections.go b/slippy/projections.go deleted file mode 100644 index 5e03a6c7..00000000 --- a/slippy/projections.go +++ /dev/null @@ -1,11 +0,0 @@ -package slippy - -// MvtTileDim is the number of pixels in a tile -const MvtTileDim = 4096.0 - -// PixelsToProjectedUnits scalar conversion of pixels into projected units -// TODO (@ear7h): this only considers the tile's native width -func PixelsToNative(g Grid, zoom uint, pixels uint) float64 { - ext, _ := Extent(g, NewTile(zoom, 0, 0)) - return ext.XSpan() / MvtTileDim -} diff --git a/slippy/testdata/failed_output/.gitignore b/slippy/testdata/failed_output/.gitignore new file mode 100644 index 00000000..798f7581 --- /dev/null +++ b/slippy/testdata/failed_output/.gitignore @@ -0,0 +1 @@ +*.tiles \ No newline at end of file diff --git a/slippy/testdata/for_bounds/san_diego_11z.coords b/slippy/testdata/for_bounds/san_diego_11z.coords new file mode 100644 index 00000000..d7c408e6 --- /dev/null +++ b/slippy/testdata/for_bounds/san_diego_11z.coords @@ -0,0 +1,4 @@ +# Bounds: spherical.Hull([2]float64{-117.15, 32.6894743}, [2]float64{-116.804, 32.6339}), +11/357/827 +11/358/827 +11/359/827 diff --git a/slippy/testdata/for_bounds/san_diego_15z.coords b/slippy/testdata/for_bounds/san_diego_15z.coords new file mode 100644 index 00000000..27a0ef94 --- /dev/null +++ b/slippy/testdata/for_bounds/san_diego_15z.coords @@ -0,0 +1,49 @@ +# coords for San Diego 15z +# Bounds: spherical.Hull([2]float64{-117.15, 32.6894743}, [2]float64{-116.804, 32.6339}), +{Z: 15, X: 5720, Y: 13232}, {Z: 15, X: 5720, Y: 13233}, {Z: 15, X: 5720, Y: 13234}, {Z: 15, X: 5720, Y: 13235}, {Z: 15, X: 5720, Y: 13236}, +{Z: 15, X: 5720, Y: 13237}, {Z: 15, X: 5720, Y: 13238}, {Z: 15, X: 5721, Y: 13232}, {Z: 15, X: 5721, Y: 13233}, {Z: 15, X: 5721, Y: 13234}, +{Z: 15, X: 5721, Y: 13235}, {Z: 15, X: 5721, Y: 13236}, {Z: 15, X: 5721, Y: 13237}, {Z: 15, X: 5721, Y: 13238}, {Z: 15, X: 5722, Y: 13232}, +{Z: 15, X: 5722, Y: 13233}, {Z: 15, X: 5722, Y: 13234}, {Z: 15, X: 5722, Y: 13235}, {Z: 15, X: 5722, Y: 13236}, {Z: 15, X: 5722, Y: 13237}, +{Z: 15, X: 5722, Y: 13238}, {Z: 15, X: 5723, Y: 13232}, {Z: 15, X: 5723, Y: 13233}, {Z: 15, X: 5723, Y: 13234}, {Z: 15, X: 5723, Y: 13235}, +{Z: 15, X: 5723, Y: 13236}, {Z: 15, X: 5723, Y: 13237}, {Z: 15, X: 5723, Y: 13238}, {Z: 15, X: 5724, Y: 13232}, {Z: 15, X: 5724, Y: 13233}, +{Z: 15, X: 5724, Y: 13234}, {Z: 15, X: 5724, Y: 13235}, {Z: 15, X: 5724, Y: 13236}, {Z: 15, X: 5724, Y: 13237}, {Z: 15, X: 5724, Y: 13238}, +{Z: 15, X: 5725, Y: 13232}, {Z: 15, X: 5725, Y: 13233}, {Z: 15, X: 5725, Y: 13234}, {Z: 15, X: 5725, Y: 13235}, {Z: 15, X: 5725, Y: 13236}, +{Z: 15, X: 5725, Y: 13237}, {Z: 15, X: 5725, Y: 13238}, {Z: 15, X: 5726, Y: 13232}, {Z: 15, X: 5726, Y: 13233}, {Z: 15, X: 5726, Y: 13234}, +{Z: 15, X: 5726, Y: 13235}, {Z: 15, X: 5726, Y: 13236}, {Z: 15, X: 5726, Y: 13237}, {Z: 15, X: 5726, Y: 13238}, {Z: 15, X: 5727, Y: 13232}, +{Z: 15, X: 5727, Y: 13233}, {Z: 15, X: 5727, Y: 13234}, {Z: 15, X: 5727, Y: 13235}, {Z: 15, X: 5727, Y: 13236}, {Z: 15, X: 5727, Y: 13237}, +{Z: 15, X: 5727, Y: 13238}, {Z: 15, X: 5728, Y: 13232}, {Z: 15, X: 5728, Y: 13233}, {Z: 15, X: 5728, Y: 13234}, {Z: 15, X: 5728, Y: 13235}, +{Z: 15, X: 5728, Y: 13236}, {Z: 15, X: 5728, Y: 13237}, {Z: 15, X: 5728, Y: 13238}, {Z: 15, X: 5729, Y: 13232}, {Z: 15, X: 5729, Y: 13233}, +{Z: 15, X: 5729, Y: 13234}, {Z: 15, X: 5729, Y: 13235}, {Z: 15, X: 5729, Y: 13236}, {Z: 15, X: 5729, Y: 13237}, {Z: 15, X: 5729, Y: 13238}, +{Z: 15, X: 5730, Y: 13232}, {Z: 15, X: 5730, Y: 13233}, {Z: 15, X: 5730, Y: 13234}, {Z: 15, X: 5730, Y: 13235}, {Z: 15, X: 5730, Y: 13236}, +{Z: 15, X: 5730, Y: 13237}, {Z: 15, X: 5730, Y: 13238}, {Z: 15, X: 5731, Y: 13232}, {Z: 15, X: 5731, Y: 13233}, {Z: 15, X: 5731, Y: 13234}, +{Z: 15, X: 5731, Y: 13235}, {Z: 15, X: 5731, Y: 13236}, {Z: 15, X: 5731, Y: 13237}, {Z: 15, X: 5731, Y: 13238}, {Z: 15, X: 5732, Y: 13232}, +{Z: 15, X: 5732, Y: 13233}, {Z: 15, X: 5732, Y: 13234}, {Z: 15, X: 5732, Y: 13235}, {Z: 15, X: 5732, Y: 13236}, {Z: 15, X: 5732, Y: 13237}, +{Z: 15, X: 5732, Y: 13238}, {Z: 15, X: 5733, Y: 13232}, {Z: 15, X: 5733, Y: 13233}, {Z: 15, X: 5733, Y: 13234}, {Z: 15, X: 5733, Y: 13235}, +{Z: 15, X: 5733, Y: 13236}, {Z: 15, X: 5733, Y: 13237}, {Z: 15, X: 5733, Y: 13238}, {Z: 15, X: 5734, Y: 13232}, {Z: 15, X: 5734, Y: 13233}, +{Z: 15, X: 5734, Y: 13234}, {Z: 15, X: 5734, Y: 13235}, {Z: 15, X: 5734, Y: 13236}, {Z: 15, X: 5734, Y: 13237}, {Z: 15, X: 5734, Y: 13238}, +{Z: 15, X: 5735, Y: 13232}, {Z: 15, X: 5735, Y: 13233}, {Z: 15, X: 5735, Y: 13234}, {Z: 15, X: 5735, Y: 13235}, {Z: 15, X: 5735, Y: 13236}, +{Z: 15, X: 5735, Y: 13237}, {Z: 15, X: 5735, Y: 13238}, {Z: 15, X: 5736, Y: 13232}, {Z: 15, X: 5736, Y: 13233}, {Z: 15, X: 5736, Y: 13234}, +{Z: 15, X: 5736, Y: 13235}, {Z: 15, X: 5736, Y: 13236}, {Z: 15, X: 5736, Y: 13237}, {Z: 15, X: 5736, Y: 13238}, {Z: 15, X: 5737, Y: 13232}, +{Z: 15, X: 5737, Y: 13233}, {Z: 15, X: 5737, Y: 13234}, {Z: 15, X: 5737, Y: 13235}, {Z: 15, X: 5737, Y: 13236}, {Z: 15, X: 5737, Y: 13237}, +{Z: 15, X: 5737, Y: 13238}, {Z: 15, X: 5738, Y: 13232}, {Z: 15, X: 5738, Y: 13233}, {Z: 15, X: 5738, Y: 13234}, {Z: 15, X: 5738, Y: 13235}, +{Z: 15, X: 5738, Y: 13236}, {Z: 15, X: 5738, Y: 13237}, {Z: 15, X: 5738, Y: 13238}, {Z: 15, X: 5739, Y: 13232}, {Z: 15, X: 5739, Y: 13233}, +{Z: 15, X: 5739, Y: 13234}, {Z: 15, X: 5739, Y: 13235}, {Z: 15, X: 5739, Y: 13236}, {Z: 15, X: 5739, Y: 13237}, {Z: 15, X: 5739, Y: 13238}, +{Z: 15, X: 5740, Y: 13232}, {Z: 15, X: 5740, Y: 13233}, {Z: 15, X: 5740, Y: 13234}, {Z: 15, X: 5740, Y: 13235}, {Z: 15, X: 5740, Y: 13236}, +{Z: 15, X: 5740, Y: 13237}, {Z: 15, X: 5740, Y: 13238}, {Z: 15, X: 5741, Y: 13232}, {Z: 15, X: 5741, Y: 13233}, {Z: 15, X: 5741, Y: 13234}, +{Z: 15, X: 5741, Y: 13235}, {Z: 15, X: 5741, Y: 13236}, {Z: 15, X: 5741, Y: 13237}, {Z: 15, X: 5741, Y: 13238}, {Z: 15, X: 5742, Y: 13232}, +{Z: 15, X: 5742, Y: 13233}, {Z: 15, X: 5742, Y: 13234}, {Z: 15, X: 5742, Y: 13235}, {Z: 15, X: 5742, Y: 13236}, {Z: 15, X: 5742, Y: 13237}, +{Z: 15, X: 5742, Y: 13238}, {Z: 15, X: 5743, Y: 13232}, {Z: 15, X: 5743, Y: 13233}, {Z: 15, X: 5743, Y: 13234}, {Z: 15, X: 5743, Y: 13235}, +{Z: 15, X: 5743, Y: 13236}, {Z: 15, X: 5743, Y: 13237}, {Z: 15, X: 5743, Y: 13238}, {Z: 15, X: 5744, Y: 13232}, {Z: 15, X: 5744, Y: 13233}, +{Z: 15, X: 5744, Y: 13234}, {Z: 15, X: 5744, Y: 13235}, {Z: 15, X: 5744, Y: 13236}, {Z: 15, X: 5744, Y: 13237}, {Z: 15, X: 5744, Y: 13238}, +{Z: 15, X: 5745, Y: 13232}, {Z: 15, X: 5745, Y: 13233}, {Z: 15, X: 5745, Y: 13234}, {Z: 15, X: 5745, Y: 13235}, {Z: 15, X: 5745, Y: 13236}, +{Z: 15, X: 5745, Y: 13237}, {Z: 15, X: 5745, Y: 13238}, {Z: 15, X: 5746, Y: 13232}, {Z: 15, X: 5746, Y: 13233}, {Z: 15, X: 5746, Y: 13234}, +{Z: 15, X: 5746, Y: 13235}, {Z: 15, X: 5746, Y: 13236}, {Z: 15, X: 5746, Y: 13237}, {Z: 15, X: 5746, Y: 13238}, {Z: 15, X: 5747, Y: 13232}, +{Z: 15, X: 5747, Y: 13233}, {Z: 15, X: 5747, Y: 13234}, {Z: 15, X: 5747, Y: 13235}, {Z: 15, X: 5747, Y: 13236}, {Z: 15, X: 5747, Y: 13237}, +{Z: 15, X: 5747, Y: 13238}, {Z: 15, X: 5748, Y: 13232}, {Z: 15, X: 5748, Y: 13233}, {Z: 15, X: 5748, Y: 13234}, {Z: 15, X: 5748, Y: 13235}, +{Z: 15, X: 5748, Y: 13236}, {Z: 15, X: 5748, Y: 13237}, {Z: 15, X: 5748, Y: 13238}, {Z: 15, X: 5749, Y: 13232}, {Z: 15, X: 5749, Y: 13233}, +{Z: 15, X: 5749, Y: 13234}, {Z: 15, X: 5749, Y: 13235}, {Z: 15, X: 5749, Y: 13236}, {Z: 15, X: 5749, Y: 13237}, {Z: 15, X: 5749, Y: 13238}, +{Z: 15, X: 5750, Y: 13232}, {Z: 15, X: 5750, Y: 13233}, {Z: 15, X: 5750, Y: 13234}, {Z: 15, X: 5750, Y: 13235}, {Z: 15, X: 5750, Y: 13236}, +{Z: 15, X: 5750, Y: 13237}, {Z: 15, X: 5750, Y: 13238}, {Z: 15, X: 5751, Y: 13232}, {Z: 15, X: 5751, Y: 13233}, {Z: 15, X: 5751, Y: 13234}, +{Z: 15, X: 5751, Y: 13235}, {Z: 15, X: 5751, Y: 13236}, {Z: 15, X: 5751, Y: 13237}, {Z: 15, X: 5751, Y: 13238}, {Z: 15, X: 5752, Y: 13232}, +{Z: 15, X: 5752, Y: 13233}, {Z: 15, X: 5752, Y: 13234}, {Z: 15, X: 5752, Y: 13235}, {Z: 15, X: 5752, Y: 13236}, {Z: 15, X: 5752, Y: 13237}, +{Z: 15, X: 5752, Y: 13238}, \ No newline at end of file diff --git a/slippy/testdata/for_bounds/san_diego_9z.coords b/slippy/testdata/for_bounds/san_diego_9z.coords new file mode 100644 index 00000000..7f6914b0 --- /dev/null +++ b/slippy/testdata/for_bounds/san_diego_9z.coords @@ -0,0 +1,2 @@ +# Bounds: spherical.Hull([2]float64{-117.15, 32.6894743}, [2]float64{-116.804, 32.6339}), +9/89/206 diff --git a/slippy/testdata/for_bounds/tegola_issue_997.coords b/slippy/testdata/for_bounds/tegola_issue_997.coords new file mode 100644 index 00000000..35b8e667 --- /dev/null +++ b/slippy/testdata/for_bounds/tegola_issue_997.coords @@ -0,0 +1,9 @@ +7/64/41 +7/64/42 +7/64/43 +7/65/41 +7/65/42 +7/65/43 +7/66/41 +7/66/42 +7/66/43 diff --git a/slippy/testdata/for_bounds/tegola_issue_997_w_seeding_bounds.coords b/slippy/testdata/for_bounds/tegola_issue_997_w_seeding_bounds.coords new file mode 100644 index 00000000..1f462687 --- /dev/null +++ b/slippy/testdata/for_bounds/tegola_issue_997_w_seeding_bounds.coords @@ -0,0 +1,8 @@ +7/65/40 +7/65/41 +7/65/42 +7/65/43 +7/66/40 +7/66/41 +7/66/42 +7/66/43 diff --git a/slippy/tile.go b/slippy/tile.go index 2b46d8a4..131ef984 100644 --- a/slippy/tile.go +++ b/slippy/tile.go @@ -2,77 +2,109 @@ package slippy import ( "errors" - "fmt" + "math" + "strconv" "github.com/go-spatial/geom" ) +var ( + ErrNilBounds = errors.New("slippy: Bounds cannot be nil") +) + // MaxZoom is the lowest zoom (furthest in) const MaxZoom = 22 -// NewTile returns a Tile of Z,X,Y passed in -func NewTile(z, x, y uint) *Tile { - return &Tile{ +// Zoom represents a zoom level; this usually goes from 0 to 22 +type Zoom uint + +func (z Zoom) N() float64 { return math.Exp2(float64(z)) } +func (z Zoom) TileSize() Tile { + n := int(z.N()) + return Tile{ Z: z, - X: x, - Y: y, + X: n, + Y: n, } } // Tile describes a slippy tile. type Tile struct { // zoom - Z uint + Z Zoom // column - X uint + X int // row - Y uint + Y int } -// NewTileMinMaxer returns the smallest tile which fits the -// geom.MinMaxer. Note: it assumes the values of ext are -// EPSG:4326 (lng/lat) -func NewTileMinMaxer(g Grid, ext geom.MinMaxer) (*Tile, bool) { - tile, ok := g.FromNative(MaxZoom, geom.Point{ - ext.MinX(), - ext.MinY(), - }) - - if !ok { - return nil, false - } +// Equal tests for equality +func (tile Tile) Equal(other Tile) bool { + return tile.Z == other.Z && tile.X == other.X && tile.Y == other.Y +} +func (tile Tile) String() string { + return strconv.FormatInt(int64(tile.Z), 10) + "/" + + strconv.FormatInt(int64(tile.X), 10) + "/" + + strconv.FormatInt(int64(tile.Y), 10) +} - var ret *Tile +// FamilyAt returns an iterator function that will call the yield function with every related tile at the requested +// zoom. This will include the provided tile itself. (if the same zoom is provided). The parent (overlapping tile at a lower zoom level), +// or children (overlapping tiles at a higher zoom level). +// +// This function is structured so that it can take advantage of go1.23's Range Funcs. e.g.: +// for tile := range aTile.FamilyAt(10) { +// fmt.Printf("got tile: %v\n",tile) +// } +func (tile Tile) FamilyAt(zoom Zoom) func(yield func(Tile) bool) { + return func(yield func(Tile) bool) { + // handle ancestors and self + if zoom <= tile.Z { + mag := tile.Z - zoom + yield(Tile{Z: zoom, X: tile.X >> mag, Y: tile.Y >> mag}) + return + } - for z := uint(MaxZoom); int(z) >= 0 && ret == nil; z-- { - RangeFamilyAt(g, tile, z, func(tile *Tile) error { - if ext, ok := Extent(g, tile); ok && ext.Contains(geom.Point(ext.Max())) { - ret = tile - return errors.New("stop iter") + // handle descendants + mag := int(zoom) - int(tile.Z) + delta := int(math.Exp2(float64(mag))) + leastX := tile.X << mag + leastY := tile.Y << mag + for x := leastX; x < leastX+delta; x++ { + for y := leastY; y < leastY+delta; y++ { + if !yield(Tile{Z: zoom, X: x, Y: y}) { + // stop iterating + return + } } - - return nil - }) + } } - - return ret, true } -// FromBounds returns a list of tiles that make up the bound given. The bounds should be defined as the following lng/lat points [4]float64{west,south,east,north} -func FromBounds(g Grid, bounds *geom.Extent, z uint) []Tile { +// RangeFamilyAt returns an iterator function that will call the yield function with every related tile at the requested +// zoom. This will include the provided tile itself. (if the same zoom is provided). The parent (overlapping tile at a lower zoom level), +// or children (overlapping tiles at a higher zoom level). +func RangeFamilyAt(tile Tile, zoom Zoom, yield func(Tile) bool) { tile.FamilyAt(zoom)(yield) } + +// FromBounds returns a list of tiles that make up the bound given. +// The bounds should be defined as the following lng/lat points [4]float64{west,south,east,north} +// +// The only errors this generates are if the bounds is nil, or any errors grid returns from +// transformation the bounds points. +func FromBounds(g TileGriddor, bounds geom.PtMinMaxer, z Zoom) ([]Tile, error) { if bounds == nil { - return nil + return nil, ErrNilBounds } - p1, ok := g.FromNative(z, bounds.Min()) - if !ok { - return nil + p1, err := g.FromNative(z, bounds.Min()) + if err != nil { + return nil, err } - p2, ok := g.FromNative(z, bounds.Max()) - if !ok { - return nil + p2, err := g.FromNative(z, bounds.Max()) + if err != nil { + return nil, err } minx, maxx := p1.X, p2.X @@ -93,45 +125,5 @@ func FromBounds(g Grid, bounds *geom.Extent, z uint) []Tile { } } - return ret -} - -// ZXY returns back the z,x,y of the tile -func (t Tile) ZXY() (uint, uint, uint) { return t.Z, t.X, t.Y } - -type Iterator func(*Tile) error - -// RangeFamilyAt calls f on every tile vertically related to t at the specified zoom -// TODO (ear7h): sibling support -func RangeFamilyAt(g Grid, t *Tile, zoom uint, f Iterator) error { - tl, ok := g.ToNative(t) - if !ok { - return fmt.Errorf("tile %v not valid for grid", t) - } - - br, ok := g.ToNative(NewTile(t.Z, t.X+1, t.Y+1)) - if !ok { - return fmt.Errorf("tile %v not valid for grid", t) - } - - tlt, ok := g.FromNative(zoom, tl) - if !ok { - return fmt.Errorf("tile %v not valid for grid", t) - } - - brt, ok := g.FromNative(zoom, br) - if !ok { - return fmt.Errorf("tile %v not valid for grid", t) - } - - for x := tlt.X; x < brt.X; x++ { - for y := tlt.Y; y < brt.Y; y++ { - err := f(NewTile(zoom, x, y)) - if err != nil { - return err - } - } - } - - return nil + return ret, nil } diff --git a/slippy/tile_grid.go b/slippy/tile_grid.go index 62e3f2ea..bbf4a4db 100644 --- a/slippy/tile_grid.go +++ b/slippy/tile_grid.go @@ -2,130 +2,165 @@ package slippy import ( "fmt" - "math" "github.com/go-spatial/geom" + "github.com/go-spatial/proj" ) -// TileGrid contains the tile layout, including ability to get WGS84 coordinates for tile extents -type Grid interface { +// TileGriddor contains the tile layout, including ability to get WGS84 coordinates for tile extents +type TileGriddor interface { // SRID returns the SRID of the coordinate system of the - // implementer. The geomtries returned by the other methods + // implementer. The geometries returned by the other methods // will be in these coordinates. - SRID() uint + SRID() proj.EPSGCode // Size returns a tile where the X and Y are the size of that zoom's // tile grid. AKA: // Tile{z, MaxX + 1, MaxY + 1 - Size(z uint) (*Tile, bool) + Size(z Zoom) (Tile, bool) // FromNative converts from a point (in the Grid's coordinates system) and zoom - // to a tile. ok will be false if the point is not valid for this coordinate - // system. - FromNative(z uint, pt geom.Point) (tile *Tile, ok bool) + // to a tile. + FromNative(z Zoom, pt geom.Point) (tile Tile, err error) // ToNative returns the tiles upper left point. ok will be false if - // the tile is not valid. A note on implemetation is that this method + // the tile is not valid. A note on implementation is that this method // should be able to take tiles with x and y values 1 higher than the max, // this is to fetch the bottom right corner of the grid - ToNative(*Tile) (pt geom.Point, ok bool) + ToNative(Tile) (pt geom.Point, err error) } -func Extent(g Grid, t *Tile) (ext *geom.Extent, ok bool) { - if t == nil { - return nil, false +// NewGrid will return a grid for the requested EPSGCode. +// if tileSize is zero, then the DefaultTileSize is used +func NewGrid(srid proj.EPSGCode, tileSize uint32) TileGriddor { + if tileSize == 0 { + tileSize = DefaultTileSize } - - tl, ok := g.ToNative(t) - if !ok { - return nil, false + if srid == proj.EPSG4326 { + return Grid4326{tileSize: tileSize} } - br, ok := g.ToNative(NewTile(t.Z, t.X + 1, t.Y + 1)) - if !ok { - return nil, false + return Grid{ + tileSize: tileSize, + Srid: srid, } - - return geom.NewExtentFromPoints(tl, br), true } -// NewGrid returns the grid conventionally used with the -// given SRID. Errors if the SRID is not supported. The -// currently supported SRID's are: -// 4326 -// 3857 -func NewGrid(srid uint) (Grid, error) { - switch srid { - case 4326: - return &grid{ - srid: srid, - tilingRatio: 2, - maxx: LonMax, - maxy: LatMax, - }, nil - case 3857: - return &grid{ - srid: srid, - tilingRatio: 1, - maxx: WebMercatorMax, - maxy: WebMercatorMax, - }, nil - default: - return nil, fmt.Errorf("unsupported srid: %v", srid) +func Extent(g TileGriddor, t Tile) (*geom.Extent, error) { + topLeft, err := g.ToNative(t) + if err != nil { + return nil, fmt.Errorf("failed get top left point: %w", err) + } + bottomRight, err := g.ToNative(Tile{Z: t.Z, X: t.X + 1, Y: t.Y + 1}) + if err != nil { + return nil, fmt.Errorf("failed get bottom right point: %w", err) } + return geom.NewExtentFromPoints(topLeft, bottomRight), nil } -// WebMercatorMax is the max size in meters of a tile -const WebMercatorMax = 20037508.34 -const LatMax = 90 -const LonMax = 180 - -type grid struct { - srid uint - // aspect ratio of tile scheme (colums / rows) - // TODO(ear7h) this will break tall maps(the x from Size(0) will be 0) - tilingRatio float64 - // TODO(ear7h) change this to ranges and origin point - // might be needed for future coordinate systems. - maxx, maxy float64 // in native coords +func NewTileMinMaxer(g TileGriddor, ext geom.MinMaxer) (Tile, error) { + tile, err := g.FromNative(MaxZoom, geom.Point{ext.MinX(), ext.MinY()}) + if err != nil { + return Tile{}, fmt.Errorf("failed get tile for min points: %w", err) + } + var ( + ret Tile + found bool + ext1 *geom.Extent + ) + + for z := Zoom(MaxZoom); int(z) >= 0 && !found; z-- { + err = nil + tile.FamilyAt(z)(func(tile Tile) bool { + ext1, err = Extent(g, tile) + if err != nil { + // stop iteration + return false + } + if ext1.Contains(geom.Point(ext1.Max())) { + ret = tile + found = true + return false + } + return true + }) + if err != nil { + return Tile{}, fmt.Errorf("failed get min tile: %w", err) + } + } + if !found { + return Tile{}, fmt.Errorf("tile for min point not found") + } + + return ret, nil } -func (g *grid) SRID() uint { - return g.srid +type Grid struct { + tileSize uint32 + Srid proj.EPSGCode } -func (g *grid) Size(zoom uint) (*Tile, bool) { - if zoom > MaxZoom { - return nil, false +func (g Grid) SRID() proj.EPSGCode { return g.Srid } +func (g Grid) Size(z Zoom) (Tile, bool) { + if z > MaxZoom { + return Tile{}, false } - - dim := uint(1) << zoom // hopefully the zoom isn't larger than 64 - return NewTile(zoom, dim * uint(g.tilingRatio), dim), true + return z.TileSize(), true } - -func (g *grid) FromNative(zoom uint, pt geom.Point) (*Tile, bool) { - if zoom > MaxZoom || pt.X() > g.maxx || pt.Y() > g.maxy { - return nil, false +func (g Grid) FromNative(z Zoom, pt geom.Point) (tile Tile, err error) { + pts, err := proj.Inverse(g.Srid, pt[:]) + if err != nil { + return Tile{}, fmt.Errorf("failed to convert to 4326: %w", err) } + x := lon2Num(g.tileSize, z, pts[0]) + y := lat2Num(g.tileSize, z, pts[1]) + return Tile{ + Z: z, + X: x, + Y: y, + }, nil +} +func (g Grid) ToNative(tile Tile) (pt geom.Point, err error) { + lat := y2deg(tile.Z, tile.Y) + lon := x2deg(tile.Z, tile.X) + pts, err := proj.Convert(g.Srid, []float64{lon, lat}) + if err != nil { + return geom.Point{}, fmt.Errorf("failed to convert from 4326: %w", err) + } + return geom.Point{pts[0], pts[1]}, nil +} - res := g.maxx * 2 / g.tilingRatio / math.Exp2(float64(zoom)) - x := uint((pt.X() + g.maxx) / res) - - res = g.maxy * 2 / math.Exp2(float64(zoom)) - y := uint(-(pt.Y() - g.maxy) / res) - - return NewTile(zoom, x, y), true +type Grid4326 struct { + // TileSize if 0 will default to DefaultTileSize + tileSize uint32 } -func (g *grid) ToNative(tile *Tile) (geom.Point, bool) { - if max, ok := g.Size(tile.Z); !ok || tile.X > max.X || tile.Y > max.Y { - return geom.Point{}, false +func (Grid4326) SRID() proj.EPSGCode { return proj.EPSG4326 } +func (Grid4326) Size(z Zoom) (Tile, bool) { + if z > MaxZoom { + return Tile{}, false } + return z.TileSize(), true +} - res := g.maxx * 2 / g.tilingRatio / math.Exp2(float64(tile.Z)) - x := -g.maxx + float64(tile.X)*res - - res = g.maxy * 2 / math.Exp2(float64(tile.Z)) - y := g.maxy - float64(tile.Y)*res +func (g Grid4326) TileSize() uint32 { + if g.tileSize == 0 { + return DefaultTileSize + } + return g.tileSize +} - return geom.Point{x, y}, true +// FromNative will convert a pt in 3857 coordinates and a zoom to a Tile coordinate +func (g Grid4326) FromNative(z Zoom, pt geom.Point) (tile Tile, err error) { + y := lat2Num(g.tileSize, z, pt.Lat()) + x := lon2Num(g.tileSize, z, pt.Lon()) + return Tile{ + Z: z, + X: x, + Y: y, + }, nil +} +func (g Grid4326) ToNative(tile Tile) (pt geom.Point, err error) { + lat := y2deg(tile.Z, tile.Y) + lon := x2deg(tile.Z, tile.X) + return PtFromLatLon(lat, lon), nil } diff --git a/slippy/tile_grid_test.go b/slippy/tile_grid_test.go deleted file mode 100644 index 6b742a75..00000000 --- a/slippy/tile_grid_test.go +++ /dev/null @@ -1,344 +0,0 @@ -package slippy - -import ( - "testing" - "github.com/go-spatial/proj" - "github.com/go-spatial/geom" - "github.com/go-spatial/geom/cmp" -) - -func TestTileGridSize(t *testing.T) { - type tcase struct { - srid uint - zoom uint - expectedSizeX uint - expectedSizeY uint - } - - fn := func(tc tcase) func(t *testing.T) { - return func(t *testing.T) { - grid, err := NewGrid(tc.srid) - if err != nil { - t.Fatal(err) - } - if grid.SRID() != tc.srid { - t.Fatal(err) - } - tile, ok := grid.Size(tc.zoom) - if !ok { - t.Fatal("expected ok") - } - if tile.X != tc.expectedSizeX { - t.Errorf("got %v expected %v", tile.X, tc.expectedSizeX) - } - if tile.Y != tc.expectedSizeY { - t.Errorf("got %v expected %v", tile.Y, tc.expectedSizeY) - } - } - } - - tests := map[string]tcase{ - "4326_zoom0": { - srid: 4326, - zoom: 0, - expectedSizeX: 2, - expectedSizeY: 1, - }, - "3857_zoom0": { - srid: 3857, - zoom: 0, - expectedSizeX: 1, - expectedSizeY: 1, - }, - "4326_zoom15": { - srid: 4326, - zoom: 15, - expectedSizeX: 65536, - expectedSizeY: 32768, - }, - "3857_zoom15": { - srid: 3857, - zoom: 15, - expectedSizeX: 32768, - expectedSizeY: 32768, - }, - } - - for name, tc := range tests { - t.Run(name, fn(tc)) - } -} - -func TestTileGridContains(t *testing.T) { - type tcase struct { - srid uint - zoom uint - x uint - y uint - expected bool - } - - fn := func(tc tcase) func(t *testing.T) { - return func(t *testing.T) { - grid, err := NewGrid(tc.srid) - if err != nil { - t.Fatal(err) - } - - _, ok := grid.ToNative(NewTile(tc.zoom, tc.x, tc.y)) - - if ok != tc.expected { - t.Errorf("got %v expected %v", ok, tc.expected) - } - } - } - - tests := map[string]tcase{ - "3857_zoom0_pass": { - srid: 3857, - zoom: 0, - x: 0, - y: 0, - expected: true, - }, - "3857_zoom0_fail": { - srid: 3857, - zoom: 0, - x: 2, - y: 0, - expected: false, - }, - "3857_zoom15_extent": { - srid: 3857, - zoom: 15, - x: 32767, - y: 32767, - expected: true, - }, - "4326_zoom0_pass": { - srid: 4326, - zoom: 0, - x: 2, - y: 0, - expected: true, - }, - "4326_zoom0_fail": { - srid: 4326, - zoom: 0, - x: 0, - y: 2, - expected: false, - }, - "4326_zoom12_pass": { - srid: 4326, - zoom: 12, - x: 8191, - y: 4095, - expected: true, - }, - } - - for name, tc := range tests { - t.Run(name, fn(tc)) - } -} - -func TestFromNative(t *testing.T) { - type tcase struct { - point geom.Point - srid uint - expected *Tile - } - - fn := func(tc tcase) func(t *testing.T) { - return func(t *testing.T) { - grid, err := NewGrid(tc.srid) - if err != nil { - t.Fatal(err) - } - - pt := tc.point - if tc.srid != 4326 { - pts, err := proj.Convert(proj.EPSGCode(tc.srid), pt[:]) - if err != nil { - t.Fatal(err, tc.srid) - } - pt = geom.Point{pts[0], pts[1]} - } - - tile, ok := grid.FromNative(tc.expected.Z, pt) - if !ok { - t.Fatal("expected ok") - } - - if *tc.expected != *tile { - t.Errorf("got %v expected %v", *tile, *tc.expected) - } - } - } - - // expected = tile column, tile row - tests := map[string]tcase{ - "3857_z0": { - point: geom.Point{0.0, 0.0}, - srid: 3857, - expected: NewTile(0, 0, 0), - }, - "3857_z0_random": { - point: geom.Point{96.7283, 43.5473}, - srid: 3857, - expected: NewTile(0, 0, 0), - }, - "3857_z10_quad1": { - point: geom.Point{179.99999, 85.0511}, - srid: 3857, - expected: NewTile(10, 1023, 0), - }, - "3857_z10_quad2": { - point: geom.Point{-179.99999, 85.0511}, - srid: 3857, - expected: NewTile(10, 0, 0), - }, - "3857_z10_quad3": { - point: geom.Point{-179.99999, -85.0511}, - srid: 3857, - expected: NewTile(10, 0, 1023), - }, - "3857_z10_quad4": { - point: geom.Point{179.99999, -85.0511}, - srid: 3857, - expected: NewTile(10, 1023, 1023), - }, - "4326_z0_quad1": { - point: geom.Point{0.0, 0.0}, - srid: 4326, - expected: NewTile(0, 1, 0), - }, - "4326_z0_quad2": { - point: geom.Point{-1.0, 0.0}, - srid: 4326, - expected: NewTile(0, 0, 0), - }, - "4326_z10_quad1": { - point: geom.Point{179.99999, 89.99999}, - srid: 4326, - expected: NewTile(10, 2047, 0), - }, - "4326_z10_quad2": { - point: geom.Point{-179.99999, 89.99999}, - srid: 4326, - expected: NewTile(10, 0, 0), - }, - "4326_z10_quad3": { - point: geom.Point{-179.99999, -89.99999}, - srid: 4326, - expected: NewTile(10, 0, 1023), - }, - "4326_z10_quad4": { - point: geom.Point{179.99999, -89.99999}, - srid: 4326, - expected: NewTile(10, 2047, 1023), - }, - } - - for name, tc := range tests { - t.Run(name, fn(tc)) - } -} - -func TestToNative(t *testing.T) { - type tcase struct { - tile *Tile - srid uint - expected geom.Point - } - - fn := func(tc tcase) func(t *testing.T) { - return func(t *testing.T) { - grid, err := NewGrid(tc.srid) - if err != nil { - t.Fatal(err) - } - - pt, ok := grid.ToNative(tc.tile) - if !ok { - t.Fatal("expected ok") - } - - if tc.srid != 4326 { - pts, err := proj.Inverse(proj.EPSGCode(tc.srid), pt[:]) - if err != nil { - t.Fatal(err, tc.srid, pt) - } - pt = geom.Point{pts[0], pts[1]} - } - - if !cmp.PointEqual(pt, tc.expected) { - t.Errorf("got %v expected %v", pt, tc.expected) - } - - } - } - - tests := map[string]tcase{ - "3857_z0": { - tile: NewTile(0, 0, 0), - srid: 3857, - expected: geom.Point{-179.9999999749438, 85.05112877764508}, - }, - "3857_z10_q1": { - tile: NewTile(10, 1023, 0), - srid: 3857, - expected: geom.Point{179.64843747499273, 85.05112877764508}, - }, - "3857_z10_q2": { - tile: NewTile(10, 0, 0), - srid: 3857, - expected: geom.Point{-179.9999999749438, 85.05112877764508}, - }, - "3857_z10_q3": { - tile: NewTile(10, 0, 1023), - srid: 3857, - expected: geom.Point{-179.9999999749438, -85.0207077409554}, - }, - "3857_z10_q4": { - tile: NewTile(10, 1023, 1023), - srid: 3857, - expected: geom.Point{179.64843747499273, -85.0207077409554}, - }, - "4326_z0_q1": { - tile: NewTile(0, 1, 0), - srid: 4326, - expected: geom.Point{0, 90}, - }, - "4326_z0_q2": { - tile: NewTile(0, 0, 0), - srid: 4326, - expected: geom.Point{-180, 90}, - }, - "4326_z10_q1": { - tile: NewTile(10, 2047, 0), - srid: 4326, - expected: geom.Point{179.82421875, 90}, - }, - "4326_z10_q2": { - tile: NewTile(10, 0, 0), - srid: 4326, - expected: geom.Point{-180, 90}, - }, - "4326_z10_q3": { - tile: NewTile(10, 0, 1023), - srid: 4326, - expected: geom.Point{-180, -89.82421875}, - }, - "4326_z10_q4": { - tile: NewTile(10, 2047, 1023), - srid: 4326, - expected: geom.Point{179.82421875, -89.82421875}, - }, - } - - for name, tc := range tests { - t.Run(name, fn(tc)) - } -} diff --git a/slippy/tile_test.go b/slippy/tile_test.go index 2c38f8f1..eeeefbeb 100644 --- a/slippy/tile_test.go +++ b/slippy/tile_test.go @@ -1,187 +1,248 @@ -package slippy_test +package slippy import ( + "embed" + "errors" + "fmt" + "github.com/go-spatial/geom" + "github.com/go-spatial/geom/spherical" + "github.com/go-spatial/proj" + "os" + "path/filepath" "strconv" "testing" + "unicode" +) - "github.com/go-spatial/geom/slippy" +var ( + //go:embed testdata + testdata embed.FS ) -func TestNewTile(t *testing.T) { - type tcase struct { - z, x, y uint +func must[T any](a T, err error) T { + if err != nil { + panic(err) } - fn := func(tc tcase) func(t *testing.T) { - return func(t *testing.T) { + return a +} - // Test the new functions. - tile := slippy.NewTile(tc.z, tc.x, tc.y) - { - gz, gx, gy := tile.ZXY() - if gz != tc.z { - t.Errorf("z, expected %v got %v", tc.z, gz) - } - if gx != tc.x { - t.Errorf("x, expected %v got %v", tc.x, gx) - } - if gy != tc.y { - t.Errorf("y, expected %v got %v", tc.y, gy) - } - } +func tcCurry[T any](fn func(T, *testing.T)) func(T) func(*testing.T) { + return func(tc T) func(*testing.T) { + return func(t *testing.T) { + fn(tc, t) } } - tests := [...]tcase{ - { - z: 2, - x: 1, - y: 1, - }, - { - z: 16, - x: 11436, - y: 26461, - }, - { - z: 0, - x: 0, - y: 0, - }, - } - for i, tc := range tests { - t.Run(strconv.FormatUint(uint64(i), 10), fn(tc)) - } +} +type FindableTile struct { + Tile + found bool } -/* -func TestRangeFamilyAt(t *testing.T) { - type coord struct { - z, x, y uint +func tilesFromSlice(coords []int) ([]FindableTile, error) { + if len(coords)%3 != 0 { + return nil, fmt.Errorf("tilesFromSlice expects an number of coordinates to be a multiple of 3: got %v", len(coords)) } - - type tcase struct { - tile *slippy.Tile - tileSRID uint - zoomAt uint - expected []coord + tiles := make([]FindableTile, 0, len(coords)/3) + for i := 0; i < len(coords); i += 3 { + tiles = append(tiles, FindableTile{Tile: Tile{Z: Zoom(coords[i]), X: coords[i+1], Y: coords[i+2]}}) } + return tiles, nil +} - isIn := func(arr []coord, c coord) bool { - for _, v := range arr { - if v == c { - return true +func LoadCoords(bytes []byte) ([]int, error) { + // file should be a set of 3 integers separated by space, representing a tile. + // comments are `#` followed by the comment till the end of the line. + var ( + text = string(bytes) + inComment = false + numberBuffer = make([]rune, 0, 10) + line = 1 + offset = 0 + aNum int64 + err error + coords []int + ) + for _, aChar := range text { + offset++ + if inComment { + // check to see if we have a newline + if aChar == '\n' { + line++ + offset = 0 + inComment = false } + continue } + if unicode.IsSpace(aChar) || aChar == ',' || aChar == '/' || aChar == '\n' || aChar == '#' { + if aChar == '\n' { + line++ + offset = 0 + } + + if aChar == '#' { + // we will be in a comment after this + inComment = true + } - return false + // found a separator character + if len(numberBuffer) == 0 { + continue + } + aNum, err = strconv.ParseInt(string(numberBuffer), 0, 64) + if err != nil { + return nil, fmt.Errorf("error parsing number on line %v offset %v: %w", line, offset, err) + } + // reset our buffer + numberBuffer = numberBuffer[:0] + coords = append(coords, int(aNum)) + continue + } + if unicode.IsDigit(aChar) { + numberBuffer = append(numberBuffer, aChar) + } + } + if len(numberBuffer) != 0 { + aNum, err = strconv.ParseInt(string(numberBuffer), 0, 64) + if err != nil { + return nil, fmt.Errorf("error parsing number on line %v offset %v: %w", line, offset, err) + } + numberBuffer = numberBuffer[:0] + coords = append(coords, int(aNum)) + } + return coords, nil +} +func LoadTiles(bytes []byte) ([]FindableTile, error) { + coords, err := LoadCoords(bytes) + if err != nil { + return nil, err } + return tilesFromSlice(coords) +} - fn := func(tc tcase) func(t *testing.T) { - return func(t *testing.T) { +func WriteTilesToFile[S fmt.Stringer](filename string, tiles []S) error { + file, err := os.Create(filename) + if err != nil { + return err + } + defer func() { _ = file.Close() }() + for _, tile := range tiles { + if _, err = file.WriteString(tile.String() + "\n"); err != nil { + return err + } + } + return nil +} - coordList := make([]coord, 0, len(tc.expected)) - tc.tile.RangeFamilyAt(tc.zoomAt, tc.tileSRID, func(tile *slippy.Tile, srid uint) error { - z, x, y := tile.ZXY() - c := coord{z, x, y} +func TestRangeFamilyAt2(t *testing.T) { - coordList = append(coordList, c) + type tcase struct { + Tile + Zoom Zoom + expected []int + } - return nil - }) + fn := tcCurry(func(tc tcase, t *testing.T) { - if len(coordList) != len(tc.expected) { - t.Fatalf("coordinate list length, expected %d, got %d", len(tc.expected), len(coordList)) + count := 0 + expectedTiles, err := tilesFromSlice(tc.expected) + if err != nil { + t.Fatalf("tilesFromSlice error: %v", err) + } + RangeFamilyAt(tc.Tile, tc.Zoom, func(tile Tile) bool { + count++ + t.Logf("Got tile %v", tile) + + found := false + for i := range expectedTiles { + if !expectedTiles[i].Equal(tile) { + continue + } + found = true + expectedTiles[i].found = true + break + } + if !found { + t.Errorf("tile %v not found in expected tiles", tile) } + return true + }) - for _, v := range tc.expected { - if !isIn(coordList, v) { - t.Logf("coordinates: %v", coordList) - t.Fatalf("coordinate exists, expected %v, got missing", v) - } + for _, v := range expectedTiles { + if !v.found { + t.Errorf("expected tile %v not found", v) } + } + if count != len(expectedTiles) { + t.Fatalf("list length, expected %d, got %d", len(expectedTiles), count) } - } + }) testcases := map[string]tcase{ "children 1": { - tile: slippy.NewTile(0, 0, 0), - tileSRID: proj.WebMercator, - zoomAt: 1, - expected: []coord{ - {1, 0, 0}, - {1, 0, 1}, - {1, 1, 0}, - {1, 1, 1}, + Tile: Tile{0, 0, 0}, + Zoom: 1, + expected: []int{ + 1, 0, 0, + 1, 0, 1, + 1, 1, 0, + 1, 1, 1, }, }, "children 2": { - tile: slippy.NewTile(8, 3, 5), - tileSRID: proj.WebMercator, - zoomAt: 10, - expected: []coord{ - {10, 12, 20}, - {10, 12, 21}, - {10, 12, 22}, - {10, 12, 23}, + Tile: Tile{8, 3, 5}, + Zoom: 10, + expected: []int{ + 10, 12, 20, + 10, 12, 21, + 10, 12, 22, + 10, 12, 23, // - {10, 13, 20}, - {10, 13, 21}, - {10, 13, 22}, - {10, 13, 23}, + 10, 13, 20, + 10, 13, 21, + 10, 13, 22, + 10, 13, 23, // - {10, 14, 20}, - {10, 14, 21}, - {10, 14, 22}, - {10, 14, 23}, + 10, 14, 20, + 10, 14, 21, + 10, 14, 22, + 10, 14, 23, // - {10, 15, 20}, - {10, 15, 21}, - {10, 15, 22}, - {10, 15, 23}, + 10, 15, 20, + 10, 15, 21, + 10, 15, 22, + 10, 15, 23, }, }, "parent 1": { - tile: slippy.NewTile(1, 0, 0), - tileSRID: proj.WebMercator, - zoomAt: 0, - expected: []coord{ - {0, 0, 0}, - }, + Tile: Tile{1, 0, 0}, + Zoom: 0, + expected: []int{0, 0, 0}, }, "parent 2": { - tile: slippy.NewTile(3, 3, 5), - tileSRID: proj.WebMercator, - zoomAt: 1, - expected: []coord{ - {1, 0, 1}, - }, + Tile: Tile{3, 3, 5}, + Zoom: 1, + expected: []int{1, 0, 1}, }, - "parent 4326 1": { - tile: slippy.NewTile(1, 3, 0), - tileSRID: 4326, - zoomAt: 0, - expected: []coord{ - {0, 1, 0}, - }, + "parent 1.1 ": { + Tile: Tile{1, 3, 0}, + Zoom: 0, + expected: []int{0, 1, 0}, }, - "parent 4326 2": { - tile: slippy.NewTile(4, 31, 15), - tileSRID: 4326, - zoomAt: 1, - expected: []coord{ - {1, 3, 1}, - }, + "parent 2.1": { + Tile: Tile{4, 31, 15}, + Zoom: 1, + expected: []int{1, 3, 1}, }, - "children 4326": { - tile: slippy.NewTile(2, 7, 3), - tileSRID: 4326, - zoomAt: 3, - expected: []coord{ - {3, 14, 6}, - {3, 15, 6}, - {3, 14, 7}, - {3, 15, 7}, + "children 1.1": { + Tile: Tile{2, 7, 3}, + Zoom: 3, + expected: []int{ + 3, 14, 6, + 3, 15, 6, + 3, 14, 7, + 3, 15, 7, }, }, } @@ -191,137 +252,148 @@ func TestRangeFamilyAt(t *testing.T) { } } - -func TestNewTileMinMaxer(t *testing.T) { +func TestFromBounds2(t *testing.T) { type tcase struct { - mm geom.MinMaxer - tile *slippy.Tile - tileSRID uint + Grid TileGriddor // if nil, we will default to Grid4326 + Bounds geom.PtMinMaxer + Z Zoom + Tiles []FindableTile + + err error } - fn := func(tc tcase) func(t *testing.T) { - return func(t *testing.T) { + fn := tcCurry(func(tc tcase, t *testing.T) { + if tc.Grid == nil { + tc.Grid = Grid4326{} + } - tile := slippy.NewTileMinMaxer(tc.mm, tc.tileSRID) - if !reflect.DeepEqual(tile, tc.tile) { - t.Errorf("tile, expected %v, got %v", tc.tile, tile) + tiles, err := FromBounds(tc.Grid, tc.Bounds, tc.Z) + if tc.err != nil { + if err == nil { + t.Errorf("error expected %v, got nil", tc.err) + return } - + if !errors.Is(err, tc.err) { + t.Errorf("error expected %v, got %v", tc.err, err) + return + } + return } - } - - testcases := map[string]tcase{ - "1": { - mm: spherical.Hull( - [2]float64{-179.0, 85.0}, - [2]float64{179.0, -85.0}), - tile: slippy.NewTile(0, 0, 0), - tileSRID: proj.WebMercator, - }, - "2": { - mm: slippy.NewTile(15, 2, 98).Extent4326(3857), - tile: slippy.NewTile(15, 2, 98), - tileSRID: proj.WebMercator, - }, - } - - for name, tc := range testcases { - t.Run(name, fn(tc)) - } -} - - -func TestFromBounds(t *testing.T) { - - type tcase struct { - Bounds *geom.Extent - Z uint - Tiles []slippy.Tile - TileSRID uint - } - - fn := func(tc tcase) func(t *testing.T) { - return func(t *testing.T) { - - tiles := slippy.FromBounds(tc.Bounds, tc.Z, 3857) - if !reflect.DeepEqual(tiles, tc.Tiles) { - t.Errorf("tiles, expected %v, got %v", tc.Tiles, tiles) + if err != nil { + t.Errorf("error expected nil, got %v", err) + return + } + defer func() { + if !t.Failed() { + return } - + // we failed the test, let's dump the tiles + var ( + filename = fmt.Sprintf("testdata/failed_output/%v.failed.tiles", filepath.Base(t.Name())) + err error + ) + if err = WriteTilesToFile(filename, tiles); err != nil { + t.Logf("failed to write failed tiles file(%v): %v", filename, err) + return + } else { + t.Logf("wrote failed tiles file(%v)", filename) + } + filename = fmt.Sprintf("testdata/failed_output/%v.expected.tiles", filepath.Base(t.Name())) + if err = WriteTilesToFile(filename, tc.Tiles); err != nil { + t.Logf("failed to write expected tiles file(%v): %v", filename, err) + return + } else { + t.Logf("wrote expected tiles file(%v)", filename) + } + }() + if len(tiles) != len(tc.Tiles) { + t.Errorf("len expected %v, got %v", len(tc.Tiles), len(tiles)) + return } - } + NextTile: + for i := range tiles { + for j, tile := range tc.Tiles { + if tile.found { + continue + } + if tile.Equal(tiles[i]) { + tc.Tiles[j].found = true + continue NextTile + } + } + t.Errorf("tile[%v] %v not found in expected tiles", i, tiles[i]) + } + for i, tile := range tc.Tiles { + if tile.found { + continue + } + t.Errorf("expected tile[%v] %v not found in tiles", i, tc.Tiles[i]) + } + + }) + tests := map[string]tcase{ - "nil bounds": tcase{}, - "San Diego 15z": tcase{ - Z: 15, - Bounds: spherical.Hull([2]float64{-117.15, 32.6894743}, [2]float64{-116.804, 32.6339}), - TileSRID: 3857, - Tiles: []slippy.Tile{ - {Z: 15, X: 5720, Y: 13232}, {Z: 15, X: 5720, Y: 13233}, {Z: 15, X: 5720, Y: 13234}, {Z: 15, X: 5720, Y: 13235}, {Z: 15, X: 5720, Y: 13236}, - {Z: 15, X: 5720, Y: 13237}, {Z: 15, X: 5720, Y: 13238}, {Z: 15, X: 5721, Y: 13232}, {Z: 15, X: 5721, Y: 13233}, {Z: 15, X: 5721, Y: 13234}, - {Z: 15, X: 5721, Y: 13235}, {Z: 15, X: 5721, Y: 13236}, {Z: 15, X: 5721, Y: 13237}, {Z: 15, X: 5721, Y: 13238}, {Z: 15, X: 5722, Y: 13232}, - {Z: 15, X: 5722, Y: 13233}, {Z: 15, X: 5722, Y: 13234}, {Z: 15, X: 5722, Y: 13235}, {Z: 15, X: 5722, Y: 13236}, {Z: 15, X: 5722, Y: 13237}, - {Z: 15, X: 5722, Y: 13238}, {Z: 15, X: 5723, Y: 13232}, {Z: 15, X: 5723, Y: 13233}, {Z: 15, X: 5723, Y: 13234}, {Z: 15, X: 5723, Y: 13235}, - {Z: 15, X: 5723, Y: 13236}, {Z: 15, X: 5723, Y: 13237}, {Z: 15, X: 5723, Y: 13238}, {Z: 15, X: 5724, Y: 13232}, {Z: 15, X: 5724, Y: 13233}, - {Z: 15, X: 5724, Y: 13234}, {Z: 15, X: 5724, Y: 13235}, {Z: 15, X: 5724, Y: 13236}, {Z: 15, X: 5724, Y: 13237}, {Z: 15, X: 5724, Y: 13238}, - {Z: 15, X: 5725, Y: 13232}, {Z: 15, X: 5725, Y: 13233}, {Z: 15, X: 5725, Y: 13234}, {Z: 15, X: 5725, Y: 13235}, {Z: 15, X: 5725, Y: 13236}, - {Z: 15, X: 5725, Y: 13237}, {Z: 15, X: 5725, Y: 13238}, {Z: 15, X: 5726, Y: 13232}, {Z: 15, X: 5726, Y: 13233}, {Z: 15, X: 5726, Y: 13234}, - {Z: 15, X: 5726, Y: 13235}, {Z: 15, X: 5726, Y: 13236}, {Z: 15, X: 5726, Y: 13237}, {Z: 15, X: 5726, Y: 13238}, {Z: 15, X: 5727, Y: 13232}, - {Z: 15, X: 5727, Y: 13233}, {Z: 15, X: 5727, Y: 13234}, {Z: 15, X: 5727, Y: 13235}, {Z: 15, X: 5727, Y: 13236}, {Z: 15, X: 5727, Y: 13237}, - {Z: 15, X: 5727, Y: 13238}, {Z: 15, X: 5728, Y: 13232}, {Z: 15, X: 5728, Y: 13233}, {Z: 15, X: 5728, Y: 13234}, {Z: 15, X: 5728, Y: 13235}, - {Z: 15, X: 5728, Y: 13236}, {Z: 15, X: 5728, Y: 13237}, {Z: 15, X: 5728, Y: 13238}, {Z: 15, X: 5729, Y: 13232}, {Z: 15, X: 5729, Y: 13233}, - {Z: 15, X: 5729, Y: 13234}, {Z: 15, X: 5729, Y: 13235}, {Z: 15, X: 5729, Y: 13236}, {Z: 15, X: 5729, Y: 13237}, {Z: 15, X: 5729, Y: 13238}, - {Z: 15, X: 5730, Y: 13232}, {Z: 15, X: 5730, Y: 13233}, {Z: 15, X: 5730, Y: 13234}, {Z: 15, X: 5730, Y: 13235}, {Z: 15, X: 5730, Y: 13236}, - {Z: 15, X: 5730, Y: 13237}, {Z: 15, X: 5730, Y: 13238}, {Z: 15, X: 5731, Y: 13232}, {Z: 15, X: 5731, Y: 13233}, {Z: 15, X: 5731, Y: 13234}, - {Z: 15, X: 5731, Y: 13235}, {Z: 15, X: 5731, Y: 13236}, {Z: 15, X: 5731, Y: 13237}, {Z: 15, X: 5731, Y: 13238}, {Z: 15, X: 5732, Y: 13232}, - {Z: 15, X: 5732, Y: 13233}, {Z: 15, X: 5732, Y: 13234}, {Z: 15, X: 5732, Y: 13235}, {Z: 15, X: 5732, Y: 13236}, {Z: 15, X: 5732, Y: 13237}, - {Z: 15, X: 5732, Y: 13238}, {Z: 15, X: 5733, Y: 13232}, {Z: 15, X: 5733, Y: 13233}, {Z: 15, X: 5733, Y: 13234}, {Z: 15, X: 5733, Y: 13235}, - {Z: 15, X: 5733, Y: 13236}, {Z: 15, X: 5733, Y: 13237}, {Z: 15, X: 5733, Y: 13238}, {Z: 15, X: 5734, Y: 13232}, {Z: 15, X: 5734, Y: 13233}, - {Z: 15, X: 5734, Y: 13234}, {Z: 15, X: 5734, Y: 13235}, {Z: 15, X: 5734, Y: 13236}, {Z: 15, X: 5734, Y: 13237}, {Z: 15, X: 5734, Y: 13238}, - {Z: 15, X: 5735, Y: 13232}, {Z: 15, X: 5735, Y: 13233}, {Z: 15, X: 5735, Y: 13234}, {Z: 15, X: 5735, Y: 13235}, {Z: 15, X: 5735, Y: 13236}, - {Z: 15, X: 5735, Y: 13237}, {Z: 15, X: 5735, Y: 13238}, {Z: 15, X: 5736, Y: 13232}, {Z: 15, X: 5736, Y: 13233}, {Z: 15, X: 5736, Y: 13234}, - {Z: 15, X: 5736, Y: 13235}, {Z: 15, X: 5736, Y: 13236}, {Z: 15, X: 5736, Y: 13237}, {Z: 15, X: 5736, Y: 13238}, {Z: 15, X: 5737, Y: 13232}, - {Z: 15, X: 5737, Y: 13233}, {Z: 15, X: 5737, Y: 13234}, {Z: 15, X: 5737, Y: 13235}, {Z: 15, X: 5737, Y: 13236}, {Z: 15, X: 5737, Y: 13237}, - {Z: 15, X: 5737, Y: 13238}, {Z: 15, X: 5738, Y: 13232}, {Z: 15, X: 5738, Y: 13233}, {Z: 15, X: 5738, Y: 13234}, {Z: 15, X: 5738, Y: 13235}, - {Z: 15, X: 5738, Y: 13236}, {Z: 15, X: 5738, Y: 13237}, {Z: 15, X: 5738, Y: 13238}, {Z: 15, X: 5739, Y: 13232}, {Z: 15, X: 5739, Y: 13233}, - {Z: 15, X: 5739, Y: 13234}, {Z: 15, X: 5739, Y: 13235}, {Z: 15, X: 5739, Y: 13236}, {Z: 15, X: 5739, Y: 13237}, {Z: 15, X: 5739, Y: 13238}, - {Z: 15, X: 5740, Y: 13232}, {Z: 15, X: 5740, Y: 13233}, {Z: 15, X: 5740, Y: 13234}, {Z: 15, X: 5740, Y: 13235}, {Z: 15, X: 5740, Y: 13236}, - {Z: 15, X: 5740, Y: 13237}, {Z: 15, X: 5740, Y: 13238}, {Z: 15, X: 5741, Y: 13232}, {Z: 15, X: 5741, Y: 13233}, {Z: 15, X: 5741, Y: 13234}, - {Z: 15, X: 5741, Y: 13235}, {Z: 15, X: 5741, Y: 13236}, {Z: 15, X: 5741, Y: 13237}, {Z: 15, X: 5741, Y: 13238}, {Z: 15, X: 5742, Y: 13232}, - {Z: 15, X: 5742, Y: 13233}, {Z: 15, X: 5742, Y: 13234}, {Z: 15, X: 5742, Y: 13235}, {Z: 15, X: 5742, Y: 13236}, {Z: 15, X: 5742, Y: 13237}, - {Z: 15, X: 5742, Y: 13238}, {Z: 15, X: 5743, Y: 13232}, {Z: 15, X: 5743, Y: 13233}, {Z: 15, X: 5743, Y: 13234}, {Z: 15, X: 5743, Y: 13235}, - {Z: 15, X: 5743, Y: 13236}, {Z: 15, X: 5743, Y: 13237}, {Z: 15, X: 5743, Y: 13238}, {Z: 15, X: 5744, Y: 13232}, {Z: 15, X: 5744, Y: 13233}, - {Z: 15, X: 5744, Y: 13234}, {Z: 15, X: 5744, Y: 13235}, {Z: 15, X: 5744, Y: 13236}, {Z: 15, X: 5744, Y: 13237}, {Z: 15, X: 5744, Y: 13238}, - {Z: 15, X: 5745, Y: 13232}, {Z: 15, X: 5745, Y: 13233}, {Z: 15, X: 5745, Y: 13234}, {Z: 15, X: 5745, Y: 13235}, {Z: 15, X: 5745, Y: 13236}, - {Z: 15, X: 5745, Y: 13237}, {Z: 15, X: 5745, Y: 13238}, {Z: 15, X: 5746, Y: 13232}, {Z: 15, X: 5746, Y: 13233}, {Z: 15, X: 5746, Y: 13234}, - {Z: 15, X: 5746, Y: 13235}, {Z: 15, X: 5746, Y: 13236}, {Z: 15, X: 5746, Y: 13237}, {Z: 15, X: 5746, Y: 13238}, {Z: 15, X: 5747, Y: 13232}, - {Z: 15, X: 5747, Y: 13233}, {Z: 15, X: 5747, Y: 13234}, {Z: 15, X: 5747, Y: 13235}, {Z: 15, X: 5747, Y: 13236}, {Z: 15, X: 5747, Y: 13237}, - {Z: 15, X: 5747, Y: 13238}, {Z: 15, X: 5748, Y: 13232}, {Z: 15, X: 5748, Y: 13233}, {Z: 15, X: 5748, Y: 13234}, {Z: 15, X: 5748, Y: 13235}, - {Z: 15, X: 5748, Y: 13236}, {Z: 15, X: 5748, Y: 13237}, {Z: 15, X: 5748, Y: 13238}, {Z: 15, X: 5749, Y: 13232}, {Z: 15, X: 5749, Y: 13233}, - {Z: 15, X: 5749, Y: 13234}, {Z: 15, X: 5749, Y: 13235}, {Z: 15, X: 5749, Y: 13236}, {Z: 15, X: 5749, Y: 13237}, {Z: 15, X: 5749, Y: 13238}, - {Z: 15, X: 5750, Y: 13232}, {Z: 15, X: 5750, Y: 13233}, {Z: 15, X: 5750, Y: 13234}, {Z: 15, X: 5750, Y: 13235}, {Z: 15, X: 5750, Y: 13236}, - {Z: 15, X: 5750, Y: 13237}, {Z: 15, X: 5750, Y: 13238}, {Z: 15, X: 5751, Y: 13232}, {Z: 15, X: 5751, Y: 13233}, {Z: 15, X: 5751, Y: 13234}, - {Z: 15, X: 5751, Y: 13235}, {Z: 15, X: 5751, Y: 13236}, {Z: 15, X: 5751, Y: 13237}, {Z: 15, X: 5751, Y: 13238}, {Z: 15, X: 5752, Y: 13232}, - {Z: 15, X: 5752, Y: 13233}, {Z: 15, X: 5752, Y: 13234}, {Z: 15, X: 5752, Y: 13235}, {Z: 15, X: 5752, Y: 13236}, {Z: 15, X: 5752, Y: 13237}, - {Z: 15, X: 5752, Y: 13238}, - }, + "nil bounds": {err: ErrNilBounds}, + "San Diego 15z": { + Z: 15, + Bounds: spherical.Hull([2]float64{-117.15, 32.6894743}, [2]float64{-116.804, 32.6339}), + Tiles: must(LoadTiles(must(testdata.ReadFile("testdata/for_bounds/san_diego_15z.coords")))), + }, + "San Diego 11z": { + Z: 11, + Bounds: spherical.Hull([2]float64{-117.15, 32.6894743}, [2]float64{-116.804, 32.6339}), + Tiles: must(LoadTiles(must(testdata.ReadFile("testdata/for_bounds/san_diego_11z.coords")))), + }, + "San Diego 9z": { + Z: 9, + Bounds: spherical.Hull([2]float64{-117.15, 32.6894743}, [2]float64{-116.804, 32.6339}), + Tiles: must(LoadTiles(must(testdata.ReadFile("testdata/for_bounds/san_diego_9z.coords")))), }, - "San Diego 11z": tcase{ - Z: 11, - Bounds: spherical.Hull([2]float64{-117.15, 32.6894743}, [2]float64{-116.804, 32.6339}), - TileSRID: 3857, - Tiles: []slippy.Tile{{Z: 11, X: 357, Y: 827}, {Z: 11, X: 358, Y: 827}, {Z: 11, X: 359, Y: 827}}, + "tegola issue 997": { + Z: 7, + Bounds: spherical.Hull( + [2]float64{ + 2.636719, + 50.625073, + }, + [2]float64{ + 7.613525, + 53.820112, + }), + Tiles: must(LoadTiles(must(testdata.ReadFile("testdata/for_bounds/tegola_issue_997.coords")))), }, - "San Diego 9z": tcase{ - Z: 9, - Bounds: spherical.Hull([2]float64{-117.15, 32.6894743}, [2]float64{-116.804, 32.6339}), - TileSRID: 3857, - Tiles: []slippy.Tile{{Z: 9, X: 89, Y: 206}}, + "tegola issue 997 w seeding bounds": { + Z: 7, + Grid: NewGrid(proj.EPSG4326, 0), + Bounds: spherical.Hull( + [2]float64{ + 3.011234, + 50.16669, + }, + [2]float64{ + 7.64906, + 54.683876, + }), + Tiles: must(LoadTiles(must(testdata.ReadFile("testdata/for_bounds/tegola_issue_997_w_seeding_bounds.coords")))), + }, + "tegola issue 997 SRID 3857": { + Z: 7, + Grid: NewGrid(proj.EPSG3857, DefaultTileSize), + Bounds: spherical.Hull( + [2]float64{ + 293518.1886, + 6555239.5457, + }, + [2]float64{ + 847533.7696, + 7136160.9607, + }), + Tiles: must(LoadTiles(must(testdata.ReadFile("testdata/for_bounds/tegola_issue_997.coords")))), }, } + for name, tc := range tests { t.Run(name, fn(tc)) } } -*/