source: anuga_core/source/anuga/utilities/polygon.py @ 4249

Last change on this file since 4249 was 4241, checked in by ole, 18 years ago

Small semantic change to plot_polygons allowing figname to be omitted.

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