Fixed variable references to $thisWidth and $thisHeight; amended template match for...
[rdsigngen:mainline.git] / roadsigngen.xsl
1 <?xml version="1.0"?>
2 <xsl:stylesheet version="2.0"\r
3                 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"\r
4                 xmlns:xs="http://www.w3.org/2001/XMLSchema"\r
5                 xmlns:rdsign="http://www.example.com/rdsign"
6                 xmlns="http://www.w3.org/2000/svg"\r
7                 exclude-result-prefixes="xs rdsign">\r
8 <xsl:output method="xml" media-type="image/svg+xml"\r
9             indent="yes" encoding="ISO-8859-1" />
10 <xsl:strip-space elements="*" />
11 <xsl:param name="debug" select="0"/>
12 \r
13 <xsl:variable name="widths" as="document-node()" select="doc('widths.xml')" />\r
14 <xsl:variable name="rdsign" as="document-node()" select="/" />\r
15 <xsl:key name="IDs" match="Character" use="@id" />\r
16 \r
17 <!-- top level match: add SVG boilerplate here -->
18 <!-- it will need to have width & height calculations -->\r
19 <xsl:template match="/">
20
21     <!-- pass 1: calculate layout -->
22     <xsl:variable name="treeWithLayout">
23         <xsl:apply-templates mode="calc-layout"/>
24     </xsl:variable>
25
26     <!-- dump enhanced tree if debug flag on -->
27     <xsl:choose>
28     <xsl:when test="$debug">
29         <source><xsl:copy-of select="." /></source>
30         <with-layout><xsl:copy-of select="$treeWithLayout" /></with-layout>
31     </xsl:when>
32
33     <xsl:otherwise>
34     <!-- pass 2: now draw it, using layout-enriched tree -->
35         <svg
36     version="1.0"
37     width="100"
38     height="100">
39         <!--<xsl:apply-templates select="$treeWithLayout/*" />-->
40         <xsl:apply-templates />
41     </svg>
42
43     <!-- end of debug switch -->
44     </xsl:otherwise>
45     </xsl:choose>
46     
47 </xsl:template>
48
49 \r
50 \r
51 <xsl:template match="sign">
52     <!-- calculate sign width and height -->
53     <xsl:variable name="signwidth" select="max(\r
54                         for $legend in //legend\r
55                         return sum(rdsign:length(rdsign:characters($legend))))" />
56         <xsl:variable name="signheight" select="sum(//legend/@padding-top) + 8 * count(//legend)" /> <!-- number of lines, plus padding -->
57         
58     <!-- draw rounded rect -->
59     <xsl:call-template name="roundrect">
60         <xsl:with-param name="width" select="$signwidth + 8" />
61         <xsl:with-param name="height" select="$signheight + 7" />
62         <xsl:with-param name="x" select="0" />
63         <xsl:with-param name="y" select="0" />
64         <xsl:with-param name="strokewidth" select="1.5" />
65         <xsl:with-param name="ry" select="1.5" />
66         <xsl:with-param name="fill" select="'none'" />
67         <xsl:with-param name="stroke" select="'black'" />
68     </xsl:call-template>
69         \r
70         <!--<numchars>\r
71                 this calculates max number of characters (not very useful!)\r
72                 <xsl:value-of select="max(\r
73                         for $legend in legend\r
74                         return string-length($legend))" />\r
75         </numchars>-->\r
76     <!-- add text -->
77     <g transform="translate(4 4)"> <!-- to bring within safe margin of the rounded rect (ch 7 fig 2.4)-->
78     
79     <!-- debug rectangle
80     <rect style="fill:none;fill-opacity:1;stroke:red;stroke-width:0.5;stroke-opacity:1" width="{$signwidth}" height="{$signheight}" x="0" y="0" />
81     -->
82         <xsl:apply-templates mode="draw" />
83             
84     <text style="font-size:7.44186046512px;font-style:normal;font-weight:normal;fill:black;font-family:Transport Heavy" x="0" y="8">\r
85         <xsl:apply-templates>
86             <xsl:with-param name="signwidth" select="$signwidth" /> <!-- This will get passed all the way down the tree. Which is nice -->
87         </xsl:apply-templates>
88         </text>
89                 
90         </g>
91         \r
92 </xsl:template>\r
93 \r
94 <xsl:template match="legend">
95     <xsl:param name="signwidth" as="xs:double" required="yes" />
96     <!-- assume that x=0, y=0 is top left of panel -->
97     <xsl:variable name="legendwidth" select="sum(rdsign:length(rdsign:characters(.)))" />
98     
99     <xsl:variable name="x" as="xs:double">
100     <!-- unless centre-aligned, x=0 -->
101         <xsl:choose>
102             <xsl:when test="@align = 'center'">
103                 <xsl:value-of select="($signwidth - $legendwidth) *0.5" />
104             </xsl:when>
105             <xsl:otherwise>0</xsl:otherwise>
106         </xsl:choose>
107     </xsl:variable>
108     
109     <xsl:variable name="dy" as="xs:double">
110     <!-- difference in y: 8 sw unless extra padding specified -->
111         <xsl:choose>
112             <xsl:when test="position() = 1">
113                 0
114             </xsl:when>
115             <xsl:otherwise>
116                 <xsl:value-of select="(if (@padding-top) then @padding-top else 0) + 8" />
117             </xsl:otherwise>
118         </xsl:choose>
119     </xsl:variable>
120
121     <tspan x="{$x}" dy="{$dy}">
122         <xsl:apply-templates>
123           <xsl:with-param name="legendwidth" select="$legendwidth" />
124         </xsl:apply-templates>
125     </tspan>
126     <!-- TODO XMLise extra/reduced spacing as per chap 7 pp9-11 using string replacement
127      http://www.dpawson.co.uk/xsl/sect2/StringReplace.html#d10034e19
128      May be possible to include road numbers in this too -->
129 </xsl:template>
130
131 <xsl:template match="legend" mode="draw">
132   <xsl:variable name="legendwidth" select="sum(rdsign:length(rdsign:characters(.)))" />
133   <xsl:apply-templates select="*" mode="draw">
134     <xsl:with-param name="legendwidth" select="$legendwidth" />
135   </xsl:apply-templates>
136 </xsl:template>
137
138 <xsl:template match="rdnum">
139     <!-- match road numbers and insert extra spaces
140     Note: svg <tspan> elements can be nested -->
141     <xsl:variable name="rdnum_regex" as="xs:string">
142       (  \(? [A-Z] )   <!-- optional open bracket, plus capital letter -->
143       (  \d+       )   <!-- one or more digits -->
144       (  \(  M  \) )?  <!-- optional (M) -->
145       (  \)        )?  <!-- optional close bracket -->
146     </xsl:variable>
147     <xsl:analyze-string select="." regex="{$rdnum_regex}" flags="x">
148         <xsl:matching-substring>
149             <xsl:value-of select="regex-group(1)" />
150             <tspan dx="1"><!-- 1sw space in road number -->
151                 <xsl:value-of select="regex-group(2)" />
152             </tspan>
153             <xsl:if test="regex-group(3)">
154                 <tspan dx="1"><!-- 1sw space before the (M) -->
155                     <xsl:value-of select="regex-group(3)" />
156                 </tspan>
157             </xsl:if>
158             <xsl:value-of select="regex-group(4)" /> <!-- closing bracket -->
159         </xsl:matching-substring>
160     </xsl:analyze-string>\r
161 </xsl:template>\r
162
163 <xsl:template match="patch">
164 <!-- fig 3-8 -->
165   <xsl:variable name="gap" as="xs:double">
166     <xsl:choose>
167       <xsl:when test="@class='primary'">
168         <!-- no border to patch -->
169         <xsl:value-of select="2.5 + 1" />
170       </xsl:when>
171       <xsl:otherwise>
172         <!-- 0.5sw border to patch -->
173         <xsl:value-of select="2.5 + 0.5 + 1" />
174       </xsl:otherwise>
175     </xsl:choose>
176   </xsl:variable>
177
178   <xsl:variable name="color" as="xs:string">
179     <xsl:choose>
180       <xsl:when test="@class='primary'">yellow</xsl:when>
181       <xsl:otherwise>black</xsl:otherwise>
182     </xsl:choose>
183   </xsl:variable>
184   
185   <xsl:variable name="font" as="xs:string">
186     <xsl:choose>
187       <xsl:when test="@class='primary'">Transport</xsl:when>
188       <xsl:otherwise>Transport Heavy</xsl:otherwise>
189     </xsl:choose>
190   </xsl:variable>
191
192   <tspan dx="{$gap}" fill="{$color}" font-family="{$font}">
193     <xsl:apply-templates />
194   </tspan>
195 </xsl:template>
196
197 <xsl:template match="patch" mode="draw">
198   <xsl:param name="legendwidth" required="yes" />
199
200   <xsl:variable name="gap" as="xs:double">
201     <xsl:choose>
202       <xsl:when test="@class='primary'">
203         <!-- no border to patch -->
204         <xsl:value-of select="2.5 + 1" />
205       </xsl:when>
206       <xsl:otherwise>
207         <!-- 0.5sw border to patch -->
208         <xsl:value-of select="2.5 + 0.5 + 1" />
209       </xsl:otherwise>
210     </xsl:choose>
211   </xsl:variable>
212
213     <xsl:if test="@class='primary'">
214         <xsl:call-template name="roundrect">
215         <xsl:with-param name="width" select="sum(rdsign:length(rdsign:characters(.))) + 2" /> <!-- fig 3-8 -->
216         <xsl:with-param name="height" select="9.5" />
217         <xsl:with-param name="x" select="$legendwidth - sum(rdsign:length(rdsign:characters(.))) + $gap - 1" /> <!--temp, need to do something more sophisticated -->
218         <xsl:with-param name="y" select="-1.5" /> <!--relative to the box, see fig 3-10 (need to correct if border) -->
219         <xsl:with-param name="strokewidth" select="0" />
220         <xsl:with-param name="ry" select="1" />
221         <xsl:with-param name="fill" select="'green'" />
222         <xsl:with-param name="stroke" select="'none'" />
223     </xsl:call-template>
224     </xsl:if>
225     <!-- <xsl:apply-templates select="*" mode="draw" /> -->
226 </xsl:template>
227
228 <xsl:template match="arrow" mode="draw">
229   <xsl:call-template name="draw_arrow">
230     <xsl:with-param name="angle" select="@angle" />
231   </xsl:call-template>
232 </xsl:template>
233
234 <!-- Named templates -->
235 <xsl:template name="roundrect">
236     <xsl:param name="width" as="xs:double" required="yes" />
237     <xsl:param name="height" as="xs:double" required="yes" />
238     <xsl:param name="x" as="xs:double" required="yes" />
239     <xsl:param name="y" as="xs:double" required="yes" />
240     <xsl:param name="strokewidth" as="xs:double" required="yes" />
241     <xsl:param name="ry" as="xs:double" />
242     <xsl:param name="fill" as="xs:string" />
243     <xsl:param name="stroke" as="xs:string" />
244     <rect style="fill:{$fill};fill-opacity:1;stroke:{$stroke};stroke-width:{$strokewidth};stroke-opacity:1"
245     width="{$width - $strokewidth}" height="{$height - $strokewidth}"
246         x="{$x + 0.5*$strokewidth}" y="{$y + 0.5*$strokewidth}"
247         ry="{$ry}" />
248 </xsl:template>
249
250 <xsl:template name="draw_arrow">
251   <!-- draw arrow, as in Fig 4-4 -->
252   <xsl:param name="angle" as="xs:integer" />
253   <xsl:param name="length" as="xs:integer" select="16" /> <!-- default length 16 sw -->
254   
255   <g transform="rotate({$angle} {$length div 2} 4)">  <!-- rotate around centre of arrow -->
256     <path d="M 0,4  l 4,-4  h 4  l -2.6666,2.6666  H {$length}  v 2.6666  H 5.3333 L 8,8  h -4 Z"
257     fill="black" />
258   </g>
259 </xsl:template>
260 \r
261 <!-- Functions -->\r
262 <!-- TODO can these be coerced into named templates, to allow compatibility with XSLT1.0? 
263 http://www.dpawson.co.uk/xsl/sect2/N8090.html#d10831e823 , http://www.dpawson.co.uk/xsl/sect2/nodetest.html#d7610e279, Tenison p245-->\r
264 <xsl:function name="rdsign:characters" as="xs:string*">\r
265   <!-- breaks down a string into individual characters -->\r
266   <xsl:param name="string" as="xs:string" />\r
267   <xsl:if test="$string">\r
268     <xsl:sequence select="substring($string, 1, 1)" />\r
269     <xsl:variable name="remainder" select="substring($string, 2)" as="xs:string" />\r
270     <xsl:if test="$remainder">\r
271       <xsl:sequence select="rdsign:characters($remainder)" />\r
272     </xsl:if>\r
273   </xsl:if>\r
274 </xsl:function>
275 \r
276 <xsl:function name="rdsign:length" as="xs:decimal+">\r
277   <!-- calculates total width of a set of characters using alphabet tile widths -->\r
278   <xsl:param name="chars" as="xs:string+" />\r
279         <xsl:for-each select="$chars" >\r
280                 <xsl:value-of select="key('IDs',., $widths)/Width" />\r
281         </xsl:for-each>\r
282 </xsl:function>
283 \r
284
285 <!-- Layout calculation
286
287 This makes a copy of the source tree and adds width and height elements, for easier processing later
288 Suggest adding padding, alignment to relevant parent elements etc?
289
290 Inspired by XSLT cookbook (Mangano) pp373-4
291 http://books.google.co.uk/books?id=XhJHgaQCtuMC&lpg=PA139&ots=L_-P1QDyfJ&dq=recursive%20descend%20tree%20xslt&pg=PA374#v=onepage&q=&f=false
292
293 TODO how to specialise text nodes: legend (simple), rdnum, panel, patch etc
294
295 -->
296
297 <!-- Matches elements with no children. TODO need a template to account for legend with children (eg patch/panel)-->
298 <xsl:template match="*[not(*)]" mode="calc-layout">
299     <xsl:copy>
300         <xsl:copy-of select="@*"/>
301         <xsl:attribute name="width">
302             <xsl:value-of select="sum(rdsign:length(rdsign:characters(.)))" />
303         </xsl:attribute>
304         <xsl:attribute name="height">
305             <xsl:value-of select="8" />
306         </xsl:attribute>
307         <xsl:copy-of select="text()"/>
308     </xsl:copy>
309 </xsl:template>
310
311 <!-- This matches all other nodes -->
312 <xsl:template match="node()" mode="calc-layout">
313     <xsl:variable name="subTree">
314         <xsl:apply-templates mode="calc-layout"/>
315     </xsl:variable>
316     
317     <xsl:variable name="thisWidth" select="max($subTree/*/@width)"/>
318     <xsl:variable name="thisHeight" select="sum($subTree/*/@height)"/>
319     
320     <!-- width is the maximum of children's width
321      height is the sum of children's height
322      TODO allow for blocks aligned next to one another
323         the reverse will apply (sum the width, max the height)
324         need to think of a suitable attribute/element to indicate where this occurs -->
325    
326     <xsl:copy>
327         <xsl:copy-of select="@*"/>
328         <xsl:attribute name="width">
329             <xsl:value-of select="$thisWidth"/>
330         </xsl:attribute>
331         <xsl:attribute name="height">
332             <xsl:value-of select="$thisHeight"/>
333         </xsl:attribute>
334         <xsl:copy-of select="$subTree"/>
335     </xsl:copy>
336 </xsl:template>
337
338
339
340
341 </xsl:stylesheet>