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

Last change on this file since 7102 was 6994, checked in by ole, 16 years ago

More robust read_polygon and meaningful error_message when files can't be read.

File size: 39.5 KB
Line 
1#!/usr/bin/env python
2"""Polygon manipulations
3"""
4
5import Numeric as num
6
7from math import sqrt
8from anuga.utilities.numerical_tools import ensure_numeric
9from anuga.geospatial_data.geospatial_data import ensure_absolute, Geospatial_data
10
11
12def point_on_line(point, line, rtol=1.0e-5, atol=1.0e-8):
13    """Determine whether a point is on a line segment
14
15    Input:
16        point is given by [x, y]
17        line is given by [x0, y0], [x1, y1]] or
18        the equivalent 2x2 Numeric array with each row corresponding to a point.
19
20    Output:
21
22    Note: Line can be degenerate and function still works to discern coinciding points from non-coinciding.
23    """
24
25    point = ensure_numeric(point)
26    line = ensure_numeric(line)
27
28    res = _point_on_line(point[0], point[1],
29                         line[0,0], line[0,1],
30                         line[1,0], line[1,1],
31                         rtol, atol)
32   
33    return bool(res)
34
35
36######
37# Result functions used in intersection() below for collinear lines.
38# (p0,p1) defines line 0, (p2,p3) defines line 1.
39######
40
41# result functions for possible states
42def lines_dont_coincide(p0,p1,p2,p3):               return (3, None)
43def lines_0_fully_included_in_1(p0,p1,p2,p3):       return (2, num.array([p0,p1]))
44def lines_1_fully_included_in_0(p0,p1,p2,p3):       return (2, num.array([p2,p3]))
45def lines_overlap_same_direction(p0,p1,p2,p3):      return (2, num.array([p0,p3]))
46def lines_overlap_same_direction2(p0,p1,p2,p3):     return (2, num.array([p2,p1]))
47def lines_overlap_opposite_direction(p0,p1,p2,p3):  return (2, num.array([p0,p2]))
48def lines_overlap_opposite_direction2(p0,p1,p2,p3): return (2, num.array([p3,p1]))
49
50# this function called when an impossible state is found
51def lines_error(p1, p2, p3, p4): raise RuntimeError, ("INTERNAL ERROR: p1=%s, p2=%s, p3=%s, p4=%s" %
52                                                      (str(p1), str(p2), str(p3), str(p4)))
53
54#                     0s1    0e1    1s0    1e0   # line 0 starts on 1, 0 ends 1, 1 starts 0, 1 ends 0
55collinear_result = { (False, False, False, False): lines_dont_coincide,
56                     (False, False, False, True ): lines_error,
57                     (False, False, True,  False): lines_error,
58                     (False, False, True,  True ): lines_1_fully_included_in_0,
59                     (False, True,  False, False): lines_error,
60                     (False, True,  False, True ): lines_overlap_opposite_direction2,
61                     (False, True,  True,  False): lines_overlap_same_direction2,
62                     (False, True,  True,  True ): lines_1_fully_included_in_0,
63                     (True,  False, False, False): lines_error,
64                     (True,  False, False, True ): lines_overlap_same_direction,
65                     (True,  False, True,  False): lines_overlap_opposite_direction,
66                     (True,  False, True,  True ): lines_1_fully_included_in_0,
67                     (True,  True,  False, False): lines_0_fully_included_in_1,
68                     (True,  True,  False, True ): lines_0_fully_included_in_1,
69                     (True,  True,  True,  False): lines_0_fully_included_in_1,
70                     (True,  True,  True,  True ): lines_0_fully_included_in_1
71                   }
72
73def intersection(line0, line1, rtol=1.0e-5, atol=1.0e-8):
74    """Returns intersecting point between two line segments or None
75    (if parallel or no intersection is found).
76
77    However, if parallel lines coincide partly (i.e. shara a common segment,
78    the line segment where lines coincide is returned
79   
80
81    Inputs:
82        line0, line1: Each defined by two end points as in: [[x0, y0], [x1, y1]]
83                      A line can also be a 2x2 numpy array with each row
84                      corresponding to a point.
85
86
87    Output:
88        status, value
89
90        where status and value is interpreted as follows
91       
92        status == 0: no intersection, value set to None.
93        status == 1: intersection point found and returned in value as [x,y].
94        status == 2: Collinear overlapping lines found. Value takes the form [[x0,y0], [x1,y1]].
95        status == 3: Collinear non-overlapping lines. Value set to None.
96        status == 4: Lines are parallel with a fixed distance apart. Value set to None.
97   
98    """
99
100    # FIXME (Ole): Write this in C
101
102    line0 = ensure_numeric(line0, num.Float)
103    line1 = ensure_numeric(line1, num.Float)   
104
105    x0 = line0[0,0]; y0 = line0[0,1]
106    x1 = line0[1,0]; y1 = line0[1,1]
107
108    x2 = line1[0,0]; y2 = line1[0,1]
109    x3 = line1[1,0]; y3 = line1[1,1]
110
111    denom = (y3-y2)*(x1-x0) - (x3-x2)*(y1-y0)
112    u0 = (x3-x2)*(y0-y2) - (y3-y2)*(x0-x2)
113    u1 = (x2-x0)*(y1-y0) - (y2-y0)*(x1-x0)
114       
115    if num.allclose(denom, 0.0, rtol=rtol, atol=atol):
116        # Lines are parallel - check if they are collinear
117        if num.allclose([u0, u1], 0.0, rtol=rtol, atol=atol):
118            # We now know that the lines are collinear
119            state_tuple = (point_on_line([x0, y0], line1, rtol=rtol, atol=atol),
120                           point_on_line([x1, y1], line1, rtol=rtol, atol=atol),
121                           point_on_line([x2, y2], line0, rtol=rtol, atol=atol),
122                           point_on_line([x3, y3], line0, rtol=rtol, atol=atol))
123
124            return collinear_result[state_tuple]([x0,y0],[x1,y1],[x2,y2],[x3,y3])
125        else:
126            # Lines are parallel but aren't collinear
127            return 4, None #FIXME (Ole): Add distance here instead of None
128    else:
129        # Lines are not parallel, check if they intersect
130        u0 = u0/denom
131        u1 = u1/denom       
132
133        x = x0 + u0*(x1-x0)
134        y = y0 + u0*(y1-y0)
135
136        # Sanity check - can be removed to speed up if needed
137        assert num.allclose(x, x2 + u1*(x3-x2), rtol=rtol, atol=atol)
138        assert num.allclose(y, y2 + u1*(y3-y2), rtol=rtol, atol=atol)       
139
140        # Check if point found lies within given line segments
141        if 0.0 <= u0 <= 1.0 and 0.0 <= u1 <= 1.0: 
142            # We have intersection
143            return 1, num.array([x, y])
144        else:
145            # No intersection
146            return 0, None
147
148
149def NEW_C_intersection(line0, line1):
150    #FIXME(Ole): To write in C
151    """Returns intersecting point between two line segments or None
152    (if parallel or no intersection is found).
153
154    However, if parallel lines coincide partly (i.e. shara a common segment,
155    the line segment where lines coincide is returned
156   
157
158    Inputs:
159        line0, line1: Each defined by two end points as in: [[x0, y0], [x1, y1]]
160                      A line can also be a 2x2 numeric array with each row
161                      corresponding to a point.
162
163
164    Output:
165        status, value
166
167        where status is interpreted as follows
168       
169        status == 0: no intersection with value set to None
170        status == 1: One intersection point found and returned in value as [x,y]
171        status == 2: Coinciding line segment found. Value taks the form [[x0,y0], [x1,y1]]
172        status == 3: Lines would coincide but only if extended. Value set to None
173        status == 4: Lines are parallel with a fixed distance apart. Value set to None.
174   
175    """
176
177
178    line0 = ensure_numeric(line0, num.Float)
179    line1 = ensure_numeric(line1, num.Float)   
180
181    status, value = _intersection(line0[0,0], line0[0,1],
182                                  line0[1,0], line0[1,1],
183                                  line1[0,0], line1[0,1],
184                                  line1[1,0], line1[1,1])
185
186    return status, value
187
188def is_inside_triangle(point, triangle, 
189                       closed=True, 
190                       rtol=1.0e-12,
191                       atol=1.0e-12,                     
192                       check_inputs=True, 
193                       verbose=False):
194    """Determine if one point is inside a triangle
195   
196    This uses the barycentric method:
197   
198    Triangle is A, B, C
199    Point P can then be written as
200   
201    P = A + alpha * (C-A) + beta * (B-A)
202    or if we let
203    v=P-A, v0=C-A, v1=B-A   
204   
205    v = alpha*v0 + beta*v1
206
207    Dot this equation by v0 and v1 to get two:
208   
209    dot(v0, v) = alpha*dot(v0, v0) + beta*dot(v0, v1)
210    dot(v1, v) = alpha*dot(v1, v0) + beta*dot(v1, v1)   
211   
212    or if a_ij = dot(v_i, v_j) and b_i = dot(v_i, v)
213    the matrix equation:
214   
215    a_00 a_01   alpha     b_0
216                       =
217    a_10 a_11   beta      b_1
218   
219    Solving for alpha and beta yields:
220   
221    alpha = (b_0*a_11 - b_1*a_01)/denom
222    beta =  (b_1*a_00 - b_0*a_10)/denom
223   
224    with denom = a_11*a_00 - a_10*a_01
225   
226    The point is in the triangle whenever
227    alpha and beta and their sums are in the unit interval.
228   
229    rtol and atol will determine how close the point has to be to the edge
230    before it is deemed to be on the edge.
231   
232    """
233
234    triangle = ensure_numeric(triangle)       
235    point = ensure_numeric(point, num.Float)   
236   
237    if check_inputs is True:
238        msg = 'is_inside_triangle must be invoked with one point only'
239        assert num.allclose(point.shape, [2]), msg
240   
241   
242    # Use C-implementation
243    return bool(_is_inside_triangle(point, triangle, int(closed), rtol, atol))
244   
245   
246
247    # FIXME (Ole): The rest of this function has been made
248    # obsolete by the C extension.
249   
250    # Quickly reject points that are clearly outside
251    if point[0] < min(triangle[:,0]): return False 
252    if point[0] > max(triangle[:,0]): return False   
253    if point[1] < min(triangle[:,1]): return False
254    if point[1] > max(triangle[:,1]): return False       
255       
256    # Start search   
257    A = triangle[0, :]
258    B = triangle[1, :]
259    C = triangle[2, :]
260   
261    # Now check if point lies wholly inside triangle
262    v0 = C-A
263    v1 = B-A       
264       
265    a00 = num.innerproduct(v0, v0)
266    a10 = a01 = num.innerproduct(v0, v1)
267    a11 = num.innerproduct(v1, v1)
268   
269    denom = a11*a00 - a01*a10
270   
271    if abs(denom) > 0.0:
272        v = point-A
273        b0 = num.innerproduct(v0, v)       
274        b1 = num.innerproduct(v1, v)           
275   
276        alpha = (b0*a11 - b1*a01)/denom
277        beta = (b1*a00 - b0*a10)/denom       
278
279        if (alpha > 0.0) and (beta > 0.0) and (alpha+beta < 1.0):
280            return True 
281
282
283    if closed is True:
284        # Check if point lies on one of the edges
285       
286        for X, Y in [[A,B], [B,C], [C,A]]:
287            res = _point_on_line(point[0], point[1],
288                                 X[0], X[1],
289                                 Y[0], Y[1],
290                                 rtol, atol)
291           
292            if res:
293                return True
294               
295    return False
296
297
298   
299
300   
301   
302   
303def is_inside_polygon_quick(point, polygon, closed=True, verbose=False):
304    """Determine if one point is inside a polygon
305    Both point and polygon are assumed to be numeric arrays or lists and
306    no georeferencing etc or other checks will take place.
307   
308    As such it is faster than is_inside_polygon
309    """
310
311    # FIXME(Ole): This function isn't being used
312    polygon = ensure_numeric(polygon, num.Float)
313    points = ensure_numeric(point, num.Float) # Convert point to array of points
314    points = points[num.NewAxis, :]
315    msg = 'is_inside_polygon must be invoked with one point only'
316    msg += '\nI got %s and converted to %s' % (str(point), str(points.shape))
317    assert points.shape[0] == 1 and points.shape[1] == 2, msg
318
319   
320    indices = num.zeros(1, num.Int)
321
322    count = _separate_points_by_polygon(points, polygon, indices,
323                                        int(closed), int(verbose))
324
325   
326    #indices, count = separate_points_by_polygon(points, polygon,
327    #                                            closed=closed,
328    #                                            input_checks=False,
329    #                                            verbose=verbose)
330
331    if count > 0:
332        return True
333                                               
334       
335       
336   
337def is_inside_polygon(point, polygon, closed=True, verbose=False):
338    """Determine if one point is inside a polygon
339
340    See inside_polygon for more details
341    """
342
343    indices = inside_polygon(point, polygon, closed, verbose)
344
345    if indices.shape[0] == 1:
346        return True
347    elif indices.shape[0] == 0:
348        return False
349    else:
350        msg = 'is_inside_polygon must be invoked with one point only'
351        raise msg
352   
353
354def inside_polygon(points, polygon, closed=True, verbose=False):
355    """Determine points inside a polygon
356
357       Functions inside_polygon and outside_polygon have been defined in
358       terms af separate_by_polygon which will put all inside indices in
359       the first part of the indices array and outside indices in the last
360
361       See separate_points_by_polygon for documentation
362
363       points and polygon can be a geospatial instance,
364       a list or a numeric array
365    """
366
367    #if verbose: print 'Checking input to inside_polygon'
368
369    try:
370        points = ensure_absolute(points)
371    except NameError, e:
372        raise NameError, e
373    except:
374        # If this fails it is going to be because the points can't be
375        # converted to a numeric array.
376        msg = 'Points could not be converted to Numeric array' 
377        raise msg
378
379    polygon = ensure_absolute(polygon)       
380    try:
381        polygon = ensure_absolute(polygon)
382    except NameError, e:
383        raise NameError, e
384    except:
385        # If this fails it is going to be because the points can't be
386        # converted to a numeric array.
387        msg = 'Polygon %s could not be converted to Numeric array' %(str(polygon))
388        raise msg
389
390    if len(points.shape) == 1:
391        # Only one point was passed in. Convert to array of points
392        points = num.reshape(points, (1,2))
393
394    indices, count = separate_points_by_polygon(points, polygon,
395                                                closed=closed,
396                                                verbose=verbose)
397
398    # Return indices of points inside polygon
399    return indices[:count]
400
401
402
403def is_outside_polygon(point, polygon, closed=True, verbose=False,
404                       points_geo_ref=None, polygon_geo_ref=None):
405    """Determine if one point is outside a polygon
406
407    See outside_polygon for more details
408    """
409
410    indices = outside_polygon(point, polygon, closed, verbose)
411                              #points_geo_ref, polygon_geo_ref)
412
413    if indices.shape[0] == 1:
414        return True
415    elif indices.shape[0] == 0:
416        return False
417    else:
418        msg = 'is_outside_polygon must be invoked with one point only'
419        raise msg
420   
421
422def outside_polygon(points, polygon, closed = True, verbose = False):
423    """Determine points outside a polygon
424
425       Functions inside_polygon and outside_polygon have been defined in
426       terms af separate_by_polygon which will put all inside indices in
427       the first part of the indices array and outside indices in the last
428
429       See separate_points_by_polygon for documentation
430    """
431
432    #if verbose: print 'Checking input to outside_polygon'
433    try:
434        points = ensure_numeric(points, num.Float)
435    except NameError, e:
436        raise NameError, e
437    except:
438        msg = 'Points could not be converted to Numeric array'
439        raise msg
440
441    try:
442        polygon = ensure_numeric(polygon, num.Float)
443    except NameError, e:
444        raise NameError, e
445    except:
446        msg = 'Polygon could not be converted to Numeric array'
447        raise msg
448
449
450    if len(points.shape) == 1:
451        # Only one point was passed in. Convert to array of points
452        points = num.reshape(points, (1,2))
453
454    indices, count = separate_points_by_polygon(points, polygon,
455                                                closed=closed,
456                                                verbose=verbose)
457
458    # Return indices of points outside polygon
459    if count == len(indices):
460        # No points are outside
461        return num.array([])
462    else:
463        return indices[count:][::-1]  #return reversed
464       
465
466def in_and_outside_polygon(points, polygon, closed=True, verbose=False):
467    """Determine points inside and outside a polygon
468
469       See separate_points_by_polygon for documentation
470
471       Returns an array of points inside and an array of points outside the polygon
472    """
473
474    #if verbose: print 'Checking input to outside_polygon'
475    try:
476        points = ensure_numeric(points, num.Float)
477    except NameError, e:
478        raise NameError, e
479    except:
480        msg = 'Points could not be converted to Numeric array'
481        raise msg
482
483    try:
484        polygon = ensure_numeric(polygon, num.Float)
485    except NameError, e:
486        raise NameError, e
487    except:
488        msg = 'Polygon could not be converted to Numeric array'
489        raise msg
490
491    if len(points.shape) == 1:
492        # Only one point was passed in. Convert to array of points
493        points = num.reshape(points, (1,2))
494
495
496    indices, count = separate_points_by_polygon(points, polygon,
497                                                closed=closed,
498                                                verbose=verbose)
499   
500    # Returns indices of points inside and indices of points outside
501    # the polygon
502
503    if count == len(indices):
504        # No points are outside
505        return indices[:count],[]
506    else:
507        return  indices[:count], indices[count:][::-1]  #return reversed
508
509
510def separate_points_by_polygon(points, polygon,
511                               closed=True, 
512                               check_input=True,
513                               verbose=False):
514    """Determine whether points are inside or outside a polygon
515
516    Input:
517       points - Tuple of (x, y) coordinates, or list of tuples
518       polygon - list of vertices of polygon
519       closed - (optional) determine whether points on boundary should be
520       regarded as belonging to the polygon (closed = True)
521       or not (closed = False)
522       check_input: Allows faster execution if set to False
523
524    Outputs:
525       indices: array of same length as points with indices of points falling
526       inside the polygon listed from the beginning and indices of points
527       falling outside listed from the end.
528
529       count: count of points falling inside the polygon
530
531       The indices of points inside are obtained as indices[:count]
532       The indices of points outside are obtained as indices[count:]
533
534
535    Examples:
536       U = [[0,0], [1,0], [1,1], [0,1]] #Unit square
537
538       separate_points_by_polygon( [[0.5, 0.5], [1, -0.5], [0.3, 0.2]], U)
539       will return the indices [0, 2, 1] and count == 2 as only the first
540       and the last point are inside the unit square
541
542    Remarks:
543       The vertices may be listed clockwise or counterclockwise and
544       the first point may optionally be repeated.
545       Polygons do not need to be convex.
546       Polygons can have holes in them and points inside a hole is
547       regarded as being outside the polygon.
548
549    Algorithm is based on work by Darel Finley,
550    http://www.alienryderflex.com/polygon/
551
552    Uses underlying C-implementation in polygon_ext.c
553    """
554
555    if check_input:
556        #Input checks
557
558        assert isinstance(closed, bool), 'Keyword argument "closed" must be boolean'
559        assert isinstance(verbose, bool), 'Keyword argument "verbose" must be boolean'
560
561
562        try:
563            points = ensure_numeric(points, num.Float)
564        except NameError, e:
565            raise NameError, e
566        except:
567            msg = 'Points could not be converted to Numeric array'
568            raise msg
569
570        try:
571            polygon = ensure_numeric(polygon, num.Float)
572        except NameError, e:
573            raise NameError, e
574        except:
575            msg = 'Polygon could not be converted to Numeric array'
576            raise msg
577
578        msg = 'Polygon array must be a 2d array of vertices'
579        assert len(polygon.shape) == 2, msg
580
581        msg = 'Polygon array must have two columns' 
582        assert polygon.shape[1] == 2, msg
583
584
585        msg = 'Points array must be 1 or 2 dimensional.'
586        msg += ' I got %d dimensions' %len(points.shape)
587        assert 0 < len(points.shape) < 3, msg
588
589       
590        if len(points.shape) == 1:
591            # Only one point was passed in.
592            # Convert to array of points
593            points = num.reshape(points, (1,2))
594
595   
596            msg = 'Point array must have two columns (x,y), '
597            msg += 'I got points.shape[1] == %d' %points.shape[0]
598            assert points.shape[1] == 2, msg
599
600       
601            msg = 'Points array must be a 2d array. I got %s' %str(points[:30])
602            assert len(points.shape) == 2, msg
603
604            msg = 'Points array must have two columns'
605            assert points.shape[1] == 2, msg
606
607
608    N = polygon.shape[0] # Number of vertices in polygon
609    M = points.shape[0]  # Number of points
610
611    indices = num.zeros(M, num.Int)
612
613    count = _separate_points_by_polygon(points, polygon, indices,
614                                        int(closed), int(verbose))
615
616    if verbose: print 'Found %d points (out of %d) inside polygon'\
617       %(count, M)
618    return indices, count
619
620
621def polygon_area(input_polygon):
622    """ Determin area of arbitrary polygon
623    Reference
624    http://mathworld.wolfram.com/PolygonArea.html
625    """
626   
627    # Move polygon to origin (0,0) to avoid rounding errors
628    # This makes a copy of the polygon to avoid destroying it
629    input_polygon = ensure_numeric(input_polygon)
630    min_x = min(input_polygon[:,0])
631    min_y = min(input_polygon[:,1])   
632    polygon = input_polygon - [min_x, min_y]
633
634    # Compute area   
635    n = len(polygon)
636    poly_area = 0.0
637
638    for i in range(n):
639        pti = polygon[i]
640        if i == n-1:
641            pt1 = polygon[0]
642        else:
643            pt1 = polygon[i+1]
644        xi = pti[0]
645        yi1 = pt1[1]
646        xi1 = pt1[0]
647        yi = pti[1]
648        poly_area += xi*yi1 - xi1*yi
649       
650    return abs(poly_area/2)
651
652def plot_polygons(polygons_points, style=None, 
653                  figname=None, label=None, verbose=False):
654   
655    """ Take list of polygons and plot.
656
657    Inputs:
658
659    polygons         - list of polygons
660
661    style            - style list corresponding to each polygon
662                     - for a polygon, use 'line'
663                     - for points falling outside a polygon, use 'outside'
664                       
665    figname          - name to save figure to
666
667    label            - title for plot
668
669    Outputs:
670
671    - list of min and max of x and y coordinates
672    - plot of polygons
673    """ 
674
675    from pylab import ion, hold, plot, axis, figure, legend, savefig, xlabel, ylabel, title, close, title
676
677    assert type(polygons_points) == list,\
678               'input must be a list of polygons and/or points'
679               
680    ion()
681    hold(True)
682
683    minx = 1e10
684    maxx = 0.0
685    miny = 1e10
686    maxy = 0.0
687
688    if label is None: label = ''
689
690    n = len(polygons_points)
691    colour = []
692    if style is None:
693        style_type = 'line' 
694        style = []
695        for i in range(n):
696            style.append(style_type)
697            colour.append('b-')
698    else:
699        for s in style:
700            if s == 'line': colour.append('b-')           
701            if s == 'outside': colour.append('r.')
702            if s <> 'line':
703                if s <> 'outside':
704                    colour.append('g.')
705           
706    for i, item in enumerate(polygons_points):
707        x, y = poly_xy(item)
708        if min(x) < minx: minx = min(x)
709        if max(x) > maxx: maxx = max(x)
710        if min(y) < miny: miny = min(y)
711        if max(y) > maxy: maxy = max(y)
712        plot(x,y,colour[i])
713        xlabel('x')
714        ylabel('y')
715        title(label)
716
717    #raw_input('wait 1')
718    #FIXME(Ole): This makes for some strange scalings sometimes.
719    #if minx <> 0:
720    #    axis([minx*0.9,maxx*1.1,miny*0.9,maxy*1.1])
721    #else:
722    #    if miny == 0:
723    #        axis([-maxx*.01,maxx*1.1,-maxy*0.01,maxy*1.1])
724    #    else:
725    #        axis([-maxx*.01,maxx*1.1,miny*0.9,maxy*1.1])
726
727    if figname is not None:
728        savefig(figname)
729    else:
730        savefig('test_image')
731
732    close('all')
733
734    vec = [minx,maxx,miny,maxy]
735
736    return vec
737
738def poly_xy(polygon, verbose=False):
739    """ this is used within plot_polygons so need to duplicate
740        the first point so can have closed polygon in plot
741    """
742
743    #if verbose: print 'Checking input to poly_xy'
744
745    try:
746        polygon = ensure_numeric(polygon, num.Float)
747    except NameError, e:
748        raise NameError, e
749    except:
750        msg = 'Polygon %s could not be converted to Numeric array' %(str(polygon))
751        raise msg
752
753    x = polygon[:,0]
754    y = polygon[:,1]
755    x = num.concatenate((x, [polygon[0,0]]), axis = 0)
756    y = num.concatenate((y, [polygon[0,1]]), axis = 0)
757   
758    return x, y
759   
760#    x = []
761#    y = []
762#    n = len(poly)
763#    firstpt = poly[0]
764#    for i in range(n):
765#        thispt = poly[i]
766#        x.append(thispt[0])
767#        y.append(thispt[1])
768
769#    x.append(firstpt[0])
770#    y.append(firstpt[1])
771   
772#    return x, y
773
774class Polygon_function:
775    """Create callable object f: x,y -> z, where a,y,z are vectors and
776    where f will return different values depending on whether x,y belongs
777    to specified polygons.
778
779    To instantiate:
780
781       Polygon_function(polygons)
782
783    where polygons is a list of tuples of the form
784
785      [ (P0, v0), (P1, v1), ...]
786
787      with Pi being lists of vertices defining polygons and vi either
788      constants or functions of x,y to be applied to points with the polygon.
789
790    The function takes an optional argument, default which is the value
791    (or function) to used for points not belonging to any polygon.
792    For example:
793
794       Polygon_function(polygons, default = 0.03)
795
796    If omitted the default value will be 0.0
797
798    Note: If two polygons overlap, the one last in the list takes precedence
799
800    Coordinates specified in the call are assumed to be relative to the
801    origin (georeference) e.g. used by domain.
802    By specifying the optional argument georeference,
803    all points are made relative.
804
805    FIXME: This should really work with geo_spatial point sets.
806    """
807
808    def __init__(self,
809                 regions,
810                 default=0.0,
811                 geo_reference=None):
812
813        try:
814            len(regions)
815        except:
816            msg = 'Polygon_function takes a list of pairs (polygon, value).'
817            msg += 'Got %s' %regions
818            raise msg
819
820
821        T = regions[0]
822
823        if isinstance(T, basestring):
824            msg = 'You passed in a list of text values into polygon_function'
825            msg += ' instead of a list of pairs (polygon, value): "%s"' %T
826           
827            raise Exception, msg
828       
829        try:
830            a = len(T)
831        except:
832            msg = 'Polygon_function takes a list of pairs (polygon, value).'
833            msg += 'Got %s' %str(T)
834            raise msg
835
836        msg = 'Each entry in regions have two components: (polygon, value).'
837        msg +='I got %s' %str(T)
838        assert a == 2, msg
839
840
841        if geo_reference is None:
842            from anuga.coordinate_transforms.geo_reference import Geo_reference
843            geo_reference = Geo_reference()
844
845
846        self.default = default
847
848        # Make points in polygons relative to geo_reference
849        self.regions = []
850        for polygon, value in regions:
851            P = geo_reference.change_points_geo_ref(polygon)
852            self.regions.append((P, value))
853
854
855
856
857    def __call__(self, x, y):
858        x = num.array(x, num.Float)
859        y = num.array(y, num.Float)
860
861        N = len(x)
862        assert len(y) == N
863
864        points = num.concatenate((num.reshape(x, (N, 1)),
865                                  num.reshape(y, (N, 1))), axis=1)
866
867        if callable(self.default):
868            z = self.default(x,y)
869        else:
870            z = num.ones(N, num.Float) * self.default
871
872        for polygon, value in self.regions:
873            indices = inside_polygon(points, polygon)
874
875            # FIXME: This needs to be vectorised
876            if callable(value):
877                for i in indices:
878                    xx = num.array([x[i]])
879                    yy = num.array([y[i]])
880                    z[i] = value(xx, yy)[0]
881            else:
882                for i in indices:
883                    z[i] = value
884
885        if len(z) == 0:
886            msg = 'Warning: points provided to Polygon function did not fall within'
887            msg += 'its regions'
888            msg += 'x in [%.2f, %.2f], y in [%.2f, %.2f]' % (min(x), max(x),
889                                                             min(y), max(y))
890            print msg
891
892           
893        return z
894
895
896       
897# Functions to read and write polygon information       
898def read_polygon(filename, split=','):
899    """Read points assumed to form a polygon.
900       There must be exactly two numbers in each line separated by a comma.
901       No header.
902    """
903
904    fid = open(filename)
905    lines = fid.readlines()
906    fid.close()
907    polygon = []
908    for line in lines:
909        if line.strip() == '': continue # Skip blank lines
910        fields = line.split(split)
911
912        try:
913            x, y = float(fields[0]), float(fields[1])
914        except:
915            msg = 'Could not read line %s in file %s' %(line, filename)
916            raise Exception, msg
917           
918        polygon.append([x, y])
919
920    return polygon
921
922
923def write_polygon(polygon, filename=None):
924    """Write polygon to csv file.
925       There will be exactly two numbers, easting and northing,
926       in each line separated by a comma.
927       
928       No header.   
929    """
930
931    fid = open(filename, 'w')
932    for point in polygon:
933        fid.write('%f, %f\n' %point)
934    fid.close()
935   
936
937def read_tagged_polygons(filename):
938    """
939    """
940    pass
941   
942def populate_polygon(polygon, number_of_points, seed=None, exclude=None):
943    """Populate given polygon with uniformly distributed points.
944
945    Input:
946       polygon - list of vertices of polygon
947       number_of_points - (optional) number of points
948       seed - seed for random number generator (default=None)
949       exclude - list of polygons (inside main polygon) from where points should be excluded
950
951    Output:
952       points - list of points inside polygon
953
954    Examples:
955       populate_polygon( [[0,0], [1,0], [1,1], [0,1]], 5 )
956       will return five randomly selected points inside the unit square
957    """
958
959    from random import uniform, seed as seed_function
960
961    seed_function(seed)
962
963    points = []
964
965    # Find outer extent of polygon
966    max_x = min_x = polygon[0][0]
967    max_y = min_y = polygon[0][1]
968    for point in polygon[1:]:
969        x = point[0]
970        if x > max_x: max_x = x
971        if x < min_x: min_x = x
972        y = point[1]
973        if y > max_y: max_y = y
974        if y < min_y: min_y = y
975
976
977    while len(points) < number_of_points:
978        x = uniform(min_x, max_x)
979        y = uniform(min_y, max_y)
980
981        append = False
982        if is_inside_polygon([x,y], polygon):
983
984            append = True
985
986            #Check exclusions
987            if exclude is not None:
988                for ex_poly in exclude:
989                    if is_inside_polygon([x,y], ex_poly):
990                        append = False
991
992
993        if append is True:
994            points.append([x,y])
995
996    return points
997
998
999def point_in_polygon(polygon, delta=1e-8):
1000    """Return a point inside a given polygon which will be close to the
1001    polygon edge.
1002
1003    Input:
1004       polygon - list of vertices of polygon
1005       delta - the square root of 2 * delta is the maximum distance from the
1006       polygon points and the returned point.
1007    Output:
1008       points - a point inside polygon
1009
1010       searches in all diagonals and up and down (not left and right)
1011    """
1012    import exceptions
1013    class Found(exceptions.Exception): pass
1014
1015    polygon = ensure_numeric(polygon)
1016   
1017    point_in = False
1018    while not point_in:
1019        try:
1020            for poly_point in polygon: #[1:]:
1021                for x_mult in range (-1,2):
1022                    for y_mult in range (-1,2):
1023                        x = poly_point[0]
1024                        y = poly_point[1]
1025                        if x == 0:
1026                            x_delta = x_mult*delta
1027                        else:
1028                            x_delta = x+x_mult*x*delta
1029
1030                        if y == 0:
1031                            y_delta = y_mult*delta
1032                        else:
1033                            y_delta = y+y_mult*y*delta
1034
1035                        point = [x_delta, y_delta]
1036                        if is_inside_polygon(point, polygon, closed=False):
1037                            raise Found
1038        except Found:
1039            point_in = True
1040        else:
1041            delta = delta*0.1
1042    return point
1043
1044
1045def number_mesh_triangles(interior_regions, bounding_poly, remainder_res):
1046    """Calculate the approximate number of triangles inside the
1047    bounding polygon and the other interior regions
1048
1049    Polygon areas are converted to square Kms
1050
1051    FIXME: Add tests for this function
1052    """
1053   
1054    from anuga.utilities.polygon import polygon_area
1055
1056
1057    # TO DO check if any of the regions fall inside one another
1058
1059    print '----------------------------------------------------------------------------'
1060    print 'Polygon   Max triangle area (m^2)   Total area (km^2)   Estimated #triangles'
1061    print '----------------------------------------------------------------------------'   
1062       
1063    no_triangles = 0.0
1064    area = polygon_area(bounding_poly)
1065   
1066    for poly, resolution in interior_regions:
1067        this_area = polygon_area(poly)
1068        this_triangles = this_area/resolution
1069        no_triangles += this_triangles
1070        area -= this_area
1071       
1072        print 'Interior ',
1073        print ('%.0f' %resolution).ljust(25),
1074        print ('%.2f' %(this_area/1000000)).ljust(19),
1075        print '%d' %(this_triangles)
1076       
1077    bound_triangles = area/remainder_res
1078    no_triangles += bound_triangles
1079
1080    print 'Bounding ',
1081    print ('%.0f' %remainder_res).ljust(25),
1082    print ('%.2f' %(area/1000000)).ljust(19),
1083    print '%d' %(bound_triangles)   
1084
1085    total_number_of_triangles = no_triangles/0.7
1086
1087    print 'Estimated total number of triangles: %d' %total_number_of_triangles
1088    print 'Note: This is generally about 20% less than the final amount'   
1089
1090    return int(total_number_of_triangles)
1091
1092
1093def decimate_polygon(polygon, factor=10):
1094    """Reduce number of points in polygon by the specified
1095    factor (default=10, hence the name of the function) such that
1096    the extrema in both axes are preserved.
1097
1098    Return reduced polygon
1099    """
1100
1101    # FIXME(Ole): This doesn't work at present,
1102    # but it isn't critical either
1103
1104    # Find outer extent of polygon
1105    num_polygon = ensure_numeric(polygon)
1106    max_x = max(num_polygon[:,0])
1107    max_y = max(num_polygon[:,1])
1108    min_x = min(num_polygon[:,0])
1109    min_y = min(num_polygon[:,1])       
1110
1111    # Keep only some points making sure extrema are kept
1112    reduced_polygon = []   
1113    for i, point in enumerate(polygon):
1114        x = point[0]
1115        y = point[1]       
1116        if x in [min_x, max_x] and y in [min_y, max_y]:
1117            # Keep
1118            reduced_polygon.append(point)
1119        else:
1120            if len(reduced_polygon)*factor < i:
1121                reduced_polygon.append(point)               
1122
1123    return reduced_polygon
1124   
1125   
1126       
1127
1128##
1129# @brief Interpolate linearly from polyline nodes to midpoints of triangles.
1130# @param data The data on the polyline nodes.
1131# @param polyline_nodes ??
1132# @param gauge_neighbour_id ??  FIXME(Ole): I want to get rid of this
1133# @param point_coordinates ??
1134# @param verbose True if this function is to be verbose.
1135def interpolate_polyline(data,
1136                         polyline_nodes,
1137                         gauge_neighbour_id,
1138                         interpolation_points=None,
1139                         rtol=1.0e-6,
1140                         atol=1.0e-8,
1141                         verbose=False):
1142    """Interpolate linearly between values data on polyline nodes
1143    of a polyline to list of interpolation points.
1144
1145    data is the data on the polyline nodes.
1146
1147
1148    Inputs:
1149      data: Vector or array of data at the polyline nodes.
1150      polyline_nodes: Location of nodes where data is available.     
1151      gauge_neighbour_id: ?
1152      interpolation_points: Interpolate polyline data to these positions.
1153          List of coordinate pairs [x, y] of
1154          data points or an nx2 Numeric array or a Geospatial_data object
1155      rtol, atol: Used to determine whether a point is on the polyline or not. See point_on_line.
1156
1157    Output:
1158      Interpolated values at interpolation points
1159    """
1160   
1161    if isinstance(interpolation_points, Geospatial_data):
1162        interpolation_points = interpolation_points.get_data_points(absolute=True)
1163
1164    interpolated_values = num.zeros(len(interpolation_points), num.Float)
1165
1166    data = ensure_numeric(data, num.Float)
1167    polyline_nodes = ensure_numeric(polyline_nodes, num.Float)
1168    interpolation_points = ensure_numeric(interpolation_points, num.Float)
1169    gauge_neighbour_id = ensure_numeric(gauge_neighbour_id, num.Int)
1170
1171    n = polyline_nodes.shape[0] # Number of nodes in polyline       
1172    # Input sanity check
1173    msg = 'interpolation_points are not given (interpolate.py)'
1174    assert interpolation_points is not None, msg
1175    msg = 'function value must be specified at every interpolation node'
1176    assert data.shape[0]==polyline_nodes.shape[0], msg
1177    msg = 'Must define function value at one or more nodes'
1178    assert data.shape[0]>0, msg
1179
1180    if n == 1:
1181        msg = 'Polyline contained only one point. I need more. ' + str(data)
1182        raise Exception, msg
1183    elif n > 1:
1184        _interpolate_polyline(data,
1185                              polyline_nodes,
1186                              gauge_neighbour_id,
1187                              interpolation_points,                               
1188                              interpolated_values,
1189                              rtol,
1190                              atol)
1191       
1192    return interpolated_values
1193
1194       
1195def _interpolate_polyline(data,
1196                          polyline_nodes, 
1197                          gauge_neighbour_id, 
1198                          interpolation_points, 
1199                          interpolated_values,
1200                          rtol=1.0e-6,
1201                          atol=1.0e-8):
1202    """Auxiliary function used by interpolate_polyline
1203   
1204    NOTE: OBSOLETED BY C-EXTENSION
1205    """
1206   
1207    number_of_nodes = len(polyline_nodes)               
1208    number_of_points = len(interpolation_points)
1209   
1210    for j in range(number_of_nodes):               
1211        neighbour_id = gauge_neighbour_id[j]
1212       
1213        # FIXME(Ole): I am convinced that gauge_neighbour_id can be discarded, but need to check with John J.
1214        # Keep it for now (17 Jan 2009)
1215        # When gone, we can simply interpolate between neighbouring nodes, i.e. neighbour_id = j+1.
1216        # and the test below becomes something like: if j < number_of_nodes... 
1217       
1218        if neighbour_id >= 0:
1219            x0, y0 = polyline_nodes[j,:]
1220            x1, y1 = polyline_nodes[neighbour_id,:]
1221           
1222            segment_len = sqrt((x1-x0)**2 + (y1-y0)**2)
1223            segment_delta = data[neighbour_id] - data[j]           
1224            slope = segment_delta/segment_len
1225           
1226               
1227            for i in range(number_of_points):               
1228               
1229                x, y = interpolation_points[i,:]
1230                if point_on_line([x, y], 
1231                                 [[x0, y0], [x1, y1]], 
1232                                 rtol=rtol,
1233                                 atol=atol):
1234                                 
1235
1236                    dist = sqrt((x-x0)**2 + (y-y0)**2)
1237                    interpolated_values[i] = slope*dist + data[j]
1238     
1239
1240   
1241
1242##############################################
1243#Initialise module
1244
1245from anuga.utilities import compile
1246if compile.can_use_C_extension('polygon_ext.c'):
1247    # Underlying C implementations can be accessed
1248    from polygon_ext import _point_on_line
1249    from polygon_ext import _separate_points_by_polygon
1250    from polygon_ext import _interpolate_polyline   
1251    from polygon_ext import _is_inside_triangle       
1252    #from polygon_ext import _intersection
1253
1254else:
1255    msg = 'C implementations could not be accessed by %s.\n ' %__file__
1256    msg += 'Make sure compile_all.py has been run as described in '
1257    msg += 'the ANUGA installation guide.'
1258    raise Exception, msg
1259
1260
1261if __name__ == "__main__":
1262    pass
Note: See TracBrowser for help on using the repository browser.