source: inundation/utilities/polygon.py @ 2942

Last change on this file since 2942 was 2912, checked in by sexton, 19 years ago

incorporate plot_polygon in polygon rather than pyvolution.util; also polygon_area functionality added

File size: 19.0 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,
404                  figname,
405                  verbose = False):
406   
407    """ Take list of polygons and plot.
408
409    Inputs:
410
411    polygons         - list of polygons
412                       
413    figname          - name to save figure to
414
415    Outputs:
416
417    - list of min and max of x and y coordinates
418    - plot of polygons
419    """ 
420
421    from pylab import ion, hold, plot, axis, figure, legend, savefig, xlabel, ylabel, title, close
422
423    assert type(polygons) == list,\
424               'polygon must be a list'
425
426    ion()
427    hold(True)
428
429    minx = 1e10
430    maxx = 0.0
431    miny = 1e10
432    maxy = 0.0
433    for i in range(len(polygons)):
434        x, y = poly_xy(polygons[i]) 
435        if min(x) < minx: minx = min(x)
436        if max(x) > maxx: maxx = max(x)
437        if min(y) < miny: miny = min(y)
438        if max(y) > maxy: maxy = max(y)
439        plot(x,y,'r-')
440        xlabel('x')
441        ylabel('y')
442
443    savefig(figname)
444
445    close('all')
446
447    vec = [minx,maxx,miny,maxy]
448
449    return vec
450
451def poly_xy(poly):
452    """ this is used within plot_polygons so need to duplicate
453        the first point so can have closed polygon in plot
454    """
455
456    assert type(poly[0]) == list,\
457               'polygon must be a list of points'
458   
459    x = []
460    y = []
461    n = len(poly)
462    firstpt = poly[0]
463    for i in range(n):
464        thispt = poly[i]
465        x.append(thispt[0])
466        y.append(thispt[1])
467
468    x.append(firstpt[0])
469    y.append(firstpt[1])
470   
471    return x, y
472
473class Polygon_function:
474    """Create callable object f: x,y -> z, where a,y,z are vectors and
475    where f will return different values depending on whether x,y belongs
476    to specified polygons.
477
478    To instantiate:
479
480       Polygon_function(polygons)
481
482    where polygons is a list of tuples of the form
483
484      [ (P0, v0), (P1, v1), ...]
485
486      with Pi being lists of vertices defining polygons and vi either
487      constants or functions of x,y to be applied to points with the polygon.
488
489    The function takes an optional argument, default which is the value
490    (or function) to used for points not belonging to any polygon.
491    For example:
492
493       Polygon_function(polygons, default = 0.03)
494
495    If omitted the default value will be 0.0
496
497    Note: If two polygons overlap, the one last in the list takes precedence
498
499    Coordinates specified in the call are assumed to be relative to the origin (georeference)
500    e.g. used by domain. By specifying the optional argument georeference, all points are made relative.
501
502    FIXME: This should really work with geo_spatial point sets.
503    """
504
505    def __init__(self, regions, default = 0.0, geo_reference = None):
506
507        try:
508            len(regions)
509        except:
510            msg = 'Polygon_function takes a list of pairs (polygon, value). Got %s' %polygons
511            raise msg
512
513
514        T = regions[0]
515        try:
516            a = len(T)
517        except:
518            msg = 'Polygon_function takes a list of pairs (polygon, value). Got %s' %polygons
519            raise msg
520
521        assert a == 2, 'Must have two component each: %s' %T
522
523
524        if geo_reference is None:
525            from coordinate_transforms.geo_reference import Geo_reference
526            geo_reference = Geo_reference()
527
528
529        self.default = default
530
531        #Make points in polygons relative to geo_reference
532        self.regions = []
533        for polygon, value in regions:
534            P = geo_reference.change_points_geo_ref(polygon)
535            self.regions.append( (P, value) )
536
537
538
539
540    def __call__(self, x, y):
541        x = array(x).astype(Float)
542        y = array(y).astype(Float)
543
544        N = len(x)
545        assert len(y) == N
546
547        points = concatenate( (reshape(x, (N, 1)),
548                               reshape(y, (N, 1))), axis=1 )
549
550        if callable(self.default):
551            z = self.default(x,y)
552        else:
553            z = ones(N, Float) * self.default
554
555        for polygon, value in self.regions:
556            indices = inside_polygon(points, polygon)
557
558            #FIXME: This needs to be vectorised
559            if callable(value):
560                for i in indices:
561                    xx = array([x[i]])
562                    yy = array([y[i]])
563                    z[i] = value(xx, yy)[0]
564            else:
565                for i in indices:
566                    z[i] = value
567
568        return z
569
570def read_polygon(filename,split=','):
571    """Read points assumed to form a polygon.
572       There must be exactly two numbers in each line separated by a comma.
573       No header.
574    """
575
576    #Get polygon
577    fid = open(filename)
578    lines = fid.readlines()
579    fid.close()
580    polygon = []
581    for line in lines:
582        fields = line.split(split)
583        polygon.append( [float(fields[0]), float(fields[1])] )
584
585    return polygon
586
587def populate_polygon(polygon, number_of_points, seed = None, exclude = None):
588    """Populate given polygon with uniformly distributed points.
589
590    Input:
591       polygon - list of vertices of polygon
592       number_of_points - (optional) number of points
593       seed - seed for random number generator (default=None)
594       exclude - list of polygons (inside main polygon) from where points should be excluded
595
596    Output:
597       points - list of points inside polygon
598
599    Examples:
600       populate_polygon( [[0,0], [1,0], [1,1], [0,1]], 5 )
601       will return five randomly selected points inside the unit square
602    """
603
604    from random import uniform, seed as seed_function
605
606    seed_function(seed)
607
608    points = []
609
610    #Find outer extent of polygon
611    max_x = min_x = polygon[0][0]
612    max_y = min_y = polygon[0][1]
613    for point in polygon[1:]:
614        x = point[0]
615        if x > max_x: max_x = x
616        if x < min_x: min_x = x
617        y = point[1]
618        if y > max_y: max_y = y
619        if y < min_y: min_y = y
620
621
622    while len(points) < number_of_points:
623        x = uniform(min_x, max_x)
624        y = uniform(min_y, max_y)
625
626        append = False
627        if inside_polygon( [x,y], polygon ):
628
629            append = True
630
631            #Check exclusions
632            if exclude is not None:
633                for ex_poly in exclude:
634                    if inside_polygon( [x,y], ex_poly ):
635                        append = False
636
637
638        if append is True:
639            points.append([x,y])
640
641    return points
642
643def point_in_polygon(polygon, delta=1e-8):
644    """Return a point inside a given polygon which will be close to the
645    polygon edge.
646
647    Input:
648       polygon - list of vertices of polygon
649       delta - the square root of 2 * delta is the maximum distance from the
650       polygon points and the returned point.
651    Output:
652       points - a point inside polygon
653
654       searches in all diagonals and up and down (not left and right)
655    """
656    import exceptions
657    class Found(exceptions.Exception): pass
658
659    point_in = False
660    while not point_in:
661        try:
662            for poly_point in polygon: #[1:]:
663                for x_mult in range (-1,2):
664                    for y_mult in range (-1,2):
665                        x = poly_point[0]
666                        y = poly_point[1]
667                        if x == 0:
668                            x_delta = x_mult*delta
669                        else:
670                            x_delta = x+x_mult*x*delta
671
672                        if y == 0:
673                            y_delta = y_mult*delta
674                        else:
675                            y_delta = y+y_mult*y*delta
676
677                        point = [x_delta, y_delta]
678                        #print "point",point
679                        if inside_polygon( point, polygon, closed = False ):
680                            raise Found
681        except Found:
682            point_in = True
683        else:
684            delta = delta*0.1
685    return point
686
687
688
689##############################################
690#Initialise module
691
692import compile
693if compile.can_use_C_extension('polygon_ext.c'):
694    from polygon_ext import point_on_line
695    separate_points_by_polygon = separate_points_by_polygon_c
696
697
698if __name__ == "__main__":
699    pass
Note: See TracBrowser for help on using the repository browser.