Java tutorial
/** * Sencha GXT 4.0.0 - Sencha for GWT * Copyright (c) 2006-2015, Sencha Inc. * * licensing@sencha.com * http://www.sencha.com/products/gxt/license/ * * ================================================================================ * Open Source License * ================================================================================ * This version of Sencha GXT is licensed under the terms of the Open Source GPL v3 * license. You may use this license only if you are prepared to distribute and * share the source code of your application under the GPL v3 license: * http://www.gnu.org/licenses/gpl.html * * If you are NOT prepared to distribute and share the source code of your * application under the GPL v3 license, other commercial and oem licenses * are available for an alternate download of Sencha GXT. * * Please see the Sencha GXT Licensing page at: * http://www.sencha.com/products/gxt/license/ * * For clarification or additional options, please contact: * licensing@sencha.com * ================================================================================ * * * ================================================================================ * Disclaimer * ================================================================================ * THIS SOFTWARE IS DISTRIBUTED "AS-IS" WITHOUT ANY WARRANTIES, CONDITIONS AND * REPRESENTATIONS WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE * IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, MERCHANTABLE QUALITY, * FITNESS FOR A PARTICULAR PURPOSE, DURABILITY, NON-INFRINGEMENT, PERFORMANCE AND * THOSE ARISING BY STATUTE OR FROM CUSTOM OR USAGE OF TRADE OR COURSE OF DEALING. * ================================================================================ */ package com.sencha.gxt.chart.client.draw.engine; import java.util.HashMap; import java.util.Map; import java.util.logging.Logger; import com.google.gwt.canvas.dom.client.Context2d.LineCap; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.JsArray; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.regexp.shared.RegExp; import com.google.gwt.regexp.shared.SplitResult; import com.google.gwt.resources.client.ClientBundle; import com.google.gwt.resources.client.CssResource; import com.google.gwt.resources.client.ImageResource; import com.google.gwt.safehtml.shared.SafeHtmlUtils; import com.sencha.gxt.chart.client.draw.Color; import com.sencha.gxt.chart.client.draw.Gradient; import com.sencha.gxt.chart.client.draw.Matrix; import com.sencha.gxt.chart.client.draw.Rotation; import com.sencha.gxt.chart.client.draw.Scaling; import com.sencha.gxt.chart.client.draw.Stop; import com.sencha.gxt.chart.client.draw.Translation; import com.sencha.gxt.chart.client.draw.path.ClosePath; import com.sencha.gxt.chart.client.draw.path.CurveTo; import com.sencha.gxt.chart.client.draw.path.LineTo; import com.sencha.gxt.chart.client.draw.path.MoveTo; import com.sencha.gxt.chart.client.draw.path.PathCommand; import com.sencha.gxt.chart.client.draw.path.PathSprite; import com.sencha.gxt.chart.client.draw.sprite.CircleSprite; import com.sencha.gxt.chart.client.draw.sprite.EllipseSprite; import com.sencha.gxt.chart.client.draw.sprite.ImageSprite; import com.sencha.gxt.chart.client.draw.sprite.RectangleSprite; import com.sencha.gxt.chart.client.draw.sprite.Sprite; import com.sencha.gxt.chart.client.draw.sprite.TextSprite; import com.sencha.gxt.chart.client.draw.sprite.TextSprite.TextAnchor; import com.sencha.gxt.chart.client.draw.sprite.TextSprite.TextBaseline; import com.sencha.gxt.core.client.BindingPropertySet; import com.sencha.gxt.core.client.BindingPropertySet.PropertyName; import com.sencha.gxt.core.client.GXTLogConfiguration; import com.sencha.gxt.core.client.dom.XElement; import com.sencha.gxt.core.client.resources.StyleInjectorHelper; import com.sencha.gxt.core.client.util.PrecisePoint; import com.sencha.gxt.core.client.util.PreciseRectangle; import com.sencha.gxt.core.client.util.Size; import com.sencha.gxt.core.client.util.TextMetrics; /** * Provides specific methods to draw with VML. * */ public class VML extends DomSurface { private static final Logger logger = Logger.getLogger(VML.class.getName()); @PropertyName("gxt.vml.enableSpritePooling") public interface SpritePooling extends BindingPropertySet { @PropertyValue("true") boolean isEnabled(); } public interface VmlCss extends CssResource { @ClassName("vml") String vml(); @ClassName("x-vml-base") String baseVmlClass(); @ClassName("x-hide-visibility") String hideClass(); @ClassName("x-vml-sprite") String spriteVmlClass(); } public interface VmlBundle extends ClientBundle { @Source("vml.gss") VmlCss vml(); } /** * Returns the given number rounded to the given precision. * * @param number the number to be rounded * @param place the decimal place * @return the rounded number */ protected static native String toFixed(double number, double place) /*-{ return number.toFixed(place); }-*/; private int zoom = 21600; private Scaling viewBoxShift = null; private RegExp newLineRegExp = RegExp.compile("\n"); private Map<Sprite, PreciseRectangle> textBBoxCache = new HashMap<Sprite, PreciseRectangle>(); // store rendered text values to emulate SVG text bbox calls private Map<Sprite, PrecisePoint> textRenderedPoints = new HashMap<Sprite, PrecisePoint>(); private Map<Sprite, TextBaseline> textRenderedBaseline = new HashMap<Sprite, TextBaseline>(); protected boolean ignoreOptimizations = false; protected VmlCss css; /** * Flag to cache unused VML elements since they can leak if excessively created and deleted. Defaults to false, * and designed to be set from a set-property decl in a module file. For example, this line could turn on pooling * across all VML instances: * <code><pre> * <set-property name="gxt.vml.enableSpritePooling" value="true" /> * </pre></code> * * This protected field can also be configured programmatically, but must be set before any sprites are added to the * surface (including before any axis/series/legend is added to a chart, and before it is attached). */ protected boolean poolingEnabled = GWT.<SpritePooling>create(SpritePooling.class).isEnabled(); protected JsArray<XElement> pool = JsArray.createArray().cast(); @Override public void deleteSprite(Sprite sprite) { XElement elt = getElement(sprite); super.deleteSprite(sprite); if (sprite instanceof TextSprite) { textBBoxCache.remove(sprite); textRenderedPoints.remove(sprite); textRenderedBaseline.remove(sprite); } if (poolingEnabled && !(sprite instanceof ImageSprite) && elt != null) { if (GXTLogConfiguration.loggingIsEnabled()) { logger.finest("Saving element for reuse: " + sprite + " = " + elt); } //wipe out child elements elt.setInnerSafeHtml(SafeHtmlUtils.EMPTY_SAFE_HTML); //path elt.setAttribute("path", ""); //stroke elt.setPropertyJSO("stroke", null); //fill elt.setPropertyJSO("fill", null); //clip elt.getStyle().setProperty("clip", "auto"); //z-index elt.getStyle().clearZIndex(); //id elt.setId(null); //hidden class elt.removeClassName(css.hideClass()); pool.push(elt); } } @Override public void draw() { super.draw(); if (surfaceElement == null) { VmlBundle bundle = GWT.create(VmlBundle.class); css = bundle.vml(); StyleInjectorHelper.ensureInjected(css, true); surfaceElement = Document.get().createDivElement().cast(); surfaceElement.addClassName(css.baseVmlClass()); surfaceElement.setSize(width, height); addNamespace("vml", "urn:schemas-microsoft-com:vml"); } container.appendChild(surfaceElement); container.setSize(width, height); renderAll(); } /** * Draws the surface ignoring whether or not sprites are dirty. */ public void drawIgnoreOptimizations() { ignoreOptimizations = true; draw(); ignoreOptimizations = false; } @Override public void renderSprite(Sprite sprite) { // Does the surface element exist? if (surfaceElement == null) { return; } if (!sprite.isDirty() && !ignoreOptimizations) { return; } applyAttributes(sprite); if (sprite.isTransformDirty() || ignoreOptimizations) { transform(sprite); } sprite.clearDirtyFlags(); } @Override public void setCursor(Sprite sprite, String property) { XElement element = getElement(sprite); if (element != null) { element.getStyle().setProperty("cursor", property); } } @Override public void setViewBox(double x, double y, double width, double height) { // Handle viewbox sizing if (this.width > 0 && this.height > 0) { double relativeHeight = this.height / height; double relativeWidth = this.width / width; if (width * relativeHeight < this.width) { x -= (this.width - width * relativeHeight) / 2.0 / relativeHeight; } if (height * relativeWidth < this.height) { y -= (this.height - height * relativeWidth) / 2.0 / relativeWidth; } double size = 1.0 / Math.max(width / this.width, height / this.height); // Scale and translate group viewBoxShift = new Scaling(size, size, -x, -y); for (Sprite sprite : sprites) { transform(sprite); } } } @Override protected PreciseRectangle getBBoxText(TextSprite sprite) { XElement element = getElement(sprite); if (element == null) { return new PreciseRectangle(); } PreciseRectangle bbox = textBBoxCache.get(sprite); if (bbox == null) { Element textPath = element.childElement("textPath").cast(); if (textPath != null) { TextMetrics.get().bind(textPath); } SplitResult split = newLineRegExp.split(sprite.getText()); bbox = new PreciseRectangle(); for (int i = 0; i < split.length(); i++) { double width = TextMetrics.get().getWidth(split.get(i)); bbox.setWidth(Math.max(bbox.getWidth(), width)); } bbox.setHeight(sprite.getFontSize() * split.length()); PrecisePoint point = textRenderedPoints.get(sprite); TextBaseline baseline = textRenderedBaseline.get(sprite); bbox.setX(point.getX()); bbox.setY(point.getY()); if (baseline == TextBaseline.MIDDLE) { bbox.setY(point.getY() - (bbox.getHeight() / 2.0)); } else if (baseline == TextBaseline.BOTTOM) { bbox.setY(point.getY() - bbox.getHeight()); } textBBoxCache.put(sprite, bbox); } return bbox; } /** * Adds the passed namespace and URN to the document. * * @param namespace the namespace to be added * @param urn the schema URN of the namespace */ private native void addNamespace(String namespace, String urn) /*-{ if (!$doc.namespaces[namespace]) { $doc.namespaces.add(namespace, urn); } }-*/; /** * Applies pending attributes to the DOM element of a {@link Sprite}. * * @param sprite the sprite in need of attributes to be set. */ private void applyAttributes(Sprite sprite) { String vml = null; XElement element = getElement(sprite); // Create sprite element if necessary if (element == null) { element = createSpriteElement(sprite); } if (sprite instanceof EllipseSprite || sprite instanceof CircleSprite) { vml = ellipticalArc(sprite); } else if (sprite instanceof RectangleSprite) { RectangleSprite rect = (RectangleSprite) sprite; if (rect.isXDirty() || rect.isYDirty() || rect.isWidthDirty() || rect.isHeightDirty() || rect.isRadiusDirty() || ignoreOptimizations) { // faster conversion for rectangles without rounded corners if (Double.isNaN(rect.getRadius()) || rect.getRadius() == 0) { StringBuilder path = new StringBuilder(); long x = Math.round(rect.getX() * zoom); long y = Math.round(rect.getY() * zoom); long width = Math.round((rect.getX() + rect.getWidth()) * zoom); long height = Math.round((rect.getY() + rect.getHeight()) * zoom); vml = path.append("m").append(x).append(",").append(y).append(" l").append(width).append(",") .append(y).append(" l").append(width).append(",").append(height).append(" l").append(x) .append(",").append(height).append(" x e").toString(); } else { vml = path2vml(new PathSprite(rect)); } } } else if (sprite instanceof PathSprite) { PathSprite pathSprite = (PathSprite) sprite; vml = path2vml(pathSprite); } else if (sprite instanceof TextSprite) { // Handle text (special handling required) setTextAttributes((TextSprite) sprite, element); } else if (sprite instanceof ImageSprite) { ImageSprite image = (ImageSprite) sprite; if (image.isXDirty() || ignoreOptimizations) { element.setLeft((int) Math.round(image.getX())); } if (image.isYDirty() || ignoreOptimizations) { element.setTop((int) Math.round(image.getY())); } if (image.isWidthDirty() || image.isHeightDirty() || ignoreOptimizations) { element.setSize(new Size((int) Math.round(image.getWidth()), (int) Math.round(image.getHeight()))); } if (image.isResourceDirty() || ignoreOptimizations) { ImageResource resource = image.getResource(); if (resource != null) { StringBuilder builder = new StringBuilder(); builder.append("url(").append(image.getResource().getSafeUri().asString()).append(") "); builder.append(-resource.getLeft()).append("px "); builder.append(-resource.getTop()).append("px"); element.getStyle().setProperty("background", builder.toString()); } else { element.getStyle().clearBackgroundImage(); } } } if (vml != null) { element.setPropertyString("path", vml); } if (sprite.isZIndexDirty() || ignoreOptimizations) { applyZIndex(sprite, element); } String id = spriteIds.get(sprite); if (id != null) { element.setId(id); } // Apply clip rectangle to the sprite if (sprite.getClipRectangle() != null) { applyClip(sprite); } // Handle fill and opacity if (sprite.isOpacityDirty() || sprite.isStrokeOpacityDirty() || sprite.isFillDirty() || ignoreOptimizations) { setFill(sprite, element); } // Handle stroke (all fills require a stroke element) if (sprite.isStrokeDirty() || sprite.isStrokeWidthDirty() || sprite.isStrokeOpacityDirty() || sprite.isFillDirty() || ignoreOptimizations) { setStroke(sprite, element); } // Hide or show the sprite if (sprite.isHiddenDirty() || ignoreOptimizations) { if (sprite.isHidden()) { element.addClassName(css.hideClass()); } else { element.removeClassName(css.hideClass()); } } } /** * Applies the clip rectangle of the given sprite to its DOM element. * * @param sprite the sprite to have its clip rectangle applied */ private void applyClip(Sprite sprite) { if (sprite.getRotation() != null) { //VML cannot rotate clips, warn and do what we can logger.severe("VML Surface implementation cannot clip rotated sprites."); } PreciseRectangle clip = sprite.getClipRectangle(); if (sprite.getScaling() != null || sprite.getTranslation() != null) { PathSprite transPath = new PathSprite(new RectangleSprite(clip)); transPath = transPath.map(sprite.transformMatrix()); clip = transPath.dimensions(); } PreciseRectangle bbox = sprite.getBBox(); double top = clip.getY() - bbox.getY(); double left = clip.getX() - bbox.getX(); double right = left + clip.getWidth(); double bottom = top + clip.getHeight(); getElement(sprite).getStyle().setProperty("clip", "rect(" + top + "px " + right + "px " + bottom + "px " + left + "px)"); } /** * Applies the z-index of the given sprite to its DOM element. * * @param sprite the sprite to have its z-index applied */ private void applyZIndex(Sprite sprite, XElement element) { if (element != null) { element.getStyle().setProperty("zIndex", String.valueOf(sprite.getZIndex())); } } /** * Determines whether a {@link PathSprite} contains a command not supported by * VML. * * @param sprite the sprite to be inspected * @return true if the sprite contains an unsupported command */ private boolean containsNonVMLCommands(PathSprite sprite) { for (PathCommand command : sprite.getCommands()) { if (!(command instanceof MoveTo) && !(command instanceof CurveTo) && !(command instanceof LineTo) && !(command instanceof ClosePath)) { return true; } } return false; } /** * Creates a VML DOM node. * * @param tagName the type of node to create. * @return the created VML DOM node. */ private XElement createNode(String tagName) { Element node = Document.get().createElement("vml:" + tagName); node.setClassName(css.vml()); return XElement.as(node); } /** * Creates the DOM element of the passed {@link Sprite}. * * @param sprite the sprite in need of element creation */ private XElement createSpriteElement(Sprite sprite) { final XElement element; if (sprite instanceof ImageSprite) { element = createNode("image"); } else { if (!poolingEnabled || pool.length() == 0) { element = createNode("shape"); } else { element = pool.shift(); if (GXTLogConfiguration.loggingIsEnabled()) { logger.finest("reusing shape: " + element); } assert element.getTagName().equals("vml:shape") : element.getTagName(); } XElement skew = createNode("skew"); skew.setPropertyBoolean("on", true); element.appendChild(skew); element.setPropertyJSO("skew", skew); } element.setPropertyString("coordsize", zoom + " " + zoom); element.setPropertyString("coordorigin", "0 0"); element.addClassName(css.spriteVmlClass()); if (sprite instanceof TextSprite) { XElement path = createNode("path"); path.setPropertyBoolean("textpathok", true); XElement textPath = createNode("textpath"); textPath.setPropertyBoolean("on", true); element.appendChild(textPath); element.appendChild(path); Style textStyle = textPath.getStyle(); textStyle.setProperty("lineHeight", "normal"); textStyle.setProperty("fontVariant", "normal"); textRenderedPoints.put(sprite, new PrecisePoint()); } this.surfaceElement.appendChild(element); setElement(sprite, element); return element; } /** * Generates a path sprite made up of an elliptical arc based on the given * {@link CircleSprite} or {@link EllipseSprite}. Returns null if not an * Circle * * @param sprite the circle or ellipse to be converted to an elliptical arc * @return the generated elliptical arc */ private String ellipticalArc(Sprite sprite) { final double cx, cy, rx, ry; if (sprite instanceof EllipseSprite) { EllipseSprite ellipse = (EllipseSprite) sprite; if (!ellipse.isCenterXDirty() && !ellipse.isCenterYDirty() && !ellipse.isRadiusXDirty() && !ellipse.isRadiusYDirty() && !ignoreOptimizations) { return null; } cx = ellipse.getCenterX(); cy = ellipse.getCenterY(); rx = ellipse.getRadiusX(); ry = ellipse.getRadiusY(); } else if (sprite instanceof CircleSprite) { CircleSprite circle = (CircleSprite) sprite; if (!circle.isCenterXDirty() && !circle.isCenterYDirty() && !circle.isRadiusDirty() && !ignoreOptimizations) { return null; } cx = circle.getCenterX(); cy = circle.getCenterY(); rx = circle.getRadius(); ry = circle.getRadius(); } else { return null; } long centerX = Math.round(cx * zoom); long yShift = Math.round((cy - ry) * zoom); return new StringBuilder().append("ar").append(Math.round((cx - rx) * zoom)).append(",").append(yShift) .append(",").append(Math.round((cx + rx) * zoom)).append(",").append(Math.round((cy + ry) * zoom)) .append(",").append(centerX).append(",").append(yShift).append(",").append(centerX).append(",") .append(yShift).toString(); } /** * Returns a string representing a VML path using the the passed * {@link PathSprite}. * * @param sprite the sprite to be converted * @return the converted path */ private String path2vml(PathSprite sprite) { StringBuilder path = new StringBuilder(); if (containsNonVMLCommands(sprite)) { sprite = sprite.copy().toCurve(); } else { sprite = sprite.copy().toAbsolute(); } for (PathCommand command : sprite.getCommands()) { if (command instanceof CurveTo) { CurveTo curveto = (CurveTo) command; path.append("c").append(Math.round(curveto.getX1() * zoom)).append(",") .append(Math.round(curveto.getY1() * zoom)).append(",") .append(Math.round(curveto.getX2() * zoom)).append(",") .append(Math.round(curveto.getY2() * zoom)).append(",") .append(Math.round(curveto.getX() * zoom)).append(",") .append(Math.round(curveto.getY() * zoom)).append(" "); } else if (command instanceof MoveTo) { MoveTo moveto = (MoveTo) command; path.append("m").append(Math.round(moveto.getX() * zoom)).append(",") .append(Math.round(moveto.getY() * zoom)).append(" "); } else if (command instanceof LineTo) { LineTo lineto = (LineTo) command; path.append("l").append(Math.round(lineto.getX() * zoom)).append(",") .append(Math.round(lineto.getY() * zoom)).append(" "); } else if (command instanceof ClosePath) { path.append("x "); } } path.append("e"); return path.toString(); } /** * Applies the fill attribute of a sprite to its VML DOM element. * * @param sprite the sprite to have its fill set */ private void setFill(Sprite sprite, Element element) { Element fill = element.getPropertyJSO("fill").cast(); if (fill == null) { fill = createNode("fill"); element.setPropertyJSO("fill", fill); element.appendChild(fill); } if (sprite.getFill() == null || sprite.getFill() == Color.NONE) { fill.setPropertyBoolean("on", false); } else { if (sprite.isFillDirty() || ignoreOptimizations) { fill.setPropertyBoolean("on", true); if (sprite.getFill() instanceof Gradient) { Gradient gradient = (Gradient) sprite.getFill(); // VML angle is offset and inverted from standard, and must be // adjusted // to match rotation transform final double degrees; if (sprite.getRotation() != null) { degrees = sprite.getRotation().getDegrees(); } else { degrees = 0; } double angle; angle = -(gradient.getAngle() + 270 + degrees) % 360.0; // IE will flip the angle at 0 degrees... if (angle == 0) { angle = 180; } fill.setPropertyDouble("angle", angle); fill.setPropertyString("type", "gradient"); fill.setPropertyString("method", "sigma"); StringBuilder stops = new StringBuilder(); for (Stop stop : gradient.getStops()) { if (stops.length() > 0) { stops.append(", "); } stops.append(stop.getOffset()).append("% ").append(stop.getColor()); } Element colors = fill.getPropertyJSO("colors").cast(); colors.setPropertyString("value", stops.toString()); } else { fill.setPropertyString("color", sprite.getFill().getColor()); fill.setPropertyString("src", ""); fill.setPropertyString("type", "solid"); } } if (!Double.isNaN(sprite.getOpacity()) && (sprite.isOpacityDirty() || ignoreOptimizations)) { fill.setPropertyString("opacity", String.valueOf(sprite.getOpacity())); } if (!Double.isNaN(sprite.getFillOpacity()) && (sprite.isFillOpacityDirty() || ignoreOptimizations)) { fill.setPropertyString("opacity", String.valueOf(sprite.getFillOpacity())); } } } /** * Applies the stroke attribute of a sprite to its VML dom element. * * @param sprite the sprite to have its stroke set */ private void setStroke(Sprite sprite, XElement element) { Element stroke = element.getPropertyJSO("stroke").cast(); if (stroke == null) { stroke = createNode("stroke"); element.setPropertyJSO("stroke", stroke); element.appendChild(stroke); } Color strokeColor = sprite.getStroke(); if (strokeColor == null || strokeColor == Color.NONE || sprite.getStrokeWidth() == 0.0) { stroke.setPropertyBoolean("on", false); } else { stroke.setPropertyBoolean("on", true); if (!(strokeColor instanceof Gradient)) { // VML does NOT support a gradient stroke :( stroke.setPropertyString("color", strokeColor.getColor()); } if (sprite instanceof PathSprite) { PathSprite path = (PathSprite) sprite; LineCap strokeLineCap = path.getStrokeLineCap(); if (strokeLineCap != null) { // legal values for endcap are flat, square, round // http://msdn.microsoft.com/en-us/library/bb229428%28v=vs.85%29.aspx stroke.setPropertyString("endcap", strokeLineCap == LineCap.BUTT ? "flat" : strokeLineCap.getValue()); } if (path.getStrokeLineJoin() != null) { stroke.setPropertyString("joinstyle", path.getStrokeLineJoin().getValue()); } if (!Double.isNaN(path.getMiterLimit())) { stroke.setPropertyDouble("miterlimit", path.getMiterLimit()); } } double width = sprite.getStrokeWidth(); double opacity = sprite.getStrokeOpacity(); if (Double.isNaN(width)) { width = 0.75; } else { width *= 0.75; } if (Double.isNaN(opacity)) { opacity = 1; } // VML Does not support stroke widths under 1, so we're going to fiddle // with stroke-opacity instead. if (width < 1) { opacity *= width; width = 1; } stroke.setPropertyDouble("weight", width); stroke.setPropertyDouble("opacity", opacity); } } /** * Sets the text alignment on the given {@link Style}. * * @param style the style * @param align the text alignment */ private native void setTextAlign(Style style, String align) /*-{ style["v-text-align"] = align; }-*/; /** * Applies the attributes of the passed {@link TextSprite} to its VML element. * * @param sprite the sprite whose attributes to use */ private void setTextAttributes(TextSprite sprite, XElement element) { Element textPath = element.childElement("textPath").cast(); Style textStyle = textPath.getStyle(); textBBoxCache.remove(sprite); if (sprite.isFontSizeDirty() || ignoreOptimizations) { if (sprite.getFontSize() > 0) { textStyle.setFontSize(sprite.getFontSize(), Unit.PX); } else { textStyle.clearFontSize(); } } if (sprite.isFontStyleDirty() || ignoreOptimizations) { if (sprite.getFontStyle() != null) { textStyle.setFontStyle(sprite.getFontStyle()); } else { textStyle.clearFontStyle(); } } if (sprite.isFontWeightDirty() || ignoreOptimizations) { if (sprite.getFontWeight() != null) { textStyle.setFontWeight(sprite.getFontWeight()); } else { textStyle.clearFontWeight(); } } if (sprite.isFontDirty() || ignoreOptimizations) { if (sprite.getFont() != null) { textStyle.setProperty("fontFamily", sprite.getFont()); } else { textStyle.clearProperty("fontFamily"); } } // text-anchor emulation if (sprite.isTextAnchorDirty() || ignoreOptimizations) { if (sprite.getTextAnchor() == TextAnchor.MIDDLE) { setTextAlign(textStyle, "center"); } else if (sprite.getTextAnchor() == TextAnchor.END) { setTextAlign(textStyle, "right"); } else { setTextAlign(textStyle, "left"); } } if (sprite.isTextDirty() || ignoreOptimizations) { if (sprite.getText() != null) { textPath.setPropertyString("string", sprite.getText()); } else { textPath.setPropertyString("string", ""); } } if (sprite.isTextBaselineDirty() || sprite.isXDirty() || sprite.isYDirty() || ignoreOptimizations) { double height = sprite.getFontSize(); if (sprite.getTextBaseline() == TextBaseline.MIDDLE) { height = 0; } else if (sprite.getTextBaseline() == TextBaseline.BOTTOM) { height *= -1; } Element path = element.childElement("path").cast(); path.setPropertyString("v", new StringBuilder().append("m").append(Math.round(sprite.getX() * zoom)).append(",") .append(Math.round((sprite.getY() + (height / 2.0)) * zoom)).append(" l") .append(Math.round(sprite.getX() * zoom) + 1).append(",") .append(Math.round((sprite.getY() + (height / 2.0)) * zoom)).toString()); textRenderedPoints.put(sprite, new PrecisePoint(sprite.getX(), sprite.getY())); textRenderedBaseline.put(sprite, sprite.getTextBaseline()); } } /** * Applies transformation to passed sprite * * @param sprite the sprite to be transformed */ private void transform(Sprite sprite) { double deltaDegrees = 0; double deltaScaleX = 1; double deltaScaleY = 1; Matrix matrix = new Matrix(); Rotation rotation = sprite.getRotation(); Scaling scaling = sprite.getScaling(); Translation translation = sprite.getTranslation(); sprite.transformMatrix(); XElement element = getElement(sprite); Style style = element.getStyle(); Element skew = element.getPropertyJSO("skew").cast(); if (rotation != null) { matrix.rotate(rotation.getDegrees(), rotation.getX(), rotation.getY()); deltaDegrees += rotation.getDegrees(); } if (scaling != null) { matrix.scale(scaling.getX(), scaling.getY(), scaling.getCenterX(), scaling.getCenterY()); deltaScaleX *= scaling.getX(); deltaScaleY *= scaling.getY(); } if (translation != null) { matrix.translate(translation.getX(), translation.getY()); } if (viewBoxShift != null) { matrix.prepend(viewBoxShift.getX(), 0, 0, viewBoxShift.getX(), viewBoxShift.getCenterX() * viewBoxShift.getX(), viewBoxShift.getCenterY() * viewBoxShift.getX()); } if (!(sprite instanceof ImageSprite) && skew != null) { // matrix transform via VML skew skew.setPropertyString("origin", "0,0"); skew.setPropertyString("matrix", new StringBuilder().append(toFixed(matrix.get(0, 0), 4)).append(", ") .append(toFixed(matrix.get(0, 1), 4)).append(", ").append(toFixed(matrix.get(1, 0), 4)) .append(", ").append(toFixed(matrix.get(1, 1), 4)).append(", 0, 0").toString()); // ensure offset is less than or equal to 32767 and greater than or equal // to -32768, otherwise VMl crashes double offsetX = Math.max(Math.min(matrix.get(0, 2), 32767), -32768); double offsetY = Math.max(Math.min(matrix.get(1, 2), 32767), -32768); String offset = toFixed(offsetX, 4) + ", " + toFixed(offsetY, 4); skew.setPropertyString("offset", offset); } else { double deltaX = matrix.get(0, 2); double deltaY = matrix.get(1, 2); // Scale via coordsize property double zoomScaleX = zoom / deltaScaleX; double zoomScaleY = zoom / deltaScaleY; element.setPropertyString("coordsize", Math.abs(zoomScaleX) + " " + Math.abs(zoomScaleY)); // Rotate via rotation property double newAngle = deltaDegrees * (deltaScaleX * ((deltaScaleY < 0) ? -1 : 1)); if ((style.getProperty("rotation") == null && newAngle != 0)) { style.setProperty("rotation", String.valueOf(newAngle)); } else if (style.getProperty("rotation") != null && newAngle != Double.valueOf(style.getProperty("rotation"))) { style.setProperty("rotation", String.valueOf(newAngle)); } if (deltaDegrees != 0) { // Compensate x/y position due to rotation Matrix compMatrix = new Matrix(); compMatrix.rotate(-deltaDegrees, deltaX, deltaY); deltaX = deltaX * compMatrix.get(0, 0) + deltaY * compMatrix.get(0, 1) + compMatrix.get(0, 2); deltaY = deltaX * compMatrix.get(1, 0) + deltaY * compMatrix.get(1, 1) + compMatrix.get(1, 2); } String flip = ""; // Handle negative scaling via flipping if (deltaScaleX < 0) { flip += "x"; } if (deltaScaleY < 0) { flip += " y"; } if (!flip.equals("")) { style.setProperty("flip", flip); } // Translate via coordorigin property element.setPropertyString("coordorigin", (-zoomScaleX * (deltaX / ((ImageSprite) sprite).getWidth())) + " " + (-zoomScaleY * (deltaY / ((ImageSprite) sprite).getHeight()))); } } }