-
Notifications
You must be signed in to change notification settings - Fork 3
/
shp.go
292 lines (263 loc) · 8.93 KB
/
shp.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
package shapefile
// FIXME use .shx indexes
// FIXME factor out ParseSHPRecord
import (
"archive/zip"
"encoding/binary"
"errors"
"fmt"
"io"
"github.com/twpayne/go-geom"
)
// A SHPRecord is a record in a SHP file.
type SHPRecord struct {
Number int
ContentLength int
ShapeType ShapeType
Bounds *geom.Bounds
Geom geom.T
}
// ReadSHPOptions are options for ReadSHP.
type ReadSHPOptions struct {
MaxParts int
MaxPoints int
MaxRecordSize int
}
// A SHP is a .shp file.
type SHP struct {
SHxHeader
Records []*SHPRecord
}
// ReadSHP reads a SHP from an io.Reader.
func ReadSHP(r io.Reader, fileLength int64, options *ReadSHPOptions) (*SHP, error) {
header, err := readSHxHeader(r, fileLength)
if err != nil {
return nil, err
}
var records []*SHPRecord
RECORD:
for recordNumber := 1; ; recordNumber++ {
switch record, err := ReadSHPRecord(r, options); {
case errors.Is(err, io.EOF):
break RECORD
case err != nil:
return nil, fmt.Errorf("record %d: %w", recordNumber, err)
case record.Number != recordNumber:
return nil, fmt.Errorf("record %d: invalid record number (expected %d)", recordNumber, record.Number)
default:
records = append(records, record)
}
}
return &SHP{
SHxHeader: *header,
Records: records,
}, nil
}
// ReadSHPRecord reads the next *SHPRecord from r.
func ReadSHPRecord(r io.Reader, options *ReadSHPOptions) (*SHPRecord, error) {
recordHeaderData := make([]byte, 8)
if err := readFull(r, recordHeaderData); err != nil {
return nil, err
}
recordNumber := int(binary.BigEndian.Uint32(recordHeaderData[:4]))
contentLength := 2 * int(binary.BigEndian.Uint32(recordHeaderData[4:8]))
if contentLength < 4 {
return nil, errors.New("content length too short")
}
if options != nil && options.MaxRecordSize != 0 && contentLength > options.MaxRecordSize {
return nil, errors.New("content length too large")
}
recordData := make([]byte, contentLength)
if err := readFull(r, recordData); err != nil {
return nil, err
}
byteSliceReader := newByteSliceReader(recordData)
shapeType := ShapeType(byteSliceReader.readUint32())
expectedContentLength := 4
if shapeType == ShapeTypeNull {
if contentLength != expectedContentLength {
return nil, errors.New("invalid content length")
}
return &SHPRecord{
Number: recordNumber,
ContentLength: contentLength,
ShapeType: ShapeTypeNull,
}, nil
}
layout := geom.NoLayout
switch shapeType {
case ShapeTypeNull:
case ShapeTypePoint, ShapeTypeMultiPoint, ShapeTypePolyLine, ShapeTypePolygon:
layout = geom.XY
case ShapeTypePointM, ShapeTypeMultiPointM, ShapeTypePolyLineM, ShapeTypePolygonM:
layout = geom.XYM
case ShapeTypePointZ, ShapeTypeMultiPointZ, ShapeTypePolyLineZ, ShapeTypePolygonZ:
layout = geom.XYZM
}
switch shapeType {
case ShapeTypePoint, ShapeTypePointM, ShapeTypePointZ:
flatCoords := byteSliceReader.readFloat64s(layout.Stride())
expectedContentLength += 8 * layout.Stride()
if contentLength != expectedContentLength {
return nil, errors.New("invalid content length")
}
return &SHPRecord{
Number: recordNumber,
ContentLength: contentLength,
ShapeType: shapeType,
Geom: geom.NewPointFlat(layout, flatCoords),
}, nil
}
minX, minY := byteSliceReader.readFloat64Pair()
maxX, maxY := byteSliceReader.readFloat64Pair()
expectedContentLength += 8 * 4
var numParts int
switch shapeType {
case ShapeTypePolyLine, ShapeTypePolyLineM, ShapeTypePolyLineZ:
fallthrough
case ShapeTypePolygon, ShapeTypePolygonM, ShapeTypePolygonZ:
numParts = byteSliceReader.readUint32()
if numParts == 0 {
return nil, errors.New("invalid number of parts")
}
if options != nil && options.MaxParts != 0 && numParts > options.MaxParts {
return nil, errors.New("too many parts")
}
expectedContentLength += 4 + 4*numParts
}
numPoints := byteSliceReader.readUint32()
if options != nil && options.MaxPoints != 0 && numPoints > options.MaxPoints {
return nil, errors.New("too many points")
}
expectedContentLength += 4
switch layout {
case geom.XY:
expectedContentLength += 8 * 2 * numPoints
case geom.XYM:
expectedContentLength += 8*2*numPoints + 8*2 + 8*numPoints
case geom.XYZM:
expectedContentLength += 8*2*numPoints + 8*2 + 8*numPoints + 8*2 + 8*numPoints
}
if contentLength != expectedContentLength {
return nil, errors.New("invalid content length")
}
var ends []int
switch shapeType {
case ShapeTypePolyLine, ShapeTypePolyLineM, ShapeTypePolyLineZ:
fallthrough
case ShapeTypePolygon, ShapeTypePolygonM, ShapeTypePolygonZ:
ends = byteSliceReader.readEnds(layout, numParts, numPoints)
}
flatCoords := make([]float64, layout.Stride()*numPoints)
byteSliceReader.readXYs(flatCoords, numPoints, layout)
var bounds *geom.Bounds
switch layout {
case geom.XY:
bounds = geom.NewBounds(geom.XY).Set(minX, minY, maxX, maxY)
case geom.XYM:
minM, maxM := byteSliceReader.readFloat64Pair()
byteSliceReader.readOrdinates(flatCoords, numPoints, layout, layout.MIndex())
bounds = geom.NewBounds(geom.XYM).Set(minX, minY, minM, maxX, maxY, maxM)
case geom.XYZM:
minZ, maxZ := byteSliceReader.readFloat64Pair()
byteSliceReader.readOrdinates(flatCoords, numPoints, layout, layout.ZIndex())
minM, maxM := byteSliceReader.readFloat64Pair()
byteSliceReader.readOrdinates(flatCoords, numPoints, layout, layout.MIndex())
bounds = geom.NewBounds(geom.XYZM).Set(minX, minY, minZ, minM, maxX, maxY, maxZ, maxM)
}
if err := byteSliceReader.Err(); err != nil {
return nil, err
}
var g geom.T
switch shapeType {
case ShapeTypeMultiPoint, ShapeTypeMultiPointM, ShapeTypeMultiPointZ:
g = geom.NewMultiPointFlat(layout, flatCoords)
case ShapeTypePolyLine, ShapeTypePolyLineM, ShapeTypePolyLineZ:
g = geom.NewMultiLineStringFlat(layout, flatCoords, ends)
case ShapeTypePolygon, ShapeTypePolygonM, ShapeTypePolygonZ:
endss, err := makeMultiPolygonEndss(layout, flatCoords, ends)
if err != nil {
return nil, err
}
g = geom.NewMultiPolygonFlat(layout, flatCoords, endss)
}
return &SHPRecord{
Number: recordNumber,
ContentLength: contentLength,
ShapeType: shapeType,
Bounds: bounds,
Geom: g,
}, nil
}
// ReadSHPZipFile reads a *SHP from a *zip.File.
func ReadSHPZipFile(zipFile *zip.File, options *ReadSHPOptions) (*SHP, error) {
readCloser, err := zipFile.Open()
if err != nil {
return nil, err
}
defer readCloser.Close()
shp, err := ReadSHP(readCloser, int64(zipFile.UncompressedSize64), options)
if err != nil {
return nil, fmt.Errorf("%s: %w", zipFile.Name, err)
}
return shp, nil
}
// Record returns the ith geometry.
func (s *SHP) Record(i int) geom.T {
return s.Records[i].Geom
}
// makeMultiPolygonEndss returns the multipolygon endss by inspecting the
// orientation of the rings defined by flatCoords and ends. Each clockwise ring
// defines the outer ring of a new polygon, and each anti-clockwise ring defines
// an inner ring of the current polygon.
//
// All rings are assumed to be in order, that is that inner rings always belong
// to the polygon with the most recently defined outer ring.
//
// From the Shapefile specification:
//
// A polygon consists of one or more rings. A ring is a connected sequence of
// four or more points that form a closed, non-self-intersecting loop. A polygon
// may contain multiple outer rings. The order of vertices or orientation for a
// ring indicates which side of the ring is the interior of the polygon. The
// neighborhood to the right of an observer walking along the ring in vertex
// order is the neighborhood inside the polygon. Vertices of rings defining
// holes in polygons are in a counterclockwise direction. Vertices for a single,
// ringed polygon are, therefore, always in clockwise order. The rings of a
// polygon are referred to as its parts.
//
// Because this specification does not forbid consecutive points with identical
// coordinates, shapefile readers must handle such cases. On the other hand, the
// degenerate, zero length or zero area parts that might result are not allowed.
func makeMultiPolygonEndss(layout geom.Layout, flatCoords []float64, ends []int) ([][]int, error) {
var endss [][]int
polygonOffset := 0
offset := 0
stride := layout.Stride()
for i, end := range ends {
if (end-offset)/stride < 4 {
return nil, errors.New("too few points in ring")
}
switch doubleArea := doubleArea(flatCoords, offset, end, stride); {
case doubleArea == 0:
return nil, errors.New("zero area ring")
case i != 0 && doubleArea < 0:
endss = append(endss, ends[polygonOffset:i])
polygonOffset = i
}
offset = end
}
if len(ends) > 0 {
endss = append(endss, ends[polygonOffset:])
}
return endss, nil
}
// doubleArea returns double the area of the polygon from offset to end in
// flatCoords.
func doubleArea(flatCoords []float64, offset, end, stride int) float64 {
var doubleArea float64
for i := offset + stride; i < end; i += stride {
doubleArea += (flatCoords[i+1] - flatCoords[i+1-stride]) * (flatCoords[i] + flatCoords[i-stride])
}
return doubleArea
}