Skip to content

Commit

Permalink
Added BoundsCenterZoom algo
Browse files Browse the repository at this point in the history
internal package to calculate the bounds center zoom, to use
with the render to support the last bits we need for simplifed
to be on par with the full version.
  • Loading branch information
gdey committed Oct 5, 2018
1 parent 20744d0 commit 1add701
Show file tree
Hide file tree
Showing 2 changed files with 389 additions and 0 deletions.
172 changes: 172 additions & 0 deletions internal/bounds/bounds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
bounds
This is a temporary package to implement BoundsCenterZoom
We are hardcodeing thing here till the geom and proj packages are ready
can support these functions.
*/

package bounds

import (
"math"

"github.com/go-spatial/geom"
)

type matTransform struct {
xscale float64
xtranslate float64
yscale float64
ytranslate float64
}

func (mt *matTransform) Transform(pt [2]float64, scale float64) [2]float64 {

x, y := pt[0], pt[1]
if scale == 0.0 {
scale = 1.0
}
return [2]float64{
scale * ((mt.xscale * x) + mt.xtranslate),
scale * ((mt.yscale * y) + mt.ytranslate),
}
}

func (mt *matTransform) Untransform(pt [2]float64, scale float64) [2]float64 {
x, y := pt[0], pt[1]
if scale == 0.0 {
scale = 1.0
}
return [2]float64{
(x/scale - mt.xtranslate) / mt.xscale,
(y/scale - mt.ytranslate) / mt.yscale,
}
}

var projections = [...]struct {
name string
radius float64
maxLatitude float64
circumferenceRatio float64
tTranslate float64
bounds *geom.Extent
transformer matTransform
}{
{
name: "ESPG3857",
radius: 6378137, // earth radius for ESPG3857
circumferenceRatio: 1 / (2 * math.Pi * 6378137),
maxLatitude: 85.0511287798,
tTranslate: 0.5,
bounds: &geom.Extent{-180.0, -85.06, 180.0, 85.06},
},
}

func init() {
for i := range projections {
prj := projections[i]
projections[i].transformer = matTransform{
xscale: prj.circumferenceRatio,
xtranslate: prj.tTranslate,
yscale: -prj.circumferenceRatio,
ytranslate: prj.tTranslate,
}
}
}

type aProjection int

const (
ESPG3857 = aProjection(0)
)

func (p aProjection) String() string { return projections[int(p)].name }
func (p aProjection) Bounds() *geom.Extent { return projections[int(p)].bounds }
func (p aProjection) R() float64 { return projections[int(p)].radius }
func (p aProjection) MaxLatitude() float64 { return projections[int(p)].maxLatitude }

func (p aProjection) Transform(pt [2]float64, scale float64) [2]float64 {
return projections[int(p)].transformer.Transform(pt, scale)
}
func (p aProjection) Untransform(pt [2]float64, scale float64) [2]float64 {
return projections[int(p)].transformer.Untransform(pt, scale)
}

func (p aProjection) Project(latlng [2]float64) (xy [2]float64) {
lat, lng := latlng[0], latlng[1]
d := math.Pi / 180
max := p.MaxLatitude()
r := p.R()
_lat := math.Max(math.Min(max, lat), -max)
sin := math.Sin(_lat * d)

return [2]float64{r * lng * d, r * math.Log((1+sin)/(1-sin)) / 2}
}

func (p aProjection) Unproject(pt [2]float64) (latlng [2]float64) {
d := 180 / math.Pi
prj := projections[ESPG3857]

return [2]float64{
(2*math.Atan(math.Exp(pt[1]/prj.radius)) - (math.Pi / 2)) * d,
pt[0] * d / prj.radius,
}
}

// Zoom returns the zoom level for supplied bounds
// useful when rendering static map images
// tile size is assumed to be 256
//
// TODO: add padding support
func Zoom(bounds *geom.Extent, width, height float64) float64 {
// assume ESPG3857 for now.
prj := ESPG3857
if bounds == nil {
// we want the whole world.
bounds = prj.Bounds()
}

// for lat lng geom.Extent should be laid out as follows:
// {west, south, east, north}
nw := [2]float64{bounds[3], bounds[0]}
se := [2]float64{bounds[1], bounds[2]}

// 256 is the tile size.
ptupper := prj.Transform(prj.Project(nw), 256)
ptlower := prj.Transform(prj.Project(se), 256)

b := geom.NewExtent(ptupper, ptlower)
scale := math.Min(width/b.XSpan(), height/b.YSpan())
return math.Floor(math.Log(scale) / math.Ln2)
}

func CenterZoom(bounds *geom.Extent, width, height float64) ([2]float64, float64) {
// assume ESPG3857 for now.
prj := ESPG3857
if bounds == nil {
// we want the whole world.
bounds = prj.Bounds()
}

// calculate our zoom
zoom := Zoom(bounds, width, height)

// for lat lng geom.Extent should be laid out as follows:
// {west, south, east, north}
sw := [2]float64{bounds[1], bounds[0]}
ne := [2]float64{bounds[3], bounds[2]}

// 256 is the tile size.
swPt := prj.Transform(prj.Project(sw), 256)
nePt := prj.Transform(prj.Project(ne), 256)

// center point.
centerPtX := (swPt[0] + nePt[0]) / 2
centerPtY := (swPt[1] + nePt[1]) / 2

// 256 is the tile size.
center := prj.Unproject(prj.Untransform([2]float64{centerPtX, centerPtY}, 256))

return center, zoom
}
217 changes: 217 additions & 0 deletions internal/bounds/bounds_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package bounds

import (
"strconv"
"testing"

"github.com/go-spatial/geom"
"github.com/go-spatial/geom/cmp"
)

func TestZoom(t *testing.T) {

type tcase struct {
bounds *geom.Extent
width float64
height float64
zoom float64
}

fn := func(tc tcase) func(t *testing.T) {
return func(t *testing.T) {
zoom := Zoom(tc.bounds, tc.width, tc.height)
if !cmp.Float(tc.zoom, zoom) {
t.Errorf("zoom, expected %v got %v", tc.zoom, zoom)
}
}
}

tests := [...]tcase{
{
// {west, south, east, north}
bounds: &geom.Extent{
-117.1673735976219, // west
32.71965828903011, // south
-117.16439634561537, // east
32.7204706651118, // north
},
width: 862,
height: 300,
zoom: 18.0,
},
}

for i := range tests {
t.Run(strconv.Itoa(i), fn(tests[i]))
}

}

func TestCenterZoom(t *testing.T) {
type tcase struct {
bounds *geom.Extent
width float64
height float64
zoom float64
center [2]float64
}

fn := func(tc tcase) func(t *testing.T) {
return func(t *testing.T) {

center, zoom := CenterZoom(tc.bounds, tc.width, tc.height)
if !(cmp.Float(tc.center[0], center[0]) && cmp.Float(tc.center[0], center[0])) {
t.Errorf("center, expected %v got %v", tc.center, center)
return
}

if !cmp.Float(tc.zoom, zoom) {
t.Errorf("zoom, expected %v got %v", tc.zoom, zoom)
return
}

}
}

tests := [...]tcase{
{
// {west, south, east, north}
bounds: &geom.Extent{
-117.147086641189, // west
32.7305263087481, // south
-117.180183060805, // east
32.6963180459813, // north
},
width: 1107,
height: 360,
zoom: 13.0,
center: [2]float64{32.71342381720108, -117.163634850997},
},
{
// {west, south, east, north}
bounds: &geom.Extent{
-117.1673735976219, // west
32.71965828903011, // south
-117.16439634561537, // east
32.7204706651118, // north
},
width: 1107,
height: 360,
zoom: 18.0,
center: [2]float64{32.720064477996, -117.16588497161865},
},
}

for i := range tests {
t.Run(strconv.Itoa(i), fn(tests[i]))
}

}

func TestTransform(t *testing.T) {
type subcase struct {
point [2]float64
scale float64
pt [2]float64
}

type tcase struct {
prj aProjection
cases []subcase
}

fn := func(tc tcase) func(t *testing.T) {
return func(t *testing.T) {

fn := func(prj aProjection, tc subcase) func(t *testing.T) {
return func(t *testing.T) {
t.Run("transform", func(t *testing.T) {

pt := prj.Transform(tc.point, tc.scale)
if !(cmp.Float(pt[0], tc.pt[0]) && cmp.Float(pt[1], tc.pt[1])) {
t.Errorf(" %v Transform, expected %v got %v", prj, tc.pt, pt)
t.Logf("%v %v ", cmp.Float(pt[0], tc.pt[0]), cmp.Float(pt[1], tc.pt[1]))
}
})
t.Run("untransform", func(t *testing.T) {

point := prj.Untransform(tc.pt, tc.scale)
if !(cmp.Float(point[0], tc.point[0]) && cmp.Float(point[1], tc.point[1])) {
t.Errorf(" %v Transform, expected %v got %v", prj, tc.point, point)
t.Logf("%v %v ", cmp.Float(point[0], tc.point[0]), cmp.Float(point[1], tc.point[1]))
}
})

}
}
for i := range tc.cases {
t.Run(strconv.Itoa(i), fn(tc.prj, tc.cases[i]))
}
}
}

tests := [...]tcase{
{
prj: ESPG3857,
cases: []subcase{
{
point: [2]float64{44.68203449249269, 103.35370445251465},
scale: 2.0,
pt: [2]float64{1.0000022299196951, 0.9999948419882011},
},
},
},
}

for _, tc := range tests {
t.Run(tc.prj.String(), fn(tc))
}

}

func TestProject(t *testing.T) {
type subcase struct {
point [2]float64
pt [2]float64
}

type tcase struct {
prj aProjection
cases []subcase
}

fn := func(tc tcase) func(t *testing.T) {
return func(t *testing.T) {

fn := func(prj aProjection, tc subcase) func(t *testing.T) {
return func(t *testing.T) {

pt := prj.Project(tc.point)
if !(cmp.Float(pt[0], tc.pt[0]) && cmp.Float(pt[1], tc.pt[1])) {
t.Errorf(" %v Project, expected %v got %v", prj, tc.pt, pt)
t.Logf("%v %v ", cmp.Float(pt[0], tc.pt[0]), cmp.Float(pt[1], tc.pt[1]))
}

}
}
for i := range tc.cases {
t.Run(strconv.Itoa(i), fn(tc.prj, tc.cases[i]))
}
}
}
tests := [...]tcase{
{
prj: ESPG3857,
cases: []subcase{
{
point: [2]float64{32.7305263087481, -117.180183060805},
pt: [2]float64{-13044438.309391394, 3859590.2188198487},
},
},
},
}
for _, tc := range tests {
t.Run(tc.prj.String(), fn(tc))
}

}

0 comments on commit 1add701

Please sign in to comment.