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