/*
* {{{ header & license
* Copyright (c) 2008 elbart0 at free.fr (submitted via email)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2.1
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
* }}}
*/
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xhtmlrenderer.extend.FSImage;
import org.xhtmlrenderer.extend.ReplacedElement;
import org.xhtmlrenderer.extend.UserAgentCallback;
import org.xhtmlrenderer.extend.FSCanvas;
import org.xhtmlrenderer.layout.LayoutContext;
import org.xhtmlrenderer.render.BlockBox;
import org.xhtmlrenderer.swing.AWTFSImage;
import org.xhtmlrenderer.swing.SwingReplacedElement;
import org.xhtmlrenderer.swing.SwingReplacedElementFactory;
import org.xhtmlrenderer.util.XRLog;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
import java.util.*;
import java.util.logging.Level;
/**
* Sample for handling image maps in XHTML, as replaced elements.
*
* Sample is incomplete in current state and meant as a starting point for future work.
*/
public class ImageMapReplacedElementFactory extends SwingReplacedElementFactory {
private final ImageMapListener listener;
private static final String IMG_USEMAP_ATTR = "usemap";
private static final String MAP_ELT = "map";
private static final String MAP_NAME_ATTR = "name";
private static final String AREA_ELT = "area";
private static final String AREA_SHAPE_ATTR = "shape";
private static final String AREA_COORDS_ATTR = "coords";
private static final String AREA_HREF_ATTR = "href";
private static final String RECT_SHAPE = "rect";
private static final String RECTANGLE_SHAPE = "rectangle";
private static final String CIRC_SHAPE = "circ";
private static final String CIRCLE_SHAPE = "circle";
private static final String POLY_SHAPE = "poly";
private static final String POLYGON_SHAPE = "polygon";
public ImageMapReplacedElementFactory(ImageMapListener listener) {
super(null);
if (null == listener) {
throw new IllegalArgumentException("listener required");
}
this.listener = listener;
}
public ReplacedElement createReplacedElement(LayoutContext context, BlockBox box, UserAgentCallback uac, int cssWidth, int cssHeight) {
Element e = box.getElement();
if (e == null) {
return null;
} else if (context.getNamespaceHandler().isImageElement(e)) {
final String usemapAttr = context.getNamespaceHandler().getAttributeValue(e, IMG_USEMAP_ATTR);
if (isNotBlank(usemapAttr)) {
final ReplacedElement re = replaceImageMap(uac, context, e, usemapAttr, cssWidth, cssHeight);
if (context.isInteractive() && re instanceof SwingReplacedElement) {
FSCanvas canvas = context.getCanvas();
if (canvas instanceof JComponent) {
((JComponent) canvas).add(((SwingReplacedElement) re).getJComponent());
}
}
return re;
} else {
return replaceImage(uac, context, e, cssWidth, cssHeight);
}
} else {
return null;
}
}
private boolean isNotBlank(String _v) {
if (_v == null || _v.length() == 0) {
return false;
}
for (int i = 0; i < _v.length(); i++) {
if (Character.isWhitespace(_v.charAt(i))) continue;
return false;
}
return true;
}
// See SwingReplacedElementFactory#replaceImage
protected ReplacedElement replaceImageMap(UserAgentCallback uac, LayoutContext context, Element elem, String usemapAttr, int cssWidth, int cssHeight) {
ReplacedElement re;
// lookup in cache, or instantiate
re = lookupImageReplacedElement(elem, "");
if (re == null) {
Image im = null;
String imageSrc = context.getNamespaceHandler().getImageSourceURI(elem);
if (imageSrc == null || imageSrc.length() == 0) {
XRLog.layout(Level.WARNING, "No source provided for img element.");
re = newIrreplaceableImageElement(cssWidth, cssHeight);
} else {
FSImage fsImage = uac.getImageResource(imageSrc).getImage();
if (fsImage != null) {
im = ((AWTFSImage) fsImage).getImage();
}
if (im != null) {
final String mapName = usemapAttr.substring(1);
Node map = elem.getOwnerDocument().getElementById(mapName);
if (null == map) {
final NodeList maps = elem.getOwnerDocument().getElementsByTagName(MAP_ELT);
for (int i = 0; i < maps.getLength(); i++) {
String mapAttr = ImageMapReplacedElement.getAttribute(maps.item(i).getAttributes(), MAP_NAME_ATTR);
if (areEqual(mapName, mapAttr)) {
map = maps.item(i);
break;
}
}
if (null == map) {
XRLog.layout(Level.INFO, "No map named: '" + mapName + "'");
}
}
re = new ImageMapReplacedElement(im, map, cssWidth, cssHeight, listener);
} else {
re = newIrreplaceableImageElement(cssWidth, cssHeight);
}
}
storeImageReplacedElement(elem, re, "", -1, -1);
}
return re;
}
private static boolean areEqual(String str1, String str2) {
return (str1 == null && str2 == null) || (str1 != null && str1.equals(str2));
}
private static boolean areEqualIgnoreCase(String str1, String str2) {
return (str1 == null && str2 == null) || (str1 != null && str1.equalsIgnoreCase(str2));
}
private static class ImageMapReplacedElement extends SwingReplacedElement {
private final Map areas;
public ImageMapReplacedElement(Image image, Node map, int targetWidth, int targetHeight, final ImageMapListener listener) {
super(create(image, targetWidth, targetHeight));
areas = parseMap(map);
getJComponent().addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
final Point point = e.getPoint();
final Set set = areas.entrySet();
for (Iterator iterator = set.iterator(); iterator.hasNext();) {
Map.Entry entry = (Map.Entry) iterator.next();
if (((Shape) entry.getKey()).contains(point)) {
listener.areaClicked(new ImageMapEvent(this, (String) entry.getValue()));
}
}
}
public void mouseExited(MouseEvent e) {
getJComponent().setCursor(Cursor.getDefaultCursor());
}
});
getJComponent().addMouseMotionListener(new MouseMotionAdapter() {
public void mouseMoved(MouseEvent e) {
final JComponent c = getJComponent();
final Point point = e.getPoint();
final Set set = areas.entrySet();
for (Iterator iterator = set.iterator(); iterator.hasNext();) {
Map.Entry entry = (Map.Entry) iterator.next();
if (((Shape) entry.getKey()).contains(point)) {
updateCursor(c, Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
return;
}
}
updateCursor(c, Cursor.getDefaultCursor());
}
});
}
private static void updateCursor(JComponent c, Cursor cursor) {
if (!c.getCursor().equals(cursor)) {
c.setCursor(cursor);
}
}
private static Map parseMap(Node map) {
if (null == map) {
return Collections.emptyMap();
} else if (map.hasChildNodes()) {
final NodeList children = map.getChildNodes();
final Map areas = new HashMap(children.getLength());
for (int i = 0; i < children.getLength(); i++) {
final Node area = children.item(i);
if (areEqualIgnoreCase(AREA_ELT, area.getNodeName())) {
if (area.hasAttributes()) {
final NamedNodeMap attrs = area.getAttributes();
final String shapeAttr = getAttribute(attrs, AREA_SHAPE_ATTR);
final String[] coords = getAttribute(attrs, AREA_COORDS_ATTR).split(",");
final String href = getAttribute(attrs, AREA_HREF_ATTR);
if (areEqualIgnoreCase(RECT_SHAPE, shapeAttr) || areEqualIgnoreCase(RECTANGLE_SHAPE, shapeAttr)) {
final Shape shape = getCoords(coords, 4);
if (null != shape) {
areas.put(shape, href);
}
} else if (areEqualIgnoreCase(CIRC_SHAPE, shapeAttr) || areEqualIgnoreCase(CIRCLE_SHAPE, shapeAttr)) {
final Shape shape = getCoords(coords, 3);
if (null != shape) {
areas.put(shape, href);
}
} else if (areEqualIgnoreCase(POLY_SHAPE, shapeAttr) || areEqualIgnoreCase(POLYGON_SHAPE, shapeAttr)) {
final Shape shape = getCoords(coords, -1);
if (null != shape) {
areas.put(shape, href);
}
} else {
if (XRLog.isLoggingEnabled()) {
XRLog.layout(Level.INFO, "Unsupported shape: '" + shapeAttr + "'");
}
}
}
}
}
return areas;
} else {
return Collections.emptyMap();
}
}
private static String getAttribute(NamedNodeMap attrs, String attrName) {
final Node node = attrs.getNamedItem(attrName);
return null == node ? null : node.getNodeValue();
}
private static Shape getCoords(String[] coordValues, int length) {
if ((-1 == length && 0 == coordValues.length % 2) || length == coordValues.length) {
int[] coords = new int[coordValues.length];
int i = 0;
for (int i1 = 0; i1 < coordValues.length; i1++) {
String coord = coordValues[i1];
try {
coords[i++] = Integer.parseInt(coord.trim());
} catch (NumberFormatException e) {
XRLog.layout(Level.WARNING, "Error while parsing shape coords", e);
return null;
}
}
if (4 == length) {
return new Rectangle2D.Float(coords[0], coords[1], coords[2] - coords[0], coords[3] - coords[1]);
} else if (3 == length) {
final int radius = coords[2];
return new Ellipse2D.Float(coords[0] - radius, coords[1] - radius, radius * 2, radius * 2);
} else if (-1 == length) {
final int npoints = coords.length / 2;
final int[] xpoints = new int[npoints];
final int[] ypoints = new int[npoints];
for (int c = 0, p = 0; p < npoints; p++) {
xpoints[p] = coords[c++];
ypoints[p] = coords[c++];
}
return new Polygon(xpoints, ypoints, npoints);
} else {
XRLog.layout(Level.INFO, "Unsupported shape: '" + length + "'");
return null;
}
} else {
return null;
}
}
private static JComponent create(Image image, int targetWidth, int targetHeight) {
final JLabel component = new JLabel(new ImageIcon(image));
component.setSize(component.getPreferredSize());
return component;
}
}
}