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 }