-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
2 changed files
with
389 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
|
||
} |