/*
* Copyright (C) 2010 Brockmann Consult GmbH (info@brockmann-consult.de)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 3 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 General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, see http://www.gnu.org/licenses/
*/
package com.bc.ceres.swing.figure;
import com.bc.ceres.core.Assert;
import com.bc.ceres.grender.Rendering;
import com.bc.ceres.grender.Viewport;
import com.bc.ceres.swing.figure.support.VertexHandle;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Arrays;
/**
* A figure that is based on a Java AWT shape geometry.
* <p/>
* Sub-classes have to provide the actual shape (lines or areas) in model coordinates.
*
* @author Norman Fomferra
* @since Ceres 0.10
*/
public abstract class AbstractShapeFigure extends AbstractFigure implements ShapeFigure {
private static final double SELECTION_TOLERANCE = 12.0;
private Rank rank;
protected AbstractShapeFigure() {
}
/**
* Constructor.
*
* @param rank The rank, must be either {@link Rank#AREA} or {@link Rank#LINE}.
* @param normalStyle The style used for the "normal" state of the figure.
* @param selectedStyle The style used for the "selected" state of the figure.
*/
protected AbstractShapeFigure(Rank rank, FigureStyle normalStyle, FigureStyle selectedStyle) {
super(normalStyle, selectedStyle);
Assert.notNull(rank, "rank");
this.rank = rank;
setSelectable(true);
}
@Override
public boolean isCollection() {
return false;
}
@Override
public Rank getRank() {
return rank;
}
protected void setRank(Rank rank) {
this.rank = rank;
}
@Override
public Rectangle2D getBounds() {
return getShape().getBounds2D();
}
@Override
public void draw(Rendering rendering) {
Shape shape = getShape();
if (shape == null) {
return;
}
Viewport vp = rendering.getViewport();
Rectangle2D vbounds = vp.getViewBounds();
Rectangle2D mbounds = vp.getViewToModelTransform().createTransformedShape(vbounds).getBounds2D();
if (!getBounds().intersects(mbounds)) {
return;
}
Graphics2D g = rendering.getGraphics();
AffineTransform oldTransform = g.getTransform();
try {
g.transform(vp.getModelToViewTransform());
drawShape(rendering);
} finally {
g.setTransform(oldTransform);
}
}
/**
* Draws the {@link #getShape() shape} and other items that are used to graphically
* represent the figure, for example labels.
* For convenience, the rendering's drawing context is pre-transformed,
* so that drawing of the shape can be performed in model coordinates.
*
* @param rendering The rendering.
*
*/
protected void drawShape(Rendering rendering) {
final Viewport vp = rendering.getViewport();
final Graphics2D g = rendering.getGraphics();
final Shape shape = getShape();
if (rank == Rank.AREA) {
Paint fillPaint = getNormalStyle().getFillPaint();
if (fillPaint != null) {
g.setPaint(fillPaint);
g.fill(shape);
}
}
Paint strokePaint = getNormalStyle().getStrokePaint();
if (strokePaint != null) {
Stroke normalStroke = getNormalStyle().getStroke(1.0 / vp.getZoomFactor());
g.setPaint(strokePaint);
g.setStroke(normalStroke);
g.draw(shape);
}
if (isSelected()) {
Paint selectedStrokePaint = getSelectedStyle().getStrokePaint();
if (selectedStrokePaint != null) {
Stroke selectedStroke = getSelectedStyle().getStroke(1.0 / vp.getZoomFactor());
g.setStroke(selectedStroke);
g.setPaint(selectedStrokePaint);
g.draw(shape);
}
}
}
@Override
public boolean isCloseTo(Point2D point, AffineTransform m2v) {
if (getRank() == Rank.AREA) {
return getShape().contains(point);
} else {
try {
Point2D viewPoint = m2v.transform(point, null);
double x = viewPoint.getX() - SELECTION_TOLERANCE / 2;
double y = viewPoint.getY() - SELECTION_TOLERANCE / 2;
double w = SELECTION_TOLERANCE;
double h = SELECTION_TOLERANCE;
Rectangle2D.Double aDouble = new Rectangle2D.Double(x, y, w, h);
Rectangle2D rectangle2D = m2v.createInverse().createTransformedShape(aDouble).getBounds2D();
return getShape().intersects(rectangle2D);
} catch (NoninvertibleTransformException e) {
return false;
}
}
}
@Override
public void scale(Point2D refPoint, double sx, double sy) {
final double x0 = refPoint.getX();
final double y0 = refPoint.getY();
final PathIterator pathIterator = getShape().getPathIterator(null);
final Path2D.Double path = new Path2D.Double(pathIterator.getWindingRule());
final double[] seg = new double[6];
while (!pathIterator.isDone()) {
final int type = pathIterator.currentSegment(seg);
if (type == PathIterator.SEG_MOVETO) {
seg[0] = x0 + (seg[0] - x0) * sx;
seg[1] = y0 + (seg[1] - y0) * sy;
path.moveTo(seg[0], seg[1]);
} else if (type == PathIterator.SEG_LINETO) {
seg[0] = x0 + (seg[0] - x0) * sx;
seg[1] = y0 + (seg[1] - y0) * sy;
path.lineTo(seg[0], seg[1]);
} else if (type == PathIterator.SEG_QUADTO) {
seg[0] = x0 + (seg[0] - x0) * sx;
seg[1] = y0 + (seg[1] - y0) * sy;
seg[2] = x0 + (seg[2] - x0) * sx;
seg[3] = y0 + (seg[3] - y0) * sy;
path.quadTo(seg[0], seg[1], seg[2], seg[3]);
} else if (type == PathIterator.SEG_CUBICTO) {
seg[0] = x0 + (seg[0] - x0) * sx;
seg[1] = y0 + (seg[1] - y0) * sy;
seg[2] = x0 + (seg[2] - x0) * sx;
seg[3] = y0 + (seg[3] - y0) * sy;
seg[4] = x0 + (seg[4] - x0) * sx;
seg[5] = y0 + (seg[5] - y0) * sy;
path.curveTo(seg[0], seg[1], seg[2], seg[3], seg[4], seg[5]);
} else if (type == PathIterator.SEG_CLOSE) {
path.closePath();
}
pathIterator.next();
}
setShape(path);
}
@Override
public void move(double dx, double dy) {
final PathIterator pathIterator = getShape().getPathIterator(null);
final Path2D.Double path = new Path2D.Double(pathIterator.getWindingRule());
final double[] seg = new double[6];
while (!pathIterator.isDone()) {
final int type = pathIterator.currentSegment(seg);
if (type == PathIterator.SEG_MOVETO) {
seg[0] += dx;
seg[1] += dy;
path.moveTo(seg[0], seg[1]);
} else if (type == PathIterator.SEG_LINETO) {
seg[0] += dx;
seg[1] += dy;
path.lineTo(seg[0], seg[1]);
} else if (type == PathIterator.SEG_QUADTO) {
seg[0] += dx;
seg[1] += dy;
seg[2] += dx;
seg[3] += dy;
path.quadTo(seg[0], seg[1], seg[2], seg[3]);
} else if (type == PathIterator.SEG_CUBICTO) {
seg[0] += dx;
seg[1] += dy;
seg[2] += dx;
seg[3] += dy;
seg[4] += dx;
seg[5] += dy;
path.curveTo(seg[0], seg[1], seg[2], seg[3], seg[4], seg[5]);
} else if (type == PathIterator.SEG_CLOSE) {
path.closePath();
}
pathIterator.next();
}
setShape(path);
}
@Override
public void rotate(Point2D point, double theta) {
final AffineTransform transform = new AffineTransform();
transform.rotate(theta, point.getX(), point.getY());
final PathIterator pathIterator = getShape().getPathIterator(transform);
final Path2D.Double path = new Path2D.Double(pathIterator.getWindingRule());
final double[] seg = new double[6];
while (!pathIterator.isDone()) {
final int type = pathIterator.currentSegment(seg);
if (type == PathIterator.SEG_MOVETO) {
path.moveTo(seg[0], seg[1]);
} else if (type == PathIterator.SEG_LINETO) {
path.lineTo(seg[0], seg[1]);
} else if (type == PathIterator.SEG_QUADTO) {
path.quadTo(seg[0], seg[1], seg[2], seg[3]);
} else if (type == PathIterator.SEG_CUBICTO) {
path.curveTo(seg[0], seg[1], seg[2], seg[3], seg[4], seg[5]);
} else if (type == PathIterator.SEG_CLOSE) {
path.closePath();
}
pathIterator.next();
}
setShape(path);
}
@Override
public double[] getSegment(int index) {
final PathIterator pathIterator = getShape().getPathIterator(null);
int i = 0;
while (!pathIterator.isDone()) {
if (i == index) {
final double[] seg = new double[6];
pathIterator.currentSegment(seg);
return seg;
}
pathIterator.next();
i++;
}
return null;
}
@Override
public void setSegment(int index, double[] newSeg) {
final Path2D.Double path = new Path2D.Double();
final PathIterator pathIterator = getShape().getPathIterator(null);
double[] changedSeg = new double[6];
Arrays.fill(changedSeg, Double.NaN);
final double[] seg0 = new double[6];
int i = 0;
while (!pathIterator.isDone()) {
final int type = pathIterator.currentSegment(seg0);
double[] seg = seg0;
if (i == index) {
changedSeg = seg.clone();
seg = newSeg;
}
if (type == PathIterator.SEG_MOVETO) {
path.moveTo(seg[0], seg[1]);
} else if (type == PathIterator.SEG_LINETO) {
if (seg[0] == changedSeg[0] && seg[1] == changedSeg[1]) {
path.lineTo(newSeg[0], newSeg[1]);
} else {
path.lineTo(seg[0], seg[1]);
}
} else if (type == PathIterator.SEG_QUADTO) {
path.quadTo(seg[0], seg[1], seg[2], seg[3]);
} else if (type == PathIterator.SEG_CUBICTO) {
path.curveTo(seg[0], seg[1], seg[2], seg[3], seg[4], seg[5]);
} else if (type == PathIterator.SEG_CLOSE) {
path.closePath();
}
pathIterator.next();
i++;
}
setShape(path);
}
@Override
public void addSegment(int index, double[] segment) {
final Path2D.Double path = new Path2D.Double();
final PathIterator pathIterator = getShape().getPathIterator(null);
final double[] seg = new double[6];
int i = 0;
boolean moveToSeen = false;
while (!pathIterator.isDone()) {
final int type = pathIterator.currentSegment(seg);
if (i == index) {
if (i == 0) {
path.moveTo(segment[0], segment[1]);
moveToSeen = true;
} else {
path.lineTo(segment[0], segment[1]);
}
}
if (type == PathIterator.SEG_MOVETO) {
if (moveToSeen) {
path.lineTo(seg[0], seg[1]);
} else {
path.moveTo(seg[0], seg[1]);
}
moveToSeen = true;
} else if (type == PathIterator.SEG_LINETO) {
path.lineTo(seg[0], seg[1]);
} else if (type == PathIterator.SEG_QUADTO) {
path.quadTo(seg[0], seg[1], seg[2], seg[3]);
} else if (type == PathIterator.SEG_CUBICTO) {
path.curveTo(seg[0], seg[1], seg[2], seg[3], seg[4], seg[5]);
} else if (type == PathIterator.SEG_CLOSE) {
path.closePath();
}
pathIterator.next();
i++;
}
setShape(path);
}
@Override
public void removeSegment(int index) {
final Path2D.Double path = new Path2D.Double();
final PathIterator pathIterator = getShape().getPathIterator(null);
final double[] seg = new double[6];
int i = 0;
boolean moveToSeen = false;
while (!pathIterator.isDone()) {
final int type = pathIterator.currentSegment(seg);
if (i != index) {
if (type == PathIterator.SEG_MOVETO) {
moveToSeen = true;
path.moveTo(seg[0], seg[1]);
} else if (type == PathIterator.SEG_LINETO) {
if (moveToSeen) {
path.lineTo(seg[0], seg[1]);
} else {
path.moveTo(seg[0], seg[1]);
}
} else if (type == PathIterator.SEG_QUADTO) {
if (moveToSeen) {
path.quadTo(seg[0], seg[1], seg[2], seg[3]);
} else {
path.moveTo(seg[0], seg[1]);
}
} else if (type == PathIterator.SEG_CUBICTO) {
if (moveToSeen) {
path.curveTo(seg[0], seg[1], seg[2], seg[3], seg[4], seg[5]);
} else {
path.moveTo(seg[0], seg[1]);
}
} else if (type == PathIterator.SEG_CLOSE) {
path.closePath();
}
}
pathIterator.next();
i++;
}
setShape(path);
}
@Override
public Object createMemento() {
return getShape();
}
@Override
public void setMemento(Object memento) {
setShape((Shape) memento);
}
@Override
public int getMaxSelectionStage() {
return 4;
}
@Override
public Handle[] createHandles(int selectionStage) {
// No handles at level 1, only high-lighting, see draw() & isSelected()
if (selectionStage == 2) {
return createVertexHandles();
} else if (selectionStage == 3) {
return createScaleHandles(0.0);
} else if (selectionStage == 4) {
Handle[] vertexHandles = createVertexHandles();
Handle[] scaleHandles = createScaleHandles(8.0);
ArrayList<Handle> handles = new ArrayList<Handle>(vertexHandles.length + scaleHandles.length);
handles.addAll(Arrays.asList(vertexHandles));
handles.addAll(Arrays.asList(scaleHandles));
return handles.toArray(new Handle[handles.size()]);
}
return NO_HANDLES;
}
private Handle[] createVertexHandles() {
FigureStyle handleStyle = getHandleStyle();
FigureStyle selectedHandleStyle = getSelectedHandleStyle();
ArrayList<Handle> handleList = new ArrayList<Handle>();
PathIterator pathIterator = getShape().getPathIterator(null);
double[] firstSeg = new double[6];
Arrays.fill(firstSeg, Double.NaN);
int segmentIndex = 0;
while (!pathIterator.isDone()) {
final double[] seg = new double[6];
final int type = pathIterator.currentSegment(seg);
final boolean isEqualToFirst = seg[0] == firstSeg[0] && seg[1] == firstSeg[1];
if (type != PathIterator.SEG_CLOSE && !isEqualToFirst) {
handleList.add(new VertexHandle(this, segmentIndex, handleStyle, selectedHandleStyle));
}
if (segmentIndex == 0) {
firstSeg = seg;
}
pathIterator.next();
segmentIndex++;
}
return handleList.toArray(new Handle[handleList.size()]);
}
}