package org.geogebra.common.euclidian.controller;
import java.util.ArrayList;
import org.geogebra.common.awt.GPoint;
import org.geogebra.common.euclidian.EuclidianConstants;
import org.geogebra.common.euclidian.EuclidianController;
import org.geogebra.common.euclidian.Hits;
import org.geogebra.common.euclidian.event.PointerEventType;
import org.geogebra.common.kernel.Matrix.Coords;
import org.geogebra.common.kernel.algos.AlgoCirclePointRadius;
import org.geogebra.common.kernel.algos.AlgoElement;
import org.geogebra.common.kernel.algos.AlgoSphereNDPointRadius;
import org.geogebra.common.kernel.arithmetic.NumberValue;
import org.geogebra.common.kernel.geos.GeoConic;
import org.geogebra.common.kernel.geos.GeoElement;
import org.geogebra.common.kernel.geos.GeoLine;
import org.geogebra.common.kernel.geos.GeoNumeric;
import org.geogebra.common.kernel.geos.GeoPoint;
import org.geogebra.common.kernel.kernelND.GeoPointND;
import org.geogebra.common.main.App;
import org.geogebra.common.util.MyMath;
/**
* Handles mouse and touch events in EV
*/
public class MouseTouchGestureController {
/**
* Threshold for moving in case of a multitouch-event (pixel).
*/
public static final int MIN_MOVE = 5;
/** Application */
protected App app;
/** Controller */
protected EuclidianController ec;
/**
* The mode of the multitouch-event.
*/
protected MultitouchMode multitouchMode;
/**
* Actual scale of the axes (has to be saved during multitouch).
*/
protected double scale;
/**
* Conic which's size is changed.
*/
protected GeoConic scaleConic;
/**
* Midpoint of scaleConic: [0] ... x-coordinate [1] ... y-coordinate/
*/
protected double[] midpoint;
/**
* X-coordinates of the points that define scaleConic/
*/
protected double[] originalPointX;
/**
* Y-coordinates of the points that define scaleConic/
*/
protected double[] originalPointY;
/**
* x-coordinate of the center of the multitouch-event.
*/
protected int oldCenterX;
/**
* y-coordinate of the center of the multitouch-event.
*/
protected int oldCenterY;
private double originalRadius;
private GeoPoint firstFingerTouch;
private GeoPoint secondFingerTouch;
private GeoLine lineToMove;
private boolean firstTouchIsAttachedToStartPoint;
private boolean mAllowPropertiesView = true;
/**
* @param app
* application
* @param ec
* controller
*/
public MouseTouchGestureController(App app, EuclidianController ec) {
this.app = app;
this.ec = ec;
}
/**
* @param x1d
* first touch x
* @param y1d
* first touch y
* @param x2d
* second touch x
* @param y2d
* second touch y
*/
public void twoTouchMove(double x1d, double y1d, double x2d, double y2d) {
int x1 = (int) x1d;
int x2 = (int) x2d;
int y1 = (int) y1d;
int y2 = (int) y2d;
if ((x1 == x2 && y1 == y2) || ec.oldDistance == 0) {
return;
}
switch (multitouchMode) {
case zoomY:
if (scale == 0 || !app.isShiftDragZoomEnabled()) {
return;
}
double newRatioY = this.scale * (y1 - y2) / ec.oldDistance;
ec.getView().setCoordSystem(ec.getView().getXZero(),
ec.getView().getYZero(), ec.getView().getXscale(),
newRatioY);
break;
case zoomX:
if (this.scale == 0 || !app.isShiftDragZoomEnabled()) {
return;
}
double newRatioX = this.scale * (x1 - x2) / ec.oldDistance;
ec.getView().setCoordSystem(ec.getView().getXZero(),
ec.getView().getYZero(), newRatioX,
ec.getView().getYscale());
break;
case circle3Points:
double dist = MyMath.length(x1 - x2, y1 - y2);
this.scale = dist / ec.oldDistance;
int i = 0;
for (GeoPointND p : scaleConic.getFreeInputPoints(ec.getView())) {
double newX = midpoint[0]
+ (originalPointX[i] - midpoint[0]) * scale;
double newY = midpoint[1]
+ (originalPointY[i] - midpoint[1]) * scale;
p.setCoords(newX, newY, 1.0);
p.updateCascade();
i++;
}
ec.kernel.notifyRepaint();
break;
case circle2Points:
double dist2P = MyMath.length(x1 - x2, y1 - y2);
this.scale = dist2P / ec.oldDistance;
// index 0 is the midpoint, index 1 is the point on the circle
GeoPointND p = scaleConic.getFreeInputPoints(ec.getView()).get(1);
double newX = midpoint[0]
+ (originalPointX[1] - midpoint[0]) * scale;
double newY = midpoint[1]
+ (originalPointY[1] - midpoint[1]) * scale;
p.setCoords(newX, newY, 1.0);
p.updateCascade();
ec.kernel.notifyRepaint();
break;
case circleRadius:
double distR = MyMath.length(x1 - x2, y1 - y2);
this.scale = distR / ec.oldDistance;
GeoNumeric newRadius = new GeoNumeric(ec.kernel.getConstruction(),
this.scale * this.originalRadius);
((AlgoSphereNDPointRadius) scaleConic.getParentAlgorithm())
.setRadius(newRadius);
scaleConic.updateCascade();
ec.kernel.notifyUpdate(scaleConic);
ec.kernel.notifyRepaint();
break;
case circleFormula:
double distF = MyMath.length(x1 - x2, y1 - y2);
this.scale = distF / ec.oldDistance;
scaleConic.halfAxes[0] = this.scale * this.originalRadius;
scaleConic.halfAxes[1] = this.scale * this.originalRadius;
scaleConic.updateCascade();
ec.kernel.notifyUpdate(scaleConic);
ec.kernel.notifyRepaint();
break;
case moveLine:
// ignore minimal changes of finger-movement
if (onlyJitter(firstFingerTouch.getX(), firstFingerTouch.getY(),
secondFingerTouch.getX(), secondFingerTouch.getY(), x1d,
y1d, x2d, y2d)) {
return;
}
Coords oldStart = firstFingerTouch.getCoords();
Coords oldEnd = secondFingerTouch.getCoords();
if (firstTouchIsAttachedToStartPoint) {
firstFingerTouch.setCoords(ec.getView().toRealWorldCoordX(x1d),
ec.getView().toRealWorldCoordY(y1d), 1);
secondFingerTouch.setCoords(
ec.getView().toRealWorldCoordX(x2d), ec.getView()
.toRealWorldCoordY(y2d), 1);
} else {
secondFingerTouch.setCoords(
ec.getView().toRealWorldCoordX(x1d), ec.getView()
.toRealWorldCoordY(y1d), 1);
firstFingerTouch.setCoords(ec.getView().toRealWorldCoordX(x2d),
ec.getView().toRealWorldCoordY(y2d), 1);
}
// set line through the two finger touches
Coords crossP = firstFingerTouch.getCoords()
.crossProduct(secondFingerTouch.getCoords());
lineToMove.setCoords(crossP.getX(), crossP.getY(), crossP.getZ());
lineToMove.updateCascade();
// update coords of startPoint
lineToMove.pointChanged(lineToMove.getStartPoint());
lineToMove.getStartPoint().updateCoords();
// update coords of endPoint
lineToMove.pointChanged(lineToMove.getEndPoint());
lineToMove.getEndPoint().updateCoords();
// also move points along the line
double newStartX = lineToMove.getStartPoint().getX()
- (oldStart.getX() - firstFingerTouch.getX());
double newStartY = lineToMove.getStartPoint().getY()
- (oldStart.getY() - firstFingerTouch.getY());
double newEndX = lineToMove.getEndPoint().getX()
- (oldEnd.getX() - secondFingerTouch.getX());
double newEndY = lineToMove.getEndPoint().getY()
- (oldEnd.getY() - secondFingerTouch.getY());
lineToMove.getStartPoint().setCoords(newStartX, newStartY, 1);
lineToMove.getEndPoint().setCoords(newEndX, newEndY, 1);
lineToMove.getStartPoint().updateCascade();
lineToMove.getEndPoint().updateCascade();
ec.kernel.notifyUpdate(lineToMove.getStartPoint());
ec.kernel.notifyUpdate(lineToMove.getEndPoint());
ec.kernel.notifyRepaint();
break;
default:
if (!app.isShiftDragZoomEnabled()) {
return;
}
// pinch
ec.twoTouchMoveCommon(x1, y1, x2, y2);
int centerX = (x1 + x2) / 2;
int centerY = (y1 + y2) / 2;
if (MyMath.length(oldCenterX - centerX,
oldCenterY - centerY) > MIN_MOVE) {
ec.getView().rememberOrigins();
ec.getView().translateCoordSystemInPixels(centerX - oldCenterX,
centerY - oldCenterY, 0,
EuclidianConstants.MODE_TRANSLATEVIEW);
oldCenterX = centerX;
oldCenterY = centerY;
}
}
}
/**
* @param x1
* first touch x
* @param y1
* first touch y
* @param x2
* second touch x
* @param y2
* second touch y
*/
public void twoTouchStart(double x1, double y1, double x2, double y2) {
this.scaleConic = null;
ec.getView().setHits(new GPoint((int) x1, (int) y1),
PointerEventType.TOUCH);
// needs to be copied, because the reference is changed in the next step
Hits hits1 = new Hits();
for (GeoElement geo : ec.getView().getHits()) {
hits1.add(geo);
}
ec.getView().setHits(new GPoint((int) x2, (int) y2),
PointerEventType.TOUCH);
Hits hits2 = ec.getView().getHits();
oldCenterX = (int) (x1 + x2) / 2;
oldCenterY = (int) (y1 + y2) / 2;
if (hits1.hasYAxis() && hits2.hasYAxis()) {
this.multitouchMode = MultitouchMode.zoomY;
ec.oldDistance = y1 - y2;
this.scale = ec.getView().getYscale();
} else if (hits1.hasXAxis() && hits2.hasXAxis()) {
this.multitouchMode = MultitouchMode.zoomX;
ec.oldDistance = x1 - x2;
this.scale = ec.getView().getXscale();
} else if (hits1.size() > 0 && hits2.size() > 0
&& hits1.get(0) == hits2.get(0)
&& hits1.get(0) instanceof GeoConic
// isClosedPath: true for circle and ellipse
&& ((GeoConic) hits1.get(0)).isClosedPath()) {
this.scaleConic = (GeoConic) hits1.get(0);
// TODO: select scaleConic
if (scaleConic.getFreeInputPoints(ec.getView()) == null
&& scaleConic.isCircle()) {
this.multitouchMode = MultitouchMode.circleFormula;
this.originalRadius = scaleConic.getHalfAxis(0);
} else if (scaleConic.getFreeInputPoints(ec.getView()).size() >= 3) {
this.multitouchMode = MultitouchMode.circle3Points;
} else if (scaleConic.getFreeInputPoints(ec.getView()).size() == 2) {
this.multitouchMode = MultitouchMode.circle2Points;
} else if (scaleConic
.getParentAlgorithm() instanceof AlgoCirclePointRadius) {
this.multitouchMode = MultitouchMode.circleRadius;
AlgoElement algo = scaleConic.getParentAlgorithm();
NumberValue radius = (NumberValue) algo.input[1];
this.originalRadius = radius.getDouble();
} else {
// TODO scale other conic-types (e.g. ellipses with formula)
scaleConic = null;
ec.clearSelections();
this.multitouchMode = MultitouchMode.view;
ec.twoTouchStartCommon(x1, y1, x2, y2);
return;
}
ec.twoTouchStartCommon(x1, y1, x2, y2);
midpoint = new double[] { scaleConic.getMidpoint().getX(),
scaleConic.getMidpoint().getY() };
ArrayList<GeoPointND> points = scaleConic
.getFreeInputPoints(ec
.getView());
this.originalPointX = new double[points.size()];
this.originalPointY = new double[points.size()];
for (int i = 0; i < points.size(); i++) {
this.originalPointX[i] = points.get(i).getCoords().getX();
this.originalPointY[i] = points.get(i).getCoords().getY();
}
} else if (hits1.size() > 0 && hits2.size() > 0
&& hits1.get(0) == hits2.get(0)
&& hits1.get(0) instanceof GeoLine
&& isMovableWithTwoFingers(hits1.get(0))) {
this.multitouchMode = MultitouchMode.moveLine;
lineToMove = (GeoLine) hits1.get(0);
GeoPoint touch1 = new GeoPoint(ec.kernel.getConstruction(), ec
.getView().toRealWorldCoordX(x1), ec.getView()
.toRealWorldCoordY(y1), 1);
GeoPoint touch2 = new GeoPoint(ec.kernel.getConstruction(), ec
.getView().toRealWorldCoordX(x2), ec.getView()
.toRealWorldCoordY(y2), 1);
firstTouchIsAttachedToStartPoint = setFirstTouchToStartPoint(touch1,
touch2);
if (firstTouchIsAttachedToStartPoint) {
firstFingerTouch = touch1;
secondFingerTouch = touch2;
} else {
firstFingerTouch = touch2;
secondFingerTouch = touch1;
}
ec.twoTouchStartCommon(x1, y1, x2, y2);
} else {
ec.clearSelections();
this.multitouchMode = MultitouchMode.view;
ec.twoTouchStartCommon(x1, y1, x2, y2);
}
}
/**
* @param geoElement
* {@link GeoElement}
* @return true if GeoElement should be movable with two fingers
*/
private static boolean isMovableWithTwoFingers(GeoElement geoElement) {
return geoElement.getParentAlgorithm()
.getRelatedModeID() == EuclidianConstants.MODE_JOIN
|| geoElement.getParentAlgorithm()
.getRelatedModeID() == EuclidianConstants.MODE_SEGMENT
|| geoElement.getParentAlgorithm()
.getRelatedModeID() == EuclidianConstants.MODE_RAY;
}
/**
* @param touch1
* {@link GeoPoint}
* @param touch2
* {@link GeoPoint}
* @return true if the first touch should be attached to the startPoint
*/
private boolean setFirstTouchToStartPoint(GeoPoint touch1,
GeoPoint touch2) {
if (lineToMove.getStartPoint().getX() < lineToMove.getEndPoint()
.getX()) {
return touch1.getX() < touch2.getX();
}
return touch2.getX() < touch1.getX();
}
/**
* screen coordinates
*
* @param oldStartX
* @param oldStartY
* @param oldEndX
* @param oldEndY
* @param newStartX
* @param newStartY
* @param newEndX
* @param newEndY
* @return true if there are only minimal changes of the two finger-touches
*/
private boolean onlyJitter(double oldStartX, double oldStartY,
double oldEndX, double oldEndY, double newStartX, double newStartY,
double newEndX, double newEndY) {
double capThreshold = app.getCapturingThreshold(PointerEventType.TOUCH);
return Math.abs(oldStartX - newStartX) < capThreshold
&& Math.abs(oldStartY - newStartY) < capThreshold
&& Math.abs(oldEndX - newEndX) < capThreshold
&& Math.abs(oldEndY - newEndY) < capThreshold;
}
/**
* Used in Android
*
* @param allowProperties
* whether properties are allowed
*/
public void allowPropertiesView(boolean allowProperties) {
mAllowPropertiesView = allowProperties;
}
/**
* Used in Android
*
* @return whether properties are allowed
*/
protected boolean isAllowPropertiesView() {
return mAllowPropertiesView;
}
}