1 /*
   2  * Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.shape;
  27 
  28 import com.sun.javafx.collections.TrackableObservableList;
  29 import com.sun.javafx.geom.Path2D;
  30 import com.sun.javafx.scene.DirtyBits;
  31 import com.sun.javafx.scene.NodeHelper;
  32 import com.sun.javafx.scene.shape.PathElementHelper;
  33 import com.sun.javafx.scene.shape.PathHelper;
  34 import com.sun.javafx.scene.shape.PathUtils;
  35 import com.sun.javafx.scene.shape.ShapeHelper;
  36 import com.sun.javafx.sg.prism.NGNode;
  37 import com.sun.javafx.sg.prism.NGPath;
  38 import javafx.beans.property.ObjectProperty;
  39 import javafx.beans.property.ObjectPropertyBase;
  40 import javafx.collections.ListChangeListener.Change;
  41 import javafx.collections.ObservableList;
  42 import javafx.css.StyleableProperty;
  43 import javafx.geometry.BoundingBox;
  44 import javafx.geometry.Bounds;
  45 import javafx.scene.paint.Color;
  46 import javafx.scene.paint.Paint;
  47 
  48 import java.util.Collection;
  49 import java.util.List;
  50 import javafx.scene.Node;
  51 
  52 /**
  53  * The {@code Path} class represents a simple shape
  54  * and provides facilities required for basic construction
  55  * and management of a geometric path.  Example:
  56  *
  57 <PRE>
  58 import javafx.scene.shape.*;
  59 
  60 Path path = new Path();
  61 
  62 MoveTo moveTo = new MoveTo();
  63 moveTo.setX(0.0f);
  64 moveTo.setY(0.0f);
  65 
  66 HLineTo hLineTo = new HLineTo();
  67 hLineTo.setX(70.0f);
  68 
  69 QuadCurveTo quadCurveTo = new QuadCurveTo();
  70 quadCurveTo.setX(120.0f);
  71 quadCurveTo.setY(60.0f);
  72 quadCurveTo.setControlX(100.0f);
  73 quadCurveTo.setControlY(0.0f);
  74 
  75 LineTo lineTo = new LineTo();
  76 lineTo.setX(175.0f);
  77 lineTo.setY(55.0f);
  78 
  79 ArcTo arcTo = new ArcTo();
  80 arcTo.setX(50.0f);
  81 arcTo.setY(50.0f);
  82 arcTo.setRadiusX(50.0f);
  83 arcTo.setRadiusY(50.0f);
  84 
  85 path.getElements().add(moveTo);
  86 path.getElements().add(hLineTo);
  87 path.getElements().add(quadCurveTo);
  88 path.getElements().add(lineTo);
  89 path.getElements().add(arcTo);
  90 
  91 </PRE>
  92  * @since JavaFX 2.0
  93  */
  94 public class Path extends Shape {
  95     static {
  96         PathHelper.setPathAccessor(new PathHelper.PathAccessor() {
  97             @Override
  98             public NGNode doCreatePeer(Node node) {
  99                 return ((Path) node).doCreatePeer();
 100             }
 101 
 102             @Override
 103             public void doUpdatePeer(Node node) {
 104                 ((Path) node).doUpdatePeer();
 105             }
 106 
 107             @Override
 108             public Paint doCssGetFillInitialValue(Shape shape) {
 109                 return ((Path) shape).doCssGetFillInitialValue();
 110             }
 111 
 112             @Override
 113             public Paint doCssGetStrokeInitialValue(Shape shape) {
 114                 return ((Path) shape).doCssGetStrokeInitialValue();
 115             }
 116 
 117             @Override
 118             public com.sun.javafx.geom.Shape doConfigShape(Shape shape) {
 119                 return ((Path) shape).doConfigShape();
 120             }
 121 
 122         });
 123     }
 124 
 125     private Path2D path2d = null;
 126 
 127     {
 128         // To initialize the class helper at the begining each constructor of this class
 129         PathHelper.initHelper(this);
 130 
 131         // overriding default values for fill and stroke
 132         // Set through CSS property so that it appears to be a UA style rather
 133         // that a USER style so that fill and stroke can still be set from CSS.
 134         ((StyleableProperty)fillProperty()).applyStyle(null, null);
 135         ((StyleableProperty)strokeProperty()).applyStyle(null, Color.BLACK);
 136     }
 137 
 138     /**
 139      * Creates an empty instance of Path.
 140      */
 141     public Path() {
 142     }
 143 
 144     /**
 145      * Creates a new instance of Path
 146      * @param elements Elements of the Path
 147      * @since JavaFX 2.1
 148      */
 149     public Path(PathElement... elements) {
 150         if (elements != null) {
 151             this.elements.addAll(elements);
 152         }
 153     }
 154 
 155     /**
 156      * Creates new instance of Path
 157      * @param elements The collection of the elements of the Path
 158      * @since JavaFX 2.2
 159      */
 160     public Path(Collection<? extends PathElement> elements) {
 161         if (elements != null) {
 162             this.elements.addAll(elements);
 163         }
 164     }
 165 
 166     void markPathDirty() {
 167         path2d = null;
 168         NodeHelper.markDirty(this, DirtyBits.NODE_CONTENTS);
 169         impl_geomChanged();
 170     }
 171 
 172     /**
 173      * Defines the filling rule constant for determining the interior of the path.
 174      * The value must be one of the following constants:
 175      * {@code FillRile.EVEN_ODD} or {@code FillRule.NON_ZERO}.
 176      * The default value is {@code FillRule.NON_ZERO}.
 177      *
 178      * @defaultValue FillRule.NON_ZERO
 179      */
 180     private ObjectProperty<FillRule> fillRule;
 181 
 182     public final void setFillRule(FillRule value) {
 183         if (fillRule != null || value != FillRule.NON_ZERO) {
 184             fillRuleProperty().set(value);
 185         }
 186     }
 187 
 188     public final FillRule getFillRule() {
 189         return fillRule == null ? FillRule.NON_ZERO : fillRule.get();
 190     }
 191 
 192     public final ObjectProperty<FillRule> fillRuleProperty() {
 193         if (fillRule == null) {
 194             fillRule = new ObjectPropertyBase<FillRule>(FillRule.NON_ZERO) {
 195 
 196                 @Override
 197                 public void invalidated() {
 198                     NodeHelper.markDirty(Path.this, DirtyBits.NODE_CONTENTS);
 199                     impl_geomChanged();
 200                 }
 201 
 202                 @Override
 203                 public Object getBean() {
 204                     return Path.this;
 205                 }
 206 
 207                 @Override
 208                 public String getName() {
 209                     return "fillRule";
 210                 }
 211             };
 212         }
 213         return fillRule;
 214     }
 215 
 216     private boolean isPathValid;
 217     /**
 218      * Defines the array of path elements of this path.
 219      *
 220      * @defaultValue empty
 221      */
 222     private final ObservableList<PathElement> elements = new TrackableObservableList<PathElement>() {
 223         @Override
 224         protected void onChanged(Change<PathElement> c) {
 225             List<PathElement> list = c.getList();
 226             boolean firstElementChanged = false;
 227             while (c.next()) {
 228                 List<PathElement> removed = c.getRemoved();
 229                 for (int i = 0; i < c.getRemovedSize(); ++i) {
 230                     removed.get(i).removeNode(Path.this);
 231                 }
 232                 for (int i = c.getFrom(); i < c.getTo(); ++i) {
 233                     list.get(i).addNode(Path.this);
 234                 }
 235                 firstElementChanged |= c.getFrom() == 0;
 236             }
 237 
 238             //Note: as ArcTo may create a various number of PathElements,
 239             // we cannot count the number of PathElements removed (fast enough).
 240             // Thus we can optimize only if some elements were added to the end
 241             if (path2d != null) {
 242                 c.reset();
 243                 c.next();
 244                 // we just have to check the first change, as more changes cannot come after such change
 245                 if (c.getFrom() == c.getList().size() && !c.wasRemoved() && c.wasAdded()) {
 246                     // some elements added
 247                     for (int i = c.getFrom(); i < c.getTo(); ++i) {
 248                         PathElementHelper.addTo(list.get(i), path2d);
 249                     }
 250                 } else {
 251                     path2d = null;
 252                 }
 253             }
 254             if (firstElementChanged) {
 255                 isPathValid = isFirstPathElementValid();
 256             }
 257 
 258             NodeHelper.markDirty(Path.this, DirtyBits.NODE_CONTENTS);
 259             impl_geomChanged();
 260         }
 261     };
 262 
 263     /**
 264      * Gets observable list of path elements of this path.
 265      * @return Elements of this path
 266      */
 267     public final ObservableList<PathElement> getElements() { return elements; }
 268 
 269     /*
 270      * Note: This method MUST only be called via its accessor method.
 271      */
 272     private NGNode doCreatePeer() {
 273         return new NGPath();
 274     }
 275 
 276     /*
 277      * Note: This method MUST only be called via its accessor method.
 278      */
 279     private Path2D doConfigShape() {
 280         if (isPathValid) {
 281             if (path2d == null) {
 282                 path2d = PathUtils.configShape(getElements(), getFillRule() == FillRule.EVEN_ODD);
 283             } else {
 284                 path2d.setWindingRule(getFillRule() == FillRule.NON_ZERO ?
 285                                       Path2D.WIND_NON_ZERO : Path2D.WIND_EVEN_ODD);
 286             }
 287             return path2d;
 288         } else {
 289             return new Path2D();
 290         }
 291     }
 292 
 293     /**
 294      * @treatAsPrivate implementation detail
 295      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 296      */
 297     @Deprecated
 298     @Override
 299     protected Bounds impl_computeLayoutBounds() {
 300        if (isPathValid) {
 301            return super.impl_computeLayoutBounds();
 302        }
 303        return new BoundingBox(0, 0, -1, -1); //create empty bounds
 304     }
 305 
 306     private boolean isFirstPathElementValid() {
 307         ObservableList<PathElement> _elements = getElements();
 308         if (_elements != null && _elements.size() > 0) {
 309             PathElement firstElement = _elements.get(0);
 310             if (!firstElement.isAbsolute()) {
 311                 System.err.printf("First element of the path can not be relative. Path: %s\n", this);
 312                 return false;
 313             } else if (firstElement instanceof MoveTo) {
 314                 return true;
 315             } else {
 316                 System.err.printf("Missing initial moveto in path definition. Path: %s\n", this);
 317                 return false;
 318             }
 319         }
 320         return true;
 321     }
 322 
 323     /*
 324      * Note: This method MUST only be called via its accessor method.
 325      */
 326     private void doUpdatePeer() {
 327         if (NodeHelper.isDirty(this, DirtyBits.NODE_CONTENTS)) {
 328             NGPath peer = NodeHelper.getPeer(this);
 329             if (peer.acceptsPath2dOnUpdate()) {
 330                 peer.updateWithPath2d((Path2D) ShapeHelper.configShape(this));
 331             } else {
 332                 peer.reset();
 333                 if (isPathValid) {
 334                     peer.setFillRule(getFillRule());
 335                     for (final PathElement elt : getElements()) {
 336                         elt.addTo(peer);
 337                     }
 338                     peer.update();
 339                 }
 340             }
 341         }
 342     }
 343 
 344     /***************************************************************************
 345      *                                                                         *
 346      *                         Stylesheet Handling                             *
 347      *                                                                         *
 348      **************************************************************************/
 349 
 350     /*
 351      * Some sub-class of Shape, such as {@link Line}, override the
 352      * default value for the {@link Shape#fill} property. This allows
 353      * CSS to get the correct initial value.
 354      *
 355      * Note: This method MUST only be called via its accessor method.
 356      */
 357     private Paint doCssGetFillInitialValue() {
 358         return null;
 359     }
 360 
 361     /*
 362      * Some sub-class of Shape, such as {@link Line}, override the
 363      * default value for the {@link Shape#stroke} property. This allows
 364      * CSS to get the correct initial value.
 365      *
 366      * Note: This method MUST only be called via its accessor method.
 367      */
 368     private Paint doCssGetStrokeInitialValue() {
 369         return Color.BLACK;
 370     }
 371 
 372     /**
 373      * Returns a string representation of this {@code Path} object.
 374      * @return a string representation of this {@code Path} object.
 375      */
 376     @Override
 377     public String toString() {
 378         final StringBuilder sb = new StringBuilder("Path[");
 379 
 380         String id = getId();
 381         if (id != null) {
 382             sb.append("id=").append(id).append(", ");
 383         }
 384 
 385         sb.append("elements=").append(getElements());
 386 
 387         sb.append(", fill=").append(getFill());
 388         sb.append(", fillRule=").append(getFillRule());
 389 
 390         Paint stroke = getStroke();
 391         if (stroke != null) {
 392             sb.append(", stroke=").append(stroke);
 393             sb.append(", strokeWidth=").append(getStrokeWidth());
 394         }
 395 
 396         return sb.append("]").toString();
 397     }
 398 }