├── .hound.yml ├── .travis.yml ├── LICENSE ├── README.md ├── appveyor.yml ├── errreader.go ├── reader.go ├── reader_test.go ├── sequentialreader.go ├── sequentialreader_test.go ├── shapefile.go ├── shapefile_test.go ├── shapetype_string.go ├── test_files ├── multipatch.dbf ├── multipatch.shp ├── multipatch.shx ├── multipoint.dbf ├── multipoint.shp ├── multipoint.shx ├── multipointm.dbf ├── multipointm.shp ├── multipointm.shx ├── multipointz.dbf ├── multipointz.shp ├── multipointz.shx ├── point.dbf ├── point.shp ├── point.shx ├── pointm.dbf ├── pointm.shp ├── pointm.shx ├── pointz.dbf ├── pointz.shp ├── pointz.shx ├── polygon.dbf ├── polygon.shp ├── polygon.shx ├── polygonm.dbf ├── polygonm.shp ├── polygonm.shx ├── polygonz.dbf ├── polygonz.shp ├── polygonz.shx ├── polyline.dbf ├── polyline.shp ├── polyline.shx ├── polylinem.dbf ├── polylinem.shp ├── polylinem.shx ├── polylinez.dbf ├── polylinez.shp └── polylinez.shx ├── writer.go ├── writer_test.go ├── zipreader.go └── zipreader_test.go /.hound.yml: -------------------------------------------------------------------------------- 1 | go: 2 | enabled: true 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | 4 | go: 5 | - 1.8.x 6 | - 1.9.x 7 | - master 8 | 9 | os: 10 | - linux 11 | 12 | before_install: 13 | - go get -t -v ./... 14 | 15 | script: 16 | - go test -race -coverprofile=coverage.txt -covermode=atomic 17 | 18 | after_success: 19 | - bash <(curl -s https://codecov.io/bash) 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jonas Palm 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-shp 2 | ====== 3 | 4 | [![Build Status](https://travis-ci.org/jonas-p/go-shp.svg?branch=master)](https://travis-ci.org/jonas-p/go-shp) 5 | [![Build status](https://ci.appveyor.com/api/projects/status/b64sntax4kxlouxa?svg=true)](https://ci.appveyor.com/project/fawick/go-shp) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/jonas-p/go-shp)](https://goreportcard.com/report/github.com/jonas-p/go-shp) 7 | [![Codevov](https://codecov.io/gh/jonas-p/go-shp/branch/master/graphs/badge.svg)](https://codecov.io/gh/jonas-p/go-shp) 8 | 9 | Go library for reading and writing ESRI Shapefiles. This is a pure Golang implementation based on the ESRI Shapefile technical description. 10 | 11 | ### Usage 12 | #### Installation 13 | 14 | go get github.com/jonas-p/go-shp 15 | 16 | #### Importing 17 | 18 | ```go 19 | import "github.com/jonas-p/go-shp" 20 | ``` 21 | 22 | ### Examples 23 | #### Reading a shapefile 24 | 25 | ```go 26 | // open a shapefile for reading 27 | shape, err := shp.Open("points.shp") 28 | if err != nil { log.Fatal(err) } 29 | defer shape.Close() 30 | 31 | // fields from the attribute table (DBF) 32 | fields := shape.Fields() 33 | 34 | // loop through all features in the shapefile 35 | for shape.Next() { 36 | n, p := shape.Shape() 37 | 38 | // print feature 39 | fmt.Println(reflect.TypeOf(p).Elem(), p.BBox()) 40 | 41 | // print attributes 42 | for k, f := range fields { 43 | val := shape.ReadAttribute(n, k) 44 | fmt.Printf("\t%v: %v\n", f, val) 45 | } 46 | fmt.Println() 47 | } 48 | ``` 49 | 50 | #### Creating a shapefile 51 | 52 | ```go 53 | // points to write 54 | points := []shp.Point{ 55 | shp.Point{10.0, 10.0}, 56 | shp.Point{10.0, 15.0}, 57 | shp.Point{15.0, 15.0}, 58 | shp.Point{15.0, 10.0}, 59 | } 60 | 61 | // fields to write 62 | fields := []shp.Field{ 63 | // String attribute field with length 25 64 | shp.StringField("NAME", 25), 65 | } 66 | 67 | // create and open a shapefile for writing points 68 | shape, err := shp.Create("points.shp", shp.POINT) 69 | if err != nil { log.Fatal(err) } 70 | defer shape.Close() 71 | 72 | // setup fields for attributes 73 | shape.SetFields(fields) 74 | 75 | // write points and attributes 76 | for n, point := range points { 77 | shape.Write(&point) 78 | 79 | // write attribute for object n for field 0 (NAME) 80 | shape.WriteAttribute(n, 0, "Point " + strconv.Itoa(n + 1)) 81 | } 82 | ``` 83 | 84 | ### Resources 85 | 86 | - [Documentation on godoc.org](http://godoc.org/github.com/jonas-p/go-shp) 87 | - [ESRI Shapefile Technical Description](http://www.esri.com/library/whitepapers/pdfs/shapefile.pdf) 88 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | clone_folder: c:\go-shp 2 | 3 | environment: 4 | GOPATH: c:\gopath 5 | 6 | branches: 7 | only: 8 | - master 9 | 10 | init: 11 | - ps: >- 12 | $app = Get-WmiObject -Class Win32_Product -Filter "Vendor = 'http://golang.org'" 13 | 14 | if ($app) { 15 | $app.Uninstall() 16 | } 17 | 18 | install: 19 | - rmdir c:\go /s /q 20 | - appveyor DownloadFile https://storage.googleapis.com/golang/go1.9.windows-amd64.msi 21 | - msiexec /i go1.9.windows-amd64.msi /q 22 | - go version 23 | - go env 24 | 25 | build_script: 26 | - go test ./... 27 | -------------------------------------------------------------------------------- /errreader.go: -------------------------------------------------------------------------------- 1 | package shp 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // errReader is a helper to perform multiple successive read from another reader 9 | // and do the error checking only once afterwards. It will not perform any new 10 | // reads in case there was an error encountered earlier. 11 | type errReader struct { 12 | io.Reader 13 | e error 14 | n int64 15 | } 16 | 17 | func (er *errReader) Read(p []byte) (n int, err error) { 18 | if er.e != nil { 19 | return 0, fmt.Errorf("unable to read after previous error: %v", er.e) 20 | } 21 | n, err = er.Reader.Read(p) 22 | if n < len(p) && err != nil { 23 | er.e = err 24 | } 25 | er.n += int64(n) 26 | return n, er.e 27 | } 28 | -------------------------------------------------------------------------------- /reader.go: -------------------------------------------------------------------------------- 1 | package shp 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "math" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | ) 12 | 13 | // Reader provides a interface for reading Shapefiles. Calls 14 | // to the Next method will iterate through the objects in the 15 | // Shapefile. After a call to Next the object will be available 16 | // through the Shape method. 17 | type Reader struct { 18 | GeometryType ShapeType 19 | bbox Box 20 | err error 21 | 22 | shp readSeekCloser 23 | shape Shape 24 | num int32 25 | filename string 26 | filelength int64 27 | 28 | dbf readSeekCloser 29 | dbfFields []Field 30 | dbfNumRecords int32 31 | dbfHeaderLength int16 32 | dbfRecordLength int16 33 | } 34 | 35 | type readSeekCloser interface { 36 | io.Reader 37 | io.Seeker 38 | io.Closer 39 | } 40 | 41 | // Open opens a Shapefile for reading. 42 | func Open(filename string) (*Reader, error) { 43 | ext := filepath.Ext(filename) 44 | if strings.ToLower(ext) != ".shp" { 45 | return nil, fmt.Errorf("Invalid file extension: %s", filename) 46 | } 47 | shp, err := os.Open(filename) 48 | if err != nil { 49 | return nil, err 50 | } 51 | s := &Reader{filename: strings.TrimSuffix(filename, ext), shp: shp} 52 | return s, s.readHeaders() 53 | } 54 | 55 | // BBox returns the bounding box of the shapefile. 56 | func (r *Reader) BBox() Box { 57 | return r.bbox 58 | } 59 | 60 | // Read and parse headers in the Shapefile. This will 61 | // fill out GeometryType, filelength and bbox. 62 | func (r *Reader) readHeaders() error { 63 | er := &errReader{Reader: r.shp} 64 | // don't trust the the filelength in the header 65 | r.filelength, _ = r.shp.Seek(0, io.SeekEnd) 66 | 67 | var filelength int32 68 | r.shp.Seek(24, 0) 69 | // file length 70 | binary.Read(er, binary.BigEndian, &filelength) 71 | r.shp.Seek(32, 0) 72 | binary.Read(er, binary.LittleEndian, &r.GeometryType) 73 | r.bbox.MinX = readFloat64(er) 74 | r.bbox.MinY = readFloat64(er) 75 | r.bbox.MaxX = readFloat64(er) 76 | r.bbox.MaxY = readFloat64(er) 77 | r.shp.Seek(100, 0) 78 | return er.e 79 | } 80 | 81 | func readFloat64(r io.Reader) float64 { 82 | var bits uint64 83 | binary.Read(r, binary.LittleEndian, &bits) 84 | return math.Float64frombits(bits) 85 | } 86 | 87 | // Close closes the Shapefile. 88 | func (r *Reader) Close() error { 89 | if r.err == nil { 90 | r.err = r.shp.Close() 91 | if r.dbf != nil { 92 | r.dbf.Close() 93 | } 94 | } 95 | return r.err 96 | } 97 | 98 | // Shape returns the most recent feature that was read by 99 | // a call to Next. It returns two values, the int is the 100 | // object index starting from zero in the shapefile which 101 | // can be used as row in ReadAttribute, and the Shape is the object. 102 | func (r *Reader) Shape() (int, Shape) { 103 | return int(r.num) - 1, r.shape 104 | } 105 | 106 | // Attribute returns value of the n-th attribute of the most recent feature 107 | // that was read by a call to Next. 108 | func (r *Reader) Attribute(n int) string { 109 | return r.ReadAttribute(int(r.num)-1, n) 110 | } 111 | 112 | // newShape creates a new shape with a given type. 113 | func newShape(shapetype ShapeType) (Shape, error) { 114 | switch shapetype { 115 | case NULL: 116 | return new(Null), nil 117 | case POINT: 118 | return new(Point), nil 119 | case POLYLINE: 120 | return new(PolyLine), nil 121 | case POLYGON: 122 | return new(Polygon), nil 123 | case MULTIPOINT: 124 | return new(MultiPoint), nil 125 | case POINTZ: 126 | return new(PointZ), nil 127 | case POLYLINEZ: 128 | return new(PolyLineZ), nil 129 | case POLYGONZ: 130 | return new(PolygonZ), nil 131 | case MULTIPOINTZ: 132 | return new(MultiPointZ), nil 133 | case POINTM: 134 | return new(PointM), nil 135 | case POLYLINEM: 136 | return new(PolyLineM), nil 137 | case POLYGONM: 138 | return new(PolygonM), nil 139 | case MULTIPOINTM: 140 | return new(MultiPointM), nil 141 | case MULTIPATCH: 142 | return new(MultiPatch), nil 143 | default: 144 | return nil, fmt.Errorf("Unsupported shape type: %v", shapetype) 145 | } 146 | } 147 | 148 | // Next reads in the next Shape in the Shapefile, which 149 | // will then be available through the Shape method. It 150 | // returns false when the reader has reached the end of the 151 | // file or encounters an error. 152 | func (r *Reader) Next() bool { 153 | cur, _ := r.shp.Seek(0, io.SeekCurrent) 154 | if cur >= r.filelength { 155 | return false 156 | } 157 | 158 | var size int32 159 | var shapetype ShapeType 160 | er := &errReader{Reader: r.shp} 161 | binary.Read(er, binary.BigEndian, &r.num) 162 | binary.Read(er, binary.BigEndian, &size) 163 | binary.Read(er, binary.LittleEndian, &shapetype) 164 | if er.e != nil { 165 | if er.e != io.EOF { 166 | r.err = fmt.Errorf("Error when reading metadata of next shape: %v", er.e) 167 | } else { 168 | r.err = io.EOF 169 | } 170 | return false 171 | } 172 | 173 | var err error 174 | r.shape, err = newShape(shapetype) 175 | if err != nil { 176 | r.err = fmt.Errorf("Error decoding shape type: %v", err) 177 | return false 178 | } 179 | r.shape.read(er) 180 | if er.e != nil { 181 | r.err = fmt.Errorf("Error while reading next shape: %v", er.e) 182 | return false 183 | } 184 | 185 | // move to next object 186 | r.shp.Seek(int64(size)*2+cur+8, 0) 187 | return true 188 | } 189 | 190 | // Opens DBF file using r.filename + "dbf". This method 191 | // will parse the header and fill out all dbf* values int 192 | // the f object. 193 | func (r *Reader) openDbf() (err error) { 194 | if r.dbf != nil { 195 | return 196 | } 197 | 198 | r.dbf, err = os.Open(r.filename + ".dbf") 199 | if err != nil { 200 | return 201 | } 202 | 203 | // read header 204 | r.dbf.Seek(4, io.SeekStart) 205 | binary.Read(r.dbf, binary.LittleEndian, &r.dbfNumRecords) 206 | binary.Read(r.dbf, binary.LittleEndian, &r.dbfHeaderLength) 207 | binary.Read(r.dbf, binary.LittleEndian, &r.dbfRecordLength) 208 | 209 | r.dbf.Seek(20, io.SeekCurrent) // skip padding 210 | numFields := int(math.Floor(float64(r.dbfHeaderLength-33) / 32.0)) 211 | r.dbfFields = make([]Field, numFields) 212 | binary.Read(r.dbf, binary.LittleEndian, &r.dbfFields) 213 | return 214 | } 215 | 216 | // Fields returns a slice of Fields that are present in the 217 | // DBF table. 218 | func (r *Reader) Fields() []Field { 219 | r.openDbf() // make sure we have dbf file to read from 220 | return r.dbfFields 221 | } 222 | 223 | // Err returns the last non-EOF error encountered. 224 | func (r *Reader) Err() error { 225 | if r.err == io.EOF { 226 | return nil 227 | } 228 | return r.err 229 | } 230 | 231 | // AttributeCount returns number of records in the DBF table. 232 | func (r *Reader) AttributeCount() int { 233 | r.openDbf() // make sure we have a dbf file to read from 234 | return int(r.dbfNumRecords) 235 | } 236 | 237 | // ReadAttribute returns the attribute value at row for field in 238 | // the DBF table as a string. Both values starts at 0. 239 | func (r *Reader) ReadAttribute(row int, field int) string { 240 | r.openDbf() // make sure we have a dbf file to read from 241 | seekTo := 1 + int64(r.dbfHeaderLength) + (int64(row) * int64(r.dbfRecordLength)) 242 | for n := 0; n < field; n++ { 243 | seekTo += int64(r.dbfFields[n].Size) 244 | } 245 | r.dbf.Seek(seekTo, io.SeekStart) 246 | buf := make([]byte, r.dbfFields[field].Size) 247 | r.dbf.Read(buf) 248 | return strings.Trim(string(buf[:]), " ") 249 | } 250 | -------------------------------------------------------------------------------- /reader_test.go: -------------------------------------------------------------------------------- 1 | package shp 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "testing" 8 | ) 9 | 10 | func pointsEqual(a, b []float64) bool { 11 | if len(a) != len(b) { 12 | return false 13 | } 14 | for k, v := range a { 15 | if v != b[k] { 16 | return false 17 | } 18 | } 19 | return true 20 | } 21 | 22 | func getShapesFromFile(prefix string, t *testing.T) (shapes []Shape) { 23 | filename := prefix + ".shp" 24 | file, err := Open(filename) 25 | if err != nil { 26 | t.Fatal("Failed to open shapefile: " + filename + " (" + err.Error() + ")") 27 | } 28 | defer file.Close() 29 | 30 | for file.Next() { 31 | _, shape := file.Shape() 32 | shapes = append(shapes, shape) 33 | } 34 | if file.Err() != nil { 35 | t.Errorf("Error while getting shapes for %s: %v", prefix, file.Err()) 36 | } 37 | 38 | return shapes 39 | } 40 | 41 | type shapeGetterFunc func(string, *testing.T) []Shape 42 | 43 | type identityTestFunc func(*testing.T, [][]float64, []Shape) 44 | 45 | func testPoint(t *testing.T, points [][]float64, shapes []Shape) { 46 | for n, s := range shapes { 47 | p, ok := s.(*Point) 48 | if !ok { 49 | t.Fatal("Failed to type assert.") 50 | } 51 | if !pointsEqual([]float64{p.X, p.Y}, points[n]) { 52 | t.Error("Points did not match.") 53 | } 54 | } 55 | } 56 | 57 | func testPolyLine(t *testing.T, points [][]float64, shapes []Shape) { 58 | for n, s := range shapes { 59 | p, ok := s.(*PolyLine) 60 | if !ok { 61 | t.Fatal("Failed to type assert.") 62 | } 63 | for k, point := range p.Points { 64 | if !pointsEqual(points[n*3+k], []float64{point.X, point.Y}) { 65 | t.Error("Points did not match.") 66 | } 67 | } 68 | } 69 | } 70 | 71 | func testPolygon(t *testing.T, points [][]float64, shapes []Shape) { 72 | for n, s := range shapes { 73 | p, ok := s.(*Polygon) 74 | if !ok { 75 | t.Fatal("Failed to type assert.") 76 | } 77 | for k, point := range p.Points { 78 | if !pointsEqual(points[n*3+k], []float64{point.X, point.Y}) { 79 | t.Error("Points did not match.") 80 | } 81 | } 82 | } 83 | } 84 | 85 | func testMultiPoint(t *testing.T, points [][]float64, shapes []Shape) { 86 | for n, s := range shapes { 87 | p, ok := s.(*MultiPoint) 88 | if !ok { 89 | t.Fatal("Failed to type assert.") 90 | } 91 | for k, point := range p.Points { 92 | if !pointsEqual(points[n*3+k], []float64{point.X, point.Y}) { 93 | t.Error("Points did not match.") 94 | } 95 | } 96 | } 97 | } 98 | 99 | func testPointZ(t *testing.T, points [][]float64, shapes []Shape) { 100 | for n, s := range shapes { 101 | p, ok := s.(*PointZ) 102 | if !ok { 103 | t.Fatal("Failed to type assert.") 104 | } 105 | if !pointsEqual([]float64{p.X, p.Y, p.Z}, points[n]) { 106 | t.Error("Points did not match.") 107 | } 108 | } 109 | } 110 | 111 | func testPolyLineZ(t *testing.T, points [][]float64, shapes []Shape) { 112 | for n, s := range shapes { 113 | p, ok := s.(*PolyLineZ) 114 | if !ok { 115 | t.Fatal("Failed to type assert.") 116 | } 117 | for k, point := range p.Points { 118 | if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.ZArray[k]}) { 119 | t.Error("Points did not match.") 120 | } 121 | } 122 | } 123 | } 124 | 125 | func testPolygonZ(t *testing.T, points [][]float64, shapes []Shape) { 126 | for n, s := range shapes { 127 | p, ok := s.(*PolygonZ) 128 | if !ok { 129 | t.Fatal("Failed to type assert.") 130 | } 131 | for k, point := range p.Points { 132 | if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.ZArray[k]}) { 133 | t.Error("Points did not match.") 134 | } 135 | } 136 | } 137 | } 138 | 139 | func testMultiPointZ(t *testing.T, points [][]float64, shapes []Shape) { 140 | for n, s := range shapes { 141 | p, ok := s.(*MultiPointZ) 142 | if !ok { 143 | t.Fatal("Failed to type assert.") 144 | } 145 | for k, point := range p.Points { 146 | if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.ZArray[k]}) { 147 | t.Error("Points did not match.") 148 | } 149 | } 150 | } 151 | } 152 | 153 | func testPointM(t *testing.T, points [][]float64, shapes []Shape) { 154 | for n, s := range shapes { 155 | p, ok := s.(*PointM) 156 | if !ok { 157 | t.Fatal("Failed to type assert.") 158 | } 159 | if !pointsEqual([]float64{p.X, p.Y, p.M}, points[n]) { 160 | t.Error("Points did not match.") 161 | } 162 | } 163 | } 164 | 165 | func testPolyLineM(t *testing.T, points [][]float64, shapes []Shape) { 166 | for n, s := range shapes { 167 | p, ok := s.(*PolyLineM) 168 | if !ok { 169 | t.Fatal("Failed to type assert.") 170 | } 171 | for k, point := range p.Points { 172 | if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.MArray[k]}) { 173 | t.Error("Points did not match.") 174 | } 175 | } 176 | } 177 | } 178 | 179 | func testPolygonM(t *testing.T, points [][]float64, shapes []Shape) { 180 | for n, s := range shapes { 181 | p, ok := s.(*PolygonM) 182 | if !ok { 183 | t.Fatal("Failed to type assert.") 184 | } 185 | for k, point := range p.Points { 186 | if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.MArray[k]}) { 187 | t.Error("Points did not match.") 188 | } 189 | } 190 | } 191 | } 192 | 193 | func testMultiPointM(t *testing.T, points [][]float64, shapes []Shape) { 194 | for n, s := range shapes { 195 | p, ok := s.(*MultiPointM) 196 | if !ok { 197 | t.Fatal("Failed to type assert.") 198 | } 199 | for k, point := range p.Points { 200 | if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.MArray[k]}) { 201 | t.Error("Points did not match.") 202 | } 203 | } 204 | } 205 | } 206 | 207 | func testMultiPatch(t *testing.T, points [][]float64, shapes []Shape) { 208 | for n, s := range shapes { 209 | p, ok := s.(*MultiPatch) 210 | if !ok { 211 | t.Fatal("Failed to type assert.") 212 | } 213 | for k, point := range p.Points { 214 | if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.ZArray[k]}) { 215 | t.Error("Points did not match.") 216 | } 217 | } 218 | } 219 | } 220 | 221 | func testshapeIdentity(t *testing.T, prefix string, getter shapeGetterFunc) { 222 | shapes := getter(prefix, t) 223 | d := dataForReadTests[prefix] 224 | if len(shapes) != d.count { 225 | t.Errorf("Number of shapes for %s read was wrong. Wanted %d, got %d.", prefix, d.count, len(shapes)) 226 | } 227 | d.tester(t, d.points, shapes) 228 | } 229 | 230 | func TestReadBBox(t *testing.T) { 231 | tests := []struct { 232 | filename string 233 | want Box 234 | }{ 235 | {"test_files/multipatch.shp", Box{0, 0, 10, 10}}, 236 | {"test_files/multipoint.shp", Box{0, 5, 10, 10}}, 237 | {"test_files/multipointm.shp", Box{0, 5, 10, 10}}, 238 | {"test_files/multipointz.shp", Box{0, 5, 10, 10}}, 239 | {"test_files/point.shp", Box{0, 5, 10, 10}}, 240 | {"test_files/pointm.shp", Box{0, 5, 10, 10}}, 241 | {"test_files/pointz.shp", Box{0, 5, 10, 10}}, 242 | {"test_files/polygon.shp", Box{0, 0, 5, 5}}, 243 | {"test_files/polygonm.shp", Box{0, 0, 5, 5}}, 244 | {"test_files/polygonz.shp", Box{0, 0, 5, 5}}, 245 | {"test_files/polyline.shp", Box{0, 0, 25, 25}}, 246 | {"test_files/polylinem.shp", Box{0, 0, 25, 25}}, 247 | {"test_files/polylinez.shp", Box{0, 0, 25, 25}}, 248 | } 249 | for _, tt := range tests { 250 | r, err := Open(tt.filename) 251 | if err != nil { 252 | t.Fatalf("%v", err) 253 | } 254 | if got := r.BBox().MinX; got != tt.want.MinX { 255 | t.Errorf("got MinX = %v, want %v", got, tt.want.MinX) 256 | } 257 | if got := r.BBox().MinY; got != tt.want.MinY { 258 | t.Errorf("got MinY = %v, want %v", got, tt.want.MinY) 259 | } 260 | if got := r.BBox().MaxX; got != tt.want.MaxX { 261 | t.Errorf("got MaxX = %v, want %v", got, tt.want.MaxX) 262 | } 263 | if got := r.BBox().MaxY; got != tt.want.MaxY { 264 | t.Errorf("got MaxY = %v, want %v", got, tt.want.MaxY) 265 | } 266 | } 267 | } 268 | 269 | type testCaseData struct { 270 | points [][]float64 271 | tester identityTestFunc 272 | count int 273 | } 274 | 275 | var dataForReadTests = map[string]testCaseData{ 276 | "test_files/polygonm": { 277 | points: [][]float64{ 278 | {0, 0, 0}, 279 | {0, 5, 5}, 280 | {5, 5, 10}, 281 | {5, 0, 15}, 282 | {0, 0, 0}, 283 | }, 284 | tester: testPolygonM, 285 | count: 1, 286 | }, 287 | "test_files/multipointm": { 288 | points: [][]float64{ 289 | {10, 10, 100}, 290 | {5, 5, 50}, 291 | {0, 10, 75}, 292 | }, 293 | tester: testMultiPointM, 294 | count: 1, 295 | }, 296 | "test_files/multipatch": { 297 | points: [][]float64{ 298 | {0, 0, 0}, 299 | {10, 0, 0}, 300 | {10, 10, 0}, 301 | {0, 10, 0}, 302 | {0, 0, 0}, 303 | {0, 10, 0}, 304 | {0, 10, 10}, 305 | {0, 0, 10}, 306 | {0, 0, 0}, 307 | {0, 10, 0}, 308 | {10, 0, 0}, 309 | {10, 0, 10}, 310 | {10, 10, 10}, 311 | {10, 10, 0}, 312 | {10, 0, 0}, 313 | {0, 0, 0}, 314 | {0, 0, 10}, 315 | {10, 0, 10}, 316 | {10, 0, 0}, 317 | {0, 0, 0}, 318 | {10, 10, 0}, 319 | {10, 10, 10}, 320 | {0, 10, 10}, 321 | {0, 10, 0}, 322 | {10, 10, 0}, 323 | {0, 0, 10}, 324 | {0, 10, 10}, 325 | {10, 10, 10}, 326 | {10, 0, 10}, 327 | {0, 0, 10}, 328 | }, 329 | tester: testMultiPatch, 330 | count: 1, 331 | }, 332 | "test_files/point": { 333 | points: [][]float64{ 334 | {10, 10}, 335 | {5, 5}, 336 | {0, 10}, 337 | }, 338 | tester: testPoint, 339 | count: 3, 340 | }, 341 | "test_files/polyline": { 342 | points: [][]float64{ 343 | {0, 0}, 344 | {5, 5}, 345 | {10, 10}, 346 | {15, 15}, 347 | {20, 20}, 348 | {25, 25}, 349 | }, 350 | tester: testPolyLine, 351 | count: 2, 352 | }, 353 | "test_files/polygon": { 354 | points: [][]float64{ 355 | {0, 0}, 356 | {0, 5}, 357 | {5, 5}, 358 | {5, 0}, 359 | {0, 0}, 360 | }, 361 | tester: testPolygon, 362 | count: 1, 363 | }, 364 | "test_files/multipoint": { 365 | points: [][]float64{ 366 | {10, 10}, 367 | {5, 5}, 368 | {0, 10}, 369 | }, 370 | tester: testMultiPoint, 371 | count: 1, 372 | }, 373 | "test_files/pointz": { 374 | points: [][]float64{ 375 | {10, 10, 100}, 376 | {5, 5, 50}, 377 | {0, 10, 75}, 378 | }, 379 | tester: testPointZ, 380 | count: 3, 381 | }, 382 | "test_files/polylinez": { 383 | points: [][]float64{ 384 | {0, 0, 0}, 385 | {5, 5, 5}, 386 | {10, 10, 10}, 387 | {15, 15, 15}, 388 | {20, 20, 20}, 389 | {25, 25, 25}, 390 | }, 391 | tester: testPolyLineZ, 392 | count: 2, 393 | }, 394 | "test_files/polygonz": { 395 | points: [][]float64{ 396 | {0, 0, 0}, 397 | {0, 5, 5}, 398 | {5, 5, 10}, 399 | {5, 0, 15}, 400 | {0, 0, 0}, 401 | }, 402 | tester: testPolygonZ, 403 | count: 1, 404 | }, 405 | "test_files/multipointz": { 406 | points: [][]float64{ 407 | {10, 10, 100}, 408 | {5, 5, 50}, 409 | {0, 10, 75}, 410 | }, 411 | tester: testMultiPointZ, 412 | count: 1, 413 | }, 414 | "test_files/pointm": { 415 | points: [][]float64{ 416 | {10, 10, 100}, 417 | {5, 5, 50}, 418 | {0, 10, 75}, 419 | }, 420 | tester: testPointM, 421 | count: 3, 422 | }, 423 | "test_files/polylinem": { 424 | points: [][]float64{ 425 | {0, 0, 0}, 426 | {5, 5, 5}, 427 | {10, 10, 10}, 428 | {15, 15, 15}, 429 | {20, 20, 20}, 430 | {25, 25, 25}, 431 | }, 432 | tester: testPolyLineM, 433 | count: 2, 434 | }, 435 | } 436 | 437 | func TestReadPoint(t *testing.T) { 438 | testshapeIdentity(t, "test_files/point", getShapesFromFile) 439 | } 440 | 441 | func TestReadPolyLine(t *testing.T) { 442 | testshapeIdentity(t, "test_files/polyline", getShapesFromFile) 443 | } 444 | 445 | func TestReadPolygon(t *testing.T) { 446 | testshapeIdentity(t, "test_files/polygon", getShapesFromFile) 447 | } 448 | 449 | func TestReadMultiPoint(t *testing.T) { 450 | testshapeIdentity(t, "test_files/multipoint", getShapesFromFile) 451 | } 452 | 453 | func TestReadPointZ(t *testing.T) { 454 | testshapeIdentity(t, "test_files/pointz", getShapesFromFile) 455 | } 456 | 457 | func TestReadPolyLineZ(t *testing.T) { 458 | testshapeIdentity(t, "test_files/polylinez", getShapesFromFile) 459 | } 460 | 461 | func TestReadPolygonZ(t *testing.T) { 462 | testshapeIdentity(t, "test_files/polygonz", getShapesFromFile) 463 | } 464 | 465 | func TestReadMultiPointZ(t *testing.T) { 466 | testshapeIdentity(t, "test_files/multipointz", getShapesFromFile) 467 | } 468 | 469 | func TestReadPointM(t *testing.T) { 470 | testshapeIdentity(t, "test_files/pointm", getShapesFromFile) 471 | } 472 | 473 | func TestReadPolyLineM(t *testing.T) { 474 | testshapeIdentity(t, "test_files/polylinem", getShapesFromFile) 475 | } 476 | 477 | func TestReadPolygonM(t *testing.T) { 478 | testshapeIdentity(t, "test_files/polygonm", getShapesFromFile) 479 | } 480 | 481 | func TestReadMultiPointM(t *testing.T) { 482 | testshapeIdentity(t, "test_files/multipointm", getShapesFromFile) 483 | } 484 | 485 | func TestReadMultiPatch(t *testing.T) { 486 | testshapeIdentity(t, "test_files/multipatch", getShapesFromFile) 487 | } 488 | 489 | func newReadSeekCloser(b []byte) readSeekCloser { 490 | return struct { 491 | io.Closer 492 | io.ReadSeeker 493 | }{ 494 | ioutil.NopCloser(nil), 495 | bytes.NewReader(b), 496 | } 497 | } 498 | 499 | func TestReadInvalidShapeType(t *testing.T) { 500 | record := []byte{ 501 | 0, 0, 0, 0, 502 | 0, 0, 0, 0, 503 | 255, 255, 255, 255, // shape type 504 | } 505 | 506 | tests := []struct { 507 | r interface { 508 | Next() bool 509 | Err() error 510 | } 511 | name string 512 | }{ 513 | {&Reader{shp: newReadSeekCloser(record), filelength: int64(len(record))}, "reader"}, 514 | {&seqReader{shp: newReadSeekCloser(record), filelength: int64(len(record))}, "seqReader"}, 515 | } 516 | 517 | for _, test := range tests { 518 | t.Run(test.name, func(t *testing.T) { 519 | if test.r.Next() { 520 | t.Fatal("read unsupported shape type without stopping") 521 | } 522 | if test.r.Err() == nil { 523 | t.Fatal("read unsupported shape type without error") 524 | } 525 | }) 526 | } 527 | } 528 | -------------------------------------------------------------------------------- /sequentialreader.go: -------------------------------------------------------------------------------- 1 | package shp 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "math" 9 | "strings" 10 | ) 11 | 12 | // SequentialReader is the interface that allows reading shapes and attributes one after another. It also embeds io.Closer. 13 | type SequentialReader interface { 14 | // Close() frees the resources allocated by the SequentialReader. 15 | io.Closer 16 | 17 | // Next() tries to advance the reading by one shape and one attribute row 18 | // and returns true if the read operation could be performed without any 19 | // error. 20 | Next() bool 21 | 22 | // Shape returns the index and the last read shape. If the SequentialReader 23 | // encountered any errors, nil is returned for the Shape. 24 | Shape() (int, Shape) 25 | 26 | // Attribute returns the value of the n-th attribute in the current row. If 27 | // the SequentialReader encountered any errors, the empty string is 28 | // returned. 29 | Attribute(n int) string 30 | 31 | // Fields returns the fields of the database. If the SequentialReader 32 | // encountered any errors, nil is returned. 33 | Fields() []Field 34 | 35 | // Err returns the last non-EOF error encountered. 36 | Err() error 37 | } 38 | 39 | // Attributes returns all attributes of the shape that sr was last advanced to. 40 | func Attributes(sr SequentialReader) []string { 41 | if sr.Err() != nil { 42 | return nil 43 | } 44 | s := make([]string, len(sr.Fields())) 45 | for i := range s { 46 | s[i] = sr.Attribute(i) 47 | } 48 | return s 49 | } 50 | 51 | // AttributeCount returns the number of fields of the database. 52 | func AttributeCount(sr SequentialReader) int { 53 | return len(sr.Fields()) 54 | } 55 | 56 | // seqReader implements SequentialReader based on external io.ReadCloser 57 | // instances 58 | type seqReader struct { 59 | shp, dbf io.ReadCloser 60 | err error 61 | 62 | geometryType ShapeType 63 | bbox Box 64 | 65 | shape Shape 66 | num int32 67 | filelength int64 68 | 69 | dbfFields []Field 70 | dbfNumRecords int32 71 | dbfHeaderLength int16 72 | dbfRecordLength int16 73 | dbfRow []byte 74 | } 75 | 76 | // Read and parse headers in the Shapefile. This will fill out GeometryType, 77 | // filelength and bbox. 78 | func (sr *seqReader) readHeaders() { 79 | // contrary to Reader.readHeaders we cannot seek with the ReadCloser, so we 80 | // need to trust the filelength in the header 81 | 82 | er := &errReader{Reader: sr.shp} 83 | // shp headers 84 | io.CopyN(ioutil.Discard, er, 24) 85 | var l int32 86 | binary.Read(er, binary.BigEndian, &l) 87 | sr.filelength = int64(l) * 2 88 | io.CopyN(ioutil.Discard, er, 4) 89 | binary.Read(er, binary.LittleEndian, &sr.geometryType) 90 | sr.bbox.MinX = readFloat64(er) 91 | sr.bbox.MinY = readFloat64(er) 92 | sr.bbox.MaxX = readFloat64(er) 93 | sr.bbox.MaxY = readFloat64(er) 94 | io.CopyN(ioutil.Discard, er, 32) // skip four float64: Zmin, Zmax, Mmin, Max 95 | if er.e != nil { 96 | sr.err = fmt.Errorf("Error when reading SHP header: %v", er.e) 97 | return 98 | } 99 | 100 | // dbf header 101 | er = &errReader{Reader: sr.dbf} 102 | if sr.dbf == nil { 103 | return 104 | } 105 | io.CopyN(ioutil.Discard, er, 4) 106 | binary.Read(er, binary.LittleEndian, &sr.dbfNumRecords) 107 | binary.Read(er, binary.LittleEndian, &sr.dbfHeaderLength) 108 | binary.Read(er, binary.LittleEndian, &sr.dbfRecordLength) 109 | io.CopyN(ioutil.Discard, er, 20) // skip padding 110 | numFields := int(math.Floor(float64(sr.dbfHeaderLength-33) / 32.0)) 111 | sr.dbfFields = make([]Field, numFields) 112 | binary.Read(er, binary.LittleEndian, &sr.dbfFields) 113 | buf := make([]byte, 1) 114 | er.Read(buf[:]) 115 | if er.e != nil { 116 | sr.err = fmt.Errorf("Error when reading DBF header: %v", er.e) 117 | return 118 | } 119 | if buf[0] != 0x0d { 120 | sr.err = fmt.Errorf("Field descriptor array terminator not found") 121 | return 122 | } 123 | sr.dbfRow = make([]byte, sr.dbfRecordLength) 124 | } 125 | 126 | // Next implements a method of interface SequentialReader for seqReader. 127 | func (sr *seqReader) Next() bool { 128 | if sr.err != nil { 129 | return false 130 | } 131 | var num, size int32 132 | var shapetype ShapeType 133 | 134 | // read shape 135 | er := &errReader{Reader: sr.shp} 136 | binary.Read(er, binary.BigEndian, &num) 137 | binary.Read(er, binary.BigEndian, &size) 138 | binary.Read(er, binary.LittleEndian, &shapetype) 139 | 140 | if er.e != nil { 141 | if er.e != io.EOF { 142 | sr.err = fmt.Errorf("Error when reading shapefile header: %v", er.e) 143 | } else { 144 | sr.err = io.EOF 145 | } 146 | return false 147 | } 148 | sr.num = num 149 | var err error 150 | sr.shape, err = newShape(shapetype) 151 | if err != nil { 152 | sr.err = fmt.Errorf("Error decoding shape type: %v", err) 153 | return false 154 | } 155 | sr.shape.read(er) 156 | switch { 157 | case er.e == io.EOF: 158 | // io.EOF means end-of-file was reached gracefully after all 159 | // shape-internal reads succeeded, so it's not a reason stop 160 | // iterating over all shapes. 161 | er.e = nil 162 | case er.e != nil: 163 | sr.err = fmt.Errorf("Error while reading next shape: %v", er.e) 164 | return false 165 | } 166 | skipBytes := int64(size)*2 + 8 - er.n 167 | _, ce := io.CopyN(ioutil.Discard, er, skipBytes) 168 | if er.e != nil { 169 | sr.err = er.e 170 | return false 171 | } 172 | if ce != nil { 173 | sr.err = fmt.Errorf("Error when discarding bytes on sequential read: %v", ce) 174 | return false 175 | } 176 | if _, err := io.ReadFull(sr.dbf, sr.dbfRow); err != nil { 177 | sr.err = fmt.Errorf("Error when reading DBF row: %v", err) 178 | return false 179 | } 180 | if sr.dbfRow[0] != 0x20 && sr.dbfRow[0] != 0x2a { 181 | sr.err = fmt.Errorf("Attribute row %d starts with incorrect deletion indicator", num) 182 | } 183 | return sr.err == nil 184 | } 185 | 186 | // Shape implements a method of interface SequentialReader for seqReader. 187 | func (sr *seqReader) Shape() (int, Shape) { 188 | return int(sr.num) - 1, sr.shape 189 | } 190 | 191 | // Attribute implements a method of interface SequentialReader for seqReader. 192 | func (sr *seqReader) Attribute(n int) string { 193 | if sr.err != nil { 194 | return "" 195 | } 196 | start := 1 197 | f := 0 198 | for ; f < n; f++ { 199 | start += int(sr.dbfFields[f].Size) 200 | } 201 | s := string(sr.dbfRow[start : start+int(sr.dbfFields[f].Size)]) 202 | return strings.Trim(s, " ") 203 | } 204 | 205 | // Err returns the first non-EOF error that was encountered. 206 | func (sr *seqReader) Err() error { 207 | if sr.err == io.EOF { 208 | return nil 209 | } 210 | return sr.err 211 | } 212 | 213 | // Close closes the seqReader and free all the allocated resources. 214 | func (sr *seqReader) Close() error { 215 | if err := sr.shp.Close(); err != nil { 216 | return err 217 | } 218 | if err := sr.dbf.Close(); err != nil { 219 | return err 220 | } 221 | return nil 222 | } 223 | 224 | // Fields returns a slice of the fields that are present in the DBF table. 225 | func (sr *seqReader) Fields() []Field { 226 | return sr.dbfFields 227 | } 228 | 229 | // SequentialReaderFromExt returns a new SequentialReader that interprets shp 230 | // as a source of shapes whose attributes can be retrieved from dbf. 231 | func SequentialReaderFromExt(shp, dbf io.ReadCloser) SequentialReader { 232 | sr := &seqReader{shp: shp, dbf: dbf} 233 | sr.readHeaders() 234 | return sr 235 | } 236 | -------------------------------------------------------------------------------- /sequentialreader_test.go: -------------------------------------------------------------------------------- 1 | package shp 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func openFile(name string, t *testing.T) *os.File { 9 | f, err := os.Open(name) 10 | if err != nil { 11 | t.Fatalf("Failed to open %s: %v", name, err) 12 | } 13 | return f 14 | } 15 | 16 | func getShapesSequentially(prefix string, t *testing.T) (shapes []Shape) { 17 | shp := openFile(prefix+".shp", t) 18 | dbf := openFile(prefix+".dbf", t) 19 | 20 | sr := SequentialReaderFromExt(shp, dbf) 21 | if err := sr.Err(); err != nil { 22 | t.Fatalf("Error when iterating over the shapefile header: %v", err) 23 | } 24 | for sr.Next() { 25 | _, shape := sr.Shape() 26 | shapes = append(shapes, shape) 27 | } 28 | if err := sr.Err(); err != nil { 29 | t.Errorf("Error when iterating over the shapes: %v", err) 30 | } 31 | 32 | if err := sr.Close(); err != nil { 33 | t.Errorf("Could not close sequential reader: %v", err) 34 | } 35 | return shapes 36 | } 37 | 38 | func TestSequentialReader(t *testing.T) { 39 | for prefix := range dataForReadTests { 40 | t.Logf("Testing sequential read for %s", prefix) 41 | testshapeIdentity(t, prefix, getShapesSequentially) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /shapefile.go: -------------------------------------------------------------------------------- 1 | package shp 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | //go:generate stringer -type=ShapeType 10 | 11 | // ShapeType is a identifier for the the type of shapes. 12 | type ShapeType int32 13 | 14 | // These are the possible shape types. 15 | const ( 16 | NULL ShapeType = 0 17 | POINT ShapeType = 1 18 | POLYLINE ShapeType = 3 19 | POLYGON ShapeType = 5 20 | MULTIPOINT ShapeType = 8 21 | POINTZ ShapeType = 11 22 | POLYLINEZ ShapeType = 13 23 | POLYGONZ ShapeType = 15 24 | MULTIPOINTZ ShapeType = 18 25 | POINTM ShapeType = 21 26 | POLYLINEM ShapeType = 23 27 | POLYGONM ShapeType = 25 28 | MULTIPOINTM ShapeType = 28 29 | MULTIPATCH ShapeType = 31 30 | ) 31 | 32 | // Box structure made up from four coordinates. This type 33 | // is used to represent bounding boxes 34 | type Box struct { 35 | MinX, MinY, MaxX, MaxY float64 36 | } 37 | 38 | // Extend extends the box with coordinates from the provided 39 | // box. This method calls Box.ExtendWithPoint twice with 40 | // {MinX, MinY} and {MaxX, MaxY} 41 | func (b *Box) Extend(box Box) { 42 | b.ExtendWithPoint(Point{box.MinX, box.MinY}) 43 | b.ExtendWithPoint(Point{box.MaxX, box.MaxY}) 44 | } 45 | 46 | // ExtendWithPoint extends box with coordinates from point 47 | // if they are outside the range of the current box. 48 | func (b *Box) ExtendWithPoint(p Point) { 49 | if p.X < b.MinX { 50 | b.MinX = p.X 51 | } 52 | if p.Y < b.MinY { 53 | b.MinY = p.Y 54 | } 55 | if p.X > b.MaxX { 56 | b.MaxX = p.X 57 | } 58 | if p.Y > b.MaxY { 59 | b.MaxY = p.Y 60 | } 61 | } 62 | 63 | // BBoxFromPoints returns the bounding box calculated 64 | // from points. 65 | func BBoxFromPoints(points []Point) (box Box) { 66 | for k, p := range points { 67 | if k == 0 { 68 | box = Box{p.X, p.Y, p.X, p.Y} 69 | } else { 70 | box.ExtendWithPoint(p) 71 | } 72 | } 73 | return 74 | } 75 | 76 | // Shape interface 77 | type Shape interface { 78 | BBox() Box 79 | 80 | read(io.Reader) 81 | write(io.Writer) 82 | } 83 | 84 | // Null is an empty shape. 85 | type Null struct { 86 | } 87 | 88 | // BBox Returns an empty BBox at the geometry origin. 89 | func (n Null) BBox() Box { 90 | return Box{0.0, 0.0, 0.0, 0.0} 91 | } 92 | 93 | func (n *Null) read(file io.Reader) { 94 | binary.Read(file, binary.LittleEndian, n) 95 | } 96 | 97 | func (n *Null) write(file io.Writer) { 98 | binary.Write(file, binary.LittleEndian, n) 99 | } 100 | 101 | // Point is the shape that consists of single a geometry point. 102 | type Point struct { 103 | X, Y float64 104 | } 105 | 106 | // BBox returns the bounding box of the Point feature, i.e. an empty area at 107 | // the point location itself. 108 | func (p Point) BBox() Box { 109 | return Box{p.X, p.Y, p.X, p.Y} 110 | } 111 | 112 | func (p *Point) read(file io.Reader) { 113 | binary.Read(file, binary.LittleEndian, p) 114 | } 115 | 116 | func (p *Point) write(file io.Writer) { 117 | binary.Write(file, binary.LittleEndian, p) 118 | } 119 | 120 | func flatten(points [][]Point) []Point { 121 | n, i := 0, 0 122 | for _, v := range points { 123 | n += len(v) 124 | } 125 | r := make([]Point, n) 126 | for _, v := range points { 127 | for _, p := range v { 128 | r[i] = p 129 | i++ 130 | } 131 | } 132 | return r 133 | } 134 | 135 | // PolyLine is a shape type that consists of an ordered set of vertices that 136 | // consists of one or more parts. A part is a connected sequence of two ore 137 | // more points. Parts may or may not be connected to another and may or may not 138 | // intersect each other. 139 | type PolyLine struct { 140 | Box 141 | NumParts int32 142 | NumPoints int32 143 | Parts []int32 144 | Points []Point 145 | } 146 | 147 | // NewPolyLine returns a pointer a new PolyLine created 148 | // with the provided points. The inner slice should be 149 | // the points that the parent part consists of. 150 | func NewPolyLine(parts [][]Point) *PolyLine { 151 | points := flatten(parts) 152 | 153 | p := &PolyLine{} 154 | p.NumParts = int32(len(parts)) 155 | p.NumPoints = int32(len(points)) 156 | p.Parts = make([]int32, len(parts)) 157 | var marker int32 158 | for i, part := range parts { 159 | p.Parts[i] = marker 160 | marker += int32(len(part)) 161 | } 162 | p.Points = points 163 | p.Box = p.BBox() 164 | 165 | return p 166 | } 167 | 168 | // BBox returns the bounding box of the PolyLine feature 169 | func (p PolyLine) BBox() Box { 170 | return BBoxFromPoints(p.Points) 171 | } 172 | 173 | func (p *PolyLine) read(file io.Reader) { 174 | binary.Read(file, binary.LittleEndian, &p.Box) 175 | binary.Read(file, binary.LittleEndian, &p.NumParts) 176 | binary.Read(file, binary.LittleEndian, &p.NumPoints) 177 | p.Parts = make([]int32, p.NumParts) 178 | p.Points = make([]Point, p.NumPoints) 179 | binary.Read(file, binary.LittleEndian, &p.Parts) 180 | binary.Read(file, binary.LittleEndian, &p.Points) 181 | } 182 | 183 | func (p *PolyLine) write(file io.Writer) { 184 | binary.Write(file, binary.LittleEndian, p.Box) 185 | binary.Write(file, binary.LittleEndian, p.NumParts) 186 | binary.Write(file, binary.LittleEndian, p.NumPoints) 187 | binary.Write(file, binary.LittleEndian, p.Parts) 188 | binary.Write(file, binary.LittleEndian, p.Points) 189 | } 190 | 191 | // Polygon is identical to the PolyLine struct. However the parts must form 192 | // rings that may not intersect. 193 | type Polygon PolyLine 194 | 195 | // BBox returns the bounding box of the Polygon feature 196 | func (p Polygon) BBox() Box { 197 | return BBoxFromPoints(p.Points) 198 | } 199 | 200 | func (p *Polygon) read(file io.Reader) { 201 | binary.Read(file, binary.LittleEndian, &p.Box) 202 | binary.Read(file, binary.LittleEndian, &p.NumParts) 203 | binary.Read(file, binary.LittleEndian, &p.NumPoints) 204 | p.Parts = make([]int32, p.NumParts) 205 | p.Points = make([]Point, p.NumPoints) 206 | binary.Read(file, binary.LittleEndian, &p.Parts) 207 | binary.Read(file, binary.LittleEndian, &p.Points) 208 | } 209 | 210 | func (p *Polygon) write(file io.Writer) { 211 | binary.Write(file, binary.LittleEndian, p.Box) 212 | binary.Write(file, binary.LittleEndian, p.NumParts) 213 | binary.Write(file, binary.LittleEndian, p.NumPoints) 214 | binary.Write(file, binary.LittleEndian, p.Parts) 215 | binary.Write(file, binary.LittleEndian, p.Points) 216 | } 217 | 218 | // MultiPoint is the shape that consists of multiple points. 219 | type MultiPoint struct { 220 | Box Box 221 | NumPoints int32 222 | Points []Point 223 | } 224 | 225 | // BBox returns the bounding box of the MultiPoint feature 226 | func (p MultiPoint) BBox() Box { 227 | return BBoxFromPoints(p.Points) 228 | } 229 | 230 | func (p *MultiPoint) read(file io.Reader) { 231 | binary.Read(file, binary.LittleEndian, &p.Box) 232 | binary.Read(file, binary.LittleEndian, &p.NumPoints) 233 | p.Points = make([]Point, p.NumPoints) 234 | binary.Read(file, binary.LittleEndian, &p.Points) 235 | } 236 | 237 | func (p *MultiPoint) write(file io.Writer) { 238 | binary.Write(file, binary.LittleEndian, p.Box) 239 | binary.Write(file, binary.LittleEndian, p.NumPoints) 240 | binary.Write(file, binary.LittleEndian, p.Points) 241 | } 242 | 243 | // PointZ is a triplet of double precision coordinates plus a measure. 244 | type PointZ struct { 245 | X float64 246 | Y float64 247 | Z float64 248 | M float64 249 | } 250 | 251 | // BBox eturns the bounding box of the PointZ feature which is an zero-sized area 252 | // at the X and Y coordinates of the feature. 253 | func (p PointZ) BBox() Box { 254 | return Box{p.X, p.Y, p.X, p.Y} 255 | } 256 | 257 | func (p *PointZ) read(file io.Reader) { 258 | binary.Read(file, binary.LittleEndian, p) 259 | } 260 | 261 | func (p *PointZ) write(file io.Writer) { 262 | binary.Write(file, binary.LittleEndian, p) 263 | } 264 | 265 | // PolyLineZ is a shape which consists of one or more parts. A part is a 266 | // connected sequence of two or more points. Parts may or may not be connected 267 | // and may or may not intersect one another. 268 | type PolyLineZ struct { 269 | Box Box 270 | NumParts int32 271 | NumPoints int32 272 | Parts []int32 273 | Points []Point 274 | ZRange [2]float64 275 | ZArray []float64 276 | MRange [2]float64 277 | MArray []float64 278 | } 279 | 280 | // BBox eturns the bounding box of the PolyLineZ feature. 281 | func (p PolyLineZ) BBox() Box { 282 | return BBoxFromPoints(p.Points) 283 | } 284 | 285 | func (p *PolyLineZ) read(file io.Reader) { 286 | binary.Read(file, binary.LittleEndian, &p.Box) 287 | binary.Read(file, binary.LittleEndian, &p.NumParts) 288 | binary.Read(file, binary.LittleEndian, &p.NumPoints) 289 | p.Parts = make([]int32, p.NumParts) 290 | p.Points = make([]Point, p.NumPoints) 291 | p.ZArray = make([]float64, p.NumPoints) 292 | p.MArray = make([]float64, p.NumPoints) 293 | binary.Read(file, binary.LittleEndian, &p.Parts) 294 | binary.Read(file, binary.LittleEndian, &p.Points) 295 | binary.Read(file, binary.LittleEndian, &p.ZRange) 296 | binary.Read(file, binary.LittleEndian, &p.ZArray) 297 | binary.Read(file, binary.LittleEndian, &p.MRange) 298 | binary.Read(file, binary.LittleEndian, &p.MArray) 299 | } 300 | 301 | func (p *PolyLineZ) write(file io.Writer) { 302 | binary.Write(file, binary.LittleEndian, p.Box) 303 | binary.Write(file, binary.LittleEndian, p.NumParts) 304 | binary.Write(file, binary.LittleEndian, p.NumPoints) 305 | binary.Write(file, binary.LittleEndian, p.Parts) 306 | binary.Write(file, binary.LittleEndian, p.Points) 307 | binary.Write(file, binary.LittleEndian, p.ZRange) 308 | binary.Write(file, binary.LittleEndian, p.ZArray) 309 | binary.Write(file, binary.LittleEndian, p.MRange) 310 | binary.Write(file, binary.LittleEndian, p.MArray) 311 | } 312 | 313 | // PolygonZ structure is identical to the PolyLineZ structure. 314 | type PolygonZ PolyLineZ 315 | 316 | // BBox returns the bounding box of the PolygonZ feature 317 | func (p PolygonZ) BBox() Box { 318 | return BBoxFromPoints(p.Points) 319 | } 320 | 321 | func (p *PolygonZ) read(file io.Reader) { 322 | binary.Read(file, binary.LittleEndian, &p.Box) 323 | binary.Read(file, binary.LittleEndian, &p.NumParts) 324 | binary.Read(file, binary.LittleEndian, &p.NumPoints) 325 | p.Parts = make([]int32, p.NumParts) 326 | p.Points = make([]Point, p.NumPoints) 327 | p.ZArray = make([]float64, p.NumPoints) 328 | p.MArray = make([]float64, p.NumPoints) 329 | binary.Read(file, binary.LittleEndian, &p.Parts) 330 | binary.Read(file, binary.LittleEndian, &p.Points) 331 | binary.Read(file, binary.LittleEndian, &p.ZRange) 332 | binary.Read(file, binary.LittleEndian, &p.ZArray) 333 | binary.Read(file, binary.LittleEndian, &p.MRange) 334 | binary.Read(file, binary.LittleEndian, &p.MArray) 335 | } 336 | 337 | func (p *PolygonZ) write(file io.Writer) { 338 | binary.Write(file, binary.LittleEndian, p.Box) 339 | binary.Write(file, binary.LittleEndian, p.NumParts) 340 | binary.Write(file, binary.LittleEndian, p.NumPoints) 341 | binary.Write(file, binary.LittleEndian, p.Parts) 342 | binary.Write(file, binary.LittleEndian, p.Points) 343 | binary.Write(file, binary.LittleEndian, p.ZRange) 344 | binary.Write(file, binary.LittleEndian, p.ZArray) 345 | binary.Write(file, binary.LittleEndian, p.MRange) 346 | binary.Write(file, binary.LittleEndian, p.MArray) 347 | } 348 | 349 | // MultiPointZ consists of one ore more PointZ. 350 | type MultiPointZ struct { 351 | Box Box 352 | NumPoints int32 353 | Points []Point 354 | ZRange [2]float64 355 | ZArray []float64 356 | MRange [2]float64 357 | MArray []float64 358 | } 359 | 360 | // BBox eturns the bounding box of the MultiPointZ feature. 361 | func (p MultiPointZ) BBox() Box { 362 | return BBoxFromPoints(p.Points) 363 | } 364 | 365 | func (p *MultiPointZ) read(file io.Reader) { 366 | binary.Read(file, binary.LittleEndian, &p.Box) 367 | binary.Read(file, binary.LittleEndian, &p.NumPoints) 368 | p.Points = make([]Point, p.NumPoints) 369 | p.ZArray = make([]float64, p.NumPoints) 370 | p.MArray = make([]float64, p.NumPoints) 371 | binary.Read(file, binary.LittleEndian, &p.Points) 372 | binary.Read(file, binary.LittleEndian, &p.ZRange) 373 | binary.Read(file, binary.LittleEndian, &p.ZArray) 374 | binary.Read(file, binary.LittleEndian, &p.MRange) 375 | binary.Read(file, binary.LittleEndian, &p.MArray) 376 | } 377 | 378 | func (p *MultiPointZ) write(file io.Writer) { 379 | binary.Write(file, binary.LittleEndian, p.Box) 380 | binary.Write(file, binary.LittleEndian, p.NumPoints) 381 | binary.Write(file, binary.LittleEndian, p.Points) 382 | binary.Write(file, binary.LittleEndian, p.ZRange) 383 | binary.Write(file, binary.LittleEndian, p.ZArray) 384 | binary.Write(file, binary.LittleEndian, p.MRange) 385 | binary.Write(file, binary.LittleEndian, p.MArray) 386 | } 387 | 388 | // PointM is a point with a measure. 389 | type PointM struct { 390 | X float64 391 | Y float64 392 | M float64 393 | } 394 | 395 | // BBox returns the bounding box of the PointM feature which is a zero-sized 396 | // area at the X- and Y-coordinates of the point. 397 | func (p PointM) BBox() Box { 398 | return Box{p.X, p.Y, p.X, p.Y} 399 | } 400 | 401 | func (p *PointM) read(file io.Reader) { 402 | binary.Read(file, binary.LittleEndian, p) 403 | } 404 | 405 | func (p *PointM) write(file io.Writer) { 406 | binary.Write(file, binary.LittleEndian, p) 407 | } 408 | 409 | // PolyLineM is the polyline in which each point also has a measure. 410 | type PolyLineM struct { 411 | Box Box 412 | NumParts int32 413 | NumPoints int32 414 | Parts []int32 415 | Points []Point 416 | MRange [2]float64 417 | MArray []float64 418 | } 419 | 420 | // BBox returns the bounding box of the PolyLineM feature. 421 | func (p PolyLineM) BBox() Box { 422 | return BBoxFromPoints(p.Points) 423 | } 424 | 425 | func (p *PolyLineM) read(file io.Reader) { 426 | binary.Read(file, binary.LittleEndian, &p.Box) 427 | binary.Read(file, binary.LittleEndian, &p.NumParts) 428 | binary.Read(file, binary.LittleEndian, &p.NumPoints) 429 | p.Parts = make([]int32, p.NumParts) 430 | p.Points = make([]Point, p.NumPoints) 431 | p.MArray = make([]float64, p.NumPoints) 432 | binary.Read(file, binary.LittleEndian, &p.Parts) 433 | binary.Read(file, binary.LittleEndian, &p.Points) 434 | binary.Read(file, binary.LittleEndian, &p.MRange) 435 | binary.Read(file, binary.LittleEndian, &p.MArray) 436 | } 437 | 438 | func (p *PolyLineM) write(file io.Writer) { 439 | binary.Write(file, binary.LittleEndian, p.Box) 440 | binary.Write(file, binary.LittleEndian, p.NumParts) 441 | binary.Write(file, binary.LittleEndian, p.NumPoints) 442 | binary.Write(file, binary.LittleEndian, p.Parts) 443 | binary.Write(file, binary.LittleEndian, p.Points) 444 | binary.Write(file, binary.LittleEndian, p.MRange) 445 | binary.Write(file, binary.LittleEndian, p.MArray) 446 | } 447 | 448 | // PolygonM structure is identical to the PolyLineZ structure. 449 | type PolygonM PolyLineZ 450 | 451 | // BBox returns the bounding box of the PolygonM feature. 452 | func (p PolygonM) BBox() Box { 453 | return BBoxFromPoints(p.Points) 454 | } 455 | 456 | func (p *PolygonM) read(file io.Reader) { 457 | binary.Read(file, binary.LittleEndian, &p.Box) 458 | binary.Read(file, binary.LittleEndian, &p.NumParts) 459 | binary.Read(file, binary.LittleEndian, &p.NumPoints) 460 | p.Parts = make([]int32, p.NumParts) 461 | p.Points = make([]Point, p.NumPoints) 462 | p.MArray = make([]float64, p.NumPoints) 463 | binary.Read(file, binary.LittleEndian, &p.Parts) 464 | binary.Read(file, binary.LittleEndian, &p.Points) 465 | binary.Read(file, binary.LittleEndian, &p.MRange) 466 | binary.Read(file, binary.LittleEndian, &p.MArray) 467 | } 468 | 469 | func (p *PolygonM) write(file io.Writer) { 470 | binary.Write(file, binary.LittleEndian, p.Box) 471 | binary.Write(file, binary.LittleEndian, p.NumParts) 472 | binary.Write(file, binary.LittleEndian, p.NumPoints) 473 | binary.Write(file, binary.LittleEndian, p.Parts) 474 | binary.Write(file, binary.LittleEndian, p.Points) 475 | binary.Write(file, binary.LittleEndian, p.MRange) 476 | binary.Write(file, binary.LittleEndian, p.MArray) 477 | } 478 | 479 | // MultiPointM is the collection of multiple points with measures. 480 | type MultiPointM struct { 481 | Box Box 482 | NumPoints int32 483 | Points []Point 484 | MRange [2]float64 485 | MArray []float64 486 | } 487 | 488 | // BBox eturns the bounding box of the MultiPointM feature 489 | func (p MultiPointM) BBox() Box { 490 | return BBoxFromPoints(p.Points) 491 | } 492 | 493 | func (p *MultiPointM) read(file io.Reader) { 494 | binary.Read(file, binary.LittleEndian, &p.Box) 495 | binary.Read(file, binary.LittleEndian, &p.NumPoints) 496 | p.Points = make([]Point, p.NumPoints) 497 | p.MArray = make([]float64, p.NumPoints) 498 | binary.Read(file, binary.LittleEndian, &p.Points) 499 | binary.Read(file, binary.LittleEndian, &p.MRange) 500 | binary.Read(file, binary.LittleEndian, &p.MArray) 501 | } 502 | 503 | func (p *MultiPointM) write(file io.Writer) { 504 | binary.Write(file, binary.LittleEndian, p.Box) 505 | binary.Write(file, binary.LittleEndian, p.NumPoints) 506 | binary.Write(file, binary.LittleEndian, p.Points) 507 | binary.Write(file, binary.LittleEndian, p.MRange) 508 | binary.Write(file, binary.LittleEndian, p.MArray) 509 | } 510 | 511 | // MultiPatch consists of a number of surfaces patches. Each surface path 512 | // descries a surface. The surface patches of a MultiPatch are referred to as 513 | // its parts, and the type of part controls how the order of vertices of an 514 | // MultiPatch part is interpreted. 515 | type MultiPatch struct { 516 | Box Box 517 | NumParts int32 518 | NumPoints int32 519 | Parts []int32 520 | PartTypes []int32 521 | Points []Point 522 | ZRange [2]float64 523 | ZArray []float64 524 | MRange [2]float64 525 | MArray []float64 526 | } 527 | 528 | // BBox returns the bounding box of the MultiPatch feature 529 | func (p MultiPatch) BBox() Box { 530 | return BBoxFromPoints(p.Points) 531 | } 532 | 533 | func (p *MultiPatch) read(file io.Reader) { 534 | binary.Read(file, binary.LittleEndian, &p.Box) 535 | binary.Read(file, binary.LittleEndian, &p.NumParts) 536 | binary.Read(file, binary.LittleEndian, &p.NumPoints) 537 | p.Parts = make([]int32, p.NumParts) 538 | p.PartTypes = make([]int32, p.NumParts) 539 | p.Points = make([]Point, p.NumPoints) 540 | p.ZArray = make([]float64, p.NumPoints) 541 | p.MArray = make([]float64, p.NumPoints) 542 | binary.Read(file, binary.LittleEndian, &p.Parts) 543 | binary.Read(file, binary.LittleEndian, &p.PartTypes) 544 | binary.Read(file, binary.LittleEndian, &p.Points) 545 | binary.Read(file, binary.LittleEndian, &p.ZRange) 546 | binary.Read(file, binary.LittleEndian, &p.ZArray) 547 | binary.Read(file, binary.LittleEndian, &p.MRange) 548 | binary.Read(file, binary.LittleEndian, &p.MArray) 549 | } 550 | 551 | func (p *MultiPatch) write(file io.Writer) { 552 | binary.Write(file, binary.LittleEndian, p.Box) 553 | binary.Write(file, binary.LittleEndian, p.NumParts) 554 | binary.Write(file, binary.LittleEndian, p.NumPoints) 555 | binary.Write(file, binary.LittleEndian, p.Parts) 556 | binary.Write(file, binary.LittleEndian, p.PartTypes) 557 | binary.Write(file, binary.LittleEndian, p.Points) 558 | binary.Write(file, binary.LittleEndian, p.ZRange) 559 | binary.Write(file, binary.LittleEndian, p.ZArray) 560 | binary.Write(file, binary.LittleEndian, p.MRange) 561 | binary.Write(file, binary.LittleEndian, p.MArray) 562 | } 563 | 564 | // Field representation of a field object in the DBF file 565 | type Field struct { 566 | Name [11]byte 567 | Fieldtype byte 568 | Addr [4]byte // not used 569 | Size uint8 570 | Precision uint8 571 | Padding [14]byte 572 | } 573 | 574 | // Returns a string representation of the Field. Currently 575 | // this only returns field name. 576 | func (f Field) String() string { 577 | return strings.TrimRight(string(f.Name[:]), "\x00") 578 | } 579 | 580 | // StringField returns a Field that can be used in SetFields to initialize the 581 | // DBF file. 582 | func StringField(name string, length uint8) Field { 583 | // TODO: Error checking 584 | field := Field{Fieldtype: 'C', Size: length} 585 | copy(field.Name[:], []byte(name)) 586 | return field 587 | } 588 | 589 | // NumberField returns a Field that can be used in SetFields to initialize the 590 | // DBF file. 591 | func NumberField(name string, length uint8) Field { 592 | field := Field{Fieldtype: 'N', Size: length} 593 | copy(field.Name[:], []byte(name)) 594 | return field 595 | } 596 | 597 | // FloatField returns a Field that can be used in SetFields to initialize the 598 | // DBF file. Used to store floating points with precision in the DBF. 599 | func FloatField(name string, length uint8, precision uint8) Field { 600 | field := Field{Fieldtype: 'F', Size: length, Precision: precision} 601 | copy(field.Name[:], []byte(name)) 602 | return field 603 | } 604 | 605 | // DateField feturns a Field that can be used in SetFields to initialize the 606 | // DBF file. Used to store Date strings formatted as YYYYMMDD. Data wise this 607 | // is the same as a StringField with length 8. 608 | func DateField(name string) Field { 609 | field := Field{Fieldtype: 'D', Size: 8} 610 | copy(field.Name[:], []byte(name)) 611 | return field 612 | } 613 | -------------------------------------------------------------------------------- /shapefile_test.go: -------------------------------------------------------------------------------- 1 | package shp 2 | 3 | import "testing" 4 | 5 | func TestBoxExtend(t *testing.T) { 6 | a := Box{-124.763068, 45.543541, -116.915989, 49.002494} 7 | b := Box{-92.888114, 42.49192, -86.805415, 47.080621} 8 | a.Extend(b) 9 | c := Box{-124.763068, 42.49192, -86.805415, 49.002494} 10 | if a.MinX != c.MinX { 11 | t.Errorf("a.MinX = %v, want %v", a.MinX, c.MinX) 12 | } 13 | if a.MinY != c.MinY { 14 | t.Errorf("a.MinY = %v, want %v", a.MinY, c.MinY) 15 | } 16 | if a.MaxX != c.MaxX { 17 | t.Errorf("a.MaxX = %v, want %v", a.MaxX, c.MaxX) 18 | } 19 | if a.MaxY != c.MaxY { 20 | t.Errorf("a.MaxY = %v, want %v", a.MaxY, c.MaxY) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /shapetype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=ShapeType"; DO NOT EDIT. 2 | 3 | package shp 4 | 5 | import "strconv" 6 | 7 | const _ShapeType_name = "NULLPOINTPOLYLINEPOLYGONMULTIPOINTPOINTZPOLYLINEZPOLYGONZMULTIPOINTZPOINTMPOLYLINEMPOLYGONMMULTIPOINTMMULTIPATCH" 8 | 9 | var _ShapeType_map = map[ShapeType]string{ 10 | 0: _ShapeType_name[0:4], 11 | 1: _ShapeType_name[4:9], 12 | 3: _ShapeType_name[9:17], 13 | 5: _ShapeType_name[17:24], 14 | 8: _ShapeType_name[24:34], 15 | 11: _ShapeType_name[34:40], 16 | 13: _ShapeType_name[40:49], 17 | 15: _ShapeType_name[49:57], 18 | 18: _ShapeType_name[57:68], 19 | 21: _ShapeType_name[68:74], 20 | 23: _ShapeType_name[74:83], 21 | 25: _ShapeType_name[83:91], 22 | 28: _ShapeType_name[91:102], 23 | 31: _ShapeType_name[102:112], 24 | } 25 | 26 | func (i ShapeType) String() string { 27 | if str, ok := _ShapeType_map[i]; ok { 28 | return str 29 | } 30 | return "ShapeType(" + strconv.FormatInt(int64(i), 10) + ")" 31 | } 32 | -------------------------------------------------------------------------------- /test_files/multipatch.dbf: -------------------------------------------------------------------------------- 1 | r AWmultipa_IDN  -------------------------------------------------------------------------------- /test_files/multipatch.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/multipatch.shp -------------------------------------------------------------------------------- /test_files/multipatch.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/multipatch.shx -------------------------------------------------------------------------------- /test_files/multipoint.dbf: -------------------------------------------------------------------------------- 1 | r AWmultipo_IDN  -------------------------------------------------------------------------------- /test_files/multipoint.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/multipoint.shp -------------------------------------------------------------------------------- /test_files/multipoint.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/multipoint.shx -------------------------------------------------------------------------------- /test_files/multipointm.dbf: -------------------------------------------------------------------------------- 1 | r AWmultipo_IDN  -------------------------------------------------------------------------------- /test_files/multipointm.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/multipointm.shp -------------------------------------------------------------------------------- /test_files/multipointm.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/multipointm.shx -------------------------------------------------------------------------------- /test_files/multipointz.dbf: -------------------------------------------------------------------------------- 1 | r AWmultipo_IDN  -------------------------------------------------------------------------------- /test_files/multipointz.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/multipointz.shp -------------------------------------------------------------------------------- /test_files/multipointz.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/multipointz.shx -------------------------------------------------------------------------------- /test_files/point.dbf: -------------------------------------------------------------------------------- 1 | r AWpoint_IDN  -------------------------------------------------------------------------------- /test_files/point.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/point.shp -------------------------------------------------------------------------------- /test_files/point.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/point.shx -------------------------------------------------------------------------------- /test_files/pointm.dbf: -------------------------------------------------------------------------------- 1 | r AWpointm_IDN  -------------------------------------------------------------------------------- /test_files/pointm.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/pointm.shp -------------------------------------------------------------------------------- /test_files/pointm.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/pointm.shx -------------------------------------------------------------------------------- /test_files/pointz.dbf: -------------------------------------------------------------------------------- 1 | r AWpointz_IDN  -------------------------------------------------------------------------------- /test_files/pointz.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/pointz.shp -------------------------------------------------------------------------------- /test_files/pointz.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/pointz.shx -------------------------------------------------------------------------------- /test_files/polygon.dbf: -------------------------------------------------------------------------------- 1 | r aWpolygon_IDNAREAN  -------------------------------------------------------------------------------- /test_files/polygon.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/polygon.shp -------------------------------------------------------------------------------- /test_files/polygon.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/polygon.shx -------------------------------------------------------------------------------- /test_files/polygonm.dbf: -------------------------------------------------------------------------------- 1 | r AWpolygon_IDN  -------------------------------------------------------------------------------- /test_files/polygonm.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/polygonm.shp -------------------------------------------------------------------------------- /test_files/polygonm.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/polygonm.shx -------------------------------------------------------------------------------- /test_files/polygonz.dbf: -------------------------------------------------------------------------------- 1 | r AWpolygon_IDN  -------------------------------------------------------------------------------- /test_files/polygonz.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/polygonz.shp -------------------------------------------------------------------------------- /test_files/polygonz.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/polygonz.shx -------------------------------------------------------------------------------- /test_files/polyline.dbf: -------------------------------------------------------------------------------- 1 | r AWpolylin_IDN  -------------------------------------------------------------------------------- /test_files/polyline.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/polyline.shp -------------------------------------------------------------------------------- /test_files/polyline.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/polyline.shx -------------------------------------------------------------------------------- /test_files/polylinem.dbf: -------------------------------------------------------------------------------- 1 | r AWpolylin_IDN  -------------------------------------------------------------------------------- /test_files/polylinem.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/polylinem.shp -------------------------------------------------------------------------------- /test_files/polylinem.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/polylinem.shx -------------------------------------------------------------------------------- /test_files/polylinez.dbf: -------------------------------------------------------------------------------- 1 | r AWpolylin_IDN  -------------------------------------------------------------------------------- /test_files/polylinez.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/polylinez.shp -------------------------------------------------------------------------------- /test_files/polylinez.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-p/go-shp/9fd306ae10a60713c10ab7d9412126642d0911c5/test_files/polylinez.shx -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package shp 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "math" 9 | "os" 10 | "path/filepath" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | // Writer is the type that is used to write a new shapefile. 16 | type Writer struct { 17 | filename string 18 | shp writeSeekCloser 19 | shx writeSeekCloser 20 | GeometryType ShapeType 21 | num int32 22 | bbox Box 23 | 24 | dbf writeSeekCloser 25 | dbfFields []Field 26 | dbfHeaderLength int16 27 | dbfRecordLength int16 28 | } 29 | 30 | type writeSeekCloser interface { 31 | io.Writer 32 | io.Seeker 33 | io.Closer 34 | } 35 | 36 | // Create returns a point to new Writer and the first error that was 37 | // encountered. In case an error occurred the returned Writer point will be nil 38 | // This also creates a corresponding SHX file. It is important to use Close() 39 | // when done because that method writes all the headers for each file (SHP, SHX 40 | // and DBF). 41 | // If filename does not end on ".shp" already, it will be treated as the basename 42 | // for the file and the ".shp" extension will be appended to that name. 43 | func Create(filename string, t ShapeType) (*Writer, error) { 44 | if strings.HasSuffix(strings.ToLower(filename), ".shp") { 45 | filename = filename[0 : len(filename)-4] 46 | } 47 | shp, err := os.Create(filename + ".shp") 48 | if err != nil { 49 | return nil, err 50 | } 51 | shx, err := os.Create(filename + ".shx") 52 | if err != nil { 53 | return nil, err 54 | } 55 | shp.Seek(100, io.SeekStart) 56 | shx.Seek(100, io.SeekStart) 57 | w := &Writer{ 58 | filename: filename, 59 | shp: shp, 60 | shx: shx, 61 | GeometryType: t, 62 | } 63 | return w, nil 64 | } 65 | 66 | // Append returns a Writer pointer that will append to the given shapefile and 67 | // the first error that was encounted during creation of that Writer. The 68 | // shapefile must have a valid index file. 69 | func Append(filename string) (*Writer, error) { 70 | shp, err := os.OpenFile(filename, os.O_RDWR, 0666) 71 | if err != nil { 72 | return nil, err 73 | } 74 | ext := filepath.Ext(filename) 75 | basename := filename[:len(filename)-len(ext)] 76 | w := &Writer{ 77 | filename: basename, 78 | shp: shp, 79 | } 80 | _, err = shp.Seek(32, io.SeekStart) 81 | if err != nil { 82 | return nil, fmt.Errorf("cannot seek to SHP geometry type: %v", err) 83 | } 84 | err = binary.Read(shp, binary.LittleEndian, &w.GeometryType) 85 | if err != nil { 86 | return nil, fmt.Errorf("cannot read geometry type: %v", err) 87 | } 88 | er := &errReader{Reader: shp} 89 | w.bbox.MinX = readFloat64(er) 90 | w.bbox.MinY = readFloat64(er) 91 | w.bbox.MaxX = readFloat64(er) 92 | w.bbox.MaxY = readFloat64(er) 93 | if er.e != nil { 94 | return nil, fmt.Errorf("cannot read bounding box: %v", er.e) 95 | } 96 | 97 | shx, err := os.OpenFile(basename+".shx", os.O_RDWR, 0666) 98 | if os.IsNotExist(err) { 99 | // TODO allow index file to not exist, in that case just 100 | // read through all the shapes and create it on the fly 101 | } 102 | if err != nil { 103 | return nil, fmt.Errorf("cannot open shapefile index: %v", err) 104 | } 105 | _, err = shx.Seek(-8, io.SeekEnd) 106 | if err != nil { 107 | return nil, fmt.Errorf("cannot seek to last shape index: %v", err) 108 | } 109 | var offset int32 110 | err = binary.Read(shx, binary.BigEndian, &offset) 111 | if err != nil { 112 | return nil, fmt.Errorf("cannot read last shape index: %v", err) 113 | } 114 | offset = offset * 2 115 | _, err = shp.Seek(int64(offset), io.SeekStart) 116 | if err != nil { 117 | return nil, fmt.Errorf("cannot seek to last shape: %v", err) 118 | } 119 | err = binary.Read(shp, binary.BigEndian, &w.num) 120 | if err != nil { 121 | return nil, fmt.Errorf("cannot read number of last shape: %v", err) 122 | } 123 | _, err = shp.Seek(0, io.SeekEnd) 124 | if err != nil { 125 | return nil, fmt.Errorf("cannot seek to SHP end: %v", err) 126 | } 127 | _, err = shx.Seek(0, io.SeekEnd) 128 | if err != nil { 129 | return nil, fmt.Errorf("cannot seek to SHX end: %v", err) 130 | } 131 | w.shx = shx 132 | 133 | dbf, err := os.Open(basename + ".dbf") 134 | if os.IsNotExist(err) { 135 | return w, nil // it's okay if the DBF does not exist 136 | } 137 | if err != nil { 138 | return nil, fmt.Errorf("cannot open DBF: %v", err) 139 | } 140 | 141 | _, err = dbf.Seek(8, io.SeekStart) 142 | if err != nil { 143 | return nil, fmt.Errorf("cannot seek in DBF: %v", err) 144 | } 145 | err = binary.Read(dbf, binary.LittleEndian, &w.dbfHeaderLength) 146 | if err != nil { 147 | return nil, fmt.Errorf("cannot read header length from DBF: %v", err) 148 | } 149 | err = binary.Read(dbf, binary.LittleEndian, &w.dbfRecordLength) 150 | if err != nil { 151 | return nil, fmt.Errorf("cannot read record length from DBF: %v", err) 152 | } 153 | 154 | _, err = dbf.Seek(20, io.SeekCurrent) // skip padding 155 | if err != nil { 156 | return nil, fmt.Errorf("cannot seek in DBF: %v", err) 157 | } 158 | numFields := int(math.Floor(float64(w.dbfHeaderLength-33) / 32.0)) 159 | w.dbfFields = make([]Field, numFields) 160 | err = binary.Read(dbf, binary.LittleEndian, &w.dbfFields) 161 | if err != nil { 162 | return nil, fmt.Errorf("cannot read number of fields from DBF: %v", err) 163 | } 164 | _, err = dbf.Seek(0, io.SeekEnd) // skip padding 165 | if err != nil { 166 | return nil, fmt.Errorf("cannot seek to DBF end: %v", err) 167 | } 168 | w.dbf = dbf 169 | 170 | return w, nil 171 | } 172 | 173 | // Write shape to the Shapefile. This also creates 174 | // a record in the SHX file and DBF file (if it is 175 | // initialized). Returns the index of the written object 176 | // which can be used in WriteAttribute. 177 | func (w *Writer) Write(shape Shape) int32 { 178 | // increate bbox 179 | if w.num == 0 { 180 | w.bbox = shape.BBox() 181 | } else { 182 | w.bbox.Extend(shape.BBox()) 183 | } 184 | 185 | w.num++ 186 | binary.Write(w.shp, binary.BigEndian, w.num) 187 | w.shp.Seek(4, io.SeekCurrent) 188 | start, _ := w.shp.Seek(0, io.SeekCurrent) 189 | binary.Write(w.shp, binary.LittleEndian, w.GeometryType) 190 | shape.write(w.shp) 191 | finish, _ := w.shp.Seek(0, io.SeekCurrent) 192 | length := int32(math.Floor((float64(finish) - float64(start)) / 2.0)) 193 | w.shp.Seek(start-4, io.SeekStart) 194 | binary.Write(w.shp, binary.BigEndian, length) 195 | w.shp.Seek(finish, io.SeekStart) 196 | 197 | // write shx 198 | binary.Write(w.shx, binary.BigEndian, int32((start-8)/2)) 199 | binary.Write(w.shx, binary.BigEndian, length) 200 | 201 | // write empty record to dbf 202 | if w.dbf != nil { 203 | w.writeEmptyRecord() 204 | } 205 | 206 | return w.num - 1 207 | } 208 | 209 | // Close closes the Writer. This must be used at the end of 210 | // the transaction because it writes the correct headers 211 | // to the SHP/SHX and DBF files before closing. 212 | func (w *Writer) Close() { 213 | w.writeHeader(w.shx) 214 | w.writeHeader(w.shp) 215 | w.shp.Close() 216 | w.shx.Close() 217 | 218 | if w.dbf == nil { 219 | w.SetFields([]Field{}) 220 | } 221 | w.writeDbfHeader(w.dbf) 222 | w.dbf.Close() 223 | } 224 | 225 | // writeHeader wrires SHP/SHX headers to ws. 226 | func (w *Writer) writeHeader(ws io.WriteSeeker) { 227 | filelength, _ := ws.Seek(0, io.SeekEnd) 228 | if filelength == 0 { 229 | filelength = 100 230 | } 231 | ws.Seek(0, io.SeekStart) 232 | // file code 233 | binary.Write(ws, binary.BigEndian, []int32{9994, 0, 0, 0, 0, 0}) 234 | // file length 235 | binary.Write(ws, binary.BigEndian, int32(filelength/2)) 236 | // version and shape type 237 | binary.Write(ws, binary.LittleEndian, []int32{1000, int32(w.GeometryType)}) 238 | // bounding box 239 | binary.Write(ws, binary.LittleEndian, w.bbox) 240 | // elevation, measure 241 | binary.Write(ws, binary.LittleEndian, []float64{0.0, 0.0, 0.0, 0.0}) 242 | } 243 | 244 | // writeDbfHeader writes a DBF header to ws. 245 | func (w *Writer) writeDbfHeader(ws io.WriteSeeker) { 246 | ws.Seek(0, 0) 247 | // version, year (YEAR-1990), month, day 248 | binary.Write(ws, binary.LittleEndian, []byte{3, 24, 5, 3}) 249 | // number of records 250 | binary.Write(ws, binary.LittleEndian, w.num) 251 | // header length, record length 252 | binary.Write(ws, binary.LittleEndian, []int16{w.dbfHeaderLength, w.dbfRecordLength}) 253 | // padding 254 | binary.Write(ws, binary.LittleEndian, make([]byte, 20)) 255 | 256 | for _, field := range w.dbfFields { 257 | binary.Write(ws, binary.LittleEndian, field) 258 | } 259 | 260 | // end with return 261 | ws.Write([]byte("\r")) 262 | } 263 | 264 | // SetFields sets field values in the DBF. This initializes the DBF file and 265 | // should be used prior to writing any attributes. 266 | func (w *Writer) SetFields(fields []Field) error { 267 | if w.dbf != nil { 268 | return errors.New("Cannot set fields in existing dbf") 269 | } 270 | 271 | var err error 272 | w.dbf, err = os.Create(w.filename + ".dbf") 273 | if err != nil { 274 | return fmt.Errorf("Failed to open %s.dbf: %v", w.filename, err) 275 | } 276 | w.dbfFields = fields 277 | 278 | // calculate record length 279 | w.dbfRecordLength = int16(1) 280 | for _, field := range w.dbfFields { 281 | w.dbfRecordLength += int16(field.Size) 282 | } 283 | 284 | // header lengh 285 | w.dbfHeaderLength = int16(len(w.dbfFields)*32 + 33) 286 | 287 | // fill header space with empty bytes for now 288 | buf := make([]byte, w.dbfHeaderLength) 289 | binary.Write(w.dbf, binary.LittleEndian, buf) 290 | 291 | // write empty records 292 | for n := int32(0); n < w.num; n++ { 293 | w.writeEmptyRecord() 294 | } 295 | return nil 296 | } 297 | 298 | // Writes an empty record to the end of the DBF. This 299 | // works by seeking to the end of the file and writing 300 | // dbfRecordLength number of bytes. The first byte is a 301 | // space that indicates a new record. 302 | func (w *Writer) writeEmptyRecord() { 303 | w.dbf.Seek(0, io.SeekEnd) 304 | buf := make([]byte, w.dbfRecordLength) 305 | buf[0] = ' ' 306 | binary.Write(w.dbf, binary.LittleEndian, buf) 307 | } 308 | 309 | // WriteAttribute writes value for field into the given row in the DBF. Row 310 | // number should be the same as the order the Shape was written to the 311 | // Shapefile. The field value corresponds to the field in the slice used in 312 | // SetFields. 313 | func (w *Writer) WriteAttribute(row int, field int, value interface{}) error { 314 | var buf []byte 315 | switch v := value.(type) { 316 | case int: 317 | buf = []byte(strconv.Itoa(v)) 318 | case float64: 319 | precision := w.dbfFields[field].Precision 320 | buf = []byte(strconv.FormatFloat(v, 'f', int(precision), 64)) 321 | case string: 322 | buf = []byte(v) 323 | default: 324 | return fmt.Errorf("Unsupported value type: %T", v) 325 | } 326 | 327 | if w.dbf == nil { 328 | return errors.New("Initialize DBF by using SetFields first") 329 | } 330 | if sz := int(w.dbfFields[field].Size); len(buf) > sz { 331 | return fmt.Errorf("Unable to write field %v: %q exceeds field length %v", field, buf, sz) 332 | } 333 | 334 | seekTo := 1 + int64(w.dbfHeaderLength) + (int64(row) * int64(w.dbfRecordLength)) 335 | for n := 0; n < field; n++ { 336 | seekTo += int64(w.dbfFields[n].Size) 337 | } 338 | w.dbf.Seek(seekTo, io.SeekStart) 339 | return binary.Write(w.dbf, binary.LittleEndian, buf) 340 | } 341 | 342 | // BBox returns the bounding box of the Writer. 343 | func (w *Writer) BBox() Box { 344 | return w.bbox 345 | } 346 | -------------------------------------------------------------------------------- /writer_test.go: -------------------------------------------------------------------------------- 1 | package shp 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | var filenamePrefix = "test_files/write_" 12 | 13 | func removeShapefile(filename string) { 14 | os.Remove(filename + ".shp") 15 | os.Remove(filename + ".shx") 16 | os.Remove(filename + ".dbf") 17 | } 18 | 19 | func pointsToFloats(points []Point) [][]float64 { 20 | floats := make([][]float64, len(points)) 21 | for k, v := range points { 22 | floats[k] = make([]float64, 2) 23 | floats[k][0] = v.X 24 | floats[k][1] = v.Y 25 | } 26 | return floats 27 | } 28 | 29 | func TestAppend(t *testing.T) { 30 | filename := filenamePrefix + "point" 31 | defer removeShapefile(filename) 32 | points := [][]float64{ 33 | {0.0, 0.0}, 34 | {5.0, 5.0}, 35 | {10.0, 10.0}, 36 | } 37 | 38 | shape, err := Create(filename+".shp", POINT) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | for _, p := range points { 43 | shape.Write(&Point{p[0], p[1]}) 44 | } 45 | wantNum := shape.num 46 | shape.Close() 47 | 48 | newPoints := [][]float64{ 49 | {15.0, 15.0}, 50 | {20.0, 20.0}, 51 | {25.0, 25.0}, 52 | } 53 | shape, err = Append(filename + ".shp") 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | if shape.GeometryType != POINT { 58 | t.Fatalf("wanted geo type %d, got %d", POINT, shape.GeometryType) 59 | } 60 | if shape.num != wantNum { 61 | t.Fatalf("wrong 'num', wanted type %d, got %d", wantNum, shape.num) 62 | } 63 | 64 | for _, p := range newPoints { 65 | shape.Write(&Point{p[0], p[1]}) 66 | } 67 | 68 | points = append(points, newPoints...) 69 | 70 | shapes := getShapesFromFile(filename, t) 71 | if len(shapes) != len(points) { 72 | t.Error("Number of shapes read was wrong") 73 | } 74 | testPoint(t, points, shapes) 75 | } 76 | 77 | func TestWritePoint(t *testing.T) { 78 | filename := filenamePrefix + "point" 79 | defer removeShapefile(filename) 80 | 81 | points := [][]float64{ 82 | {0.0, 0.0}, 83 | {5.0, 5.0}, 84 | {10.0, 10.0}, 85 | } 86 | 87 | shape, err := Create(filename+".shp", POINT) 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | for _, p := range points { 92 | shape.Write(&Point{p[0], p[1]}) 93 | } 94 | shape.Close() 95 | 96 | shapes := getShapesFromFile(filename, t) 97 | if len(shapes) != len(points) { 98 | t.Error("Number of shapes read was wrong") 99 | } 100 | testPoint(t, points, shapes) 101 | } 102 | 103 | func TestWritePolyLine(t *testing.T) { 104 | filename := filenamePrefix + "polyline" 105 | defer removeShapefile(filename) 106 | 107 | points := [][]Point{ 108 | {Point{0.0, 0.0}, Point{5.0, 5.0}}, 109 | {Point{10.0, 10.0}, Point{15.0, 15.0}}, 110 | } 111 | 112 | shape, err := Create(filename+".shp", POLYLINE) 113 | if err != nil { 114 | t.Log(shape, err) 115 | } 116 | 117 | l := NewPolyLine(points) 118 | 119 | lWant := &PolyLine{ 120 | Box: Box{MinX: 0, MinY: 0, MaxX: 15, MaxY: 15}, 121 | NumParts: 2, 122 | NumPoints: 4, 123 | Parts: []int32{0, 2}, 124 | Points: []Point{{X: 0, Y: 0}, 125 | {X: 5, Y: 5}, 126 | {X: 10, Y: 10}, 127 | {X: 15, Y: 15}, 128 | }, 129 | } 130 | if !reflect.DeepEqual(l, lWant) { 131 | t.Errorf("incorrect NewLine: have: %+v; want: %+v", l, lWant) 132 | } 133 | 134 | shape.Write(l) 135 | shape.Close() 136 | 137 | shapes := getShapesFromFile(filename, t) 138 | if len(shapes) != 1 { 139 | t.Error("Number of shapes read was wrong") 140 | } 141 | testPolyLine(t, pointsToFloats(flatten(points)), shapes) 142 | } 143 | 144 | type seekTracker struct { 145 | io.Writer 146 | offset int64 147 | } 148 | 149 | func (s *seekTracker) Seek(offset int64, whence int) (int64, error) { 150 | s.offset = offset 151 | return s.offset, nil 152 | } 153 | 154 | func (s *seekTracker) Close() error { 155 | return nil 156 | } 157 | 158 | func TestWriteAttribute(t *testing.T) { 159 | buf := new(bytes.Buffer) 160 | s := &seekTracker{Writer: buf} 161 | w := Writer{ 162 | dbf: s, 163 | dbfFields: []Field{ 164 | StringField("A_STRING", 6), 165 | FloatField("A_FLOAT", 8, 4), 166 | NumberField("AN_INT", 4), 167 | }, 168 | dbfRecordLength: 100, 169 | } 170 | 171 | tests := []struct { 172 | name string 173 | row int 174 | field int 175 | data interface{} 176 | wantOffset int64 177 | wantData string 178 | }{ 179 | {"string-0", 0, 0, "test", 1, "test"}, 180 | {"string-0-overflow-1", 0, 0, "overflo", 0, ""}, 181 | {"string-0-overflow-n", 0, 0, "overflowing", 0, ""}, 182 | {"string-3", 3, 0, "things", 301, "things"}, 183 | {"float-0", 0, 1, 123.44, 7, "123.4400"}, 184 | {"float-0-overflow-1", 0, 1, 1234.0, 0, ""}, 185 | {"float-0-overflow-n", 0, 1, 123456789.0, 0, ""}, 186 | {"int-0", 0, 2, 4242, 15, "4242"}, 187 | {"int-0-overflow-1", 0, 2, 42424, 0, ""}, 188 | {"int-0-overflow-n", 0, 2, 42424343, 0, ""}, 189 | } 190 | 191 | for _, test := range tests { 192 | t.Run(test.name, func(t *testing.T) { 193 | buf.Reset() 194 | s.offset = 0 195 | 196 | err := w.WriteAttribute(test.row, test.field, test.data) 197 | 198 | if buf.String() != test.wantData { 199 | t.Errorf("got data: %v, want: %v", buf.String(), test.wantData) 200 | } 201 | if s.offset != test.wantOffset { 202 | t.Errorf("got seek offset: %v, want: %v", s.offset, test.wantOffset) 203 | } 204 | if err == nil && test.wantData == "" { 205 | t.Error("got no data and no error") 206 | } 207 | }) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /zipreader.go: -------------------------------------------------------------------------------- 1 | package shp 2 | 3 | import ( 4 | "archive/zip" 5 | "fmt" 6 | "io" 7 | "path" 8 | "strings" 9 | ) 10 | 11 | // ZipReader provides an interface for reading Shapefiles that are compressed in a ZIP archive. 12 | type ZipReader struct { 13 | sr SequentialReader 14 | z *zip.ReadCloser 15 | } 16 | 17 | // openFromZIP is convenience function for opening the file called name that is 18 | // compressed in z for reading. 19 | func openFromZIP(z *zip.ReadCloser, name string) (io.ReadCloser, error) { 20 | for _, f := range z.File { 21 | if f.Name == name { 22 | return f.Open() 23 | 24 | } 25 | } 26 | return nil, fmt.Errorf("No such file in archive: %s", name) 27 | } 28 | 29 | // OpenZip opens a ZIP file that contains a single shapefile. 30 | func OpenZip(zipFilePath string) (*ZipReader, error) { 31 | z, err := zip.OpenReader(zipFilePath) 32 | if err != nil { 33 | return nil, err 34 | } 35 | zr := &ZipReader{ 36 | z: z, 37 | } 38 | shapeFiles := shapesInZip(z) 39 | if len(shapeFiles) == 0 { 40 | return nil, fmt.Errorf("archive does not contain a .shp file") 41 | } 42 | if len(shapeFiles) > 1 { 43 | return nil, fmt.Errorf("archive does contain multiple .shp files") 44 | } 45 | 46 | shp, err := openFromZIP(zr.z, shapeFiles[0].Name) 47 | if err != nil { 48 | return nil, err 49 | } 50 | withoutExt := strings.TrimSuffix(shapeFiles[0].Name, ".shp") 51 | // dbf is optional, so no error checking here 52 | dbf, _ := openFromZIP(zr.z, withoutExt+".dbf") 53 | zr.sr = SequentialReaderFromExt(shp, dbf) 54 | return zr, nil 55 | } 56 | 57 | // ShapesInZip returns a string-slice with the names (i.e. relatives paths in 58 | // archive file tree) of all shapes that are in the ZIP archive at zipFilePath. 59 | func ShapesInZip(zipFilePath string) ([]string, error) { 60 | var names []string 61 | z, err := zip.OpenReader(zipFilePath) 62 | if err != nil { 63 | return nil, err 64 | } 65 | shapeFiles := shapesInZip(z) 66 | for i := range shapeFiles { 67 | names = append(names, shapeFiles[i].Name) 68 | } 69 | return names, nil 70 | } 71 | 72 | func shapesInZip(z *zip.ReadCloser) []*zip.File { 73 | var shapeFiles []*zip.File 74 | for _, f := range z.File { 75 | if strings.HasSuffix(f.Name, ".shp") { 76 | shapeFiles = append(shapeFiles, f) 77 | } 78 | } 79 | return shapeFiles 80 | } 81 | 82 | // OpenShapeFromZip opens a shape file that is contained in a ZIP archive. The 83 | // parameter name is name of the shape file. 84 | // The name of the shapefile must be a relative path: it must not start with a 85 | // drive letter (e.g. C:) or leading slash, and only forward slashes are 86 | // allowed. These rules are the same as in 87 | // https://golang.org/pkg/archive/zip/#FileHeader. 88 | func OpenShapeFromZip(zipFilePath string, name string) (*ZipReader, error) { 89 | z, err := zip.OpenReader(zipFilePath) 90 | if err != nil { 91 | return nil, err 92 | } 93 | zr := &ZipReader{ 94 | z: z, 95 | } 96 | 97 | shp, err := openFromZIP(zr.z, name) 98 | if err != nil { 99 | return nil, err 100 | } 101 | // dbf is optional, so no error checking here 102 | prefix := strings.TrimSuffix(name, path.Ext(name)) 103 | dbf, _ := openFromZIP(zr.z, prefix+".dbf") 104 | zr.sr = SequentialReaderFromExt(shp, dbf) 105 | return zr, nil 106 | } 107 | 108 | // Close closes the ZipReader and frees the allocated resources. 109 | func (zr *ZipReader) Close() error { 110 | s := "" 111 | err := zr.sr.Close() 112 | if err != nil { 113 | s += err.Error() + ". " 114 | } 115 | err = zr.z.Close() 116 | if err != nil { 117 | s += err.Error() + ". " 118 | } 119 | if s != "" { 120 | return fmt.Errorf(s) 121 | } 122 | return nil 123 | } 124 | 125 | // Next reads the next shape in the shapefile and the next row in the DBF. Call 126 | // Shape() and Attribute() to access the values. 127 | func (zr *ZipReader) Next() bool { 128 | return zr.sr.Next() 129 | } 130 | 131 | // Shape returns the shape that was last read as well as the current index. 132 | func (zr *ZipReader) Shape() (int, Shape) { 133 | return zr.sr.Shape() 134 | } 135 | 136 | // Attribute returns the n-th field of the last row that was read. If there 137 | // were any errors before, the empty string is returned. 138 | func (zr *ZipReader) Attribute(n int) string { 139 | return zr.sr.Attribute(n) 140 | } 141 | 142 | // Fields returns a slice of Fields that are present in the 143 | // DBF table. 144 | func (zr *ZipReader) Fields() []Field { 145 | return zr.sr.Fields() 146 | } 147 | 148 | // Err returns the last non-EOF error that was encountered by this ZipReader. 149 | func (zr *ZipReader) Err() error { 150 | return zr.sr.Err() 151 | } 152 | -------------------------------------------------------------------------------- /zipreader_test.go: -------------------------------------------------------------------------------- 1 | package shp 2 | 3 | import ( 4 | "archive/zip" 5 | "io" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "path" 10 | "path/filepath" 11 | "testing" 12 | ) 13 | 14 | func compressFileToZIP(zw *zip.Writer, src, tgt string, t *testing.T) { 15 | r, err := os.Open(src) 16 | if err != nil { 17 | t.Fatalf("Could not open for compression %s: %v", src, err) 18 | } 19 | w, err := zw.Create(tgt) 20 | if err != nil { 21 | t.Fatalf("Could not start to compress %s: %v", tgt, err) 22 | } 23 | _, err = io.Copy(w, r) 24 | if err != nil { 25 | t.Fatalf("Could not compress contents for %s: %v", tgt, err) 26 | } 27 | } 28 | 29 | // createTempZIP packs the SHP, SHX, and DBF into a ZIP in a temporary 30 | // directory 31 | func createTempZIP(prefix string, t *testing.T) (dir, filename string) { 32 | dir, err := ioutil.TempDir("", "go-shp-test") 33 | if err != nil { 34 | t.Fatalf("Could not create temporary directory: %v", err) 35 | } 36 | base := filepath.Base(prefix) 37 | zipName := base + ".zip" 38 | w, err := os.Create(filepath.Join(dir, zipName)) 39 | if err != nil { 40 | t.Fatalf("Could not create temporary zip file: %v", err) 41 | } 42 | zw := zip.NewWriter(w) 43 | for _, suffix := range []string{".shp", ".shx", ".dbf"} { 44 | compressFileToZIP(zw, prefix+suffix, base+suffix, t) 45 | } 46 | if err := zw.Close(); err != nil { 47 | t.Fatalf("Could not close the written zip: %v", err) 48 | } 49 | return dir, zipName 50 | } 51 | 52 | func getShapesZipped(prefix string, t *testing.T) (shapes []Shape) { 53 | dir, filename := createTempZIP(prefix, t) 54 | defer os.RemoveAll(dir) 55 | zr, err := OpenZip(filepath.Join(dir, filename)) 56 | if err != nil { 57 | t.Errorf("Error when opening zip file: %v", err) 58 | } 59 | for zr.Next() { 60 | _, shape := zr.Shape() 61 | shapes = append(shapes, shape) 62 | } 63 | if err := zr.Err(); err != nil { 64 | t.Errorf("Error when iterating over the shapes: %v", err) 65 | } 66 | 67 | if err := zr.Close(); err != nil { 68 | t.Errorf("Could not close zipreader: %v", err) 69 | } 70 | return shapes 71 | } 72 | 73 | func TestZipReader(t *testing.T) { 74 | for prefix := range dataForReadTests { 75 | t.Logf("Testing zipped reading for %s", prefix) 76 | testshapeIdentity(t, prefix, getShapesZipped) 77 | } 78 | } 79 | 80 | func unzipToTempDir(t *testing.T, p string) string { 81 | td, err := ioutil.TempDir("", "") 82 | if err != nil { 83 | t.Fatalf("%v", err) 84 | } 85 | zip, err := zip.OpenReader(p) 86 | if err != nil { 87 | t.Fatalf("%v", err) 88 | } 89 | defer zip.Close() 90 | for _, f := range zip.File { 91 | _, fn := path.Split(f.Name) 92 | pn := filepath.Join(td, fn) 93 | t.Logf("Uncompress: %s -> %s", f.Name, pn) 94 | w, err := os.Create(pn) 95 | if err != nil { 96 | t.Fatalf("Cannot unzip %s: %v", p, err) 97 | } 98 | defer w.Close() 99 | r, err := f.Open() 100 | if err != nil { 101 | t.Fatalf("Cannot unzip %s: %v", p, err) 102 | } 103 | defer r.Close() 104 | _, err = io.Copy(w, r) 105 | if err != nil { 106 | t.Fatalf("Cannot unzip %s: %v", p, err) 107 | } 108 | } 109 | return td 110 | } 111 | 112 | // TestZipReaderAttributes reads the same shapesfile twice, first directly from 113 | // the Shp with a Reader, and, second, from a zip. It compares the fields as 114 | // well as the shapes and the attributes. For this test, the Shapes are 115 | // considered to be equal if their bounding boxes are equal. 116 | func TestZipReaderAttribute(t *testing.T) { 117 | b := "ne_110m_admin_0_countries" 118 | skipOrDownloadNaturalEarth(t, b+".zip") 119 | d := unzipToTempDir(t, b+".zip") 120 | defer os.RemoveAll(d) 121 | lr, err := Open(filepath.Join(d, b+".shp")) 122 | if err != nil { 123 | t.Fatal(err) 124 | } 125 | defer lr.Close() 126 | zr, err := OpenZip(b + ".zip") 127 | if os.IsNotExist(err) { 128 | t.Skipf("Skipping test, as Natural Earth dataset wasn't found") 129 | } 130 | if err != nil { 131 | t.Fatal(err) 132 | } 133 | defer zr.Close() 134 | fsl := lr.Fields() 135 | fsz := zr.Fields() 136 | if len(fsl) != len(fsz) { 137 | t.Fatalf("Number of attributes do not match: Wanted %d, got %d", len(fsl), len(fsz)) 138 | } 139 | for i := range fsl { 140 | if fsl[i] != fsz[i] { 141 | t.Fatalf("Attribute %d (%s) does not match (%s)", i, fsl[i], fsz[i]) 142 | } 143 | } 144 | for zr.Next() && lr.Next() { 145 | ln, ls := lr.Shape() 146 | zn, zs := zr.Shape() 147 | if ln != zn { 148 | t.Fatalf("Sequence number wrong: Wanted %d, got %d", ln, zn) 149 | } 150 | if ls.BBox() != zs.BBox() { 151 | t.Fatalf("Bounding boxes for shape #%d do not match", ln+1) 152 | } 153 | for i := range fsl { 154 | la := lr.Attribute(i) 155 | za := zr.Attribute(i) 156 | if la != za { 157 | t.Fatalf("Shape %d: Attribute %d (%s) are unequal: '%s' vs '%s'", 158 | ln+1, i, fsl[i].String(), la, za) 159 | } 160 | } 161 | } 162 | if lr.Err() != nil { 163 | t.Logf("Reader error: %v / ZipReader error: %v", lr.Err(), zr.Err()) 164 | t.FailNow() 165 | } 166 | } 167 | 168 | func skipOrDownloadNaturalEarth(t *testing.T, p string) { 169 | if _, err := os.Stat(p); os.IsNotExist(err) { 170 | dl := false 171 | for _, a := range os.Args { 172 | if a == "download" { 173 | dl = true 174 | break 175 | } 176 | } 177 | u := "http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/cultural/ne_110m_admin_0_countries.zip" 178 | if !dl { 179 | t.Skipf("Skipped, as %s does not exist. Consider calling tests with '-args download` "+ 180 | "or download manually from '%s'", p, u) 181 | } else { 182 | t.Logf("Downloading %s", u) 183 | w, err := os.Create(p) 184 | if err != nil { 185 | t.Fatalf("Could not create %q: %v", p, err) 186 | } 187 | defer w.Close() 188 | resp, err := http.Get(u) 189 | if err != nil { 190 | t.Fatalf("Could not download %q: %v", u, err) 191 | } 192 | defer resp.Body.Close() 193 | _, err = io.Copy(w, resp.Body) 194 | if err != nil { 195 | t.Fatalf("Could not download %q: %v", u, err) 196 | } 197 | t.Logf("Download complete") 198 | } 199 | } 200 | } 201 | 202 | func TestNaturalEarthZip(t *testing.T) { 203 | type metaShape struct { 204 | Attributes map[string]string 205 | Shape 206 | } 207 | p := "ne_110m_admin_0_countries.zip" 208 | skipOrDownloadNaturalEarth(t, p) 209 | zr, err := OpenZip(p) 210 | if err != nil { 211 | t.Fatal(err) 212 | } 213 | defer zr.Close() 214 | 215 | fs := zr.Fields() 216 | if len(fs) != 63 { 217 | t.Fatalf("Expected 63 columns in Natural Earth dataset, got %d", len(fs)) 218 | } 219 | var metas []metaShape 220 | for zr.Next() { 221 | m := metaShape{ 222 | Attributes: make(map[string]string), 223 | } 224 | _, m.Shape = zr.Shape() 225 | for n := range fs { 226 | m.Attributes[fs[n].String()] = zr.Attribute(n) 227 | } 228 | metas = append(metas, m) 229 | } 230 | if zr.Err() != nil { 231 | t.Fatal(zr.Err()) 232 | } 233 | for _, m := range metas { 234 | t.Log(m.Attributes["name"]) 235 | } 236 | } 237 | --------------------------------------------------------------------------------