diff --git a/planar/makevalid/makevalid_test.go b/planar/makevalid/makevalid_test.go index 58d68b7a..8fe5c7d0 100644 --- a/planar/makevalid/makevalid_test.go +++ b/planar/makevalid/makevalid_test.go @@ -8,6 +8,8 @@ import ( "strings" "testing" + "github.com/go-spatial/proj" + "github.com/go-spatial/geom/encoding/wkt" "github.com/go-spatial/geom/slippy" @@ -132,7 +134,7 @@ func checkMakeValid(tb testing.TB) { didClip: true, }, "issue#70_full": { - ClipBox: slippy.NewTile(13, 8054, 2677).Extent3857().ExpandBy(64.0), + ClipBox: slippy.NewTile(13, 8054, 2677).Extent3857(proj.WebMercator).ExpandBy(64.0), MultiPolygon: func() *geom.MultiPolygon { b, err := ioutil.ReadFile(`testdata/issue70.polygon`) if err != nil { diff --git a/slippy/projections.go b/slippy/projections.go index d4e18917..a869bd59 100644 --- a/slippy/projections.go +++ b/slippy/projections.go @@ -1,36 +1,22 @@ package slippy -import "math" +import ( + "fmt" + "math" -// ==== lat lon (aka WGS 84) ==== + "github.com/go-spatial/geom" +) -// Lat2Tile takes a zoom and a lat to produce the lon -func Lat2Tile(zoom uint, lat float64) (y uint) { - latRad := lat * math.Pi / 180 - - return uint(math.Exp2(float64(zoom))* - (1.0-math.Log( - math.Tan(latRad)+ - (1/math.Cos(latRad)))/math.Pi)) / - 2.0 -} - -// Lon2Tile takes in a zoom and lon to produce the lat -func Lon2Tile(zoom uint, lon float64) (x uint) { - return uint(math.Exp2(float64(zoom)) * (lon + 180.0) / 360.0) +type extents struct { + NativeExtents *geom.Extent + WGS84Extents *geom.Extent + Grid TileGrid } -// Tile2Lon will return the west most longitude -func Tile2Lon(zoom, x uint) float64 { return float64(x)/math.Exp2(float64(zoom))*360.0 - 180.0 } - -// Tile2Lat will return the north most latitude -func Tile2Lat(zoom, y uint) float64 { - var n float64 = math.Pi - if y != 0 { - n = math.Pi - 2.0*math.Pi*float64(y)/math.Exp2(float64(zoom)) - } - - return 180.0 / math.Pi * math.Atan(0.5*(math.Exp(n)-math.Exp(-n))) +// SupportedProjections contains supported projection native and lat/long extents as well as tile layout ratio +var SupportedProjections = map[uint]extents{ + 3857: extents{NativeExtents: &geom.Extent{-20026376.39, -20048966.10, 20026376.39, 20048966.10}, WGS84Extents: &geom.Extent{-180.0, -85.0511, 180.0, 85.0511}, Grid: GetGrid(3857)}, + 4326: extents{NativeExtents: &geom.Extent{-180.0, -90.0, 180.0, 90.0}, WGS84Extents: &geom.Extent{-180.0, -90.0, 180.0, 90.0}, Grid: GetGrid(4326)}, } // ==== Web Mercator ==== @@ -38,41 +24,33 @@ func Tile2Lat(zoom, y uint) float64 { // WebMercatorMax is the max size in meters of a tile const WebMercatorMax = 20037508.34 -// Tile2WebX returns the side of the tile in the -x side -func Tile2WebX(zoom uint, n uint) float64 { +// Tile2WebX returns the side of the tile in the -x side in webmercator +func Tile2WebX(zoom uint, n uint, srid uint) float64 { res := (WebMercatorMax * 2) / math.Exp2(float64(zoom)) - return -WebMercatorMax + float64(n)*res } -// Tile2WebY returns the side of the tile in the +y side -func Tile2WebY(zoom uint, n uint) float64 { +// Tile2WebY returns the side of the tile in the +y side in webmercator +func Tile2WebY(zoom uint, n uint, srid uint) float64 { res := (WebMercatorMax * 2) / math.Exp2(float64(zoom)) return WebMercatorMax - float64(n)*res } -// WebX2Tile returns the column of the tile given the web mercator x value -func WebX2Tile(zoom uint, x float64) uint { - res := (WebMercatorMax * 2) / math.Exp2(float64(zoom)) - - return uint((x + WebMercatorMax) / res) -} - -// WebY2Tile returns the row of the tile given the web mercator y value -func WebY2Tile(zoom uint, y float64) uint { - res := (WebMercatorMax * 2) / math.Exp2(float64(zoom)) - - return uint(-(y - WebMercatorMax) / res) -} - // ==== pixels ==== // MvtTileDim is the number of pixels in a tile const MvtTileDim = 4096.0 -// Pixels2Webs scalar conversion of pixels into web mercator units +// PixelsToProjectedUnits scalar conversion of pixels into projected units // TODO (@ear7h): perhaps rethink this -func Pixels2Webs(zoom uint, pixels uint) float64 { - return WebMercatorMax * 2 / math.Exp2(float64(zoom)) * float64(pixels) / MvtTileDim +func PixelsToProjectedUnits(zoom uint, pixels uint, srid uint) float64 { + switch srid { + case 3857: + return WebMercatorMax * 2 / math.Exp2(float64(zoom)) * float64(pixels) / MvtTileDim + case 4326: + return 360.0 / math.Exp2(float64(zoom)) * float64(pixels) / MvtTileDim / 2 + default: + panic(fmt.Sprintf("unsupported srid: %v", srid)) + } } diff --git a/slippy/tile.go b/slippy/tile.go index 33959b26..818be742 100644 --- a/slippy/tile.go +++ b/slippy/tile.go @@ -1,9 +1,11 @@ package slippy import ( + "errors" + "fmt" "math" - "errors" + "github.com/go-spatial/proj" "github.com/go-spatial/geom" ) @@ -33,15 +35,16 @@ type Tile struct { // NewTileMinMaxer returns the smallest tile which fits the // geom.MinMaxer. Note: it assumes the values of ext are // EPSG:4326 (lng/lat) -func NewTileMinMaxer(ext geom.MinMaxer) *Tile { - upperLeft := NewTileLatLon(MaxZoom, ext.MaxY(), ext.MinX()) +//TODO (meilinger): we need this anymore? +func NewTileMinMaxer(ext geom.MinMaxer, tileSRID uint) *Tile { + upperLeft := NewTileLatLon(MaxZoom, ext.MaxY(), ext.MinX(), tileSRID) point := &geom.Point{ext.MaxX(), ext.MinY()} var ret *Tile for z := uint(MaxZoom); int(z) >= 0 && ret == nil; z-- { - upperLeft.RangeFamilyAt(z, func(tile *Tile) error { - if tile.Extent4326().Contains(point) { + upperLeft.RangeFamilyAt(z, tileSRID, func(tile *Tile, srid uint) error { + if tile.Extent4326(tileSRID).Contains(point) { ret = tile return errors.New("stop iter") } @@ -54,9 +57,10 @@ func NewTileMinMaxer(ext geom.MinMaxer) *Tile { } // NewTileLatLon instantiates a tile containing the coordinate with the specified zoom -func NewTileLatLon(z uint, lat, lon float64) *Tile { - x := Lon2Tile(z, lon) - y := Lat2Tile(z, lat) +func NewTileLatLon(z uint, lat, lon float64, srid uint) *Tile { + grid := GetGrid(srid) + x := grid.Lon2XIndex(z, lon) + y := grid.Lat2YIndex(z, lat) return &Tile{ Z: z, @@ -73,13 +77,16 @@ func minmax(a, b uint) (uint, uint) { } // 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(bounds *geom.Extent, z uint) []Tile { +func FromBounds(bounds *geom.Extent, z uint, tileSRID uint) []Tile { if bounds == nil { return nil } - minx, maxx := minmax(Lon2Tile(z, bounds[0]), Lon2Tile(z, bounds[2])) - miny, maxy := minmax(Lat2Tile(z, bounds[1]), Lat2Tile(z, bounds[3])) + grid := GetGrid(tileSRID) + + minx, maxx := minmax(grid.Lon2XIndex(z, bounds[0]), grid.Lon2XIndex(z, bounds[2])) + miny, maxy := minmax(grid.Lat2YIndex(z, bounds[1]), grid.Lat2YIndex(z, bounds[3])) + // tiles := make([]Tile, (maxx-minx)*(maxy-miny)) var tiles []Tile for x := minx; x <= maxx; x++ { @@ -94,30 +101,64 @@ func FromBounds(bounds *geom.Extent, z uint) []Tile { // ZXY returns back the z,x,y of the tile func (t Tile) ZXY() (uint, uint, uint) { return t.Z, t.X, t.Y } +// Extent gets the extent of the tile in the units of the tileSRID +func (t Tile) NativeExtent(tileSRID uint) *geom.Extent { + if _, ok := SupportedProjections[tileSRID]; !ok { + panic(fmt.Sprintf("unsupported tileSRID %v", tileSRID)) + } + + grid := GetGrid(tileSRID) + pts := []float64{grid.XIndex2Lon(t.Z, t.X), grid.YIndex2Lat(t.Z, t.Y+1), grid.XIndex2Lon(t.Z, t.X+1), grid.YIndex2Lat(t.Z, t.Y)} + + // No need to go further, we've already got the WGS84 extents + if tileSRID == proj.WGS84 { + return geom.NewExtent( + [2]float64{pts[0], pts[1]}, + [2]float64{pts[2], pts[3]}, + ) + } + + pts, err := proj.Convert(proj.EPSGCode(tileSRID), pts) + if err != nil { + panic(fmt.Sprintf("error converting %v to %v", pts, tileSRID)) + } + + return geom.NewExtent( + [2]float64{pts[0], pts[1]}, + [2]float64{pts[2], pts[3]}, + ) +} + // Extent3857 returns the tile's extent in EPSG:3857 (aka Web Mercator) projection -func (t Tile) Extent3857() *geom.Extent { +func (t Tile) Extent3857(tileSRID uint) *geom.Extent { + if tileSRID != 3857 { + // Can't necessarily get webmercator extent for 4326 tile + panic("unable to get 3857 extent on 4326 tile") + } return geom.NewExtent( - [2]float64{Tile2WebX(t.Z, t.X), Tile2WebY(t.Z, t.Y+1)}, - [2]float64{Tile2WebX(t.Z, t.X+1), Tile2WebY(t.Z, t.Y)}, + [2]float64{Tile2WebX(t.Z, t.X, tileSRID), Tile2WebY(t.Z, t.Y+1, tileSRID)}, + [2]float64{Tile2WebX(t.Z, t.X+1, tileSRID), Tile2WebY(t.Z, t.Y, tileSRID)}, ) } -// Extent4326 returns the tile's extent in EPSG:4326 (aka lat/long) -func (t Tile) Extent4326() *geom.Extent { +// Extent4326 returns the tile's extent in EPSG:4326 (aka lat/long) given the tilespace's SRID +func (t Tile) Extent4326(tileSRID uint) *geom.Extent { + grid := GetGrid(tileSRID) return geom.NewExtent( - [2]float64{Tile2Lon(t.Z, t.X), Tile2Lat(t.Z, t.Y+1)}, - [2]float64{Tile2Lon(t.Z, t.X+1), Tile2Lat(t.Z, t.Y)}, + [2]float64{grid.XIndex2Lon(t.Z, t.X), grid.YIndex2Lat(t.Z, t.Y+1)}, + [2]float64{grid.XIndex2Lon(t.Z, t.X+1), grid.YIndex2Lat(t.Z, t.Y)}, ) } // RangeFamilyAt calls f on every tile vertically related to t at the specified zoom // TODO (ear7h): sibling support -func (t Tile) RangeFamilyAt(zoom uint, f func(*Tile) error) error { +//TODO (meilinger): should this be part of TileGrid? +func (t Tile) RangeFamilyAt(zoom, srid uint, f func(tile *Tile, srid uint) error) error { // handle ancestors and self if zoom <= t.Z { mag := t.Z - zoom arg := NewTile(zoom, t.X>>mag, t.Y>>mag) - return f(arg) + return f(arg, srid) } // handle descendants @@ -129,7 +170,7 @@ func (t Tile) RangeFamilyAt(zoom uint, f func(*Tile) error) error { for x := leastX; x < leastX+delta; x++ { for y := leastY; y < leastY+delta; y++ { - err := f(NewTile(zoom, x, y)) + err := f(NewTile(zoom, x, y), srid) if err != nil { return err } diff --git a/slippy/tile_grid.go b/slippy/tile_grid.go new file mode 100644 index 00000000..30e8e982 --- /dev/null +++ b/slippy/tile_grid.go @@ -0,0 +1,107 @@ +package slippy + +import ( + "fmt" + "math" +) + +// TileGrid contains the tile layout, including ability to get WGS84 coordinates for tile extents +type TileGrid interface { + ContainsIndex(zoom, x, y uint) bool + GridSize(zoom uint) (x, y uint) + MaxXY(zoom uint) (maxx, maxy uint) + Lat2YIndex(zoom uint, lat float64) (gridy uint) + Lon2XIndex(zoom uint, lon float64) (gridx uint) + XIndex2Lon(zoom, x uint) (lon float64) + YIndex2Lat(zoom, y uint) (lat float64) +} + +func GetGrid(srid uint) TileGrid { + switch srid { + case 4326: + return &grid{tileExtentRatio: 2, srid: srid} + case 3857: + return &grid{tileExtentRatio: 1, srid: srid} + default: + panic(fmt.Sprintf("unsupported srid: %v", srid)) + } +} + +type grid struct { + tileExtentRatio float64 + srid uint +} + +func (g *grid) ContainsIndex(zoom, x, y uint) bool { + xsize, ysize := g.GridSize(zoom) + if x < xsize && y < ysize { + return true + } + + return false +} + +func (g *grid) GridSize(zoom uint) (x, y uint) { + dim := uint(math.Exp2(float64(zoom))) + return uint(float64(dim) * g.tileExtentRatio), dim +} + +func (g *grid) MaxXY(zoom uint) (maxx, maxy uint) { + xsize, ysize := g.GridSize(zoom) + + return xsize - 1, ysize - 1 +} + +func (g *grid) Lat2YIndex(zoom uint, lat float64) (gridy uint) { + switch g.srid { + case 3857: + latRad := lat * math.Pi / 180 + return uint(math.Exp2(float64(zoom))* + (1.0-math.Log( + math.Tan(latRad)+ + (1/math.Cos(latRad)))/math.Pi)) / + 2.0 + case 4326: + return uint(math.Exp2(float64(zoom)) * -(lat - 90.0) / (360.0 / g.tileExtentRatio)) + default: + panic(fmt.Sprintf("unsupported srid: %v", g.srid)) + } +} + +func (g *grid) Lon2XIndex(zoom uint, lon float64) (gridx uint) { + switch g.srid { + case 3857: + fallthrough + case 4326: + return uint(math.Exp2(float64(zoom)) * (lon + 180.0) / (360.0 / g.tileExtentRatio)) + default: + panic(fmt.Sprintf("unsupported srid: %v", g.srid)) + } +} + +func (g *grid) XIndex2Lon(zoom, x uint) (lon float64) { + switch g.srid { + case 3857: + fallthrough + case 4326: + return float64(x)/math.Exp2(float64(zoom))*(360.0/g.tileExtentRatio) - 180.0 + default: + panic(fmt.Sprintf("unsupported srid: %v", g.srid)) + } +} + +func (g *grid) YIndex2Lat(zoom, y uint) (lat float64) { + switch g.srid { + case 3857: + var n = math.Pi + if y != 0 { + n = math.Pi - 2.0*math.Pi*float64(y)/math.Exp2(float64(zoom)) + } + + return 180.0 / math.Pi * math.Atan(0.5*(math.Exp(n)-math.Exp(-n))) + case 4326: + return -(180.0/math.Exp2(float64(zoom))*float64(y) - 90.0) + default: + panic(fmt.Sprintf("unsupported srid: %v", g.srid)) + } +} diff --git a/slippy/tile_grid_test.go b/slippy/tile_grid_test.go new file mode 100644 index 00000000..72dfad93 --- /dev/null +++ b/slippy/tile_grid_test.go @@ -0,0 +1,439 @@ +package slippy + +import ( + "fmt" + "testing" +) + +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 := GetGrid(tc.srid) + sizex, sizey := grid.GridSize(tc.zoom) + if sizex != tc.expectedSizeX { + t.Errorf("testcase (%v) failed. output (%v) does not match expected (%v)", t.Name(), sizex, tc.expectedSizeX) + } + if sizey != tc.expectedSizeY { + t.Errorf("testcase (%v) failed. output (%v) does not match expected (%v)", t.Name(), sizey, 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) { + grid := GetGrid(tc.srid) + + return func(t *testing.T) { + output := grid.ContainsIndex(tc.zoom, tc.x, tc.y) + + if output != tc.expected { + t.Errorf("testcase (%v) failed. output (%v) does not match expected (%v)", t.Name(), output, 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: 1, + 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: 1, + y: 0, + expected: true, + }, + "4326_zoom0_fail": { + srid: 4326, + zoom: 0, + x: 0, + y: 1, + 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 TestLat2YIndex(t *testing.T) { + type tcase struct { + lat float64 + srid uint + zoom uint + expected uint + } + + fn := func(tc tcase) func(t *testing.T) { + grid := GetGrid(tc.srid) + return func(t *testing.T) { + output := grid.Lat2YIndex(tc.zoom, tc.lat) + if output != tc.expected { + t.Errorf("testcase (%v) failed. output (%v) does not match expected (%v)", t.Name(), output, tc.expected) + } + } + } + + tests := map[string]tcase{ + "3857_0": { + lat: 0.0, + srid: 3857, + zoom: 0, + expected: 0, + }, + "3857_south": { + lat: -85.0511, + srid: 3857, + zoom: 0, + expected: 0, + }, + "3857_north": { + lat: 85.0511, + srid: 3857, + zoom: 0, + expected: 0, + }, + "3857_z10_north": { + lat: 85.0511, + srid: 3857, + zoom: 10, + expected: 0, + }, + "3857_z10_south": { + lat: -85.0511, + srid: 3857, + zoom: 10, + expected: 1023, + }, + "4326_0": { + lat: 0.0, + srid: 4326, + zoom: 0, + expected: 0, + }, + "4326_south": { + lat: -89.99999, + srid: 4326, + zoom: 0, + expected: 0, + }, + "4326_north": { + lat: 89.99999, + srid: 4326, + zoom: 0, + expected: 0, + }, + "4326_z10_north": { + lat: 89.99999, + srid: 4326, + zoom: 10, + expected: 0, + }, + "4326_z10_south": { + lat: -89.99999, + srid: 4326, + zoom: 10, + expected: 1023, + }, + } + + for name, tc := range tests { + t.Run(name, fn(tc)) + } +} + +func TestLon2XIndex(t *testing.T) { + type tcase struct { + lon float64 + srid uint + zoom uint + expected uint + } + + fn := func(tc tcase) func(t *testing.T) { + grid := GetGrid(tc.srid) + return func(t *testing.T) { + output := grid.Lon2XIndex(tc.zoom, tc.lon) + if output != tc.expected { + t.Errorf("testcase (%v) failed. output (%v) does not match expected (%v)", t.Name(), output, tc.expected) + } + } + } + + tests := map[string]tcase{ + "3857_0": { + lon: 0.0, + srid: 3857, + zoom: 0, + expected: 0, + }, + "3857_west": { + lon: -179.99999, + srid: 3857, + zoom: 0, + expected: 0, + }, + "3857_east": { + lon: 179.99999, + srid: 3857, + zoom: 0, + expected: 0, + }, + "3857_z10_west": { + lon: -179.99999, + srid: 3857, + zoom: 10, + expected: 0, + }, + "3857_z10_east": { + lon: 179.99999, + srid: 3857, + zoom: 10, + expected: 1023, + }, + "4326_0": { + lon: 0.0, + srid: 4326, + zoom: 0, + expected: 1, + }, + "4326_west": { + lon: -179.99999, + srid: 4326, + zoom: 0, + expected: 0, + }, + "4326_east": { + lon: 179.99999, + srid: 4326, + zoom: 0, + expected: 1, + }, + "4326_z10_west": { + lon: -179.99999, + srid: 4326, + zoom: 10, + expected: 0, + }, + "4326_z10_east": { + lon: 179.99999, + srid: 4326, + zoom: 10, + expected: 2047, + }, + } + + for name, tc := range tests { + t.Run(name, fn(tc)) + } +} + +func TestXIndex2Lon(t *testing.T) { + type tcase struct { + x uint + srid uint + zoom uint + expected float64 + } + + fn := func(tc tcase) func(t *testing.T) { + grid := GetGrid(tc.srid) + return func(t *testing.T) { + output := grid.XIndex2Lon(tc.zoom, tc.x) + if output != tc.expected { + t.Errorf("testcase (%v) failed. output (%v) does not match expected (%v)", t.Name(), output, tc.expected) + } + } + } + + tests := map[string]tcase{ + "3857_z0_west": { + x: 0, + srid: 3857, + zoom: 0, + expected: -180, + }, + "3857_z10_west": { + x: 0, + srid: 3857, + zoom: 10, + expected: -180, + }, + "3857_z10_east": { + x: 1023, + srid: 3857, + zoom: 10, + expected: 179.6484375, + }, + "4326_z0_west": { + x: 0, + srid: 4326, + zoom: 0, + expected: -180, + }, + "4326_z0_east": { + x: 1, + srid: 4326, + zoom: 0, + expected: 0, + }, + "4326_z10_west": { + x: 0, + srid: 4326, + zoom: 10, + expected: -180.0, + }, + "4326_z10_east": { + x: 2047, + srid: 4326, + zoom: 10, + expected: (179.6484375 + 180.0) / 2.0, + }, + "4326_z10_center": { + x: 1024, + srid: 4326, + zoom: 10, + expected: 0.0, + }, + } + + for name, tc := range tests { + t.Run(name, fn(tc)) + } +} + +func TestYIndex2Lat(t *testing.T) { + type tcase struct { + y uint + srid uint + zoom uint + expected float64 + } + + fn := func(tc tcase) func(t *testing.T) { + grid := GetGrid(tc.srid) + return func(t *testing.T) { + output := grid.YIndex2Lat(tc.zoom, tc.y) + outs := fmt.Sprintf("%.8f", output) + if outs != fmt.Sprintf("%.8f", tc.expected) { + t.Errorf("testcase (%v) failed. output (%v) does not match expected (%v) close enough", t.Name(), output, tc.expected) + } + } + } + + tests := map[string]tcase{ + "3857_z0_north": { + y: 0, + srid: 3857, + zoom: 0, + expected: 85.05112878, + }, + "3857_z10_north": { + y: 0, + srid: 3857, + zoom: 10, + expected: 85.05112878, + }, + "3857_z10_south": { + y: 1023, + srid: 3857, + zoom: 10, + expected: -85.02070774, + }, + "4326_z0_north": { + y: 0, + srid: 4326, + zoom: 0, + expected: 90, + }, + "4326_z10_north": { + y: 0, + srid: 4326, + zoom: 10, + expected: 90, + }, + "4326_z10_south": { + y: 1023, + srid: 4326, + zoom: 10, + expected: -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 df51bf6a..9563f10e 100644 --- a/slippy/tile_test.go +++ b/slippy/tile_test.go @@ -5,6 +5,8 @@ import ( "strconv" "testing" + "github.com/go-spatial/proj" + "github.com/go-spatial/geom/spherical" "reflect" @@ -18,7 +20,7 @@ func TestNewTile(t *testing.T) { type tcase struct { z, x, y uint buffer float64 - srid uint64 + srid uint eBounds *geom.Extent eExtent *geom.Extent eBExtent *geom.Extent @@ -41,7 +43,7 @@ func TestNewTile(t *testing.T) { } } { - bounds := tile.Extent4326() + bounds := tile.Extent4326(tc.srid) bitTolerence2 := int64(math.Float64bits(1.01) - math.Float64bits(1.00)) for i := 0; i < 4; i++ { if !cmp.Float64(bounds[i], tc.eBounds[i], 0.01, bitTolerence2) { @@ -51,14 +53,14 @@ func TestNewTile(t *testing.T) { } } { - bufferedExtent := tile.Extent3857().ExpandBy(slippy.Pixels2Webs(tile.Z, uint(tc.buffer))) + bufferedExtent := tile.NativeExtent(tc.srid).ExpandBy(slippy.PixelsToProjectedUnits(tile.Z, uint(tc.buffer), tc.srid)) if !cmp.GeomExtent(tc.eBExtent, bufferedExtent) { t.Errorf("buffered extent, expected %v got %v", tc.eBExtent, bufferedExtent) } } { - extent := tile.Extent3857() + extent := tile.NativeExtent(tc.srid) if !cmp.GeomExtent(tc.eExtent, extent) { t.Errorf("extent, expected %v got %v", tc.eExtent, extent) @@ -73,6 +75,7 @@ func TestNewTile(t *testing.T) { x: 1, y: 1, buffer: 64, + srid: proj.WebMercator, eExtent: &geom.Extent{ -10018754.17, 0, 0, 10018754.17, @@ -91,6 +94,7 @@ func TestNewTile(t *testing.T) { x: 11436, y: 26461, buffer: 64, + srid: proj.WebMercator, eExtent: &geom.Extent{ -13044437.497219238996, 3856095.202393799, -13043826.000993041, 3856706.6986199953, @@ -104,6 +108,25 @@ func TestNewTile(t *testing.T) { [2]float64{-117.17, 32.70}, ), }, + { + z: 0, + x: 0, + y: 0, + buffer: 64, + srid: 4326, + eExtent: &geom.Extent{ + -180, -90, + 0, 90, + }, + eBExtent: &geom.Extent{ + -182.8125, -92.8125, + 2.8125, 92.8125, + }, + eBounds: spherical.Hull( + [2]float64{-180, -90}, + [2]float64{0, 90}, + ), + }, } for i, tc := range tests { t.Run(strconv.FormatUint(uint64(i), 10), fn(tc)) @@ -116,13 +139,13 @@ func TestNewTileLatLon(t *testing.T) { z, x, y uint lat, lon float64 buffer float64 - srid uint64 + srid uint } fn := func(tc tcase) func(t *testing.T) { return func(t *testing.T) { // Test the new functions. - tile := slippy.NewTileLatLon(tc.z, tc.lat, tc.lon) + tile := slippy.NewTileLatLon(tc.z, tc.lat, tc.lon, tc.srid) { gz, gx, gy := tile.ZXY() if gz != tc.z { @@ -146,6 +169,7 @@ func TestNewTileLatLon(t *testing.T) { lat: 0, lon: 0, buffer: 64, + srid: 3857, }, "center": { z: 8, @@ -154,6 +178,7 @@ func TestNewTileLatLon(t *testing.T) { lat: 0, lon: 0, buffer: 64, + srid: 3857, }, "arbitrary zoom 2": { z: 2, @@ -162,6 +187,7 @@ func TestNewTileLatLon(t *testing.T) { lat: -70, lon: 20, buffer: 64, + srid: 3857, }, "arbitrary zoom 16": { z: 16, @@ -170,6 +196,7 @@ func TestNewTileLatLon(t *testing.T) { lat: 32.705, lon: -117.176, buffer: 64, + srid: 3857, }, } @@ -185,6 +212,7 @@ func TestRangeFamilyAt(t *testing.T) { type tcase struct { tile *slippy.Tile + tileSRID uint zoomAt uint expected []coord } @@ -203,7 +231,7 @@ func TestRangeFamilyAt(t *testing.T) { return func(t *testing.T) { coordList := make([]coord, 0, len(tc.expected)) - tc.tile.RangeFamilyAt(tc.zoomAt, func(tile *slippy.Tile) error { + tc.tile.RangeFamilyAt(tc.zoomAt, tc.tileSRID, func(tile *slippy.Tile, srid uint) error { z, x, y := tile.ZXY() c := coord{z, x, y} @@ -228,8 +256,9 @@ func TestRangeFamilyAt(t *testing.T) { testcases := map[string]tcase{ "children 1": { - tile: slippy.NewTile(0, 0, 0), - zoomAt: 1, + tile: slippy.NewTile(0, 0, 0), + tileSRID: proj.WebMercator, + zoomAt: 1, expected: []coord{ {1, 0, 0}, {1, 0, 1}, @@ -238,8 +267,9 @@ func TestRangeFamilyAt(t *testing.T) { }, }, "children 2": { - tile: slippy.NewTile(8, 3, 5), - zoomAt: 10, + tile: slippy.NewTile(8, 3, 5), + tileSRID: proj.WebMercator, + zoomAt: 10, expected: []coord{ {10, 12, 20}, {10, 12, 21}, @@ -263,19 +293,48 @@ func TestRangeFamilyAt(t *testing.T) { }, }, "parent 1": { - tile: slippy.NewTile(1, 0, 0), - zoomAt: 0, + tile: slippy.NewTile(1, 0, 0), + tileSRID: proj.WebMercator, + zoomAt: 0, expected: []coord{ {0, 0, 0}, }, }, "parent 2": { - tile: slippy.NewTile(3, 3, 5), - zoomAt: 1, + tile: slippy.NewTile(3, 3, 5), + tileSRID: proj.WebMercator, + zoomAt: 1, expected: []coord{ {1, 0, 1}, }, }, + "parent 4326 1": { + tile: slippy.NewTile(1, 3, 0), + tileSRID: 4326, + zoomAt: 0, + expected: []coord{ + {0, 1, 0}, + }, + }, + "parent 4326 2": { + tile: slippy.NewTile(4, 31, 15), + tileSRID: 4326, + zoomAt: 1, + expected: []coord{ + {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}, + }, + }, } for name, tc := range testcases { @@ -285,14 +344,15 @@ func TestRangeFamilyAt(t *testing.T) { func TestNewTileMinMaxer(t *testing.T) { type tcase struct { - mm geom.MinMaxer - tile *slippy.Tile + mm geom.MinMaxer + tile *slippy.Tile + tileSRID uint } fn := func(tc tcase) func(t *testing.T) { return func(t *testing.T) { - tile := slippy.NewTileMinMaxer(tc.mm) + tile := slippy.NewTileMinMaxer(tc.mm, tc.tileSRID) if !reflect.DeepEqual(tile, tc.tile) { t.Errorf("tile, expected %v, got %v", tc.tile, tile) } @@ -305,11 +365,13 @@ func TestNewTileMinMaxer(t *testing.T) { mm: spherical.Hull( [2]float64{-179.0, 85.0}, [2]float64{179.0, -85.0}), - tile: slippy.NewTile(0, 0, 0), + tile: slippy.NewTile(0, 0, 0), + tileSRID: proj.WebMercator, }, "2": { - mm: slippy.NewTile(15, 2, 98).Extent4326(), - tile: slippy.NewTile(15, 2, 98), + mm: slippy.NewTile(15, 2, 98).Extent4326(3857), + tile: slippy.NewTile(15, 2, 98), + tileSRID: proj.WebMercator, }, } @@ -321,15 +383,16 @@ func TestNewTileMinMaxer(t *testing.T) { func TestFromBounds(t *testing.T) { type tcase struct { - Bounds *geom.Extent - Z uint - Tiles []slippy.Tile + 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) + tiles := slippy.FromBounds(tc.Bounds, tc.Z, 3857) if !reflect.DeepEqual(tiles, tc.Tiles) { t.Errorf("tiles, expected %v, got %v", tc.Tiles, tiles) } @@ -339,8 +402,9 @@ func TestFromBounds(t *testing.T) { 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}), + 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}, @@ -392,14 +456,16 @@ func TestFromBounds(t *testing.T) { }, }, "San Diego 11z": tcase{ - Z: 11, - Bounds: spherical.Hull([2]float64{-117.15, 32.6894743}, [2]float64{-116.804, 32.6339}), - Tiles: []slippy.Tile{{Z: 11, X: 357, Y: 827}, {Z: 11, X: 358, Y: 827}, {Z: 11, X: 359, Y: 827}}, + 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}}, }, "San Diego 9z": tcase{ - Z: 9, - Bounds: spherical.Hull([2]float64{-117.15, 32.6894743}, [2]float64{-116.804, 32.6339}), - Tiles: []slippy.Tile{{Z: 9, X: 89, Y: 206}}, + 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}}, }, } for name, tc := range tests {