lib/rsyntaxtree/svg_graph.rb in rsyntaxtree-0.9.3 vs lib/rsyntaxtree/svg_graph.rb in rsyntaxtree-1.0.1

- old
+ new

@@ -4,360 +4,506 @@ #========================== # svg_graph.rb #========================== # # Parses an element list into an SVG tree. -# -# This file is part of RSyntaxTree, which is a ruby port of Andre Eisenbach's -# excellent program phpSyntaxTree. -# # Copyright (c) 2007-2021 Yoichiro Hasebe <yohasebe@gmail.com> -# Copyright (c) 2003-2004 Andre Eisenbach <andre@ironcreek.net> require "tempfile" -require 'graph' +require 'base_graph' +require 'utils' -class SVGGraph < Graph +module RSyntaxTree + class SVGGraph < BaseGraph + attr_accessor :width, :height - def initialize(e_list, metrics, symmetrize, color, leafstyle, multibyte, fontstyle, font, font_cjk, font_size, margin, transparent) - - # Store class-specific parameters - @font = multibyte ? font_cjk : font - @font_size = font_size - @transparent = transparent - - case fontstyle - when /(?:sans|cjk)/ - @fontstyle = "\"'Noto Sans JP', 'Noto Sans', sans-serif\"" - @fontcss = "http://fonts.googleapis.com/earlyaccess/notosansjp.css" - when /(?:serif)/ - @fontstyle = "\"'Noto Serif JP', 'Noto Serif', serif\"" - @fontcss = "https://fonts.googleapis.com/css?family=Noto+Serif+JP" - when /(?:math)/ - @fontstyle = "\"Latin Modern Roman', sans-serif\"" - @fontcss = "https://cdn.jsdelivr.net/gh/sugina-dev/latin-modern-web@1.0.1/style/latinmodern-roman.css" + def initialize(element_list, params) + @height = 0 + @width = 0 + @extra_lines = [] + @fontset = params[:fontset] + @fontsize = params[:fontsize] + @transparent = params[:transparent] + @color = params[:color] + @fontstyle = params[:fontstyle] + @margin = params[:margin].to_i + @line_styles = "<line style='stroke:black; stroke-width:#{FONT_SCALING};' x1='X1' y1='Y1' x2='X2' y2='Y2' />\n" + @polygon_styles = "<polygon style='fill: none; stroke: black; stroke-width:#{FONT_SCALING};' points='X1 Y1 X2 Y2 X3 Y3' />\n" + @text_styles = "<text white-space='pre' alignment-baseline='text-top' style='fill: COLOR; font-size: fontsize' x='X_VALUE' y='Y_VALUE'>CONTENT</text>\n" + @tree_data = String.new + @visited_x = {} + @visited_y = {} + super(element_list, params) end - @margin = margin.to_i + def svg_data + metrics = parse_list + @height = metrics[:height] + @margin * 2 + @width = metrics[:width] + @margin * 2 - super(e_list, metrics, symmetrize, color, leafstyle, multibyte, @fontstyle, @font_size) + x1 = 0 - @margin + y1 = 0 - @margin + x2 = @width + @margin + y2 = @height + @margin + extra_lines = @extra_lines.join("\n") - @line_styles = "<line style='stroke:black; stroke-width:#{FONT_SCALING};' x1='X1' y1='Y1' x2='X2' y2='Y2' />\n" - @polygon_styles = "<polygon style='fill: none; stroke: black; stroke-width:#{FONT_SCALING};' points='X1 Y1 X2 Y2 X3 Y3' />\n" - @text_styles = "<text letter-spacing='0' word-spacing='0' kerning='0' style='fill: COLOR; font-size: FONT_SIZE ST WA' x='X_VALUE' y='Y_VALUE' TD font-family=#{@fontstyle}>CONTENT</text>\n" - @tree_data = String.new - end + as2 = $h_gap_between_nodes / 2 * 0.8 + as = as2 / 2 - def get_left_most(tree_data) - xs = @tree_data.scan(/x1?=['"]([^'"]+)['"]/).map{|m| m.first.to_i} - xs.min - end - - def svg_data - parse_list - lm = get_left_most(@tree_data) - width = @width - lm + @margin * 2 - height = @height + @margin * 2 - - header =<<EOD + header =<<EOD <?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="#{-@margin + lm}, -#{@margin}, #{@width - lm + @margin * 2}, #{@height + @margin * 2}" version="1.1" xmlns="http://www.w3.org/2000/svg"> -<defs> -<style> -@import url(#{@fontcss}); -</style> -</defs> +<!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="strokeWidth" markerWidth="#{as2}" markerHeight="#{as2}" viewBox="0 0 #{as2} #{as2}" refX="#{as}" refY="0"> + <polyline fill="none" stroke="#{@col_path}" stroke-width="1" points="0,#{as2},#{as},0,#{as2},#{as2}" /> + </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> EOD - - rect =<<EOD -<rect x="#{-@margin + lm}" y="-#{@margin}" width="#{@width - lm + @margin * 2}" height="#{@height + @margin * 2}" stroke="none" fill="white" />" + rect =<<EOD +<rect x="#{x1}" y="#{y1}" width="#{x2}" height="#{y2}" stroke="none" fill="white" />" EOD - footer = "</svg>" + footer = "</svg>" - if @transparent - header + @tree_data + footer - else - header + rect + @tree_data + footer + if @transparent + header + @tree_data + extra_lines + footer + else + header + rect + @tree_data + extra_lines + footer + end end - end - # Create a temporary file and returns only its filename - def create_tempf(basename, ext, num = 10) - flags = File::RDWR | File::CREAT | File::EXCL - tfname = "" - num.times do |i| - begin - tfname = "#{basename}.#{$$}.#{i}.#{ext}" - tfile = File.open(tfname, flags, 0600) - rescue Errno::EEXIST - next + def draw_a_path(s_x, s_y, t_x, t_y, target_arrow = :none) + + x_spacing = $h_gap_between_nodes * 1.25 + y_spacing = $height_connector * 0.65 + + ymax = [s_y, t_y].max + if ymax < @height + new_y = @height + y_spacing + else + new_y = ymax + y_spacing end - tfile.close - return tfname - end - end - :private - # Add the element into the tree (draw it) - def draw_element(x, y, w, string, type) - string = string.sub(/\^\z/){""} - # Calculate element dimensions and position - if (type == ETYPE_LEAF) and @leafstyle == "nothing" - top = row2px(y - 1) + (@font_size * 1.5) - else - top = row2px(y) - end - left = x + @m[:b_side] - bottom = top + @e_height - right = left + w + 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 - # Split the string into the main part and the - # subscript part of the element (if any) - parts = string.split("_", 2) - if(parts.length > 1 ) - main = parts[0].strip - sub = parts[1].gsub(/_/, " ").strip - else - main = parts[0].strip - sub = "" - 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 - if /\A\=(.+)\=\z/ =~ main - main = $1 - main_decoration= "overline" - elsif /\A\-(.+)\-\z/ =~ main - main = $1 - main_decoration= "underline" - elsif /\A\~(.+)\~\z/ =~ main - main = $1 - main_decoration= "line-through" - else - main_decoration= "" - end + s_y += $h_gap_between_nodes / 2 + t_y += $h_gap_between_nodes / 2 + new_y += $h_gap_between_nodes / 2 - if /\A\*\*\*(.+)\*\*\*\z/ =~ main - main = $1 - main_style = "font-style: italic" - main_weight = "font-weight: bold" - elsif /\A\*\*(.+)\*\*\z/ =~ main - main = $1 - main_style = "" - main_weight = "font-weight: bold" - elsif /\A\*(.+)\*\z/ =~ main - main = $1 - main_style = "font-style: italic" - main_weight = "" - else - main_style = "" - main_weight = "" - end + dashed = true if target_arrow == :none - if /\A#(.+)#\z/ =~ main - main = $1 + if target_arrow == :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, 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) + elsif target_arrow == :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 - # Calculate text size for the main and the - # subscript part of the element - # symbols for underline/overline removed temporarily + def draw_element(element) + top = element.vertical_indent - main_width = 0 - main_height = 0 - main.split(/\\n/).each do |l| - l_width = img_get_txt_width(l, @font, @font_size) - main_width = l_width if main_width < l_width - main_height += img_get_txt_height(l, @font, @font_size) - end + left = element.horizontal_indent + bottom = top +$single_line_height + right = left + element.content_width + txt_pos = left + (right - left) / 2 - if sub != "" - if /\A\=(.+)\=\z/ =~ sub - sub = $1 - sub_decoration= "overline" - elsif /\A\-(.+)\-\z/ =~ sub - sub = $1 - sub_decoration= "underline" - elsif /\A\~(.+)\~\z/ =~ sub - sub = $1 - sub_decoration= "line-through" + if(element.type == ETYPE_LEAF) + col = @col_leaf else - sub_decoration= "" + col = @col_node end - if /\A\*\*\*(.+)\*\*\*\z/ =~ sub - sub = $1 - sub_style = "font-style: italic" - sub_weight = "font-weight: bold" - elsif /\A\*\*(.+)\*\*\z/ =~ sub - sub = $1 - sub_style = "" - sub_weight = "font-weight: bold" - elsif /\A\*(.+)\*\z/ =~ sub - sub = $1 - sub_style = "font-style: italic" - sub_weight = "" - else - sub_style = "" - sub_weight = "" - end - sub_height = img_get_txt_height(sub, @font, @font_size) - sub_width = img_get_txt_width(sub.to_s, @font, @sub_size) - else - sub_width = 0 - sub_height = 0 - 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 + $single_line_height - $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 - $h_gap_between_nodes / 2 , :y => top, :width => element.content_width + $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 == 0 + text_y -= l[:height] + elsif + text_y += l[:height] + end + y1 = text_y - $single_line_height / 8 + x2 = text_x + element.content_width + y2 = y1 + this_width = x2 - x1 + case l[:type] + when :border + stroke_width = FONT_SCALING + when :bborder + stroke_width = FONT_SCALING * 2 + end + @extra_lines << "<line style=\"stroke:#{col}; 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 - if /\A#(.+)#\z/ =~ sub - sub = $1 - end + l[:elements].each_with_index do |e, idx| + escaped_text = e[:text].gsub('>', '&gt;').gsub('<', '&lt;'); + decorations = [] + if e[:decoration].include?(:overline) + decorations << "overline" + end - # Center text in the element - txt_pos = left + (right - left) / 2 + if e[:decoration].include?(:underline) + decorations << "underline" + end - # Select apropriate color - if(type == ETYPE_LEAF) - col = @col_leaf - else - col = @col_node - end + if e[:decoration].include?(:linethrough) + decorations << "line-through" + end + decoration ="text-decoration=\"" + decorations.join(" ") + "\"" - if(main[0].chr == "<" && main[-1].chr == ">") - col = @col_trace - end + style = "style=\"" + if e[:decoration].include?(:small) + style += "font-size: #{(SUBSCRIPT_CONST.to_f * 100).to_i}%; " + this_y = text_y - (($single_X_metrics.height - $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 - ($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 - # Draw main text - main_data = @text_styles.sub(/COLOR/, col) - main_data = main_data.sub(/FONT_SIZE/, @font_size.to_s + "px;") - main_x = txt_pos - (main_width + sub_width) / 2 - main_y = top + @e_height - @m[:e_padd] - main_data = main_data.sub(/X_VALUE/, main_x.to_s) - main_data = main_data.sub(/Y_VALUE/, main_y.to_s) - if /\\n/ =~ main - lines = main.split(/\\n/) - new_main = "" - dy = 0 - lines.each_with_index do |l, idx| - if idx == 0 - dy = 0 - else - dy = 1 - main_y += img_get_txt_height(l, @font, @font_size) + if e[:decoration].include?(:bold) || e[:decoration].include?(:bolditalic) + style += "font-weight: bold; " + end + + if e[:decoration].include?(:italic) || e[:decoration].include?(:bolditalic) + style += "font-style: italic; " + end + + style += "\"" + + case @fontstyle + when /(?:cjk)/ + fontstyle = "'WenQuanYi Zen Hei', 'Noto Sans', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', sans-serif" + when /(?:sans)/ + if e[:cjk] + fontstyle = "'Noto Sans JP', 'Noto Sans', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', sans-serif" + else + fontstyle = "'Noto Sans', 'Noto Sans JP', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', sans-serif" + end + when /(?:serif)/ + if e[:cjk] + fontstyle = "'Noto Serif JP', 'Noto Serif', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', serif" + else + fontstyle = "'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 + FONT_SCALING + + if e[:text].size == 1 + enc_width = e[:width] + enc_x = this_x + else + enc_width = e[:width] + enc_x = this_x + end + + if e[:decoration].include?(:hatched) + case element.type + when ETYPE_LEAF + if @color + fill = "url(#hatchForLeaf)" + else + fill = "url(#hatchBlack)" + end + when ETYPE_NODE + if @color + fill = "url(#hatchForNode)" + else + fill = "url(#hatchBlack)" + end + end + else + fill = "none" + end + + enc = nil + bar = nil + + if e[:decoration].include?(:bstroke) + stroke_width = FONT_SCALING * 2.5 + else + stroke_width = FONT_SCALING + end + + if e[:decoration].include?(:box) + enc = "<rect style='stroke: #{col}; 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='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='stroke:#{col}; stroke-width:#{stroke_width};' x1='#{x1}' y1='#{y1}' x2='#{x2}' y2='#{y2}'></line>\n" + @extra_lines << bar + + if e[:decoration].include?(:arrow_to_l) + l_arrowhead = "<polyline stroke-linejoin='bevel' fill='none' stroke='#{col}' stroke-width='#{stroke_width}' points='#{x1 + ar_hwidth},#{y1 + ar_hwidth / 2} #{x1},#{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='bevel' fill='none' stroke='#{col}' stroke-width='#{stroke_width}' points='#{x2 - ar_hwidth},#{y2 - ar_hwidth / 2} #{x2},#{y2} #{x2 - ar_hwidth},#{y2 + ar_hwidth / 2}' />\n" + @extra_lines << r_arrowhead + end + + + end + + @extra_lines << enc if enc + + if e[:text].size == 1 + this_x += (e[:height] - e[:content_width]) / 2 + else + this_x += $width_half_X / 2 + end + new_text << set_tspan(this_x, this_y, style, decoration, fontstyle, escaped_text) + if e[:text].size == 1 + this_x += e[:content_width] + this_x += (e[:height] - e[:content_width]) / 2 + else + this_x += e[:content_width] + this_x += $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 - this_width = img_get_txt_width(l, @font, @font_size) - this_x = txt_pos - (this_width + sub_width) / 2 - new_main << "<tspan x='#{this_x}' y='#{main_y}'>#{l}</tspan>" - @height = main_y if main_y > @height + @height = text_y if text_y > @height end - main = new_main + bc[:y] = bc[:y] + $height_connector_to_text * 3 / 4 + bc[:height] = text_y - bc[:y] + $height_connector_to_text + if element.enclosure == :brackets + @extra_lines << generate_line(bc[:x], bc[:y], bc[:x] + $h_gap_between_nodes / 2, bc[:y], col) + @extra_lines << generate_line(bc[:x], bc[:y], bc[:x], bc[:y] + bc[:height], col) + @extra_lines << generate_line(bc[:x], bc[:y] + bc[:height], bc[:x] + $h_gap_between_nodes / 2, bc[:y] + bc[:height], col) + @extra_lines << generate_line(bc[:x] + bc[:width], bc[:y], bc[:x] + bc[:width] - $h_gap_between_nodes / 2, bc[:y], col) + @extra_lines << generate_line(bc[:x] + bc[:width], bc[:y], bc[:x] + bc[:width], bc[:y] + bc[:height], col) + @extra_lines << generate_line(bc[:x] + bc[:width], bc[:y] + bc[:height], bc[:x] + bc[:width] - $h_gap_between_nodes / 2, bc[:y] + bc[:height], col) + elsif element.enclosure == :rectangle + @extra_lines << generate_line(bc[:x], bc[:y], bc[:x] + bc[:width], bc[:y], col) + @extra_lines << generate_line(bc[:x], bc[:y], bc[:x], bc[:y] + bc[:height], col) + @extra_lines << generate_line(bc[:x] + bc[:width], bc[:y], bc[:x] + bc[:width], bc[:y] + bc[:height], col) + @extra_lines << generate_line(bc[:x], bc[:y] + bc[:height], bc[:x] + bc[:width], bc[:y] + bc[:height], col) + elsif element.enclosure == :brectangle + @extra_lines << generate_line(bc[:x], bc[:y], bc[:x] + bc[:width], bc[:y], col, false, false, 2) + @extra_lines << generate_line(bc[:x], bc[:y], bc[:x], bc[:y] + bc[:height], col, false, false, 2) + @extra_lines << generate_line(bc[:x] + bc[:width], bc[:y], bc[:x] + bc[:width], bc[:y] + bc[:height], col, false, false, 2) + @extra_lines << generate_line(bc[:x], bc[:y] + bc[:height], bc[:x] + bc[:width], bc[:y] + bc[:height], col, false, false, 2) + end + + element.content_height = bc[:height] + @tree_data += text_data.sub(/CONTENT/, new_text) end - @tree_data += main_data.sub(/TD/, "text-decoration='#{main_decoration}'") - .sub(/ST/, main_style + ";") - .sub(/WA/, main_weight + ";") - .sub(/CONTENT/, main) - # Draw subscript text - if sub && sub != "" - sub_data = @text_styles.sub(/COLOR/, col) - sub_data = sub_data.sub(/FONT_SIZE/, @sub_size.to_s) - sub_x = txt_pos + (main_width / 2) - (sub_width / 2) - sub_y = main_y + sub_height / 6 - sub_data = sub_data.sub(/X_VALUE/, sub_x.to_s) - sub_data = sub_data.sub(/Y_VALUE/, sub_y.to_s) - @tree_data += sub_data.sub(/TD/, "text-decoration='#{sub_decoration}'") - .sub(/ST/, sub_style) - .sub(/WA/, sub_weight) - .sub(/CONTENT/, sub) - @height += sub_height / 4 + 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 - end - # Draw a line between child/parent elements - def line_to_parent(fromX, fromY, fromW, toX, toW) - if (fromY == 0 ) - return - end + def draw_paths + rockbottom = 0 + path_pool_target = {} + path_pool_other = {} + path_pool_source = {} + path_flags = [] + elist = @element_list.get_elements.reverse - fromTop = row2px(fromY) - fromLeft = (fromX + fromW / 2 + @m[:b_side]) - toBot = (row2px(fromY - 1 ) + @e_height) - toLeft = (toX + toW / 2 + @m[:b_side]) + elist.each do |element| + x1 = element.horizontal_indent + element.content_width / 2 + y1 = element.vertical_indent + element.content_height + y1 += $height_connector_to_text if element.enclosure != :none + et = element.path + et.each do |tr| + if /\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 + elsif path_pool_source[tr] + if path_pool_other[tr] + path_pool_other[tr] << [x1, y1] + else + path_pool_other[tr] = [[x1, y1]] + end + else + path_pool_source[tr] = [x1, y1] + end + path_flags << tr + if path_flags.tally.any?{|k, v| v > 2} + raise RSTError, "Error: input text contains a path having more than two ends:\n > #{tr}" + end + end + end - line_data = @line_styles.sub(/X1/, fromLeft.to_s) - line_data = line_data.sub(/Y1/, fromTop.to_s) - line_data = line_data.sub(/X2/, toLeft.to_s) - @tree_data += line_data.sub(/Y2/, toBot.to_s) + path_flags.tally.each do |k, v| + if v == 1 + raise RSTError, "Error: input text contains a path having only one end:\n > #{k}" + end + end - end + paths = [] + 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 - # Draw a triangle between child/parent elements - def triangle_to_parent(fromX, fromY, fromW, textW, symmetrize = true) - if (fromY == 0) - return - 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 - toX = fromX - fromCenter = (fromX + fromW / 2 + @m[:b_side]) + paths.each do |t| + draw_a_path(t[:x1], t[:y1] + $height_connector_to_text / 2, + t[:x2], t[:y2] + $height_connector_to_text / 2, + t[:arrow]) + end - fromTop = row2px(fromY) - fromLeft1 = (fromCenter + textW / 2) - fromLeft2 = (fromCenter - textW / 2) - toBot = (row2px(fromY - 1) + @e_height) + paths.size + end - if symmetrize - toLeft = (toX + textW / 2 + @m[:b_side]) - else - toLeft = (toX + textW / 2 + @m[:b_side] * 3) + def generate_line(x1, y1, x2, y2, col, dashed = false, arrow = false, stroke_width = 1) + if arrow + string = "marker-end='url(#arrow)' " + else + string = "" + end + dasharray = dashed ? "stroke-dasharray='8 8'" : "" + swidth = FONT_SCALING * stroke_width + + "<line x1='#{x1}' y1='#{y1}' x2='#{x2}' y2='#{y2}' style='fill: none; stroke: #{col}; stroke-width:#{swidth}' #{dasharray} #{string}/>" end - polygon_data = @polygon_styles.sub(/X1/, fromLeft1.to_s) - polygon_data = polygon_data.sub(/Y1/, fromTop.to_s) - polygon_data = polygon_data.sub(/X2/, fromLeft2.to_s) - polygon_data = polygon_data.sub(/Y2/, fromTop.to_s) - polygon_data = polygon_data.sub(/X3/, toLeft.to_s) - @tree_data += polygon_data.sub(/Y3/, toBot.to_s) - end + # Draw a line between child/parent elements + def line_to_parent(parent, child) + if (child.horizontal_indent == 0 ) + return + end - # If a node element text is wider than the sum of it's - # child elements, then the child elements need to - # be resized to even out the space. This function - # recurses down the a child tree and sizes the - # children appropriately. - def fix_child_size(id, current, target) - children = @e_list.get_children(id) - @e_list.set_element_width(id, target) + x1 = child.horizontal_indent + child.content_width / 2 + y1 = child.vertical_indent + $height_connector_to_text / 2 + x2 = parent.horizontal_indent + parent.content_width / 2 + y2 = parent.vertical_indent + parent.content_height + $height_connector_to_text - if(children.length > 0 ) - delta = target - current - target_delta = delta / children.length + 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 - children.each do |child| - child_width = @e_list.get_element_width(child) - fix_child_size(child, child_width, child_width + target_delta) + # Draw a triangle between child/parent elements + def triangle_to_parent(parent, child) + if (child.horizontal_indent == 0) + return end - end - end - def img_get_txt_width(text, font, font_size, multiline = true) - parts = text.split("_", 2) - main_before = parts[0].strip - sub = parts[1] - main = get_txt_only(main_before) - main_metrics = img_get_txt_metrics(main, font, font_size, multiline) - width = main_metrics.width - if sub - sub_metrics = img_get_txt_metrics(sub.strip, font, font_size * SUBSCRIPT_CONST, multiline) - width += sub_metrics.width + x1 = child.horizontal_indent + y1 = child.vertical_indent + $height_connector_to_text / 2 + x2 = child.horizontal_indent + child.content_width + y2 = child.vertical_indent + $height_connector_to_text / 2 + x3 = parent.horizontal_indent + parent.content_width / 2 + y3 = parent.vertical_indent + parent.content_height + $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 - return width end - - def img_get_txt_height(text, font, font_size) - main_metrics = img_get_txt_metrics(text, font, font_size, false) - main_metrics.height - end - end