14
14
import time
15
15
import array
16
16
import tempfile
17
- import warnings
17
+ import logging
18
18
import io
19
19
from datetime import date
20
20
21
21
22
+ # Module settings
23
+ VERBOSE = True
24
+
22
25
# Constants for shape types
23
26
NULL = 0
24
27
POINT = 1
@@ -279,10 +282,11 @@ def ring_contains_ring(coords1, coords2):
279
282
'''
280
283
return all ((ring_contains_point (coords1 , p2 ) for p2 in coords2 ))
281
284
282
- def organize_polygon_rings (rings ):
285
+ def organize_polygon_rings (rings , return_errors = None ):
283
286
'''Organize a list of coordinate rings into one or more polygons with holes.
284
287
Returns a list of polygons, where each polygon is composed of a single exterior
285
- ring, and one or more interior holes.
288
+ ring, and one or more interior holes. If a return_errors dict is provided (optional),
289
+ any errors encountered will be added to it.
286
290
287
291
Rings must be closed, and cannot intersect each other (non-self-intersecting polygon).
288
292
Rings are determined as exteriors if they run in clockwise direction, or interior
@@ -360,7 +364,6 @@ def organize_polygon_rings(rings):
360
364
orphan_holes = []
361
365
for hole_i ,exterior_candidates in list (hole_exteriors .items ()):
362
366
if not exterior_candidates :
363
- warnings .warn ('Shapefile shape has invalid polygon: found orphan hole (not contained by any of the exteriors); interpreting as exterior.' )
364
367
orphan_holes .append ( hole_i )
365
368
del hole_exteriors [hole_i ]
366
369
continue
@@ -384,11 +387,15 @@ def organize_polygon_rings(rings):
384
387
poly = [ext ]
385
388
polys .append (poly )
386
389
390
+ if orphan_holes and return_errors is not None :
391
+ return_errors ['polygon_orphaned_holes' ] = len (orphan_holes )
392
+
387
393
return polys
388
394
389
395
# no exteriors, be nice and assume due to incorrect winding order
390
396
else :
391
- warnings .warn ('Shapefile shape has invalid polygon: no exterior rings found (must have clockwise orientation); interpreting holes as exteriors.' )
397
+ if return_errors is not None :
398
+ return_errors ['polygon_only_holes' ] = len (holes )
392
399
exteriors = holes # could potentially reverse their order, but in geojson winding order doesn't matter
393
400
polys = [[ext ] for ext in exteriors ]
394
401
return polys
@@ -411,6 +418,8 @@ def __init__(self, shapeType=NULL, points=None, parts=None, partTypes=None):
411
418
self .parts = parts or []
412
419
if partTypes :
413
420
self .partTypes = partTypes
421
+ # and a dict to silently record any errors encountered
422
+ self ._errors = {}
414
423
415
424
@property
416
425
def __geo_interface__ (self ):
@@ -490,7 +499,25 @@ def __geo_interface__(self):
490
499
491
500
# organize rings into list of polygons, where each polygon is defined as list of rings.
492
501
# the first ring is the exterior and any remaining rings are holes (same as GeoJSON).
493
- polys = organize_polygon_rings (rings )
502
+ polys = organize_polygon_rings (rings , self ._errors )
503
+
504
+ # if VERBOSE is True, issue detailed warning about any shape errors
505
+ # encountered during the Shapefile to GeoJSON conversion
506
+ if VERBOSE and self ._errors :
507
+ header = 'Possible issue encountered when converting Shape to GeoJSON: '
508
+ orphans = self ._errors .get ('polygon_orphaned_holes' , None )
509
+ if orphans :
510
+ msg = header + 'GeoJSON format requires that all polygon interior holes be contained by an exterior ring, \
511
+ but the Shape contained interior holes (defined by counter-clockwise orientation in the shapefile format) that were \
512
+ orphaned, i.e. not contained by any exterior rings. The rings were still included but were \
513
+ encoded as GeoJSON exterior rings instead of holes.'
514
+ logging .warning (msg )
515
+ only_holes = self ._errors .get ('polygon_only_holes' , None )
516
+ if only_holes :
517
+ msg = header + 'GeoJSON format requires that polygons contain at least one exterior ring, \
518
+ but the Shape was entirely made up of interior holes (defined by counter-clockwise orientation in the shapefile format). The rings were \
519
+ still included but were encoded as GeoJSON exterior rings instead of holes.'
520
+ logging .warning (msg )
494
521
495
522
# return as geojson
496
523
if len (polys ) == 1 :
@@ -749,8 +776,9 @@ def __repr__(self):
749
776
def __geo_interface__ (self ):
750
777
# Note: currently this will fail if any of the shapes are null-geometries
751
778
# could be fixed by storing the shapefile shapeType upon init, returning geojson type with empty coords
752
- return {'type' : 'GeometryCollection' ,
753
- 'geometries' : [shape .__geo_interface__ for shape in self ]}
779
+ collection = {'type' : 'GeometryCollection' ,
780
+ 'geometries' : [shape .__geo_interface__ for shape in self ]}
781
+ return collection
754
782
755
783
class ShapeRecords (list ):
756
784
"""A class to hold a list of ShapeRecord objects. Subclasses list to ensure compatibility with
@@ -763,13 +791,50 @@ def __repr__(self):
763
791
764
792
@property
765
793
def __geo_interface__ (self ):
766
- return {'type' : 'FeatureCollection' ,
767
- 'features' : [shaperec .__geo_interface__ for shaperec in self ]}
794
+ collection = {'type' : 'FeatureCollection' ,
795
+ 'features' : [shaperec .__geo_interface__ for shaperec in self ]}
796
+ return collection
768
797
769
798
class ShapefileException (Exception ):
770
799
"""An exception to handle shapefile specific problems."""
771
800
pass
772
801
802
+ # def warn_geojson_collection(shapes):
803
+ # # collect information about any potential errors with the GeoJSON
804
+ # errors = {}
805
+ # for i,shape in enumerate(shapes):
806
+ # shape_errors = shape._errors
807
+ # if shape_errors:
808
+ # for error in shape_errors.keys():
809
+ # errors[error] = errors[error] + [i] if error in errors else []
810
+
811
+ # # warn if any errors were found
812
+ # if errors:
813
+ # messages = ['Summary of possibles issues encountered during shapefile to GeoJSON conversion:']
814
+
815
+ # # polygon orphan holes
816
+ # orphans = errors.get('polygon_orphaned_holes', None)
817
+ # if orphans:
818
+ # msg = 'GeoJSON format requires that all interior holes be contained by an exterior ring, \
819
+ # but the Shapefile contained {} records of polygons where some of its interior holes were \
820
+ # orphaned (not contained by any other rings). The rings were still included but were \
821
+ # encoded as GeoJSON exterior rings instead of holes. Shape ids: {}'.format(len(orphans), orphans)
822
+ # messages.append(msg)
823
+
824
+ # # polygon only holes/wrong orientation
825
+ # only_holes = errors.get('polygon_only_holes', None)
826
+ # if only_holes:
827
+ # msg = 'GeoJSON format requires that polygons contain at least one exterior ring, but \
828
+ # the Shapefile contained {} records of polygons where all of its component rings were stored as interior \
829
+ # holes. The rings were still included but were encoded as GeoJSON exterior rings instead of holes. \
830
+ # Shape ids: {}'.format(len(only_holes), only_holes)
831
+ # messages.append(msg)
832
+
833
+ # if len(messages) > 1:
834
+ # # more than just the "Summary of..." header
835
+ # msg = '\n'.join(messages)
836
+ # logging.warning(msg)
837
+
773
838
class Reader (object ):
774
839
"""Reads the three files of a shapefile as a unit or
775
840
separately. If one of the three files (.shp, .shx,
0 commit comments