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
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,31 +140,32 @@ data, err := layers.Marshal() // this data is NOT gzipped.
data, err := layers.MarshalGzipped()
```

## Decoding WKB from a database query
## Decoding WKB/EWKB from a database query

Geometries are usually returned from databases in WKB format. The [encoding/wkb](encoding/wkb)
Geometries are usually returned from databases in WKB or EWKB format. The [encoding/ewkb](encoding/ewkb)
sub-package offers helpers to "scan" the data into the base types directly.
For example:

```go
db.Exec(
"INSERT INTO postgis_table (point_column) VALUES (ST_GeomFromEWKB(?))",
ewkb.Value(orb.Point{1, 2}, 4326),
)

row := db.QueryRow("SELECT ST_AsBinary(point_column) FROM postgis_table")

var p orb.Point
err := row.Scan(wkb.Scanner(&p))

db.Exec("INSERT INTO table (point_column) VALUES (ST_GeomFromWKB(?))", wkb.Value(p))
err := row.Scan(ewkb.Scanner(&p))
```

Scanning directly from MySQL columns is supported. By default MySQL returns geometry
data as WKB but prefixed with a 4 byte SRID. To support this, if the data is not
valid WKB, the code will strip the first 4 bytes, the SRID, and try again.
This works for most use cases.
For more information see the readme in the [encoding/ewkb](encoding/ewkb) package.

## List of sub-package utilities

- [`clip`](clip) - clipping geometry to a bounding box
- [`encoding/mvt`](encoding/mvt) - encoded and decoding from [Mapbox Vector Tiles](https://www.mapbox.com/vector-tiles/)
- [`encoding/wkb`](encoding/wkb) - well-known binary as well as helpers to decode from the database queries
- [`encoding/ewkb`](encoding/ewkb) - extended well-known binary format that includes the SRID
- [`encoding/wkt`](encoding/wkt) - well-known text encoding
- [`geojson`](geojson) - working with geojson and the types in this package
- [`maptile`](maptile) - working with mercator map tiles
Expand Down
107 changes: 107 additions & 0 deletions encoding/ewkb/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# encoding/ewkb [![Godoc Reference](https://pkg.go.dev/badge/github.com/paulmach/orb)](https://pkg.go.dev/github.com/paulmach/orb/encoding/ewkb)

This package provides encoding and decoding of [extended WKB](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry#Format_variations)
data. This format includes the [SRID](https://en.wikipedia.org/wiki/Spatial_reference_system) in the data.
If the SRID is not needed use the [wkb](../wkb) package for a simpler interface.
The interface is defined as:

```go
func Marshal(geom orb.Geometry, srid int, byteOrder ...binary.ByteOrder) ([]byte, error)
func MarshalToHex(geom orb.Geometry, srid int, byteOrder ...binary.ByteOrder) (string, error)
func MustMarshal(geom orb.Geometry, srid int, byteOrder ...binary.ByteOrder) []byte
func MustMarshalToHex(geom orb.Geometry, srid int, byteOrder ...binary.ByteOrder) string

func NewEncoder(w io.Writer) *Encoder
func (e *Encoder) SetByteOrder(bo binary.ByteOrder) *Encoder
func (e *Encoder) SetSRID(srid int) *Encoder
func (e *Encoder) Encode(geom orb.Geometry) error

func Unmarshal(b []byte) (orb.Geometry, int, error)

func NewDecoder(r io.Reader) *Decoder
func (d *Decoder) Decode() (orb.Geometry, int, error)
```

## Inserting geometry into a database

Depending on the database different formats and functions are supported.

### PostgreSQL and PostGIS

PostGIS stores geometry as EWKB internally. As a result it can be inserted without
a wrapper function.

```go
db.Exec("INSERT INTO geodata(geom) VALUES (ST_GeomFromEWKB($1))", ewkb.Value(coord, 4326))

db.Exec("INSERT INTO geodata(geom) VALUES ($1)", ewkb.Value(coord, 4326))
```

### MySQL/MariaDB

MySQL and MariaDB
[store geometry](https://dev.mysql.com/doc/refman/5.7/en/gis-data-formats.html)
data in WKB format with a 4 byte SRID prefix.

```go
coord := orb.Point{1, 2}

// as WKB in hex format
data := wkb.MustMarshalToHex(coord)
db.Exec("INSERT INTO geodata(geom) VALUES (ST_GeomFromWKB(UNHEX(?), 4326))", data)

// relying on the raw encoding
db.Exec("INSERT INTO geodata(geom) VALUES (?)", ewkb.ValuePrefixSRID(coord, 4326))
```

## Reading geometry from a database query

As stated above, different databases supported different formats and functions.

### PostgreSQL and PostGIS

When working with PostGIS the raw format is EWKB so the wrapper function is not necessary

```go
// both of these queries return the same data
row := db.QueryRow("SELECT ST_AsEWKB(geom) FROM geodata")
row := db.QueryRow("SELECT geom FROM geodata")

// if you don't need the SRID
p := orb.Point{}
err := row.Scan(ewkb.Scanner(&p))
log.Printf("geom: %v", p)

// if you need the SRID
p := orb.Point{}
gs := ewkb.Scanner(&p)
err := row.Scan(gs)

log.Printf("srid: %v", gs.SRID)
log.Printf("geom: %v", gs.Geometry)
log.Printf("also geom: %v", p)
```

### MySQL/MariaDB

```go
// using the ST_AsBinary function
row := db.QueryRow("SELECT st_srid(geom), ST_AsBinary(geom) FROM geodata")
row.Scan(&srid, ewkb.Scanner(&data))

// relying on the raw encoding
row := db.QueryRow("SELECT geom FROM geodata")

// if you don't need the SRID
p := orb.Point{}
err := row.Scan(ewkb.ScannerPrefixSRID(&p))
log.Printf("geom: %v", p)

// if you need the SRID
p := orb.Point{}
gs := ewkb.ScannerPrefixSRID(&p)
err := row.Scan(gs)

log.Printf("srid: %v", gs.SRID)
log.Printf("geom: %v", gs.Geometry)
```
64 changes: 64 additions & 0 deletions encoding/ewkb/collection_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package ewkb

import (
"testing"

"github.com/paulmach/orb"
"github.com/paulmach/orb/encoding/internal/wkbcommon"
)

func TestCollection(t *testing.T) {
large := orb.Collection{}
for i := 0; i < wkbcommon.MaxMultiAlloc+100; i++ {
large = append(large, orb.Point{float64(i), float64(-i)})
}

cases := []struct {
name string
srid int
data []byte
expected orb.Collection
}{
{
name: "large",
srid: 123,
data: MustMarshal(large, 123),
expected: large,
},
{
name: "collection with point",
data: MustDecodeHex("0107000020e6100000010000000101000000000000000000f03f0000000000000040"),
srid: 4326,
expected: orb.Collection{orb.Point{1, 2}},
},
{
name: "collection with point and line",
data: MustDecodeHex("0020000007000010e60000000200000000013ff000000000000040000000000000000000000002000000023ff0000000000000400000000000000040080000000000004010000000000000"),
srid: 4326,
expected: orb.Collection{
orb.Point{1, 2},
orb.LineString{{1, 2}, {3, 4}},
},
},
{
name: "collection with point and line and polygon",
data: MustDecodeHex("0107000020e6100000030000000101000000000000000000f03f0000000000000040010200000002000000000000000000f03f00000000000000400000000000000840000000000000104001030000000300000004000000000000000000f03f00000000000000400000000000000840000000000000104000000000000014400000000000001840000000000000f03f000000000000004004000000000000000000264000000000000028400000000000002a400000000000002c400000000000002e4000000000000030400000000000002640000000000000284004000000000000000000354000000000000036400000000000003740000000000000384000000000000039400000000000003a4000000000000035400000000000003640"),
srid: 4326,
expected: orb.Collection{
orb.Point{1, 2},
orb.LineString{{1, 2}, {3, 4}},
orb.Polygon{
{{1, 2}, {3, 4}, {5, 6}, {1, 2}},
{{11, 12}, {13, 14}, {15, 16}, {11, 12}},
{{21, 22}, {23, 24}, {25, 26}, {21, 22}},
},
},
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
compare(t, tc.expected, tc.srid, tc.data)
})
}
}
179 changes: 179 additions & 0 deletions encoding/ewkb/ewkb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package ewkb

import (
"bytes"
"encoding/binary"
"encoding/hex"
"errors"
"io"

"github.com/paulmach/orb"
"github.com/paulmach/orb/encoding/internal/wkbcommon"
)

var (
// ErrUnsupportedDataType is returned by Scan methods when asked to scan
// non []byte data from the database. This should never happen
// if the driver is acting appropriately.
ErrUnsupportedDataType = errors.New("wkb: scan value must be []byte")

// ErrNotEWKB is returned when unmarshalling EWKB and the data is not valid.
ErrNotEWKB = errors.New("wkb: invalid data")

// ErrIncorrectGeometry is returned when unmarshalling EWKB data into the wrong type.
// For example, unmarshaling linestring data into a point.
ErrIncorrectGeometry = errors.New("wkb: incorrect geometry")

// ErrUnsupportedGeometry is returned when geometry type is not supported by this lib.
ErrUnsupportedGeometry = errors.New("wkb: unsupported geometry")
)

var commonErrorMap = map[error]error{
wkbcommon.ErrUnsupportedDataType: ErrUnsupportedDataType,
wkbcommon.ErrNotWKB: ErrNotEWKB,
wkbcommon.ErrNotWKBHeader: ErrNotEWKB,
wkbcommon.ErrIncorrectGeometry: ErrIncorrectGeometry,
wkbcommon.ErrUnsupportedGeometry: ErrUnsupportedGeometry,
}

func mapCommonError(err error) error {
e, ok := commonErrorMap[err]
if ok {
return e
}

return err
}

// DefaultByteOrder is the order used for marshalling or encoding is none is specified.
var DefaultByteOrder binary.ByteOrder = binary.LittleEndian

// DefaultSRID is set to 4326, a common SRID, which represents spatial data using
// longitude and latitude coordinates on the Earth's surface as defined in the WGS84 standard,
// which is also used for the Global Positioning System (GPS).
// This will be used by the encoder if non is specified.
var DefaultSRID int = 4326

// An Encoder will encode a geometry as EWKB to the writer given at creation time.
type Encoder struct {
srid int
e *wkbcommon.Encoder
}

// MustMarshal will encode the geometry and panic on error.
// Currently there is no reason to error during geometry marshalling.
func MustMarshal(geom orb.Geometry, srid int, byteOrder ...binary.ByteOrder) []byte {
d, err := Marshal(geom, srid, byteOrder...)
if err != nil {
panic(err)
}

return d
}

// Marshal encodes the geometry with the given byte order.
// An SRID of 0 will not be included in the encoding and the result will be a wkb encoding of the geometry.
func Marshal(geom orb.Geometry, srid int, byteOrder ...binary.ByteOrder) ([]byte, error) {
buf := bytes.NewBuffer(make([]byte, 0, wkbcommon.GeomLength(geom, srid != 0)))

e := NewEncoder(buf)
e.SetSRID(srid)

if len(byteOrder) > 0 {
e.SetByteOrder(byteOrder[0])
}

err := e.Encode(geom)
if err != nil {
return nil, err
}

if buf.Len() == 0 {
return nil, nil
}

return buf.Bytes(), nil
}

// MarshalToHex will encode the geometry into a hex string representation of the binary ewkb.
func MarshalToHex(geom orb.Geometry, srid int, byteOrder ...binary.ByteOrder) (string, error) {
data, err := Marshal(geom, srid, byteOrder...)
if err != nil {
return "", err
}

return hex.EncodeToString(data), nil
}

// MustMarshalToHex will encode the geometry and panic on error.
// Currently there is no reason to error during geometry marshalling.
func MustMarshalToHex(geom orb.Geometry, srid int, byteOrder ...binary.ByteOrder) string {
d, err := MarshalToHex(geom, srid, byteOrder...)
if err != nil {
panic(err)
}

return d
}

// NewEncoder creates a new Encoder for the given writer.
func NewEncoder(w io.Writer) *Encoder {
e := wkbcommon.NewEncoder(w)
e.SetByteOrder(DefaultByteOrder)
return &Encoder{e: e, srid: DefaultSRID}
}

// SetByteOrder will override the default byte order set when
// the encoder was created.
func (e *Encoder) SetByteOrder(bo binary.ByteOrder) *Encoder {
e.e.SetByteOrder(bo)
return e
}

// SetSRID will override the default srid.
func (e *Encoder) SetSRID(srid int) *Encoder {
e.srid = srid
return e
}

// Encode will write the geometry encoded as EWKB to the given writer.
func (e *Encoder) Encode(geom orb.Geometry, srid ...int) error {
s := e.srid
if len(srid) > 0 {
s = srid[0]
}

return e.e.Encode(geom, s)
}

// Decoder can decoder WKB geometry off of the stream.
type Decoder struct {
d *wkbcommon.Decoder
}

// Unmarshal will decode the type into a Geometry.
func Unmarshal(data []byte) (orb.Geometry, int, error) {
g, srid, err := wkbcommon.Unmarshal(data)
if err != nil {
return nil, 0, mapCommonError(err)
}

return g, srid, nil
}

// NewDecoder will create a new EWKB decoder.
func NewDecoder(r io.Reader) *Decoder {
return &Decoder{
d: wkbcommon.NewDecoder(r),
}
}

// Decode will decode the next geometry off of the stream.
func (d *Decoder) Decode() (orb.Geometry, int, error) {
g, srid, err := d.d.Decode()
if err != nil {
return nil, 0, mapCommonError(err)
}

return g, srid, nil
}
Loading