source: inundation/utilities/polygon.py @ 3390

Last change on this file since 3390 was 3054, checked in by duncan, 19 years ago

removing geo-ref from inside_polygon

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