Таблица стилей XSLT для преобразования Graphml в SVG

У меня больше проблем, чем ожидалось, с поиском таблицы стилей XSLT, которая преобразует простой документ Graphml в диаграмму SVG. Я искал довольно широко, и пока у меня только частичный успех. Вот мой входной файл Graphml:

<?xml version="1.0" encoding="UTF-8"?>
<graphml xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<graph id="G" edgedefault="directed">
  <node id="d1e2"/>
  <node id="d1e4"/>
  <node id="d1e7"/>
  <node id="d1e9"/>
  <node id="d1e11"/>
  <node id="d1e14"/>
  <node id="d1e17"/>
  <node id="d1e21"/>
  <node id="d1e23"/>
  <node id="d1e26"/>
  <node id="d1e29"/>
  <node id="d1e33"/>
  <node id="d1e35"/>
  <node id="d1e38"/>
  <node id="d1e41"/>
  <edge source="d1e2" target="d1e4"/>
  <edge source="d1e2" target="d1e7"/>
  <edge source="d1e7" target="d1e9"/>
  <edge source="d1e9" target="d1e11"/>
  <edge source="d1e9" target="d1e14"/>
  <edge source="d1e9" target="d1e17"/>
  <edge source="d1e7" target="d1e21"/>
  <edge source="d1e21" target="d1e23"/>
  <edge source="d1e21" target="d1e26"/>
  <edge source="d1e21" target="d1e29"/>
  <edge source="d1e7" target="d1e33"/>
  <edge source="d1e33" target="d1e35"/>
  <edge source="d1e33" target="d1e38"/>
  <edge source="d1e33" target="d1e41"/>
</graph>
</graphml>

и вот моя таблица стилей (из http://www.svgopen.org/2003//papers/ComparisonXML2SVGTransformationMechanisms/index.html#S2). Обратите внимание, что я временно отключил обработку краев, чтобы сосредоточиться на размещении узлов:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/2000/svg">
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

  <!-- for a 'graph' element, creates an 'svg' element -->
  <xsl:template match="graph">
  <!-- first give a CSS reference for the generated SVG -->
    <xsl:processing-instruction name="xml-stylesheet">type="text/css" href="default.css"</xsl:processing-instruction> 
    <svg>
      <!-- defs section for the arrow -->
      <!-- ... -->
      <!-- recurse 'node' elements of the graph to find graph root -->
      <xsl:apply-templates select="node"/>
    </svg>
  </xsl:template>

  <!-- recurse 'node' element to find graph root -->
  <xsl:template match="node">
    <!-- check if the first 'edge' has current 'node' as target -->
    <xsl:apply-templates select="../edge[1]">
       <xsl:with-param name="n" select="."/>
    </xsl:apply-templates>
  </xsl:template>

  <!-- check if a 'node' ($n) is a target of the current 'edge' -->
  <xsl:template match="edge">
    <xsl:param name="n">null</xsl:param>
    <!-- if the 'node' is not a target of the current 'edge' -->
    <xsl:if test="not(@target=$n/@id)">
      <!-- advance to the next edge -->
      <xsl:apply-templates select="following-sibling::edge[position()=1]">
        <xsl:with-param name="n" select="$n"/>
      </xsl:apply-templates>
      <!-- if all edges have been queried  -->
      <xsl:if test="not(following-sibling::edge[position()=1])">
        <!-- the 'node' ($n) is the root, create it -->
        <xsl:call-template name="create-node">
          <xsl:with-param name="n" select="$n"/>
        </xsl:call-template>
      </xsl:if>
    </xsl:if>
  </xsl:template>

  <!-- transform a 'node' to SVG and recurse through its children -->
  <xsl:template name="create-node">
    <xsl:param name="n">null</xsl:param>
    <xsl:param name="level">0</xsl:param>
    <xsl:param name="count">0</xsl:param>
    <xsl:param name="edge">null</xsl:param>
    <xsl:param name="x1">0</xsl:param>
    <xsl:param name="y1">0</xsl:param>
    <!-- some helpers -->
    <xsl:variable name="side" select="1-2*($count mod 2)"/>
    <xsl:variable name="x" select="$level*150"/>
    <xsl:variable name="y" select="$y1 - 50+$side*ceiling($count div 2)*150"/>
    <!-- create the 'node' itself and position it -->
    <g class="node">
      <rect x="{$x}" y="{$y}" width="100" height="100"/>
      <text text-anchor="middle" x="{$x+50}" y="{$y+55}">
        <xsl:value-of select="$n/@id"/>
      </text>
    </g>
    <!-- if there is an 'edge' ($edge) draw it -->
    <xsl:if test="$edge!='null'">
      <!-- the 'edge' position goes from previous 'node' position to $n one -->
      <!--
      <line class="edge" x1="{$x1}" y1="{$y1}" x2="{$x}" y2="{$y+50}">
        <xsl:attribute name="style">marker-end:url(#arrow)</xsl:attribute>
      </line>
      -->
    </xsl:if>
    <!-- now that the 'node' is created, recurse to children through edges -->
    <xsl:call-template name="query-edge">
      <xsl:with-param name="edge" select="$n/../edge[@source=$n/@id][1]"/>
      <xsl:with-param name="x1" select="$x+100"/>
      <xsl:with-param name="y1" select="$y+50"/>
      <xsl:with-param name="n" select="$n"/>
      <!-- going to the upper level, increment level -->
      <xsl:with-param name="level" select="$level+1"/>
      <!-- going to the first child, set counter to 0 -->
      <xsl:with-param name="count" select="0"/>
    </xsl:call-template>
  </xsl:template>

  <!-- recurse a 'node' ($n) edges to find 'node' children -->
  <xsl:template name="query-edge">
    <xsl:param name="edge">null</xsl:param>
    <xsl:param name="x1">0</xsl:param>
    <xsl:param name="y1">0</xsl:param>
    <xsl:param name="n">null</xsl:param>
    <xsl:param name="level">0</xsl:param>
    <xsl:param name="count">0</xsl:param>
    <xsl:variable name="target" select="$edge/@target"/>
    <!-- if there is an 'edge' -->
    <xsl:if test="$edge!='null'">
      <!-- go down the tree, create the 'node' of the 'edge' target -->
      <xsl:call-template name="create-node">
        <xsl:with-param name="n" select="$edge/../node[@id=$target]"/>
        <xsl:with-param name="level" select="$level"/>
        <xsl:with-param name="count" select="$count"/>
        <xsl:with-param name="edge" select="$edge"/>
        <xsl:with-param name="x1" select="$x1"/>
        <xsl:with-param name="y1" select="$y1"/>
      </xsl:call-template>
      <!-- go to the next 'edge' that has also the 'node' ($n) has source -->
      <xsl:variable name="next-edge" select="$edge/following-sibling::edge[position()=1][@source=$n/@id]"/>
      <xsl:call-template name="query-edge">
       <xsl:with-param name="edge" select="$next-edge"/>
       <xsl:with-param name="x1" select="$x1"/>
       <xsl:with-param name="y1" select="$y1"/>
       <xsl:with-param name="n" select="$n"/>
       <xsl:with-param name="level" select="$level"/>
       <!-- next 'edge', increment counter -->
       <xsl:with-param name="count" select="$count+1"/>
     </xsl:call-template>
    </xsl:if>
  </xsl:template>
</xsl:stylesheet>

Когда я запускаю это через Saxon, я получаю следующий файл SVG:

    <?xml version="1.0" encoding="UTF-8"?>
   <?xml-stylesheet type="text/css" href="default.css"?><svg xmlns="http://www.w3.org/2000/svg">
   <g class="node">
      <rect x="0" y="-50" width="100" height="100"/>
      <text text-anchor="middle" x="50" y="5">d1e2</text>
   </g>
   <g class="node">
      <rect x="150" y="-50" width="100" height="100"/>
      <text text-anchor="middle" x="200" y="5">d1e4</text>
   </g>
   <g class="node">
      <rect x="150" y="-200" width="100" height="100"/>
      <text text-anchor="middle" x="200" y="-145">d1e7</text>
   </g>
   <g class="node">
      <rect x="300" y="-200" width="100" height="100"/>
      <text text-anchor="middle" x="350" y="-145">d1e9</text>
   </g>
   <g class="node">
      <rect x="450" y="-200" width="100" height="100"/>
      <text text-anchor="middle" x="500" y="-145">d1e11</text>
   </g>
   <g class="node">
      <rect x="450" y="-350" width="100" height="100"/>
      <text text-anchor="middle" x="500" y="-295">d1e14</text>
   </g>
   <g class="node">
      <rect x="450" y="-50" width="100" height="100"/>
      <text text-anchor="middle" x="500" y="5">d1e17</text>
   </g>
</svg>

Это довольно хорошо, но он потерял все узлы после узла d1e17, и я не могу понять, почему.

Кто-нибудь может найти ошибку в таблице стилей? Или у кого-нибудь есть лучшая таблица стилей для этой цели?

Спасибо за любую помощь, Мартин


person user304582    schedule 05.06.2017    source источник
comment
Я думаю, что я частично определил проблему. Я думаю, что таблица стилей выполняет итерацию по всем дочерним элементам узла только в том случае, если ребра этого узла находятся рядом друг с другом в файле Graphml. То есть, если я сначала отсортирую ребра по атрибуту источника, то все узлы будут обработаны, как и ожидалось. Я думаю, что эта строка делает выбор братьев и сестер: ‹xsl:apply-templates select=following-sibling::edge[position()=1]› Поэтому мне нужно выяснить, как он выбирает братьев и сестер, и изменить его, чтобы он не предполагает, что дочерние элементы являются смежными элементами.   -  person user304582    schedule 06.06.2017
comment
Верно ли, что узел d1e17 расположен на y=-50? не должно ли быть y=-500?   -  person fafl    schedule 06.06.2017
comment
Это результат использования боковой переменной, определенной здесь: ‹xsl:variable name=side select=1-2*($count mod 2)/> По сути, это попеременно размещает дочерние узлы ниже и выше родительского узла. Таким образом, первый дочерний узел находится на уровне (y = -200), следующий на 150 ниже первого (y = -350), следующий на 150 выше первого (y = -50).   -  person user304582    schedule 06.06.2017


Ответы (1)


Если вы сначала преобразуете плоский список узлов в дерево, то сможете использовать оси для поиска информации о структуре.

Попробуйте что-то вроде этого:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="2.0">
  <xsl:output method="xml" omit-xml-declaration="yes" encoding="UTF-8" indent="yes" />

  <xsl:template match="/graphml/graph">

    <!-- Find the root ID -->
    <xsl:variable name="rootId">
      <xsl:for-each select="node">
        <xsl:variable name="nodeId" select="@id"/>
        <xsl:if test="not(../edge[@target=$nodeId])">
          <xsl:value-of select="$nodeId"/>
        </xsl:if>
      </xsl:for-each>
    </xsl:variable>

    <!-- Turn flat list into a tree -->
    <xsl:variable name="tree">
      <xsl:apply-templates select="node[@id=$rootId]"/>
    </xsl:variable>

    <!-- Turn tree into a flat list of svg elements -->
    <svg>
      <g transform="translate(50 50)">
        <xsl:apply-templates select="$tree"/>
      </g>
    </svg>
  </xsl:template>

  <xsl:template match="node">
    <xsl:variable name="nodeId" select="@id"/>
    <xsl:variable name="childIds" select="//edge[@source=$nodeId]/@target"/>
    <treeNode id="{@id}">
      <xsl:apply-templates select="//node[@id=$childIds]"/>
    </treeNode>
  </xsl:template>

  <xsl:template match="svg:treeNode">
    <xsl:variable name="level" select="count(ancestor::*)"/>
    <xsl:variable name="leafChildren" select="count(descendant::*[not(descendant::*)])"/>
    <xsl:variable name="earlierChildren" select="count(preceding::*[not(descendant::*)])"/>
    <xsl:variable name="x" select="100 * $level"/>
    <xsl:variable name="y" select="50 * $earlierChildren + 25 * max((0, $leafChildren - 1))"/>
    <g class="node">
      <circle cx="{$x}" cy="{$y}" r="10"/>
      <text x="{$x - 10}" y="{$y + 25}"><xsl:value-of select="@id"/></text>

      <!-- Draw line to parent -->
      <xsl:if test="$level != 0">
        <xsl:variable name="parentLevel" select="$level - 1"/>
        <xsl:variable name="parentLeafChildren" select="count(../descendant::*[not(descendant::*)])"/>
        <xsl:variable name="parentEarlierChildren" select="count(../preceding::*[not(descendant::*)])"/>
        <xsl:variable name="parentX" select="100 * $parentLevel"/>
        <xsl:variable name="parentY" select="50 * $parentEarlierChildren + 25 * max((0, $parentLeafChildren - 1))"/>
        <path d="M {$parentX} {$parentY} C {$parentX + 50} {$parentY}, {$x - 50} {$y}, {$x} {$y}" stroke="black" fill="transparent" stroke-width="2"/>
      </xsl:if>
    </g>
    <xsl:apply-templates select="child::*"/>
  </xsl:template>

</xsl:stylesheet>

Результат:

скриншот сгенерированного графика

person fafl    schedule 08.06.2017
comment
Вау - это очень приятно. Это выглядит почти так, как я имел в виду. - person user304582; 08.06.2017