/*
* This is part of Geomajas, a GIS framework, http://www.geomajas.org/.
*
* Copyright 2008-2015 Geosparc nv, http://www.geosparc.com/, Belgium.
*
* The program is available in open source according to the GNU Affero
* General Public License. All contributions in this program are covered
* by the Geomajas Contributors License Agreement. For full licensing
* details, see LICENSE.txt in the project root.
*/
package org.geomajas.gwt.client.gfx.context;
import org.geomajas.configuration.SymbolInfo;
import org.geomajas.geometry.Coordinate;
import org.geomajas.gwt.client.controller.GraphicsController;
import org.geomajas.gwt.client.gfx.GraphicsContext;
import org.geomajas.gwt.client.gfx.context.DomHelper.Namespace;
import org.geomajas.gwt.client.gfx.paintable.Composite;
import org.geomajas.gwt.client.gfx.style.FontStyle;
import org.geomajas.gwt.client.gfx.style.PictureStyle;
import org.geomajas.gwt.client.gfx.style.ShapeStyle;
import org.geomajas.gwt.client.gfx.style.Style;
import org.geomajas.gwt.client.spatial.Bbox;
import org.geomajas.gwt.client.spatial.Matrix;
import org.geomajas.gwt.client.spatial.geometry.LineString;
import org.geomajas.gwt.client.spatial.geometry.Polygon;
import org.geomajas.gwt.client.util.Dom;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Style.Overflow;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.ui.Widget;
/**
* Implementation of the GraphicsContext interface using the SVG language. This class is used in all browsers except
* Internet Explorer.
*
* @see org.geomajas.gwt.client.gfx.context.VmlGraphicsContext
*
* @author Pieter De Graef
*/
public class SvgGraphicsContext implements GraphicsContext {
private Composite defsGroup;
private Element defs;
private int width;
private int height;
private String id;
private DomHelper helper;
private Widget parent;
/**
* Constructs . It will create the initial DOM structure setup.
*
* @param parent
* The parent element, onto whom to attach the initial DOM structure.
*/
public SvgGraphicsContext(Widget parent) {
this.parent = parent;
// the root SVG node
Element rootNode = Dom.createElementNS(Dom.NS_SVG, "svg");
// needed for IE9
rootNode.getStyle().setOverflow(Overflow.HIDDEN);
String sWidth = Integer.toString(width);
String sHeight = Integer.toString(height);
Dom.setElementAttribute(rootNode, "width", sWidth);
Dom.setElementAttribute(rootNode, "height", sHeight);
Dom.setElementAttribute(rootNode, "viewBox", "0 0 " + sWidth + " " + sHeight);
//TODO: is xml:base attribute used in any way?
Dom.setElementAttribute(rootNode, "xml:base", GWT.getHostPageBaseURL());
helper = new DomHelper(rootNode, Namespace.SVG);
// Point style definitions:
defsGroup = new Composite("style_defs");
defs = helper.drawGroup(null, defsGroup, "defs");
// Append to parent: we need a top div or the svg is blocked by any peer div !!!
Element divNode = Dom.createElementNS(Dom.NS_HTML, "div");
Dom.setStyleAttribute(divNode, "position", "absolute");
Dom.setStyleAttribute(divNode, "width", "100%");
Dom.setStyleAttribute(divNode, "height", "100%");
id = Dom.createUniqueId();
divNode.setId(id);
parent.getElement().appendChild(divNode);
divNode.appendChild(rootNode);
}
/**
* Delete this element from the graphics DOM structure.
*
* @param parent
* parent group object
* @param name
* The element's name.
*/
public void deleteElement(Object parent, String name) {
if (isAttached()) {
helper.deleteElement(parent, name);
}
}
/**
* Delete this group from the graphics DOM structure.
*
* @param object
* The group's object.
*/
public void deleteGroup(Object object) {
if (isAttached()) {
helper.deleteGroup(object);
}
}
/**
* Draw a circle on the <code>GraphicsContext</code>.
*
* @param parent
* parent group object
* @param name
* The circle's name.
* @param position
* The center position as a coordinate.
* @param radius
* The circle's radius.
* @param style
* The styling object by which the circle should be drawn.
*/
public void drawCircle(Object parent, String name, Coordinate position, double radius, ShapeStyle style) {
if (isAttached()) {
Element circle = helper.createOrUpdateElement(parent, name, "circle", style);
Dom.setElementAttribute(circle, "cx", Integer.toString((int) position.getX()));
Dom.setElementAttribute(circle, "cy", Integer.toString((int) position.getY()));
Dom.setElementAttribute(circle, "r", Integer.toString((int) radius));
}
}
/**
* Draw inner group data directly (implementation-specific shortcut). This method can only be called once, creating
* the group. Delete the group first to redraw with different data.
*
* @param parent
* The parent group's object
* @param object
* The group's object
* @param data
* SVG fragment
* @param transformation
* transformation to apply to the group
*/
public void drawData(Object parent, Object object, String data, Matrix transformation) {
if (isAttached()) {
Element group = helper.getGroup(object);
if (group == null) {
group = helper.createOrUpdateGroup(parent, object, transformation, null);
Dom.setInnerSvg(group, data);
}
}
}
/**
* Creates a group element in the technology (SVG/VML/...) of this context. A group is meant to group other elements
* together.
*
* @param parent
* parent group object
* @param object
* group object
*/
public void drawGroup(Object parent, Object object) {
if (isAttached()) {
helper.createOrUpdateGroup(parent, object, null, null);
}
}
/**
* Creates a group element in the technology (SVG/VML/...) of this context with the specified tag name.
*
* @param parent
* parent group object
* @param object
* group object
* @param tagName
* the tag name
* @return element which was drawn
*/
public Element drawGroup(Object parent, Object object, String tagName) {
if (isAttached()) {
return helper.drawGroup(parent, object, tagName);
} else {
return null;
}
}
/**
* Creates a group element in the technology (SVG/VML/...) of this context. A group is meant to group other elements
* together, possibly applying a transformation upon them.
*
* @param parent
* parent group object
* @param object
* group object
* @param transformation
* On each group, it is possible to apply a matrix transformation (currently translation only). This is
* the real strength of a group element.
*/
public void drawGroup(Object parent, Object object, Matrix transformation) {
if (isAttached()) {
helper.drawGroup(parent, object, transformation);
}
}
/**
* Creates a group element in the technology (SVG/VML/...) of this context. A group is meant to group other elements
* together, and in this case applying a style on them.
*
* @param parent
* parent group object
* @param object
* group object
* @param style
* Add a style to a group.
*/
public void drawGroup(Object parent, Object object, Style style) {
if (isAttached()) {
helper.drawGroup(parent, object, style);
}
}
/**
* Creates a group element in the technology (SVG/VML/...) of this context. A group is meant to group other elements
* together, possibly applying a transformation upon them.
*
* @param parent
* parent group object
* @param object
* group object
* @param transformation
* On each group, it is possible to apply a matrix transformation (currently translation only). This is
* the real strength of a group element.
* @param style
* Add a style to a group.
*/
public void drawGroup(Object parent, Object object, Matrix transformation, Style style) {
if (isAttached()) {
helper.createOrUpdateGroup(parent, object, transformation, style);
}
}
/**
* Draw an image onto the the <code>GraphicsContext</code>.
*
* @param parent
* parent group object
* @param name
* The image's name.
* @param href
* The image's location (URL).
* @param bounds
* The bounding box that sets the image's origin (x and y), it's width and it's height.
* @param style
* A styling object to be passed along with the image. Can be null.
*/
public void drawImage(Object parent, String name, String href, Bbox bounds, PictureStyle style) {
if (isAttached()) {
Element image = helper.createOrUpdateElement(parent, name, "image", style);
Dom.setElementAttribute(image, "x", Integer.toString((int) bounds.getX()));
Dom.setElementAttribute(image, "y", Integer.toString((int) bounds.getY()));
Dom.setElementAttribute(image, "width", Integer.toString((int) bounds.getWidth()));
Dom.setElementAttribute(image, "height", Integer.toString((int) bounds.getHeight()));
Dom.setElementAttributeNS(Dom.NS_XLINK, image, "xlink:href", Dom.makeUrlAbsolute(href));
}
}
/**
* Draw a {@link LineString} geometry onto the <code>GraphicsContext</code>.
*
* @param parent
* parent group object
* @param name
* The LineString's name.
* @param line
* The LineString to be drawn.
* @param style
* The styling object for the LineString. Watch out for fill colors! If the fill opacity is not 0, then
* the LineString will have a fill surface.
*/
public void drawLine(Object parent, String name, LineString line, ShapeStyle style) {
if (isAttached()) {
Element element = helper.createOrUpdateElement(parent, name, "path", style);
if (line != null) {
Dom.setElementAttribute(element, "d", SvgPathDecoder.decode(line));
}
}
}
/**
* Draw a {@link Polygon} geometry onto the <code>GraphicsContext</code>.
*
* @param parent
* parent group object
* @param name
* The Polygon's name.
* @param polygon
* The Polygon to be drawn.
* @param style
* The styling object for the Polygon.
*/
public void drawPolygon(Object parent, String name, Polygon polygon, ShapeStyle style) {
if (isAttached()) {
Element element = helper.createOrUpdateElement(parent, name, "path", style);
if (polygon != null) {
Dom.setElementAttribute(element, "d", SvgPathDecoder.decode(polygon));
Dom.setElementAttribute(element, "fill-rule", "evenodd");
}
}
}
/**
* Draw a rectangle onto the <code>GraphicsContext</code>.
*
* @param parent
* parent group object
* @param name
* The rectangle's name.
* @param rectangle
* The rectangle to be drawn. The bounding box's origin, is the rectangle's upper left corner on the
* screen.
* @param style
* The styling object for the rectangle.
*/
public void drawRectangle(Object parent, String name, Bbox rectangle, ShapeStyle style) {
if (isAttached()) {
Element element = helper.createOrUpdateElement(parent, name, "rect", style);
Dom.setElementAttribute(element, "x", Double.toString(rectangle.getX()));
Dom.setElementAttribute(element, "y", Double.toString(rectangle.getY()));
Dom.setElementAttribute(element, "width", Double.toString(rectangle.getWidth()));
Dom.setElementAttribute(element, "height", Double.toString(rectangle.getHeight()));
}
}
/**
* Draw a type (def/symbol for svg).
*
* @param parent
* All shape-types are placed in the "defs" group. This parameter is therefore ignored.
* @param id
* the types's unique identifier
* @param symbol
* the symbol information
* @param style
* No default style is allowed at the moment in SVG. This parameter is ignored.
* @param transformation
* the transformation to apply on the symbol
*/
public void drawSymbolDefinition(Object parent, String id, SymbolInfo symbol, ShapeStyle style,
Matrix transformation) {
if (isAttached()) {
if (symbol == null) {
return;
}
// Step1: get or create the symbol element:
// check existence
Element def = Dom.getElementById(id);
boolean isNew = (def == null);
// create or update
def = helper.createOrUpdateElement(defsGroup, id, "symbol", null, false);
Dom.setElementAttribute(def, "overflow", "visible");
// Step2: fill in the correct values:
Element node = null;
if (symbol.getRect() != null) {
// Create the rectangle symbol:
long width = (long) symbol.getRect().getW();
long height = (long) symbol.getRect().getH();
if (transformation != null && transformation.getXx() != 0) {
double scale = transformation.getXx();
width = Math.round(width / scale);
height = Math.round(height / scale);
}
node = Dom.createElementNS(Dom.NS_SVG, "rect");
Dom.setElementAttribute(node, "width", Long.toString(width));
Dom.setElementAttribute(node, "height", Long.toString(height));
Dom.setElementAttribute(node, "x", Long.toString(-Math.round(width / 2)));
Dom.setElementAttribute(node, "y", Long.toString(-Math.round(height / 2)));
} else if (symbol.getCircle() != null) {
// Create the circle symbol:
long radius = (long) symbol.getCircle().getR();
if (transformation != null && transformation.getXx() != 0) {
double scale = transformation.getXx();
radius = Math.round(radius / scale);
}
node = Dom.createElementNS(Dom.NS_SVG, "circle");
Dom.setElementAttribute(node, "cx", "0");
Dom.setElementAttribute(node, "cy", "0");
Dom.setElementAttribute(node, "r", Long.toString(radius));
} else if (symbol.getImage() != null) {
// Create the image symbol:
node = Dom.createElementNS(Dom.NS_SVG, "image");
Dom.setElementAttributeNS(Dom.NS_XLINK, node, "xlink:href",
Dom.makeUrlAbsolute(symbol.getImage().getHref()));
long width = (long) symbol.getImage().getWidth();
long height = (long) symbol.getImage().getHeight();
if (transformation != null && transformation.getXx() != 0) {
double scale = transformation.getXx();
width = Math.round(width / scale);
height = Math.round(height / scale);
}
Dom.setElementAttribute(node, "width", Long.toString(width));
Dom.setElementAttribute(node, "height", Long.toString(height));
Dom.setElementAttribute(node, "x", Long.toString(-Math.round(width / 2)));
Dom.setElementAttribute(node, "y", Long.toString(-Math.round(height / 2)));
if (isNew) {
Element node2 = Dom.createElementNS(Dom.NS_SVG, "image");
Dom.setElementAttributeNS(Dom.NS_XLINK, node2, "xlink:href",
Dom.makeUrlAbsolute(symbol.getImage().getSelectionHref()));
Dom.setElementAttribute(node2, "width", Long.toString(width));
Dom.setElementAttribute(node2, "height", Long.toString(height));
Dom.setElementAttribute(node2, "x", Long.toString(-Math.round(width / 2)));
Dom.setElementAttribute(node2, "y", Long.toString(-Math.round(height / 2)));
Element def2 = helper.createOrUpdateElement(defsGroup, id + "-selection", "symbol", null, false);
Dom.setElementAttribute(def2, "overflow", "visible");
def2.appendChild(node2);
defs.appendChild(def2);
}
}
// Step3: Append the symbol definition:
if (isNew) {
def.appendChild(node);
defs.appendChild(def);
} else {
while (def.hasChildNodes()) {
Dom.removeChild(def, (com.google.gwt.user.client.Element) def.getFirstChildElement());
}
def.appendChild(node);
}
}
}
/**
* Draw a symbol, using some predefined ShapeType.
*
* @param parent
* parent group object
* @param name
* The symbol's name.
* @param position
* The symbol's (X,Y) location on the graphics.
* @param style
* The style to apply on the symbol.
* @param shapeTypeId
* The name of the predefined ShapeType. This symbol will create a reference to this predefined type and
* take on it's characteristics.
*/
public void drawSymbol(Object parent, String name, Coordinate position, ShapeStyle style, String shapeTypeId) {
if (isAttached()) {
Element useElement = helper.createOrUpdateElement(parent, name, "use", style);
Dom.setElementAttributeNS(Dom.NS_XLINK, useElement, "xlink:href", "#" + shapeTypeId);
if (position != null) {
Dom.setElementAttribute(useElement, "x", Double.toString(position.getX()));
Dom.setElementAttribute(useElement, "y", Double.toString(position.getY()));
}
}
}
/**
* Draw a string of text onto the <code>GraphicsContext</code>.
*
* @param parent
* parent group object
* @param name
* The text's name.
* @param text
* The actual string content.
* @param position
* The upper left corner for the text to originate.
* @param style
* The styling object for the text.
*/
public void drawText(Object parent, String name, String text, Coordinate position, FontStyle style) {
if (isAttached()) {
Element element = helper.createOrUpdateElement(parent, name, "text", style);
if (text != null) {
element.setInnerText(text);
}
if (position != null) {
int fontSize = 12;
if (style != null) {
fontSize = style.getFontSize();
}
Dom.setElementAttribute(element, "x", Double.toString(position.getX()));
Dom.setElementAttribute(element, "y", Double.toString(position.getY() + fontSize));
}
}
}
/**
* Return the (enclosing) group for the specified element id.
*
* @param id
* group id
* @return the group object
*/
public Object getGroupById(String id) {
if (isAttached()) {
return helper.getGroupById(id);
} else {
return null;
}
}
/**
* Return the id of the specified group.
*
* @param group
* the group object
* @return the corresponding element id or null if the group has not been drawn.
*/
public String getId(Object group) {
return helper.getId(group);
}
/**
* Return the unique id of the container div of this context.
*
* @return the unique id of the container div.
*/
public String getId() {
return id;
}
/**
* Return the element name for the specified id.
*
* @param id
* element id
* @return the name of the element
*/
public String getNameById(String id) {
if (isAttached()) {
return helper.getNameById(id);
} else {
return null;
}
}
/**
* Return the current graphics height.
*/
public int getHeight() {
return height;
}
/**
* Return the current graphics width.
*/
public int getWidth() {
return width;
}
/**
* Hide the specified element in the specified group. If the element does not exist, nothing will happen.
*
* @param group
* The group object.
* @param name
* The element name.
*/
public void hide(Object group, String name) {
if (isAttached()) {
Element element = helper.getElement(group, name);
if (element != null) {
Dom.setElementAttribute(element, "display", "none");
}
}
}
/**
* Hide the specified group. If the group does not exist, nothing will happen.
*
* @param group
* The group object.
*/
public void hide(Object group) {
if (isAttached()) {
Element element = helper.getGroup(group);
if (element != null) {
Dom.setElementAttribute(element, "display", "none");
}
}
}
/**
* Set the controller on an element of this <code>GraphicsContext</code> so it can react to events.
*
* @param object
* the element on which the controller should be set.
* @param controller
* The new <code>GraphicsController</code>
*/
public void setController(Object object, GraphicsController controller) {
if (isAttached()) {
helper.setController(object, controller);
}
}
/**
* Set the controller on an element of this <code>GraphicsContext</code> so it can react to events.
*
* @param parent
* the parent of the element on which the controller should be set.
* @param name
* the name of the child element on which the controller should be set
* @param controller
* The new <code>GraphicsController</code>
*/
public void setController(Object parent, String name, GraphicsController controller) {
if (isAttached()) {
helper.setController(parent, name, controller);
}
}
/**
* Set the controller on an element of this <code>GraphicsContext</code> so it can react to events.
*
* @param object
* the element on which the controller should be set.
* @param controller
* The new <code>GraphicsController</code>
* @param eventMask
* a bitmask to specify which events to listen for {@link com.google.gwt.user.client.Event}
*/
public void setController(Object object, GraphicsController controller, int eventMask) {
if (isAttached()) {
helper.setController(object, controller, eventMask);
}
}
/**
* Set the controller on an element of this <code>GraphicsContext</code> so it can react to events.
*
* @param parent
* the parent of the element on which the controller should be set.
* @param name
* the name of the child element on which the controller should be set
* @param controller
* The new <code>GraphicsController</code>
* @param eventMask
* a bitmask to specify which events to listen for {@link com.google.gwt.user.client.Event}
*/
public void setController(Object parent, String name, GraphicsController controller, int eventMask) {
if (isAttached()) {
helper.setController(parent, name, controller, eventMask);
}
}
/**
* Set a specific cursor on an element of this <code>GraphicsContext</code>.
*
* @param object
* the element on which the controller should be set.
* @param cursor
* The string representation of the cursor to use.
*/
public void setCursor(Object object, String cursor) {
if (isAttached()) {
helper.setCursor(object, cursor);
}
}
/**
* Set a specific cursor on an element of this <code>GraphicsContext</code>.
*
* @param parent
* the parent of the element on which the cursor should be set.
* @param name
* the name of the child element on which the cursor should be set
* @param cursor
* The string representation of the cursor to use.
*/
public void setCursor(Object parent, String name, String cursor) {
if (isAttached()) {
helper.setCursor(parent, name, cursor);
}
}
/**
* Apply a new size on the graphics context.
*
* @param newWidth
* The new newWidth in pixels for this graphics context.
* @param newHeight
* The new newHeight in pixels for this graphics context.
*/
public void setSize(int newWidth, int newHeight) {
this.width = newWidth;
this.height = newHeight;
if (helper.getRootElement() != null) {
String sWidth = Integer.toString(newWidth);
String sHeight = Integer.toString(newHeight);
Dom.setElementAttribute(helper.getRootElement(), "width", sWidth);
Dom.setElementAttribute(helper.getRootElement(), "height", sHeight);
Dom.setElementAttribute(helper.getRootElement(), "viewBox", "0 0 " + sWidth + " " + sHeight);
}
}
/**
* Show the specified element in the specified group. If the element does not exist, nothing will happen.
*
* @param group
* The group object.
* @param name
* The element name.
*/
public void unhide(Object group, String name) {
if (isAttached()) {
Element element = helper.getElement(group, name);
if (element != null) {
Dom.setElementAttribute(element, "display", "inline");
}
}
}
/**
* Show the specified group. If the group does not exist, nothing will happen.
*
* @param group
* The group object.
*/
public void unhide(Object group) {
if (isAttached()) {
Element element = helper.getGroup(group);
if (element != null) {
Dom.setElementAttribute(element, "display", "inline");
}
}
}
private boolean isAttached() {
return parent != null && parent.isAttached();
}
/**
* Move an element from on group to another. The elements name will remain the same.
*
* @param name
* The name of the element within the sourceParent group.
* @param sourceParent
* The original parent object associated with the element.
* @param targetParent
* The target parent object to be associated with the element.
* @since 1.8.0
*/
public void moveElement(String name, Object sourceParent, Object targetParent) {
helper.moveElement(name, sourceParent, targetParent);
}
@Override
public void bringToFront(final Object object, final String name) {
helper.bringToFront(object, name);
}
@Override
public void moveToBack(Object object, String name) {
helper.moveToBack(object, name);
}
}