diff --git a/encoding/mvt/README.md b/encoding/mvt/README.md new file mode 100644 index 00000000..b426588a --- /dev/null +++ b/encoding/mvt/README.md @@ -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) + diff --git a/encoding/mvt/mvt.go b/encoding/mvt/mvt.go index ed64ac6f..be0aec4e 100644 --- a/encoding/mvt/mvt.go +++ b/encoding/mvt/mvt.go @@ -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 ( diff --git a/encoding/mvt/prepare.go b/encoding/mvt/prepare.go index 0053728a..ce778aaa 100644 --- a/encoding/mvt/prepare.go +++ b/encoding/mvt/prepare.go @@ -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() @@ -21,18 +24,18 @@ 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) } @@ -40,12 +43,12 @@ func PrepareGeo(geo tegola.Geometry, tile *tegola.Tile) geom.Geometry { 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) } @@ -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)) @@ -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. diff --git a/encoding/mvt/prepare_internal_test.go b/encoding/mvt/prepare_internal_test.go index 2222e465..85fcac6e 100644 --- a/encoding/mvt/prepare_internal_test.go +++ b/encoding/mvt/prepare_internal_test.go @@ -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}, }, }