diff --git a/shapefile.py b/shapefile.py index bce0350..f9f5639 100644 --- a/shapefile.py +++ b/shapefile.py @@ -182,6 +182,14 @@ def bbox_overlap(bbox1, bbox2): overlap = (xmin1 <= xmax2 and xmax1 >= xmin2 and ymin1 <= ymax2 and ymax1 >= ymin2) return overlap +def bbox_contains(bbox1, bbox2): + """Tests whether bbox1 fully contains bbox2, returning a boolean + """ + xmin1,ymin1,xmax1,ymax1 = bbox1 + xmin2,ymin2,xmax2,ymax2 = bbox2 + contains = (xmin1 < xmin2 and xmax1 > xmax2 and ymin1 < ymin2 and ymax1 > ymax2) + return contains + def ring_contains_point(coords, p): """Fast point-in-polygon crossings algorithm, MacMartin optimization. @@ -224,6 +232,44 @@ def ring_contains_point(coords, p): return inside_flag +def ring_sample(coords, ccw=False): + """Return a sample point guaranteed to be within a ring, by efficiently + finding the first centroid of a coordinate triplet whose orientation + matches the orientation of the ring and passes the point-in-ring test. + The orientation of the ring is assumed to be clockwise, unless ccw + (counter-clockwise) is set to True. + """ + coords = tuple(coords) + (coords[1],) # add the second coordinate to the end to allow checking the last triplet + triplet = [] + for p in coords: + # add point to triplet (but not if duplicate) + if p not in triplet: + triplet.append(p) + + # new triplet, try to get sample + if len(triplet) == 3: + # check that triplet does not form a straight line (not a triangle) + is_straight_line = (triplet[0][1] - triplet[1][1]) * (triplet[0][0] - triplet[2][0]) == (triplet[0][1] - triplet[2][1]) * (triplet[0][0] - triplet[1][0]) + if not is_straight_line: + # get triplet orientation + closed_triplet = triplet + [triplet[0]] + triplet_ccw = signed_area(closed_triplet) >= 0 + # check that triplet has the same orientation as the ring (means triangle is inside the ring) + if ccw == triplet_ccw: + # get triplet centroid + xs,ys = zip(*triplet) + xmean,ymean = sum(xs) / 3.0, sum(ys) / 3.0 + # check that triplet centroid is truly inside the ring + if ring_contains_point(coords, (xmean,ymean)): + return xmean,ymean + + # failed to get sample point from this triplet + # remove oldest triplet coord to allow iterating to next triplet + triplet.pop(0) + + else: + raise Exception('Unexpected error: Unable to find a ring sample point.') + def ring_contains_ring(coords1, coords2): '''Returns True if all vertexes in coords2 are fully inside coords1. ''' @@ -272,25 +318,26 @@ def organize_polygon_rings(rings): polys.append(poly) return polys - # first determine each hole's candidate exteriors based on simple bbox overlap test + # first determine each hole's candidate exteriors based on simple bbox contains test hole_exteriors = dict([(hole_i,[]) for hole_i in xrange(len(holes))]) exterior_bboxes = [ring_bbox(ring) for ring in exteriors] for hole_i in hole_exteriors.keys(): hole_bbox = ring_bbox(holes[hole_i]) for ext_i,ext_bbox in enumerate(exterior_bboxes): - if bbox_overlap(hole_bbox, ext_bbox): + if bbox_contains(ext_bbox, hole_bbox): hole_exteriors[hole_i].append( ext_i ) # then, for holes with still more than one possible exterior, do more detailed hole-in-ring test for hole_i,exterior_candidates in hole_exteriors.items(): if len(exterior_candidates) > 1: - # get new exterior candidates - hole = holes[hole_i] + # get hole sample point + hole_sample = ring_sample(holes[hole_i], ccw=True) + # collect new exterior candidates new_exterior_candidates = [] for ext_i in exterior_candidates: - ext = exteriors[ext_i] - hole_in_exterior = ring_contains_ring(ext, hole) + # check that hole sample point is inside exterior + hole_in_exterior = ring_contains_point(exteriors[ext_i], hole_sample) if hole_in_exterior: new_exterior_candidates.append(ext_i) diff --git a/shapefiles/test/balancing.dbf b/shapefiles/test/balancing.dbf index 4c7e992..db4fab4 100644 Binary files a/shapefiles/test/balancing.dbf and b/shapefiles/test/balancing.dbf differ diff --git a/shapefiles/test/contextwriter.dbf b/shapefiles/test/contextwriter.dbf index 25aa008..15229fa 100644 Binary files a/shapefiles/test/contextwriter.dbf and b/shapefiles/test/contextwriter.dbf differ diff --git a/shapefiles/test/dtype.dbf b/shapefiles/test/dtype.dbf index d65e8e6..e753d24 100644 Binary files a/shapefiles/test/dtype.dbf and b/shapefiles/test/dtype.dbf differ diff --git a/shapefiles/test/line.dbf b/shapefiles/test/line.dbf index 7a9fb5c..49a0ad0 100644 Binary files a/shapefiles/test/line.dbf and b/shapefiles/test/line.dbf differ diff --git a/shapefiles/test/linem.dbf b/shapefiles/test/linem.dbf index ddd59de..38ca81e 100644 Binary files a/shapefiles/test/linem.dbf and b/shapefiles/test/linem.dbf differ diff --git a/shapefiles/test/linez.dbf b/shapefiles/test/linez.dbf index 46a339e..dcc50fd 100644 Binary files a/shapefiles/test/linez.dbf and b/shapefiles/test/linez.dbf differ diff --git a/shapefiles/test/multipatch.dbf b/shapefiles/test/multipatch.dbf index 822cd31..e506fd7 100644 Binary files a/shapefiles/test/multipatch.dbf and b/shapefiles/test/multipatch.dbf differ diff --git a/shapefiles/test/multipoint.dbf b/shapefiles/test/multipoint.dbf index 8b7d2e1..9ddef88 100644 Binary files a/shapefiles/test/multipoint.dbf and b/shapefiles/test/multipoint.dbf differ diff --git a/shapefiles/test/onlydbf.dbf b/shapefiles/test/onlydbf.dbf index 25aa008..15229fa 100644 Binary files a/shapefiles/test/onlydbf.dbf and b/shapefiles/test/onlydbf.dbf differ diff --git a/shapefiles/test/point.dbf b/shapefiles/test/point.dbf index 0bc56c5..fa058bc 100644 Binary files a/shapefiles/test/point.dbf and b/shapefiles/test/point.dbf differ diff --git a/shapefiles/test/polygon.dbf b/shapefiles/test/polygon.dbf index 0427d4f..20dc6ae 100644 Binary files a/shapefiles/test/polygon.dbf and b/shapefiles/test/polygon.dbf differ diff --git a/shapefiles/test/shapetype.dbf b/shapefiles/test/shapetype.dbf index 25aa008..15229fa 100644 Binary files a/shapefiles/test/shapetype.dbf and b/shapefiles/test/shapetype.dbf differ diff --git a/shapefiles/test/testfile.dbf b/shapefiles/test/testfile.dbf index 25aa008..15229fa 100644 Binary files a/shapefiles/test/testfile.dbf and b/shapefiles/test/testfile.dbf differ diff --git a/test_shapefile.py b/test_shapefile.py index 81de1a6..21a4217 100644 --- a/test_shapefile.py +++ b/test_shapefile.py @@ -126,6 +126,28 @@ ], ]} ), + (shapefile.POLYGON, # multi polygon, nested exteriors with holes (unordered and tricky holes designed to throw off ring_sample() test) + [(1,1),(1,9),(9,9),(9,1),(1,1), # exterior 1 + (3,3),(3,7),(7,7),(7,3),(3,3), # exterior 2 + (4.5,4.5),(4.5,5.5),(5.5,5.5),(5.5,4.5),(4.5,4.5), # exterior 3 + (4,4),(4,4),(6,4),(6,4),(6,4),(6,6),(4,6),(4,4), # hole 2.1 (hole has duplicate coords) + (2,2),(3,3),(4,2),(8,2),(8,8),(4,8),(2,8),(2,4),(2,2), # hole 1.1 (hole coords form straight line and starts in concave orientation) + ], + [0,5,10,15,20+3], + {'type':'MultiPolygon','coordinates':[ + [ # poly 1 + [(1,1),(1,9),(9,9),(9,1),(1,1)], # exterior 1 + [(2,2),(3,3),(4,2),(8,2),(8,8),(4,8),(2,8),(2,4),(2,2)], # hole 1.1 + ], + [ # poly 2 + [(3,3),(3,7),(7,7),(7,3),(3,3)], # exterior 2 + [(4,4),(4,4),(6,4),(6,4),(6,4),(6,6),(4,6),(4,4)], # hole 2.1 + ], + [ # poly 3 + [(4.5,4.5),(4.5,5.5),(5.5,5.5),(5.5,4.5),(4.5,4.5)], # exterior 3 + ], + ]} + ), (shapefile.POLYGON, # multi polygon, holes incl orphaned holes (unordered), should raise warning [(1,1),(1,9),(9,9),(9,1),(1,1), # exterior 1 (11,11),(11,19),(19,19),(19,11),(11,11), # exterior 2 @@ -152,7 +174,7 @@ ], ]} ), - (shapefile.POLYGON, # multi polygon, exteriors with wrong orientation (be nice and interpret as such) + (shapefile.POLYGON, # multi polygon, exteriors with wrong orientation (be nice and interpret as such), should raise warning [(1,1),(9,1),(9,9),(1,9),(1,1), # exterior with hole-orientation (11,11),(19,11),(19,19),(11,19),(11,11), # exterior with hole-orientation ],