package org.archstudio.bna.logics.editing;
import static org.archstudio.sysutils.SystemUtils.castOrNull;
import java.awt.Dimension;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.archstudio.bna.BNAModelEvent;
import org.archstudio.bna.IBNAView;
import org.archstudio.bna.IBNAWorld;
import org.archstudio.bna.ICoordinate;
import org.archstudio.bna.ICoordinateMapper;
import org.archstudio.bna.IThing;
import org.archstudio.bna.IThingPeer;
import org.archstudio.bna.ThingEvent;
import org.archstudio.bna.constants.MouseType;
import org.archstudio.bna.constants.StickyMode;
import org.archstudio.bna.facets.IHasAnchorPoint;
import org.archstudio.bna.facets.IHasEndpoints;
import org.archstudio.bna.facets.IHasMidpoints;
import org.archstudio.bna.facets.IHasMutableEndpoints;
import org.archstudio.bna.facets.IHasMutableMidpoints;
import org.archstudio.bna.facets.IHasStandardCursor;
import org.archstudio.bna.facets.IHasStickyShape;
import org.archstudio.bna.keys.IThingKey;
import org.archstudio.bna.keys.IThingRefKey;
import org.archstudio.bna.keys.ThingKey;
import org.archstudio.bna.keys.ThingRefKey;
import org.archstudio.bna.logics.coordinating.DynamicStickPointLogic;
import org.archstudio.bna.logics.coordinating.MirrorValueLogic;
import org.archstudio.bna.logics.coordinating.StickPointLogic;
import org.archstudio.bna.logics.events.DragMoveEvent;
import org.archstudio.bna.things.shapes.LocalShapeThing;
import org.archstudio.bna.things.shapes.ReshapeHandleThing;
import org.archstudio.bna.ui.IBNAMouseClickListener2;
import org.archstudio.bna.utils.Assemblies;
import org.archstudio.bna.utils.BNAUtils;
import org.archstudio.bna.utils.BNAUtils2.ThingsAtLocation;
import org.archstudio.bna.utils.DefaultCoordinate;
import org.archstudio.bna.utils.ShapeUtils;
import org.archstudio.bna.utils.UserEditableUtils;
import org.archstudio.sysutils.SystemUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
public class ReshapeSplineLogic extends AbstractReshapeLogic<IThing, Integer> implements IBNAMouseClickListener2 {
public static final int SELECT_DIST = 8;
private static IThingKey<IThingKey<Point2D>> POINT_KEY_KEY = ThingKey.create("pointKey");
private static IThingKey<IThingKey<List<Point2D>>> POINTS_KEY_KEY = ThingKey.create("pointsKey");
private static IThingKey<Integer> POINTS_INDEX_KEY = ThingKey.create("index");
private static IThingKey<Boolean> REMOVE_KEY = ThingKey.create("remove");
private static IThingRefKey<LocalShapeThing> REMOVE_PART_KEY = ThingRefKey.create("remove-part");
private static final int ADD_DIST = 8;
private static final int MERGE_DIST = 8;
private static final int STICK_DIST = 8;
private static final int UNSTICK_DIST = 8;
protected final DynamicStickPointLogic dynamicStickLogic;
protected final StickPointLogic stickLogic;
protected final MirrorValueLogic mirrorLogic;
private final List<IReshapeSplineGuide> reshapeSplineGuides = Lists.newArrayList();
public ReshapeSplineLogic(IBNAWorld world) {
super(world, IThing.class);
dynamicStickLogic = logics.addThingLogic(DynamicStickPointLogic.class);
stickLogic = logics.addThingLogic(StickPointLogic.class);
mirrorLogic = logics.addThingLogic(MirrorValueLogic.class);
logics.addThingLogic(StandardCursorLogic.class);
}
public void addReshapeSplineGuides(IReshapeSplineGuide... guides) {
BNAUtils.checkLock();
reshapeSplineGuides.addAll(Arrays.asList(guides));
}
@SuppressWarnings("unchecked")
private void augmentHandle(ReshapeHandleThing handle, IThingKey<?> key, int index) {
if (index == -1) {
handle.set(POINT_KEY_KEY, (IThingKey<Point2D>) key);
}
else {
handle.set(POINTS_KEY_KEY, (IThingKey<List<Point2D>>) key);
handle.set(POINTS_INDEX_KEY, index);
}
handle.set(REMOVE_KEY, false);
}
@SuppressWarnings("unchecked")
@Override
public void bnaModelChanged(BNAModelEvent evt) {
BNAUtils.checkLock();
super.bnaModelChanged(evt);
ThingEvent thingEvent = evt.getThingEvent();
if (thingEvent != null) {
if (IHasMidpoints.MIDPOINTS_KEY.equals(thingEvent.getPropertyName())) {
int oldSize = ((Collection<Point>) thingEvent.getOldPropertyValue()).size();
int newSize = ((Collection<Point>) thingEvent.getNewPropertyValue()).size();
if (oldSize != newSize) {
resetHandles();
}
}
}
}
@Override
protected void addHandles(IThing reshapingThing) {
int index = 0;
if (reshapingThing instanceof IHasEndpoints) {
if (UserEditableUtils.isEditableForAnyQualities(reshapingThing,
IHasMutableEndpoints.USER_MAY_MOVE_ENDPOINT_2, IHasMutableEndpoints.USER_MAY_RESTICK_ENDPOINT_2)) {
augmentHandle(addHandle(reshapingThing, Assemblies.createHandle(world, null, null), index++),
IHasEndpoints.ENDPOINT_2_KEY, -1);
}
}
if (reshapingThing instanceof IHasMidpoints) {
if (UserEditableUtils.isEditableForAnyQualities(reshapingThing,
IHasMutableMidpoints.USER_MAY_MOVE_MIDPOINTS, IHasMutableMidpoints.USER_MAY_REMOVE_MIDPOINTS)) {
List<Point2D> midpoints = ((IHasMidpoints) reshapingThing).getMidpoints();
for (int midpointIndex = 0; midpointIndex < midpoints.size(); midpointIndex++) {
augmentHandle(addHandle(reshapingThing, Assemblies.createHandle(world, null, null), index++),
IHasMidpoints.MIDPOINTS_KEY, midpointIndex);
}
}
}
// cover this endpoint last so that it is the topmost handle
if (reshapingThing instanceof IHasEndpoints) {
if (UserEditableUtils.isEditableForAnyQualities(reshapingThing,
IHasMutableEndpoints.USER_MAY_MOVE_ENDPOINT_1, IHasMutableEndpoints.USER_MAY_RESTICK_ENDPOINT_1)) {
augmentHandle(addHandle(reshapingThing, Assemblies.createHandle(world, null, null), index++),
IHasEndpoints.ENDPOINT_1_KEY, -1);
}
}
}
@Override
protected Runnable takeSnapshot(IThing reshapingThing) {
final Object reshapingThingID = reshapingThing.getID();
final Point2D endpoint1 = reshapingThing.get(IHasEndpoints.ENDPOINT_1_KEY);
final StickyMode stickToThingMode1 =
reshapingThing.get(dynamicStickLogic.getStickyModeKey(IHasEndpoints.ENDPOINT_1_KEY));
final Object stickToThingID1 =
reshapingThing.get(dynamicStickLogic.getStickyThingKey(IHasEndpoints.ENDPOINT_1_KEY));
final List<Point2D> midpoints = reshapingThing.get(IHasMidpoints.MIDPOINTS_KEY);
final Point2D endpoint2 = reshapingThing.get(IHasEndpoints.ENDPOINT_2_KEY);
final StickyMode stickToThingMode2 =
reshapingThing.get(dynamicStickLogic.getStickyModeKey(IHasEndpoints.ENDPOINT_2_KEY));
final Object stickToThingID2 =
reshapingThing.get(dynamicStickLogic.getStickyThingKey(IHasEndpoints.ENDPOINT_2_KEY));
return new Runnable() {
@Override
public void run() {
IThing t = SystemUtils.castOrNull(model.getThing(reshapingThingID), IThing.class);
if (t != null) {
if (stickToThingID1 != null && stickToThingMode1 != null) {
t.set(dynamicStickLogic.getStickyModeKey(IHasEndpoints.ENDPOINT_1_KEY), stickToThingMode1);
t.set(dynamicStickLogic.getStickyThingKey(IHasEndpoints.ENDPOINT_1_KEY), stickToThingID1);
}
else {
t.remove(dynamicStickLogic.getStickyModeKey(IHasEndpoints.ENDPOINT_1_KEY));
t.remove(dynamicStickLogic.getStickyThingKey(IHasEndpoints.ENDPOINT_1_KEY));
}
if (endpoint1 != null) {
t.set(IHasEndpoints.ENDPOINT_1_KEY, endpoint1);
}
if (midpoints != null) {
t.set(IHasMidpoints.MIDPOINTS_KEY, midpoints);
}
if (stickToThingID2 != null && stickToThingMode2 != null) {
t.set(dynamicStickLogic.getStickyModeKey(IHasEndpoints.ENDPOINT_2_KEY), stickToThingMode2);
t.set(dynamicStickLogic.getStickyThingKey(IHasEndpoints.ENDPOINT_2_KEY), stickToThingID2);
}
else {
t.remove(dynamicStickLogic.getStickyModeKey(IHasEndpoints.ENDPOINT_2_KEY));
t.remove(dynamicStickLogic.getStickyThingKey(IHasEndpoints.ENDPOINT_2_KEY));
}
if (endpoint2 != null) {
t.set(IHasEndpoints.ENDPOINT_2_KEY, endpoint2);
}
}
}
};
}
@Override
protected void updateHandle(IThing reshapingThing, ReshapeHandleThing handle, Integer data) {
Point2D point = handle.getAnchorPoint();
if (handle.get(POINT_KEY_KEY) != null) {
IThingKey<Point2D> pointKey = handle.get(POINT_KEY_KEY);
point = stickLogic.getStuckPoint(reshapingThing, pointKey);
}
if (handle.get(POINTS_KEY_KEY) != null) {
IThingKey<List<Point2D>> pointsKey = handle.get(POINTS_KEY_KEY);
int index = handle.get(POINTS_INDEX_KEY);
List<Point2D> points = reshapingThing.get(pointsKey);
if (index < points.size()) {
point = points.get(index);
}
}
handle.setAnchorPoint(point);
handle.set(IHasStandardCursor.STANDARD_CURSOR_KEY, SWT.CURSOR_SIZEALL);
boolean shouldBeStuck = false;
boolean isStuck = false;
IThingKey<Point2D> pointKey = handle.get(POINT_KEY_KEY);
if (pointKey != null) {
for (IReshapeSplineGuide guide : reshapeSplineGuides) {
shouldBeStuck |= guide.shouldBeStuck(reshapingThing, pointKey);
}
isStuck |= stickLogic.getStickyThingID(reshapingThing, pointKey) != null;
}
handle.setColor(shouldBeStuck ? isStuck ? ReshapeHandleThing.STUCK_COLOR : ReshapeHandleThing.UNSTUCK_COLOR
: ReshapeHandleThing.NORMAL_COLOR);
if (handle.has(REMOVE_KEY, true)) {
if (Assemblies.getPart(model, handle, REMOVE_PART_KEY) == null) {
LocalShapeThing removalIndicatorThing = model.addThing(new LocalShapeThing(null));
removalIndicatorThing.setShape(ShapeUtils.createUnitX(0.35, 0.1, 0));
removalIndicatorThing.setColor(new RGB(255, 0, 0));
removalIndicatorThing.setEdgeColor(new RGB(0, 0, 0));
removalIndicatorThing.setSize(new Dimension(20, 20));
mirrorLogic.mirrorValue(handle, IHasAnchorPoint.ANCHOR_POINT_KEY, removalIndicatorThing,
IHasAnchorPoint.ANCHOR_POINT_KEY);
Assemblies.markPart(handle, REMOVE_PART_KEY, removalIndicatorThing);
}
}
else {
Assemblies.removeRootAndParts(model, REMOVE_PART_KEY.get(handle, model));
}
}
@Override
protected void handleMoved(IThing reshapingThing, ReshapeHandleThing handle, Integer data, DragMoveEvent evt) {
Point2D mp = BNAUtils.toPoint2D(evt.getMouseLocation().getWorldPoint());
Point2D amp = BNAUtils.toPoint2D(evt.getAdjustedMouseLocation().getWorldPoint());
ICoordinateMapper cm = evt.getView().getCoordinateMapper();
if (handle.get(POINT_KEY_KEY) != null) {
IThingKey<Point2D> pointKey = handle.get(POINT_KEY_KEY);
// an explicit point is being moved
StickyMode stickToThingMode = reshapingThing.get(dynamicStickLogic.getStickyModeKey(pointKey));
Object stickToThingID = reshapingThing.get(dynamicStickLogic.getStickyThingKey(pointKey));
// if pulled far away from a stuck point, unstick it
if (BNAUtils.getDistance(reshapingThing.get(pointKey), amp) >= UNSTICK_DIST) {
stickToThingID = null;
}
// if moved close to a sticky thing, stick to it
for (IHasStickyShape stickyThing : Iterables.filter(Lists.reverse(model.getAllThings()),
IHasStickyShape.class)) {
if (stickyThing instanceof ReshapeHandleThing) {
continue;
}
for (IReshapeSplineGuide guide : reshapeSplineGuides) {
StickyMode stickyMode = guide.getStickyMode(reshapingThing, stickyThing, pointKey);
if (stickyMode != null) {
Point2D stuckPoint =
BNAUtils.getClosestPointOnShape(stickyThing.getStickyShape(), mp.getX(), mp.getY());
IBNAView view = evt.getView();
IThingPeer<?> peer = view.getThingPeer(stickyThing);
if (stuckPoint != null && (BNAUtils.getDistance(stuckPoint, mp) <= STICK_DIST
|| peer.isInThing(DefaultCoordinate.forWorld(BNAUtils.toPoint(mp), cm))
|| BNAUtils.getDistance(stuckPoint, amp) <= STICK_DIST
|| peer.isInThing(DefaultCoordinate.forWorld(BNAUtils.toPoint(mp), cm)))) {
stickToThingID = stickyThing.getID();
stickToThingMode = stickyMode;
}
}
}
}
// update the stuck thing
reshapingThing.set(dynamicStickLogic.getStickyModeKey(pointKey), stickToThingMode);
reshapingThing.set(dynamicStickLogic.getStickyThingKey(pointKey), stickToThingID);
// update the point if not stuck
if (stickToThingID == null || stickToThingMode == null) {
reshapingThing.set(pointKey, amp);
}
}
if (handle.get(POINTS_KEY_KEY) != null) {
IThingKey<List<Point2D>> pointsKey = handle.get(POINTS_KEY_KEY);
int index = handle.get(POINTS_INDEX_KEY);
// remove point if it is dragged close to its neighbors
if (IHasMidpoints.MIDPOINTS_KEY.equals(pointsKey) && UserEditableUtils
.isEditableForAnyQualities(reshapingThing, IHasMutableMidpoints.USER_MAY_REMOVE_MIDPOINTS)) {
List<Point2D> points = reshapingThing.get(pointsKey);
if (index < points.size()) {
// midpoint being tested for removal
Point2D lmp = cm.worldToLocal(mp);
// next point on one side
Point2D p1 = null;
if (index > 0) {
p1 = points.get(index - 1);
}
else {
if (reshapingThing instanceof IHasEndpoints) {
p1 = stickLogic.getStuckPoint(reshapingThing, IHasEndpoints.ENDPOINT_1_KEY);
}
}
Point2D lp1 = p1 != null ? cm.worldToLocal(p1) : null;
// next point on other side
Point2D p2 = null;
if (index < points.size() - 1) {
p2 = points.get(index + 1);
}
else {
if (reshapingThing instanceof IHasEndpoints) {
p2 = stickLogic.getStuckPoint(reshapingThing, IHasEndpoints.ENDPOINT_2_KEY);
}
}
Point2D lp2 = p2 != null ? cm.worldToLocal(p2) : null;
// if it's close enough, mark it to be removed
boolean remove = false;
if (lp1 != null && (BNAUtils.getDistance(lmp, lp1) <= MERGE_DIST || amp.equals(p1))) {
remove = true;
amp = p1;
}
if (lp2 != null && (BNAUtils.getDistance(lmp, lp2) <= MERGE_DIST || amp.equals(p2))) {
remove = true;
amp = p2;
}
handle.set(REMOVE_KEY, remove);
}
}
List<Point2D> points = reshapingThing.get(pointsKey);
if (index < points.size()) {
points.set(index, amp);
reshapingThing.set(pointsKey, points);
}
}
}
@Override
protected void handleMoveFinished(IThing reshapingThing, ReshapeHandleThing handle, Integer data,
DragMoveEvent evt) {
if (handle.get(POINT_KEY_KEY) != null) {
IThingKey<Point2D> pointKey = handle.get(POINT_KEY_KEY);
if (handle.get(REMOVE_KEY, false)) {
reshapingThing.remove(pointKey);
}
}
if (handle.get(POINTS_KEY_KEY) != null) {
IThingKey<List<Point2D>> pointsKey = handle.get(POINTS_KEY_KEY);
int index = handle.get(POINTS_INDEX_KEY);
if (handle.get(REMOVE_KEY, false)) {
List<Point2D> points = reshapingThing.get(pointsKey);
if (index < points.size()) {
points.remove(index);
reshapingThing.set(pointsKey, points);
}
}
}
super.handleMoveFinished(reshapingThing, handle, data, evt);
}
@Override
public void mouseDown(IBNAView view, MouseType type, MouseEvent evt, ICoordinate location,
ThingsAtLocation thingsAtLocation) {
}
@Override
public void mouseUp(IBNAView view, MouseType type, MouseEvent evt, ICoordinate location,
ThingsAtLocation thingsAtLocation) {
}
@Override
public void mouseClick(IBNAView view, MouseType type, MouseEvent evt, ICoordinate location,
ThingsAtLocation thingsAtLocation) {
BNAUtils.checkLock();
if (evt.count == 2 && thingsAtLocation.getThingAtLocation() != null) {
final IHasMutableMidpoints t =
castOrNull(thingsAtLocation.getThingAtLocation().getThing(), IHasMutableMidpoints.class);
if (t != null
&& UserEditableUtils.isEditableForAllQualities(t, IHasMutableMidpoints.USER_MAY_ADD_MIDPOINTS)) {
final List<Point2D> oldPoints = t.getMidpoints();
if (t instanceof IHasEndpoints) {
oldPoints.add(0, ((IHasEndpoints) t).getEndpoint1());
oldPoints.add(((IHasEndpoints) t).getEndpoint2());
}
final List<Point2D> newPoints = Lists.newArrayList(oldPoints);
// insert the new point
boolean pointAdded = false;
Point worldPoint = view.getCoordinateMapper().localToWorld(new Point(evt.x, evt.y));
for (int i = 1; i < newPoints.size(); i++) {
Point2D p1 = newPoints.get(i - 1);
Point2D p2 = newPoints.get(i);
double dist =
Line2D.ptSegDist(p2.getX(), p2.getY(), p1.getX(), p1.getY(), worldPoint.x, worldPoint.y);
if (dist <= ADD_DIST) {
pointAdded = true;
Point2D newPoint =
BNAUtils.getClosestPointOnShape(new Line2D.Double(p1, p2), worldPoint.x, worldPoint.y);
newPoints.add(i, newPoint);
break;
}
}
// if a point wasn't added, do so now
if (!pointAdded) {
newPoints.add(new Point2D.Double(worldPoint.x, worldPoint.y));
}
BNAOperations.runnable("Reshape", new Runnable() {
@Override
public void run() {
if (t instanceof IHasMutableEndpoints) {
((IHasMutableEndpoints) t).setEndpoint1(oldPoints.get(0));
((IHasMutableEndpoints) t).setEndpoint2(oldPoints.get(oldPoints.size() - 1));
t.setMidpoints(oldPoints.subList(1, oldPoints.size() - 1));
}
else {
t.setMidpoints(oldPoints);
}
}
}, new Runnable() {
@Override
public void run() {
if (t instanceof IHasMutableEndpoints) {
((IHasMutableEndpoints) t).setEndpoint1(newPoints.get(0));
((IHasMutableEndpoints) t).setEndpoint2(newPoints.get(newPoints.size() - 1));
t.setMidpoints(newPoints.subList(1, newPoints.size() - 1));
}
else {
t.setMidpoints(newPoints);
}
}
}, true);
}
}
}
}