View Javadoc

1   /*
2    * Copyright 2002 - 2007 JEuclid, http://jeuclid.sf.net
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  /* $Id: JEuclidView.java,v 371548310efa 2010/08/09 21:15:33 max $ */
18  
19  package net.sourceforge.jeuclid.layout;
20  
21  import java.awt.Color;
22  import java.awt.Graphics2D;
23  import java.awt.Image;
24  import java.awt.Rectangle;
25  import java.awt.RenderingHints;
26  import java.awt.geom.Line2D;
27  import java.awt.geom.Rectangle2D;
28  import java.awt.image.BufferedImage;
29  import java.util.ArrayList;
30  import java.util.HashMap;
31  import java.util.LinkedList;
32  import java.util.List;
33  import java.util.Map;
34  
35  import net.sourceforge.jeuclid.DOMBuilder;
36  import net.sourceforge.jeuclid.LayoutContext;
37  import net.sourceforge.jeuclid.context.Parameter;
38  import net.sourceforge.jeuclid.elements.generic.DocumentElement;
39  import net.sourceforge.jeuclid.elements.presentation.token.Mo;
40  
41  import org.apache.commons.logging.Log;
42  import org.apache.commons.logging.LogFactory;
43  import org.w3c.dom.Node;
44  import org.w3c.dom.NodeList;
45  import org.w3c.dom.events.Event;
46  import org.w3c.dom.events.EventListener;
47  import org.w3c.dom.events.EventTarget;
48  import org.w3c.dom.views.AbstractView;
49  import org.w3c.dom.views.DocumentView;
50  
51  /**
52   * @version $Revision: 371548310efa $
53   */
54  public class JEuclidView implements AbstractView, LayoutView, EventListener {
55  
56      private static final Log LOGGER = LogFactory.getLog(JEuclidView.class);
57  
58      private final LayoutableDocument document;
59  
60      private final Map<Node, LayoutInfo> layoutMap;
61  
62      private final LayoutContext context;
63  
64      private final Graphics2D graphics;
65  
66      /**
67       * Default Constructor.
68       * 
69       * @param node
70       *            document to layout.
71       * @param layoutGraphics
72       *            Graphics context to use for layout calculations. This should
73       *            be compatible to the context used for painting, but does not
74       *            have to be the same. If it is null, a default Graphics context
75       *            is created.
76       * @param layoutContext
77       *            layoutContext to use.
78       */
79      public JEuclidView(final Node node, final LayoutContext layoutContext,
80              final Graphics2D layoutGraphics) {
81          assert node != null : "Node must not be null";
82          assert layoutContext != null : "LayoutContext must not be null";
83          if (node instanceof LayoutableDocument) {
84              this.document = (LayoutableDocument) node;
85          } else {
86              this.document = DOMBuilder.getInstance().createJeuclidDom(node,
87                      true, true);
88          }
89          if (layoutGraphics == null) {
90              final Image tempimage = new BufferedImage(1, 1,
91                      BufferedImage.TYPE_INT_ARGB);
92              this.graphics = (Graphics2D) tempimage.getGraphics();
93          } else {
94              this.graphics = layoutGraphics;
95          }
96          this.context = layoutContext;
97          this.layoutMap = new HashMap<Node, LayoutInfo>();
98      }
99  
100     /**
101      * replace old node with new node in JEuclid document.
102      * 
103      * @param jDocOld
104      *            old JEuclid document
105      * @param oldNode
106      *            Node to remove
107      * @param newNode
108      *            Node to insert
109      * 
110      * @return new JEuclid Document
111      */
112     public static DocumentElement replaceNodes(final DocumentElement jDocOld,
113             final Node oldNode, final Node newNode) {
114         DocumentElement jDocNew;
115         Node imported;
116         Node parent;
117         List<Integer> path;
118         int i;
119 
120         // create jeuclid dom of node
121         jDocNew = DOMBuilder.getInstance().createJeuclidDom(newNode);
122 
123         // check if newNode is root of new tree
124         if (newNode.getParentNode().getParentNode() == null) {
125             return jDocNew;
126         } else {
127             imported = jDocOld.importNode(jDocNew.getDocumentElement(), true);
128 
129             path = new ArrayList<Integer>();
130             parent = oldNode;
131 
132             while (parent.getParentNode() != null) {
133                 i = 0;
134                 while (parent.getPreviousSibling() != null) {
135                     parent = parent.getPreviousSibling();
136                     i--;
137                 }
138                 path.add(-i);
139                 parent = parent.getParentNode();
140             }
141 
142             parent = jDocOld.getDocumentElement();
143             for (i = path.size() - 2; i > 0; i--) {
144                 parent = parent.getChildNodes().item(path.get(i));
145             }
146 
147             final Node realOldNode = parent.getChildNodes().item(path.get(0));
148 
149             JEuclidView.LOGGER.debug("replace " + realOldNode.getNodeName()
150                     + " with " + imported.getNodeName() + " under "
151                     + parent.getNodeName());
152 
153             parent.insertBefore(imported, realOldNode);
154             parent.removeChild(realOldNode);
155 
156             return jDocOld;
157         }
158     }
159 
160     /** {@inheritDoc} */
161     public DocumentView getDocument() {
162         return this.document;
163     }
164 
165     /**
166      * Draw this view onto a Graphics context.
167      * 
168      * @param x
169      *            x-offset for left edge
170      * @param y
171      *            y-offset for baseline
172      * @param g
173      *            Graphics context for painting. Should be compatible to the
174      *            context used during construction, but does not have to be
175      *            the same.
176      */
177     public void draw(final Graphics2D g, final float x, final float y) {
178         this.layout();
179         final RenderingHints hints = g.getRenderingHints();
180         if ((Boolean) this.context.getParameter(Parameter.ANTIALIAS)) {
181             hints.add(new RenderingHints(RenderingHints.KEY_ANTIALIASING,
182                     RenderingHints.VALUE_ANTIALIAS_ON));
183         }
184         hints.add(new RenderingHints(RenderingHints.KEY_STROKE_CONTROL,
185                 RenderingHints.VALUE_STROKE_NORMALIZE));
186         hints.add(new RenderingHints(RenderingHints.KEY_RENDERING,
187                 RenderingHints.VALUE_RENDER_QUALITY));
188         g.setRenderingHints(hints);
189 
190         final boolean debug = (Boolean) this.context
191                 .getParameter(Parameter.DEBUG);
192         this.drawNode(this.document, g, x, y, debug);
193 
194     }
195 
196     private void drawNode(final LayoutableNode node, final Graphics2D g,
197             final float x, final float y, final boolean debug) {
198 
199         final LayoutInfo myInfo = this.getInfo(node);
200         if (debug) {
201             final float x1 = x;
202             final float x2 = x + myInfo.getWidth(LayoutStage.STAGE2);
203             final float y1 = y - myInfo.getAscentHeight(LayoutStage.STAGE2);
204             final float y2 = y + myInfo.getDescentHeight(LayoutStage.STAGE2);
205             g.setColor(Color.BLUE);
206             g.draw(new Line2D.Float(x1, y1, x2, y1));
207             g.draw(new Line2D.Float(x1, y1, x1, y2));
208             g.draw(new Line2D.Float(x2, y1, x2, y2));
209             g.draw(new Line2D.Float(x1, y2, x2, y2));
210             g.setColor(Color.RED);
211             g.draw(new Line2D.Float(x1, y, x2, y));
212         }
213         for (final GraphicsObject go : myInfo.getGraphicObjects()) {
214             go.paint(x, y, g);
215         }
216 
217         for (final LayoutableNode child : node.getChildrenToDraw()) {
218             final LayoutInfo childInfo = this.getInfo(child);
219             this.drawNode(child, g,
220                     x + childInfo.getPosX(LayoutStage.STAGE2), y
221                             + childInfo.getPosY(LayoutStage.STAGE2), debug);
222         }
223     }
224 
225     private LayoutInfo layout() {
226         return this.layout(this.document, LayoutStage.STAGE2, this.context);
227     }
228 
229     private LayoutInfo layout(final LayoutableNode node,
230             final LayoutStage toStage, final LayoutContext parentContext) {
231         final LayoutInfo info = this.getInfo(node);
232 
233         if (node instanceof EventTarget) {
234             final EventTarget evtNode = (EventTarget) node;
235             evtNode.addEventListener("DOMSubtreeModified", this, false);
236             evtNode.addEventListener(Mo.MOEVENT, this, false);
237         }
238 
239         if (LayoutStage.NONE.equals(info.getLayoutStage())) {
240             LayoutStage childMinStage = LayoutStage.STAGE2;
241             int count = 0;
242             for (final LayoutableNode l : node.getChildrenToLayout()) {
243                 final LayoutInfo in = this.layout(l, LayoutStage.STAGE1, node
244                         .getChildLayoutContext(count, parentContext));
245                 count++;
246                 if (LayoutStage.STAGE1.equals(in.getLayoutStage())) {
247                     childMinStage = LayoutStage.STAGE1;
248                 }
249             }
250             node.layoutStage1(this, info, childMinStage, parentContext);
251         }
252         if (LayoutStage.STAGE1.equals(info.getLayoutStage())
253                 && LayoutStage.STAGE2.equals(toStage)) {
254             int count = 0;
255             for (final LayoutableNode l : node.getChildrenToLayout()) {
256                 this.layout(l, LayoutStage.STAGE2, node
257                         .getChildLayoutContext(count, parentContext));
258                 count++;
259             }
260             node.layoutStage2(this, info, parentContext);
261         }
262         return info;
263     }
264 
265     /** {@inheritDoc} */
266     public LayoutInfo getInfo(final LayoutableNode node) {
267         if (node == null) {
268             return null;
269         }
270         LayoutInfo info = this.layoutMap.get(node);
271         if (info == null) {
272             info = new LayoutInfoImpl();
273             this.layoutMap.put(node, info);
274         }
275         return info;
276     }
277 
278     /**
279      * @return width of this view.
280      */
281     public float getWidth() {
282         final LayoutInfo info = this.layout();
283         return info.getWidth(LayoutStage.STAGE2);
284     }
285 
286     /**
287      * @return ascent height.
288      */
289     public float getAscentHeight() {
290         final LayoutInfo info = this.layout();
291         return info.getAscentHeight(LayoutStage.STAGE2);
292     }
293 
294     /**
295      * @return descent height.
296      */
297     public float getDescentHeight() {
298         final LayoutInfo info = this.layout();
299         return info.getDescentHeight(LayoutStage.STAGE2);
300     }
301 
302     /** {@inheritDoc} */
303     public Graphics2D getGraphics() {
304         return this.graphics;
305     }
306 
307     /** {@inheritDoc} */
308     public void handleEvent(final Event evt) {
309         final EventTarget origin = evt.getCurrentTarget();
310         if (origin instanceof LayoutableNode) {
311             final LayoutableNode lorigin = (LayoutableNode) origin;
312             final LayoutInfo info = this.getInfo(lorigin);
313             info.setLayoutStage(LayoutStage.NONE);
314         }
315     }
316 
317     /**
318      * Data structure for storing a {@link Node} along with its rendering
319      * boundary ({@link Rectangle2D}).
320      */
321     public static final class NodeRect {
322         private final Node node;
323 
324         private final Rectangle2D rect;
325 
326         private NodeRect(final Node n, final Rectangle2D r) {
327             this.node = n;
328             this.rect = r;
329         }
330 
331         /**
332          * @return The Node this rectangle refers to.
333          */
334         public Node getNode() {
335             return this.node;
336         }
337 
338         /**
339          * @return The rendering boundary.
340          */
341         public Rectangle2D getRect() {
342             return this.rect;
343         }
344 
345         /** {@inheritDoc} */
346         @Override
347         public String toString() {
348             final StringBuilder b = new StringBuilder();
349             b.append(this.node).append('/').append(this.rect);
350             return b.toString();
351         }
352 
353     }
354 
355     /**
356      * Get the node and rendering information from a mouse position.
357      * 
358      * @param x
359      *            x-coord
360      * @param y
361      *            y-coord
362      * @param offsetX
363      *            starting x position offset
364      * @param offsetY
365      *            starting y position offset
366      * @return list of nodes with rendering information
367      */
368     public List<JEuclidView.NodeRect> getNodesAt(final float x,
369             final float y, final float offsetX, final float offsetY) {
370         this.layout();
371         final List<JEuclidView.NodeRect> nodes = new LinkedList<JEuclidView.NodeRect>();
372         this.getNodesAtRec(x, y, offsetX, offsetY, this.document, nodes);
373         return nodes;
374     }
375 
376     /**
377      * Check whether the given mouse position (with given offset) is in the
378      * rendering area of the given node - if so, append it to the nodes list
379      * 
380      * @param x
381      *            x-coord
382      * @param y
383      *            y-coord
384      * @param offsetX
385      *            x position offset to node
386      * @param offsetY
387      *            y position offset to node
388      * @param node
389      *            node to check
390      * @param nodesSoFar
391      *            vector of nodes so far
392      */
393     private void getNodesAtRec(final float x, final float y,
394             final float offsetX, final float offsetY, final Node node,
395             final List<JEuclidView.NodeRect> nodesSoFar) {
396         if (node instanceof LayoutableNode) {
397             final LayoutInfo info = this.layoutMap.get(node);
398 
399             // this will be STAGE2
400             final LayoutStage stage = info.getLayoutStage();
401 
402             // find top-left corner of rendering area for this node
403             final float infoX = info.getPosX(stage) + offsetX;
404             final float infoY = info.getPosY(stage) + offsetY
405                     - info.getAscentHeight(stage);
406 
407             // create rectangle of rendered node area
408             final Rectangle2D.Float rect = new Rectangle.Float(infoX, infoY,
409                     info.getWidth(stage), info.getAscentHeight(stage)
410                             + info.getDescentHeight(stage));
411 
412             // record node and rectangle if it contains the mouse position
413             if (rect.contains(x, y)) {
414                 nodesSoFar.add(new NodeRect(node, rect));
415 
416                 // recurse on child nodes
417                 final NodeList nodeList = node.getChildNodes();
418                 for (int i = 0; i < nodeList.getLength(); i++) {
419                     this.getNodesAtRec(x, y, infoX, infoY
420                             + info.getAscentHeight(stage), nodeList.item(i),
421                             nodesSoFar);
422                 }
423             }
424         }
425     }
426 
427     /**
428      * Gets the absolute Bounds for a given node and offset. May return null
429      * if the node could not be found.
430      * 
431      * @param offsetX
432      *            x position offset to node
433      * @param offsetY
434      *            y position offset to node
435      * 
436      * @param node
437      *            A layoutable node which was layouted in the current view.
438      * @return the rectangle with the absolute bounds or null if the given
439      *         node was not layouted in this view.
440      * 
441      */
442     public Rectangle2D getRect(final float offsetX, final float offsetY,
443             final LayoutableNode node) {
444         this.layout();
445         final LayoutInfo info = this.layoutMap.get(node);
446         final Rectangle2D retVal;
447         if (info == null) {
448             retVal = null;
449         } else {
450             LayoutableNode recNode = node;
451             float recInfoX = info.getPosX(LayoutStage.STAGE2) + offsetX;
452             float recInfoY = info.getPosY(LayoutStage.STAGE2) + offsetY
453                     - info.getAscentHeight(LayoutStage.STAGE2);
454             while (recNode.getParentNode() instanceof LayoutableNode) {
455                 recNode = (LayoutableNode) recNode.getParentNode();
456                 final LayoutInfo recInfo = this.layoutMap.get(recNode);
457                 recInfoX = recInfoX + recInfo.getPosX(LayoutStage.STAGE2);
458                 recInfoY = recInfoY + recInfo.getPosY(LayoutStage.STAGE2);
459             }
460             retVal = new Rectangle.Float(recInfoX, recInfoY, info
461                     .getWidth(LayoutStage.STAGE2), info
462                     .getAscentHeight(LayoutStage.STAGE2)
463                     + info.getDescentHeight(LayoutStage.STAGE2));
464         }
465         return retVal;
466     }
467 
468 }