/* * $Id$ * This file is a part of the Arakhne Foundation Classes, http://www.arakhne.org/afc * * Copyright (c) 2000-2012 Stephane GALLAND. * Copyright (c) 2005-10, Multiagent Team, Laboratoire Systemes et Transports, * Universite de Technologie de Belfort-Montbeliard. * Copyright (c) 2013-2016 The original authors, and other authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.arakhne.afc.math.geometry.d2.ifx; import java.util.ArrayList; import java.util.Iterator; import java.util.Objects; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyListProperty; import javafx.beans.property.ReadOnlyListWrapper; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import org.eclipse.xtext.xbase.lib.Pure; import org.arakhne.afc.math.MathConstants; import org.arakhne.afc.math.geometry.MathFXAttributeNames; import org.arakhne.afc.math.geometry.PathElementType; import org.arakhne.afc.math.geometry.PathWindingRule; import org.arakhne.afc.math.geometry.d2.Point2D; import org.arakhne.afc.math.geometry.d2.Transform2D; import org.arakhne.afc.math.geometry.d2.ai.Path2ai; import org.arakhne.afc.math.geometry.d2.ai.PathIterator2ai; import org.arakhne.afc.vmutil.asserts.AssertMessages; import org.arakhne.afc.vmutil.locale.Locale; /** Path with 2 integer FX properties. * * @author $Author: sgalland$ * @version $FullVersion$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ * @since 13.0 */ public class Path2ifx extends AbstractShape2ifx<Path2ifx> implements Path2ai<Shape2ifx<?>, Path2ifx, PathElement2ifx, Point2ifx, Vector2ifx, Rectangle2ifx> { private static final long serialVersionUID = -5410743023218999966L; /** Array of types. */ private ReadOnlyListWrapper<PathElementType> types; /** Array of coords. */ private ReadOnlyListWrapper<Point2ifx> coords; /** Winding rule for the path. */ private ObjectProperty<PathWindingRule> windingRule; /** Indicates if the path is empty. * The path is empty when there is no point inside, or * all the points are at the same coordinate, or * when the path does not represents a drawable path * (a path with a line or a curve). */ private BooleanProperty isEmpty; /** Indicates if the path is a polyline. */ private BooleanProperty isPolyline; /** Indicates if the path is curved. */ private BooleanProperty isCurved; /** Indicates if the path is a polygon. */ private BooleanProperty isPolygon; /** Indicates if the path is multipart. */ private BooleanProperty isMultipart; /** Buffer for the bounds of the path that corresponds * to all the points added in the path. */ private ObjectProperty<Rectangle2ifx> logicalBounds; /** Construct an empty path. */ public Path2ifx() { this(DEFAULT_WINDING_RULE); } /** Construct a path by copying the given elements. * @param iterator the iterator that provides the elements to copy. */ public Path2ifx(Iterator<PathElement2ifx> iterator) { this(DEFAULT_WINDING_RULE, iterator); } /** Construct an empty path with the given path winding rule. * @param windingRule the path winding rule. */ public Path2ifx(PathWindingRule windingRule) { assert windingRule != null : AssertMessages.notNullParameter(); if (windingRule != DEFAULT_WINDING_RULE) { windingRuleProperty().set(windingRule); } } /** Construct a path by copying the given elements, and the given path winding rule. * @param windingRule the path winding rule. * @param iterator the iterator that provides the elements to copy. */ public Path2ifx(PathWindingRule windingRule, Iterator<PathElement2ifx> iterator) { assert windingRule != null : AssertMessages.notNullParameter(0); assert iterator != null : AssertMessages.notNullParameter(1); if (windingRule != DEFAULT_WINDING_RULE) { windingRuleProperty().set(windingRule); } add(iterator); } /** Constructor by copy. * @param path the path to copy. */ public Path2ifx(Path2ai<?, ?, ?, ?, ?, ?> path) { set(path); } @Pure @Override public boolean containsControlPoint(Point2D<?, ?> pt) { assert pt != null : AssertMessages.notNullParameter(); if (this.coords != null && !this.coords.isEmpty()) { final int size = this.coords.size(); for (int i = 0; i < size; i++) { final Point2ifx point = this.coords.get(i); if (point.ix() == pt.ix() && point.iy() == pt.iy()) { return true; } } } return false; } @Override public void clear() { if (this.types != null) { this.types.clear(); } if (this.coords != null) { this.coords.clear(); } } @Pure @Override public Path2ifx clone() { final Path2ifx clone = super.clone(); clone.coords = null; if (this.coords != null && !this.coords.isEmpty()) { clone.innerPointsProperty().addAll(this.coords); } clone.types = null; if (this.types != null && !this.types.isEmpty()) { clone.innerTypesProperty().addAll(this.types); } clone.windingRule = null; if (this.windingRule != null) { clone.windingRuleProperty().set(this.windingRule.get()); } clone.boundingBox = null; clone.logicalBounds = null; clone.isCurved = null; clone.isMultipart = null; clone.isPolyline = null; clone.isPolygon = null; clone.isEmpty = null; return clone; } @Pure @Override public int hashCode() { int bits = 1; bits = 31 * bits + Objects.hashCode(this.coords); bits = 31 * bits + Objects.hashCode(this.types); bits = 31 * bits + Objects.hashCode(this.windingRule); return bits ^ (bits >> 31); } @Override public void translate(int dx, int dy) { for (final Point2ifx pt : this.coords) { pt.add(dx, dy); } } @Override public void transform(Transform2D transform) { assert transform != null : AssertMessages.notNullParameter(); for (final Point2ifx pt : this.coords) { transform.transform(pt); } } /** Replies the isEmpty property. * * @return the isEmpty property. */ public BooleanProperty isEmptyProperty() { if (this.isEmpty == null) { this.isEmpty = new SimpleBooleanProperty(this, MathFXAttributeNames.IS_EMPTY); this.isEmpty.bind(Bindings.createBooleanBinding(() -> { final PathIterator2ai<PathElement2ifx> pi = getPathIterator(); while (pi.hasNext()) { final PathElement2ifx pe = pi.next(); if (pe.isDrawable()) { return false; } } return true; }, innerTypesProperty(), innerPointsProperty())); } return this.isEmpty; } @Override public boolean isEmpty() { return isEmptyProperty().get(); } @Override public ObjectProperty<Rectangle2ifx> boundingBoxProperty() { if (this.boundingBox == null) { this.boundingBox = new ReadOnlyObjectWrapper<>(this, MathFXAttributeNames.BOUNDING_BOX); this.boundingBox.bind(Bindings.createObjectBinding(() -> { final Rectangle2ifx bb = getGeomFactory().newBox(); Path2ai.calculatesDrawableElementBoundingBox( getPathIterator(MathConstants.SPLINE_APPROXIMATION_RATIO), bb); return bb; }, innerPointsProperty())); } return this.boundingBox; } /** Replies the property that corresponds to the bounding box of the control points. * * <p>The replied box is not the one corresponding to the drawable elements, as replied * by {@link #boundingBoxProperty()}. * * @return the bounding box of the control points. */ public ObjectProperty<Rectangle2ifx> controlPointBoundingBoxProperty() { if (this.logicalBounds == null) { this.logicalBounds = new ReadOnlyObjectWrapper<>(this, MathFXAttributeNames.CONTROL_POINT_BOUNDING_BOX); this.logicalBounds.bind(Bindings.createObjectBinding(() -> { final Rectangle2ifx bb = getGeomFactory().newBox(); Path2ai.calculatesControlPointBoundingBox( getPathIterator(), bb); return bb; }, innerPointsProperty())); } return this.logicalBounds; } @Override public Rectangle2ifx toBoundingBox() { return boundingBoxProperty().get().clone(); } @Override public void toBoundingBox(Rectangle2ifx box) { assert box != null : AssertMessages.notNullParameter(); box.set(boundingBoxProperty().get()); } /** Replies the windingRule property. * * @return the windingRule property. */ public ObjectProperty<PathWindingRule> windingRuleProperty() { if (this.windingRule == null) { this.windingRule = new SimpleObjectProperty<>(this, MathFXAttributeNames.WINDING_RULE, DEFAULT_WINDING_RULE); } return this.windingRule; } @Override public PathWindingRule getWindingRule() { return this.windingRule == null ? DEFAULT_WINDING_RULE : this.windingRule.get(); } @Override public void setWindingRule(PathWindingRule rule) { assert rule != null : AssertMessages.notNullParameter(); if (this.windingRule != null || rule != DEFAULT_WINDING_RULE) { windingRuleProperty().set(rule); } } /** Replies the isPolyline property. * * @return the isPolyline property. */ public BooleanProperty isPolylineProperty() { if (this.isPolyline == null) { this.isPolyline = new ReadOnlyBooleanWrapper(this, MathFXAttributeNames.IS_POLYLINE, false); this.isPolyline.bind(Bindings.createBooleanBinding(() -> { boolean first = true; boolean hasOneLine = false; for (final PathElementType type : innerTypesProperty()) { if (first) { if (type != PathElementType.MOVE_TO) { return false; } first = false; } else if (type != PathElementType.LINE_TO) { return false; } else { hasOneLine = true; } } return hasOneLine; }, innerTypesProperty())); } return this.isPolyline; } @Override public boolean isPolyline() { return isPolylineProperty().get(); } /** Replies the isCurved property. * * @return the isCurved property. */ public BooleanProperty isCurvedProperty() { if (this.isCurved == null) { this.isCurved = new ReadOnlyBooleanWrapper(this, MathFXAttributeNames.IS_CURVED, false); this.isCurved.bind(Bindings.createBooleanBinding(() -> { for (final PathElementType type : innerTypesProperty()) { if (type == PathElementType.CURVE_TO || type == PathElementType.QUAD_TO) { return true; } } return false; }, innerTypesProperty())); } return this.isCurved; } @Override public boolean isCurved() { return isCurvedProperty().get(); } /** Replies the isMultiParts property. * * @return the isMultiParts property. */ public BooleanProperty isMultiPartsProperty() { if (this.isMultipart == null) { this.isMultipart = new ReadOnlyBooleanWrapper(this, MathFXAttributeNames.IS_MULTIPARTS, false); this.isMultipart.bind(Bindings.createBooleanBinding(() -> { boolean foundOne = false; for (final PathElementType type : innerTypesProperty()) { if (type == PathElementType.MOVE_TO) { if (foundOne) { return true; } foundOne = true; } } return false; }, innerTypesProperty())); } return this.isMultipart; } @Override public boolean isMultiParts() { return isMultiPartsProperty().get(); } /** Replies the isPolygon property. * * @return the isPolygon property. */ public BooleanProperty isPolygonProperty() { if (this.isPolygon == null) { this.isPolygon = new ReadOnlyBooleanWrapper(this, MathFXAttributeNames.IS_POLYGON, false); this.isPolygon.bind(Bindings.createBooleanBinding(() -> { boolean first = true; boolean lastIsClose = false; for (final PathElementType type : innerTypesProperty()) { lastIsClose = false; if (first) { if (type != PathElementType.MOVE_TO) { return false; } first = false; } else if (type == PathElementType.MOVE_TO) { return false; } else if (type == PathElementType.CLOSE) { lastIsClose = true; } } return lastIsClose; }, innerTypesProperty())); } return this.isPolygon; } @Override public boolean isPolygon() { return isPolygonProperty().get(); } @Override public void closePath() { if (this.types == null || this.types.isEmpty() || (this.types.get(this.types.size() - 1) != PathElementType.CLOSE && this.types.get(this.types.size() - 1) != PathElementType.MOVE_TO)) { this.types.add(PathElementType.CLOSE); } } @Override public Rectangle2ifx toBoundingBoxWithCtrlPoints() { return controlPointBoundingBoxProperty().get().clone(); } @Override public void toBoundingBoxWithCtrlPoints(Rectangle2ifx box) { assert box != null : AssertMessages.notNullParameter(); box.set(controlPointBoundingBoxProperty().get()); } @Override public int[] toIntArray(Transform2D transform) { final int n = (this.coords != null) ? this.coords.size() * 2 : 0; final int[] clone = new int[n]; if (n > 0) { final Iterator<Point2ifx> iterator = this.coords.iterator(); Point2ifx point = iterator.next(); for (int i = 0; i < n; i += 2) { if (!(transform == null || transform.isIdentity())) { transform.transform(point); } clone[i] = point.ix(); clone[i + 1] = point.iy(); point = iterator.next(); } } return clone; } @Override public float[] toFloatArray(Transform2D transform) { final int n = (this.coords != null) ? this.coords.size() * 2 : 0; final float[] clone = new float[n]; if (n > 0) { final Iterator<Point2ifx> iterator = this.coords.iterator(); Point2ifx point = iterator.next(); for (int i = 0; i < n; i += 2) { if (!(transform == null || transform.isIdentity())) { transform.transform(point); } clone[i] = (float) point.getX(); clone[i + 1] = (float) point.getY(); point = iterator.next(); } } return clone; } @Override public double[] toDoubleArray(Transform2D transform) { final int n = (this.coords != null) ? this.coords.size() * 2 : 0; final double[] clone = new double[n]; if (n > 0) { final Iterator<Point2ifx> iterator = this.coords.iterator(); Point2ifx point = iterator.next(); for (int i = 0; i < n; i += 2) { if (!(transform == null || transform.isIdentity())) { transform.transform(point); } clone[i] = point.getX(); clone[i + 1] = point.getY(); point = iterator.next(); } } return clone; } @Override public Point2ifx[] toPointArray(Transform2D transform) { final int n = (this.coords != null) ? this.coords.size() : 0; final Point2ifx[] clone = new Point2ifx[n]; if (n > 0) { final Iterator<Point2ifx> iterator = this.coords.iterator(); Point2ifx point = iterator.next(); for (int i = 0; i < n; ++i) { if (!(transform == null || transform.isIdentity())) { transform.transform(point); } clone[i] = point; point = iterator.next(); } } return clone; } @Override public Point2ifx getPointAt(int index) { if (this.coords == null) { throw new IndexOutOfBoundsException(); } return this.coords.get(index); } @Override @Pure public int getCurrentX() { if (this.coords == null) { throw new IndexOutOfBoundsException(); } final int index = this.coords.size() - 1; return this.coords.get(index).ix(); } @Override @Pure public int getCurrentY() { if (this.coords == null) { throw new IndexOutOfBoundsException(); } final int index = this.coords.size() - 1; return this.coords.get(index).iy(); } @Override @Pure public Point2ifx getCurrentPoint() { if (this.coords == null) { throw new IndexOutOfBoundsException(); } final int baseIdx = this.coords.size() - 1; return this.coords.get(baseIdx); } @Override public int size() { return (this.coords == null) ? 0 : this.coords.size(); } @Override @SuppressWarnings("checkstyle:magicnumber") public void removeLast() { if (this.types != null && !this.types.isEmpty() && this.coords != null && !this.coords.isEmpty()) { final int lastIndex = this.types.size() - 1; final int coordSize = this.coords.size(); final int coordIndex; switch (this.types.get(lastIndex)) { case CLOSE: // no coord to remove coordIndex = coordSize; break; case MOVE_TO: coordIndex = coordSize - 1; break; case LINE_TO: coordIndex = coordSize - 1; break; case CURVE_TO: coordIndex = coordSize - 3; break; case QUAD_TO: coordIndex = coordSize - 2; break; case ARC_TO: default: throw new IllegalStateException(); } this.coords.remove(coordIndex, coordSize); this.types.remove(lastIndex); } else { throw new IllegalStateException(); } } @Override public void moveTo(int x, int y) { if (this.types != null && !this.types.isEmpty() && this.types.get(this.types.size() - 1) == PathElementType.MOVE_TO) { assert this.coords != null && !this.coords.isEmpty(); final int idx = this.coords.size() - 1; this.coords.set(idx, getGeomFactory().newPoint(x, y)); } else { innerTypesProperty().add(PathElementType.MOVE_TO); final ReadOnlyListWrapper<Point2ifx> coords = innerPointsProperty(); coords.add(getGeomFactory().newPoint(x, y)); } } @Override public void moveTo(Point2D<?, ?> position) { assert position != null : AssertMessages.notNullParameter(); if (this.types != null && !this.types.isEmpty() && this.types.get(this.types.size() - 1) == PathElementType.MOVE_TO) { assert this.coords != null && !this.coords.isEmpty(); final int idx = this.coords.size() - 1; this.coords.set(idx, getGeomFactory().convertToPoint(position)); } else { innerTypesProperty().add(PathElementType.MOVE_TO); final ReadOnlyListWrapper<Point2ifx> coords = innerPointsProperty(); coords.add(getGeomFactory().convertToPoint(position)); } } private void ensureMoveTo() { if (this.types == null || this.types.isEmpty()) { throw new IllegalStateException(Locale.getString("E1")); //$NON-NLS-1$ } } @Override public void lineTo(int x, int y) { ensureMoveTo(); innerTypesProperty().add(PathElementType.LINE_TO); final ReadOnlyListWrapper<Point2ifx> coords = innerPointsProperty(); coords.add(getGeomFactory().newPoint(x, y)); } @Override public void lineTo(Point2D<?, ?> to) { assert to != null : AssertMessages.notNullParameter(); ensureMoveTo(); innerTypesProperty().add(PathElementType.LINE_TO); final ReadOnlyListWrapper<Point2ifx> coords = innerPointsProperty(); coords.add(getGeomFactory().convertToPoint(to)); } @Override public void quadTo(int x1, int y1, int x2, int y2) { ensureMoveTo(); innerTypesProperty().add(PathElementType.QUAD_TO); final ReadOnlyListWrapper<Point2ifx> coords = innerPointsProperty(); coords.add(getGeomFactory().newPoint(x1, y1)); coords.add(getGeomFactory().newPoint(x2, y2)); } @Override public void quadTo(Point2D<?, ?> ctrl, Point2D<?, ?> to) { assert ctrl != null : AssertMessages.notNullParameter(0); assert to != null : AssertMessages.notNullParameter(2); ensureMoveTo(); innerTypesProperty().add(PathElementType.QUAD_TO); final ReadOnlyListWrapper<Point2ifx> coords = innerPointsProperty(); coords.add(getGeomFactory().convertToPoint(ctrl)); coords.add(getGeomFactory().convertToPoint(to)); } @Override public void curveTo(int x1, int y1, int x2, int y2, int x3, int y3) { ensureMoveTo(); innerTypesProperty().add(PathElementType.CURVE_TO); final ReadOnlyListWrapper<Point2ifx> coords = innerPointsProperty(); coords.add(getGeomFactory().newPoint(x1, y1)); coords.add(getGeomFactory().newPoint(x2, y2)); coords.add(getGeomFactory().newPoint(x3, y3)); } @Override public void curveTo(Point2D<?, ?> ctrl1, Point2D<?, ?> ctrl2, Point2D<?, ?> to) { assert ctrl1 != null : AssertMessages.notNullParameter(0); assert ctrl2 != null : AssertMessages.notNullParameter(1); assert to != null : AssertMessages.notNullParameter(2); ensureMoveTo(); innerTypesProperty().add(PathElementType.CURVE_TO); final ReadOnlyListWrapper<Point2ifx> coords = innerPointsProperty(); coords.add(getGeomFactory().convertToPoint(ctrl1)); coords.add(getGeomFactory().convertToPoint(ctrl2)); coords.add(getGeomFactory().convertToPoint(to)); } /** Replies the private coordinates property. * * @return the private coordinates property. */ protected ReadOnlyListWrapper<Point2ifx> innerPointsProperty() { if (this.coords == null) { this.coords = new ReadOnlyListWrapper<>(this, MathFXAttributeNames.COORDINATES, FXCollections.observableList(new ArrayList<>())); } return this.coords; } /** Replies the coordinates property. * * @return the coordinates property. */ public ReadOnlyListProperty<Point2ifx> coordinatesProperty() { return innerPointsProperty().getReadOnlyProperty(); } @Override public int getCoordAt(int index) { if (this.coords == null) { throw new IndexOutOfBoundsException(); } final Point2ifx point = this.coords.get(index / 2); return index % 2 == 0 ? point.ix() : point.iy(); } @Override public void setLastPoint(int x, int y) { if (this.coords != null && !this.coords.isEmpty()) { final int idx = this.coords.size() - 1; final Point2ifx point = this.coords.get(idx); point.setX(x); point.setY(y); } else { throw new IllegalStateException(); } } @Override public void setLastPoint(Point2D<?, ?> point) { if (this.coords != null && !this.coords.isEmpty()) { final int idx = this.coords.size() - 1; this.coords.get(idx).set(point.ix(), point.iy()); } else { throw new IllegalStateException(); } } @Override @SuppressWarnings({"checkstyle:magicnumber", "checkstyle:cyclomaticcomplexity"}) public boolean remove(int x, int y) { if (this.types != null && !this.types.isEmpty() && this.coords != null && !this.coords.isEmpty()) { for (int i = 0, j = 0; i < this.coords.size() && j < this.types.size(); j++) { final Point2ifx point = this.coords.get(i); switch (this.types.get(j)) { case MOVE_TO: //$FALL-THROUGH$ case LINE_TO: if (x == point.ix() && y == point.iy()) { this.coords.remove(i); this.types.remove(j); return true; } i++; break; case CURVE_TO: final Point2ifx p2 = this.coords.get(i + 1); final Point2ifx p3 = this.coords.get(i + 2); if ((x == point.ix() && y == point.iy()) || (x == p2.ix() && y == p2.iy()) || (x == p3.ix() && y == p3.iy())) { this.coords.remove(i, i + 3); this.types.remove(j); return true; } i += 3; break; case QUAD_TO: final Point2ifx pt = this.coords.get(i + 1); if ((x == point.ix() && y == point.iy()) || (x == pt.ix() && y == pt.iy())) { this.coords.remove(i, i + 2); this.types.remove(j); return true; } i += 2; break; case CLOSE: break; case ARC_TO: throw new IllegalStateException(); default: break; } } } return false; } /** Remove the point from this path. * * <p>If the given point do not match exactly a point in the path, nothing is removed. * * @param point the point to remove. * @return <code>true</code> if the point was removed; <code>false</code> otherwise. */ @SuppressWarnings({"checkstyle:magicnumber", "checkstyle:cyclomaticcomplexity"}) public boolean remove(Point2D<?, ?> point) { if (this.types != null && !this.types.isEmpty() && this.coords != null && !this.coords.isEmpty()) { for (int i = 0, j = 0; i < this.coords.size() && j < this.types.size(); j++) { final Point2ifx currentPoint = this.coords.get(i); switch (this.types.get(j)) { case MOVE_TO: //$FALL-THROUGH$ case LINE_TO: if (point.equals(currentPoint)) { this.coords.remove(i); this.types.remove(j); return true; } i++; break; case CURVE_TO: final Point2ifx p2 = this.coords.get(i + 1); final Point2ifx p3 = this.coords.get(i + 2); if ((point.equals(currentPoint)) || (point.equals(p2)) || (point.equals(p3))) { this.coords.remove(i, i + 3); this.types.remove(j); return true; } i = i + 3; break; case QUAD_TO: final Point2ifx pt = this.coords.get(i + 1); if ((point.equals(currentPoint)) || (point.equals(pt))) { this.coords.remove(i, i + 2); this.types.remove(j); return true; } i = i + 2; break; case CLOSE: break; case ARC_TO: throw new IllegalStateException(); default: break; } } } return false; } @Override public void set(Path2ifx path) { assert path != null : AssertMessages.notNullParameter(); clear(); add(path.getPathIterator()); } /** Replies the private types property. * * @return the private types property. */ protected ReadOnlyListWrapper<PathElementType> innerTypesProperty() { if (this.types == null) { this.types = new ReadOnlyListWrapper<>(this, MathFXAttributeNames.TYPES, FXCollections.observableList(new ArrayList<>())); } return this.types; } /** Replies the types property. * * @return the types property. */ public ReadOnlyListProperty<PathElementType> typesProperty() { return innerTypesProperty().getReadOnlyProperty(); } @Override public int getPathElementCount() { return this.types == null ? 0 : innerTypesProperty().size(); } @Override public PathElementType getPathElementTypeAt(int index) { if (this.types == null) { throw new IndexOutOfBoundsException(); } return this.types.get(index); } }