Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions encoding/mvt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# MVT

[![GoDoc](https://godoc.org/github.com/go-spatial/geom/encoding/mvt?status.svg)](https://godoc.org/github.com/go-spatial/geom/encoding/mvt)

This package contains functions and types for encoding a geometry in
[Mapbox Vector Tiles](https://github.com/mapbox/vector-tile-spec). This
package depends on the [protbuf](https://github.com/golang/protobuf) package.

In short, a `Tile`s has `Layer`s, which have `Feauture`s. The `Feature` type
is what holds a single `geom.Geometry` and associated metadata.

To encode a geometry into a tile, you need:
* a geometry
* a tile's `geom.Extent` in the same projection as the geometry
* the size of the tile you want to output in pixels

**note**: the geometry must not go outside the tile extent. If this is unknown,
use the [clip package](https://godoc.org/github.com/go-spatial/geom/planar/clip#Geometry)
before encoding.

To encode:
1. Call `PrepareGeomtry`, it returns a `geom.Geometry` that is "reprojected"
into pixel values relative to the tile
2. Add the returned geometry to a `Feature`, optionally with an ID and
tags by calling `NewFeatures`.
3. Add the feature to a `Layer` with a name for the layer
by calling `(*Layer).AddFeatures`
4. Add the layer to a `Tile` by calling `(*Tile).AddLayers`
5. Get the `protobuf` tile by calling `(*Tile).VTile`
6. Encode the `protobuf` into bytes with `proto.Marshal`

For an example, check the use of this package in [tegola/atlas/map.go](https://github.com/go-spatial/tegola/blob/master/atlas/map.go)

29 changes: 29 additions & 0 deletions encoding/mvt/mvt.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
/*
Package mvt is used to encode MVT tiles

In short, a `Tile`s has `Layer`s, which have `Feauture`s. The `Feature` type
is what holds a single `geom.Geometry` and associated metadata.

To encode a geometry into a tile, you need:
* a geometry
* a tile's `geom.Extent` in the same projection as the geometry
* the size of the tile you want to output in pixels

note: the geometry must not go outside the tile extent. If this is unknown,
use the clip package before encoding.
(https://godoc.org/github.com/go-spatial/geom/planar/clip#Geometry)


To encode:
1. Call `PrepareGeomtry`, it returns a `geom.Geometry` that is "reprojected"
into pixel values relative to the tile
2. Add the returned geometry to a `Feature`, optionally with an ID and
tags by calling `NewFeatures`.
3. Add the feature to a `Layer` with a name for the layer
by calling `(*Layer).AddFeatures`
4. Add the layer to a `Tile` by calling `(*Tile).AddLayers`
5. Get the `protobuf` tile by calling `(*Tile).VTile`
6. Encode the `protobuf` into bytes with `proto.Marshal`

For an example, check the use of this package in tegola/atlas/map.go (https://github.com/go-spatial/tegola/blob/master/atlas/map.go)
*/
package mvt

const (
Expand Down
51 changes: 24 additions & 27 deletions encoding/mvt/prepare.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ import (
"log"

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

// PrepareGeo converts the geometry's coordinates to tile coordinates
func PrepareGeo(geo tegola.Geometry, tile *tegola.Tile) geom.Geometry {
// PrepareGeo converts the geometry's coordinates to tile coordinates. tile should be the
// extent of the tile, in the same projection as geo. pixelExtent is the dimension of the
// (square) tile in pixels usually 4096, see DefaultExtent.
// The geometry must not go outside the tile extent. If this is unknown,
// use the clip package before encoding.
func PrepareGeo(geo geom.Geometry, tile *geom.Extent, pixelExtent float64) geom.Geometry {
switch g := geo.(type) {
case geom.Point:
return preparept(g, tile)
return preparept(g, tile, pixelExtent)

case geom.MultiPoint:
pts := g.Points()
Expand All @@ -21,31 +24,31 @@ func PrepareGeo(geo tegola.Geometry, tile *tegola.Tile) geom.Geometry {

mp := make(geom.MultiPoint, len(pts))
for i, pt := range g {
mp[i] = preparept(pt, tile)
mp[i] = preparept(pt, tile, pixelExtent)
}

return mp

case geom.LineString:
return preparelinestr(g, tile)
return preparelinestr(g, tile, pixelExtent)

case geom.MultiLineString:
var ml geom.MultiLineString
for _, l := range g.LineStrings() {
nl := preparelinestr(l, tile)
nl := preparelinestr(l, tile, pixelExtent)
if len(nl) > 0 {
ml = append(ml, nl)
}
}
return ml

case geom.Polygon:
return preparePolygon(g, tile)
return preparePolygon(g, tile, pixelExtent)

case geom.MultiPolygon:
var mp geom.MultiPolygon
for _, p := range g.Polygons() {
np := preparePolygon(p, tile)
np := preparePolygon(p, tile, pixelExtent)
if len(np) > 0 {
mp = append(mp, np)
}
Expand All @@ -56,36 +59,30 @@ func PrepareGeo(geo tegola.Geometry, tile *tegola.Tile) geom.Geometry {
return nil
}

func preparept(g geom.Point, tile *tegola.Tile) geom.Point {
pt, err := tile.ToPixel(tegola.WebMercator, g)
if err != nil {
panic(err)
}
return geom.Point(pt)
func preparept(g geom.Point, tile *geom.Extent, pixelExtent float64) geom.Point {
px := (g.X() - tile.MinX()) / tile.XSpan() * pixelExtent
py := (g.Y() - tile.MinY()) / tile.YSpan() * pixelExtent

return geom.Point{px, py}
}

func preparelinestr(g geom.LineString, tile *tegola.Tile) (ls geom.LineString) {
func preparelinestr(g geom.LineString, tile *geom.Extent, pixelExtent float64) (ls geom.LineString) {
pts := g
// If the linestring
if len(pts) < 2 {
// Not enought points to make a line.
return nil
}
ls = make(geom.LineString, 0, len(pts))
ls = append(ls, preparept(pts[0], tile))
for i := 1; i < len(pts); i++ {
npt := preparept(pts[i], tile)
ls = append(ls, npt)
}

if len(ls) < 2 {
// Not enough points. the zoom must be too far out for this ring.
return nil
ls = make(geom.LineString, len(pts))
for i := 0; i < len(pts); i++ {
ls[i] = preparept(pts[i], tile, pixelExtent)
}

return ls
}

func preparePolygon(g geom.Polygon, tile *tegola.Tile) (p geom.Polygon) {
func preparePolygon(g geom.Polygon, tile *geom.Extent, pixelExtent float64) (p geom.Polygon) {
lines := geom.MultiLineString(g.LinearRings())
p = make(geom.Polygon, 0, len(lines))

Expand All @@ -94,7 +91,7 @@ func preparePolygon(g geom.Polygon, tile *tegola.Tile) (p geom.Polygon) {
}

for _, line := range lines.LineStrings() {
ln := preparelinestr(line, tile)
ln := preparelinestr(line, tile, pixelExtent)
if len(ln) < 2 {
if debug {
// skip lines that have been reduced to less then 2 points.
Expand Down
60 changes: 32 additions & 28 deletions encoding/mvt/prepare_internal_test.go
Original file line number Diff line number Diff line change
@@ -1,57 +1,61 @@
package mvt

import (
"fmt"
"reflect"
"testing"

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

func TestPrepareLinestring(t *testing.T) {
tile := tegola.NewTile(20, 0, 0)

newLine := func(ptpairs ...float64) (ln geom.LineString) {
for i, j := 0, 1; j < len(ptpairs); i, j = i+2, j+2 {
pt, err := tile.FromPixel(tegola.WebMercator, [2]float64{ptpairs[i], ptpairs[j]})
if err != nil {
panic(fmt.Sprintf("error trying to convert %v,%v to WebMercator. %v", ptpairs[i], ptpairs[j], err))
}

ln = append(ln, geom.Point(pt))
}

return ln
}

type tcase struct {
g geom.LineString
e geom.LineString
in geom.LineString
out geom.LineString
tile geom.Extent
}

fn := func(tc tcase) func(t *testing.T) {
return func(t *testing.T) {
got := preparelinestr(tc.g, tile)
got := preparelinestr(tc.in, &tc.tile, float64(DefaultExtent))

if len(got) != len(tc.out) {
t.Errorf("expected %v got %v", tc.out, got)
}

if !reflect.DeepEqual(tc.e, got) {
t.Errorf("expected %v got %v", tc.e, got)
for i := range got {
if !cmp.PointEqual(tc.out[i], got[i]) {
t.Errorf("expected (%d) %v got %v", i, tc.out, got)
}
}
}
}

tests := map[string]tcase{
"duplicate pt simple line": {
g: newLine(9.0, 9.0, 9.0, 9.0),
e: geom.LineString{{9.0, 9.0}, {9.0, 9.0}},
in: geom.LineString{{9.0, 9.0}, {9.0, 9.0}},
out: geom.LineString{{9.0, 9.0}, {9.0, 9.0}},
tile: geom.Extent{0.0, 0.0, 4096.0, 4096.0},
},
"simple line": {
g: newLine(10.0, 10.0, 11.0, 11.0),
e: geom.LineString{{9.0, 9.0}, {11.0, 11.0}},
in: geom.LineString{{9.0, 9.0}, {11.0, 11.0}},
out: geom.LineString{{9.0, 9.0}, {11.0, 11.0}},
tile: geom.Extent{0.0, 0.0, 4096.0, 4096.0},
},
"edge line": {
in: geom.LineString{{0.0, 0.0}, {4096.0, 20.0}},
out: geom.LineString{{0.0, 0.0}, {4096.0, 20.0}},
tile: geom.Extent{0.0, 0.0, 4096.0, 4096.0},
},
"simple line 3pt": {
g: newLine(10.0, 10.0, 11.0, 10.0, 11.0, 15.0),
e: geom.LineString{{9.0, 9.0}, {11.0, 9.0}, {11.0, 14.0}},
in: geom.LineString{{9.0, 9.0}, {11.0, 9.0}, {11.0, 14.0}},
out: geom.LineString{{9.0, 9.0}, {11.0, 9.0}, {11.0, 14.0}},
tile: geom.Extent{0.0, 0.0, 4096.0, 4096.0},
},
"scale" : {
in: geom.LineString{{100.0, 100.0}, {300.0, 300.0}},
out: geom.LineString{{1024.0, 1024.0}, {3072.0, 3072.0}},
tile: geom.Extent{0.0, 0.0, 400.0, 400.0},
},
}

Expand Down