Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/tools/contributed/sumopy/coremodules/misc/shapefile.py
169689 views
1
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
2
# Copyright (C) 2016-2025 German Aerospace Center (DLR) and others.
3
# SUMOPy module
4
# Copyright (C) 2012-2021 University of Bologna - DICAM
5
# This program and the accompanying materials are made available under the
6
# terms of the Eclipse Public License 2.0 which is available at
7
# https://www.eclipse.org/legal/epl-2.0/
8
# This Source Code may also be made available under the following Secondary
9
# Licenses when the conditions for such availability set forth in the Eclipse
10
# Public License 2.0 are satisfied: GNU General Public License, version 2
11
# or later which is available at
12
# https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
13
# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
14
15
# @file shapefile.py
16
# @author Joerg Schweizer
17
# @date 2012
18
19
"""
20
shapefile.py
21
Provides read and write support for ESRI Shapefiles.
22
author: jlawhead<at>geospatialpython.com
23
date: 20110927
24
version: 1.1.4
25
Compatible with Python versions 2.4-3.x
26
"""
27
28
from struct import pack, unpack, calcsize, error
29
import os
30
import sys
31
import time
32
import array
33
#
34
# Constants for shape types
35
NULL = 0
36
POINT = 1
37
POLYLINE = 3
38
POLYGON = 5
39
MULTIPOINT = 8
40
POINTZ = 11
41
POLYLINEZ = 13
42
POLYGONZ = 15
43
MULTIPOINTZ = 18
44
POINTM = 21
45
POLYLINEM = 23
46
POLYGONM = 25
47
MULTIPOINTM = 28
48
MULTIPATCH = 31
49
50
PYTHON3 = sys.version_info[0] == 3
51
52
53
def b(v):
54
if PYTHON3:
55
if isinstance(v, str):
56
# For python 3 encode str to bytes.
57
return v.encode('utf-8')
58
elif isinstance(v, bytes):
59
# Already bytes.
60
return v
61
else:
62
# Error.
63
raise Exception('Unknown input type')
64
else:
65
# For python 2 assume str passed in and return str.
66
return v
67
68
69
def u(v):
70
if PYTHON3:
71
if isinstance(v, bytes):
72
# For python 3 decode bytes to str.
73
return v.decode('utf-8')
74
elif isinstance(v, str):
75
# Already str.
76
return v
77
else:
78
# Error.
79
raise Exception('Unknown input type')
80
else:
81
# For python 2 assume str passed in and return str.
82
return v
83
84
85
def is_string(v):
86
if PYTHON3:
87
return isinstance(v, str)
88
else:
89
return isinstance(v, basestring)
90
91
92
class _Array(array.array):
93
"""Converts python tuples to lits of the appropritate type.
94
Used to unpack different shapefile header parts."""
95
96
def __repr__(self):
97
return str(self.tolist())
98
99
100
class _Shape:
101
def __init__(self, shapeType=None):
102
"""Stores the geometry of the different shape types
103
specified in the Shapefile spec. Shape types are
104
usually point, polyline, or polygons. Every shape type
105
except the "Null" type contains points at some level for
106
example verticies in a polygon. If a shape type has
107
multiple shapes containing points within a single
108
geometry record then those shapes are called parts. Parts
109
are designated by their starting index in geometry record's
110
list of shapes."""
111
self.shapeType = shapeType
112
self.points = []
113
114
115
class _ShapeRecord:
116
"""A shape object of any type."""
117
118
def __init__(self, shape=None, record=None):
119
self.shape = shape
120
self.record = record
121
122
123
class ShapefileException(Exception):
124
"""An exception to handle shapefile specific problems."""
125
pass
126
127
128
class Reader:
129
"""Reads the three files of a shapefile as a unit or
130
separately. If one of the three files (.shp, .shx,
131
.dbf) is missing no exception is thrown until you try
132
to call a method that depends on that particular file.
133
The .shx index file is used if available for efficiency
134
but is not required to read the geometry from the .shp
135
file. The "shapefile" argument in the constructor is the
136
name of the file you want to open.
137
138
You can instantiate a Reader without specifying a shapefile
139
and then specify one later with the load() method.
140
141
Only the shapefile headers are read upon loading. Content
142
within each file is only accessed when required and as
143
efficiently as possible. Shapefiles are usually not large
144
but they can be.
145
"""
146
147
def __init__(self, *args, **kwargs):
148
self.shp = None
149
self.shx = None
150
self.dbf = None
151
self.shapeName = "Not specified"
152
self._offsets = []
153
self.shpLength = None
154
self.numRecords = None
155
self.fields = []
156
self.__dbfHdrLength = 0
157
# See if a shapefile name was passed as an argument
158
if len(args) > 0:
159
if type(args[0]) is type("stringTest"):
160
# print '\n\nReader.__init__: load',args[0]
161
self.load(args[0])
162
return
163
if "shp" in kwargs.keys():
164
if hasattr(kwargs["shp"], "read"):
165
self.shp = kwargs["shp"]
166
if hasattr(self.shp, "seek"):
167
self.shp.seek(0)
168
if "shx" in kwargs.keys():
169
if hasattr(kwargs["shx"], "read"):
170
self.shx = kwargs["shx"]
171
if hasattr(self.shx, "seek"):
172
self.shx.seek(0)
173
if "dbf" in kwargs.keys():
174
if hasattr(kwargs["dbf"], "read"):
175
self.dbf = kwargs["dbf"]
176
if hasattr(self.dbf, "seek"):
177
self.dbf.seek(0)
178
if self.shp or self.dbf:
179
self.load()
180
else:
181
raise ShapefileException("Shapefile Reader requires a shapefile or file-like object.")
182
183
def load(self, shapefile=None):
184
"""Opens a shapefile from a filename or file-like
185
object. Normally this method would be called by the
186
constructor with the file object or file name as an
187
argument."""
188
if shapefile:
189
(shapeName, ext) = os.path.splitext(shapefile)
190
self.shapeName = shapeName
191
try:
192
self.shp = open("%s.shp" % shapeName, "rb")
193
except IOError:
194
raise ShapefileException("Unable to open %s.shp" % shapeName)
195
try:
196
self.shx = open("%s.shx" % shapeName, "rb")
197
except IOError:
198
raise ShapefileException("Unable to open %s.shx" % shapeName)
199
try:
200
self.dbf = open("%s.dbf" % shapeName, "rb")
201
except IOError:
202
raise ShapefileException("Unable to open %s.dbf" % shapeName)
203
if self.shp:
204
self.__shpHeader()
205
if self.dbf:
206
self.__dbfHeader()
207
208
def __getFileObj(self, f):
209
"""Checks to see if the requested shapefile file object is
210
available. If not a ShapefileException is raised."""
211
if not f:
212
raise ShapefileException("Shapefile Reader requires a shapefile or file-like object.")
213
if self.shp and self.shpLength is None:
214
self.load()
215
if self.dbf and len(self.fields) == 0:
216
self.load()
217
return f
218
219
def __restrictIndex(self, i):
220
"""Provides list-like handling of a record index with a clearer
221
error message if the index is out of bounds."""
222
if self.numRecords:
223
rmax = self.numRecords - 1
224
if abs(i) > rmax:
225
raise IndexError("Shape or Record index out of range.")
226
if i < 0:
227
i = range(self.numRecords)[i]
228
return i
229
230
def __shpHeader(self):
231
"""Reads the header information from a .shp or .shx file."""
232
if not self.shp:
233
raise ShapefileException("Shapefile Reader requires a shapefile or file-like object. (no shp file found")
234
shp = self.shp
235
# File length (16-bit word * 2 = bytes)
236
shp.seek(24)
237
self.shpLength = unpack(">i", shp.read(4))[0] * 2
238
# Shape type
239
shp.seek(32)
240
self.shapeType = unpack("<i", shp.read(4))[0]
241
# The shapefile's bounding box (lower left, upper right)
242
self.bbox = _Array('d', unpack("<4d", shp.read(32)))
243
# Elevation
244
self.elevation = _Array('d', unpack("<2d", shp.read(16)))
245
# Measure
246
self.measure = _Array('d', unpack("<2d", shp.read(16)))
247
248
def __shape(self):
249
"""Returns the header info and geometry for a single shape."""
250
f = self.__getFileObj(self.shp)
251
record = _Shape()
252
nParts = nPoints = zmin = zmax = mmin = mmax = None
253
(recNum, recLength) = unpack(">2i", f.read(8))
254
shapeType = unpack("<i", f.read(4))[0]
255
record.shapeType = shapeType
256
# For Null shapes create an empty points list for consistency
257
if shapeType == 0:
258
record.points = []
259
# All shape types capable of having a bounding box
260
elif shapeType in (3, 5, 8, 13, 15, 18, 23, 25, 28, 31):
261
record.bbox = _Array('d', unpack("<4d", f.read(32)))
262
# Shape types with parts
263
if shapeType in (3, 5, 13, 15, 23, 25, 31):
264
nParts = unpack("<i", f.read(4))[0]
265
# Shape types with points
266
if shapeType in (3, 5, 8, 13, 15, 23, 25, 31):
267
nPoints = unpack("<i", f.read(4))[0]
268
# Read parts
269
if nParts:
270
record.parts = _Array('i', unpack("<%si" % nParts, f.read(nParts * 4)))
271
# Read part types for Multipatch - 31
272
if shapeType == 31:
273
record.partTypes = _Array('i', unpack("<%si" % nParts, f.read(nParts * 4)))
274
# Read points - produces a list of [x,y] values
275
if nPoints:
276
record.points = [_Array('d', unpack("<2d", f.read(16))) for p in range(nPoints)]
277
# Read z extremes and values
278
if shapeType in (13, 15, 18, 31):
279
(zmin, zmax) = unpack("<2d", f.read(16))
280
record.z = _Array('d', unpack("<%sd" % nPoints, f.read(nPoints * 8)))
281
# Read m extremes and values
282
if shapeType in (13, 15, 18, 23, 25, 28, 31):
283
(mmin, mmax) = unpack("<2d", f.read(16))
284
# Measure values less than -10e38 are nodata values according to the spec
285
record.m = []
286
for m in _Array('d', unpack("%sd" % nPoints, f.read(nPoints * 8))):
287
if m > -10e38:
288
record.m.append(m)
289
else:
290
record.m.append(None)
291
# Read a single point
292
if shapeType in (1, 11, 21):
293
record.points = [_Array('d', unpack("<2d", f.read(16)))]
294
# Read a single Z value
295
if shapeType == 11:
296
record.z = unpack("<d", f.read(8))
297
# Read a single M value
298
if shapeType in (11, 21):
299
record.m = unpack("<d", f.read(8))
300
return record
301
302
def __shapeIndex(self, i=None):
303
"""Returns the offset in a .shp file for a shape based on information
304
in the .shx index file."""
305
shx = self.shx
306
if not shx:
307
return None
308
if not self._offsets:
309
# File length (16-bit word * 2 = bytes) - header length
310
shx.seek(24)
311
shxRecordLength = (unpack(">i", shx.read(4))[0] * 2) - 100
312
numRecords = shxRecordLength // 8
313
# Jump to the first record.
314
shx.seek(100)
315
for r in range(numRecords):
316
# Offsets are 16-bit words just like the file length
317
self._offsets.append(unpack(">i", shx.read(4))[0] * 2)
318
shx.seek(shx.tell() + 4)
319
if not i is None:
320
return self._offsets[i]
321
322
def shape(self, i=0):
323
"""Returns a shape object for a shape in the geometry
324
record file."""
325
shp = self.__getFileObj(self.shp)
326
i = self.__restrictIndex(i)
327
offset = self.__shapeIndex(i)
328
if not offset:
329
# Shx index not available so use the full list.
330
shapes = self.shapes()
331
return shapes[i]
332
shp.seek(offset)
333
return self.__shape()
334
335
def shapes(self):
336
"""Returns all shapes in a shapefile."""
337
shp = self.__getFileObj(self.shp)
338
shp.seek(100)
339
shapes = []
340
while shp.tell() < self.shpLength:
341
shapes.append(self.__shape())
342
return shapes
343
344
def __dbfHeaderLength(self):
345
"""Retrieves the header length of a dbf file header."""
346
if not self.__dbfHdrLength:
347
if not self.dbf:
348
raise ShapefileException(
349
"Shapefile Reader requires a shapefile or file-like object. (no dbf file found)")
350
dbf = self.dbf
351
(self.numRecords, self.__dbfHdrLength) = \
352
unpack("<xxxxLH22x", dbf.read(32))
353
return self.__dbfHdrLength
354
355
def __dbfHeader(self):
356
"""Reads a dbf header. Xbase-related code borrows heavily from ActiveState Python Cookbook Recipe 362715 by Raymond Hettinger"""
357
if not self.dbf:
358
raise ShapefileException("Shapefile Reader requires a shapefile or file-like object. (no dbf file found)")
359
dbf = self.dbf
360
headerLength = self.__dbfHeaderLength()
361
numFields = (headerLength - 33) // 32
362
for field in range(numFields):
363
fieldDesc = list(unpack("<11sc4xBB14x", dbf.read(32)))
364
name = 0
365
idx = 0
366
if b("\x00") in fieldDesc[name]:
367
idx = fieldDesc[name].index(b("\x00"))
368
else:
369
idx = len(fieldDesc[name]) - 1
370
fieldDesc[name] = fieldDesc[name][:idx]
371
fieldDesc[name] = u(fieldDesc[name])
372
fieldDesc[name] = fieldDesc[name].lstrip()
373
fieldDesc[1] = u(fieldDesc[1])
374
self.fields.append(fieldDesc)
375
terminator = dbf.read(1)
376
assert terminator == b("\r")
377
self.fields.insert(0, ('DeletionFlag', 'C', 1, 0))
378
379
def __recordFmt(self):
380
"""Calculates the size of a .shp geometry record."""
381
if not self.numRecords:
382
self.__dbfHeader()
383
fmt = ''.join(['%ds' % fieldinfo[2] for fieldinfo in self.fields])
384
fmtSize = calcsize(fmt)
385
return (fmt, fmtSize)
386
387
def __record(self):
388
"""Reads and returns a dbf record row as a list of values."""
389
f = self.__getFileObj(self.dbf)
390
recFmt = self.__recordFmt()
391
recordContents = unpack(recFmt[0], f.read(recFmt[1]))
392
if recordContents[0] != b(' '):
393
# deleted record
394
return None
395
record = []
396
for (name, typ, size, deci), value in zip(self.fields,
397
recordContents):
398
if name == 'DeletionFlag':
399
continue
400
elif not value.strip():
401
record.append(value)
402
continue
403
elif typ == "N":
404
value = value.replace(b('\0'), b('')).strip()
405
if value == b(''):
406
value = 0
407
elif deci:
408
value = float(value)
409
else:
410
try:
411
value = int(float(value))
412
except:
413
value = None
414
elif typ == b('D'):
415
try:
416
y, m, d = int(value[:4]), int(value[4:6]), int(value[6:8])
417
value = [y, m, d]
418
except:
419
value = value.strip()
420
elif typ == b('L'):
421
value = (value in b('YyTt') and b('T')) or \
422
(value in b('NnFf') and b('F')) or b('?')
423
else:
424
value = u(value)
425
value = value.strip()
426
record.append(value)
427
return record
428
429
def record(self, i=0):
430
"""Returns a specific dbf record based on the supplied index."""
431
f = self.__getFileObj(self.dbf)
432
if not self.numRecords:
433
self.__dbfHeader()
434
i = self.__restrictIndex(i)
435
recSize = self.__recordFmt()[1]
436
f.seek(0)
437
f.seek(self.__dbfHeaderLength() + (i * recSize))
438
return self.__record()
439
440
def records(self):
441
"""Returns all records in a dbf file."""
442
if not self.numRecords:
443
self.__dbfHeader()
444
records = []
445
f = self.__getFileObj(self.dbf)
446
f.seek(self.__dbfHeaderLength())
447
for i in range(self.numRecords):
448
r = self.__record()
449
if r:
450
records.append(r)
451
return records
452
453
def shapeRecord(self, i=0):
454
"""Returns a combination geometry and attribute record for the
455
supplied record index."""
456
i = self.__restrictIndex(i)
457
return _ShapeRecord(shape=self.shape(i),
458
record=self.record(i))
459
460
def shapeRecords(self):
461
"""Returns a list of combination geometry/attribute records for
462
all records in a shapefile."""
463
shapeRecords = []
464
return [_ShapeRecord(shape=rec[0], record=rec[1])
465
for rec in zip(self.shapes(), self.records())]
466
467
468
class Writer:
469
"""Provides write support for ESRI Shapefiles."""
470
471
def __init__(self, shapeType=None):
472
self._shapes = []
473
self.fields = []
474
self.records = []
475
self.shapeType = shapeType
476
self.shp = None
477
self.shx = None
478
self.dbf = None
479
# Geometry record offsets and lengths for writing shx file.
480
self._offsets = []
481
self._lengths = []
482
# Use deletion flags in dbf? Default is false (0).
483
self.deletionFlag = 0
484
485
def __getFileObj(self, f):
486
"""Safety handler to verify file-like objects"""
487
if not f:
488
raise ShapefileException("No file-like object available.")
489
elif hasattr(f, "write"):
490
return f
491
else:
492
pth = os.path.split(f)[0]
493
if pth and not os.path.exists(pth):
494
os.makedirs(pth)
495
return open(f, "wb")
496
497
def __shpFileLength(self):
498
"""Calculates the file length of the shp file."""
499
# Start with header length
500
size = 100
501
# Calculate size of all shapes
502
for s in self._shapes:
503
# Add in record header and shape type fields
504
size += 12
505
# nParts and nPoints do not apply to all shapes
506
# if self.shapeType not in (0,1):
507
# nParts = len(s.parts)
508
# nPoints = len(s.points)
509
if hasattr(s, 'parts'):
510
nParts = len(s.parts)
511
if hasattr(s, 'points'):
512
nPoints = len(s.points)
513
# All shape types capable of having a bounding box
514
if self.shapeType in (3, 5, 8, 13, 15, 18, 23, 25, 28, 31):
515
size += 32
516
# Shape types with parts
517
if self.shapeType in (3, 5, 13, 15, 23, 25, 31):
518
# Parts count
519
size += 4
520
# Parts index array
521
size += nParts * 4
522
# Shape types with points
523
if self.shapeType in (3, 5, 8, 13, 15, 23, 25, 31):
524
# Points count
525
size += 4
526
# Points array
527
size += 16 * nPoints
528
# Calc size of part types for Multipatch (31)
529
if self.shapeType == 31:
530
size += nParts * 4
531
# Calc z extremes and values
532
if self.shapeType in (13, 15, 18, 31):
533
# z extremes
534
size += 16
535
# z array
536
size += 8 * nPoints
537
# Calc m extremes and values
538
if self.shapeType in (23, 25, 31):
539
# m extremes
540
size += 16
541
# m array
542
size += 8 * nPoints
543
# Calc a single point
544
if self.shapeType in (1, 11, 21):
545
size += 16
546
# Calc a single Z value
547
if self.shapeType == 11:
548
size += 8
549
# Calc a single M value
550
if self.shapeType in (11, 21):
551
size += 8
552
# Calculate size as 16-bit words
553
size //= 2
554
return size
555
556
def __bbox(self, shapes, shapeTypes=[]):
557
x = []
558
y = []
559
for s in shapes:
560
shapeType = self.shapeType
561
if shapeTypes:
562
shapeType = shapeTypes[shapes.index(s)]
563
px, py = list(zip(*s.points))[:2]
564
x.extend(px)
565
y.extend(py)
566
return [min(x), min(y), max(x), max(y)]
567
568
def __zbox(self, shapes, shapeTypes=[]):
569
z = []
570
for s in shapes:
571
try:
572
for p in s.points:
573
z.append(p[2])
574
except IndexError:
575
pass
576
if not z:
577
z.append(0)
578
return [min(z), max(z)]
579
580
def __mbox(self, shapes, shapeTypes=[]):
581
m = [0]
582
for s in shapes:
583
try:
584
for p in s.points:
585
m.append(p[3])
586
except IndexError:
587
pass
588
return [min(m), max(m)]
589
590
def bbox(self):
591
"""Returns the current bounding box for the shapefile which is
592
the lower-left and upper-right corners. It does not contain the
593
elevation or measure extremes."""
594
return self.__bbox(self._shapes)
595
596
def zbox(self):
597
"""Returns the current z extremes for the shapefile."""
598
return self.__zbox(self._shapes)
599
600
def mbox(self):
601
"""Returns the current m extremes for the shapefile."""
602
return self.__mbox(self._shapes)
603
604
def __shapefileHeader(self, fileObj, headerType='shp'):
605
"""Writes the specified header type to the specified file-like object.
606
Several of the shapefile formats are so similar that a single generic
607
method to read or write them is warranted."""
608
f = self.__getFileObj(fileObj)
609
f.seek(0)
610
# File code, Unused bytes
611
f.write(pack(">6i", 9994, 0, 0, 0, 0, 0))
612
# File length (Bytes / 2 = 16-bit words)
613
if headerType == 'shp':
614
f.write(pack(">i", self.__shpFileLength()))
615
elif headerType == 'shx':
616
f.write(pack('>i', ((100 + (len(self._shapes) * 8)) // 2)))
617
# Version, Shape type
618
f.write(pack("<2i", 1000, self.shapeType))
619
# The shapefile's bounding box (lower left, upper right)
620
if self.shapeType != 0:
621
try:
622
f.write(pack("<4d", *self.bbox()))
623
except error:
624
raise ShapefileException("Failed to write shapefile bounding box. Floats required.")
625
else:
626
f.write(pack("<4d", 0, 0, 0, 0))
627
# Elevation
628
z = self.zbox()
629
# Measure
630
m = self.mbox()
631
try:
632
f.write(pack("<4d", z[0], z[1], m[0], m[1]))
633
except error:
634
raise ShapefileException("Failed to write shapefile elevation and measure values. Floats required.")
635
636
def __dbfHeader(self):
637
"""Writes the dbf header and field descriptors."""
638
f = self.__getFileObj(self.dbf)
639
f.seek(0)
640
version = 3
641
year, month, day = time.localtime()[:3]
642
year -= 1900
643
# Remove deletion flag placeholder from fields
644
for field in self.fields:
645
if field[0].startswith("Deletion"):
646
self.fields.remove(field)
647
numRecs = len(self.records)
648
numFields = len(self.fields)
649
headerLength = numFields * 32 + 33
650
recordLength = sum([int(field[2]) for field in self.fields]) + 1
651
header = pack('<BBBBLHH20x', version, year, month, day, numRecs,
652
headerLength, recordLength)
653
f.write(header)
654
# Field descriptors
655
for field in self.fields:
656
name, fieldType, size, decimal = field
657
name = b(name)
658
name = name.replace(b(' '), b('_'))
659
name = name.ljust(11).replace(b(' '), b('\x00'))
660
fieldType = b(fieldType)
661
size = int(size)
662
fld = pack('<11sc4xBB14x', name, fieldType, size, decimal)
663
f.write(fld)
664
# Terminator
665
f.write(b('\r'))
666
667
def __shpRecords(self):
668
"""Write the shp records"""
669
f = self.__getFileObj(self.shp)
670
f.seek(100)
671
recNum = 1
672
for s in self._shapes:
673
self._offsets.append(f.tell())
674
# Record number, Content length place holder
675
f.write(pack(">2i", recNum, 0))
676
recNum += 1
677
start = f.tell()
678
# Shape Type
679
f.write(pack("<i", s.shapeType))
680
# All shape types capable of having a bounding box
681
if s.shapeType in (3, 5, 8, 13, 15, 18, 23, 25, 28, 31):
682
try:
683
f.write(pack("<4d", *self.__bbox([s])))
684
except error:
685
raise ShapefileException("Falied to write bounding box for record %s. Expected floats." % recNum)
686
# Shape types with parts
687
if s.shapeType in (3, 5, 13, 15, 23, 25, 31):
688
# Number of parts
689
f.write(pack("<i", len(s.parts)))
690
# Shape types with multiple points per record
691
if s.shapeType in (3, 5, 8, 13, 15, 23, 25, 31):
692
# Number of points
693
f.write(pack("<i", len(s.points)))
694
# Write part indexes
695
if s.shapeType in (3, 5, 13, 15, 23, 25, 31):
696
for p in s.parts:
697
f.write(pack("<i", p))
698
# Part types for Multipatch (31)
699
if s.shapeType == 31:
700
for pt in s.partTypes:
701
f.write(pack("<i", pt))
702
# Write points for multiple-point records
703
if s.shapeType in (3, 5, 8, 13, 15, 23, 25, 31):
704
try:
705
[f.write(pack("<2d", *p[:2])) for p in s.points]
706
except error:
707
raise ShapefileException("Failed to write points for record %s. Expected floats." % recNum)
708
# Write z extremes and values
709
if s.shapeType in (13, 15, 18, 31):
710
try:
711
f.write(pack("<2d", *self.__zbox([s])))
712
except error:
713
raise ShapefileException(
714
"Failed to write elevation extremes for record %s. Expected floats." % recNum)
715
try:
716
[f.write(pack("<d", p[2])) for p in s.points]
717
except error:
718
raise ShapefileException(
719
"Failed to write elevation values for record %s. Expected floats." % recNum)
720
# Write m extremes and values
721
if s.shapeType in (23, 25, 31):
722
try:
723
f.write(pack("<2d", *self.__mbox([s])))
724
except error:
725
raise ShapefileException("Failed to write measure extremes for record %s. Expected floats" % recNum)
726
try:
727
[f.write(pack("<d", p[3])) for p in s.points]
728
except error:
729
raise ShapefileException("Failed to write measure values for record %s. Expected floats" % recNum)
730
# Write a single point
731
if s.shapeType in (1, 11, 21):
732
try:
733
f.write(pack("<2d", s.points[0][0], s.points[0][1]))
734
except error:
735
raise ShapefileException("Failed to write point for record %s. Expected floats." % recNum)
736
# Write a single Z value
737
if s.shapeType == 11:
738
try:
739
f.write(pack("<1d", s.points[0][2]))
740
except error:
741
raise ShapefileException("Failed to write elevation value for record %s. Expected floats." % recNum)
742
# Write a single M value
743
if s.shapeType in (11, 21):
744
try:
745
f.write(pack("<1d", s.points[0][3]))
746
except error:
747
raise ShapefileException("Failed to write measure value for record %s. Expected floats." % recNum)
748
# Finalize record length as 16-bit words
749
finish = f.tell()
750
length = (finish - start) // 2
751
self._lengths.append(length)
752
# start - 4 bytes is the content length field
753
f.seek(start-4)
754
f.write(pack(">i", length))
755
f.seek(finish)
756
757
def __shxRecords(self):
758
"""Writes the shx records."""
759
f = self.__getFileObj(self.shx)
760
f.seek(100)
761
for i in range(len(self._shapes)):
762
f.write(pack(">i", self._offsets[i] // 2))
763
f.write(pack(">i", self._lengths[i]))
764
765
def __dbfRecords(self):
766
"""Writes the dbf records."""
767
f = self.__getFileObj(self.dbf)
768
for record in self.records:
769
if not self.fields[0][0].startswith("Deletion"):
770
f.write(b(' ')) # deletion flag
771
for (fieldName, fieldType, size, dec), value in zip(self.fields, record):
772
fieldType = fieldType.upper()
773
size = int(size)
774
if fieldType.upper() == "N":
775
value = str(value).rjust(size)
776
elif fieldType == 'L':
777
value = str(value)[0].upper()
778
else:
779
value = str(value)[:size].ljust(size)
780
# print ' __dbfRecords',fieldName,value,len(value),size
781
assert len(value) == size
782
value = b(value)
783
f.write(value)
784
785
def null(self):
786
"""Creates a null shape."""
787
self._shapes.append(_Shape(NULL))
788
789
def point(self, x, y, z=0, m=0):
790
"""Creates a point shape."""
791
pointShape = _Shape(self.shapeType)
792
pointShape.points.append([x, y, z, m])
793
self._shapes.append(pointShape)
794
795
def line(self, parts=[], shapeType=POLYLINE):
796
"""Creates a line shape. This method is just a convienience method
797
which wraps 'poly()'.
798
"""
799
self.poly(parts, shapeType, [])
800
801
def poly(self, parts=[], shapeType=POLYGON, partTypes=[]):
802
"""Creates a shape that has multiple collections of points (parts)
803
including lines, polygons, and even multipoint shapes. If no shape type
804
is specified it defaults to 'polygon'. If no part types are specified
805
(which they normally won't be) then all parts default to the shape type.
806
"""
807
polyShape = _Shape(shapeType)
808
polyShape.parts = []
809
polyShape.points = []
810
for part in parts:
811
polyShape.parts.append(len(polyShape.points))
812
for point in part:
813
# Ensure point is list
814
if not isinstance(point, list):
815
point = list(point)
816
# Make sure point has z and m values
817
while len(point) < 4:
818
point.append(0)
819
polyShape.points.append(point)
820
if polyShape.shapeType == 31:
821
if not partTypes:
822
for part in parts:
823
partTypes.append(polyShape.shapeType)
824
polyShape.partTypes = partTypes
825
self._shapes.append(polyShape)
826
827
def field(self, name, fieldType="C", size="50", decimal=0):
828
"""Adds a dbf field descriptor to the shapefile."""
829
self.fields.append((name, fieldType, size, decimal))
830
831
def record(self, *recordList, **recordDict):
832
"""Creates a dbf attribute record. You can submit either a sequence of
833
field values or keyword arguments of field names and values. Before
834
adding records you must add fields for the record values using the
835
fields() method. If the record values exceed the number of fields the
836
extra ones won't be added. In the case of using keyword arguments to specify
837
field/value pairs only fields matching the already registered fields
838
will be added."""
839
record = []
840
fieldCount = len(self.fields)
841
# Compensate for deletion flag
842
if self.fields[0][0].startswith("Deletion"):
843
fieldCount -= 1
844
if recordList:
845
[record.append(recordList[i]) for i in range(fieldCount)]
846
elif recordDict:
847
for field in self.fields:
848
if field[0] in recordDict:
849
val = recordDict[field[0]]
850
if val:
851
record.append(val)
852
else:
853
record.append("")
854
if record:
855
self.records.append(record)
856
857
def shape(self, i):
858
return self._shapes[i]
859
860
def shapes(self):
861
"""Return the current list of shapes."""
862
return self._shapes
863
864
def saveShp(self, target):
865
"""Save an shp file."""
866
if not hasattr(target, "write"):
867
target = os.path.splitext(target)[0] + '.shp'
868
if not self.shapeType:
869
self.shapeType = self._shapes[0].shapeType
870
self.shp = self.__getFileObj(target)
871
self.__shapefileHeader(self.shp, headerType='shp')
872
self.__shpRecords()
873
874
def saveShx(self, target):
875
"""Save an shx file."""
876
if not hasattr(target, "write"):
877
target = os.path.splitext(target)[0] + '.shx'
878
if not self.shapeType:
879
self.shapeType = self._shapes[0].shapeType
880
self.shx = self.__getFileObj(target)
881
self.__shapefileHeader(self.shx, headerType='shx')
882
self.__shxRecords()
883
884
def saveDbf(self, target):
885
"""Save a dbf file."""
886
if not hasattr(target, "write"):
887
target = os.path.splitext(target)[0] + '.dbf'
888
self.dbf = self.__getFileObj(target)
889
self.__dbfHeader()
890
self.__dbfRecords()
891
892
def save(self, target=None, shp=None, shx=None, dbf=None):
893
"""Save the shapefile data to three files or
894
three file-like objects. SHP and DBF files can also
895
be written exclusively using saveShp, saveShx, and saveDbf respectively."""
896
# TODO: Create a unique filename for target if None.
897
if shp:
898
self.saveShp(shp)
899
if shx:
900
self.saveShx(shx)
901
if dbf:
902
self.saveDbf(dbf)
903
elif target:
904
self.saveShp(target)
905
self.shp.close()
906
self.saveShx(target)
907
self.shx.close()
908
self.saveDbf(target)
909
self.dbf.close()
910
911
912
class Editor(Writer):
913
def __init__(self, shapefile=None, shapeType=POINT, autoBalance=1):
914
self.autoBalance = autoBalance
915
if not shapefile:
916
Writer.__init__(self, shapeType)
917
elif is_string(shapefile):
918
base = os.path.splitext(shapefile)[0]
919
if os.path.isfile("%s.shp" % base):
920
r = Reader(base)
921
Writer.__init__(self, r.shapeType)
922
self._shapes = r.shapes()
923
self.fields = r.fields
924
self.records = r.records()
925
926
def select(self, expr):
927
"""Select one or more shapes (to be implemented)"""
928
# TODO: Implement expressions to select shapes.
929
pass
930
931
def delete(self, shape=None, part=None, point=None):
932
"""Deletes the specified part of any shape by specifying a shape
933
number, part number, or point number."""
934
# shape, part, point
935
if shape and part and point:
936
del self._shapes[shape][part][point]
937
# shape, part
938
elif shape and part and not point:
939
del self._shapes[shape][part]
940
# shape
941
elif shape and not part and not point:
942
del self._shapes[shape]
943
# point
944
elif not shape and not part and point:
945
for s in self._shapes:
946
if s.shapeType == 1:
947
del self._shapes[point]
948
else:
949
for part in s.parts:
950
del s[part][point]
951
# part, point
952
elif not shape and part and point:
953
for s in self._shapes:
954
del s[part][point]
955
# part
956
elif not shape and part and not point:
957
for s in self._shapes:
958
del s[part]
959
960
def point(self, x=None, y=None, z=None, m=None, shape=None, part=None, point=None, addr=None):
961
"""Creates/updates a point shape. The arguments allows
962
you to update a specific point by shape, part, point of any
963
shape type."""
964
# shape, part, point
965
if shape and part and point:
966
try:
967
self._shapes[shape]
968
except IndexError:
969
self._shapes.append([])
970
try:
971
self._shapes[shape][part]
972
except IndexError:
973
self._shapes[shape].append([])
974
try:
975
self._shapes[shape][part][point]
976
except IndexError:
977
self._shapes[shape][part].append([])
978
p = self._shapes[shape][part][point]
979
if x:
980
p[0] = x
981
if y:
982
p[1] = y
983
if z:
984
p[2] = z
985
if m:
986
p[3] = m
987
self._shapes[shape][part][point] = p
988
# shape, part
989
elif shape and part and not point:
990
try:
991
self._shapes[shape]
992
except IndexError:
993
self._shapes.append([])
994
try:
995
self._shapes[shape][part]
996
except IndexError:
997
self._shapes[shape].append([])
998
points = self._shapes[shape][part]
999
for i in range(len(points)):
1000
p = points[i]
1001
if x:
1002
p[0] = x
1003
if y:
1004
p[1] = y
1005
if z:
1006
p[2] = z
1007
if m:
1008
p[3] = m
1009
self._shapes[shape][part][i] = p
1010
# shape
1011
elif shape and not part and not point:
1012
try:
1013
self._shapes[shape]
1014
except IndexError:
1015
self._shapes.append([])
1016
1017
# point
1018
# part
1019
if addr:
1020
shape, part, point = addr
1021
self._shapes[shape][part][point] = [x, y, z, m]
1022
else:
1023
Writer.point(self, x, y, z, m)
1024
if self.autoBalance:
1025
self.balance()
1026
1027
def validate(self):
1028
"""An optional method to try and validate the shapefile
1029
as much as possible before writing it (not implemented)."""
1030
# TODO: Implement validation method
1031
pass
1032
1033
def balance(self):
1034
"""Adds a corresponding empty attribute or null geometry record depending
1035
on which type of record was created to make sure all three files
1036
are in synch."""
1037
if len(self.records) > len(self._shapes):
1038
self.null()
1039
elif len(self.records) < len(self._shapes):
1040
self.record()
1041
1042
def __fieldNorm(self, fieldName):
1043
"""Normalizes a dbf field name to fit within the spec and the
1044
expectations of certain ESRI software."""
1045
if len(fieldName) > 11:
1046
fieldName = fieldName[:11]
1047
fieldName = fieldName.upper()
1048
fieldName.replace(' ', '_')
1049
1050
# Begin Testing
1051
1052
1053
def test():
1054
import doctest
1055
doctest.NORMALIZE_WHITESPACE = 1
1056
doctest.testfile("README.txt", verbose=1)
1057
1058
1059
if __name__ == "__main__":
1060
"""
1061
Doctests are contained in the module 'pyshp_usage.py'. This library was developed
1062
using Python 2.3. Python 2.4 and above have some excellent improvements in the built-in
1063
testing libraries but for now unit testing is done using what's available in
1064
2.3.
1065
"""
1066
test()
1067
1068