source: inundation/utilities/polygon.py @ 2378

Last change on this file since 2378 was 2378, checked in by ole, 19 years ago

Input checks

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