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

Last change on this file since 4845 was 4845, checked in by sexton, 16 years ago

generate plot to show interpolation points (which could be the model domain) with polygon of boundary file

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