/* * @(#)ImageMapOutputFormat.java * * Copyright (c) 2007-2010 The authors and contributors of JHotDraw. * You may not use, copy or modify this file, except in compliance with the * accompanying license terms. */ package org.jhotdraw.samples.svg.io; import org.jhotdraw.geom.GrowStroke; import org.jhotdraw.gui.filechooser.ExtensionFileFilter; import org.jhotdraw.draw.io.OutputFormat; import java.awt.*; import java.awt.datatransfer.*; import java.awt.geom.*; import java.io.*; import java.net.URI; import javax.swing.*; import net.n3.nanoxml.*; import org.jhotdraw.draw.*; import org.jhotdraw.gui.datatransfer.*; import static org.jhotdraw.samples.svg.SVGAttributeKeys.*; import org.jhotdraw.samples.svg.figures.SVGBezierFigure; import org.jhotdraw.samples.svg.figures.SVGEllipseFigure; import org.jhotdraw.samples.svg.figures.SVGFigure; import org.jhotdraw.samples.svg.figures.SVGGroupFigure; import org.jhotdraw.samples.svg.figures.SVGImageFigure; import org.jhotdraw.samples.svg.figures.SVGPathFigure; import org.jhotdraw.samples.svg.figures.SVGRectFigure; import org.jhotdraw.samples.svg.figures.SVGTextAreaFigure; import org.jhotdraw.samples.svg.figures.SVGTextFigure; import org.jhotdraw.util.*; /** * ImageMapOutputFormat exports a SVG drawing as an HTML 4.01 <code>MAP</code> * element. * For more information see: * http://www.w3.org/TR/html401/struct/objects.html#h-13.6.2 * * * @author Werner Randelshofer * @version $Id$ */ public class ImageMapOutputFormat implements OutputFormat { /** * The affine transformation for the output. This is used * to create scaled image maps. */ private AffineTransform drawingTransform = new AffineTransform(); private static boolean DEBUG = true; /** * Set this to true, if AREA elements with <code>nohref="true"</code> * shall e included in the image map. */ private boolean isIncludeNohref = false; /** * Image dimension. We only include AREA elements which are within the * image dimension. */ private Rectangle bounds = new Rectangle(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE); /** Creates a new instance. */ public ImageMapOutputFormat() { } @Override public javax.swing.filechooser.FileFilter getFileFilter() { return new ExtensionFileFilter("HTML Image Map", "html"); } @Override public String getFileExtension() { return "html"; } @Override public JComponent getOutputFormatAccessory() { return null; } @Override public void write(URI uri, Drawing drawing) throws IOException { write(new File(uri),drawing); } public void write(File file, Drawing drawing) throws IOException { BufferedOutputStream out = new BufferedOutputStream( new FileOutputStream(file)); try { write(out, drawing); } finally { out.close(); } } @Override public void write(OutputStream out, Drawing drawing) throws IOException { write(out, drawing.getChildren()); } /** * Writes the drawing to the specified output stream. * This method applies the specified drawingTransform to the drawing, and draws * it on an image of the specified getChildCount. */ public void write(OutputStream out, Drawing drawing, AffineTransform drawingTransform, Dimension imageSize) throws IOException { write(out, drawing.getChildren(), drawingTransform, imageSize); } /** * Writes the figures to the specified output stream. * This method applies the specified drawingTransform to the drawing, and draws * it on an image of the specified getChildCount. * * All other write methods delegate their work to here. */ public void write(OutputStream out, java.util.List<Figure> figures, AffineTransform drawingTransform, Dimension imageSize) throws IOException { this.drawingTransform = (drawingTransform == null) ? new AffineTransform() : drawingTransform; this.bounds = (imageSize == null) ? new Rectangle(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE) : new Rectangle(0, 0, imageSize.width, imageSize.height); XMLElement document = new XMLElement("map"); // Note: Image map elements need to be written from front to back for (Figure f : new ReversedList<Figure>(figures)) { writeElement(document, f); } // Strip AREA elements with "nohref" attributes from the end of the // map if (!isIncludeNohref) { for (int i = document.getChildrenCount() - 1; i >= 0; i--) { XMLElement child = (XMLElement) document.getChildAtIndex(i); if (child.hasAttribute("nohref")) { document.removeChildAtIndex(i); } } } // Write XML content PrintWriter writer = new PrintWriter( new OutputStreamWriter(out, "UTF-8")); //new XMLWriter(writer).write(document); for (Object o : document.getChildren()) { XMLElement child = (XMLElement) o; new XMLWriter(writer).write(child); } // Flush writer writer.flush(); } /** * All other write methods delegate their work to here. */ public void write(OutputStream out, java.util.List<Figure> figures) throws IOException { Rectangle2D.Double drawingRect = null; for (Figure f : figures) { if (drawingRect == null) { drawingRect = f.getBounds(); } else { drawingRect.add(f.getBounds()); } } AffineTransform tx = new AffineTransform(); tx.translate( -Math.min(0, drawingRect.x), -Math.min(0, drawingRect.y)); write(out, figures, tx, new Dimension( (int) (Math.abs(drawingRect.x) + drawingRect.width), (int) (Math.abs(drawingRect.y) + drawingRect.height))); } @Override public Transferable createTransferable(Drawing drawing, java.util.List<Figure> figures, double scaleFactor) throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); write(buf, figures); return new InputStreamTransferable(new DataFlavor("text/html", "HTML Image Map"), buf.toByteArray()); } protected void writeElement(IXMLElement parent, Figure f) throws IOException { if (f instanceof SVGEllipseFigure) { writeEllipseElement(parent, (SVGEllipseFigure) f); } else if (f instanceof SVGGroupFigure) { writeGElement(parent, (SVGGroupFigure) f); } else if (f instanceof SVGImageFigure) { writeImageElement(parent, (SVGImageFigure) f); } else if (f instanceof SVGPathFigure) { SVGPathFigure path = (SVGPathFigure) f; if (path.getChildCount() == 1) { BezierFigure bezier = (BezierFigure) path.getChild(0); boolean isLinear = true; for (int i = 0, n = bezier.getNodeCount(); i < n; i++) { if (bezier.getNode(i).getMask() != 0) { isLinear = false; break; } } if (isLinear) { if (bezier.isClosed()) { writePolygonElement(parent, path); } else { if (bezier.getNodeCount() == 2) { writeLineElement(parent, path); } else { writePolylineElement(parent, path); } } } else { writePathElement(parent, path); } } else { writePathElement(parent, path); } } else if (f instanceof SVGRectFigure) { writeRectElement(parent, (SVGRectFigure) f); } else if (f instanceof SVGTextFigure) { writeTextElement(parent, (SVGTextFigure) f); } else if (f instanceof SVGTextAreaFigure) { writeTextAreaElement(parent, (SVGTextAreaFigure) f); } else { System.out.println("Unable to write: " + f); } } /** * Writes the <code>shape</code>, <code>coords</code>, <code>href</code>, * <code>nohref</code> Attribute for the specified figure and ellipse. * * @return Returns true, if the circle is inside of the image bounds. */ private boolean writeCircleAttributes(IXMLElement elem, SVGFigure f, Ellipse2D.Double ellipse) { AffineTransform t = TRANSFORM.getClone(f); if (t == null) { t = drawingTransform; } else { t.preConcatenate(drawingTransform); } if ((t.getType() & (AffineTransform.TYPE_UNIFORM_SCALE | AffineTransform.TYPE_TRANSLATION)) == t.getType() && ellipse.width == ellipse.height) { Point2D.Double start = new Point2D.Double(ellipse.x, ellipse.y); Point2D.Double end = new Point2D.Double(ellipse.x + ellipse.width, ellipse.y + ellipse.height); t.transform(start, start); t.transform(end, end); ellipse.x = Math.min(start.x, end.x); ellipse.y = Math.min(start.y, end.y); ellipse.width = Math.abs(start.x - end.x); ellipse.height = Math.abs(start.y - end.y); elem.setAttribute("shape", "circle"); elem.setAttribute("coords", (int) (ellipse.x + ellipse.width / 2d) + "," + (int) (ellipse.y + ellipse.height / 2d) + "," + (int) (ellipse.width / 2d)); writeHrefAttribute(elem, f); return bounds.intersects(ellipse.getBounds()); } else { return writePolyAttributes(elem, f, (Shape) ellipse); } } /** * Writes the <code>shape</code>, <code>coords</code>, <code>href</code>, * <code>nohref</code> Attribute for the specified figure and rectangle. * * @return Returns true, if the rect is inside of the image bounds. */ private boolean writeRectAttributes(IXMLElement elem, SVGFigure f, Rectangle2D.Double rect) { AffineTransform t = TRANSFORM.getClone(f); if (t == null) { t = drawingTransform; } else { t.preConcatenate(drawingTransform); } if ((t.getType() & (AffineTransform.TYPE_UNIFORM_SCALE | AffineTransform.TYPE_TRANSLATION)) == t.getType()) { Point2D.Double start = new Point2D.Double(rect.x, rect.y); Point2D.Double end = new Point2D.Double(rect.x + rect.width, rect.y + rect.height); t.transform(start, start); t.transform(end, end); Rectangle r = new Rectangle( (int) Math.min(start.x, end.x), (int) Math.min(start.y, end.y), (int) Math.abs(start.x - end.x), (int) Math.abs(start.y - end.y)); elem.setAttribute("shape", "rect"); elem.setAttribute("coords", r.x + "," + r.y + "," + (r.x + r.width) + "," + (r.y + r.height)); writeHrefAttribute(elem, f); return bounds.intersects(r); } else { return writePolyAttributes(elem, f, (Shape) rect); } } private void writeHrefAttribute(IXMLElement elem, SVGFigure f) { String link = f.get(LINK); if (link != null && link.trim().length() > 0) { elem.setAttribute("href", link); elem.setAttribute("title", link); elem.setAttribute("alt", link); String linkTarget = f.get(LINK_TARGET); if (linkTarget != null && linkTarget.trim().length() > 0) { elem.setAttribute("target", linkTarget); } } else { elem.setAttribute("nohref", "true"); } } /** * Writes the <code>shape</code>, <code>coords</code>, <code>href</code>, * <code>nohref</code> Attribute for the specified figure and shape. * * @return Returns true, if the polygon is inside of the image bounds. */ private boolean writePolyAttributes(IXMLElement elem, SVGFigure f, Shape shape) { AffineTransform t = TRANSFORM.getClone(f); if (t == null) { t = drawingTransform; } else { t.preConcatenate(drawingTransform); } StringBuilder buf = new StringBuilder(); float[] coords = new float[6]; Path2D.Double path = new Path2D.Double(); for (PathIterator i = shape.getPathIterator(t, 1.5f); !i.isDone(); i.next()) { switch (i.currentSegment(coords)) { case PathIterator.SEG_MOVETO: if (buf.length() != 0) { throw new IllegalArgumentException("Illegal shape " + shape); } if (buf.length() != 0) { buf.append(','); } buf.append((int) coords[0]); buf.append(','); buf.append((int) coords[1]); path.moveTo(coords[0], coords[1]); break; case PathIterator.SEG_LINETO: if (buf.length() != 0) { buf.append(','); } buf.append((int) coords[0]); buf.append(','); buf.append((int) coords[1]); path.lineTo(coords[0], coords[1]); break; case PathIterator.SEG_CLOSE: path.closePath(); break; default: throw new InternalError("Illegal segment type " + i.currentSegment(coords)); } } elem.setAttribute("shape", "poly"); elem.setAttribute("coords", buf.toString()); writeHrefAttribute(elem, f); return path.intersects(new Rectangle2D.Float(bounds.x, bounds.y, bounds.width, bounds.height)); } private void writePathElement(IXMLElement parent, SVGPathFigure f) throws IOException { GrowStroke growStroke = new GrowStroke( (getStrokeTotalWidth(f, 1.0) / 2d), getStrokeTotalWidth(f, 1.0)); BasicStroke basicStroke = new BasicStroke((float) getStrokeTotalWidth(f, 1.0)); for (Figure child : f.getChildren()) { SVGBezierFigure bezier = (SVGBezierFigure) child; IXMLElement elem = parent.createElement("area"); if (bezier.isClosed()) { writePolyAttributes(elem, f, growStroke.createStrokedShape(bezier.getBezierPath())); } else { writePolyAttributes(elem, f, basicStroke.createStrokedShape(bezier.getBezierPath())); } parent.addChild(elem); } } private void writePolygonElement(IXMLElement parent, SVGPathFigure f) throws IOException { IXMLElement elem = parent.createElement("area"); if (writePolyAttributes(elem, f, new GrowStroke( (getStrokeTotalWidth(f, 1.0) / 2d), getStrokeTotalWidth(f, 1.0)).createStrokedShape(f.getChild(0).getBezierPath()))) { parent.addChild(elem); } } private void writePolylineElement(IXMLElement parent, SVGPathFigure f) throws IOException { IXMLElement elem = parent.createElement("area"); if (writePolyAttributes(elem, f, new BasicStroke((float) getStrokeTotalWidth(f, 1.0)).createStrokedShape(f.getChild(0).getBezierPath()))) { parent.addChild(elem); } } private void writeLineElement(IXMLElement parent, SVGPathFigure f) throws IOException { IXMLElement elem = parent.createElement("area"); if (writePolyAttributes(elem, f, new GrowStroke( (getStrokeTotalWidth(f, 1.0) / 2d), getStrokeTotalWidth(f, 1.0)).createStrokedShape(new Line2D.Double( f.getStartPoint(), f.getEndPoint())))) { parent.addChild(elem); } } private void writeRectElement(IXMLElement parent, SVGRectFigure f) throws IOException { IXMLElement elem = parent.createElement("AREA"); boolean isContained; if (f.getArcHeight() == 0 && f.getArcWidth() == 0) { Rectangle2D.Double rect = f.getBounds(); double grow = getPerpendicularHitGrowth(f, 1.0); rect.x -= grow; rect.y -= grow; rect.width += grow; rect.height += grow; isContained = writeRectAttributes(elem, f, rect); } else { isContained = writePolyAttributes(elem, f, new GrowStroke( (getStrokeTotalWidth(f, 1.0) / 2d), getStrokeTotalWidth(f, 1.0)).createStrokedShape(new RoundRectangle2D.Double( f.getX(), f.getY(), f.getWidth(), f.getHeight(), f.getArcWidth(), f.getArcHeight()))); } if (isContained) { parent.addChild(elem); } } private void writeTextElement(IXMLElement parent, SVGTextFigure f) throws IOException { IXMLElement elem = parent.createElement("AREA"); Rectangle2D.Double rect = f.getBounds(); double grow = getPerpendicularHitGrowth(f, 1.0); rect.x -= grow; rect.y -= grow; rect.width += grow; rect.height += grow; if (writeRectAttributes(elem, f, rect)) { parent.addChild(elem); } } private void writeTextAreaElement(IXMLElement parent, SVGTextAreaFigure f) throws IOException { IXMLElement elem = parent.createElement("AREA"); Rectangle2D.Double rect = f.getBounds(); double grow = getPerpendicularHitGrowth(f, 1.0); rect.x -= grow; rect.y -= grow; rect.width += grow; rect.height += grow; if (writeRectAttributes(elem, f, rect)) { parent.addChild(elem); } } private void writeEllipseElement(IXMLElement parent, SVGEllipseFigure f) throws IOException { IXMLElement elem = parent.createElement("area"); Rectangle2D.Double r = f.getBounds(); double grow = getPerpendicularHitGrowth(f, 1.0); Ellipse2D.Double ellipse = new Ellipse2D.Double(r.x - grow, r.y - grow, r.width + grow, r.height + grow); if (writeCircleAttributes(elem, f, ellipse)) { parent.addChild(elem); } } private void writeGElement(IXMLElement parent, SVGGroupFigure f) throws IOException { // Note: Image map elements need to be written from front to back for (Figure child : new ReversedList<Figure>(f.getChildren())) { writeElement(parent, child); } } private void writeImageElement(IXMLElement parent, SVGImageFigure f) { IXMLElement elem = parent.createElement("area"); Rectangle2D.Double rect = f.getBounds(); writeRectAttributes(elem, f, rect); parent.addChild(elem); } }