pax_global_header00006660000000000000000000000064137724712350014525gustar00rootroot0000000000000052 comment=6efcf0f613fe7e63732394e1e1d14a630f5f4c21 golang-github-jonas-p-go-shp-0.1.1/000077500000000000000000000000001377247123500170135ustar00rootroot00000000000000golang-github-jonas-p-go-shp-0.1.1/.hound.yml000066400000000000000000000000241377247123500207250ustar00rootroot00000000000000go: enabled: true golang-github-jonas-p-go-shp-0.1.1/.travis.yml000066400000000000000000000003761377247123500211320ustar00rootroot00000000000000language: go sudo: false go: - 1.8.x - 1.9.x - master os: - linux before_install: - go get -t -v ./... script: - go test -race -coverprofile=coverage.txt -covermode=atomic after_success: - bash <(curl -s https://codecov.io/bash) golang-github-jonas-p-go-shp-0.1.1/LICENSE000066400000000000000000000020651377247123500200230ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2014 Jonas Palm Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-github-jonas-p-go-shp-0.1.1/README.md000066400000000000000000000042741377247123500203010ustar00rootroot00000000000000go-shp ====== [![Build Status](https://travis-ci.org/jonas-p/go-shp.svg?branch=master)](https://travis-ci.org/jonas-p/go-shp) [![Build status](https://ci.appveyor.com/api/projects/status/b64sntax4kxlouxa?svg=true)](https://ci.appveyor.com/project/fawick/go-shp) [![Go Report Card](https://goreportcard.com/badge/github.com/jonas-p/go-shp)](https://goreportcard.com/report/github.com/jonas-p/go-shp) [![Codevov](https://codecov.io/gh/jonas-p/go-shp/branch/master/graphs/badge.svg)](https://codecov.io/gh/jonas-p/go-shp) Go library for reading and writing ESRI Shapefiles. This is a pure Golang implementation based on the ESRI Shapefile technical description. ### Usage #### Installation go get github.com/jonas-p/go-shp #### Importing ```go import "github.com/jonas-p/go-shp" ``` ### Examples #### Reading a shapefile ```go // open a shapefile for reading shape, err := shp.Open("points.shp") if err != nil { log.Fatal(err) } defer shape.Close() // fields from the attribute table (DBF) fields := shape.Fields() // loop through all features in the shapefile for shape.Next() { n, p := shape.Shape() // print feature fmt.Println(reflect.TypeOf(p).Elem(), p.BBox()) // print attributes for k, f := range fields { val := shape.ReadAttribute(n, k) fmt.Printf("\t%v: %v\n", f, val) } fmt.Println() } ``` #### Creating a shapefile ```go // points to write points := []shp.Point{ shp.Point{10.0, 10.0}, shp.Point{10.0, 15.0}, shp.Point{15.0, 15.0}, shp.Point{15.0, 10.0}, } // fields to write fields := []shp.Field{ // String attribute field with length 25 shp.StringField("NAME", 25), } // create and open a shapefile for writing points shape, err := shp.Create("points.shp", shp.POINT) if err != nil { log.Fatal(err) } defer shape.Close() // setup fields for attributes shape.SetFields(fields) // write points and attributes for n, point := range points { shape.Write(&point) // write attribute for object n for field 0 (NAME) shape.WriteAttribute(n, 0, "Point " + strconv.Itoa(n + 1)) } ``` ### Resources - [Documentation on godoc.org](http://godoc.org/github.com/jonas-p/go-shp) - [ESRI Shapefile Technical Description](http://www.esri.com/library/whitepapers/pdfs/shapefile.pdf) golang-github-jonas-p-go-shp-0.1.1/appveyor.yml000066400000000000000000000007241377247123500214060ustar00rootroot00000000000000clone_folder: c:\go-shp environment: GOPATH: c:\gopath branches: only: - master init: - ps: >- $app = Get-WmiObject -Class Win32_Product -Filter "Vendor = 'http://golang.org'" if ($app) { $app.Uninstall() } install: - rmdir c:\go /s /q - appveyor DownloadFile https://storage.googleapis.com/golang/go1.9.windows-amd64.msi - msiexec /i go1.9.windows-amd64.msi /q - go version - go env build_script: - go test ./... golang-github-jonas-p-go-shp-0.1.1/errreader.go000066400000000000000000000010161377247123500213130ustar00rootroot00000000000000package shp import ( "fmt" "io" ) // errReader is a helper to perform multiple successive read from another reader // and do the error checking only once afterwards. It will not perform any new // reads in case there was an error encountered earlier. type errReader struct { io.Reader e error n int64 } func (er *errReader) Read(p []byte) (n int, err error) { if er.e != nil { return 0, fmt.Errorf("unable to read after previous error: %v", er.e) } n, er.e = er.Reader.Read(p) er.n += int64(n) return n, er.e } golang-github-jonas-p-go-shp-0.1.1/reader.go000066400000000000000000000137161377247123500206140ustar00rootroot00000000000000package shp import ( "encoding/binary" "fmt" "io" "math" "os" "strings" ) // Reader provides a interface for reading Shapefiles. Calls // to the Next method will iterate through the objects in the // Shapefile. After a call to Next the object will be available // through the Shape method. type Reader struct { GeometryType ShapeType bbox Box err error shp readSeekCloser shape Shape num int32 filename string filelength int64 dbf readSeekCloser dbfFields []Field dbfNumRecords int32 dbfHeaderLength int16 dbfRecordLength int16 } type readSeekCloser interface { io.Reader io.Seeker io.Closer } // Open opens a Shapefile for reading. func Open(filename string) (*Reader, error) { filename = filename[0 : len(filename)-3] shp, err := os.Open(filename + "shp") if err != nil { return nil, err } s := &Reader{filename: filename, shp: shp} s.readHeaders() return s, nil } // BBox returns the bounding box of the shapefile. func (r *Reader) BBox() Box { return r.bbox } // Read and parse headers in the Shapefile. This will // fill out GeometryType, filelength and bbox. func (r *Reader) readHeaders() { // don't trust the the filelength in the header r.filelength, _ = r.shp.Seek(0, io.SeekEnd) var filelength int32 r.shp.Seek(24, 0) // file length binary.Read(r.shp, binary.BigEndian, &filelength) r.shp.Seek(32, 0) binary.Read(r.shp, binary.LittleEndian, &r.GeometryType) r.bbox.MinX = readFloat64(r.shp) r.bbox.MinY = readFloat64(r.shp) r.bbox.MaxX = readFloat64(r.shp) r.bbox.MaxY = readFloat64(r.shp) r.shp.Seek(100, 0) } func readFloat64(r io.Reader) float64 { var bits uint64 binary.Read(r, binary.LittleEndian, &bits) return math.Float64frombits(bits) } // Close closes the Shapefile. func (r *Reader) Close() error { if r.err == nil { r.err = r.shp.Close() if r.dbf != nil { r.dbf.Close() } } return r.err } // Shape returns the most recent feature that was read by // a call to Next. It returns two values, the int is the // object index starting from zero in the shapefile which // can be used as row in ReadAttribute, and the Shape is the object. func (r *Reader) Shape() (int, Shape) { return int(r.num) - 1, r.shape } // Attribute returns value of the n-th attribute of the most recent feature // that was read by a call to Next. func (r *Reader) Attribute(n int) string { return r.ReadAttribute(int(r.num)-1, n) } // newShape creates a new shape with a given type. func newShape(shapetype ShapeType) (Shape, error) { switch shapetype { case NULL: return new(Null), nil case POINT: return new(Point), nil case POLYLINE: return new(PolyLine), nil case POLYGON: return new(Polygon), nil case MULTIPOINT: return new(MultiPoint), nil case POINTZ: return new(PointZ), nil case POLYLINEZ: return new(PolyLineZ), nil case POLYGONZ: return new(PolygonZ), nil case MULTIPOINTZ: return new(MultiPointZ), nil case POINTM: return new(PointM), nil case POLYLINEM: return new(PolyLineM), nil case POLYGONM: return new(PolygonM), nil case MULTIPOINTM: return new(MultiPointM), nil case MULTIPATCH: return new(MultiPatch), nil default: return nil, fmt.Errorf("Unsupported shape type: %v", shapetype) } } // Next reads in the next Shape in the Shapefile, which // will then be available through the Shape method. It // returns false when the reader has reached the end of the // file or encounters an error. func (r *Reader) Next() bool { cur, _ := r.shp.Seek(0, io.SeekCurrent) if cur >= r.filelength { return false } var size int32 var shapetype ShapeType er := &errReader{Reader: r.shp} binary.Read(er, binary.BigEndian, &r.num) binary.Read(er, binary.BigEndian, &size) binary.Read(er, binary.LittleEndian, &shapetype) if er.e != nil { if er.e != io.EOF { r.err = fmt.Errorf("Error when reading metadata of next shape: %v", er.e) } else { r.err = io.EOF } return false } var err error r.shape, err = newShape(shapetype) if err != nil { r.err = fmt.Errorf("Error decoding shape type: %v", err) return false } r.shape.read(er) if er.e != nil { r.err = fmt.Errorf("Error while reading next shape: %v", er.e) return false } // move to next object r.shp.Seek(int64(size)*2+cur+8, 0) return true } // Opens DBF file using r.filename + "dbf". This method // will parse the header and fill out all dbf* values int // the f object. func (r *Reader) openDbf() (err error) { if r.dbf != nil { return } r.dbf, err = os.Open(r.filename + "dbf") if err != nil { return } // read header r.dbf.Seek(4, io.SeekStart) binary.Read(r.dbf, binary.LittleEndian, &r.dbfNumRecords) binary.Read(r.dbf, binary.LittleEndian, &r.dbfHeaderLength) binary.Read(r.dbf, binary.LittleEndian, &r.dbfRecordLength) r.dbf.Seek(20, io.SeekCurrent) // skip padding numFields := int(math.Floor(float64(r.dbfHeaderLength-33) / 32.0)) r.dbfFields = make([]Field, numFields) binary.Read(r.dbf, binary.LittleEndian, &r.dbfFields) return } // Fields returns a slice of Fields that are present in the // DBF table. func (r *Reader) Fields() []Field { r.openDbf() // make sure we have dbf file to read from return r.dbfFields } // Err returns the last non-EOF error encountered. func (r *Reader) Err() error { if r.err == io.EOF { return nil } return r.err } // AttributeCount returns number of records in the DBF table. func (r *Reader) AttributeCount() int { r.openDbf() // make sure we have a dbf file to read from return int(r.dbfNumRecords) } // ReadAttribute returns the attribute value at row for field in // the DBF table as a string. Both values starts at 0. func (r *Reader) ReadAttribute(row int, field int) string { r.openDbf() // make sure we have a dbf file to read from seekTo := 1 + int64(r.dbfHeaderLength) + (int64(row) * int64(r.dbfRecordLength)) for n := 0; n < field; n++ { seekTo += int64(r.dbfFields[n].Size) } r.dbf.Seek(seekTo, io.SeekStart) buf := make([]byte, r.dbfFields[field].Size) r.dbf.Read(buf) return strings.Trim(string(buf[:]), " ") } golang-github-jonas-p-go-shp-0.1.1/reader_test.go000066400000000000000000000261251377247123500216510ustar00rootroot00000000000000package shp import ( "bytes" "io" "io/ioutil" "testing" ) func pointsEqual(a, b []float64) bool { if len(a) != len(b) { return false } for k, v := range a { if v != b[k] { return false } } return true } func getShapesFromFile(prefix string, t *testing.T) (shapes []Shape) { filename := prefix + ".shp" file, err := Open(filename) if err != nil { t.Fatal("Failed to open shapefile: " + filename + " (" + err.Error() + ")") } defer file.Close() for file.Next() { _, shape := file.Shape() shapes = append(shapes, shape) } if file.Err() != nil { t.Errorf("Error while getting shapes for %s: %v", prefix, file.Err()) } return shapes } type shapeGetterFunc func(string, *testing.T) []Shape type identityTestFunc func(*testing.T, [][]float64, []Shape) func testPoint(t *testing.T, points [][]float64, shapes []Shape) { for n, s := range shapes { p, ok := s.(*Point) if !ok { t.Fatal("Failed to type assert.") } if !pointsEqual([]float64{p.X, p.Y}, points[n]) { t.Error("Points did not match.") } } } func testPolyLine(t *testing.T, points [][]float64, shapes []Shape) { for n, s := range shapes { p, ok := s.(*PolyLine) if !ok { t.Fatal("Failed to type assert.") } for k, point := range p.Points { if !pointsEqual(points[n*3+k], []float64{point.X, point.Y}) { t.Error("Points did not match.") } } } } func testPolygon(t *testing.T, points [][]float64, shapes []Shape) { for n, s := range shapes { p, ok := s.(*Polygon) if !ok { t.Fatal("Failed to type assert.") } for k, point := range p.Points { if !pointsEqual(points[n*3+k], []float64{point.X, point.Y}) { t.Error("Points did not match.") } } } } func testMultiPoint(t *testing.T, points [][]float64, shapes []Shape) { for n, s := range shapes { p, ok := s.(*MultiPoint) if !ok { t.Fatal("Failed to type assert.") } for k, point := range p.Points { if !pointsEqual(points[n*3+k], []float64{point.X, point.Y}) { t.Error("Points did not match.") } } } } func testPointZ(t *testing.T, points [][]float64, shapes []Shape) { for n, s := range shapes { p, ok := s.(*PointZ) if !ok { t.Fatal("Failed to type assert.") } if !pointsEqual([]float64{p.X, p.Y, p.Z}, points[n]) { t.Error("Points did not match.") } } } func testPolyLineZ(t *testing.T, points [][]float64, shapes []Shape) { for n, s := range shapes { p, ok := s.(*PolyLineZ) if !ok { t.Fatal("Failed to type assert.") } for k, point := range p.Points { if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.ZArray[k]}) { t.Error("Points did not match.") } } } } func testPolygonZ(t *testing.T, points [][]float64, shapes []Shape) { for n, s := range shapes { p, ok := s.(*PolygonZ) if !ok { t.Fatal("Failed to type assert.") } for k, point := range p.Points { if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.ZArray[k]}) { t.Error("Points did not match.") } } } } func testMultiPointZ(t *testing.T, points [][]float64, shapes []Shape) { for n, s := range shapes { p, ok := s.(*MultiPointZ) if !ok { t.Fatal("Failed to type assert.") } for k, point := range p.Points { if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.ZArray[k]}) { t.Error("Points did not match.") } } } } func testPointM(t *testing.T, points [][]float64, shapes []Shape) { for n, s := range shapes { p, ok := s.(*PointM) if !ok { t.Fatal("Failed to type assert.") } if !pointsEqual([]float64{p.X, p.Y, p.M}, points[n]) { t.Error("Points did not match.") } } } func testPolyLineM(t *testing.T, points [][]float64, shapes []Shape) { for n, s := range shapes { p, ok := s.(*PolyLineM) if !ok { t.Fatal("Failed to type assert.") } for k, point := range p.Points { if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.MArray[k]}) { t.Error("Points did not match.") } } } } func testPolygonM(t *testing.T, points [][]float64, shapes []Shape) { for n, s := range shapes { p, ok := s.(*PolygonM) if !ok { t.Fatal("Failed to type assert.") } for k, point := range p.Points { if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.MArray[k]}) { t.Error("Points did not match.") } } } } func testMultiPointM(t *testing.T, points [][]float64, shapes []Shape) { for n, s := range shapes { p, ok := s.(*MultiPointM) if !ok { t.Fatal("Failed to type assert.") } for k, point := range p.Points { if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.MArray[k]}) { t.Error("Points did not match.") } } } } func testMultiPatch(t *testing.T, points [][]float64, shapes []Shape) { for n, s := range shapes { p, ok := s.(*MultiPatch) if !ok { t.Fatal("Failed to type assert.") } for k, point := range p.Points { if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.ZArray[k]}) { t.Error("Points did not match.") } } } } func testshapeIdentity(t *testing.T, prefix string, getter shapeGetterFunc) { shapes := getter(prefix, t) d := dataForReadTests[prefix] if len(shapes) != d.count { t.Errorf("Number of shapes for %s read was wrong. Wanted %d, got %d.", prefix, d.count, len(shapes)) } d.tester(t, d.points, shapes) } func TestReadBBox(t *testing.T) { tests := []struct { filename string want Box }{ {"test_files/multipatch.shp", Box{0, 0, 10, 10}}, {"test_files/multipoint.shp", Box{0, 5, 10, 10}}, {"test_files/multipointm.shp", Box{0, 5, 10, 10}}, {"test_files/multipointz.shp", Box{0, 5, 10, 10}}, {"test_files/point.shp", Box{0, 5, 10, 10}}, {"test_files/pointm.shp", Box{0, 5, 10, 10}}, {"test_files/pointz.shp", Box{0, 5, 10, 10}}, {"test_files/polygon.shp", Box{0, 0, 5, 5}}, {"test_files/polygonm.shp", Box{0, 0, 5, 5}}, {"test_files/polygonz.shp", Box{0, 0, 5, 5}}, {"test_files/polyline.shp", Box{0, 0, 25, 25}}, {"test_files/polylinem.shp", Box{0, 0, 25, 25}}, {"test_files/polylinez.shp", Box{0, 0, 25, 25}}, } for _, tt := range tests { r, err := Open(tt.filename) if err != nil { t.Fatalf("%v", err) } if got := r.BBox().MinX; got != tt.want.MinX { t.Errorf("got MinX = %v, want %v", got, tt.want.MinX) } if got := r.BBox().MinY; got != tt.want.MinY { t.Errorf("got MinY = %v, want %v", got, tt.want.MinY) } if got := r.BBox().MaxX; got != tt.want.MaxX { t.Errorf("got MaxX = %v, want %v", got, tt.want.MaxX) } if got := r.BBox().MaxY; got != tt.want.MaxY { t.Errorf("got MaxY = %v, want %v", got, tt.want.MaxY) } } } type testCaseData struct { points [][]float64 tester identityTestFunc count int } var dataForReadTests = map[string]testCaseData{ "test_files/polygonm": { points: [][]float64{ {0, 0, 0}, {0, 5, 5}, {5, 5, 10}, {5, 0, 15}, {0, 0, 0}, }, tester: testPolygonM, count: 1, }, "test_files/multipointm": { points: [][]float64{ {10, 10, 100}, {5, 5, 50}, {0, 10, 75}, }, tester: testMultiPointM, count: 1, }, "test_files/multipatch": { points: [][]float64{ {0, 0, 0}, {10, 0, 0}, {10, 10, 0}, {0, 10, 0}, {0, 0, 0}, {0, 10, 0}, {0, 10, 10}, {0, 0, 10}, {0, 0, 0}, {0, 10, 0}, {10, 0, 0}, {10, 0, 10}, {10, 10, 10}, {10, 10, 0}, {10, 0, 0}, {0, 0, 0}, {0, 0, 10}, {10, 0, 10}, {10, 0, 0}, {0, 0, 0}, {10, 10, 0}, {10, 10, 10}, {0, 10, 10}, {0, 10, 0}, {10, 10, 0}, {0, 0, 10}, {0, 10, 10}, {10, 10, 10}, {10, 0, 10}, {0, 0, 10}, }, tester: testMultiPatch, count: 1, }, "test_files/point": { points: [][]float64{ {10, 10}, {5, 5}, {0, 10}, }, tester: testPoint, count: 3, }, "test_files/polyline": { points: [][]float64{ {0, 0}, {5, 5}, {10, 10}, {15, 15}, {20, 20}, {25, 25}, }, tester: testPolyLine, count: 2, }, "test_files/polygon": { points: [][]float64{ {0, 0}, {0, 5}, {5, 5}, {5, 0}, {0, 0}, }, tester: testPolygon, count: 1, }, "test_files/multipoint": { points: [][]float64{ {10, 10}, {5, 5}, {0, 10}, }, tester: testMultiPoint, count: 1, }, "test_files/pointz": { points: [][]float64{ {10, 10, 100}, {5, 5, 50}, {0, 10, 75}, }, tester: testPointZ, count: 3, }, "test_files/polylinez": { points: [][]float64{ {0, 0, 0}, {5, 5, 5}, {10, 10, 10}, {15, 15, 15}, {20, 20, 20}, {25, 25, 25}, }, tester: testPolyLineZ, count: 2, }, "test_files/polygonz": { points: [][]float64{ {0, 0, 0}, {0, 5, 5}, {5, 5, 10}, {5, 0, 15}, {0, 0, 0}, }, tester: testPolygonZ, count: 1, }, "test_files/multipointz": { points: [][]float64{ {10, 10, 100}, {5, 5, 50}, {0, 10, 75}, }, tester: testMultiPointZ, count: 1, }, "test_files/pointm": { points: [][]float64{ {10, 10, 100}, {5, 5, 50}, {0, 10, 75}, }, tester: testPointM, count: 3, }, "test_files/polylinem": { points: [][]float64{ {0, 0, 0}, {5, 5, 5}, {10, 10, 10}, {15, 15, 15}, {20, 20, 20}, {25, 25, 25}, }, tester: testPolyLineM, count: 2, }, } func TestReadPoint(t *testing.T) { testshapeIdentity(t, "test_files/point", getShapesFromFile) } func TestReadPolyLine(t *testing.T) { testshapeIdentity(t, "test_files/polyline", getShapesFromFile) } func TestReadPolygon(t *testing.T) { testshapeIdentity(t, "test_files/polygon", getShapesFromFile) } func TestReadMultiPoint(t *testing.T) { testshapeIdentity(t, "test_files/multipoint", getShapesFromFile) } func TestReadPointZ(t *testing.T) { testshapeIdentity(t, "test_files/pointz", getShapesFromFile) } func TestReadPolyLineZ(t *testing.T) { testshapeIdentity(t, "test_files/polylinez", getShapesFromFile) } func TestReadPolygonZ(t *testing.T) { testshapeIdentity(t, "test_files/polygonz", getShapesFromFile) } func TestReadMultiPointZ(t *testing.T) { testshapeIdentity(t, "test_files/multipointz", getShapesFromFile) } func TestReadPointM(t *testing.T) { testshapeIdentity(t, "test_files/pointm", getShapesFromFile) } func TestReadPolyLineM(t *testing.T) { testshapeIdentity(t, "test_files/polylinem", getShapesFromFile) } func TestReadPolygonM(t *testing.T) { testshapeIdentity(t, "test_files/polygonm", getShapesFromFile) } func TestReadMultiPointM(t *testing.T) { testshapeIdentity(t, "test_files/multipointm", getShapesFromFile) } func TestReadMultiPatch(t *testing.T) { testshapeIdentity(t, "test_files/multipatch", getShapesFromFile) } func newReadSeekCloser(b []byte) readSeekCloser { return struct { io.Closer io.ReadSeeker }{ ioutil.NopCloser(nil), bytes.NewReader(b), } } func TestReadInvalidShapeType(t *testing.T) { record := []byte{ 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, // shape type } tests := []struct { r interface { Next() bool Err() error } name string }{ {&Reader{shp: newReadSeekCloser(record), filelength: int64(len(record))}, "reader"}, {&seqReader{shp: newReadSeekCloser(record), filelength: int64(len(record))}, "seqReader"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if test.r.Next() { t.Fatal("read unsupported shape type without stopping") } if test.r.Err() == nil { t.Fatal("read unsupported shape type without error") } }) } } golang-github-jonas-p-go-shp-0.1.1/sequentialreader.go000066400000000000000000000142611377247123500227030ustar00rootroot00000000000000package shp import ( "encoding/binary" "fmt" "io" "io/ioutil" "math" "strings" ) // SequentialReader is the interface that allows reading shapes and attributes one after another. It also embeds io.Closer. type SequentialReader interface { // Close() frees the resources allocated by the SequentialReader. io.Closer // Next() tries to advance the reading by one shape and one attribute row // and returns true if the read operation could be performed without any // error. Next() bool // Shape returns the index and the last read shape. If the SequentialReader // encountered any errors, nil is returned for the Shape. Shape() (int, Shape) // Attribute returns the value of the n-th attribute in the current row. If // the SequentialReader encountered any errors, the empty string is // returned. Attribute(n int) string // Fields returns the fields of the database. If the SequentialReader // encountered any errors, nil is returned. Fields() []Field // Err returns the last non-EOF error encountered. Err() error } // Attributes returns all attributes of the shape that sr was last advanced to. func Attributes(sr SequentialReader) []string { if sr.Err() != nil { return nil } s := make([]string, len(sr.Fields())) for i := range s { s[i] = sr.Attribute(i) } return s } // AttributeCount returns the number of fields of the database. func AttributeCount(sr SequentialReader) int { return len(sr.Fields()) } // seqReader implements SequentialReader based on external io.ReadCloser // instances type seqReader struct { shp, dbf io.ReadCloser err error geometryType ShapeType bbox Box shape Shape num int32 filelength int64 dbfFields []Field dbfNumRecords int32 dbfHeaderLength int16 dbfRecordLength int16 dbfRow []byte } // Read and parse headers in the Shapefile. This will fill out GeometryType, // filelength and bbox. func (sr *seqReader) readHeaders() { // contrary to Reader.readHeaders we cannot seek with the ReadCloser, so we // need to trust the filelength in the header er := &errReader{Reader: sr.shp} // shp headers io.CopyN(ioutil.Discard, er, 24) var l int32 binary.Read(er, binary.BigEndian, &l) sr.filelength = int64(l) * 2 io.CopyN(ioutil.Discard, er, 4) binary.Read(er, binary.LittleEndian, &sr.geometryType) sr.bbox.MinX = readFloat64(er) sr.bbox.MinY = readFloat64(er) sr.bbox.MaxX = readFloat64(er) sr.bbox.MaxY = readFloat64(er) io.CopyN(ioutil.Discard, er, 32) // skip four float64: Zmin, Zmax, Mmin, Max if er.e != nil { sr.err = fmt.Errorf("Error when reading SHP header: %v", er.e) return } // dbf header er = &errReader{Reader: sr.dbf} if sr.dbf == nil { return } io.CopyN(ioutil.Discard, er, 4) binary.Read(er, binary.LittleEndian, &sr.dbfNumRecords) binary.Read(er, binary.LittleEndian, &sr.dbfHeaderLength) binary.Read(er, binary.LittleEndian, &sr.dbfRecordLength) io.CopyN(ioutil.Discard, er, 20) // skip padding numFields := int(math.Floor(float64(sr.dbfHeaderLength-33) / 32.0)) sr.dbfFields = make([]Field, numFields) binary.Read(er, binary.LittleEndian, &sr.dbfFields) buf := make([]byte, 1) er.Read(buf[:]) if er.e != nil { sr.err = fmt.Errorf("Error when reading DBF header: %v", er.e) return } if buf[0] != 0x0d { sr.err = fmt.Errorf("Field descriptor array terminator not found") return } sr.dbfRow = make([]byte, sr.dbfRecordLength) } // Next implements a method of interface SequentialReader for seqReader. func (sr *seqReader) Next() bool { if sr.err != nil { return false } var num, size int32 var shapetype ShapeType // read shape er := &errReader{Reader: sr.shp} binary.Read(er, binary.BigEndian, &num) binary.Read(er, binary.BigEndian, &size) binary.Read(er, binary.LittleEndian, &shapetype) if er.e != nil { if er.e != io.EOF { sr.err = fmt.Errorf("Error when reading shapefile header: %v", er.e) } else { sr.err = io.EOF } return false } sr.num = num var err error sr.shape, err = newShape(shapetype) if err != nil { sr.err = fmt.Errorf("Error decoding shape type: %v", err) return false } sr.shape.read(er) switch { case er.e == io.EOF: // io.EOF means end-of-file was reached gracefully after all // shape-internal reads succeeded, so it's not a reason stop // iterating over all shapes. er.e = nil case er.e != nil: sr.err = fmt.Errorf("Error while reading next shape: %v", er.e) return false } skipBytes := int64(size)*2 + 8 - er.n _, ce := io.CopyN(ioutil.Discard, er, skipBytes) if er.e != nil { sr.err = er.e return false } if ce != nil { sr.err = fmt.Errorf("Error when discarding bytes on sequential read: %v", ce) return false } if _, err := io.ReadFull(sr.dbf, sr.dbfRow); err != nil { sr.err = fmt.Errorf("Error when reading DBF row: %v", err) return false } if sr.dbfRow[0] != 0x20 && sr.dbfRow[0] != 0x2a { sr.err = fmt.Errorf("Attribute row %d starts with incorrect deletion indicator", num) } return sr.err == nil } // Shape implements a method of interface SequentialReader for seqReader. func (sr *seqReader) Shape() (int, Shape) { return int(sr.num) - 1, sr.shape } // Attribute implements a method of interface SequentialReader for seqReader. func (sr *seqReader) Attribute(n int) string { if sr.err != nil { return "" } start := 1 f := 0 for ; f < n; f++ { start += int(sr.dbfFields[f].Size) } s := string(sr.dbfRow[start : start+int(sr.dbfFields[f].Size)]) return strings.Trim(s, " ") } // Err returns the first non-EOF error that was encountered. func (sr *seqReader) Err() error { if sr.err == io.EOF { return nil } return sr.err } // Close closes the seqReader and free all the allocated resources. func (sr *seqReader) Close() error { if err := sr.shp.Close(); err != nil { return err } if err := sr.dbf.Close(); err != nil { return err } return nil } // Fields returns a slice of the fields that are present in the DBF table. func (sr *seqReader) Fields() []Field { return sr.dbfFields } // SequentialReaderFromExt returns a new SequentialReader that interprets shp // as a source of shapes whose attributes can be retrieved from dbf. func SequentialReaderFromExt(shp, dbf io.ReadCloser) SequentialReader { sr := &seqReader{shp: shp, dbf: dbf} sr.readHeaders() return sr } golang-github-jonas-p-go-shp-0.1.1/sequentialreader_test.go000066400000000000000000000017171377247123500237440ustar00rootroot00000000000000package shp import ( "os" "testing" ) func openFile(name string, t *testing.T) *os.File { f, err := os.Open(name) if err != nil { t.Fatalf("Failed to open %s: %v", name, err) } return f } func getShapesSequentially(prefix string, t *testing.T) (shapes []Shape) { shp := openFile(prefix+".shp", t) dbf := openFile(prefix+".dbf", t) sr := SequentialReaderFromExt(shp, dbf) if err := sr.Err(); err != nil { t.Fatalf("Error when iterating over the shapefile header: %v", err) } for sr.Next() { _, shape := sr.Shape() shapes = append(shapes, shape) } if err := sr.Err(); err != nil { t.Errorf("Error when iterating over the shapes: %v", err) } if err := sr.Close(); err != nil { t.Errorf("Could not close sequential reader: %v", err) } return shapes } func TestSequentialReader(t *testing.T) { for prefix := range dataForReadTests { t.Logf("Testing sequential read for %s", prefix) testshapeIdentity(t, prefix, getShapesSequentially) } } golang-github-jonas-p-go-shp-0.1.1/shapefile.go000066400000000000000000000436451377247123500213160ustar00rootroot00000000000000package shp import ( "encoding/binary" "io" "strings" ) // ShapeType is a identifier for the the type of shapes. type ShapeType int32 // These are the possible shape types. const ( NULL ShapeType = 0 POINT = 1 POLYLINE = 3 POLYGON = 5 MULTIPOINT = 8 POINTZ = 11 POLYLINEZ = 13 POLYGONZ = 15 MULTIPOINTZ = 18 POINTM = 21 POLYLINEM = 23 POLYGONM = 25 MULTIPOINTM = 28 MULTIPATCH = 31 ) // Box structure made up from four coordinates. This type // is used to represent bounding boxes type Box struct { MinX, MinY, MaxX, MaxY float64 } // Extend extends the box with coordinates from the provided // box. This method calls Box.ExtendWithPoint twice with // {MinX, MinY} and {MaxX, MaxY} func (b *Box) Extend(box Box) { b.ExtendWithPoint(Point{box.MinX, box.MinY}) b.ExtendWithPoint(Point{box.MaxX, box.MaxY}) } // ExtendWithPoint extends box with coordinates from point // if they are outside the range of the current box. func (b *Box) ExtendWithPoint(p Point) { if p.X < b.MinX { b.MinX = p.X } if p.Y < b.MinY { b.MinY = p.Y } if p.X > b.MaxX { b.MaxX = p.X } if p.Y > b.MaxY { b.MaxY = p.Y } } // BBoxFromPoints returns the bounding box calculated // from points. func BBoxFromPoints(points []Point) (box Box) { for k, p := range points { if k == 0 { box = Box{p.X, p.Y, p.X, p.Y} } else { if p.X < box.MinX { box.MinX = p.X } if p.Y < box.MinY { box.MinY = p.Y } if p.X > box.MaxX { box.MaxX = p.X } if p.Y > box.MaxY { box.MaxY = p.Y } } } return } // Shape interface type Shape interface { BBox() Box read(io.Reader) write(io.Writer) } // Null is an empty shape. type Null struct { } // BBox Returns an empty BBox at the geometry origin. func (n Null) BBox() Box { return Box{0.0, 0.0, 0.0, 0.0} } func (n *Null) read(file io.Reader) { binary.Read(file, binary.LittleEndian, n) } func (n *Null) write(file io.Writer) { binary.Write(file, binary.LittleEndian, n) } // Point is the shape that consists of single a geometry point. type Point struct { X, Y float64 } // BBox returns the bounding box of the Point feature, i.e. an empty area at // the point location itself. func (p Point) BBox() Box { return Box{p.X, p.Y, p.X, p.Y} } func (p *Point) read(file io.Reader) { binary.Read(file, binary.LittleEndian, p) } func (p *Point) write(file io.Writer) { binary.Write(file, binary.LittleEndian, p) } func flatten(points [][]Point) []Point { n, i := 0, 0 for _, v := range points { n += len(v) } r := make([]Point, n) for _, v := range points { for _, p := range v { r[i] = p i++ } } return r } // PolyLine is a shape type that consists of an ordered set of vertices that // consists of one or more parts. A part is a connected sequence of two ore // more points. Parts may or may not be connected to another and may or may not // intersect each other. type PolyLine struct { Box NumParts int32 NumPoints int32 Parts []int32 Points []Point } // NewPolyLine returns a pointer a new PolyLine created // with the provided points. The inner slice should be // the points that the parent part consists of. func NewPolyLine(parts [][]Point) *PolyLine { points := flatten(parts) p := &PolyLine{} p.NumParts = int32(len(parts)) p.NumPoints = int32(len(points)) p.Parts = make([]int32, len(parts)) var marker int32 for i, part := range parts { p.Parts[i] = marker marker += int32(len(part)) } p.Points = points p.Box = p.BBox() return p } // BBox returns the bounding box of the PolyLine feature func (p PolyLine) BBox() Box { return BBoxFromPoints(p.Points) } func (p *PolyLine) read(file io.Reader) { binary.Read(file, binary.LittleEndian, &p.Box) binary.Read(file, binary.LittleEndian, &p.NumParts) binary.Read(file, binary.LittleEndian, &p.NumPoints) p.Parts = make([]int32, p.NumParts) p.Points = make([]Point, p.NumPoints) binary.Read(file, binary.LittleEndian, &p.Parts) binary.Read(file, binary.LittleEndian, &p.Points) } func (p *PolyLine) write(file io.Writer) { binary.Write(file, binary.LittleEndian, p.Box) binary.Write(file, binary.LittleEndian, p.NumParts) binary.Write(file, binary.LittleEndian, p.NumPoints) binary.Write(file, binary.LittleEndian, p.Parts) binary.Write(file, binary.LittleEndian, p.Points) } // Polygon is identical to the PolyLine struct. However the parts must form // rings that may not intersect. type Polygon PolyLine // BBox returns the bounding box of the Polygon feature func (p Polygon) BBox() Box { return BBoxFromPoints(p.Points) } func (p *Polygon) read(file io.Reader) { binary.Read(file, binary.LittleEndian, &p.Box) binary.Read(file, binary.LittleEndian, &p.NumParts) binary.Read(file, binary.LittleEndian, &p.NumPoints) p.Parts = make([]int32, p.NumParts) p.Points = make([]Point, p.NumPoints) binary.Read(file, binary.LittleEndian, &p.Parts) binary.Read(file, binary.LittleEndian, &p.Points) } func (p *Polygon) write(file io.Writer) { binary.Write(file, binary.LittleEndian, p.Box) binary.Write(file, binary.LittleEndian, p.NumParts) binary.Write(file, binary.LittleEndian, p.NumPoints) binary.Write(file, binary.LittleEndian, p.Parts) binary.Write(file, binary.LittleEndian, p.Points) } // MultiPoint is the shape that consists of multiple points. type MultiPoint struct { Box Box NumPoints int32 Points []Point } // BBox returns the bounding box of the MultiPoint feature func (p MultiPoint) BBox() Box { return BBoxFromPoints(p.Points) } func (p *MultiPoint) read(file io.Reader) { binary.Read(file, binary.LittleEndian, &p.Box) binary.Read(file, binary.LittleEndian, &p.NumPoints) p.Points = make([]Point, p.NumPoints) binary.Read(file, binary.LittleEndian, &p.Points) } func (p *MultiPoint) write(file io.Writer) { binary.Write(file, binary.LittleEndian, p.Box) binary.Write(file, binary.LittleEndian, p.NumPoints) binary.Write(file, binary.LittleEndian, p.Points) } // PointZ is a triplet of double precision coordinates plus a measure. type PointZ struct { X float64 Y float64 Z float64 M float64 } // BBox eturns the bounding box of the PointZ feature which is an zero-sized area // at the X and Y coordinates of the feature. func (p PointZ) BBox() Box { return Box{p.X, p.Y, p.X, p.Y} } func (p *PointZ) read(file io.Reader) { binary.Read(file, binary.LittleEndian, p) } func (p *PointZ) write(file io.Writer) { binary.Write(file, binary.LittleEndian, p) } // PolyLineZ is a shape which consists of one or more parts. A part is a // connected sequence of two or more points. Parts may or may not be connected // and may or may not intersect one another. type PolyLineZ struct { Box Box NumParts int32 NumPoints int32 Parts []int32 Points []Point ZRange [2]float64 ZArray []float64 MRange [2]float64 MArray []float64 } // BBox eturns the bounding box of the PolyLineZ feature. func (p PolyLineZ) BBox() Box { return BBoxFromPoints(p.Points) } func (p *PolyLineZ) read(file io.Reader) { binary.Read(file, binary.LittleEndian, &p.Box) binary.Read(file, binary.LittleEndian, &p.NumParts) binary.Read(file, binary.LittleEndian, &p.NumPoints) p.Parts = make([]int32, p.NumParts) p.Points = make([]Point, p.NumPoints) p.ZArray = make([]float64, p.NumPoints) p.MArray = make([]float64, p.NumPoints) binary.Read(file, binary.LittleEndian, &p.Parts) binary.Read(file, binary.LittleEndian, &p.Points) binary.Read(file, binary.LittleEndian, &p.ZRange) binary.Read(file, binary.LittleEndian, &p.ZArray) binary.Read(file, binary.LittleEndian, &p.MRange) binary.Read(file, binary.LittleEndian, &p.MArray) } func (p *PolyLineZ) write(file io.Writer) { binary.Write(file, binary.LittleEndian, p.Box) binary.Write(file, binary.LittleEndian, p.NumParts) binary.Write(file, binary.LittleEndian, p.NumPoints) binary.Write(file, binary.LittleEndian, p.Parts) binary.Write(file, binary.LittleEndian, p.Points) binary.Write(file, binary.LittleEndian, p.ZRange) binary.Write(file, binary.LittleEndian, p.ZArray) binary.Write(file, binary.LittleEndian, p.MRange) binary.Write(file, binary.LittleEndian, p.MArray) } // PolygonZ structure is identical to the PolyLineZ structure. type PolygonZ PolyLineZ // BBox returns the bounding box of the PolygonZ feature func (p PolygonZ) BBox() Box { return BBoxFromPoints(p.Points) } func (p *PolygonZ) read(file io.Reader) { binary.Read(file, binary.LittleEndian, &p.Box) binary.Read(file, binary.LittleEndian, &p.NumParts) binary.Read(file, binary.LittleEndian, &p.NumPoints) p.Parts = make([]int32, p.NumParts) p.Points = make([]Point, p.NumPoints) p.ZArray = make([]float64, p.NumPoints) p.MArray = make([]float64, p.NumPoints) binary.Read(file, binary.LittleEndian, &p.Parts) binary.Read(file, binary.LittleEndian, &p.Points) binary.Read(file, binary.LittleEndian, &p.ZRange) binary.Read(file, binary.LittleEndian, &p.ZArray) binary.Read(file, binary.LittleEndian, &p.MRange) binary.Read(file, binary.LittleEndian, &p.MArray) } func (p *PolygonZ) write(file io.Writer) { binary.Write(file, binary.LittleEndian, p.Box) binary.Write(file, binary.LittleEndian, p.NumParts) binary.Write(file, binary.LittleEndian, p.NumPoints) binary.Write(file, binary.LittleEndian, p.Parts) binary.Write(file, binary.LittleEndian, p.Points) binary.Write(file, binary.LittleEndian, p.ZRange) binary.Write(file, binary.LittleEndian, p.ZArray) binary.Write(file, binary.LittleEndian, p.MRange) binary.Write(file, binary.LittleEndian, p.MArray) } // MultiPointZ consists of one ore more PointZ. type MultiPointZ struct { Box Box NumPoints int32 Points []Point ZRange [2]float64 ZArray []float64 MRange [2]float64 MArray []float64 } // BBox eturns the bounding box of the MultiPointZ feature. func (p MultiPointZ) BBox() Box { return BBoxFromPoints(p.Points) } func (p *MultiPointZ) read(file io.Reader) { binary.Read(file, binary.LittleEndian, &p.Box) binary.Read(file, binary.LittleEndian, &p.NumPoints) p.Points = make([]Point, p.NumPoints) p.ZArray = make([]float64, p.NumPoints) p.MArray = make([]float64, p.NumPoints) binary.Read(file, binary.LittleEndian, &p.Points) binary.Read(file, binary.LittleEndian, &p.ZRange) binary.Read(file, binary.LittleEndian, &p.ZArray) binary.Read(file, binary.LittleEndian, &p.MRange) binary.Read(file, binary.LittleEndian, &p.MArray) } func (p *MultiPointZ) write(file io.Writer) { binary.Write(file, binary.LittleEndian, p.Box) binary.Write(file, binary.LittleEndian, p.NumPoints) binary.Write(file, binary.LittleEndian, p.Points) binary.Write(file, binary.LittleEndian, p.ZRange) binary.Write(file, binary.LittleEndian, p.ZArray) binary.Write(file, binary.LittleEndian, p.MRange) binary.Write(file, binary.LittleEndian, p.MArray) } // PointM is a point with a measure. type PointM struct { X float64 Y float64 M float64 } // BBox returns the bounding box of the PointM feature which is a zero-sized // area at the X- and Y-coordinates of the point. func (p PointM) BBox() Box { return Box{p.X, p.Y, p.X, p.Y} } func (p *PointM) read(file io.Reader) { binary.Read(file, binary.LittleEndian, p) } func (p *PointM) write(file io.Writer) { binary.Write(file, binary.LittleEndian, p) } // PolyLineM is the polyline in which each point also has a measure. type PolyLineM struct { Box Box NumParts int32 NumPoints int32 Parts []int32 Points []Point MRange [2]float64 MArray []float64 } // BBox returns the bounding box of the PolyLineM feature. func (p PolyLineM) BBox() Box { return BBoxFromPoints(p.Points) } func (p *PolyLineM) read(file io.Reader) { binary.Read(file, binary.LittleEndian, &p.Box) binary.Read(file, binary.LittleEndian, &p.NumParts) binary.Read(file, binary.LittleEndian, &p.NumPoints) p.Parts = make([]int32, p.NumParts) p.Points = make([]Point, p.NumPoints) p.MArray = make([]float64, p.NumPoints) binary.Read(file, binary.LittleEndian, &p.Parts) binary.Read(file, binary.LittleEndian, &p.Points) binary.Read(file, binary.LittleEndian, &p.MRange) binary.Read(file, binary.LittleEndian, &p.MArray) } func (p *PolyLineM) write(file io.Writer) { binary.Write(file, binary.LittleEndian, p.Box) binary.Write(file, binary.LittleEndian, p.NumParts) binary.Write(file, binary.LittleEndian, p.NumPoints) binary.Write(file, binary.LittleEndian, p.Parts) binary.Write(file, binary.LittleEndian, p.Points) binary.Write(file, binary.LittleEndian, p.MRange) binary.Write(file, binary.LittleEndian, p.MArray) } // PolygonM structure is identical to the PolyLineZ structure. type PolygonM PolyLineZ // BBox returns the bounding box of the PolygonM feature. func (p PolygonM) BBox() Box { return BBoxFromPoints(p.Points) } func (p *PolygonM) read(file io.Reader) { binary.Read(file, binary.LittleEndian, &p.Box) binary.Read(file, binary.LittleEndian, &p.NumParts) binary.Read(file, binary.LittleEndian, &p.NumPoints) p.Parts = make([]int32, p.NumParts) p.Points = make([]Point, p.NumPoints) p.MArray = make([]float64, p.NumPoints) binary.Read(file, binary.LittleEndian, &p.Parts) binary.Read(file, binary.LittleEndian, &p.Points) binary.Read(file, binary.LittleEndian, &p.MRange) binary.Read(file, binary.LittleEndian, &p.MArray) } func (p *PolygonM) write(file io.Writer) { binary.Write(file, binary.LittleEndian, p.Box) binary.Write(file, binary.LittleEndian, p.NumParts) binary.Write(file, binary.LittleEndian, p.NumPoints) binary.Write(file, binary.LittleEndian, p.Parts) binary.Write(file, binary.LittleEndian, p.Points) binary.Write(file, binary.LittleEndian, p.MRange) binary.Write(file, binary.LittleEndian, p.MArray) } // MultiPointM is the collection of multiple points with measures. type MultiPointM struct { Box Box NumPoints int32 Points []Point MRange [2]float64 MArray []float64 } // BBox eturns the bounding box of the MultiPointM feature func (p MultiPointM) BBox() Box { return BBoxFromPoints(p.Points) } func (p *MultiPointM) read(file io.Reader) { binary.Read(file, binary.LittleEndian, &p.Box) binary.Read(file, binary.LittleEndian, &p.NumPoints) p.Points = make([]Point, p.NumPoints) p.MArray = make([]float64, p.NumPoints) binary.Read(file, binary.LittleEndian, &p.Points) binary.Read(file, binary.LittleEndian, &p.MRange) binary.Read(file, binary.LittleEndian, &p.MArray) } func (p *MultiPointM) write(file io.Writer) { binary.Write(file, binary.LittleEndian, p.Box) binary.Write(file, binary.LittleEndian, p.NumPoints) binary.Write(file, binary.LittleEndian, p.Points) binary.Write(file, binary.LittleEndian, p.MRange) binary.Write(file, binary.LittleEndian, p.MArray) } // MultiPatch consists of a number of surfaces patches. Each surface path // descries a surface. The surface patches of a MultiPatch are referred to as // its parts, and the type of part controls how the order of vertices of an // MultiPatch part is interpreted. type MultiPatch struct { Box Box NumParts int32 NumPoints int32 Parts []int32 PartTypes []int32 Points []Point ZRange [2]float64 ZArray []float64 MRange [2]float64 MArray []float64 } // BBox returns the bounding box of the MultiPatch feature func (p MultiPatch) BBox() Box { return BBoxFromPoints(p.Points) } func (p *MultiPatch) read(file io.Reader) { binary.Read(file, binary.LittleEndian, &p.Box) binary.Read(file, binary.LittleEndian, &p.NumParts) binary.Read(file, binary.LittleEndian, &p.NumPoints) p.Parts = make([]int32, p.NumParts) p.PartTypes = make([]int32, p.NumParts) p.Points = make([]Point, p.NumPoints) p.ZArray = make([]float64, p.NumPoints) p.MArray = make([]float64, p.NumPoints) binary.Read(file, binary.LittleEndian, &p.Parts) binary.Read(file, binary.LittleEndian, &p.PartTypes) binary.Read(file, binary.LittleEndian, &p.Points) binary.Read(file, binary.LittleEndian, &p.ZRange) binary.Read(file, binary.LittleEndian, &p.ZArray) binary.Read(file, binary.LittleEndian, &p.MRange) binary.Read(file, binary.LittleEndian, &p.MArray) } func (p *MultiPatch) write(file io.Writer) { binary.Write(file, binary.LittleEndian, p.Box) binary.Write(file, binary.LittleEndian, p.NumParts) binary.Write(file, binary.LittleEndian, p.NumPoints) binary.Write(file, binary.LittleEndian, p.Parts) binary.Write(file, binary.LittleEndian, p.PartTypes) binary.Write(file, binary.LittleEndian, p.Points) binary.Write(file, binary.LittleEndian, p.ZRange) binary.Write(file, binary.LittleEndian, p.ZArray) binary.Write(file, binary.LittleEndian, p.MRange) binary.Write(file, binary.LittleEndian, p.MArray) } // Field representation of a field object in the DBF file type Field struct { Name [11]byte Fieldtype byte Addr [4]byte // not used Size uint8 Precision uint8 Padding [14]byte } // Returns a string representation of the Field. Currently // this only returns field name. func (f Field) String() string { return strings.TrimRight(string(f.Name[:]), "\x00") } // StringField returns a Field that can be used in SetFields to initialize the // DBF file. func StringField(name string, length uint8) Field { // TODO: Error checking field := Field{Fieldtype: 'C', Size: length} copy(field.Name[:], []byte(name)) return field } // NumberField returns a Field that can be used in SetFields to initialize the // DBF file. func NumberField(name string, length uint8) Field { field := Field{Fieldtype: 'N', Size: length} copy(field.Name[:], []byte(name)) return field } // FloatField returns a Field that can be used in SetFields to initialize the // DBF file. Used to store floating points with precision in the DBF. func FloatField(name string, length uint8, precision uint8) Field { field := Field{Fieldtype: 'F', Size: length, Precision: precision} copy(field.Name[:], []byte(name)) return field } // DateField feturns a Field that can be used in SetFields to initialize the // DBF file. Used to store Date strings formatted as YYYYMMDD. Data wise this // is the same as a StringField with length 8. func DateField(name string) Field { field := Field{Fieldtype: 'D', Size: 8} copy(field.Name[:], []byte(name)) return field } golang-github-jonas-p-go-shp-0.1.1/shapefile_test.go000066400000000000000000000010561377247123500223430ustar00rootroot00000000000000package shp import "testing" func TestBoxExtend(t *testing.T) { a := Box{-124.763068, 45.543541, -116.915989, 49.002494} b := Box{-92.888114, 42.49192, -86.805415, 47.080621} a.Extend(b) c := Box{-124.763068, 42.49192, -86.805415, 49.002494} if a.MinX != c.MinX { t.Errorf("a.MinX = %v, want %v", a.MinX, c.MinX) } if a.MinY != c.MinY { t.Errorf("a.MinY = %v, want %v", a.MinY, c.MinY) } if a.MaxX != c.MaxX { t.Errorf("a.MaxX = %v, want %v", a.MaxX, c.MaxX) } if a.MaxY != c.MaxY { t.Errorf("a.MaxY = %v, want %v", a.MaxY, c.MaxY) } } golang-github-jonas-p-go-shp-0.1.1/test_files/000077500000000000000000000000001377247123500211545ustar00rootroot00000000000000golang-github-jonas-p-go-shp-0.1.1/test_files/multipatch.dbf000066400000000000000000000001101377247123500237730ustar00rootroot00000000000000r AWmultipa_IDN golang-github-jonas-p-go-shp-0.1.1/test_files/multipatch.shp000066400000000000000000000022501377247123500240410ustar00rootroot00000000000000' Ôè$@$@$@$@ $@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@golang-github-jonas-p-go-shp-0.1.1/test_files/multipatch.shx000066400000000000000000000001541377247123500240520ustar00rootroot00000000000000' 6è$@$@2žgolang-github-jonas-p-go-shp-0.1.1/test_files/multipoint.dbf000066400000000000000000000001101377247123500240250ustar00rootroot00000000000000r AWmultipo_IDN golang-github-jonas-p-go-shp-0.1.1/test_files/multipoint.shp000066400000000000000000000003041377247123500240710ustar00rootroot00000000000000' bè@$@$@,@$@$@$@$@@@$@golang-github-jonas-p-go-shp-0.1.1/test_files/multipoint.shx000066400000000000000000000001541377247123500241040ustar00rootroot00000000000000' 6è@$@$@2,golang-github-jonas-p-go-shp-0.1.1/test_files/multipointm.dbf000066400000000000000000000001101377247123500242020ustar00rootroot00000000000000r AWmultipo_IDN golang-github-jonas-p-go-shp-0.1.1/test_files/multipointm.shp000066400000000000000000000003541377247123500242530ustar00rootroot00000000000000' vè@$@$@@@$@$@$@$@@@$@I@Y@Y@I@ÀR@golang-github-jonas-p-go-shp-0.1.1/test_files/multipointm.shx000066400000000000000000000001541377247123500242610ustar00rootroot00000000000000' 6è@$@$@2@golang-github-jonas-p-go-shp-0.1.1/test_files/multipointz.dbf000066400000000000000000000001101377247123500242170ustar00rootroot00000000000000r AWmultipo_IDN golang-github-jonas-p-go-shp-0.1.1/test_files/multipointz.shp000066400000000000000000000004241377247123500242660ustar00rootroot00000000000000' Šè@$@$@I@Y@T@$@$@$@$@@@$@I@Y@Y@I@ÀR@ððÿÿÿÿÿÿÿïÿÿÿÿÿÿÿïÿÿÿÿÿÿÿïÿgolang-github-jonas-p-go-shp-0.1.1/test_files/multipointz.shx000066400000000000000000000001541377247123500242760ustar00rootroot00000000000000' 6è@$@$@I@Y@2Tgolang-github-jonas-p-go-shp-0.1.1/test_files/point.dbf000066400000000000000000000001241377247123500227570ustar00rootroot00000000000000r AWpoint_IDN golang-github-jonas-p-go-shp-0.1.1/test_files/point.shp000066400000000000000000000002701377247123500230200ustar00rootroot00000000000000' \è@$@$@ $@$@ @@ $@golang-github-jonas-p-go-shp-0.1.1/test_files/point.shx000066400000000000000000000001741377247123500230330ustar00rootroot00000000000000' >è@$@$@2 @ N golang-github-jonas-p-go-shp-0.1.1/test_files/pointm.dbf000066400000000000000000000001241377247123500231340ustar00rootroot00000000000000r AWpointm_IDN golang-github-jonas-p-go-shp-0.1.1/test_files/pointm.shp000066400000000000000000000003201377247123500231710ustar00rootroot00000000000000' hè@$@$@$@$@Y@@@I@$@ÀR@golang-github-jonas-p-go-shp-0.1.1/test_files/pointm.shx000066400000000000000000000001741377247123500232100ustar00rootroot00000000000000' >è@$@$@2DVgolang-github-jonas-p-go-shp-0.1.1/test_files/pointz.dbf000066400000000000000000000001241377247123500231510ustar00rootroot00000000000000r AWpointz_IDN golang-github-jonas-p-go-shp-0.1.1/test_files/pointz.shp000066400000000000000000000003501377247123500232110ustar00rootroot00000000000000' tè @$@$@I@Y@ $@$@Y@ÿÿÿÿÿÿïÿ @@I@ÿÿÿÿÿÿïÿ $@ÀR@ÿÿÿÿÿÿïÿgolang-github-jonas-p-go-shp-0.1.1/test_files/pointz.shx000066400000000000000000000001741377247123500232250ustar00rootroot00000000000000' >è @$@$@I@Y@2H^golang-github-jonas-p-go-shp-0.1.1/test_files/polygon.dbf000066400000000000000000000001671377247123500233240ustar00rootroot00000000000000r aWpolygon_IDNAREAN golang-github-jonas-p-go-shp-0.1.1/test_files/polygon.shp000066400000000000000000000003541377247123500233610ustar00rootroot00000000000000' vè@@@@@@@@@golang-github-jonas-p-go-shp-0.1.1/test_files/polygon.shx000066400000000000000000000001541377247123500233670ustar00rootroot00000000000000' 6è@@2@golang-github-jonas-p-go-shp-0.1.1/test_files/polygonm.dbf000066400000000000000000000001101377247123500234650ustar00rootroot00000000000000r AWpolygon_IDN golang-github-jonas-p-go-shp-0.1.1/test_files/polygonm.shp000066400000000000000000000004441377247123500235360ustar00rootroot00000000000000' ’è@@\@@@@@@.@@$@.@golang-github-jonas-p-go-shp-0.1.1/test_files/polygonm.shx000066400000000000000000000001541377247123500235440ustar00rootroot00000000000000' 6è@@2\golang-github-jonas-p-go-shp-0.1.1/test_files/polygonz.dbf000066400000000000000000000001101377247123500235020ustar00rootroot00000000000000r AWpolygon_IDN golang-github-jonas-p-go-shp-0.1.1/test_files/polygonz.shp000066400000000000000000000005341377247123500235530ustar00rootroot00000000000000' ®è@@.@x@@@@@@.@@$@.@ððÿÿÿÿÿÿÿïÿÿÿÿÿÿÿïÿÿÿÿÿÿÿïÿÿÿÿÿÿÿïÿÿÿÿÿÿÿïÿgolang-github-jonas-p-go-shp-0.1.1/test_files/polygonz.shx000066400000000000000000000001541377247123500235610ustar00rootroot00000000000000' 6è@@.@2xgolang-github-jonas-p-go-shp-0.1.1/test_files/polyline.dbf000066400000000000000000000001161377247123500234620ustar00rootroot00000000000000r AWpolylin_IDN golang-github-jonas-p-go-shp-0.1.1/test_files/polyline.shp000066400000000000000000000004641377247123500235270ustar00rootroot00000000000000' šè9@9@0$@$@@@$@$@0.@.@9@9@.@.@4@4@9@9@golang-github-jonas-p-go-shp-0.1.1/test_files/polyline.shx000066400000000000000000000001641377247123500235340ustar00rootroot00000000000000' :è9@9@20f0golang-github-jonas-p-go-shp-0.1.1/test_files/polylinem.dbf000066400000000000000000000001161377247123500236370ustar00rootroot00000000000000r AWpolylin_IDN golang-github-jonas-p-go-shp-0.1.1/test_files/polylinem.shp000066400000000000000000000006041377247123500237000ustar00rootroot00000000000000' Âè9@9@D$@$@@@$@$@$@@$@D.@.@9@9@.@.@4@4@9@9@.@9@.@4@9@golang-github-jonas-p-go-shp-0.1.1/test_files/polylinem.shx000066400000000000000000000001641377247123500237110ustar00rootroot00000000000000' :è9@9@2DzDgolang-github-jonas-p-go-shp-0.1.1/test_files/polylinez.dbf000066400000000000000000000001161377247123500236540ustar00rootroot00000000000000r AWpolylin_IDN golang-github-jonas-p-go-shp-0.1.1/test_files/polylinez.shp000066400000000000000000000007241377247123500237200ustar00rootroot00000000000000' êè 9@9@9@X $@$@@@$@$@$@@$@ððÿÿÿÿÿÿÿïÿÿÿÿÿÿÿïÿÿÿÿÿÿÿïÿX .@.@9@9@.@.@4@4@9@9@.@9@.@4@9@ððÿÿÿÿÿÿÿïÿÿÿÿÿÿÿïÿÿÿÿÿÿÿïÿgolang-github-jonas-p-go-shp-0.1.1/test_files/polylinez.shx000066400000000000000000000001641377247123500237260ustar00rootroot00000000000000' :è 9@9@9@2XŽXgolang-github-jonas-p-go-shp-0.1.1/writer.go000066400000000000000000000231141377247123500206570ustar00rootroot00000000000000package shp import ( "encoding/binary" "errors" "fmt" "io" "math" "os" "path/filepath" "strconv" "strings" ) // Writer is the type that is used to write a new shapefile. type Writer struct { filename string shp writeSeekCloser shx writeSeekCloser GeometryType ShapeType num int32 bbox Box dbf writeSeekCloser dbfFields []Field dbfHeaderLength int16 dbfRecordLength int16 } type writeSeekCloser interface { io.Writer io.Seeker io.Closer } // Create returns a point to new Writer and the first error that was // encountered. In case an error occurred the returned Writer point will be nil // This also creates a corresponding SHX file. It is important to use Close() // when done because that method writes all the headers for each file (SHP, SHX // and DBF). // If filename does not end on ".shp" already, it will be treated as the basename // for the file and the ".shp" extension will be appended to that name. func Create(filename string, t ShapeType) (*Writer, error) { if strings.HasSuffix(strings.ToLower(filename), ".shp") { filename = filename[0 : len(filename)-4] } shp, err := os.Create(filename + ".shp") if err != nil { return nil, err } shx, err := os.Create(filename + ".shx") if err != nil { return nil, err } shp.Seek(100, io.SeekStart) shx.Seek(100, io.SeekStart) w := &Writer{ filename: filename, shp: shp, shx: shx, GeometryType: t, } return w, nil } // Append returns a Writer pointer that will append to the given shapefile and // the first error that was encounted during creation of that Writer. The // shapefile must have a valid index file. func Append(filename string) (*Writer, error) { shp, err := os.OpenFile(filename, os.O_RDWR, 0666) if err != nil { return nil, err } ext := filepath.Ext(filename) basename := filename[:len(filename)-len(ext)] w := &Writer{ filename: basename, shp: shp, } _, err = shp.Seek(32, io.SeekStart) if err != nil { return nil, fmt.Errorf("cannot seek to SHP geometry type: %v", err) } err = binary.Read(shp, binary.LittleEndian, &w.GeometryType) if err != nil { return nil, fmt.Errorf("cannot read geometry type: %v", err) } er := &errReader{Reader: shp} w.bbox.MinX = readFloat64(er) w.bbox.MinY = readFloat64(er) w.bbox.MaxX = readFloat64(er) w.bbox.MaxY = readFloat64(er) if er.e != nil { return nil, fmt.Errorf("cannot read bounding box: %v", er.e) } shx, err := os.OpenFile(basename+".shx", os.O_RDWR, 0666) if os.IsNotExist(err) { // TODO allow index file to not exist, in that case just // read through all the shapes and create it on the fly } if err != nil { return nil, fmt.Errorf("cannot open shapefile index: %v", err) } _, err = shx.Seek(-8, io.SeekEnd) if err != nil { return nil, fmt.Errorf("cannot seek to last shape index: %v", err) } var offset int32 err = binary.Read(shx, binary.BigEndian, &offset) if err != nil { return nil, fmt.Errorf("cannot read last shape index: %v", err) } offset = offset * 2 _, err = shp.Seek(int64(offset), io.SeekStart) if err != nil { return nil, fmt.Errorf("cannot seek to last shape: %v", err) } err = binary.Read(shp, binary.BigEndian, &w.num) if err != nil { return nil, fmt.Errorf("cannot read number of last shape: %v", err) } _, err = shp.Seek(0, io.SeekEnd) if err != nil { return nil, fmt.Errorf("cannot seek to SHP end: %v", err) } _, err = shx.Seek(0, io.SeekEnd) if err != nil { return nil, fmt.Errorf("cannot seek to SHX end: %v", err) } w.shx = shx dbf, err := os.Open(basename + ".dbf") if os.IsNotExist(err) { return w, nil // it's okay if the DBF does not exist } if err != nil { return nil, fmt.Errorf("cannot open DBF: %v", err) } _, err = dbf.Seek(8, io.SeekStart) if err != nil { return nil, fmt.Errorf("cannot seek in DBF: %v", err) } err = binary.Read(dbf, binary.LittleEndian, &w.dbfHeaderLength) if err != nil { return nil, fmt.Errorf("cannot read header length from DBF: %v", err) } err = binary.Read(dbf, binary.LittleEndian, &w.dbfRecordLength) if err != nil { return nil, fmt.Errorf("cannot read record length from DBF: %v", err) } _, err = dbf.Seek(20, io.SeekCurrent) // skip padding if err != nil { return nil, fmt.Errorf("cannot seek in DBF: %v", err) } numFields := int(math.Floor(float64(w.dbfHeaderLength-33) / 32.0)) w.dbfFields = make([]Field, numFields) err = binary.Read(dbf, binary.LittleEndian, &w.dbfFields) if err != nil { return nil, fmt.Errorf("cannot read number of fields from DBF: %v", err) } _, err = dbf.Seek(0, io.SeekEnd) // skip padding if err != nil { return nil, fmt.Errorf("cannot seek to DBF end: %v", err) } w.dbf = dbf return w, nil } // Write shape to the Shapefile. This also creates // a record in the SHX file and DBF file (if it is // initialized). Returns the index of the written object // which can be used in WriteAttribute. func (w *Writer) Write(shape Shape) int32 { // increate bbox if w.num == 0 { w.bbox = shape.BBox() } else { w.bbox.Extend(shape.BBox()) } w.num++ binary.Write(w.shp, binary.BigEndian, w.num) w.shp.Seek(4, io.SeekCurrent) start, _ := w.shp.Seek(0, io.SeekCurrent) binary.Write(w.shp, binary.LittleEndian, w.GeometryType) shape.write(w.shp) finish, _ := w.shp.Seek(0, io.SeekCurrent) length := int32(math.Floor((float64(finish) - float64(start)) / 2.0)) w.shp.Seek(start-4, io.SeekStart) binary.Write(w.shp, binary.BigEndian, length) w.shp.Seek(finish, io.SeekStart) // write shx binary.Write(w.shx, binary.BigEndian, int32((start-8)/2)) binary.Write(w.shx, binary.BigEndian, length) // write empty record to dbf if w.dbf != nil { w.writeEmptyRecord() } return w.num - 1 } // Close closes the Writer. This must be used at the end of // the transaction because it writes the correct headers // to the SHP/SHX and DBF files before closing. func (w *Writer) Close() { w.writeHeader(w.shx) w.writeHeader(w.shp) w.shp.Close() w.shx.Close() if w.dbf == nil { w.SetFields([]Field{}) } w.writeDbfHeader(w.dbf) w.dbf.Close() } // writeHeader wrires SHP/SHX headers to ws. func (w *Writer) writeHeader(ws io.WriteSeeker) { filelength, _ := ws.Seek(0, io.SeekEnd) if filelength == 0 { filelength = 100 } ws.Seek(0, io.SeekStart) // file code binary.Write(ws, binary.BigEndian, []int32{9994, 0, 0, 0, 0, 0}) // file length binary.Write(ws, binary.BigEndian, int32(filelength/2)) // version and shape type binary.Write(ws, binary.LittleEndian, []int32{1000, int32(w.GeometryType)}) // bounding box binary.Write(ws, binary.LittleEndian, w.bbox) // elevation, measure binary.Write(ws, binary.LittleEndian, []float64{0.0, 0.0, 0.0, 0.0}) } // writeDbfHeader writes a DBF header to ws. func (w *Writer) writeDbfHeader(ws io.WriteSeeker) { ws.Seek(0, 0) // version, year (YEAR-1990), month, day binary.Write(ws, binary.LittleEndian, []byte{3, 24, 5, 3}) // number of records binary.Write(ws, binary.LittleEndian, w.num) // header length, record length binary.Write(ws, binary.LittleEndian, []int16{w.dbfHeaderLength, w.dbfRecordLength}) // padding binary.Write(ws, binary.LittleEndian, make([]byte, 20)) for _, field := range w.dbfFields { binary.Write(ws, binary.LittleEndian, field) } // end with return ws.Write([]byte("\r")) } // SetFields sets field values in the DBF. This initializes the DBF file and // should be used prior to writing any attributes. func (w *Writer) SetFields(fields []Field) error { if w.dbf != nil { return errors.New("Cannot set fields in existing dbf") } var err error w.dbf, err = os.Create(w.filename + "dbf") if err != nil { return fmt.Errorf("Failed to open %s.dbf: %v", w.filename, err) } w.dbfFields = fields // calculate record length w.dbfRecordLength = int16(1) for _, field := range w.dbfFields { w.dbfRecordLength += int16(field.Size) } // header lengh w.dbfHeaderLength = int16(len(w.dbfFields)*32 + 33) // fill header space with empty bytes for now buf := make([]byte, w.dbfHeaderLength) binary.Write(w.dbf, binary.LittleEndian, buf) // write empty records for n := int32(0); n < w.num; n++ { w.writeEmptyRecord() } return nil } // Writes an empty record to the end of the DBF. This // works by seeking to the end of the file and writing // dbfRecordLength number of bytes. The first byte is a // space that indicates a new record. func (w *Writer) writeEmptyRecord() { w.dbf.Seek(0, io.SeekEnd) buf := make([]byte, w.dbfRecordLength) buf[0] = ' ' binary.Write(w.dbf, binary.LittleEndian, buf) } // WriteAttribute writes value for field into the given row in the DBF. Row // number should be the same as the order the Shape was written to the // Shapefile. The field value corresponds to the field in the slice used in // SetFields. func (w *Writer) WriteAttribute(row int, field int, value interface{}) error { var buf []byte switch v := value.(type) { case int: buf = []byte(strconv.Itoa(v)) case float64: precision := w.dbfFields[field].Precision buf = []byte(strconv.FormatFloat(v, 'f', int(precision), 64)) case string: buf = []byte(v) default: return fmt.Errorf("Unsupported value type: %T", v) } if w.dbf == nil { return errors.New("Initialize DBF by using SetFields first") } if sz := int(w.dbfFields[field].Size); len(buf) > sz { return fmt.Errorf("Unable to write field %v: %q exceeds field length %v", field, buf, sz) } seekTo := 1 + int64(w.dbfHeaderLength) + (int64(row) * int64(w.dbfRecordLength)) for n := 0; n < field; n++ { seekTo += int64(w.dbfFields[n].Size) } w.dbf.Seek(seekTo, io.SeekStart) return binary.Write(w.dbf, binary.LittleEndian, buf) } // BBox returns the bounding box of the Writer. func (w *Writer) BBox() Box { return w.bbox } golang-github-jonas-p-go-shp-0.1.1/writer_test.go000066400000000000000000000104651377247123500217230ustar00rootroot00000000000000package shp import ( "bytes" "io" "os" "reflect" "testing" ) var filenamePrefix = "test_files/write_" func removeShapefile(filename string) { os.Remove(filename + ".shp") os.Remove(filename + ".shx") os.Remove(filename + ".dbf") } func pointsToFloats(points []Point) [][]float64 { floats := make([][]float64, len(points)) for k, v := range points { floats[k] = make([]float64, 2) floats[k][0] = v.X floats[k][1] = v.Y } return floats } func TestAppend(t *testing.T) { filename := filenamePrefix + "point" defer removeShapefile(filename) points := [][]float64{ {0.0, 0.0}, {5.0, 5.0}, {10.0, 10.0}, } shape, err := Create(filename+".shp", POINT) if err != nil { t.Fatal(err) } for _, p := range points { shape.Write(&Point{p[0], p[1]}) } wantNum := shape.num shape.Close() newPoints := [][]float64{ {15.0, 15.0}, {20.0, 20.0}, {25.0, 25.0}, } shape, err = Append(filename + ".shp") if err != nil { t.Fatal(err) } if shape.GeometryType != POINT { t.Fatalf("wanted geo type %d, got %d", POINT, shape.GeometryType) } if shape.num != wantNum { t.Fatalf("wrong 'num', wanted type %d, got %d", wantNum, shape.num) } for _, p := range newPoints { shape.Write(&Point{p[0], p[1]}) } points = append(points, newPoints...) shapes := getShapesFromFile(filename, t) if len(shapes) != len(points) { t.Error("Number of shapes read was wrong") } testPoint(t, points, shapes) } func TestWritePoint(t *testing.T) { filename := filenamePrefix + "point" defer removeShapefile(filename) points := [][]float64{ {0.0, 0.0}, {5.0, 5.0}, {10.0, 10.0}, } shape, err := Create(filename+".shp", POINT) if err != nil { t.Fatal(err) } for _, p := range points { shape.Write(&Point{p[0], p[1]}) } shape.Close() shapes := getShapesFromFile(filename, t) if len(shapes) != len(points) { t.Error("Number of shapes read was wrong") } testPoint(t, points, shapes) } func TestWritePolyLine(t *testing.T) { filename := filenamePrefix + "polyline" defer removeShapefile(filename) points := [][]Point{ {Point{0.0, 0.0}, Point{5.0, 5.0}}, {Point{10.0, 10.0}, Point{15.0, 15.0}}, } shape, err := Create(filename+".shp", POLYLINE) if err != nil { t.Log(shape, err) } l := NewPolyLine(points) lWant := &PolyLine{ Box: Box{MinX: 0, MinY: 0, MaxX: 15, MaxY: 15}, NumParts: 2, NumPoints: 4, Parts: []int32{0, 2}, Points: []Point{{X: 0, Y: 0}, {X: 5, Y: 5}, {X: 10, Y: 10}, {X: 15, Y: 15}, }, } if !reflect.DeepEqual(l, lWant) { t.Errorf("incorrect NewLine: have: %+v; want: %+v", l, lWant) } shape.Write(l) shape.Close() shapes := getShapesFromFile(filename, t) if len(shapes) != 1 { t.Error("Number of shapes read was wrong") } testPolyLine(t, pointsToFloats(flatten(points)), shapes) } type seekTracker struct { io.Writer offset int64 } func (s *seekTracker) Seek(offset int64, whence int) (int64, error) { s.offset = offset return s.offset, nil } func (s *seekTracker) Close() error { return nil } func TestWriteAttribute(t *testing.T) { buf := new(bytes.Buffer) s := &seekTracker{Writer: buf} w := Writer{ dbf: s, dbfFields: []Field{ StringField("A_STRING", 6), FloatField("A_FLOAT", 8, 4), NumberField("AN_INT", 4), }, dbfRecordLength: 100, } tests := []struct { name string row int field int data interface{} wantOffset int64 wantData string }{ {"string-0", 0, 0, "test", 1, "test"}, {"string-0-overflow-1", 0, 0, "overflo", 0, ""}, {"string-0-overflow-n", 0, 0, "overflowing", 0, ""}, {"string-3", 3, 0, "things", 301, "things"}, {"float-0", 0, 1, 123.44, 7, "123.4400"}, {"float-0-overflow-1", 0, 1, 1234.0, 0, ""}, {"float-0-overflow-n", 0, 1, 123456789.0, 0, ""}, {"int-0", 0, 2, 4242, 15, "4242"}, {"int-0-overflow-1", 0, 2, 42424, 0, ""}, {"int-0-overflow-n", 0, 2, 42424343, 0, ""}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { buf.Reset() s.offset = 0 err := w.WriteAttribute(test.row, test.field, test.data) if buf.String() != test.wantData { t.Errorf("got data: %v, want: %v", buf.String(), test.wantData) } if s.offset != test.wantOffset { t.Errorf("got seek offset: %v, want: %v", s.offset, test.wantOffset) } if err == nil && test.wantData == "" { t.Error("got no data and no error") } }) } } golang-github-jonas-p-go-shp-0.1.1/zipreader.go000066400000000000000000000075261377247123500213410ustar00rootroot00000000000000package shp import ( "archive/zip" "fmt" "io" "path" "strings" ) // ZipReader provides an interface for reading Shapefiles that are compressed in a ZIP archive. type ZipReader struct { sr SequentialReader z *zip.ReadCloser } // openFromZIP is convenience function for opening the file called name that is // compressed in z for reading. func openFromZIP(z *zip.ReadCloser, name string) (io.ReadCloser, error) { for _, f := range z.File { if f.Name == name { return f.Open() } } return nil, fmt.Errorf("No such file in archive: %s", name) } // OpenZip opens a ZIP file that contains a single shapefile. func OpenZip(zipFilePath string) (*ZipReader, error) { z, err := zip.OpenReader(zipFilePath) if err != nil { return nil, err } zr := &ZipReader{ z: z, } shapeFiles := shapesInZip(z) if len(shapeFiles) == 0 { return nil, fmt.Errorf("archive does not contain a .shp file") } if len(shapeFiles) > 1 { return nil, fmt.Errorf("archive does contain multiple .shp files") } shp, err := openFromZIP(zr.z, shapeFiles[0].Name) if err != nil { return nil, err } withoutExt := strings.TrimSuffix(shapeFiles[0].Name, ".shp") // dbf is optional, so no error checking here dbf, _ := openFromZIP(zr.z, withoutExt+".dbf") zr.sr = SequentialReaderFromExt(shp, dbf) return zr, nil } // ShapesInZip returns a string-slice with the names (i.e. relatives paths in // archive file tree) of all shapes that are in the ZIP archive at zipFilePath. func ShapesInZip(zipFilePath string) ([]string, error) { var names []string z, err := zip.OpenReader(zipFilePath) if err != nil { return nil, err } shapeFiles := shapesInZip(z) for i := range shapeFiles { names = append(names, shapeFiles[i].Name) } return names, nil } func shapesInZip(z *zip.ReadCloser) []*zip.File { var shapeFiles []*zip.File for _, f := range z.File { if strings.HasSuffix(f.Name, ".shp") { shapeFiles = append(shapeFiles, f) } } return shapeFiles } // OpenShapeFromZip opens a shape file that is contained in a ZIP archive. The // parameter name is name of the shape file. // The name of the shapefile must be a relative path: it must not start with a // drive letter (e.g. C:) or leading slash, and only forward slashes are // allowed. These rules are the same as in // https://golang.org/pkg/archive/zip/#FileHeader. func OpenShapeFromZip(zipFilePath string, name string) (*ZipReader, error) { z, err := zip.OpenReader(zipFilePath) if err != nil { return nil, err } zr := &ZipReader{ z: z, } shp, err := openFromZIP(zr.z, name) if err != nil { return nil, err } // dbf is optional, so no error checking here prefix := strings.TrimSuffix(name, path.Ext(name)) dbf, _ := openFromZIP(zr.z, prefix+".dbf") zr.sr = SequentialReaderFromExt(shp, dbf) return zr, nil } // Close closes the ZipReader and frees the allocated resources. func (zr *ZipReader) Close() error { s := "" err := zr.sr.Close() if err != nil { s += err.Error() + ". " } err = zr.z.Close() if err != nil { s += err.Error() + ". " } if s != "" { return fmt.Errorf(s) } return nil } // Next reads the next shape in the shapefile and the next row in the DBF. Call // Shape() and Attribute() to access the values. func (zr *ZipReader) Next() bool { return zr.sr.Next() } // Shape returns the shape that was last read as well as the current index. func (zr *ZipReader) Shape() (int, Shape) { return zr.sr.Shape() } // Attribute returns the n-th field of the last row that was read. If there // were any errors before, the empty string is returned. func (zr *ZipReader) Attribute(n int) string { return zr.sr.Attribute(n) } // Fields returns a slice of Fields that are present in the // DBF table. func (zr *ZipReader) Fields() []Field { return zr.sr.Fields() } // Err returns the last non-EOF error that was encountered by this ZipReader. func (zr *ZipReader) Err() error { return zr.sr.Err() } golang-github-jonas-p-go-shp-0.1.1/zipreader_test.go000066400000000000000000000133531377247123500223730ustar00rootroot00000000000000package shp import ( "archive/zip" "io" "io/ioutil" "net/http" "os" "path" "path/filepath" "testing" ) func compressFileToZIP(zw *zip.Writer, src, tgt string, t *testing.T) { r, err := os.Open(src) if err != nil { t.Fatalf("Could not open for compression %s: %v", src, err) } w, err := zw.Create(tgt) if err != nil { t.Fatalf("Could not start to compress %s: %v", tgt, err) } _, err = io.Copy(w, r) if err != nil { t.Fatalf("Could not compress contents for %s: %v", tgt, err) } } // createTempZIP packs the SHP, SHX, and DBF into a ZIP in a temporary // directory func createTempZIP(prefix string, t *testing.T) (dir, filename string) { dir, err := ioutil.TempDir("", "go-shp-test") if err != nil { t.Fatalf("Could not create temporary directory: %v", err) } base := filepath.Base(prefix) zipName := base + ".zip" w, err := os.Create(filepath.Join(dir, zipName)) if err != nil { t.Fatalf("Could not create temporary zip file: %v", err) } zw := zip.NewWriter(w) for _, suffix := range []string{".shp", ".shx", ".dbf"} { compressFileToZIP(zw, prefix+suffix, base+suffix, t) } if err := zw.Close(); err != nil { t.Fatalf("Could not close the written zip: %v", err) } return dir, zipName } func getShapesZipped(prefix string, t *testing.T) (shapes []Shape) { dir, filename := createTempZIP(prefix, t) defer os.RemoveAll(dir) zr, err := OpenZip(filepath.Join(dir, filename)) if err != nil { t.Errorf("Error when opening zip file: %v", err) } for zr.Next() { _, shape := zr.Shape() shapes = append(shapes, shape) } if err := zr.Err(); err != nil { t.Errorf("Error when iterating over the shapes: %v", err) } if err := zr.Close(); err != nil { t.Errorf("Could not close zipreader: %v", err) } return shapes } func TestZipReader(t *testing.T) { for prefix := range dataForReadTests { t.Logf("Testing zipped reading for %s", prefix) testshapeIdentity(t, prefix, getShapesZipped) } } func unzipToTempDir(t *testing.T, p string) string { td, err := ioutil.TempDir("", "") if err != nil { t.Fatalf("%v", err) } zip, err := zip.OpenReader(p) if err != nil { t.Fatalf("%v", err) } defer zip.Close() for _, f := range zip.File { _, fn := path.Split(f.Name) pn := filepath.Join(td, fn) t.Logf("Uncompress: %s -> %s", f.Name, pn) w, err := os.Create(pn) if err != nil { t.Fatalf("Cannot unzip %s: %v", p, err) } defer w.Close() r, err := f.Open() if err != nil { t.Fatalf("Cannot unzip %s: %v", p, err) } defer r.Close() _, err = io.Copy(w, r) if err != nil { t.Fatalf("Cannot unzip %s: %v", p, err) } } return td } // TestZipReaderAttributes reads the same shapesfile twice, first directly from // the Shp with a Reader, and, second, from a zip. It compares the fields as // well as the shapes and the attributes. For this test, the Shapes are // considered to be equal if their bounding boxes are equal. func TestZipReaderAttribute(t *testing.T) { b := "ne_110m_admin_0_countries" skipOrDownloadNaturalEarth(t, b+".zip") d := unzipToTempDir(t, b+".zip") defer os.RemoveAll(d) lr, err := Open(filepath.Join(d, b+".shp")) if err != nil { t.Fatal(err) } defer lr.Close() zr, err := OpenZip(b + ".zip") if os.IsNotExist(err) { t.Skipf("Skipping test, as Natural Earth dataset wasn't found") } if err != nil { t.Fatal(err) } defer zr.Close() fsl := lr.Fields() fsz := zr.Fields() if len(fsl) != len(fsz) { t.Fatalf("Number of attributes do not match: Wanted %d, got %d", len(fsl), len(fsz)) } for i := range fsl { if fsl[i] != fsz[i] { t.Fatalf("Attribute %d (%s) does not match (%s)", i, fsl[i], fsz[i]) } } for zr.Next() && lr.Next() { ln, ls := lr.Shape() zn, zs := zr.Shape() if ln != zn { t.Fatalf("Sequence number wrong: Wanted %d, got %d", ln, zn) } if ls.BBox() != zs.BBox() { t.Fatalf("Bounding boxes for shape #%d do not match", ln+1) } for i := range fsl { la := lr.Attribute(i) za := zr.Attribute(i) if la != za { t.Fatalf("Shape %d: Attribute %d (%s) are unequal: '%s' vs '%s'", ln+1, i, fsl[i].String(), la, za) } } } if lr.Err() != nil { t.Logf("Reader error: %v / ZipReader error: %v", lr.Err(), zr.Err()) t.FailNow() } } func skipOrDownloadNaturalEarth(t *testing.T, p string) { if _, err := os.Stat(p); os.IsNotExist(err) { dl := false for _, a := range os.Args { if a == "download" { dl = true break } } u := "http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/cultural/ne_110m_admin_0_countries.zip" if !dl { t.Skipf("Skipped, as %s does not exist. Consider calling tests with '-args download` "+ "or download manually from '%s'", p, u) } else { t.Logf("Downloading %s", u) w, err := os.Create(p) if err != nil { t.Fatalf("Could not create %q: %v", p, err) } defer w.Close() resp, err := http.Get(u) if err != nil { t.Fatalf("Could not download %q: %v", u, err) } defer resp.Body.Close() _, err = io.Copy(w, resp.Body) if err != nil { t.Fatalf("Could not download %q: %v", u, err) } t.Logf("Download complete") } } } func TestNaturalEarthZip(t *testing.T) { type metaShape struct { Attributes map[string]string Shape } p := "ne_110m_admin_0_countries.zip" skipOrDownloadNaturalEarth(t, p) zr, err := OpenZip(p) if err != nil { t.Fatal(err) } defer zr.Close() fs := zr.Fields() if len(fs) != 63 { t.Fatalf("Expected 63 columns in Natural Earth dataset, got %d", len(fs)) } var metas []metaShape for zr.Next() { m := metaShape{ Attributes: make(map[string]string), } _, m.Shape = zr.Shape() for n := range fs { m.Attributes[fs[n].String()] = zr.Attribute(n) } metas = append(metas, m) } if zr.Err() != nil { t.Fatal(zr.Err()) } for _, m := range metas { t.Log(m.Attributes["name"]) } }