text: #!/usr/bin/env python
  # -*- coding: utf-8 -*-
  
  # Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
  
  from gerber.utils import inch, metric, write_gerber_value
  from gerber.cam import FileSettings
  from gerberex.utility import is_equal_point, is_equal_value, normalize_vec2d, dot_vec2d
  from gerberex.excellon import CoordinateStmtEx
  
  class DxfPath(object):
      def __init__(self, statements, error_range=0):
          self.statements = statements
          self.error_range = error_range
          self.bounding_box = statements[0].bounding_box
          self.containers = []
          for statement in statements[1:]:
              self._merge_bounding_box(statement.bounding_box)
      
      @property
      def start(self):
          return self.statements[0].start
      
      @property
      def end(self):
          return self.statements[-1].end
  
      @property
      def is_closed(self):
          if len(self.statements) == 1:
              return self.statements[0].is_closed
          else:
              return is_equal_point(self.start, self.end, self.error_range)
      
      def is_equal_to(self, target, error_range=0):
          if not isinstance(target, DxfPath):
              return False
          if len(self.statements) != len(target.statements):
              return False
          if is_equal_point(self.start, target.start, error_range) and \
             is_equal_point(self.end, target.end, error_range):
              for i in range(0, len(self.statements)):
                 if not self.statements[i].is_equal_to(target.statements[i], error_range):
                     return False
              return True
          elif is_equal_point(self.start, target.end, error_range) and \
               is_equal_point(self.end, target.start, error_range):
              for i in range(0, len(self.statements)):
                 if not self.statements[i].is_equal_to(target.statements[-1 - i], error_range):
                     return False
              return True
          return False
      
      def contain(self, target, error_range=0):
          for statement in self.statements:
              if statement.is_equal_to(target, error_range):
                  return True
          else:
              return False
  
      def to_inch(self):
          self.error_range = inch(self.error_range)
          for statement in self.statements:
              statement.to_inch()
  
      def to_metric(self):
          self.error_range = metric(self.error_range)
          for statement in self.statements:
              statement.to_metric()
  
      def offset(self, offset_x, offset_y):
          for statement in self.statements:
              statement.offset(offset_x, offset_y)
  
      def rotate(self, angle, center=(0, 0)):
          for statement in self.statements:
              statement.rotate(angle, center)
  
      def reverse(self):
          rlist = []
          for statement in reversed(self.statements):
              statement.reverse()
              rlist.append(statement)
          self.statements = rlist
      
      def merge(self, element, error_range=0):
          if self.is_closed or element.is_closed:
              return False
          if not error_range:
              error_range = self.error_range
          if is_equal_point(self.end, element.start, error_range):
              return self._append_at_end(element, error_range)
          elif is_equal_point(self.end, element.end, error_range):
              element.reverse()
              return self._append_at_end(element, error_range)
          elif is_equal_point(self.start, element.end, error_range):
              return self._insert_on_top(element, error_range)
          elif is_equal_point(self.start, element.start, error_range):
              element.reverse()
              return self._insert_on_top(element, error_range)
          else:
              return False
      
      def _append_at_end(self, element, error_range=0):
          if isinstance(element, DxfPath):
              if self.is_equal_to(element, error_range):
                  return False
              for i in range(0, min(len(self.statements), len(element.statements))):
                  if not self.statements[-1 - i].is_equal_to(element.statements[i]):
                      break
              for j in range(0, min(len(self.statements), len(element.statements))):
                  if not self.statements[j].is_equal_to(element.statements[-1 - j]):
                      break
              if i + j >= len(element.statements):
                  return False
              mergee = list(element.statements)
              if i > 0:
                  del mergee[0:i]
                  del self.statements[-i]
              if j > 0:
                  del mergee[-j]
                  del self.statements[0:j]
              for statement in mergee:
                  self._merge_bounding_box(statement.bounding_box)
              self.statements.extend(mergee)
              return True
          else:
              if self.statements[-1].is_equal_to(element, error_range) or \
                 self.statements[0].is_equal_to(element, error_range):
                  return False
              self._merge_bounding_box(element.bounding_box)
              self.statements.appen(element)
              return True
  
      def _insert_on_top(self, element, error_range=0):
          if isinstance(element, DxfPath):
              if self.is_equal_to(element, error_range):
                  return False
              for i in range(0, min(len(self.statements), len(element.statements))):
                  if not self.statements[-1 - i].is_equal_to(element.statements[i]):
                      break
              for j in range(0, min(len(self.statements), len(element.statements))):
                  if not self.statements[j].is_equal_to(element.statements[-1 - j]):
                      break
              if i + j >= len(element.statements):
                  return False
              mergee = list(element.statements)
              if i > 0:
                  del mergee[0:i]
                  del self.statements[-i]
              if j > 0:
                  del mergee[-j]
                  del self.statements[0:j]
              self.statements[0:0] = mergee
              return True
          else:
              if self.statements[-1].is_equal_to(element, error_range) or \
                 self.statements[0].is_equal_to(element, error_range):
                  return False
              self.statements.insert(0, element)
              return True
  
      def _merge_bounding_box(self, box):
          self.bounding_box = (min(self.bounding_box[0], box[0]),
                               min(self.bounding_box[1], box[1]),
                               max(self.bounding_box[2], box[2]),
                               max(self.bounding_box[3], box[3]))
  
      def may_be_in_collision(self, path):
          if self.bounding_box[0] >= path.bounding_box[2] or \
             self.bounding_box[1] >= path.bounding_box[3] or \
             self.bounding_box[2] <= path.bounding_box[0] or \
             self.bounding_box[3] <= path.bounding_box[1]:
              return False
          else:
              return True
  
      def to_gerber(self, settings=FileSettings(), pitch=0, width=0):
          from gerberex.dxf import DxfArcStatement
          if pitch == 0:
              x0, y0 = self.statements[0].start
              gerber = 'G01*\nX{0}Y{1}D02*\nG75*'.format(
                  write_gerber_value(x0, settings.format,
                                     settings.zero_suppression),
                  write_gerber_value(y0, settings.format,
                                     settings.zero_suppression),
              )
  
              for statement in self.statements:
                  x0, y0 = statement.start
                  x1, y1 = statement.end
                  if isinstance(statement, DxfArcStatement):
                      xc, yc = statement.center
                      gerber += '\nG{0}*\nX{1}Y{2}I{3}J{4}D01*'.format(
                          '03' if statement.end_angle > statement.start_angle else '02',
                          write_gerber_value(x1, settings.format,
                                             settings.zero_suppression),
                          write_gerber_value(y1, settings.format,
                                             settings.zero_suppression),
                          write_gerber_value(xc - x0, settings.format,
                                             settings.zero_suppression),
                          write_gerber_value(yc - y0, settings.format,
                                             settings.zero_suppression)
                      )
                  else:
                      gerber += '\nG01*\nX{0}Y{1}D01*'.format(
                          write_gerber_value(x1, settings.format,
                                             settings.zero_suppression),
                          write_gerber_value(y1, settings.format,
                                             settings.zero_suppression),
                      )
          else:
              def ploter(x, y):
                  return 'X{0}Y{1}D03*\n'.format(
                      write_gerber_value(x, settings.format,
                                         settings.zero_suppression),
                      write_gerber_value(y, settings.format,
                                            settings.zero_suppression),
                  )
              gerber = self._plot_dots(pitch, width, ploter)
  
          return gerber
  
      def to_excellon(self, settings=FileSettings(), pitch=0, width=0):
          from gerberex.dxf import DxfArcStatement
          if pitch == 0:
              x0, y0 = self.statements[0].start
              excellon = 'G00{0}\nM15\n'.format(
                  CoordinateStmtEx(x=x0, y=y0).to_excellon(settings))
  
              for statement in self.statements:
                  x0, y0 = statement.start
                  x1, y1 = statement.end
                  if isinstance(statement, DxfArcStatement):
                      i = statement.center[0] - x0
                      j = statement.center[1] - y0
                      excellon += '{0}{1}\n'.format(
                          'G03' if statement.end_angle > statement.start_angle else 'G02',
                          CoordinateStmtEx(x=x1, y=y1, i=i, j=j).to_excellon(settings))
                  else:
                      excellon += 'G01{0}\n'.format(
                          CoordinateStmtEx(x=x1, y=y1).to_excellon(settings))
              
              excellon += 'M16\nG05\n'
          else:
              def ploter(x, y):
                  return CoordinateStmtEx(x=x, y=y).to_excellon(settings) + '\n'
              excellon = self._plot_dots(pitch, width, ploter)
  
          return excellon
  
      def _plot_dots(self, pitch, width, ploter):
          out = ''
          offset = 0
          for idx in range(0, len(self.statements)):
              statement = self.statements[idx]
              if offset < 0:
                  offset += pitch
              for dot, offset in statement.dots(pitch, width, offset):
                  if dot is None:
                      break
                  if offset > 0 and (statement.is_closed or idx != len(self.statements) - 1):
                      break
                  #if idx == len(self.statements) - 1 and statement.is_closed and offset > -pitch:
                  #    break
                  out += ploter(dot[0], dot[1])
          return out
  
      def intersections_with_halfline(self, point_from, point_to, error_range=0):
          def calculator(statement):
              return statement.intersections_with_halfline(point_from, point_to, error_range)
          def validator(pt, statement, idx):
              if is_equal_point(pt, statement.end, error_range) and \
                  not self._judge_cross(point_from, point_to, idx, error_range):
                      return False
              return True
          return self._collect_intersections(calculator, validator, error_range)
  
      def intersections_with_arc(self, center, radius, angle_regions, error_range=0):
          def calculator(statement):
              return statement.intersections_with_arc(center, radius, angle_regions, error_range)
          return self._collect_intersections(calculator, None, error_range)
  
      def _collect_intersections(self, calculator, validator, error_range):
          allpts = []
          last = allpts
          for i in range(0, len(self.statements)):
              statement = self.statements[i]
              cur = calculator(statement)
              if cur:
                  for pt in cur:
                      for dest in allpts:
                          if is_equal_point(pt, dest, error_range):
                              break
                      else:
                          if validator is not None and not validator(pt, statement, i):
                              continue
                          allpts.append(pt)
              last = cur
          return allpts
      
      def _judge_cross(self, from_pt, to_pt, index, error_range):
          standard = normalize_vec2d((to_pt[0] - from_pt[0], to_pt[1] - from_pt[1]))
          normal = (standard[1], -standard[0])
          def statements():
              for i in range(index, len(self.statements)):
                  yield self.statements[i]
              for i in range(0, index):
                  yield self.statements[i]
          dot_standard = None
          for statement in statements():
              tstart = statement.start
              tend = statement.end
              target = normalize_vec2d((tend[0] - tstart[0], tend[1] - tstart[1]))
              dot= dot_vec2d(normal, target)
              if dot_standard is None:
                  dot_standard = dot
                  continue
              if is_equal_point(standard, target, error_range):
                  continue
              return (dot_standard > 0 and dot > 0) or (dot_standard < 0 and dot < 0)
          raise Exception('inconsistensy is detected while cross judgement between paths')
              
  def generate_paths(statements, error_range=0):
      from gerberex.dxf import DxfPolylineStatement
  
      paths = []
      for statement in filter(lambda s: isinstance(s, DxfPolylineStatement), statements):
          units = [unit for unit in statement.disassemble()]
          paths.append(DxfPath(units, error_range))
  
      unique_statements = []
      redundant = 0
      for statement in filter(lambda s: not isinstance(s, DxfPolylineStatement), statements):
          for path in paths:
              if path.contain(statement):
                  redundant += 1
                  break
          else:
              for target in unique_statements:
                  if statement.is_equal_to(target, error_range):
                      redundant += 1
                      break
              else:
                  unique_statements.append(statement)
  
      paths.extend([DxfPath([s], error_range) for s in unique_statements])
  
      prev_paths_num = 0
      while prev_paths_num != len(paths):
          working = []
          for i in range(len(paths)):
              mergee = paths[i]
              for j in range(i + 1, len(paths)):
                  target = paths[j]
                  if target.merge(mergee, error_range):
                      break
              else:
                  working.append(mergee)
          prev_paths_num = len(paths)
          paths = working
      
      closed_path = list(filter(lambda p: p.is_closed, paths))
      open_path = list(filter(lambda p: not p.is_closed, paths))
      return (closed_path, open_path)
  
  def judge_containment(path1, path2, error_range=0):
      from gerberex.dxf import DxfArcStatement, DxfLineStatement
  
      nocontainment = (None, None)
      if not path1.may_be_in_collision(path2):
          return nocontainment
      
      def is_in_line_segment(point_from, point_to, point):
          dx = point_to[0] - point_from[0]
          ratio = (point[0] - point_from[0]) / dx if dx != 0 else \
                  (point[1] - point_from[1]) / (point_to[1] - point_from[1])
          return ratio >= 0 and ratio <= 1
  
      def contain_in_path(statement, path):
          if isinstance(statement, DxfLineStatement):
              segment = (statement.start, statement.end)
          elif isinstance(statement, DxfArcStatement):
              if statement.start == statement.end:
                  segment = (statement.start, statement.center)
              else:
                  segment = (statement.start, statement.end)
          else:
              raise Exception('invalid dxf statement type')
          pts = path.intersections_with_halfline(segment[0], segment[1], error_range)
          if len(pts) % 2 == 0:
              return False
          for pt in pts:
              if is_in_line_segment(segment[0], segment[1], pt):
                  return False
          if isinstance(statement, DxfArcStatement):
              pts = path.intersections_with_arc(
                  statement.center, statement.radius, statement.angle_regions, error_range)
              if len(pts) > 0:
                  return False
          return True
      
      if contain_in_path(path1.statements[0], path2):
          containment = [path1, path2]
      elif contain_in_path(path2.statements[0], path1):
          containment = [path2, path1]
      else:
          return nocontainment
      for i in range(1, len(containment[0].statements)):
          if not contain_in_path(containment[0].statements[i], containment[1]):
              return nocontainment
      return containment
  