text: import math
  
  from svg.path import parse_path
  
  DEFAULT_MOVEMENT = 1600  # If no other movement is specified, go 1600px to the right.
  POSITION_ATTRIBS = ['data-x', 'data-y', 'data-z', 'data-rotate-x',
                      'data-rotate-y', 'data-rotate-z', 'data-scale']
  
  
  def gather_positions(tree):
      """Makes a list of positions and position commands from the tree"""
      pos = {'data-x': 'r0',
             'data-y': 'r0',
             'data-z': 'r0',
             'data-rotate-x': 'r0',
             'data-rotate-y': 'r0',
             'data-rotate-z': 'r0',
             'data-scale': 'r0',
             'is_path': False
             }
  
      steps = 0
      default_movement = True
  
      for step in tree.findall('step'):
          steps += 1
  
          for key in POSITION_ATTRIBS:
              value = step.get(key)
  
              if value is not None:
                  # We have a new value
                  default_movement = False  # No longer use the default movement
                  pos[key] = value
              elif pos[key] and not pos[key].startswith('r'):
                  # The old value was absolute and no new value, so stop
                  pos[key] = 'r0'
              # We had no new value, and the old value was a relative
              # movement, so we just keep moving.
  
          if steps == 1 and pos['data-scale'] == 'r0':
              # No scale given for first slide, it needs to start at 1
              pos['data-scale'] = '1'
  
          if default_movement and steps != 1:
              # No positioning has been given, use default:
              pos['data-x'] = 'r%s' % DEFAULT_MOVEMENT
  
          if 'data-rotate' in step.attrib:
              # data-rotate is an alias for data-rotate-z
              pos['data-rotate-z'] = step.get('data-rotate')
              del step.attrib['data-rotate']
  
          if 'hovercraft-path' in step.attrib:
              # Path given x and y will be calculated from the path
              default_movement = False  # No longer use the default movement
              pos['is_path'] = True
              # Add the path spec
              pos['path'] = step.attrib['hovercraft-path']
              yield pos.copy()
              # And get rid of it for the next step
              del pos['path']
          else:
              if 'data-x' in step.attrib or 'data-y' in step.attrib:
                  # No longer using a path
                  pos['is_path'] = False
              yield pos.copy()
  
  
  def _coord_to_pos(coord):
      return {'data-x': int(coord.real), 'data-y': int(coord.imag)}
  
  
  def _pos_to_cord(coord):
      return coord['data-x'] + coord['data-y'] * 1j
  
  
  def _path_angle(path, point):
      start = point - 0.01
      end = point + 0.01
      if start < 0:
          start = 0
          end += 0.01
      elif end > 1:
          end = 1
          start -= 0.01
  
      distance = path.point(end) - path.point(start)
      hyp = math.hypot(distance.real, distance.imag)
      result = math.degrees(math.asin(distance.imag / hyp))
  
      if distance.real < 0:
          result = -180 - result
  
      if abs(result) < 0.1:
          result = 0
  
      return result
  
  
  def num(s):
      try:
          return int(s)
      except ValueError:
          return float(s)
  
  
  def _update_position(pos1, pos2):
  
      for key in POSITION_ATTRIBS:
          val = pos2.get(key)
          if val is not None:
              plus = val.find("+")
              minus = val.find("-")
              if plus > -1:
                  newval = num(val[plus+1:])
                  pos1[key + "-rel"] = val[0:plus]
              elif minus > -1 and not val.startswith("r-"):
                  newval = num(val[minus:])
                  pos1[key + "-rel"] = val[0:minus]
              else:
                  if val[0] == 'r':
                  # Relative movement
                      newval = pos1[key] + num(val[1:])
                  else:
                      newval = num(val)
                      pos1.pop(key+"-rel",  None)
              pos1[key] = newval
  
  
  def calculate_positions(positions):
      """Calculates position information"""
      current_position = {'data-x': 0,
                          'data-y': 0,
                          'data-z': 0,
                          'data-rotate-x': 0,
                          'data-rotate-y': 0,
                          'data-rotate-z': 0,
                          'data-scale': 1,
                          }
  
      positer = iter(positions)
      position = next(positer)
      _update_position(current_position, position)
  
      while True:
  
          if 'path' in position:
              # Start of a new path!
              path = position['path']
              # Follow the path specification
              first_point = _pos_to_cord(current_position)
  
              # Paths that end in Z or z are closed.
              closed_path = path.strip()[-1].upper() == 'Z'
              path = parse_path(path)
  
              # Find out how many positions should be calculated:
              count = 1
              last = False
              deferred_positions = []
              while True:
                  try:
                      position = next(positer)
                      deferred_positions.append(position)
                  except StopIteration:
                      last = True  # This path goes to the end
                      break
                  if not position.get('is_path') or 'path' in position:
                      # The end of the path, or the start of a new one
                      break
                  count += 1
  
              if count < 2:
                  raise AssertionError("The path specification is only used for "
                                       "one slide, which makes it pointless.")
  
              if closed_path:
                  # This path closes in on itself. Skip the last part, so that
                  # the first and last step doesn't overlap.
                  endcount = count + 1
              else:
                  endcount = count
  
              multiplier = (endcount * DEFAULT_MOVEMENT) / path.length()
              offset = path.point(0)
  
              path_iter = iter(deferred_positions)
              for x in range(count):
  
                  point = path.point(x / (endcount - 1))
                  point = ((point - offset) * multiplier) + first_point
  
                  current_position.update(_coord_to_pos(point))
  
                  rotation = _path_angle(path, x / (endcount - 1))
                  current_position['data-rotate-z'] = rotation
                  yield current_position.copy()
                  try:
                      position = next(path_iter)
                  except StopIteration:
                      last = True
                      break
                  _update_position(current_position, position)
  
              if last:
                  break
  
              continue
  
          yield current_position.copy()
          try:
              position = next(positer)
          except StopIteration:
              break
          _update_position(current_position, position)
  
  
  def update_positions(tree, positions):
      """Updates the tree with new positions"""
  
      for step, pos in zip(tree.findall('step'), positions):
          for key in sorted(pos):
              value = pos.get(key)
              if key.endswith("-rel"):
                  abs_key = key[:key.index("-rel")]
                  if value is not None:
                      els = tree.findall(".//*[@id='" + value + "']")
                      for el in els :
                          pos[abs_key] = num(el.get(abs_key)) + pos.get(abs_key)
                          step.attrib[abs_key] = str(pos.get(abs_key))
              else:
                  step.attrib[key] = str(pos[key])
  
          if 'hovercraft-path' in step.attrib:
              del step.attrib['hovercraft-path']
  
  
  def position_slides(tree):
      """Position the slides in the tree"""
  
      positions = gather_positions(tree)
      positions = calculate_positions(positions)
      update_positions(tree, positions)
  