#!/usr/bin/env python
"""This file handles the writing of the scoring lines Gerber file
--------------------------------------------------------------------

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 config
import util
import makestroke

# Add a horizontal line if its within the extents of the panel. Also, trim
# start and/or end points to the extents.
def addHorizontalLine(Lines, x1, x2, y, extents):
  assert (x1 < x2)

  # For a horizontal line, y must be above extents[1] and below extents[3].
  if extents[1] < y < extents[3]:
    # Now trim endpoints to be greater than extents[0] and below extents[2]
    line = (max(extents[0], x1), y, min(extents[2], x2), y)
    Lines.append(line)

# Add a vertical line if its within the extents of the panel. Also, trim
# start and/or end points to the extents.
def addVerticalLine(Lines, x, y1, y2, extents):
  assert (y1 < y2)

  # For a vertical line, x must be above extents[0] and below extents[2].
  if extents[0] < x < extents[2]:
    # Now trim endpoints to be greater than extents[1] and below extents[3]
    line = (x, max(extents[1], y1), x, min(extents[3], y2))
    Lines.append(line)

def isHorizontal(line):
  return line[1]==line[3]

def isVertical(line):
  return line[0]==line[2]

def clusterOrdinates(values):
  """Create a list of tuples where each tuple is a variable-length list of items
  from 'values' that are all within 2 mils of each other."""

  # First, make sure the values are sorted. Then, take the first one and go along
  # the list clustering as many as possible.
  values.sort()
  currCluster = None
  L = []
  for val in values:
    if currCluster is None:
      currCluster = (val,)
    else:
      if (val - currCluster[0]) <= 0.002:
        currCluster = currCluster + (val,)
      else:
        L.append(currCluster)
        currCluster = (val,)
    
  if currCluster is not None:
    L.append(currCluster)

  return L

def mergeHLines(Lines):
  """Lines is a list of 4-tuples (lines) that have nearly the same Y ordinate and are to be
  optimized by combining overlapping lines."""

  # First, make sure lines are sorted by starting X ordinate and that all lines
  # proceed to the right.
  Lines.sort()
  for line in Lines:
    assert line[0] < line[2]

  # Obtain the average value of the Y ordinate and use that as the Y ordinate for
  # all lines.
  yavg = 0.0
  for line in Lines:
    yavg += line[1]
  yavg /= len(Lines)

  NewLines = []

  # Now proceed to pick off one line at a time and try to merge it with
  # the next one in sequence.
  currLine = None
  for line in Lines:
    if currLine is None:
      currLine = line
    else:
      # If the line to examine starts to the left of (within 0.002") the end
      # of the current line, extend the current line.
      if line[0] <= currLine[2]+0.002:
        currLine = (currLine[0], yavg, max(line[2],currLine[2]), yavg)
      else:
        NewLines.append(currLine)
        currLine = line

  NewLines.append(currLine)

  return NewLines

def sortByY(A,B):
  "Helper function to sort two lines (4-tuples) by their starting Y ordinate"
  return cmp(A[1], B[1])

def mergeVLines(Lines):
  """Lines is a list of 4-tuples (lines) that have nearly the same X ordinate and are to be
  optimized by combining overlapping lines."""

  # First, make sure lines are sorted by starting Y ordinate and that all lines
  # proceed up.
  Lines.sort(sortByY)
  for line in Lines:
    assert line[1] < line[3]

  # Obtain the average value of the X ordinate and use that as the X ordinate for
  # all lines.
  xavg = 0.0
  for line in Lines:
    xavg += line[0]
  xavg /= len(Lines)

  NewLines = []

  # Now proceed to pick off one line at a time and try to merge it with
  # the next one in sequence.
  currLine = None
  for line in Lines:
    if currLine is None:
      currLine = line
    else:
      # If the line to examine starts below (within 0.002") the end
      # of the current line, extend the current line.
      if line[1] <= currLine[3]+0.002:
        currLine = (xavg, currLine[1], xavg, max(line[3],currLine[3]))
      else:
        NewLines.append(currLine)
        currLine = line

  NewLines.append(currLine)

  return NewLines

def mergeLines(Lines):
  # All lines extend up (vertical) and to the right (horizontal). First, do
  # simple merges. Sort all lines, which will order the lines with starting
  # points in increasing X order (i.e., to the right).
  Lines.sort()

  # Now sort the lines into horizontal lines and vertical lines. For each
  # ordinate, group all lines by that ordinate in a dictionary. Thus, all
  # horizontal lines will be grouped together by Y ordinate, and all
  # vertical lines will be grouped together by X ordinate.
  HLines = {}
  VLines = {}

  for line in Lines:
    if isHorizontal(line):
      try:
        HLines[line[1]].append(line)
      except KeyError:
        HLines[line[1]] = [line]
    else:
      try:
        VLines[line[0]].append(line)
      except KeyError:
        VLines[line[0]] = [line]

  # I don't think the next two blocks of code are necessary (merging lines
  # that are at exactly the same ordinate) since the last two blocks of
  # code do the same thing more generically by merging lines at close-enough
  # ordinates.

  # Extend horizontal lines
  NewHLines = {}
  for yval,lines in HLines.items():
    # yval is the Y ordinate of this group of lines. lines is the set of all
    # lines with this Y ordinate.
    NewHLines[yval] = []
    
    # Try to extend the first element of this list, which will be the leftmost.
    xline = lines[0]
    for line in lines[1:]:
      # If this line's left edge is within 2 mil of the right edge of the line
      # we're currently trying to grow, then grow it.
      if abs(line[0] - xline[2]) <= 0.002:    # Arbitrary 2mil?
        # Extend...
        xline = (xline[0], xline[1], line[2], xline[1])
      else:
        # ...otherwise, append the currently-extended line and make this
        # line the new one we try to extend.
        NewHLines[yval].append(xline)
        xline = line
    NewHLines[yval].append(xline)

  # Extend vertical lines
  NewVLines = {}
  for xval,lines in VLines.items():
    # xval is the X ordinate of this group of lines. lines is the set of all
    # lines with this X ordinate.
    NewVLines[xval] = []
    
    # Try to extend the first element of this list, which will be the bottom-most.
    xline = lines[0]
    for line in lines[1:]:
      # If this line's bottom edge is within 2 mil of the top edge of the line
      # we're currently trying to grow, then grow it.
      if abs(line[1] - xline[3]) <= 0.002:    # Arbitrary 2mil?
        # Extend...
        xline = (xline[0], xline[1], xline[0], line[3])
      else:
        # ...otherwise, append the currently-extended line and make this
        # line the new one we try to extend.
        NewVLines[xval].append(xline)
        xline = line
    NewVLines[xval].append(xline)

  HLines = NewHLines
  VLines = NewVLines
  NewHLines = []
  NewVLines = []

  # Now combine lines that have their endpoints either very near each other
  # or within each other. We will have to sort all horizontal lines by their
  # Y ordinates and group them according to Y ordinates that are close enough
  # to each other.
  yvals = HLines.keys()
  clusters = clusterOrdinates(yvals)  # A list of clustered tuples containing yvals

  for cluster in clusters:
    clusterLines = []
    for yval in cluster:
      clusterLines.extend(HLines[yval])

    # clusterLines is now a list of lines (4-tuples) that all have nearly the same
    # Y ordinate. Merge them together.
    NewHLines.extend(mergeHLines(clusterLines))

  xvals = VLines.keys()
  clusters = clusterOrdinates(xvals)
  for cluster in clusters:
    clusterLines = []
    for xval in cluster:
      clusterLines.extend(VLines[xval])

    # clusterLines is now a list of lines (4-tuples) that all have nearly the same
    # X ordinate. Merge them together.
    NewVLines.extend(mergeVLines(clusterLines))

  Lines = NewHLines + NewVLines

  return Lines

# Main entry point. Gerber file has already been opened, header written
# out, 1mil tool selected.
def writeScoring(fid, Place, OriginX, OriginY, MaxXExtent, MaxYExtent):
  # For each job, write out 4 score lines, above, to the right, below, and
  # to the left. After we collect all potential scoring lines, we worry
  # about merging, etc.
  dx = config.Config['xspacing']/2.0
  dy = config.Config['yspacing']/2.0
  extents = (OriginX, OriginY, MaxXExtent, MaxYExtent)

  Lines = []
  for layout in Place.jobs:
    x = layout.x - dx
    y = layout.y - dy
    X = layout.x + layout.width_in() + dx
    Y = layout.y + layout.height_in() + dy

    # Just so we don't get 3.75000000004 and 3.75000000009, we round to
    # 2.5 limits.
    x,y,X,Y = [round(val,5) for val in [x,y,X,Y]]

    if 0: # Scoring lines go all the way across the panel now
      addHorizontalLine(Lines, x, X, Y, extents)   # above job
      addVerticalLine(Lines, X, y, Y, extents)     # to the right of job
      addHorizontalLine(Lines, x, X, y, extents)   # below job
      addVerticalLine(Lines, x, y, Y, extents)     # to the left of job
    else:
      addHorizontalLine(Lines, OriginX, MaxXExtent, Y, extents)   # above job
      addVerticalLine(Lines, X, OriginY, MaxYExtent, extents)     # to the right of job
      addHorizontalLine(Lines, OriginX, MaxXExtent, y, extents)   # below job
      addVerticalLine(Lines, x, OriginY, MaxYExtent, extents)     # to the left of job

  # Combine disparate lines into single lines
  Lines = mergeLines(Lines)

  #for line in Lines:
  #  print [round(x,3) for x in line]

  # Write 'em out
  for line in Lines:
    makestroke.drawPolyline(fid, [(util.in2gerb(line[0]),util.in2gerb(line[1])), \
                                  (util.in2gerb(line[2]),util.in2gerb(line[3]))], 0, 0)

# vim: expandtab ts=2 sw=2 ai syntax=python
