Java etc...

アクセスカウンタ

help RSS The JavaFX Properties binding(4) - Gesture Event

<<   作成日時 : 2013/02/13 01:19   >>

ブログ気持玉 0 / トラックバック 0 / コメント 0

The JavaFX Properties binding for computing the area of triangle(4) - enabling Gesture Event Handling



This article is a continuation of The JavaFX Properties binding for computing the area of triangle(3).

The goal of this part is the handling GestureEvents, specially TouchEvent, ZoomEvent and RotateEvent.

The handler for TouchEvent can handle at only three points multi touch occurs at the same time. When three point multi touch, the three small circles are drawn at the touch points, and three lines are stroked to connect between each pair of two circles. It will construct a triangle.

The handle for ZoomEvent and RotateEvent can handle at two points multi touch occurs. The rotate handler handle rotation of the triangle on its center of gravity.The zoom handler change a radius of the circles represents the vertexes of the triangle, and thickness of the lines represents the sides of the triangle.It dose not change the size of the triangle.

I had some difficulty doing both GestureEvent and MouseEvent deal with in a proper way, such as, after the ZoomEvent or RotateEvent handlers are done, almost all case, the MouseEvent handler also performs. It is quite difference from what I expected. So, a couple of codes are added in the handler of MouseEvent, which is If the isSynthesized() method return true, then consume() the event.

My runtime environment is that Splashtop Win8 Metro Testbed on iPad2 is used at Remote Desktop Side as Multi Touch device, and Splashtop Streamer on Windwos 8 Pro on SONY VAIO note PC is used for the program running.

The tools of Splashtop was introduced by @aoetk on his LT session at Study Meeting for JavaFX in Japan on 2012/11/30.

Enabling Rotate, Zoom and MultiTouch Gestures


In addition, for debugging, the TextField and Slider is added on the right bottom area. The TextField is for rotation angle, and Slider is extension ratio of zooming. These controls are introduced for the easy testing and debugging of the handler logics on the environment without multitouch support.

Here is the code. It's so long.

Since a lot of white space for indentation is cause the exceed the limit of the length for a single blog article, there are wide width white space characters for indentation, and there are no comments. If you copy and paste the following code, and compile it, then javac will complain of illegal characters in the source code.

import java.util.List;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.NumberBinding;
import javafx.beans.binding.When;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.GroupBuilder;
import javafx.scene.Scene;
import javafx.scene.SceneBuilder;
import javafx.scene.control.Label;
import javafx.scene.control.LabelBuilder;
import javafx.scene.control.Slider;
import javafx.scene.control.SliderBuilder;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFieldBuilder;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.RotateEvent;
import javafx.scene.input.TouchEvent;
import javafx.scene.input.TouchPoint;
import javafx.scene.input.ZoomEvent;
import javafx.scene.layout.HBoxBuilder;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.CircleBuilder;
import javafx.scene.shape.Line;
import javafx.scene.shape.LineBuilder;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.RectangleBuilder;
import javafx.stage.Stage;
import javafx.util.StringConverter;

public class MultiTouch1 extends Application {
 final double PPI = 96.0;
 final double INCHI = 2.54;
 final double PPC = PPI/INCHI;

 final int WIDTH = 800;
 final int HEIGHT = 600;
 final int MARGIN = 5;
 final int OFFSET = 5;

 final int INNER_WIDTH = WIDTH - MARGIN * 2;
 final int INNER_HEIGHT = HEIGHT - MARGIN * 2;

 final int ORIGIN_X = MARGIN + OFFSET;
 final int ORIGIN_Y = INNER_HEIGHT - OFFSET;

 final double THIN_STROKE = 0.1;
 final double THICK_STROKE = 0.3;

 private int clickcount = 0;

 private Rectangle rectangle;
 private Circle c[] = new Circle[3];
 private Line l[] = new Line[3];
 private Line a_x_l[] = new Line[(int)(INNER_HEIGHT/PPC)];
 private Line a_y_l[] = new Line[(int)(INNER_WIDTH/PPC)];
 private TextField tx[] = new TextField[3];
 private TextField ty[] = new TextField[3];
 private TextField tr;
 private Label area_Label = new Label();
 private Slider slider;
 private Circle gp_c = CircleBuilder.create()
  .radius(0.0).fill(Color.RED).build();

 private DoubleProperty org_x[] =
  new SimpleDoubleProperty[3];

 private DoubleProperty org_y[]
   new SimpleDoubleProperty[3];

 private NumberBinding map_x[] = new NumberBinding[3];
 private NumberBinding map_y[] = new NumberBinding[3];
 private NumberBinding area;
 private NumberBinding gp_x;
 private NumberBinding gp_y;

 private DoubleProperty rotation_angle =
  new SimpleDoubleProperty(0.0);

 private Color color[] = { Color.RED, Color.BLUE, Color.GREEN };

 {
  rectangle = RectangleBuilder.create()
   .layoutX(MARGIN).layoutY(MARGIN)
   .width(INNER_WIDTH).height(INNER_HEIGHT)
   .fill(Color.LIGHTYELLOW)
   .build();

  slider = SliderBuilder.create()
   .min(0.0).max(10.0).value(1.0)
   .showTickMarks(true).showTickLabels(true)
   .majorTickUnit(1.0).blockIncrement(0.5)
   .orientation(Orientation.HORIZONTAL)
   .build();

  tr = TextFieldBuilder.create()
   .prefColumnCount(4)
   .text("0.0")
   .onAction(new EventHandler<ActionEvent>() {
    @Override public void handle(ActionEvent e) {
     double angle = Math.toRadians(
      Double.parseDouble(
       ((TextField)e.getSource()).getText()));
     rotation_angle.set(angle%360.0);
     rotation(angle);
    }
   })
   .build();

  for(int i = 0; i < 3; i++ ) {
   c[i] = CircleBuilder.create()
    .radius(0.0).fill(color[i]).build();

   l[i] = LineBuilder.create()
    .strokeWidth(0.0).stroke(Color.BLACK).build();

   org_x[i] = new SimpleDoubleProperty(ORIGIN_X);
   org_y[i] = new SimpleDoubleProperty(ORIGIN_Y);
   map_x[i] = org_x[i].subtract(ORIGIN_X).divide(PPC);
   map_y[i] = org_y[i].negate().add(ORIGIN_Y).divide(PPC);
   c[i].radiusProperty().bind(
       Bindings.multiply(slider.valueProperty(), 10.0));

   l[i].strokeWidthProperty().bind(
       Bindings.multiply(slider.valueProperty(), 5.0));

   tx[i] = TextFieldBuilder.create()
    .prefColumnCount(2).build();

   ty[i] = TextFieldBuilder.create()
    .prefColumnCount(2)build();
  }    

  for (int i = 0; i < a_x_l.length; i++) {
   double delta_y = (INNER_HEIGHT - 5) - (i+1) * (PPC);
   a_x_l[i] = new Line(
       MARGIN, delta_y, MARGIN + INNER_WIDTH, delta_y);
   a_x_l[i].setStrokeWidth(
          (i+1)%5==0 ? THICK_STROKE : THIN_STROKE);
  }

  for (int i = 0; i < a_y_l.length; i++) {
   double delta_x = (MARGIN + 5) + (i+1) * (PPC);
   a_y_l[i] = new Line(
      delta_x, MARGIN, delta_x, MARGIN + INNER_HEIGHT);
   a_y_l[i].setStrokeWidth(
          (i+1)%5==0 ? THICK_STROKE : THIN_STROKE);
  }

  gp_x = org_x[0].add(org_x[1]).add(org_x[2]).divide(3.0);
  gp_y = org_y[0].add(org_y[1]).add(org_y[2]).divide(3.0);

  gp_c.centerXProperty().bind(gp_x);
  gp_c.centerYProperty().bind(gp_y);
  gp_c.radiusProperty().bind(
        Bindings.min(slider.valueProperty(), 2.0));
 }

 StringConverter<Number> sc_x =
  new StringConverter<Number>() {
   @Override public Number fromString(String from) {
    double map_x = Double.parseDouble(from);
    double org_x = (MARGIN + 5) + (map_x*PPC);
    return new Double(org_x);
   }

   @Override public String toString(Number org_x) {
    double map_x = (org_x.doubleValue() - (MARGIN + 5))/PPC;
    return String.format("%2.1f", map_x);
   }
 };

 StringConverter<Number> sc_y =
  new StringConverter<Number>() {
   @Override public Number fromString(String from) {
    double map_y = Double.parseDouble(from);
    double org_y = (INNER_HEIGHT - 5) - (map_y*PPC);
    return new Double(org_y);
   }

   @Override public String toString(Number org_y) {
    double map_y = ((INNER_HEIGHT - 5) - org_y.doubleValue())/PPC;
    return String.format("%2.1f", map_y);
   }
 };

 StringConverter<Number> sc_angle =
  new StringConverter<Number>() {
   @Override public Number fromString(String from) {
    double angle = Double.parseDouble(from);
    double r_angle = Math.toRadians(angle%360.0);
    return new Double(r_angle);
   }

   @Override public String toString(Number r_angle) {
    double angle = Math.toDegrees(r_angle.doubleValue());
    return String.format("%2.1f", (angle%360.0));
   }
 };

 public MultiTouch1() {
  initialize();
    
  for(int i = 0; i < 3; i++) {
   int j = (i == 2) ? 0 : i+1;

   l[i].startXProperty().bind(c[i].centerXProperty());
   l[i].startYProperty().bind(c[i].centerYProperty());
   l[i].endXProperty().bind(c[j].centerXProperty());
   l[i].endYProperty().bind(c[j].centerYProperty());

   Bindings.bindBidirectional(
            c[i].centerXProperty(), org_x[i]);
   Bindings.bindBidirectional(
            c[i].centerYProperty(), org_y[i]);
   Bindings.bindBidirectional(
        tx[i].textProperty(), c[i].centerXProperty(), sc_x);
   Bindings.bindBidirectional(
        ty[i].textProperty(), c[i].centerYProperty(), sc_y);
   Bindings.bindBidirectional(
        tr.textProperty(), rotation_angle, sc_angle);
  }

  NumberBinding tmp_area =
   map_x[0].multiply(map_y[1])
    .add(map_x[1].multiply(map_y[2]))
    .add(map_x[2].multiply(map_y[0]))
    .subtract(map_x[0].multiply(map_y[2]))
    .subtract(map_x[1].multiply(map_y[0]))
    .subtract(map_x[2].multiply(map_y[1]))
    .divide(2.0);

  area = new When(tmp_area.lessThan(0))
       .then(tmp_area.negate())
       .otherwise(tmp_area);
    
  area_Label.textProperty()
   .bind(new SimpleStringProperty("Area = ")
    .concat(area.asString("%2.1f")));
 }

 private void initialize() {
  clickcount = 0;
 }

 @Override public void start(Stage stage) {
  final Group root;    
  Scene scene = SceneBuilder.create()
   .width(WIDTH).height(HEIGHT)
   .fill(Color.WHITE)
   .root(root = GroupBuilder.create()
   .children(rectangle)
   .build()
  ).build();
   
  root.getChildren().addAll(a_x_l);
  root.getChildren().addAll(a_y_l);
    
  root.getChildren().addAll(
   LineBuilder.create()
    .startX(MARGIN).startY(ORIGIN_Y)
    .endX(MARGIN + INNER_WIDTH).endY(ORIGIN_Y)
    .build(),
   LineBuilder.create()
    .startX(ORIGIN_X).startY(MARGIN)
    .endX(ORIGIN_X).endY(MARGIN + INNER_HEIGHT)
    .build()
  );

  root.getChildren().addAll(
   HBoxBuilder.create()
    .spacing(3).padding(new Insets(10, 10, 10, 10))
    .layoutX(10)
    .alignment(Pos.BOTTOM_CENTER)
    .children(
     LabelBuilder.create().text("A(").build(),
     tx[0],
     LabelBuilder.create().text(",").build(),
     ty[0],
     LabelBuilder.create().text("),").build(),
     LabelBuilder.create().text("B(").build(),
     tx[1],
     LabelBuilder.create().text(",").build(),
     ty[1],
     LabelBuilder.create().text("),").build(),
     LabelBuilder.create().text("C(").build(),
     tx[2],
     LabelBuilder.create().text(",").build(),
     ty[2],
     LabelBuilder.create().text(") ⇒ ").build(),
     area_Label
    )
    .build(),
   HBoxBuilder.create()
    .layoutX(INNER_WIDTH - 270).layoutY(INNER_HEIGHT - 30)
    .children(
     LabelBuilder.create().text("Rotate").build(),
     tr,
     LabelBuilder.create().text("Zoome").build(),
     slider
    )
    .build()
   );

  rectangle.setOnMouseClicked(new EventHandler<MouseEvent>() {
   @Override public void handle(MouseEvent e) {
    if(e.isSynthesized()) { e.consume(); return; }
    if (clickcount > 2) { initialize(); }

    final int i = clickcount;
    final double x = e.getSceneX();
    final double y = e.getSceneY();

    Task<Void> task = new Task<Void>() {
     @Override public Void call() {
      Platform.runLater(new Runnable() {
       @Override public void run() {
        if(i == 0) {
         root.getChildren()
           .removeAll(l[0], l[1], l[2], c[0], c[1], c[2], gp_c);
        }
        org_x[i].set(x);
        org_y[i].set(y);
        switch(i) {
         case 0:
          root.getChildren().add(c[0]);
          break;
         case 1:
          root.getChildren().remove(c[0]);
          root.getChildren().addAll(l[0], c[0], c[1]);
          break;
         case 2:
          root.getChildren().removeAll(c[0], c[1], l[0]);
          root.getChildren()
            .addAll(l[0], l[1], l[2], c[0], c[1], c[2], gp_c);
          break;
         default :
        }
       }
      });
      return null;
     }
    };
    new Thread(task).start();
    clickcount++;
    e.consume();
   }
  });

  rectangle.setOnTouchPressed(new EventHandler<TouchEvent>() {
   @Override public void handle(TouchEvent e) {
    int tc = e.getTouchCount();
    if(tc != 3) { e.consume(); return; }
    final List<TouchPoint> tps = e.getTouchPoints();
    Platform.runLater(new Runnable() {
     @Override public void run() {
      root.getChildren().removeAll(
          l[0], l[1], l[2], c[0], c[1], c[2], gp_c);
      int i = 0;
      for(TouchPoint p : tps) {
       org_x[i].set(p.getSceneX());
       org_y[i].set(p.getSceneY());
       i++;
      }
      root.getChildren().addAll(
          l[0], l[1], l[2], c[0], c[1], c[2], gp_c);
     }
    });
    clickcount += tc;
    e.consume();
   }
  });

  rectangle.setOnZoom(new EventHandler<ZoomEvent> (){
   @Override public void handle(ZoomEvent e) {
    double scale = e.getZoomFactor()
             * slider.valueProperty().get();
    slider.valueProperty().set(scale);
    e.consume();
   }
  });

  rectangle.setOnRotate(new EventHandler<RotateEvent>() {
   @Override public void handle(RotateEvent e) {
    double angle = e.getAngle()/20.0;
    rotation_angle.set(e.getTotalAngle()%360.0);
    rotation(angle);
    e.consume();
   }
  });

  stage.setTitle("Triangle Area");
  stage.setScene(scene);
  stage.show();
 }

 private void rotation(double angle) {
  double o_x[] = new double[3];
  double o_y[] = new double[3];
  double r_x[] = new double[3];
  double r_y[] = new double[3];
  double g_x = gp_x.doubleValue();
  double g_y = gp_y.doubleValue();
  for(int i = 0; i < 3; i++) {
   o_x[i] = org_x[i].get();
   o_y[i] = org_y[i].get();
   r_x[i] = (o_x[i]-g_x)*Math.cos(angle
        - (o_y[i]-g_y)*Math.sin(angle) + g_x;
   r_y[i] = (o_x[i]-g_x)*Math.sin(angle)
        + (o_y[i]-g_y)*Math.cos(angle) + g_y;
  }
  for(int i = 0; i < 3; i++) {
   org_x[i].set(r_x[i]);
   org_y[i].set(r_y[i]);
  }
 }

 public static void main(String[] args) {
  launch(args);
 }
}


Another notice is that the above program can resize the window size, but the its own Coordinate area is not expanded.

This is the end of the series of the JavaFX Properties binding.

テーマ

注目テーマ 一覧


月別リンク

ブログ気持玉

クリックして気持ちを伝えよう!
ログインしてクリックすれば、自分のブログへのリンクが付きます。
→ログインへ

トラックバック(0件)

タイトル (本文) ブログ名/日時

トラックバック用URL help


自分のブログにトラックバック記事作成(会員用) help

タイトル
本 文

コメント(0件)

内 容 ニックネーム/日時

コメントする help

ニックネーム
本 文
The JavaFX Properties binding(4) - Gesture Event Java etc.../BIGLOBEウェブリブログ