View Javadoc

1   package org.lcsim.geometry.compact.converter.svg;
2   
3   import java.text.DecimalFormat;
4   import java.util.ArrayList;
5   import java.util.Collections;
6   import java.util.Comparator;
7   import java.util.List;
8   
9   import org.jdom.Element;
10  import org.jdom.Namespace;
11  import org.lcsim.detector.IDetectorElement;
12  import org.lcsim.detector.solids.Tube;
13  import org.lcsim.geometry.Calorimeter;
14  import org.lcsim.geometry.Calorimeter.CalorimeterType;
15  import org.lcsim.geometry.Tracker;
16  import org.lcsim.geometry.compact.Detector;
17  import org.lcsim.geometry.compact.Subdetector;
18  import org.lcsim.geometry.compact.VisAttributes;
19  import org.lcsim.geometry.subdetector.DiskTracker;
20  import org.lcsim.geometry.subdetector.MultiLayerTracker;
21  import org.lcsim.geometry.subdetector.PolyconeSupport;
22  import org.lcsim.geometry.subdetector.SiTrackerBarrel;
23  import org.lcsim.geometry.subdetector.TubeSegment;
24  import org.lcsim.geometry.subdetector.PolyconeSupport.ZPlane;
25  
26  /**
27   * Convert from a compact detector description to an SVG view. This will only work correctly for full ILC physics
28   * detectors.
29   * 
30   * @author jeremym
31   */
32  // TODO Fix manual scaling where possible (primarily in text and associated lines).
33  // TODO Add groups for label lines so user units (from Detector) can be used instead of scaling.
34  // TODO For Calorimeters that are large enough (Hcal, Muon) add layering instead of fill.
35  // TODO Add lefthand labels of Calorimeter types, Tracker types, and Coil/Solenoid.
36  class SvgConverter
37  {
38      // Namespaces.
39      private static final Namespace ns = Namespace.getNamespace("http://www.w3.org/2000/svg");
40      private static final Namespace xlink = Namespace.getNamespace("xlink", "http://www.w3.org/1999/xlink");
41  
42      // Scaling of Subdetector drawing from LCSim natural units (mm).
43      private static final double scale = 0.1;
44  
45      // These margins are for positioning the Subdetector group and 
46      // are in LCSim units (mm).
47      private static final double xmargin = 1500;
48      private static final double ymargin = 1000;
49  
50      // FIXME This needs to be determined dynamically from size of
51      // left margin + left labels + detector area.
52      private static final double viewportX = 1200;
53  
54      // FIXME This needs to be determined dynamically from size of
55      // upper margin + detector area + bottom label area.
56      private static final double viewportY = 1000;
57  
58      // Formatting for length measurements.
59      private static final DecimalFormat df = new DecimalFormat("#.##");
60  
61      // Static access to important grouping elements.
62      private static Element labelsY;
63      private static Element labelsX;
64      // private static Element labelLinesX;
65      // private static Element labelLinesY;
66      // private static Element subdetGroup;
67  
68      // If barrel detector is less than this thick,
69      // its outer radius label will not be printed.
70      private static double yLabelTolerance = 200.;
71  
72      // Label text margin from leftmost edge.
73      private static double yLabelMarginX = 10;
74  
75      // Starting offset for text as bottom labels are created.
76      // This incremented as Z labels are added.
77      private static double zLabelOffsetY = 25;
78  
79      // If a layer is less than this size in LCSim units (mm),
80      // then it will be drawn as a line rather than a rectangle.
81      private static double minTrackerLayerThickness = 50;
82  
83      public static Element convert(Detector d)
84      {
85          // Convert to specific subclass for more method access.
86          Detector detector = (org.lcsim.geometry.compact.Detector)d;
87  
88          // Compute max ZY measurements of detector.
89          double[] zy = findMaxZY(detector);
90          // System.out.println("max xy = " + zy[0] + ", " + zy[1]);
91  
92          // SVG root element.
93          Element root = new Element("svg");
94          root.setNamespace(ns);
95          root.addNamespaceDeclaration(xlink);
96          root.setAttribute("version", "1.2");
97  
98          // Set viewport window.
99          root.setAttribute("width", viewportX + "px");
100         root.setAttribute("height", viewportY + "px");
101 
102         // All elements go into this group.
103         Element g = new Element("g", ns);
104         root.addContent(g);
105 
106         // Header with name.
107         Element header = text(g, detector.getName(), viewportX / 2, 50);
108         header.setAttribute("font-family", "Arial");
109         header.setAttribute("font-size", "32");
110 
111         // Subdetector group.
112         Element gs = new Element("g", ns);
113         g.addContent(gs);
114         gs.setAttribute("transform", "scale(" + scale + ") " + "translate(" + xmargin + ", " + ymargin + ")");
115 
116         // SVG X axis.
117         Element xaxis = line(gs, 0., zy[1], zy[0], zy[1]);
118         xaxis.setAttribute("stroke-width", "1");
119         xaxis.setAttribute("stroke", "black");
120         xaxis.setAttribute("id", "xaxis");
121 
122         // SVG Y axis.
123         Element yaxis = line(gs, 0., 0., 0., zy[1]);
124         yaxis.setAttribute("stroke-width", "1");
125         yaxis.setAttribute("stroke", "black");
126         yaxis.setAttribute("id", "yaxis");
127 
128         // SVG Y labels group.
129         labelsY = new Element("g", ns);
130         g.addContent(labelsY);
131         labelsY.setAttribute("font-size", "12");
132         labelsY.setAttribute("id", "ylabels");
133         labelsY.setAttribute("transform", "translate(" + 0 + ", " + ymargin * scale + ")");
134 
135         // SVG X labels group.
136         labelsX = new Element("g", ns);
137         g.addContent(labelsX);
138         labelsX.setAttribute("font-size", "12");
139         labelsX.setAttribute("id", "xlabels");
140         labelsX.setAttribute("transform", "translate(" + (xmargin * scale) + ", " + ((ymargin + zy[1]) * scale) + ")");
141 
142         // Make ZY view of Detector.
143         convertSubdetectors(gs, detector, zy[0], zy[1]);
144 
145         // Return the created Element to be written out by Main.
146         return root;
147     }
148 
149     private static String convertColor(VisAttributes vis)
150     {
151         float[] rgba = vis.getRGBA();
152         return "rgb(" + rgba[0] * 100 + "%, " + rgba[1] * 100 + "%, " + rgba[2] * 100 + "%)";
153     }
154 
155     private static class InnerRadiusCompare implements Comparator<Calorimeter>
156     {
157         public int compare(Calorimeter subdet1, Calorimeter subdet2)
158         {
159             double ir1 = subdet1.getInnerRadius();
160             double ir2 = subdet2.getInnerRadius();
161             if (ir1 > ir2)
162             {
163                 return 1;
164             }
165             else if (ir1 < ir2)
166             {
167                 return -1;
168             }
169             else
170             {
171                 return 0;
172             }
173         }
174     }
175 
176     private static class InnerZCompare implements Comparator<Calorimeter>
177     {
178         public int compare(Calorimeter subdet1, Calorimeter subdet2)
179         {
180             double ir1 = subdet1.getInnerZ();
181             double ir2 = subdet2.getInnerZ();
182             if (ir1 > ir2)
183             {
184                 return 1;
185             }
186             else if (ir1 < ir2)
187             {
188                 return -1;
189             }
190             else
191             {
192                 return 0;
193             }
194         }
195     }
196 
197     private static void convertSubdetectors(Element parent, Detector detector, double maxZ, double maxY)
198     {        
199         List<Subdetector> subdetectors = detector.getSubdetectorList();
200         
201         // Look at Calorimeters first.
202         List<Calorimeter> calorimeters = new ArrayList<Calorimeter>();
203         List<Calorimeter> barrelCalorimeters = new ArrayList<Calorimeter>();
204         List<Calorimeter> endcapCalorimeters = new ArrayList<Calorimeter>();
205         for (Subdetector subdet : subdetectors)
206         {
207             if (subdet instanceof Calorimeter)
208             {
209                 if (subdet.isBarrel())
210                 {
211                     barrelCalorimeters.add((Calorimeter)subdet);
212                 }
213                 else if (subdet.isEndcap())
214                 {
215                     endcapCalorimeters.add((Calorimeter)subdet);
216                 }
217             }
218         }        
219         
220         // Sort Calorimeter barrels on innerR.
221         Collections.sort(barrelCalorimeters, new InnerRadiusCompare());
222         
223         // Add sorted barrel calorimeters to list.
224         calorimeters.addAll(barrelCalorimeters);        
225 
226         // Sort Calorimeter endcaps on innerZ.
227         Collections.sort(endcapCalorimeters, new InnerZCompare());
228         
229         // Add sorted endcap calorimeters to list.
230         calorimeters.addAll(endcapCalorimeters);
231 
232         // Make the SVG for ordered Calorimeters.
233         for (Calorimeter cal : calorimeters)
234         {
235             SvgConverter.convertSubdetector(parent, (Subdetector)cal, maxZ, maxY);
236         }
237         
238         // Now draw the Trackers, supports, and dead material.
239         for (org.lcsim.geometry.Subdetector subdet : detector.getSubdetectors().values())
240         {
241             if (!(subdet instanceof Calorimeter))
242             {
243                 SvgConverter.convertSubdetector(parent, (Subdetector)subdet, maxZ, maxY);
244             }
245         }
246     }
247 
248     /**
249      * Order a List of ZPlanes by Z value.
250      * 
251      * @author jeremym
252      * 
253      */
254     private static class ZPlaneCompare implements Comparator<ZPlane>
255     {
256         public int compare(ZPlane zp1, ZPlane zp2)
257         {
258             double z1 = zp1.getZ();
259             double z2 = zp2.getZ();
260             if (z1 > z2)
261                 return 1;
262             else if (z1 < z2)
263                 return -1;
264             else
265                 return 0;
266         }
267     }
268 
269     private static void convertSubdetector(Element parent, Subdetector subdet, double maxZ, double maxY)
270     {
271         // Debug print.
272         System.out.println(">> " + subdet.getName());
273 
274         // Get VisAttributes of Subdetector.
275         VisAttributes vis = subdet.getVisAttributes();
276 
277         // If not visible then immediately return without drawing anything.
278         if (!vis.getVisible())
279         {
280             System.out.println("    *not visible* ... skipping");
281             return;
282         }
283 
284         // Convert color parameters to SVG format.
285         String color = convertColor(vis);
286         float alpha = vis.getRGBA()[3];
287 
288         // Make a group for this Subdetector.
289         Element g = new Element("g", ns);
290         g.setAttribute("id", subdet.getName());
291         parent.addContent(g);
292 
293         // Set Subdetector's line and fill colors.
294         g.setAttribute("stroke", color);
295         g.setAttribute("fill", color);
296         g.setAttribute("stroke-width", "3"); // Default stroke-width.
297         g.setAttribute("opacity", Float.toString(alpha));
298 
299         // Draw Calorimeters.
300         if (subdet instanceof Calorimeter)
301         {
302             // Turn off shape outline.
303             g.setAttribute("stroke-width", "0");
304 
305             // Get Calorimeter generic parameters.
306             Calorimeter cal = (Calorimeter)subdet;
307             double innerR = cal.getInnerRadius();
308             double outerR = cal.getOuterRadius();            
309             double halfZ = cal.getZLength() / 2;
310             double zlength = cal.getZLength();
311             
312             // The labels group is accessed statically to avoid having to pass it down.
313             Element labelGroup = labelsY;
314 
315             // Draw barrel calorimeters.
316             if (subdet.isBarrel())
317             {
318                 // Make a rectangular outline of calorimeter barrel.
319                 rect(g, 0., maxY - outerR, outerR - innerR, halfZ);
320 
321                 // Line at inner radius of barrel calorimeter.
322                 Element lineInner =
323                         line(labelGroup, 75., ((maxY - innerR) * scale), xmargin * scale, (maxY - innerR) * scale);
324                 lineInner.setAttribute("stroke-dasharray", "6,3");
325                 lineInner.setAttribute("stroke", "gray");
326                 lineInner.setAttribute("stroke-width", "1");
327 
328                 // Label inner radius measurement.
329                 text(labelGroup, df.format(innerR), yLabelMarginX, ((maxY - innerR) * scale) + 5);
330 
331                 // Outer R is only labeled if there is enough space for it.
332                 if (outerR - innerR > yLabelTolerance)
333                 {
334                     // Line at outer radius of barrel calorimeter.
335                     Element lineOuter =
336                             line(labelGroup, 75., ((maxY - outerR) * scale), xmargin * scale, (maxY - outerR) * scale);
337                     lineOuter.setAttribute("stroke-dasharray", "6,3");
338                     lineOuter.setAttribute("stroke", "gray");
339                     lineOuter.setAttribute("stroke-width", "1");
340 
341                     // Label outer radius measurement.
342                     text(labelGroup, df.format(outerR), yLabelMarginX, ((maxY - outerR) * scale) + 5);
343                 }
344 
345                 // Do bottom labels now. Switch variable reference.
346                 labelGroup = labelsX;
347 
348                 // Dashed lines at bottom along z direction indicating barrel z measurements.
349                 Element lineBottom = line(labelGroup, 0, zLabelOffsetY, halfZ * scale, zLabelOffsetY);
350                 lineBottom.setAttribute("stroke-dasharray", "6,3");
351                 lineBottom.setAttribute("stroke", "gray");
352                 lineBottom.setAttribute("stroke-width", "1");
353 
354                 // Label measurement for barrel z.
355                 text(labelGroup, df.format(halfZ), ((halfZ * scale) / 2), zLabelOffsetY - 5);
356 
357                 // Increment for next label.
358                 zLabelOffsetY += 25;
359                 
360                 Calorimeter.CalorimeterType calType = cal.getCalorimeterType();
361                 System.out.println(calType.toString());
362                 if (!calType.equals(CalorimeterType.UNKNOWN))
363                 {
364                     String calLabel = calType.toString().replace("_BARREL", "");
365                     double calThickness = outerR - innerR;
366                     double y = (maxY - outerR) + calThickness / 2;
367                     y *= scale;
368                     Element calText = text(labelsY, calLabel, 80, y + 5);
369                     calText.setAttribute("font-size", "12");
370                 }
371             }
372             // Draw endcap calorimeters that are reflected.
373             else if (subdet.isEndcap() && subdet.getReflect())
374             {
375                 // Get geometry parameters.
376                 double innerZ = cal.getInnerZ();
377                 double outerZ = cal.getOuterZ();
378 
379                 // Make a rectangle for the endpca.
380                 rect(g, innerZ, maxY - outerR, outerR - innerR, halfZ * 2);
381 
382                 double thickness = outerZ - innerZ;
383 
384                 // Setup reference to make a label and line on the xaxis.
385                 labelGroup = labelsX;
386 
387                 // Line indicating endcap extent in X (or Z in LCSim coordinates).
388                 Element lineEndcap = line(labelGroup, innerZ * scale, zLabelOffsetY, outerZ * scale, zLabelOffsetY);
389                 lineEndcap.setAttribute("stroke-dasharray", "6,3");
390                 lineEndcap.setAttribute("stroke", "gray");
391                 lineEndcap.setAttribute("stroke-width", "1");
392 
393                 // Label measurement of endcap z length.
394                 // FIXME Manual 10 pix adjustment.
395                 text(labelGroup, df.format(zlength), ((innerZ + thickness / 2) * scale) - 10, zLabelOffsetY - 5);
396 
397                 // Increment labeling offset.
398                 zLabelOffsetY += 25;
399             }
400         }
401         // Draw trackers.
402         else if (subdet instanceof Tracker)
403         {
404             // Draw barrel trackers.
405             if (subdet instanceof SiTrackerBarrel || subdet instanceof MultiLayerTracker)
406             {
407                 double minR = 9999999.;
408                 double maxR = 0;
409                 // Loop over SiTrackerBarrel layers.
410                 IDetectorElement de = subdet.getDetectorElement();
411                 for (IDetectorElement layer : de.getChildren())
412                 {
413                     // Get parameters from layer's tube shape.
414                     Tube tube = (Tube)layer.getGeometry().getLogicalVolume().getSolid();
415                     double thickness = tube.getOuterRadius() - tube.getInnerRadius();
416                     double r = tube.getInnerRadius() + thickness / 2;
417                     double halfZ = tube.getZHalfLength();
418                     double outerR = tube.getOuterRadius();
419                     double innerR = tube.getInnerRadius();
420                     
421                     if (innerR < minR)
422                     {
423                         minR = innerR;
424                     }
425                     
426                     if (outerR > maxR)
427                     {
428                         maxR = outerR;
429                     }
430 
431                     // Draw a line for this tracker layer.
432                     if (thickness < minTrackerLayerThickness)
433                     {
434                         // Draw a line with Subdetector's color.
435                         line(g, 0, maxY - r, halfZ, maxY - r);
436                     }
437                     // Draw a layer rectangle if component is thick enough.
438                     else
439                     {
440                         // Use a black outline to separate nearby layers.
441                         g.setAttribute("stroke", "black");
442 
443                         // Make a rectangle for the layer.
444                         rect(g, 0., maxY - outerR, outerR - innerR, halfZ);
445                     }
446                 }
447                 
448                 System.out.println("maxR = " + maxR);
449                 System.out.println("minR = " + minR);
450             }
451             // Draw DiskTracker.
452             // FIXME Replace compact based code with IDetectorElement, but DiskTracker
453             // layers need their own DetectorElements first.
454             else if (subdet instanceof DiskTracker)
455             {
456                 DiskTracker diskTracker = (DiskTracker)subdet;
457                 int nlayers = diskTracker.getInnerR().length;
458                 for (int i = 0, n = nlayers; i < n; i++ )
459                 {
460                     double innerR = diskTracker.getInnerR()[i];
461                     double outerR = diskTracker.getOuterR()[i];
462                     double z = diskTracker.getThickness()[i];
463                     double innerZ = diskTracker.getInnerZ()[i];
464                     double midZ = innerZ + z / 2;
465 
466                     // Draw a line for this tracker layer.
467                     if (z < minTrackerLayerThickness)
468                     {
469                         // Draw a line with Subdetector's color.
470                         line(g, midZ, maxY - outerR, midZ, maxY - innerR);
471                     }
472                     // Draw a layer rectangle if component is thick enough.
473                     else
474                     {
475                         // Use a black outline to separate nearby layers.
476                         g.setAttribute("stroke", "black");
477 
478                         // Make a rectangle for the layer.
479                         rect(g, innerZ, maxY - outerR, outerR - innerR, z);
480                     }
481                 }
482             }
483         }
484         else if (subdet instanceof PolyconeSupport)
485         {
486             PolyconeSupport support = (PolyconeSupport)subdet;
487 
488             // Sort zplanes by z, from negative to positive.
489             List<ZPlane> zplanes = new ArrayList<ZPlane>(support.getZPlanes());
490             Collections.sort(zplanes, new ZPlaneCompare());
491 
492             // Make a list of usable ZPlanes.
493             List<ZPlane> zplanesUse = new ArrayList<ZPlane>();
494             for (int i = 0, n = zplanes.size(); i < n; i++ )
495             {
496                 ZPlane zplane = zplanes.get(i);
497 
498                 // Only use ZPlanes in positive Z.
499                 if (zplane.getZ() > 0)
500                 {
501                     // Make a modified ZPlane for components that cross the xaxis.
502                     if (i > 0 && zplanesUse.size() == 0)
503                     {
504                         // Get prior ZPlane with negative Z coordinate.
505                         ZPlane lastNegZPlane = zplanes.get(i - 1);
506 
507                         // If radii are the same, then draw from Y axis.
508                         if (lastNegZPlane.getRMin() == zplane.getRMin() && lastNegZPlane.getRMax() == zplane.getRMax())
509                         {
510                             ZPlane borderZPlane = new ZPlane(lastNegZPlane.getRMin(), lastNegZPlane.getRMax(), 0);
511                             zplanesUse.add(borderZPlane);
512                         }
513                         // FIXME Handle ZPlanes crossing xaxis that have different radii from the next ZPlane.
514                     }
515                     // Add positive ZPlane to usable list.
516                     else
517                     {
518                         zplanesUse.add(zplane);
519                     }
520                 }
521             }
522 
523             if (zplanesUse.size() > 0)
524             {
525                 // Buffer to store positions for polygon.
526                 StringBuffer buff = new StringBuffer();
527 
528                 // Add outer radii points going in positive X direction.
529                 for (ZPlane zplane : zplanesUse)
530                 {
531                     double outerR = zplane.getRMax();
532                     double z = zplane.getZ();
533                     buff.append(z + "," + (maxY - outerR) + " ");
534                 }
535 
536                 // Make a reverse list of the ZPlanes.
537                 List<ZPlane> reverseZPlanes = new ArrayList<ZPlane>(zplanesUse);
538                 Collections.reverse(reverseZPlanes);
539 
540                 // Add inner radii points going in the negative X direction.
541                 for (ZPlane zplane : reverseZPlanes)
542                 {
543                     double innerR = zplane.getRMin();
544                     double z = zplane.getZ();
545                     buff.append(z + "," + (maxY - innerR) + " ");
546                 }
547                                 
548                 String points = buff.toString();
549                 points.trim();
550 
551                 // Make the polygon using the list of points.
552                 Element polygon = new Element("polygon", ns);
553                 polygon.setAttribute("points", points);
554                 g.addContent(polygon);
555             }
556         }
557         else if (subdet instanceof TubeSegment)
558         {            
559             TubeSegment tube = (TubeSegment)subdet;
560                         
561             if (tube.getTransform().getTranslation().z() > 0)
562             {
563                 double innerR = tube.getInnerRadius();
564                 double outerR = tube.getOuterRadius();
565                 double halfZ = tube.getZHalfLength();
566                 double zmin = tube.getTransform().getTranslation().z() - halfZ;
567                            
568                 // Only draw components with a positive z position.
569                 if (zmin > 0)
570                 {
571                     rect(g, zmin, maxY - outerR, outerR - innerR, halfZ);
572                 }
573                 
574                 // FIXME: Rotation is completely ignored.
575                 // FIXME: TubeSegments that go across Y axis into positive X region are ignored.
576             }
577         }
578         // TODO Handle these additional types...
579         // SiTrackerEndcap
580         // SiTrackerEndcap2
581     }
582 
583     private static Element line(Element parent, double x1, double y1, double x2, double y2)
584     {
585         Element line = new Element("line", ns);
586         parent.addContent(line);
587         line.setAttribute("x1", df.format(x1));
588         line.setAttribute("y1", df.format(y1));
589         line.setAttribute("x2", df.format(x2));
590         line.setAttribute("y2", df.format(y2));
591         return line;
592     }
593 
594     private static Element rect(Element parent, double x, double y, double height, double width)
595     {
596         Element rect = new Element("rect", ns);
597         parent.addContent(rect);
598         rect.setAttribute("x", df.format(x));
599         rect.setAttribute("y", df.format(y));
600         rect.setAttribute("height", df.format(height));
601         rect.setAttribute("width", df.format(width));
602         return rect;
603     }
604 
605     private static Element text(Element parent, String text, double x, double y)
606     {
607         Element t = new Element("text", ns);
608         parent.addContent(t);
609         t.setText(text);
610         t.setAttribute("x", df.format(x));
611         t.setAttribute("y", df.format(y));
612         return t;
613     }
614 
615     private static double[] findMaxZY(Detector detector)
616     {
617         double[] zy = new double[2];
618         double z = 0;
619         double y = 0;
620 
621         // Assume calorimeter with largest extent defines max ZX.
622         for (Subdetector subdet : detector.getSubdetectors().values())
623         {
624             if (subdet instanceof Calorimeter)
625             {
626                 Calorimeter cal = (Calorimeter)subdet;
627                 if (cal.getOuterRadius() > y)
628                 {
629                     y = cal.getOuterRadius();
630                 }
631                 if (cal.getOuterZ() > z)
632                 {
633                     z = cal.getOuterZ();
634                 }
635             }
636         }
637 
638         if (z == 0 || y == 0)
639         {
640             throw new RuntimeException("Could not find ZY extent of this Detector!");
641         }
642 
643         zy[0] = z;
644         zy[1] = y;
645 
646         return zy;
647     }
648 }