001    /*
002     * Copyright 2002 - 2007 JEuclid, http://jeuclid.sf.net
003     *
004     * Licensed under the Apache License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     *      http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    
017    /* $Id: JEuclidView.java,v 371548310efa 2010/08/09 21:15:33 max $ */
018    
019    package net.sourceforge.jeuclid.layout;
020    
021    import java.awt.Color;
022    import java.awt.Graphics2D;
023    import java.awt.Image;
024    import java.awt.Rectangle;
025    import java.awt.RenderingHints;
026    import java.awt.geom.Line2D;
027    import java.awt.geom.Rectangle2D;
028    import java.awt.image.BufferedImage;
029    import java.util.ArrayList;
030    import java.util.HashMap;
031    import java.util.LinkedList;
032    import java.util.List;
033    import java.util.Map;
034    
035    import net.sourceforge.jeuclid.DOMBuilder;
036    import net.sourceforge.jeuclid.LayoutContext;
037    import net.sourceforge.jeuclid.context.Parameter;
038    import net.sourceforge.jeuclid.elements.generic.DocumentElement;
039    import net.sourceforge.jeuclid.elements.presentation.token.Mo;
040    
041    import org.apache.commons.logging.Log;
042    import org.apache.commons.logging.LogFactory;
043    import org.w3c.dom.Node;
044    import org.w3c.dom.NodeList;
045    import org.w3c.dom.events.Event;
046    import org.w3c.dom.events.EventListener;
047    import org.w3c.dom.events.EventTarget;
048    import org.w3c.dom.views.AbstractView;
049    import org.w3c.dom.views.DocumentView;
050    
051    /**
052     * @version $Revision: 371548310efa $
053     */
054    public class JEuclidView implements AbstractView, LayoutView, EventListener {
055    
056        private static final Log LOGGER = LogFactory.getLog(JEuclidView.class);
057    
058        private final LayoutableDocument document;
059    
060        private final Map<Node, LayoutInfo> layoutMap;
061    
062        private final LayoutContext context;
063    
064        private final Graphics2D graphics;
065    
066        /**
067         * Default Constructor.
068         * 
069         * @param node
070         *            document to layout.
071         * @param layoutGraphics
072         *            Graphics context to use for layout calculations. This should
073         *            be compatible to the context used for painting, but does not
074         *            have to be the same. If it is null, a default Graphics context
075         *            is created.
076         * @param layoutContext
077         *            layoutContext to use.
078         */
079        public JEuclidView(final Node node, final LayoutContext layoutContext,
080                final Graphics2D layoutGraphics) {
081            assert node != null : "Node must not be null";
082            assert layoutContext != null : "LayoutContext must not be null";
083            if (node instanceof LayoutableDocument) {
084                this.document = (LayoutableDocument) node;
085            } else {
086                this.document = DOMBuilder.getInstance().createJeuclidDom(node,
087                        true, true);
088            }
089            if (layoutGraphics == null) {
090                final Image tempimage = new BufferedImage(1, 1,
091                        BufferedImage.TYPE_INT_ARGB);
092                this.graphics = (Graphics2D) tempimage.getGraphics();
093            } else {
094                this.graphics = layoutGraphics;
095            }
096            this.context = layoutContext;
097            this.layoutMap = new HashMap<Node, LayoutInfo>();
098        }
099    
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    }