# frozen_string_literal: true

#==========================
# svg_graph.rb
#==========================
#
# Parses an element list into an SVG tree.
# Copyright (c) 2007-2023 Yoichiro Hasebe <yohasebe@gmail.com>

require "tempfile"
require_relative 'base_graph'
require_relative 'utils'

module RSyntaxTree
  class SVGGraph < BaseGraph
    attr_accessor :width, :height

    def initialize(element_list, params, global)
      super(element_list, params, global)
      @height = 0
      @width  = 0
      @extra_lines = []
      @fontset = params[:fontset]
      @fontsize = params[:fontsize]
      @linewidth = params[:linewidth]
      @transparent = params[:transparent]
      @color = params[:color]
      @fontstyle = params[:fontstyle]
      @polyline = params[:polyline]
      @line_styles = "<line style='fill: none; stroke:#{@col_line}; stroke-width:#{@linewidth + LINE_SCALING}; stroke-linejoin:round; stroke-linecap:round;' x1='X1' y1='Y1' x2='X2' y2='Y2' />\n"
      @polyline_styles = "<polyline style='stroke:#{@col_line}; stroke-width:#{@linewidth + LINE_SCALING}; fill:none; stroke-linejoin:round; stroke-linecap:round;'
                            points='CHIX CHIY MIDX1 MIDY1 MIDX2 MIDY2 PARX PARY' />\n"
      @polygon_styles = "<polygon style='fill: none; stroke: black; stroke-width:#{@linewidth + LINE_SCALING}; stroke-linejoin:round;stroke-linecap:round;' points='X1 Y1 X2 Y2 X3 Y3' />\n"
      @text_styles = "<text white-space='pre' alignment-baseline='text-top' style='fill: COLOR; storoke-width: 0; font-size: fontsize' x='X_VALUE' y='Y_VALUE'>CONTENT</text>\n"
      @tree_data = String.new
      @visited_x = {}
      @visited_y = {}
      @global = global
    end

    def svg_data
      metrics = parse_list

      @width = metrics[:width] + @global[:h_gap_between_nodes] * 2
      @height = metrics[:height] + @global[:height_connector_to_text] / 2

      x1 = 0 - @global[:h_gap_between_nodes]
      y1 = 0
      x2 = @width + @global[:h_gap_between_nodes]
      y2 = @height + @global[:height_connector_to_text] / 2

      extra_lines = @extra_lines.join("\n")

      as2 = @global[:h_gap_between_nodes] * 1.0
      as4 = as2 * 3

      header = <<~HDR
        <?xml version="1.0" standalone="no"?>
        <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
          <svg width="#{@width}" height="#{@height}" viewBox="#{x1}, #{y1}, #{x2}, #{y2}" version="1.1" xmlns="http://www.w3.org/2000/svg">
          <defs>
            <marker id="arrow" markerUnits="userSpaceOnUse" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="#{as2}" markerHeight="#{as2}" orient="auto">
              <path d="M 0 0 L 10 5 L 0 10" fill="#{@col_extra}"/>
            </marker>
            <marker id="arrowBackward" markerUnits="userSpaceOnUse" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="#{as2}" markerHeight="#{as2}" orient="auto">
              <path d="M 0 0 L 10 5 L 0 10 z" fill="#{@col_extra}"/>
            </marker>
            <marker id="arrowForward" markerUnits="userSpaceOnUse" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="#{as2}" markerHeight="#{as2}" orient="auto">
              <path d="M 10 0 L 0 5 L 10 10 z" fill="#{@col_extra}"/>
            </marker>
            <marker id="arrowBothways" markerUnits="userSpaceOnUse" viewBox="0 0 30 10" refX="15" refY="5" markerWidth="#{as4}" markerHeight="#{as2}" orient="auto">
              <path d="M 0 5 L 10 0 L 10 5 L 20 5 L 20 0 L 30 5 L 20 10 L 20 5 L 10 5 L 10 10 z" fill="#{@col_extra}"/>
            </marker>
            <pattern id="hatchBlack" x="10" y="10" width="10" height="10" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
              <line x1="0" y="0" x2="0" y2="10" stroke="black" stroke-width="4"></line>
            </pattern>
            <pattern id="hatchForNode" x="10" y="10" width="10" height="10" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
              <line x1="0" y="0" x2="0" y2="10" stroke="#{@col_node}" stroke-width="4"></line>
            </pattern>
            <pattern id="hatchForLeaf" x="10" y="10" width="10" height="10" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
              <line x1="0" y="0" x2="0" y2="10" stroke="#{@col_leaf}" stroke-width="4"></line>
            </pattern>
          </defs>
      HDR

      rect = <<~RCT
        <rect x="#{x1}" y="#{y1}" width="#{x2}" height="#{y2}" stroke="none" fill="white" />"
      RCT

      footer = "</svg>"

      if @transparent
        header + @tree_data + extra_lines + footer
      else
        header + rect + @tree_data + extra_lines + footer
      end
    end

    def draw_a_path(s_x, s_y, t_x, t_y, target_arrow = :none)
      x_spacing = @global[:h_gap_between_nodes] * 1.25
      y_spacing = @global[:height_connector] * 0.75

      ymax = [s_y, t_y].max
      new_y = if ymax < @height
                @height + y_spacing
              else
                ymax + y_spacing
              end

      if @visited_x[s_x]
        new_s_x = s_x - x_spacing * @visited_x[s_x]
        @visited_x[s_x] += 1
      else
        new_s_x = s_x
        @visited_x[s_x] = 1
      end

      if @visited_x[t_x]
        new_t_x = t_x - x_spacing * @visited_x[t_x]
        @visited_x[t_x] += 1
      else
        new_t_x = t_x
        @visited_x[t_x] = 1
      end

      dashed = true if target_arrow == :none

      case target_arrow
      when :single
        @extra_lines << generate_line(new_s_x, s_y, new_s_x, new_y, @col_path, dashed)
        @extra_lines << generate_line(new_s_x, s_y, new_s_x, new_y, @col_path, dashed)
        @extra_lines << generate_line(new_s_x, new_y, new_t_x, new_y, @col_path, dashed)
        @extra_lines << generate_line(new_t_x, new_y, new_t_x, t_y, @col_path, dashed, true)
      when :double
        @extra_lines << generate_line(new_s_x, new_y, new_s_x, s_y, @col_path, dashed, true)
        @extra_lines << generate_line(new_s_x, new_y, new_t_x, new_y, @col_path, dashed)
        @extra_lines << generate_line(new_t_x, new_y, new_t_x, t_y, @col_path, dashed, true)
      else
        @extra_lines << generate_line(new_s_x, s_y, new_s_x, new_y, @col_path, dashed)
        @extra_lines << generate_line(new_s_x, new_y, new_t_x, new_y, @col_path, dashed)
        @extra_lines << generate_line(new_t_x, new_y, new_t_x, t_y, @col_path, dashed)
      end
      @height = new_y if new_y > @height
    end

    def draw_element(element)
      top = element.vertical_indent
      left = element.horizontal_indent
      right = left + element.content_width
      txt_pos = left + (right - left) / 2

      col = if element.type == ETYPE_LEAF
              @col_leaf
            else
              @col_node
            end

      text_data = @text_styles.sub(/COLOR/, col)
      text_data = text_data.sub(/fontsize/, @fontsize.to_s + "px;")
      text_x = txt_pos - element.content_width / 2
      text_y = top + @global[:single_line_height] - @global[:height_connector_to_text]
      text_data  = text_data.sub(/X_VALUE/, text_x.to_s)
      text_data  = text_data.sub(/Y_VALUE/, text_y.to_s)
      new_text = +""
      this_x = 0
      this_y = 0
      bc = { x: text_x - @global[:h_gap_between_nodes] / 2, y: top, width: element.content_width + @global[:h_gap_between_nodes], height: nil }
      element.content.each_with_index do |l, idx|
        case l[:type]
        when :border, :bborder
          x1 = text_x
          if idx.zero?
            text_y -= l[:height]
          else
            text_y += l[:height]
          end
          y1 = text_y - @global[:single_line_height] / 8
          x2 = text_x + element.content_width
          y2 = y1
          case l[:type]
          when :border
            stroke_width = @linewidth + LINE_SCALING
          when :bborder
            stroke_width = @linewidth + BLINE_SCALING
          end
          @extra_lines << "<line style=\"stroke:#{col}; fill:none; stroke-linecap:round; stroke-width:#{stroke_width}; \" x1=\"#{x1}\" y1=\"#{y1}\" x2=\"#{x2}\" y2=\"#{y2}\"></line>"
        else
          if element.enclosure == :brackets
            this_x = txt_pos - element.content_width / 2
          else
            ewidth = 0
            l[:elements].each do |e|
              ewidth += e[:width]
            end
            this_x = txt_pos - (ewidth / 2)
          end
          text_y += l[:elements].map { |e| e[:height] }.max if idx != 0

          l[:elements].each do |e|
            escaped_text = e[:text].gsub('>', '&gt;').gsub('<', '&lt;');
            decorations = []
            decorations << "overline" if e[:decoration].include?(:overline)
            decorations << "underline" if e[:decoration].include?(:underline)
            decorations << "line-through" if e[:decoration].include?(:linethrough)
            decoration = "text-decoration=\"" + decorations.join(" ") + "\""

            style = "style=\""
            if e[:decoration].include?(:small)
              style += "font-size: #{(SUBSCRIPT_CONST.to_f * 100).to_i}%; "
              this_y = text_y - ((@global[:single_x_metrics].height - @global[:single_x_metrics].height * SUBSCRIPT_CONST) / 4) + 2
            elsif e[:decoration].include?(:superscript)
              style += "font-size: #{(SUBSCRIPT_CONST.to_f * 100).to_i}%; "
              this_y = text_y - (@global[:single_x_metrics].height / 4) + 1
            elsif e[:decoration].include?(:subscript)
              style += "font-size: #{(SUBSCRIPT_CONST.to_f * 100).to_i}%; "
              this_y = text_y + 4
            else
              this_y = text_y
            end

            style += "font-weight: bold; fill: #{@col_emph}; " if e[:decoration].include?(:bold) || e[:decoration].include?(:bolditalic)
            style += "font-style: italic; " if e[:decoration].include?(:italic) || e[:decoration].include?(:bolditalic)
            style += "\""

            case @fontstyle
            when /(?:cjk)/
              fontstyle = "'WenQuanYi Zen Hei', 'Noto Sans', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', sans-serif"
            when /(?:mono)/
              fontstyle = if e[:cjk]
                            "'Noto Sans JP', 'Noto Sans Mono SemiCondensed', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', sans-serif"
                          else
                            "'Noto Sans Mono SemiCondensed', 'Noto Sans JP', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', sans-serif"
                          end
            when /(?:sans)/
              fontstyle = if e[:cjk]
                            "'Noto Sans JP', 'Noto Sans', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', sans-serif"
                          else
                            "'Noto Sans', 'Noto Sans JP', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', sans-serif"
                          end
            when /(?:serif)/
              fontstyle = if e[:cjk]
                            "'Noto Serif JP', 'Noto Serif', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', serif"
                          else
                            "'Noto Serif', 'Noto Serif JP', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', serif"
                          end
            end

            if e[:decoration].include?(:box) || e[:decoration].include?(:circle) || e[:decoration].include?(:bar)
              enc_height = e[:height]
              enc_y = this_y - e[:height] * 0.8
              enc_width = e[:width]
              enc_x = this_x

              if e[:decoration].include?(:hatched)
                case element.type
                when ETYPE_LEAF
                  fill = if @color
                           "url(#hatchForLeaf)"
                         else
                           "url(#hatchBlack)"
                         end
                when ETYPE_NODE
                  fill = if @color
                           "url(#hatchForNode)"
                         else
                           "url(#hatchBlack)"
                         end
                end
              else
                fill = "none"
              end

              enc = nil

              stroke_width = if e[:decoration].include?(:bstroke)
                               @linewidth + BLINE_SCALING
                             else
                               @linewidth + LINE_SCALING
                             end

              if e[:decoration].include?(:box)
                enc = "<rect style='fill: none; stroke: #{col}; stroke-linejoin:round; stroke-width:#{stroke_width};'
                        x='#{enc_x}' y='#{enc_y}'
                        width='#{enc_width}' height='#{enc_height}'
                        fill='#{fill}' />\n"
              elsif e[:decoration].include?(:circle)
                enc = "<rect style='fill: none; stroke: #{col}; stroke-width:#{stroke_width};'
                        x='#{enc_x}' y='#{enc_y}' rx='#{enc_height / 2}' ry='#{enc_height / 2}'
                        width='#{enc_width}' height='#{enc_height}'
                        fill='#{fill}' />\n"
              elsif e[:decoration].include?(:bar)
                x1 = enc_x
                y1 = enc_y + enc_height / 2
                x2 = enc_x + enc_width
                y2 = y1
                ar_hwidth = e[:width] / 4.0
                bar = "<line style='fill:none; stroke:#{col}; stroke-linejoin:round; stroke-linecap:round; stroke-width:#{stroke_width};' x1='#{x1 + stroke_width / 2}' y1='#{y1}' x2='#{x2 - stroke_width / 2}' y2='#{y2}'></line>\n"
                @extra_lines << bar

                if e[:decoration].include?(:arrow_to_l)
                  l_arrowhead = "<polyline stroke-linejoin='round' stroke-linecap='round' fill='none' stroke='#{col}' stroke-width='#{stroke_width}' points='#{x1 + ar_hwidth},#{y1 + ar_hwidth / 2} #{x1 + stroke_width / 2},#{y1} #{x1 + ar_hwidth},#{y1 - ar_hwidth / 2}' />\n"
                  @extra_lines << l_arrowhead
                end

                if e[:decoration].include?(:arrow_to_r)
                  r_arrowhead = "<polyline stroke-linejoin='round' stroke-linecap='round' fill='none' stroke='#{col}' stroke-width='#{stroke_width}' points='#{x2 - ar_hwidth},#{y2 - ar_hwidth / 2} #{x2 - stroke_width / 2},#{y2} #{x2 - ar_hwidth},#{y2 + ar_hwidth / 2}' />\n"
                  @extra_lines << r_arrowhead
                end
              end

              @extra_lines << enc if enc

              this_x += if e[:text].size == 1
                          (e[:height] - e[:content_width]) / 2
                        else
                          @global[:width_half_x] / 2
                        end

              new_text << set_tspan(this_x, this_y, style, decoration, fontstyle, escaped_text)
              this_x += if e[:text].size == 1
                          e[:content_width] + (e[:height] - e[:content_width]) / 2
                        else
                          e[:content_width] + @global[:width_half_x] / 2
                        end

            elsif e[:decoration].include?(:whitespace)
              this_x += e[:width]
              next
            else
              new_text << set_tspan(this_x, this_y, style, decoration, fontstyle, escaped_text)
              this_x += e[:width]
            end
          end
        end
        @height = text_y if text_y > @height
      end

      bc[:y] = bc[:y] + @global[:height_connector_to_text] * 3 / 4
      bc[:height] = text_y - bc[:y] + @global[:height_connector_to_text]
      case element.enclosure
      when :brackets
        draw_bracket(bc[:x], bc[:y], bc[:width], bc[:height], col)
      when :rectangle
        draw_rectangle(bc[:x], bc[:y], bc[:width], bc[:height], col)
      when :brectangle
        draw_rectangle(bc[:x], bc[:y], bc[:width], bc[:height], col, true)
      end

      element.content_height = bc[:height]
      @tree_data += text_data.sub(/CONTENT/, new_text)
    end

    def draw_rectangle(x1, y1, width, height, col, bline = false)
      swidth = bline ? @linewidth + BLINE_SCALING : @linewidth + LINE_SCALING
      @extra_lines << "<polygon style='stroke:#{col}; stroke-width:#{swidth}; fill:none; stroke-linejoin:round; stroke-linecap:round;'
                            points='#{x1},#{y1} #{x1 + width},#{y1} #{x1 + width},#{y1 + height} #{x1},#{y1 + height}' />\n"
    end

    def draw_bracket(x1, y1, width, height, col, bline = false)
      swidth = bline ? @linewidth + BLINE_SCALING : @linewidth + LINE_SCALING
      slwidth = @global[:h_gap_between_nodes] / 2
      @extra_lines << "<polyline style='stroke:#{col}; stroke-width:#{swidth}; fill:none; stroke-linejoin:round; stroke-linecap:round;'
                            points='#{x1 + slwidth},#{y1} #{x1},#{y1} #{x1},#{y1 + height} #{x1 + slwidth},#{y1 + height}' />\n"
      @extra_lines << "<polyline style='stroke:#{col}; stroke-width:#{swidth}; fill:none; stroke-linejoin:round; stroke-linecap:round;'
                            points='#{x1 + width - slwidth},#{y1} #{x1 + width},#{y1} #{x1 + width},#{y1 + height} #{x1 + width - slwidth},#{y1 + height}' />\n"
    end

    def set_tspan(this_x, this_y, style, decoration, fontstyle, text)
      text.gsub!(/■+/) do |x|
        num_spaces = x.size
        "<tspan style='fill:none;'>" + "■" * num_spaces + "</tspan>"
      end
      "<tspan x='#{this_x}' y='#{this_y}' #{style} #{decoration} font-family=\"#{fontstyle}\">#{text}</tspan>\n"
    end

    def draw_paths
      paths = []
      path_pool_target = {}
      path_pool_other = {}
      path_pool_source = {}
      path_flags = []

      line_pool = {}
      line_flags = []

      elist = @element_list.get_elements

      elist.each do |element|
        x0 = element.horizontal_indent - @global[:h_gap_between_nodes]
        x1 = element.horizontal_indent + element.content_width / 2
        x2 = element.horizontal_indent + element.content_width + @global[:h_gap_between_nodes]
        y0 = element.vertical_indent + @global[:height_connector_to_text] / 2
        y1 = element.vertical_indent + element.content_height + @global[:height_connector_to_text]
        et = element.path
        et.each do |tr|
          if /\A-(>|<)?(\d+)\z/ =~ tr
            arrow = $1
            tr = $2
            if line_pool[tr]
              line_pool[tr] << { x: { left: x0, center: x1, right: x2 }, y: { top: y0, center: y0 + (y1 - y0) / 2, bottom: y1 }, arrow: arrow }
            else
              line_pool[tr] = [{ x: { left: x0, center: x1, right: x2 }, y: { top: y0, center: y0 + (y1 - y0) / 2, bottom: y1 }, arrow: arrow }]
            end
            line_flags << tr
          elsif /\A(?:>|<)(\d+)\z/ =~ tr
            tr = $1
            if path_pool_target[tr]
              path_pool_target[tr] << [x1, y1]
            else
              path_pool_target[tr] = [[x1, y1]]
            end
            path_flags << tr
          elsif path_pool_source[tr]
            if path_pool_other[tr]
              path_pool_other[tr] << [x1, y1]
            else
              path_pool_other[tr] = [[x1, y1]]
            end
            path_flags << tr
          else
            path_pool_source[tr] = [x1, y1]
            path_flags << tr
          end
          raise RSTError, +"Error: input text contains a path having more than two ends:\n > #{tr}" if path_flags.tally.any? { |_k, v| v > 2 } || line_flags.tally.any? { |_k, v| v > 2 }
        end
      end

      path_flags.tally.each do |k, v|
        raise RSTError, +"Error: input text contains a path having only one end:\n > #{k}" if v == 1
      end

      path_pool_source.each do |k, v|
        path_flags.delete(k)
        if (targets = path_pool_target[k])
          targets.each do |t|
            paths << { x1: v[0], y1: v[1], x2: t[0], y2: t[1], arrow: :single }
          end
        elsif (others = path_pool_other[k])
          others.each do |t|
            paths << { x1: v[0], y1: v[1], x2: t[0], y2: t[1], arrow: :none }
          end
        end
      end

      path_flags.uniq.each do |k|
        targets = path_pool_target[k]
        fst = targets.shift
        targets.each do |t|
          paths << { x1: fst[0], y1: fst[1], x2: t[0], y2: t[1], arrow: :double }
        end
      end

      paths.each do |t|
        draw_a_path(t[:x1], t[:y1], t[:x2], t[:y2], t[:arrow])
      end

      line_pool.each do |_k, v|
        a = v[0]
        b = v[1]

        if a[:y][:top] > b[:y][:bottom]
          generate_connectors(a[:x][:center], a[:y][:top], b[:x][:center], b[:y][:bottom], @col_extra, false, a[:arrow], b[:arrow])
        elsif a[:y][:bottom] < b[:y][:top]
          generate_connectors(b[:x][:center], b[:y][:top], a[:x][:center], a[:y][:bottom], @col_extra, false, b[:arrow], a[:arrow])
        elsif a[:x][:center] < b[:x][:center]
          if a[:y][:top] == b[:y][:top]
            upper_y = a[:y][:center] < b[:y][:center] ? a[:y][:center] : b[:y][:center]
            generate_connectors(a[:x][:right], upper_y, b[:x][:left], upper_y, @col_extra, false, a[:arrow], b[:arrow])
          else
            generate_connectors(a[:x][:right], a[:y][:center], b[:x][:left], b[:y][:center], @col_extra, false, a[:arrow], b[:arrow])
          end
        elsif a[:y][:top] == b[:y][:top]
          upper_y = a[:y][:center] < b[:y][:center] ? a[:y][:center] : b[:y][:center]
          generate_connectors(b[:x][:right], upper_y, a[:x][:left], upper_y, @col_extra, false, b[:arrow], a[:arrow])
        else
          generate_connectors(b[:x][:right], b[:y][:center], a[:x][:left], a[:y][:center], @col_extra, false, b[:arrow], a[:arrow])
        end
      end
      paths.size + line_pool.keys.size
    end

    def generate_connectors(x1, y1, x2, y2, col, dashed = false, s_arrow = false, t_arrow = false, bline = false)
      string = if s_arrow && t_arrow
                 "marker-mid='url(#arrowBothways)' "
               elsif s_arrow
                 "marker-mid='url(#arrowForward)' "
               elsif t_arrow
                 "marker-mid='url(#arrowBackward)' "
               else
                 ""
               end
      dasharray = dashed ? "stroke-dasharray='8 8'" : ""
      swidth = bline ? @linewidth + BLINE_SCALING : @linewidth + LINE_SCALING

      if s_arrow || t_arrow
        x_mid = if x2 > x1
                  x1 + (x2 - x1) / 2
                else
                  x1 - (x1 - x2) / 2
                end
        y_mid = if y2 > y1
                  y1 + (y2 - y1) / 2
                else
                  y1 - (y1 - y2) / 2
                end
        @extra_lines << "<path d='M#{x1},#{y1} L#{x_mid},#{y_mid} L#{x2}, #{y2}' style='fill: none; stroke: #{col}; stroke-width:#{swidth}; stroke-linecap:round;' #{dasharray} #{string}/>"
      else
        @extra_lines << "<line x1='#{x1}' y1='#{y1}' x2='#{x2}' y2='#{y2}' style='fill: none; stroke: #{col}; stroke-width:#{swidth}; stroke-linecap:round;' #{dasharray} #{string}/>"
      end
    end

    def generate_line(x1, y1, x2, y2, col, dashed = false, arrow = false, bline = false)
      string = if arrow
                 "marker-end='url(#arrow)' "
               else
                 ""
               end
      dasharray = dashed ? "stroke-dasharray='8 8'" : ""
      swidth = bline ? @linewidth + BLINE_SCALING : @linewidth + LINE_SCALING

      "<line x1='#{x1}' y1='#{y1}' x2='#{x2}' y2='#{y2}' style='fill: none; stroke: #{col}; stroke-width:#{swidth}; stroke-linecap:round;' #{dasharray} #{string}/>"
    end

    # Draw a line between child/parent elements
    def line_to_parent(parent, child)
      return if child.horizontal_indent.zero?

      if @polyline
        chi_x = child.horizontal_indent + child.content_width / 2
        chi_y = child.vertical_indent + @global[:height_connector_to_text] / 2

        par_x = parent.horizontal_indent + parent.content_width / 2
        par_y = parent.vertical_indent + parent.content_height + @global[:height_connector_to_text]

        mid_x1 = chi_x
        mid_y1 = par_y + (chi_y - par_y) / 2

        mid_x2 = par_x
        mid_y2 = mid_y1

        @tree_data += @polyline_styles.sub(/CHIX/, chi_x.to_s)
                                      .sub(/CHIY/, chi_y.to_s)
                                      .sub(/MIDX1/, mid_x1.to_s)
                                      .sub(/MIDY1/, mid_y1.to_s)
                                      .sub(/MIDX2/, mid_x2.to_s)
                                      .sub(/MIDY2/, mid_y2.to_s)
                                      .sub(/PARX/, par_x.to_s)
                                      .sub(/PARY/, par_y.to_s)
      else
        x1 = child.horizontal_indent + child.content_width / 2
        y1 = child.vertical_indent + @global[:height_connector_to_text] / 2
        x2 = parent.horizontal_indent + parent.content_width / 2
        y2 = parent.vertical_indent + parent.content_height + @global[:height_connector_to_text]

        line_data   = @line_styles.sub(/X1/, x1.to_s)
        line_data   = line_data.sub(/Y1/, y1.to_s)
        line_data   = line_data.sub(/X2/, x2.to_s)
        @tree_data += line_data.sub(/Y2/, y2.to_s)
      end
    end

    # Draw a triangle between child/parent elements
    def triangle_to_parent(parent, child)
      return if child.horizontal_indent.zero?

      x1 = child.horizontal_indent
      y1 = child.vertical_indent + @global[:height_connector_to_text] / 2
      x2 = child.horizontal_indent + child.content_width
      y2 = child.vertical_indent + @global[:height_connector_to_text] / 2
      x3 = parent.horizontal_indent + parent.content_width / 2
      y3 = parent.vertical_indent + parent.content_height + @global[:height_connector_to_text]

      polygon_data = @polygon_styles.sub(/X1/, x1.to_s)
      polygon_data = polygon_data.sub(/Y1/, y1.to_s)
      polygon_data = polygon_data.sub(/X2/, x2.to_s)
      polygon_data = polygon_data.sub(/Y2/, y2.to_s)
      polygon_data = polygon_data.sub(/X3/, x3.to_s)
      @tree_data  += polygon_data.sub(/Y3/, y3.to_s)
    end
  end
end