1// Verbatim.java - Saxon extensions supporting DocBook verbatim environments
2
3package com.nwalsh.saxon;
4
5import java.util.Hashtable;
6import org.xml.sax.*;
7import org.w3c.dom.*;
8import javax.xml.transform.TransformerException;
9import com.icl.saxon.Controller;
10import com.icl.saxon.expr.*;
11import com.icl.saxon.om.*;
12import com.icl.saxon.pattern.*;
13import com.icl.saxon.Context;
14import com.icl.saxon.tree.*;
15import com.icl.saxon.functions.Extensions;
16
17/**
18 * <p>Saxon extensions supporting Tables</p>
19 *
20 * <p>$Id: Table.java,v 1.3 2006/04/27 08:26:47 xmldoc Exp $</p>
21 *
22 * <p>Copyright (C) 2000 Norman Walsh.</p>
23 *
24 * <p>This class provides a
25 * <a href="http://saxon.sourceforge.net/">Saxon</a>
26 * implementation of some code to adjust CALS Tables to HTML
27 * Tables.</p>
28 *
29 * <p><b>Column Widths</b></p>
30 * <p>The <tt>adjustColumnWidths</tt> method takes a result tree
31 * fragment (assumed to contain the colgroup of an HTML Table)
32 * and returns the result tree fragment with the column widths
33 * adjusted to HTML terms.</p>
34 *
35 * <p><b>Convert Lengths</b></p>
36 * <p>The <tt>convertLength</tt> method takes a length specification
37 * of the form 9999.99xx (where "xx" is a unit) and returns that length
38 * as an integral number of pixels. For convenience, percentage lengths
39 * are returned unchanged.</p>
40 * <p>The recognized units are: inches (in), centimeters (cm),
41 * millimeters (mm), picas (pc, 1pc=12pt), points (pt), and pixels (px).
42 * A number with no units is assumed to be pixels.</p>
43 *
44 * <p><b>Change Log:</b></p>
45 * <dl>
46 * <dt>1.0</dt>
47 * <dd><p>Initial release.</p></dd>
48 * </dl>
49 *
50 * @author Norman Walsh
51 * <a href="mailto:ndw@nwalsh.com">ndw@nwalsh.com</a>
52 *
53 * @version $Id: Table.java,v 1.3 2006/04/27 08:26:47 xmldoc Exp $
54 *
55 */
56public class Table {
57  /** The number of pixels per inch */
58  private static int pixelsPerInch = 96;
59
60  /** The nominal table width (6in by default). */
61  private static int nominalWidth = 6 * pixelsPerInch;
62
63  /** The default table width (100% by default). */
64  private static String tableWidth = "100%";
65
66  /** Is this an FO stylesheet? */
67  private static boolean foStylesheet = false;
68
69  /** The hash used to associate units with a length in pixels. */
70  protected static Hashtable unitHash = null;
71
72  /**
73   * <p>Constructor for Verbatim</p>
74   *
75   * <p>All of the methods are static, so the constructor does nothing.</p>
76   */
77  public Table() {
78  }
79
80  /** Initialize the internal hash table with proper values. */
81  protected static void initializeHash() {
82    unitHash = new Hashtable();
83    unitHash.put("in", new Float(pixelsPerInch));
84    unitHash.put("cm", new Float(pixelsPerInch / 2.54));
85    unitHash.put("mm", new Float(pixelsPerInch / 25.4));
86    unitHash.put("pc", new Float((pixelsPerInch / 72) * 12));
87    unitHash.put("pt", new Float(pixelsPerInch / 72));
88    unitHash.put("px", new Float(1));
89  }
90
91  /** Set the pixels-per-inch value. Only positive values are legal. */
92  public static void setPixelsPerInch(int value) {
93    if (value > 0) {
94      pixelsPerInch = value;
95      initializeHash();
96    }
97  }
98
99  /** Return the current pixels-per-inch value. */
100  public int getPixelsPerInch() {
101    return pixelsPerInch;
102  }
103
104  /**
105   * <p>Convert a length specification to a number of pixels.</p>
106   *
107   * <p>The specified length should be of the form [+/-]999.99xx,
108   * where xx is a valid unit.</p>
109   */
110  public static int convertLength(String length) {
111    // The format of length should be 999.999xx
112    int sign = 1;
113    String digits = "";
114    String units = "";
115    char lench[] = length.toCharArray();
116    float flength = 0;
117    boolean done = false;
118    int pos = 0;
119    float factor = 1;
120    int pixels = 0;
121
122    if (unitHash == null) {
123      initializeHash();
124    }
125
126    if (lench[pos] == '+' || lench[pos] == '-') {
127      if (lench[pos] == '-') {
128	sign = -1;
129      }
130      pos++;
131    }
132
133    while (!done) {
134      if (pos >= lench.length) {
135	done = true;
136      } else {
137	if ((lench[pos] > '9' || lench[pos] < '0') && lench[pos] != '.') {
138	  done = true;
139	  units = length.substring(pos);
140	} else {
141	  digits += lench[pos++];
142	}
143      }
144    }
145
146    try {
147      flength = Float.parseFloat(digits);
148    } catch (NumberFormatException e) {
149      System.out.println(digits + " is not a number; 1 used instead.");
150      flength = 1;
151    }
152
153    Float f = null;
154
155    if (!units.equals("")) {
156      f = (Float) unitHash.get(units);
157      if (f == null) {
158	System.out.println(units + " is not a known unit; 1 used instead.");
159	factor = 1;
160      } else {
161	factor = f.floatValue();
162      }
163    } else {
164      factor = 1;
165    }
166
167    f = new Float(flength * factor);
168
169    pixels = f.intValue() * sign;
170
171    return pixels;
172  }
173
174  /**
175   * <p>Find the string value of a stylesheet variable or parameter</p>
176   *
177   * <p>Returns the string value of <code>varName</code> in the current
178   * <code>context</code>. Returns the empty string if the variable is
179   * not defined.</p>
180   *
181   * @param context The current stylesheet context
182   * @param varName The name of the variable (without the dollar sign)
183   *
184   * @return The string value of the variable
185   */
186  protected static String getVariable(Context context, String varName)
187    throws TransformerException {
188    Value variable = null;
189    String varString = null;
190
191    try {
192      variable = Extensions.evaluate(context, "$" + varName);
193      varString = variable.asString();
194      return varString;
195    } catch (IllegalArgumentException e) {
196      System.out.println("Undefined variable: " + varName);
197      return "";
198    }
199  }
200
201  /**
202   * <p>Setup the parameters associated with column width calculations</p>
203   *
204   * <p>This method queries the stylesheet for the variables
205   * associated with table column widths. It is called automatically before
206   * column widths are adjusted. The context is used to retrieve the values,
207   * this allows templates to redefine these variables.</p>
208   *
209   * <p>The following variables are queried. If the variables do not
210   * exist, builtin defaults will be used (but you may also get a bunch
211   * of messages from the Java interpreter).</p>
212   *
213   * <dl>
214   * <dt><code>nominal.table.width</code></dt>
215   * <dd>The "normal" width for tables. This must be an absolute length.</dd>
216   * <dt><code>table.width</code></dt>
217   * <dd>The width for tables. This may be either an absolute
218   * length or a percentage.</dd>
219   * </dl>
220   *
221   * @param context The current stylesheet context
222   *
223   */
224  private static void setupColumnWidths(Context context) {
225    // Hardcoded defaults
226    nominalWidth = 6 * pixelsPerInch;
227    tableWidth = "100%";
228
229    String varString = null;
230
231    try {
232      // Get the stylesheet type
233      varString = getVariable(context, "stylesheet.result.type");
234      foStylesheet = varString.equals("fo");
235
236      // Get the nominal table width
237      varString = getVariable(context, "nominal.table.width");
238      nominalWidth = convertLength(varString);
239
240      // Get the table width
241      varString = getVariable(context, "table.width");
242      tableWidth = varString;
243    } catch (TransformerException e) {
244      //nop, can't happen
245    }
246  }
247
248  /**
249   * <p>Adjust column widths in an HTML table.</p>
250   *
251   * <p>The specification of column widths in CALS (a relative width
252   * plus an optional absolute width) are incompatible with HTML column
253   * widths. This method adjusts CALS column width specifiers in an
254   * attempt to produce equivalent HTML specifiers.</p>
255   *
256   * <p>In order for this method to work, the CALS width specifications
257   * should be placed in the "width" attribute of the &lt;col>s within
258   * a &lt;colgroup>. Then the colgroup result tree fragment is passed
259   * to this method.</p>
260   *
261   * <p>This method makes use of two parameters from the XSL stylesheet
262   * that calls it: <code>nominal.table.width</code> and
263   * <code>table.width</code>. The value of <code>nominal.table.width</code>
264   * must be an absolute distance. The value of <code>table.width</code>
265   * can be either absolute or relative.</p>
266   *
267   * <p>Presented with a mixture of relative and
268   * absolute lengths, the table width is used to calculate
269   * appropriate values. If the <code>table.width</code> is relative,
270   * the nominal width is used for this calculation.</p>
271   *
272   * <p>There are three possible combinations of values:</p>
273   *
274   * <ol>
275   * <li>There are no relative widths; in this case the absolute widths
276   * are used in the HTML table.</li>
277   * <li>There are no absolute widths; in this case the relative widths
278   * are used in the HTML table.</li>
279   * <li>There are a mixture of absolute and relative widths:
280   *   <ol>
281   *     <li>If the table width is absolute, all widths become absolute.</li>
282   *     <li>If the table width is relative, make all the widths absolute
283   *         relative to the nominal table width then turn them all
284   *         back into relative widths.</li>
285   *   </ol>
286   * </li>
287   * </ol>
288   *
289   * @param context The stylesheet context; supplied automatically by Saxon
290   * @param rtf_ns The result tree fragment containing the colgroup.
291   *
292   * @return The result tree fragment containing the adjusted colgroup.
293   *
294   */
295  public static NodeSetValue adjustColumnWidths (Context context,
296						 NodeSetValue rtf_ns) {
297
298    FragmentValue rtf = (FragmentValue) rtf_ns;
299
300    setupColumnWidths(context);
301
302    try {
303      Controller controller = context.getController();
304      NamePool namePool = controller.getNamePool();
305
306      ColumnScanEmitter csEmitter = new ColumnScanEmitter(namePool);
307      rtf.replay(csEmitter);
308
309      int numColumns = csEmitter.columnCount();
310      String widths[] = csEmitter.columnWidths();
311
312      float relTotal = 0;
313      float relParts[] = new float[numColumns];
314
315      float absTotal = 0;
316      float absParts[] = new float[numColumns];
317
318      for (int count = 0; count < numColumns; count++) {
319	String width = widths[count];
320
321	int pos = width.indexOf("*");
322	if (pos >= 0) {
323	  String relPart = width.substring(0, pos);
324	  String absPart = width.substring(pos+1);
325
326	  try {
327	    float rel = Float.parseFloat(relPart);
328	    relTotal += rel;
329	    relParts[count] = rel;
330	  } catch (NumberFormatException e) {
331	    System.out.println(relPart + " is not a valid relative unit.");
332	  }
333
334	  int pixels = 0;
335	  if (absPart != null && !absPart.equals("")) {
336	    pixels = convertLength(absPart);
337	  }
338
339	  absTotal += pixels;
340	  absParts[count] = pixels;
341	} else {
342	  relParts[count] = 0;
343
344	  int pixels = 0;
345	  if (width != null && !width.equals("")) {
346	    pixels = convertLength(width);
347	  }
348
349	  absTotal += pixels;
350	  absParts[count] = pixels;
351	}
352      }
353
354      // Ok, now we have the relative widths and absolute widths in
355      // two parallel arrays.
356      //
357      // - If there are no relative widths, output the absolute widths
358      // - If there are no absolute widths, output the relative widths
359      // - If there are a mixture of relative and absolute widths,
360      //   - If the table width is absolute, turn these all into absolute
361      //     widths.
362      //   - If the table width is relative, turn these all into absolute
363      //     widths in the nominalWidth and then turn them back into
364      //     percentages.
365
366      if (relTotal == 0) {
367	for (int count = 0; count < numColumns; count++) {
368	  Float f = new Float(absParts[count]);
369	  if (foStylesheet) {
370	    int pixels = f.intValue();
371	    float inches = (float) pixels / pixelsPerInch;
372	    widths[count] = inches + "in";
373	  } else {
374	    widths[count] = Integer.toString(f.intValue());
375	  }
376	}
377      } else if (absTotal == 0) {
378	for (int count = 0; count < numColumns; count++) {
379	  float rel = relParts[count] / relTotal * 100;
380	  Float f = new Float(rel);
381	  widths[count] = Integer.toString(f.intValue());
382	}
383	widths = correctRoundingError(widths);
384      } else {
385	int pixelWidth = nominalWidth;
386
387	if (tableWidth.indexOf("%") <= 0) {
388	  pixelWidth = convertLength(tableWidth);
389	}
390
391	if (pixelWidth <= absTotal) {
392	  System.out.println("Table is wider than table width.");
393	} else {
394	  pixelWidth -= absTotal;
395	}
396
397	absTotal = 0;
398	for (int count = 0; count < numColumns; count++) {
399	  float rel = relParts[count] / relTotal * pixelWidth;
400	  relParts[count] = rel + absParts[count];
401	  absTotal += rel + absParts[count];
402	}
403
404	if (tableWidth.indexOf("%") <= 0) {
405	  for (int count = 0; count < numColumns; count++) {
406	    Float f = new Float(relParts[count]);
407	    if (foStylesheet) {
408	      int pixels = f.intValue();
409	      float inches = (float) pixels / pixelsPerInch;
410	      widths[count] = inches + "in";
411	    } else {
412	      widths[count] = Integer.toString(f.intValue());
413	    }
414	  }
415	} else {
416	  for (int count = 0; count < numColumns; count++) {
417	    float rel = relParts[count] / absTotal * 100;
418	    Float f = new Float(rel);
419	    widths[count] = Integer.toString(f.intValue());
420	  }
421	  widths = correctRoundingError(widths);
422	}
423      }
424
425      ColumnUpdateEmitter cuEmitter = new ColumnUpdateEmitter(controller,
426							      namePool,
427							      widths);
428
429      rtf.replay(cuEmitter);
430      return cuEmitter.getResultTreeFragment();
431    } catch (TransformerException e) {
432      // This "can't" happen.
433      System.out.println("Transformer Exception in adjustColumnWidths");
434      return rtf;
435    }
436  }
437
438  /**
439   * Correct rounding errors introduced in calculating the width of each
440   * column. Make sure they sum to 100% in the end.
441   */
442  protected static String[] correctRoundingError(String widths[]) {
443    int totalWidth = 0;
444
445    for (int count = 0; count < widths.length; count++) {
446      try {
447	int width = Integer.parseInt(widths[count]);
448	totalWidth += width;
449      } catch (NumberFormatException nfe) {
450	// nop; "can't happen"
451      }
452    }
453
454    float totalError = 100 - totalWidth;
455    float columnError = totalError / widths.length;
456    float error = 0;
457
458    for (int count = 0; count < widths.length; count++) {
459      try {
460	int width = Integer.parseInt(widths[count]);
461	error = error + columnError;
462	if (error >= 1.0) {
463	  int adj = (int) Math.round(Math.floor(error));
464	  error = error - (float) Math.floor(error);
465	  width = width + adj;
466	  widths[count] = Integer.toString(width) + "%";
467	} else {
468	  widths[count] = Integer.toString(width) + "%";
469	}
470      } catch (NumberFormatException nfe) {
471	// nop; "can't happen"
472      }
473    }
474
475    return widths;
476  }
477}
478