#!/usr/bin/env python
"""
General geometry support routines.

--------------------------------------------------------------------

This program is licensed under the GNU General Public License (GPL)
Version 3.  See http://www.fsf.org for details of the license.

Rugged Circuits LLC
http://ruggedcircuits.com/gerbmerge
"""

import math

# Ensure all list elements are unique
def uniqueify(L):
  return {}.fromkeys(L).keys()

# This function rounds an (X,Y) point to integer co-ordinates
def roundPoint(pt):
  return (int(round(pt[0])),int(round(pt[1])))

# Returns True if the segment defined by endpoints p1 and p2 is vertical
def isSegmentVertical(p1, p2):
  return p1[0]==p2[0]

# Returns True if the segment defined by endpoints p1 and p2 is horizontal
def isSegmentHorizontal(p1, p2):
  return p1[1]==p2[1]

# Returns slope of a non-vertical line segment
def segmentSlope(p1, p2):
  return float(p2[1]-p1[1])/(p2[0]-p1[0])

# Determine if the (X,Y) 'point' is on the line segment defined by endpoints p1
# and p2, both (X,Y) tuples. It's assumed that the point is on the line defined
# by the segment, but just may be beyond the endpoints. NOTE: No testing is
# performed to see if the point is actually on the line defined by the segment!
# This is assumed!
def isPointOnSegment(point, p1, p2):
  if isSegmentVertical(p1,p2):
    # Treat vertical lines by comparing Y-ordinates
    return (point[1]-p2[1])*(point[1]-p1[1]) <= 0
  else:
    # Treat other lines, including horizontal lines, by comparing X-ordinates
    return (point[0]-p2[0])*(point[0]-p1[0]) <= 0

# Returns (X,Y) point where the line segment defined by (X,Y) endpoints p1 and
# p2 intersects the line segment defined by endpoints q1 and q2. Only a single
# intersection point is allowed, so no coincident lines.  If there is no point
# of intersection, None is returned.
def segmentXsegment1pt(p1, p2, q1, q2):
  A,B = p1
  C,D = p2
  P,Q = q1
  R,S = q2
  
  # We have to consider special cases of one or other line segments being vertical
  if isSegmentVertical(p1,p2):
    if isSegmentVertical(q1,q2): return None

    x = A
    y = segmentSlope(q1,q2)*(A-P) + Q
  elif isSegmentVertical(q1,q2):
    x = P
    y = segmentSlope(p1,p2)*(P-A) + B
  else:
    m1 = segmentSlope(p1,p2)
    m2 = segmentSlope(q1,q2)

    if m1==m2: return None

    x = (A*m1 - B - P*m2 + Q) / (m1-m2)
    y = m1*(x-A) + B

  # Candidate point identified. Check to make sure it's on both line segments.
  if isPointOnSegment((x,y), p1, p2) and isPointOnSegment((x,y), q1, q2):
    return roundPoint((x,y))
  else:
    return None

# Returns True if the given (X,Y) 'point' is strictly within the rectangle
# defined by (LLX,LLY,URX,URY) co-ordinates (LL=lower left, UR=upper right).
def isPointStrictlyInRectangle(point, rect):
  x,y = point
  llx,lly,urx,ury = rect
  return (llx < x < urx) and (lly < y < ury)

# This function takes two points which define the extents of a rectangle.  The
# return value is a 5-tuple (ll, ul, ur, lr, rect) which comprises 4 points
# (lower-left, upper-left, upper-right, lower-right) and a rect object (minx,
# miny, maxx, maxy). If called with a single argument, it is expected to be
# a 4-tuple (x1,y1,x2,y2).
def canonicalizeExtents(pt1, pt2=None):
  # First canonicalize lower-left and upper-right points
  if pt2 is None:
    maxx = max(pt1[0], pt1[2])
    minx = min(pt1[0], pt1[2])
    maxy = max(pt1[1], pt1[3])
    miny = min(pt1[1], pt1[3])
  else:
    maxx = max(pt1[0], pt2[0])
    minx = min(pt1[0], pt2[0])
    maxy = max(pt1[1], pt2[1])
    miny = min(pt1[1], pt2[1])

  # Construct the four corners
  llpt = (minx,miny)
  urpt = (maxx,maxy)
  ulpt = (minx,maxy)
  lrpt = (maxx,miny)

  # Construct a rect object for use by various functions
  rect = (minx, miny, maxx, maxy)

  return (llpt, ulpt, urpt, lrpt, rect)

# This function returns a list of intersection points of the line segment
# pt1-->pt2 and the box defined by corners llpt and urpt. These corners are
# canonicalized internally so they need not necessarily be lower-left and
# upper-right points.
#
# The return value may be a list of 0, 1, or 2 points.  If the list has 2
# points, then the segment intersects the box in two points since both points
# are outside the box. If the list has 1 point, then the segment has one point
# inside the box and another point outside. If the list is empty, the segment
# has both points outside the box and there is no intersection, or has both
# points inside the box.
#
# Note that segments collinear with box edges produce no points of
# intersection.
def segmentXbox(pt1, pt2, llpt, urpt):
  # First canonicalize lower-left and upper-right points
  llpt, ulpt, urpt, lrpt, rect = canonicalizeExtents(llpt, urpt)

  # Determine whether one point is inside the rectangle and the other is not.
  # Note the XOR operator '^'
  oneInOneOut = isPointStrictlyInRectangle(pt1,rect) ^ isPointStrictlyInRectangle(pt2,rect)

  # Find all intersections of the segment with the 4 sides of the box,
  # one side at a time. L will be the list of definitely-true intersections,
  # while corners is a list of potential intersections. An intersection
  # is potential if a) it is a corner, and b) there is another intersection
  # of the line with the box somewhere else. This is how we handle
  # corner intersections, which are sometimes legal (when one segment endpoint
  # is inside the box and the other isn't, or when the segment intersects the
  # box in two places) and sometimes not (when the segment is "tangent" to
  # the box at the corner and the corner is the signle point of intersection).
  L = []
  corners = []

  # Do not allow intersection if segment is collinear with box sides.  For
  # example, a horizontal line collinear with the box top side should not
  # return an intersection with the upper-left or upper-right corner.
  # Similarly, a point of intersection that is a corner should only be
  # allowed if one segment point is inside the box and the other is not,
  # otherwise it means the segment is "tangent" to the box at that corner.
  # There is a case, however, in which a corner is a point of intersection
  # with both segment points outside the box, and that is if there are two
  # points of intersection, i.e., the segment goes completely through the box.

  def checkIntersection(corner1, corner2):
    # Check intersection with side of box
    pt = segmentXsegment1pt(pt1, pt2, corner1, corner2)
    if pt in (corner1,corner2):
      # Only allow this corner intersection point if line is not
      # horizontal/vertical and one point is inside rectangle while other is
      # not, or the segment intersects the box in two places. Since oneInOneOut
      # calls isPointStrictlyInRectangle(), which automatically excludes points
      # on the box itself, horizontal/vertical lines collinear with box sides
      # will always lead to oneInOneOut==False (since both will be "out of
      # box").
      if oneInOneOut:
        L.append(pt)
      else:
        corners.append(pt)    # Potentially a point of intersection...we'll have to wait and
                              # see if there is one more point of intersection somewhere else.
    else:
      # Not a corner intersection, so it's valid
      if pt is not None: L.append(pt)

  # Check intersection with left side of box
  checkIntersection(llpt, ulpt)

  # Check intersection with top side of box
  checkIntersection(ulpt, urpt)

  # Check intersection with right side of box
  checkIntersection(urpt, lrpt)

  # Check intersection with bottom side of box
  checkIntersection(llpt, lrpt)

  # Ensure all points are unique. We may get a double hit at the corners
  # of the box.
  L = uniqueify(L)
  corners = uniqueify(corners)

  # If the total number of intersections len(L)+len(corners) is 2, the corner
  # is valid. If there is only a single corner, it's a tangent and invalid.
  # However, if both corners are on the same side of the box, it's not valid.
  numPts = len(L)+len(corners)
  assert numPts <= 2
  if numPts == 2:
    if len(corners)==2 and (isSegmentHorizontal(corners[0], corners[1]) or isSegmentVertical(corners[0],corners[1])):
      return []
    else:
      L += corners
      L.sort()      # Just for stability in assertion checking
      return L
  else:
    L.sort()
    return L      # Correct if numPts==1, since it will be empty or contain a single valid intersection
                  # Correct if numPts==0, since it will be empty

# This function determines if two rectangles defined by 4-tuples
# (minx, miny, maxx, maxy) have any rectangle in common. If so, it is
# returned as a 4-tuple, else None is returned. This function assumes
# the rectangles are canonical so that minx<maxx, miny<maxy. If the
# optional allowLines parameter is True, rectangles that overlap on
# a line are considered overlapping, otherwise they must overlap with
# a rectangle of at least width 1.
def areExtentsOverlapping(E1, E2, allowLines=False):
  minX,minY,maxX,maxY = E1
  minU,minV,maxU,maxV = E2

  if allowLines:
    if (minU > maxX) or (maxU < minX) or (minV > maxY) or (maxV < minY):
      return False
    else:
      return True
  else:
    if (minU >= maxX) or (maxU <= minX) or (minV >= maxY) or (maxV <= minY):
      return False
    else:
      return True

# Compute the intersection of two rectangles defined by 4-tuples E1 and E2,
# which are not necessarily canonicalized.
def intersectExtents(E1, E2):
  ll1, ul1, ur1, lr1, rect1 = canonicalizeExtents(E1)
  ll2, ul2, ur2, lr2, rect2 = canonicalizeExtents(E2)

  if not areExtentsOverlapping(rect1, rect2):
    return None

  xll = max(rect1[0], rect2[0])    # Maximum of minx values
  yll = max(rect1[1], rect2[1])    # Maximum of miny values
  xur = min(rect1[2], rect2[2])    # Minimum of maxx values
  yur = min(rect1[3], rect2[3])    # Minimum of maxy values
  return (xll, yll, xur, yur)

# This function returns True if rectangle E1 is wholly contained within
# rectangle E2. Both E1 and E2 are 4-tuples (minx,miny,maxx,maxy), not
# necessarily canonicalized. This function is like a slightly faster
# version of "intersectExtents(E1, E2)==E1".
def isRect1InRect2(E1, E2):
  ll1, ul1, ur1, lr1, rect1 = canonicalizeExtents(E1)
  ll2, ul2, ur2, lr2, rect2 = canonicalizeExtents(E2)
  
  return (ll1[0] >= ll2[0]) and (ll1[1] >= ll2[1]) \
     and (ur1[0] <= ur2[0]) and (ur1[1] <= ur2[1])

# Return width of rectangle, which may be 0 if bottom-left and upper-right X
# positions are the same. The rectangle is a 4-tuple (minx,miny,maxx,maxy).
def rectWidth(rect):
  return abs(rect[2]-rect[0])

# Return height of rectangle, which may be 0 if bottom-left and upper-right Y
# positions are the same. The rectangle is a 4-tuple (minx,miny,maxx,maxy).
def rectHeight(rect):
  return abs(rect[3]-rect[1])

# Return center (X,Y) co-ordinates of rectangle.
def rectCenter(rect):
  dx = rectWidth(rect)
  dy = rectHeight(rect)

  if dx & 1:    # Odd width: center is (left+right)/2 + 1/2
    X = (rect[0] + rect[2] + 1)/2
  else:         # Even width: center is (left+right)/2
    X = (rect[0] + rect[2])/2

  if dy & 1:
    Y = (rect[1] + rect[3] + 1)/2
  else:
    Y = (rect[1] + rect[3])/2

  return (X,Y)

if __name__=="__main__":
  llpt = (1000,1000)
  urpt = (5000,5000)

  # A segment that cuts across the box and intersects in corners
  assert segmentXbox((0,0), (6000,6000), llpt, urpt) == [(1000,1000), (5000,5000)]  # Two valid corners
  assert segmentXbox((0,6000), (6000,0), llpt, urpt) == [(1000,5000), (5000,1000)]  # Two valid corners
  assert segmentXbox((500,500), (2500, 2500), llpt, urpt) == [(1000,1000)]          # One valid corner
  assert segmentXbox((2500,2500), (5500, 5500), llpt, urpt) == [(5000,5000)]        # One valid corner

  # Segments collinear with box sides
  assert segmentXbox((1000,0),    (1000,6000), llpt, urpt) == []    # Box side contained in segment
  assert segmentXbox((1000,0),    (1000,3000), llpt, urpt) == []    # Box side partially overlaps segment
  assert segmentXbox((1000,2000), (1000,4000), llpt, urpt) == []    # Segment contained in box side

  # Segments fully contained within box
  assert segmentXbox((1500,2000), (2000,2500), llpt, urpt) == []

  # Segments with points on box sides
  assert segmentXbox((2500,1000), (2700,1200), llpt, urpt) == [(2500,1000)]   # One point on box side
  assert segmentXbox((2500,1000), (2700,5000), llpt, urpt) == [(2500,1000), (2700,5000)]   # Two points on box sides

  # Segment intersects box at one point
  assert segmentXbox((3500,5500), (3000, 2500), llpt, urpt) == [(3417, 5000)]  # First point outside
  assert segmentXbox((3500,1500), (3000, 6500), llpt, urpt) == [(3150, 5000)]  # Second point outside

  # Segment intersects box at two points, not corners
  assert segmentXbox((500,3000), (1500,500), llpt, urpt) == [(1000,1750), (1300,1000)]
  assert segmentXbox((2500,300), (5500,3500), llpt, urpt) == [(3156,1000), (5000,2967)]
  assert segmentXbox((5200,1200), (2000,6000), llpt, urpt) == [(2667,5000), (5000, 1500)]
  assert segmentXbox((3200,5200), (-10, 1200), llpt, urpt) == [(1000, 2459), (3040, 5000)]

  assert segmentXbox((500,2000), (5500, 2000), llpt, urpt) == [(1000,2000), (5000, 2000)]
  assert segmentXbox((5200,1250), (-200, 4800), llpt, urpt) == [(1000, 4011), (5000, 1381)]

  assert segmentXbox((1300,200), (1300, 5200), llpt, urpt) == [(1300, 1000), (1300, 5000)]
  assert segmentXbox((1200,200), (1300, 5200), llpt, urpt) == [(1216, 1000), (1296, 5000)]

  assert intersectExtents( (100,100,500,500), (500,500,900,900) ) == None
  assert intersectExtents( (100,100,500,500), (400,400,900,900) ) == (400,400,500,500)
  assert intersectExtents( (100,100,500,500), (200,0,600,300) ) == (200,100,500,300)
  assert intersectExtents( (100,100,500,500), (200,0,300,600) ) == (200,100,300,500)

  assert intersectExtents( (100,100,500,500), (0,600,50,550) ) == None
  assert intersectExtents( (100,100,500,500), (0,600,600,-10) ) == (100,100,500,500)
  assert intersectExtents( (100,100,500,500), (0,600,600,200) ) == (100,200,500,500)
  assert intersectExtents( (100,100,500,500), (0,600,300,300) ) == (100,300,300,500)

  assert isRect1InRect2( (100,100,500,500), (0,600,50,550) )  == False
  assert isRect1InRect2( (100,100,500,500), (0,600,600,-10) ) == True
  assert isRect1InRect2( (100,100,500,500), (0,600,600,200) ) == False
  assert isRect1InRect2( (100,100,500,500), (0,600,300,300) ) == False
  assert isRect1InRect2( (100,100,500,500), (0,0,500,500) )   == True

  print 'All tests pass'
