├── .gitignore ├── .travis.yml ├── LICENSE.md ├── Makefile ├── README.md ├── spatialite.go ├── spatialite_test.go └── wkb ├── geometry.go ├── geometry_test.go ├── linestring.go ├── linestring_test.go ├── point.go ├── point_test.go ├── polygon.go ├── polygon_test.go ├── primitive.go ├── primitive_test.go └── wkb.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.cov 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | matrix: 4 | fast_finish: true 5 | allow_failures: 6 | - go: tip 7 | 8 | go: 9 | - "1.8" 10 | - "1.9" 11 | - "1.10" 12 | - tip 13 | 14 | addons: 15 | apt: 16 | packages: 17 | - libsqlite3-0 18 | - libspatialite5 19 | 20 | before_install: 21 | - go get -t ./... 22 | - go get github.com/mattn/goveralls 23 | - go get github.com/wadey/gocovmerge 24 | - go get golang.org/x/tools/cmd/cover 25 | 26 | script: 27 | - make cover 28 | 29 | after_success: 30 | - $HOME/gopath/bin/goveralls -coverprofile merged.cov -service=travis-ci 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Zbigniew Mandziejewicz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BUILDTAGS=-tags "libsqlite3" 2 | 3 | test: 4 | go test -v . $(BUILDTAGS) 5 | go test -v ./wkb 6 | 7 | cover: 8 | go test -v . -covermode=count -coverprofile=profile.cov $(BUILDTAGS) 9 | go test -v ./wkb -covermode=count -coverprofile=wkb/profile.cov 10 | gocovmerge profile.cov wkb/profile.cov > merged.cov 11 | 12 | coverhtml: cover 13 | go tool cover -html=merged.cov 14 | 15 | install: 16 | go install $(BUILDTAGS) 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Spatialite 2 | [![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/gopkg.in/shaxbee/go-spatialite.v0) 3 | [![license](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://raw.githubusercontent.com/shaxbee/go-snowflake/master/LICENSE) 4 | [![build](https://travis-ci.org/shaxbee/go-spatialite.svg?branch=master)](https://travis-ci.org/shaxbee/go-spatialite) 5 | [![coverage](https://coveralls.io/repos/github/shaxbee/go-spatialite/badge.svg?branch=master)](https://coveralls.io/r/shaxbee/go-spatialite) 6 | 7 | Spatialite SQL Driver for Golang. 8 | 9 | -------------------------------------------------------------------------------- /spatialite.go: -------------------------------------------------------------------------------- 1 | package spatialite 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | 7 | "github.com/mattn/go-sqlite3" 8 | ) 9 | 10 | type entrypoint struct { 11 | lib string 12 | proc string 13 | } 14 | 15 | var LibNames = []entrypoint{ 16 | {"mod_spatialite", "sqlite3_modspatialite_init"}, 17 | {"mod_spatialite.dylib", "sqlite3_modspatialite_init"}, 18 | {"libspatialite.so", "sqlite3_modspatialite_init"}, 19 | {"libspatialite.so.5", "spatialite_init_ex"}, 20 | {"libspatialite.so", "spatialite_init_ex"}, 21 | } 22 | 23 | var ErrSpatialiteNotFound = errors.New("shaxbee/go-spatialite: spatialite extension not found.") 24 | 25 | func init() { 26 | sql.Register("spatialite", &sqlite3.SQLiteDriver{ 27 | ConnectHook: func(conn *sqlite3.SQLiteConn) error { 28 | for _, v := range LibNames { 29 | if err := conn.LoadExtension(v.lib, v.proc); err == nil { 30 | return nil 31 | } 32 | } 33 | return ErrSpatialiteNotFound 34 | }, 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /spatialite_test.go: -------------------------------------------------------------------------------- 1 | package spatialite 2 | 3 | import ( 4 | "database/sql" 5 | "testing" 6 | 7 | "github.com/shaxbee/go-spatialite/wkb" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestPoint(t *testing.T) { 13 | db := makeDB(t) 14 | defer db.Close() 15 | 16 | _, err := db.Exec("CREATE TABLE poi(title TEXT)") 17 | require.NoError(t, err) 18 | 19 | _, err = db.Exec("SELECT AddGeometryColumn('poi', 'loc', 4326, 'POINT')") 20 | require.NoError(t, err) 21 | 22 | p1 := wkb.Point{10, 10} 23 | _, err = db.Exec("INSERT INTO poi(title, loc) VALUES (?, ST_PointFromWKB(?, 4326))", "foo", p1) 24 | assert.NoError(t, err) 25 | 26 | p2 := wkb.Point{} 27 | r := db.QueryRow("SELECT ST_AsBinary(loc) AS loc FROM poi WHERE title=?", "foo") 28 | if err := r.Scan(&p2); assert.NoError(t, err) { 29 | assert.Equal(t, p1, p2) 30 | } 31 | } 32 | 33 | func makeDB(t *testing.T) *sql.DB { 34 | db, err := sql.Open("spatialite", "file:dummy.db?mode=memory&cache=shared") 35 | require.NoError(t, err) 36 | 37 | _, err = db.Exec("SELECT InitSpatialMetadata()") 38 | require.NoError(t, err) 39 | return db 40 | } 41 | -------------------------------------------------------------------------------- /wkb/geometry.go: -------------------------------------------------------------------------------- 1 | package wkb 2 | 3 | import ( 4 | "bytes" 5 | "database/sql/driver" 6 | ) 7 | 8 | func New(b []byte) (Geometry, error) { 9 | _, g, err := ReadGeometry(b) 10 | return g, err 11 | } 12 | 13 | func ReadGeometry(b []byte) ([]byte, Geometry, error) { 14 | if len(b) < HeaderSize { 15 | return nil, nil, ErrInvalidStorage 16 | } 17 | 18 | dec := byteOrder(b[0]) 19 | if dec == nil { 20 | return nil, nil, ErrInvalidStorage 21 | } 22 | 23 | _, kind := readUint32(b[ByteOrderSize:], dec) 24 | 25 | var g Geometry 26 | var err error 27 | switch kind { 28 | case GeomPoint: 29 | b, g, err = ReadPoint(b) 30 | case GeomLineString: 31 | b, g, err = ReadLineString(b) 32 | case GeomPolygon: 33 | b, g, err = ReadPolygon(b) 34 | case GeomMultiPoint: 35 | b, g, err = ReadMultiPoint(b) 36 | case GeomMultiLineString: 37 | b, g, err = ReadMultiLineString(b) 38 | case GeomMultiPolygon: 39 | b, g, err = ReadMultiPolygon(b) 40 | case GeomCollection: 41 | b, g, err = ReadGeometryCollection(b) 42 | default: 43 | return nil, nil, ErrUnsupportedValue 44 | } 45 | 46 | return b, g, err 47 | } 48 | 49 | func (gc *GeometryCollection) Scan(src interface{}) error { 50 | b, ok := src.([]byte) 51 | if !ok { 52 | return ErrInvalidStorage 53 | } 54 | 55 | _, tmp, err := ReadGeometryCollection(b) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | *gc = tmp 61 | return err 62 | } 63 | 64 | func (gc GeometryCollection) Value() (driver.Value, error) { 65 | buf := bytes.NewBuffer(make([]byte, 0, gc.ByteSize())) 66 | gc.Write(buf) 67 | return buf.Bytes(), nil 68 | } 69 | 70 | func ReadGeometryCollection(b []byte) ([]byte, GeometryCollection, error) { 71 | if len(b) < HeaderSize+CountSize { 72 | return nil, nil, ErrInvalidStorage 73 | } 74 | 75 | b, dec, err := header(b, GeomCollection) 76 | if err != nil { 77 | return nil, nil, err 78 | } 79 | 80 | b, n := readCount(b, dec) 81 | 82 | gc := make([]Geometry, n) 83 | for i := 0; i < n; i++ { 84 | b, gc[i], err = ReadGeometry(b) 85 | if err != nil { 86 | return nil, nil, err 87 | } 88 | } 89 | 90 | return b, gc, nil 91 | } 92 | 93 | func (gc GeometryCollection) ByteSize() int { 94 | size := HeaderSize + CountSize 95 | for _, g := range gc { 96 | size += g.ByteSize() 97 | } 98 | return size 99 | } 100 | 101 | func (gc GeometryCollection) Write(buf *bytes.Buffer) { 102 | writeHeader(buf, GeomCollection) 103 | writeCount(buf, len(gc)) 104 | for _, g := range gc { 105 | g.Write(buf) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /wkb/geometry_test.go: -------------------------------------------------------------------------------- 1 | package wkb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var rawGeometryCollection = []byte{ 10 | 0x01, 0x07, 0x00, 0x00, 0x00, // header 11 | 0x02, 0x00, 0x00, 0x00, // numgeometry - 2 12 | 0x01, 0x01, 0x00, 0x00, 0x00, // geometry 1 - point 13 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x40, 14 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x40, 15 | 0x01, 0x02, 0x00, 0x00, 0x00, // geometry 2 - linestring 16 | 0x02, 0x00, 0x00, 0x00, // numpoints - 2 17 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x40, 18 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x40, 19 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x40, 20 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 21 | } 22 | 23 | func TestGeometry(t *testing.T) { 24 | invalid := []struct { 25 | err error 26 | b []byte 27 | }{ 28 | // empty 29 | { 30 | ErrInvalidStorage, 31 | []byte{}, 32 | }, 33 | // invalid byte order 34 | { 35 | ErrInvalidStorage, 36 | []byte{0x02, 0x01, 0x00, 0x00, 0x00}, 37 | }, 38 | // no payload 39 | { 40 | ErrInvalidStorage, 41 | []byte{0x01, 0x01, 0x00, 0x00, 0x00}, 42 | }, 43 | // invalid type 44 | { 45 | ErrUnsupportedValue, 46 | []byte{0x01, 0x42, 0x00, 0x00, 0x00}, 47 | }, 48 | } 49 | 50 | for _, e := range invalid { 51 | if _, err := New(e.b); assert.Error(t, err) { 52 | assert.Exactly(t, e.err, err) 53 | } 54 | } 55 | 56 | if g, err := New(rawPoint); assert.NoError(t, err) { 57 | assert.Equal(t, Point{30, 10}, g) 58 | } 59 | 60 | if g, err := New(rawMultiPoint); assert.NoError(t, err) { 61 | assert.Equal(t, MultiPoint{{10, 40}, {40, 30}, {20, 20}, {30, 10}}, g) 62 | } 63 | 64 | if g, err := New(rawLineString); assert.NoError(t, err) { 65 | assert.Equal(t, LineString{{30, 10}, {10, 30}, {40, 40}}, g) 66 | } 67 | 68 | if g, err := New(rawMultiLineString); assert.NoError(t, err) { 69 | assert.Equal(t, MultiLineString{ 70 | LineString{{10, 10}, {20, 20}, {10, 40}}, 71 | LineString{{40, 40}, {30, 30}, {40, 20}, {30, 10}}, 72 | }, g) 73 | } 74 | 75 | if g, err := New(rawPolygon); assert.NoError(t, err) { 76 | assert.Equal(t, Polygon{ 77 | LinearRing{{30, 10}, {40, 40}, {20, 40}, {10, 20}, {30, 10}}, 78 | }, g) 79 | } 80 | 81 | if g, err := New(rawMultiPolygon); assert.NoError(t, err) { 82 | assert.Equal(t, MultiPolygon{ 83 | Polygon{ 84 | LinearRing{{30, 20}, {45, 40}, {10, 40}, {30, 20}}, 85 | }, 86 | Polygon{ 87 | LinearRing{{15, 5}, {40, 10}, {10, 20}, {5, 10}, {15, 5}}, 88 | }, 89 | }, g) 90 | } 91 | 92 | if g, err := New(rawGeometryCollection); assert.NoError(t, err) { 93 | assert.Equal(t, GeometryCollection{ 94 | Point{4, 6}, 95 | LineString{{4, 6}, {7, 10}}, 96 | }, g) 97 | } 98 | } 99 | 100 | func TestGeometryCollection(t *testing.T) { 101 | invalid := []struct { 102 | err error 103 | b []byte 104 | }{ 105 | // invalid type 106 | { 107 | ErrUnsupportedValue, 108 | []byte{0x01, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 109 | }, 110 | { 111 | // no payload 112 | ErrInvalidStorage, 113 | []byte{0x01, 0x07, 0x00, 0x00, 0x00}, 114 | }, 115 | // no element payload 116 | { 117 | ErrInvalidStorage, 118 | []byte{ 119 | 0x01, 0x07, 0x00, 0x00, 0x00, // header 120 | 0x02, 0x00, 0x00, 0x00, // numgeometry - 2 121 | 0x01, 0x01, 0x00, 0x00, 0x00, // geometry 1 - point 122 | }, 123 | }, 124 | } 125 | 126 | for _, e := range invalid { 127 | gc := GeometryCollection{} 128 | if err := gc.Scan(e.b); assert.Error(t, err) { 129 | assert.Exactly(t, e.err, err) 130 | } 131 | } 132 | 133 | if err := (&GeometryCollection{}).Scan(""); assert.Error(t, err) { 134 | assert.Exactly(t, ErrInvalidStorage, err) 135 | } 136 | 137 | gc := GeometryCollection{} 138 | if err := gc.Scan(rawGeometryCollection); assert.NoError(t, err) { 139 | assert.Equal(t, GeometryCollection{ 140 | Point{4, 6}, 141 | LineString{{4, 6}, {7, 10}}, 142 | }, gc) 143 | } 144 | 145 | if raw, err := gc.Value(); assert.NoError(t, err) { 146 | assert.Equal(t, rawGeometryCollection, raw) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /wkb/linestring.go: -------------------------------------------------------------------------------- 1 | package wkb 2 | 3 | import ( 4 | "bytes" 5 | "database/sql/driver" 6 | ) 7 | 8 | func (ls *LineString) Scan(src interface{}) error { 9 | b, ok := src.([]byte) 10 | if !ok { 11 | return ErrInvalidStorage 12 | } 13 | 14 | _, tmp, err := ReadLineString(b) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | *ls = tmp 20 | return nil 21 | } 22 | 23 | func (ls LineString) Value() (driver.Value, error) { 24 | buf := bytes.NewBuffer(make([]byte, 0, ls.ByteSize())) 25 | ls.Write(buf) 26 | return buf.Bytes(), nil 27 | } 28 | 29 | func ReadLineString(b []byte) ([]byte, LineString, error) { 30 | if len(b) < HeaderSize+CountSize { 31 | return nil, nil, ErrInvalidStorage 32 | } 33 | 34 | b, dec, err := header(b, GeomLineString) 35 | if err != nil { 36 | return nil, nil, err 37 | } 38 | 39 | b, pts, err := readPoints(b, dec) 40 | if err != nil { 41 | return nil, nil, err 42 | } 43 | return b, LineString(pts), err 44 | } 45 | 46 | func (ls LineString) ByteSize() int { 47 | return HeaderSize + Points(ls).byteSize() 48 | } 49 | 50 | func (ls LineString) Write(buf *bytes.Buffer) { 51 | writeHeader(buf, GeomLineString) 52 | Points(ls).write(buf) 53 | } 54 | 55 | func (mls *MultiLineString) Scan(src interface{}) error { 56 | b, ok := src.([]byte) 57 | if !ok { 58 | return ErrInvalidStorage 59 | } 60 | 61 | _, tmp, err := ReadMultiLineString(b) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | *mls = tmp 67 | return nil 68 | } 69 | 70 | func (mls MultiLineString) Value() (driver.Value, error) { 71 | buf := bytes.NewBuffer(make([]byte, 0, mls.ByteSize())) 72 | mls.Write(buf) 73 | return buf.Bytes(), nil 74 | } 75 | 76 | func ReadMultiLineString(b []byte) ([]byte, MultiLineString, error) { 77 | if len(b) < HeaderSize+CountSize { 78 | return nil, nil, ErrInvalidStorage 79 | } 80 | 81 | b, dec, err := header(b, GeomMultiLineString) 82 | if err != nil { 83 | return nil, nil, err 84 | } 85 | 86 | b, n := readCount(b, dec) 87 | 88 | mls := make([]LineString, n) 89 | for i := 0; i < n; i++ { 90 | b, mls[i], err = ReadLineString(b) 91 | if err != nil { 92 | return nil, nil, err 93 | } 94 | } 95 | return b, mls, err 96 | } 97 | 98 | func (mls MultiLineString) ByteSize() int { 99 | size := HeaderSize + CountSize 100 | for _, ls := range mls { 101 | size += ls.ByteSize() 102 | } 103 | return size 104 | } 105 | 106 | func (mls MultiLineString) Write(buf *bytes.Buffer) { 107 | writeHeader(buf, GeomMultiLineString) 108 | writeCount(buf, len(mls)) 109 | for _, ls := range mls { 110 | ls.Write(buf) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /wkb/linestring_test.go: -------------------------------------------------------------------------------- 1 | package wkb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var ( 10 | rawLineString = []byte{ 11 | 0x01, 0x02, 0x00, 0x00, 0x00, // header 12 | 0x03, 0x00, 0x00, 0x00, // numpoints - 3 13 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, // point 1 14 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 15 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, // point 2 16 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 17 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, // point 3 18 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 19 | } 20 | rawMultiLineString = []byte{ 21 | 0x01, 0x05, 0x00, 0x00, 0x00, // header 22 | 0x02, 0x00, 0x00, 0x00, // numlinestring - 2 23 | 0x01, 0x02, 0x00, 0x00, 0x00, // linestring - 1 24 | 0x03, 0x00, 0x00, 0x00, // numpoints - 3 25 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 26 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 27 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x40, 28 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x40, 29 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 30 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 31 | 0x01, 0x02, 0x00, 0x00, 0x00, // linestring - 2 32 | 0x04, 0x00, 0x00, 0x00, // numpoints - 4 33 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 34 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 35 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 36 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 37 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 38 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x40, 39 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 40 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 41 | } 42 | ) 43 | 44 | func TestLineString(t *testing.T) { 45 | invalid := []struct { 46 | err error 47 | b []byte 48 | }{ 49 | { 50 | // invalid type 51 | ErrUnsupportedValue, 52 | []byte{ 53 | 0x01, 0x42, 0x00, 0x00, 0x00, 54 | 0x00, 0x00, 0x00, 0x00, 55 | }, 56 | }, 57 | { 58 | // no payload 59 | ErrInvalidStorage, 60 | []byte{ 61 | 0x01, 0x02, 0x00, 0x00, 0x00, // header 62 | }, 63 | }, 64 | { 65 | // no points 66 | ErrInvalidStorage, 67 | []byte{ 68 | 0x01, 0x02, 0x00, 0x00, 0x00, // header 69 | 0x01, 0x00, 0x00, 0x00, // numpoints - 1 70 | }, 71 | }, 72 | } 73 | 74 | for _, e := range invalid { 75 | ls := LineString{} 76 | if err := ls.Scan(e.b); assert.Error(t, err) { 77 | assert.Exactly(t, e.err, err) 78 | } 79 | } 80 | 81 | if err := (&LineString{}).Scan(""); assert.Error(t, err) { 82 | assert.Exactly(t, ErrInvalidStorage, err) 83 | } 84 | 85 | ls := LineString{} 86 | if err := ls.Scan(rawLineString); assert.NoError(t, err) { 87 | assert.Equal(t, LineString{{30, 10}, {10, 30}, {40, 40}}, ls) 88 | } 89 | 90 | if raw, err := ls.Value(); assert.NoError(t, err) { 91 | assert.Equal(t, rawLineString, raw) 92 | } 93 | } 94 | 95 | func TestMultiLineString(t *testing.T) { 96 | invalid := []struct { 97 | err error 98 | b []byte 99 | }{ 100 | { 101 | // invalid type 102 | ErrUnsupportedValue, 103 | []byte{ 104 | 0x01, 0x42, 0x00, 0x00, 0x00, 105 | 0x00, 0x00, 0x00, 0x00, 106 | }, 107 | }, 108 | { 109 | // no payload 110 | ErrInvalidStorage, 111 | []byte{ 112 | 0x01, 0x05, 0x00, 0x00, 0x00, // header 113 | }, 114 | }, 115 | { 116 | // no elements 117 | ErrInvalidStorage, 118 | []byte{ 119 | 0x01, 0x05, 0x00, 0x00, 0x00, // header 120 | 0x01, 0x00, 0x00, 0x00, // numlinestring - 1 121 | }, 122 | }, 123 | { 124 | //invalid element type 125 | ErrUnsupportedValue, 126 | []byte{ 127 | 0x01, 0x05, 0x00, 0x00, 0x00, // header 128 | 0x01, 0x00, 0x00, 0x00, // numlinestring - 2 129 | 0x01, 0x42, 0x00, 0x00, 0x00, // header - invalid type 130 | 0x00, 0x00, 0x00, 0x00, // numpoints - 0 131 | }, 132 | }, 133 | { 134 | // no payload in element 135 | ErrInvalidStorage, 136 | []byte{ 137 | 0x01, 0x05, 0x00, 0x00, 0x00, // header 138 | 0x01, 0x00, 0x00, 0x00, // numlinestring - 2 139 | 0x01, 0x02, 0x00, 0x00, 0x00, // header - invalid type 140 | }, 141 | }, 142 | } 143 | 144 | for _, e := range invalid { 145 | mls := MultiLineString{} 146 | if err := mls.Scan(e.b); assert.Error(t, err) { 147 | assert.Exactly(t, e.err, err) 148 | } 149 | } 150 | 151 | if err := (&MultiLineString{}).Scan(""); assert.Error(t, err) { 152 | assert.Exactly(t, ErrInvalidStorage, err) 153 | } 154 | 155 | mls := MultiLineString{} 156 | if err := mls.Scan(rawMultiLineString); assert.NoError(t, err) { 157 | assert.Equal(t, MultiLineString{ 158 | LineString{{10, 10}, {20, 20}, {10, 40}}, 159 | LineString{{40, 40}, {30, 30}, {40, 20}, {30, 10}}, 160 | }, mls) 161 | } 162 | 163 | if raw, err := mls.Value(); assert.NoError(t, err) { 164 | assert.Equal(t, rawMultiLineString, raw) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /wkb/point.go: -------------------------------------------------------------------------------- 1 | package wkb 2 | 3 | import ( 4 | "bytes" 5 | "database/sql/driver" 6 | "encoding/binary" 7 | ) 8 | 9 | type Point struct { 10 | X, Y float64 11 | } 12 | 13 | func (p Point) Equal(other Point) bool { 14 | return p.X == other.X && p.Y == other.Y 15 | } 16 | 17 | func (p Point) Value() (driver.Value, error) { 18 | buf := bytes.NewBuffer(make([]byte, 0, p.ByteSize())) 19 | p.Write(buf) 20 | return buf.Bytes(), nil 21 | } 22 | 23 | func (p *Point) Scan(src interface{}) error { 24 | b, ok := src.([]byte) 25 | if !ok { 26 | return ErrInvalidStorage 27 | } 28 | 29 | _, tmp, err := ReadPoint(b) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | *p = tmp 35 | return nil 36 | } 37 | 38 | func (p Point) ByteSize() int { 39 | return HeaderSize + PointSize 40 | } 41 | 42 | func (p Point) Write(buf *bytes.Buffer) { 43 | writeHeader(buf, GeomPoint) 44 | writeFloat64(buf, p.X) 45 | writeFloat64(buf, p.Y) 46 | } 47 | 48 | func ReadPoint(b []byte) ([]byte, Point, error) { 49 | p := Point{} 50 | if len(b) < HeaderSize+PointSize { 51 | return nil, p, ErrInvalidStorage 52 | } 53 | 54 | b, dec, err := header(b, GeomPoint) 55 | if err != nil { 56 | return nil, p, err 57 | } 58 | 59 | b, p.X = readFloat64(b, dec) 60 | b, p.Y = readFloat64(b, dec) 61 | return b, p, nil 62 | } 63 | 64 | func (mp *MultiPoint) Scan(src interface{}) error { 65 | b, ok := src.([]byte) 66 | if !ok { 67 | return ErrInvalidStorage 68 | } 69 | 70 | _, tmp, err := ReadMultiPoint(b) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | *mp = tmp 76 | return nil 77 | } 78 | 79 | func (mp MultiPoint) Value() (driver.Value, error) { 80 | buf := bytes.NewBuffer(make([]byte, 0, mp.ByteSize())) 81 | mp.Write(buf) 82 | return buf.Bytes(), nil 83 | } 84 | 85 | func ReadMultiPoint(b []byte) ([]byte, MultiPoint, error) { 86 | if len(b) < HeaderSize+CountSize { 87 | return nil, nil, ErrInvalidStorage 88 | } 89 | 90 | b, dec, err := header(b, GeomMultiPoint) 91 | if err != nil { 92 | return nil, nil, err 93 | } 94 | 95 | b, n := readCount(b, dec) 96 | 97 | mp := make([]Point, n) 98 | for i := 0; i < n; i++ { 99 | b, mp[i], err = ReadPoint(b) 100 | if err != nil { 101 | return nil, nil, err 102 | } 103 | } 104 | 105 | return b, mp, nil 106 | } 107 | 108 | func (mp MultiPoint) ByteSize() int { 109 | return HeaderSize + Points(mp).byteSize() 110 | } 111 | 112 | func (mp MultiPoint) Write(buf *bytes.Buffer) { 113 | writeHeader(buf, GeomMultiPoint) 114 | writeCount(buf, len(mp)) 115 | for _, p := range mp { 116 | p.Write(buf) 117 | } 118 | } 119 | 120 | func readPoint(b []byte, dec binary.ByteOrder) ([]byte, Point) { 121 | p := Point{} 122 | b, p.X = readFloat64(b, dec) 123 | b, p.Y = readFloat64(b, dec) 124 | return b, p 125 | } 126 | 127 | func readPoints(b []byte, dec binary.ByteOrder) ([]byte, Points, error) { 128 | b, n := readCount(b, dec) 129 | 130 | if len(b) < PointSize*n { 131 | return nil, nil, ErrInvalidStorage 132 | } 133 | 134 | p := make([]Point, n) 135 | for i := 0; i < n; i++ { 136 | b, p[i] = readPoint(b, dec) 137 | } 138 | 139 | return b, p, nil 140 | } 141 | 142 | func (pts Points) byteSize() int { 143 | return CountSize + len(pts)*PointSize 144 | } 145 | 146 | func (pts Points) write(buf *bytes.Buffer) { 147 | writeCount(buf, len(pts)) 148 | for _, p := range pts { 149 | writeFloat64(buf, p.X) 150 | writeFloat64(buf, p.Y) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /wkb/point_test.go: -------------------------------------------------------------------------------- 1 | package wkb 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | var ( 11 | rawPoint = []byte{ 12 | 0x01, 0x01, 0x00, 0x00, 0x00, 13 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 14 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 15 | } 16 | 17 | rawMultiPoint = []byte{ 18 | 0x01, 0x04, 0x00, 0x00, 0x00, // header 19 | 0x04, 0x00, 0x00, 0x00, // numpoints - 4 20 | 0x01, 0x01, 0x00, 0x00, 0x00, // point 1 21 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 22 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 23 | 0x01, 0x01, 0x00, 0x00, 0x00, // point 2 24 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 25 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 26 | 0x01, 0x01, 0x00, 0x00, 0x00, // point 3 27 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x40, 28 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x40, 29 | 0x01, 0x01, 0x00, 0x00, 0x00, // point 4 30 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 31 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 32 | } 33 | ) 34 | 35 | func TestPoint(t *testing.T) { 36 | invalid := map[error][]byte{ 37 | ErrInvalidStorage: { 38 | 0x01, 39 | }, // header too short 40 | ErrInvalidStorage: { 41 | 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 42 | }, // no payload 43 | ErrInvalidStorage: { 44 | 0x02, 0x01, 0x00, 0x00, 0x00, 45 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 46 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 47 | }, // invalid endianness 48 | ErrInvalidStorage: { 49 | 0x01, 0x01, 0x00, 0x00, 0x00, 50 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 51 | }, // single coordinate only 52 | ErrUnsupportedValue: { 53 | 0x01, 0x02, 0x00, 0x00, 0x00, 54 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 55 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 56 | }, // invalid type 57 | } 58 | 59 | for expected, b := range invalid { 60 | p := Point{} 61 | if err := p.Scan(b); assert.Error(t, err) { 62 | assert.Exactly(t, expected, err, "Expected point <%s> to fail", hex.EncodeToString(b)) 63 | } 64 | } 65 | 66 | if err := (&Point{}).Scan(""); assert.Error(t, err) { 67 | assert.Exactly(t, ErrInvalidStorage, err) 68 | } 69 | 70 | p := Point{} 71 | if assert.NoError(t, p.Scan(rawPoint)) { 72 | assert.Equal(t, Point{30, 10}, p) 73 | } 74 | 75 | if raw, err := p.Value(); assert.NoError(t, err) { 76 | assert.Equal(t, rawPoint, raw) 77 | } 78 | } 79 | 80 | func TestMultiPoint(t *testing.T) { 81 | invalid := []struct { 82 | err error 83 | b []byte 84 | }{ 85 | { 86 | // invalid byte order 87 | ErrUnsupportedValue, 88 | []byte{ 89 | 0x42, 0x04, 0x00, 0x00, 0x00, 90 | 0x00, 0x00, 0x00, 0x00, 91 | }, 92 | }, 93 | { 94 | // invalid type 95 | ErrUnsupportedValue, []byte{ 96 | 0x01, 0x42, 0x00, 0x00, 0x00, 97 | 0x00, 0x00, 0x00, 0x00, 98 | }, 99 | }, 100 | { 101 | // no payload 102 | ErrInvalidStorage, []byte{ 103 | 0x01, 0x04, 0x00, 0x00, 0x00, 104 | }, 105 | }, 106 | { 107 | // no points 108 | ErrInvalidStorage, []byte{ 109 | 0x01, 0x04, 0x00, 0x00, 0x00, 110 | 0x01, 0x00, 0x00, 0x00, // numpoints - 1 111 | }, 112 | }, 113 | { 114 | // incomplete point 115 | ErrInvalidStorage, []byte{ 116 | 0x01, 0x04, 0x00, 0x00, 0x00, 117 | 0x01, 0x00, 0x00, 0x00, // numpoints - 1 118 | 0x01, 0x01, 0x00, 0x00, 0x00, // point without payload 119 | }, 120 | }, 121 | { 122 | // element not a point 123 | ErrUnsupportedValue, []byte{ 124 | 0x01, 0x04, 0x00, 0x00, 0x00, 125 | 0x01, 0x00, 0x00, 0x00, // numpoints - 1 126 | 0x01, 0x02, 0x00, 0x00, 0x00, // invalid element 127 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 128 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 129 | }, 130 | }, 131 | } 132 | 133 | for _, e := range invalid { 134 | mp := MultiPoint{} 135 | if err := mp.Scan(e.b); assert.Error(t, err) { 136 | assert.Exactly(t, e.err, err) 137 | } 138 | } 139 | 140 | if err := (&MultiPoint{}).Scan(""); assert.Error(t, err) { 141 | assert.Exactly(t, ErrInvalidStorage, err) 142 | } 143 | 144 | mp := MultiPoint{} 145 | if assert.NoError(t, mp.Scan(rawMultiPoint)) { 146 | assert.Equal(t, MultiPoint{{10, 40}, {40, 30}, {20, 20}, {30, 10}}, mp) 147 | } 148 | 149 | if raw, err := mp.Value(); assert.NoError(t, err) { 150 | assert.Equal(t, rawMultiPoint, raw) 151 | } 152 | } 153 | 154 | func TestEqual(t *testing.T) { 155 | assert.True(t, Point{10, 10}.Equal(Point{10, 10})) 156 | assert.False(t, Point{10, 20}.Equal(Point{20, 10})) 157 | } 158 | -------------------------------------------------------------------------------- /wkb/polygon.go: -------------------------------------------------------------------------------- 1 | package wkb 2 | 3 | import ( 4 | "bytes" 5 | "database/sql/driver" 6 | "encoding/binary" 7 | ) 8 | 9 | func (p *Polygon) Scan(src interface{}) error { 10 | b, ok := src.([]byte) 11 | if !ok { 12 | return ErrInvalidStorage 13 | } 14 | 15 | _, tmp, err := ReadPolygon(b) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | *p = tmp 21 | return err 22 | } 23 | 24 | func (p Polygon) Value() (driver.Value, error) { 25 | buf := bytes.NewBuffer(make([]byte, 0, p.ByteSize())) 26 | p.Write(buf) 27 | return buf.Bytes(), nil 28 | } 29 | 30 | func ReadPolygon(b []byte) ([]byte, Polygon, error) { 31 | if len(b) < HeaderSize+CountSize { 32 | return nil, nil, ErrInvalidStorage 33 | } 34 | 35 | b, dec, err := header(b, GeomPolygon) 36 | if err != nil { 37 | return nil, nil, err 38 | } 39 | 40 | b, n := readCount(b, dec) 41 | 42 | p := make([]LinearRing, n) 43 | for i := 0; i < n; i++ { 44 | if len(b) < CountSize { 45 | return nil, nil, ErrInvalidStorage 46 | } 47 | 48 | b, p[i], err = readLinearRing(b, dec) 49 | if err != nil { 50 | return nil, nil, err 51 | } 52 | } 53 | 54 | return b, p, nil 55 | } 56 | 57 | func (p Polygon) ByteSize() int { 58 | size := HeaderSize + CountSize 59 | for _, lr := range p { 60 | size += lr.byteSize() 61 | } 62 | return size 63 | } 64 | 65 | func (p Polygon) Write(buf *bytes.Buffer) { 66 | writeHeader(buf, GeomPolygon) 67 | writeCount(buf, len(p)) 68 | for _, lr := range p { 69 | lr.write(buf) 70 | } 71 | } 72 | 73 | func (mp *MultiPolygon) Scan(src interface{}) error { 74 | b, ok := src.([]byte) 75 | if !ok { 76 | return ErrInvalidStorage 77 | } 78 | 79 | _, tmp, err := ReadMultiPolygon(b) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | *mp = tmp 85 | return nil 86 | } 87 | 88 | func (mp MultiPolygon) Value() (driver.Value, error) { 89 | buf := bytes.NewBuffer(make([]byte, 0, mp.ByteSize())) 90 | mp.Write(buf) 91 | return buf.Bytes(), nil 92 | } 93 | 94 | func ReadMultiPolygon(b []byte) ([]byte, MultiPolygon, error) { 95 | if len(b) < HeaderSize+CountSize { 96 | return nil, nil, ErrInvalidStorage 97 | } 98 | 99 | b, dec, err := header(b, GeomMultiPolygon) 100 | if err != nil { 101 | return nil, nil, err 102 | } 103 | 104 | b, n := readCount(b, dec) 105 | 106 | mp := make([]Polygon, n) 107 | for i := 0; i < n; i++ { 108 | b, mp[i], err = ReadPolygon(b) 109 | if err != nil { 110 | return nil, nil, err 111 | } 112 | } 113 | 114 | return b, mp, nil 115 | } 116 | 117 | func (mp MultiPolygon) ByteSize() int { 118 | size := HeaderSize + CountSize 119 | for _, p := range mp { 120 | size += p.ByteSize() 121 | } 122 | return size 123 | } 124 | 125 | func (mp MultiPolygon) Write(buf *bytes.Buffer) { 126 | writeHeader(buf, GeomMultiPolygon) 127 | writeCount(buf, len(mp)) 128 | for _, p := range mp { 129 | p.Write(buf) 130 | } 131 | } 132 | 133 | func readLinearRing(b []byte, dec binary.ByteOrder) ([]byte, LinearRing, error) { 134 | b, pts, err := readPoints(b, dec) 135 | return b, LinearRing(pts), err 136 | } 137 | 138 | func (lr LinearRing) byteSize() int { 139 | return Points(lr).byteSize() 140 | } 141 | 142 | func (lr LinearRing) write(buf *bytes.Buffer) { 143 | Points(lr).write(buf) 144 | } 145 | -------------------------------------------------------------------------------- /wkb/polygon_test.go: -------------------------------------------------------------------------------- 1 | package wkb 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | var ( 11 | rawPolygon = []byte{ 12 | 0x01, 0x03, 0x00, 0x00, 0x00, // header 13 | 0x01, 0x00, 0x00, 0x00, // numlinearring - 1 14 | 0x05, 0x00, 0x00, 0x00, // numpoints - 5 15 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, // point 1 16 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 17 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, // point 2 18 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 19 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x40, // point 3 20 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 21 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, // point 4 22 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x40, 23 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, // point 5 24 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 25 | } 26 | rawMultiPolygon = []byte{ 27 | 0x01, 0x06, 0x00, 0x00, 0x00, // header 28 | 0x02, 0x00, 0x00, 0x00, // numpolygon - 2 29 | 0x01, 0x03, 0x00, 0x00, 0x00, // polygon 1 30 | 0x01, 0x00, 0x00, 0x00, // numlinearring - 1 31 | 0x04, 0x00, 0x00, 0x00, // numpoints - 4, 32 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 33 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x40, 34 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x46, 0x40, 35 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 36 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 37 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 38 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x40, 39 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x40, 40 | 0x01, 0x03, 0x00, 0x00, 0x00, // polygon 2 41 | 0x01, 0x00, 0x00, 0x00, // numlinearring - 1 42 | 0x05, 0x00, 0x00, 0x00, // numpoints - 5 43 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2e, 0x40, 44 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x40, 45 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x40, 46 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 47 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 48 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x40, 49 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x40, 50 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, 51 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2e, 0x40, 52 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x40, 53 | } 54 | ) 55 | 56 | func TestPolygon(t *testing.T) { 57 | invalid := []struct { 58 | err error 59 | b []byte 60 | }{ 61 | // invalid type 62 | { 63 | ErrUnsupportedValue, 64 | []byte{ 65 | 0x01, 0x42, 0x00, 0x00, 0x00, 66 | 0x00, 0x00, 0x00, 0x00, 67 | }, 68 | }, 69 | // no payload 70 | { 71 | ErrInvalidStorage, 72 | []byte{0x01, 0x03, 0x00, 0x00, 0x00}, 73 | }, 74 | // no elements 75 | { 76 | ErrInvalidStorage, 77 | []byte{ 78 | 0x01, 0x03, 0x00, 0x00, 0x00, // header 79 | 0x01, 0x00, 0x00, 0x00, // numlinearring - 1 80 | }, 81 | }, 82 | // no element payload 83 | { 84 | ErrInvalidStorage, 85 | []byte{ 86 | 0x01, 0x03, 0x00, 0x00, 0x00, // header 87 | 0x01, 0x00, 0x00, 0x00, // numlinearring - 1 88 | 0x01, 0x00, 0x00, 0x00, // numpoints - 1 89 | }, 90 | }, 91 | } 92 | 93 | for _, e := range invalid { 94 | p := Polygon{} 95 | if err := p.Scan(e.b); assert.Error(t, err) { 96 | assert.Exactly(t, e.err, err) 97 | } 98 | } 99 | 100 | if err := (&Polygon{}).Scan(""); assert.Error(t, err) { 101 | assert.Exactly(t, ErrInvalidStorage, err) 102 | } 103 | 104 | p := Polygon{} 105 | if err := p.Scan(rawPolygon); assert.NoError(t, err) { 106 | assert.Equal(t, Polygon{ 107 | LinearRing{{30, 10}, {40, 40}, {20, 40}, {10, 20}, {30, 10}}, 108 | }, p) 109 | } 110 | 111 | if raw, err := p.Value(); assert.NoError(t, err) { 112 | assert.Equal(t, rawPolygon, raw) 113 | } 114 | } 115 | 116 | func TestMultiPolygon(t *testing.T) { 117 | invalid := []struct { 118 | err error 119 | b []byte 120 | }{ 121 | // invalid type 122 | { 123 | ErrUnsupportedValue, 124 | []byte{ 125 | 0x01, 0x42, 0x00, 0x00, 0x00, 126 | 0x00, 0x00, 0x00, 0x00, 127 | }, 128 | }, 129 | // no payload 130 | { 131 | ErrInvalidStorage, 132 | []byte{0x01, 0x06, 0x00, 0x00, 0x00}, 133 | }, 134 | // no elements 135 | { 136 | ErrInvalidStorage, 137 | []byte{ 138 | 0x01, 0x06, 0x00, 0x00, 0x00, // header 139 | 0x01, 0x00, 0x00, 0x00, // numpolygon - 2 140 | }, 141 | }, 142 | // invalid element type 143 | { 144 | ErrUnsupportedValue, 145 | []byte{ 146 | 0x01, 0x06, 0x00, 0x00, 0x00, // header 147 | 0x01, 0x00, 0x00, 0x00, // numpolygon - 2 148 | 0x01, 0x42, 0x00, 0x00, 0x00, // polygon 1 149 | 0x00, 0x00, 0x00, 0x00, 150 | }, 151 | }, 152 | // no element payload 153 | { 154 | ErrInvalidStorage, 155 | []byte{ 156 | 0x01, 0x06, 0x00, 0x00, 0x00, // header 157 | 0x02, 0x00, 0x00, 0x00, // numpolygon - 2 158 | 0x01, 0x03, 0x00, 0x00, 0x00, // polygon 1 159 | }, 160 | }, 161 | } 162 | 163 | for _, e := range invalid { 164 | mp := MultiPolygon{} 165 | if err := mp.Scan(e.b); assert.Error(t, err) { 166 | assert.Exactly(t, e.err, err, "Expected MultiPolygon <%v> to fail", hex.EncodeToString(e.b)) 167 | } 168 | } 169 | 170 | if err := (&MultiPolygon{}).Scan(""); assert.Error(t, err) { 171 | assert.Exactly(t, ErrInvalidStorage, err) 172 | } 173 | 174 | mp := MultiPolygon{} 175 | if err := mp.Scan(rawMultiPolygon); assert.NoError(t, err) { 176 | assert.Equal(t, MultiPolygon{ 177 | Polygon{ 178 | LinearRing{{30, 20}, {45, 40}, {10, 40}, {30, 20}}, 179 | }, 180 | Polygon{ 181 | LinearRing{{15, 5}, {40, 10}, {10, 20}, {5, 10}, {15, 5}}, 182 | }, 183 | }, mp) 184 | } 185 | 186 | if raw, err := mp.Value(); assert.NoError(t, err) { 187 | assert.Equal(t, rawMultiPolygon, raw) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /wkb/primitive.go: -------------------------------------------------------------------------------- 1 | package wkb 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "math" 7 | ) 8 | 9 | func readUint32(b []byte, dec binary.ByteOrder) ([]byte, uint32) { 10 | return b[CountSize:], dec.Uint32(b) 11 | } 12 | 13 | func readCount(b []byte, dec binary.ByteOrder) ([]byte, int) { 14 | b, n := readUint32(b, dec) 15 | return b, int(n) 16 | } 17 | 18 | func writeCount(buf *bytes.Buffer, n int) { 19 | b := [CountSize]byte{} 20 | binary.LittleEndian.PutUint32(b[:], uint32(n)) 21 | buf.Write(b[:]) 22 | } 23 | 24 | func readFloat64(b []byte, dec binary.ByteOrder) ([]byte, float64) { 25 | return b[Float64Size:], math.Float64frombits(dec.Uint64(b)) 26 | } 27 | 28 | func writeFloat64(buf *bytes.Buffer, f float64) { 29 | b := [Float64Size]byte{} 30 | binary.LittleEndian.PutUint64(b[:], math.Float64bits(f)) 31 | buf.Write(b[:]) 32 | } 33 | 34 | func header(b []byte, tpe Kind) ([]byte, binary.ByteOrder, error) { 35 | dec := byteOrder(b[0]) 36 | if dec == nil { 37 | return nil, nil, ErrUnsupportedValue 38 | } 39 | 40 | b, kind := readUint32(b[ByteOrderSize:], dec) 41 | if tpe != Kind(kind) { 42 | return nil, nil, ErrUnsupportedValue 43 | } 44 | 45 | return b, dec, nil 46 | } 47 | 48 | func byteOrder(b byte) binary.ByteOrder { 49 | switch b { 50 | case BigEndian: 51 | return binary.BigEndian 52 | case LittleEndian: 53 | return binary.LittleEndian 54 | default: 55 | return nil 56 | } 57 | } 58 | 59 | func writeHeader(buf *bytes.Buffer, tpe Kind) { 60 | b := [HeaderSize]byte{} 61 | b[0] = LittleEndian 62 | binary.LittleEndian.PutUint32(b[ByteOrderSize:], uint32(tpe)) 63 | buf.Write(b[:]) 64 | } 65 | -------------------------------------------------------------------------------- /wkb/primitive_test.go: -------------------------------------------------------------------------------- 1 | package wkb 2 | 3 | import ( 4 | "encoding/binary" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestHeader(t *testing.T) { 11 | invalid := map[error][]byte{ 12 | ErrUnsupportedValue: {0x02, 0x01, 0x00, 0x00, 0x00}, 13 | ErrUnsupportedValue: {0x01, 0x02, 0x00, 0x00, 0x00}, 14 | } 15 | for expected, b := range invalid { 16 | if _, _, err := header(b, GeomPoint); assert.Error(t, err) { 17 | assert.Exactly(t, expected, err) 18 | } 19 | } 20 | 21 | valid := []byte{0x01, 0x01, 0x00, 0x00, 0x00} 22 | if b, bo, err := header(valid, GeomPoint); assert.NoError(t, err) { 23 | assert.Len(t, b, 0) 24 | assert.Exactly(t, binary.LittleEndian, bo) 25 | } 26 | } 27 | 28 | func TestByteOrder(t *testing.T) { 29 | assert.Exactly(t, binary.BigEndian, byteOrder(0x00)) 30 | assert.Exactly(t, binary.LittleEndian, byteOrder(0x01)) 31 | assert.Nil(t, byteOrder(0x42)) 32 | } 33 | -------------------------------------------------------------------------------- /wkb/wkb.go: -------------------------------------------------------------------------------- 1 | package wkb 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "unsafe" 7 | ) 8 | 9 | type ByteOrder byte 10 | 11 | const ( 12 | BigEndian = iota 13 | LittleEndian 14 | ) 15 | 16 | type Kind uint32 17 | 18 | const ( 19 | _ = iota 20 | GeomPoint 21 | GeomLineString 22 | GeomPolygon 23 | GeomMultiPoint 24 | GeomMultiLineString 25 | GeomMultiPolygon 26 | GeomCollection 27 | ) 28 | 29 | const ( 30 | ByteOrderSize = int(unsafe.Sizeof(ByteOrder(0))) 31 | GeomTypeSize = int(unsafe.Sizeof(Kind(0))) 32 | HeaderSize = ByteOrderSize + GeomTypeSize 33 | CountSize = int(unsafe.Sizeof(uint32(0))) 34 | Float64Size = int(unsafe.Sizeof(float64(0))) 35 | PointSize = int(unsafe.Sizeof(Point{})) 36 | ) 37 | 38 | var ( 39 | ErrInvalidStorage = errors.New("Invalid storage type or size") 40 | ErrUnsupportedValue = errors.New("Unsupported value") 41 | ) 42 | 43 | type Geometry interface { 44 | ByteSize() int 45 | Write(*bytes.Buffer) 46 | } 47 | 48 | type LineString Points 49 | type Polygon []LinearRing 50 | type MultiPoint Points 51 | type MultiLineString []LineString 52 | type MultiPolygon []Polygon 53 | type GeometryCollection []Geometry 54 | 55 | type LinearRing Points 56 | type Points []Point 57 | --------------------------------------------------------------------------------