source: inundation/utilities/polygon.py @ 3028

Last change on this file since 3028 was 2955, checked in by nick, 19 years ago

update pt hedland
geospatial_data object updated
updated polygon.py to allow numeric arrays

File size: 19.4 KB
Line 
1#!/usr/bin/env python
2"""Polygon manipulations
3
4"""
5
6
7try:
8    from scipy import Float, Int, zeros, ones, array, concatenate, reshape, dot
9except:
10    #print 'Could not find scipy - using Numeric'
11    from Numeric import Float, Int, zeros, ones, array, concatenate, reshape, dot
12
13
14from math import sqrt
15from utilities.numerical_tools import ensure_numeric
16
17
18def point_on_line(x, y, x0, y0, x1, y1):
19    """Determine whether a point is on a line segment
20
21    Input: x, y, x0, x0, x1, y1: where
22        point is given by x, y
23        line is given by (x0, y0) and (x1, y1)
24
25    """
26
27    a = array([x - x0, y - y0])
28    a_normal = array([a[1], -a[0]])
29
30    b = array([x1 - x0, y1 - y0])
31
32    if dot(a_normal, b) == 0:
33        #Point is somewhere on the infinite extension of the line
34
35        len_a = sqrt(sum(a**2))
36        len_b = sqrt(sum(b**2))
37        if dot(a, b) >= 0 and len_a <= len_b:
38           return True
39        else:
40           return False
41    else:
42      return False
43
44
45def inside_polygon(points, polygon, closed = True, verbose = False):
46    """Determine points inside a polygon
47
48       Functions inside_polygon and outside_polygon have been defined in
49       terms af separate_by_polygon which will put all inside indices in
50       the first part of the indices array and outside indices in the last
51
52       See separate_points_by_polygon for documentation
53    """
54
55    if verbose: print 'Checking input to inside_polygon'
56
57    try:
58        points = ensure_numeric(points, Float)
59    except NameError, e:
60        raise NameError, e
61    except:
62        msg = 'Points could not be converted to Numeric array'
63        raise msg
64
65    try:
66        polygon = ensure_numeric(polygon, Float)
67    except NameError, e:
68        raise NameError, e
69    except:
70        msg = 'Polygon %s could not be converted to Numeric array' %(str(polygon))
71        raise msg
72
73
74
75    if len(points.shape) == 1:
76        one_point = True
77        points = reshape(points, (1,2))
78    else:
79        one_point = False
80
81    indices, count = separate_points_by_polygon(points, polygon,
82                                                closed=closed,
83                                                verbose=verbose)
84
85    if one_point:
86        return count == 1
87    else:
88        return indices[:count]
89
90def outside_polygon(points, polygon, closed = True, verbose = False):
91    """Determine points outside a polygon
92
93       Functions inside_polygon and outside_polygon have been defined in
94       terms af separate_by_polygon which will put all inside indices in
95       the first part of the indices array and outside indices in the last
96
97       See separate_points_by_polygon for documentation
98    """
99
100    if verbose: print 'Checking input to outside_polygon'
101    try:
102        points = ensure_numeric(points, Float)
103    except NameError, e:
104        raise NameError, e
105    except:
106        msg = 'Points could not be converted to Numeric array'
107        raise msg
108
109    try:
110        polygon = ensure_numeric(polygon, Float)
111    except NameError, e:
112        raise NameError, e
113    except:
114        msg = 'Polygon could not be converted to Numeric array'
115        raise msg
116
117
118
119    if len(points.shape) == 1:
120        one_point = True
121        points = reshape(points, (1,2))
122    else:
123        one_point = False
124
125    indices, count = separate_points_by_polygon(points, polygon,
126                                                closed=closed,
127                                                verbose=verbose)
128
129   
130    if one_point:
131        return count != 1
132    else:
133        if count == len(indices):
134            # No points are outside
135            return []
136        else:
137            return indices[count:][::-1]  #return reversed
138       
139
140def in_and_outside_polygon(points, polygon, closed = True, verbose = False):
141    """Determine points inside and outside a polygon
142
143       See separate_points_by_polygon for documentation
144
145       Returns an array of points inside and an array of points outside the polygon
146    """
147
148    if verbose: print 'Checking input to outside_polygon'
149    try:
150        points = ensure_numeric(points, Float)
151    except NameError, e:
152        raise NameError, e
153    except:
154        msg = 'Points could not be converted to Numeric array'
155        raise msg
156
157    try:
158        polygon = ensure_numeric(polygon, Float)
159    except NameError, e:
160        raise NameError, e
161    except:
162        msg = 'Polygon could not be converted to Numeric array'
163        raise msg
164
165
166
167    if len(points.shape) == 1:
168        one_point = True
169        points = reshape(points, (1,2))
170    else:
171        one_point = False
172
173    indices, count = separate_points_by_polygon(points, polygon,
174                                                closed=closed,
175                                                verbose=verbose)
176    # Returns an array of points inside and an array of points outside
177    # the polygon
178 
179    if count == len(indices):
180        # No points are outside
181        return indices[:count],[]
182    else:
183        return  indices[:count], indices[count:][::-1]  #return reversed
184
185
186def separate_points_by_polygon(points, polygon,
187                               closed = True, verbose = False):
188    """Determine whether points are inside or outside a polygon
189
190    Input:
191       points - Tuple of (x, y) coordinates, or list of tuples
192       polygon - list of vertices of polygon
193       closed - (optional) determine whether points on boundary should be
194       regarded as belonging to the polygon (closed = True)
195       or not (closed = False)
196
197    Outputs:
198       indices: array of same length as points with indices of points falling
199       inside the polygon listed from the beginning and indices of points
200       falling outside listed from the end.
201
202       count: count of points falling inside the polygon
203
204       The indices of points inside are obtained as indices[:count]
205       The indices of points outside are obtained as indices[count:]
206
207
208    Examples:
209       U = [[0,0], [1,0], [1,1], [0,1]] #Unit square
210
211       separate_points_by_polygon( [[0.5, 0.5], [1, -0.5], [0.3, 0.2]], U)
212       will return the indices [0, 2, 1] and count == 2 as only the first
213       and the last point are inside the unit square
214
215    Remarks:
216       The vertices may be listed clockwise or counterclockwise and
217       the first point may optionally be repeated.
218       Polygons do not need to be convex.
219       Polygons can have holes in them and points inside a hole is
220       regarded as being outside the polygon.
221
222    Algorithm is based on work by Darel Finley,
223    http://www.alienryderflex.com/polygon/
224
225    """
226
227    #Input checks
228
229
230    try:
231        points = ensure_numeric(points, Float)
232    except NameError, e:
233        raise NameError, e
234    except:
235        msg = 'Points could not be converted to Numeric array'
236        raise msg
237
238    try:
239        polygon = ensure_numeric(polygon, Float)
240    except NameError, e:
241        raise NameError, e
242    except:
243        msg = 'Polygon could not be converted to Numeric array'
244        raise msg
245
246    assert len(polygon.shape) == 2,\
247       'Polygon array must be a 2d array of vertices'
248
249    assert polygon.shape[1] == 2,\
250       'Polygon array must have two columns'
251
252    assert len(points.shape) == 2,\
253       'Points array must be a 2d array'
254
255    assert points.shape[1] == 2,\
256       'Points array must have two columns'
257
258    N = polygon.shape[0] #Number of vertices in polygon
259    M = points.shape[0]  #Number of points
260
261    px = polygon[:,0]
262    py = polygon[:,1]
263
264    #Used for an optimisation when points are far away from polygon
265    minpx = min(px); maxpx = max(px)
266    minpy = min(py); maxpy = max(py)
267
268
269    #Begin main loop
270    indices = zeros(M, Int)
271
272    inside_index = 0    #Keep track of points inside
273    outside_index = M-1 #Keep track of points outside (starting from end)
274
275    for k in range(M):
276
277        if verbose:
278            if k %((M+10)/10)==0: print 'Doing %d of %d' %(k, M)
279
280        #for each point
281        x = points[k, 0]
282        y = points[k, 1]
283
284        inside = False
285
286        if not x > maxpx or x < minpx or y > maxpy or y < minpy:
287            #Check polygon
288            for i in range(N):
289                j = (i+1)%N
290
291                #Check for case where point is contained in line segment
292                if point_on_line(x, y, px[i], py[i], px[j], py[j]):
293                    if closed:
294                        inside = True
295                    else:
296                        inside = False
297                    break
298                else:
299                    #Check if truly inside polygon
300                    if py[i] < y and py[j] >= y or\
301                       py[j] < y and py[i] >= y:
302                        if px[i] + (y-py[i])/(py[j]-py[i])*(px[j]-px[i]) < x:
303                            inside = not inside
304
305        if inside:
306            indices[inside_index] = k
307            inside_index += 1
308        else:
309            indices[outside_index] = k
310            outside_index -= 1
311
312    return indices, inside_index
313
314
315def separate_points_by_polygon_c(points, polygon,
316                                 closed = True, verbose = False):
317    """Determine whether points are inside or outside a polygon
318
319    C-wrapper
320    """
321
322
323    if verbose: print 'Checking input to separate_points_by_polygon'
324
325
326    #Input checks
327
328    assert isinstance(closed, bool), 'Keyword argument "closed" must be boolean'
329    assert isinstance(verbose, bool), 'Keyword argument "verbose" must be boolean'
330
331
332    try:
333        points = ensure_numeric(points, Float)
334    except NameError, e:
335        raise NameError, e
336    except:
337        msg = 'Points could not be converted to Numeric array'
338        raise msg
339
340    #if verbose: print 'Checking input to separate_points_by_polygon 2'
341    try:
342        polygon = ensure_numeric(polygon, Float)
343    except NameError, e:
344        raise NameError, e
345    except:
346        msg = 'Polygon could not be converted to Numeric array'
347        raise msg
348
349    if verbose: print 'check'
350
351    assert len(polygon.shape) == 2,\
352       'Polygon array must be a 2d array of vertices'
353
354    assert polygon.shape[1] == 2,\
355       'Polygon array must have two columns'
356
357    assert len(points.shape) == 2,\
358       'Points array must be a 2d array'
359
360    assert points.shape[1] == 2,\
361       'Points array must have two columns'
362
363    N = polygon.shape[0] #Number of vertices in polygon
364    M = points.shape[0]  #Number of points
365
366    from polygon_ext import separate_points_by_polygon
367
368    if verbose: print 'Allocating array for indices'
369
370    indices = zeros( M, Int )
371
372    if verbose: print 'Calling C-version of inside poly'
373
374    count = separate_points_by_polygon(points, polygon, indices,
375                                       int(closed), int(verbose))
376
377    return indices, count
378
379
380def polygon_area(polygon):
381    """ Determin area of arbitrary polygon
382    Reference
383    http://mathworld.wolfram.com/PolygonArea.html
384    """
385   
386    n = len(polygon)
387    poly_area = 0.0
388
389    for i in range(n):
390        pti = polygon[i]
391        if i == n-1:
392            pt1 = polygon[0]
393        else:
394            pt1 = polygon[i+1]
395        xi = pti[0]
396        yi1 = pt1[1]
397        xi1 = pt1[0]
398        yi = pti[1]
399        poly_area += xi*yi1 - xi1*yi
400       
401    return abs(poly_area/2)
402
403def plot_polygons(polygons, figname, verbose=False):
404   
405    """ Take list of polygons and plot.
406
407    Inputs:
408
409    polygons         - list of polygons
410                       
411    figname          - name to save figure to
412
413    Outputs:
414
415    - list of min and max of x and y coordinates
416    - plot of polygons
417    """ 
418
419    from pylab import ion, hold, plot, axis, figure, legend, savefig, xlabel, ylabel, title, close
420
421    assert type(polygons) == list,\
422               'input must be a list of polygons'
423               
424    ion()
425    hold(True)
426
427    minx = 1e10
428    maxx = 0.0
429    miny = 1e10
430    maxy = 0.0
431   
432    for polygon in polygons:
433        x, y = poly_xy(polygon) 
434        if min(x) < minx: minx = min(x)
435        if max(x) > maxx: maxx = max(x)
436        if min(y) < miny: miny = min(y)
437        if max(y) > maxy: maxy = max(y)
438        plot(x,y,'r-')
439        xlabel('x')
440        ylabel('y')
441
442    savefig(figname)
443
444    close('all')
445
446    vec = [minx,maxx,miny,maxy]
447
448    return vec
449
450def poly_xy(polygon, verbose=False):
451    """ this is used within plot_polygons so need to duplicate
452        the first point so can have closed polygon in plot
453    """
454
455    if verbose: print 'Checking input to poly_xy'
456
457    try:
458        polygon = ensure_numeric(polygon, Float)
459    except NameError, e:
460        raise NameError, e
461    except:
462        msg = 'Polygon %s could not be converted to Numeric array' %(str(polygon))
463        raise msg
464
465    x = polygon[:,0]
466    y = polygon[:,1]
467    x = concatenate((x, [polygon[0,0]]), axis = 0)
468    y = concatenate((y, [polygon[0,1]]), axis = 0)
469   
470    return x, y
471   
472#    x = []
473#    y = []
474#    n = len(poly)
475#    firstpt = poly[0]
476#    for i in range(n):
477#        thispt = poly[i]
478#        x.append(thispt[0])
479#        y.append(thispt[1])
480
481#    x.append(firstpt[0])
482#    y.append(firstpt[1])
483   
484#    return x, y
485
486class Polygon_function:
487    """Create callable object f: x,y -> z, where a,y,z are vectors and
488    where f will return different values depending on whether x,y belongs
489    to specified polygons.
490
491    To instantiate:
492
493       Polygon_function(polygons)
494
495    where polygons is a list of tuples of the form
496
497      [ (P0, v0), (P1, v1), ...]
498
499      with Pi being lists of vertices defining polygons and vi either
500      constants or functions of x,y to be applied to points with the polygon.
501
502    The function takes an optional argument, default which is the value
503    (or function) to used for points not belonging to any polygon.
504    For example:
505
506       Polygon_function(polygons, default = 0.03)
507
508    If omitted the default value will be 0.0
509
510    Note: If two polygons overlap, the one last in the list takes precedence
511
512    Coordinates specified in the call are assumed to be relative to the origin (georeference)
513    e.g. used by domain. By specifying the optional argument georeference, all points are made relative.
514
515    FIXME: This should really work with geo_spatial point sets.
516    """
517
518    def __init__(self, regions, default = 0.0, geo_reference = None):
519
520        try:
521            len(regions)
522        except:
523            msg = 'Polygon_function takes a list of pairs (polygon, value). Got %s' %polygons
524            raise msg
525
526
527        T = regions[0]
528        try:
529            a = len(T)
530        except:
531            msg = 'Polygon_function takes a list of pairs (polygon, value). Got %s' %polygons
532            raise msg
533
534        assert a == 2, 'Must have two component each: %s' %T
535
536
537        if geo_reference is None:
538            from coordinate_transforms.geo_reference import Geo_reference
539            geo_reference = Geo_reference()
540
541
542        self.default = default
543
544        #Make points in polygons relative to geo_reference
545        self.regions = []
546        for polygon, value in regions:
547            P = geo_reference.change_points_geo_ref(polygon)
548            self.regions.append( (P, value) )
549
550
551
552
553    def __call__(self, x, y):
554        x = array(x).astype(Float)
555        y = array(y).astype(Float)
556
557        N = len(x)
558        assert len(y) == N
559
560        points = concatenate( (reshape(x, (N, 1)),
561                               reshape(y, (N, 1))), axis=1 )
562
563        if callable(self.default):
564            z = self.default(x,y)
565        else:
566            z = ones(N, Float) * self.default
567
568        for polygon, value in self.regions:
569            indices = inside_polygon(points, polygon)
570
571            #FIXME: This needs to be vectorised
572            if callable(value):
573                for i in indices:
574                    xx = array([x[i]])
575                    yy = array([y[i]])
576                    z[i] = value(xx, yy)[0]
577            else:
578                for i in indices:
579                    z[i] = value
580
581        return z
582
583def read_polygon(filename,split=','):
584    """Read points assumed to form a polygon.
585       There must be exactly two numbers in each line separated by a comma.
586       No header.
587    """
588
589    #Get polygon
590    fid = open(filename)
591    lines = fid.readlines()
592    fid.close()
593    polygon = []
594    for line in lines:
595        fields = line.split(split)
596        polygon.append( [float(fields[0]), float(fields[1])] )
597
598    return polygon
599
600def populate_polygon(polygon, number_of_points, seed = None, exclude = None):
601    """Populate given polygon with uniformly distributed points.
602
603    Input:
604       polygon - list of vertices of polygon
605       number_of_points - (optional) number of points
606       seed - seed for random number generator (default=None)
607       exclude - list of polygons (inside main polygon) from where points should be excluded
608
609    Output:
610       points - list of points inside polygon
611
612    Examples:
613       populate_polygon( [[0,0], [1,0], [1,1], [0,1]], 5 )
614       will return five randomly selected points inside the unit square
615    """
616
617    from random import uniform, seed as seed_function
618
619    seed_function(seed)
620
621    points = []
622
623    #Find outer extent of polygon
624    max_x = min_x = polygon[0][0]
625    max_y = min_y = polygon[0][1]
626    for point in polygon[1:]:
627        x = point[0]
628        if x > max_x: max_x = x
629        if x < min_x: min_x = x
630        y = point[1]
631        if y > max_y: max_y = y
632        if y < min_y: min_y = y
633
634
635    while len(points) < number_of_points:
636        x = uniform(min_x, max_x)
637        y = uniform(min_y, max_y)
638
639        append = False
640        if inside_polygon( [x,y], polygon ):
641
642            append = True
643
644            #Check exclusions
645            if exclude is not None:
646                for ex_poly in exclude:
647                    if inside_polygon( [x,y], ex_poly ):
648                        append = False
649
650
651        if append is True:
652            points.append([x,y])
653
654    return points
655
656def point_in_polygon(polygon, delta=1e-8):
657    """Return a point inside a given polygon which will be close to the
658    polygon edge.
659
660    Input:
661       polygon - list of vertices of polygon
662       delta - the square root of 2 * delta is the maximum distance from the
663       polygon points and the returned point.
664    Output:
665       points - a point inside polygon
666
667       searches in all diagonals and up and down (not left and right)
668    """
669    import exceptions
670    class Found(exceptions.Exception): pass
671
672    point_in = False
673    while not point_in:
674        try:
675            for poly_point in polygon: #[1:]:
676                for x_mult in range (-1,2):
677                    for y_mult in range (-1,2):
678                        x = poly_point[0]
679                        y = poly_point[1]
680                        if x == 0:
681                            x_delta = x_mult*delta
682                        else:
683                            x_delta = x+x_mult*x*delta
684
685                        if y == 0:
686                            y_delta = y_mult*delta
687                        else:
688                            y_delta = y+y_mult*y*delta
689
690                        point = [x_delta, y_delta]
691                        #print "point",point
692                        if inside_polygon( point, polygon, closed = False ):
693                            raise Found
694        except Found:
695            point_in = True
696        else:
697            delta = delta*0.1
698    return point
699
700
701
702##############################################
703#Initialise module
704
705import compile
706if compile.can_use_C_extension('polygon_ext.c'):
707    from polygon_ext import point_on_line
708    separate_points_by_polygon = separate_points_by_polygon_c
709
710
711if __name__ == "__main__":
712    pass
Note: See TracBrowser for help on using the repository browser.