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

Last change on this file since 7317 was 7317, checked in by rwilson, 15 years ago

Replaced 'print' statements with log.critical() calls.

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