|  | @@ -0,0 +1,956 @@
 | 
	
		
			
				|  |  | +/*
 | 
	
		
			
				|  |  | + * Copyright (C) 2011 Google Inc.
 | 
	
		
			
				|  |  | + * 
 | 
	
		
			
				|  |  | + * 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.ros.rosjava.android.views;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +import android.content.Context;
 | 
	
		
			
				|  |  | +import android.graphics.Point;
 | 
	
		
			
				|  |  | +import android.view.Gravity;
 | 
	
		
			
				|  |  | +import android.view.LayoutInflater;
 | 
	
		
			
				|  |  | +import android.view.MotionEvent;
 | 
	
		
			
				|  |  | +import android.view.View;
 | 
	
		
			
				|  |  | +import android.view.View.OnTouchListener;
 | 
	
		
			
				|  |  | +import android.view.animation.Animation;
 | 
	
		
			
				|  |  | +import android.view.animation.Animation.AnimationListener;
 | 
	
		
			
				|  |  | +import android.view.animation.AnimationSet;
 | 
	
		
			
				|  |  | +import android.view.animation.LinearInterpolator;
 | 
	
		
			
				|  |  | +import android.view.animation.RotateAnimation;
 | 
	
		
			
				|  |  | +import android.view.animation.ScaleAnimation;
 | 
	
		
			
				|  |  | +import android.widget.ImageView;
 | 
	
		
			
				|  |  | +import android.widget.RelativeLayout;
 | 
	
		
			
				|  |  | +import android.widget.TextView;
 | 
	
		
			
				|  |  | +import org.ros.address.InetAddressFactory;
 | 
	
		
			
				|  |  | +import org.ros.message.MessageListener;
 | 
	
		
			
				|  |  | +import org.ros.message.nav_msgs.Odometry;
 | 
	
		
			
				|  |  | +import org.ros.node.DefaultNodeFactory;
 | 
	
		
			
				|  |  | +import org.ros.node.Node;
 | 
	
		
			
				|  |  | +import org.ros.node.NodeConfiguration;
 | 
	
		
			
				|  |  | +import org.ros.node.topic.Publisher;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +import java.net.URI;
 | 
	
		
			
				|  |  | +import java.util.Timer;
 | 
	
		
			
				|  |  | +import java.util.TimerTask;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * VirtualJoystickView creates a virtual joystick view that publishes velocity
 | 
	
		
			
				|  |  | + * as (geometry_msgs.Twist) messages. The current version contains the following
 | 
	
		
			
				|  |  | + * features: snap to axes, turn in place, and resume previous velocity.
 | 
	
		
			
				|  |  | + * 
 | 
	
		
			
				|  |  | + * @author munjaldesai@google.com (Munjal Desai)
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +public class VirtualJoystickView extends RelativeLayout implements OnTouchListener,
 | 
	
		
			
				|  |  | +    AnimationListener, MessageListener<org.ros.message.nav_msgs.Odometry> {
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * BOX_TO_CIRCLE_RATIO The dimensions of the square box that contains the
 | 
	
		
			
				|  |  | +   * circle, rings, etc are 300x300. The circles, rings, etc have a diameter of
 | 
	
		
			
				|  |  | +   * 220. The ratio of the box to the circles is 300/220 = 1.363636. This ratio
 | 
	
		
			
				|  |  | +   * stays the same regardless of the size of the virtual joystick.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private static final float BOX_TO_CIRCLE_RATIO = 1.363636f;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * MAGNET_THETA The number of degrees before and after the major axes (0, 90,
 | 
	
		
			
				|  |  | +   * 180, and 270) where the orientation is automatically adjusted to 0, 90,
 | 
	
		
			
				|  |  | +   * 180, or 270. For example, if the contactTheta is 85 degrees and
 | 
	
		
			
				|  |  | +   * MAGNET_THETA is 10, the contactTheta will be changed to 90.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private float magnetTheta = 10.0f;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * ORIENTATION_TACK_FADE_RANGE The range in degrees around the current
 | 
	
		
			
				|  |  | +   * orientation where the {@link #orientationWidget}s will be visible.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private static final float ORIENTATION_TACK_FADE_RANGE = 40.0f;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * TURN_IN_PLACE_CONFIRMATION_DELAY Time (in milliseconds) to wait before
 | 
	
		
			
				|  |  | +   * visually changing to turn-in-place mode.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private static final long TURN_IN_PLACE_CONFIRMATION_DELAY = 200L;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * FLOAT_EPSILON Used for comparing float values.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private static final float FLOAT_EPSILON = 0.001f;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * THUMB_DIVET_RADIUS The radius of the {@link #thumbDivet}. This is also the
 | 
	
		
			
				|  |  | +   * distance threshold around the {@link #contactUpLocation} that triggers the
 | 
	
		
			
				|  |  | +   * previous-velocity-mode {@link #previousVelocityMode}. This radius is also
 | 
	
		
			
				|  |  | +   * the same for the {@link #lastVelocityDivet}.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private static final float THUMB_DIVET_RADIUS = 16.5f;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * POST_LOCK_MAGNET_THETA Replaces {@link #magnetTheta} once the contact has
 | 
	
		
			
				|  |  | +   * been magnetized/snapped around 90/270.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private static final float POST_LOCK_MAGNET_THETA = 20.0f;
 | 
	
		
			
				|  |  | +  private static final int INVALID_POINTER_ID = -1;
 | 
	
		
			
				|  |  | +  private Publisher<org.ros.message.geometry_msgs.Twist> publisher;
 | 
	
		
			
				|  |  | +  private Node node;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * mainLayout The parent layout that contains all the elements of the virtual
 | 
	
		
			
				|  |  | +   * joystick.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private RelativeLayout mainLayout;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * intensity The intensity circle that is used to show the current magnitude.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private ImageView intensity;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * thumbDivet The divet that is underneath the user's thumb. When there is no
 | 
	
		
			
				|  |  | +   * contact it moves to the center (over the center divet). An arrow inside it
 | 
	
		
			
				|  |  | +   * is used to indicate the orientation.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private ImageView thumbDivet;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * lastVelocityDivet The divet that shows the location of last contact. If a
 | 
	
		
			
				|  |  | +   * contact is placed within ({@link #THUMB_DIVET_RADIUS}) to this, the
 | 
	
		
			
				|  |  | +   * {@link #previousVelocityMode} is triggered.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private ImageView lastVelocityDivet;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * orientationWidget 4 long tacks on the major axes and 20 small tacks off of
 | 
	
		
			
				|  |  | +   * the major axes at 15 degree increments. These fade in and fade out to
 | 
	
		
			
				|  |  | +   * collectively indicate the current orientation.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private ImageView[] orientationWidget;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * magnitudeIndicator Shows the current linear velocity as a percent value.
 | 
	
		
			
				|  |  | +   * This TextView will be on the opposite side of the contact to ensure that is
 | 
	
		
			
				|  |  | +   * it visible most of the time. The font size and distance from the center of
 | 
	
		
			
				|  |  | +   * the widget are automatically computed based on the size of parent
 | 
	
		
			
				|  |  | +   * container.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private TextView magnitudeText;
 | 
	
		
			
				|  |  | +  /** contactTheta The current orientation of the virtual joystick in degrees. */
 | 
	
		
			
				|  |  | +  private float contactTheta;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * normalizedMagnitude This is the distance between the center divet and the
 | 
	
		
			
				|  |  | +   * point of contact normalized between 0 and 1. The linear velocity is based
 | 
	
		
			
				|  |  | +   * on this.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private float normalizedMagnitude;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * contactRadius This is the distance between the center of the widget and the
 | 
	
		
			
				|  |  | +   * point of contact normalized between 0 and 1. This is mostly used for
 | 
	
		
			
				|  |  | +   * animation/display calculations.
 | 
	
		
			
				|  |  | +   * 
 | 
	
		
			
				|  |  | +   * TODO(munjaldesai): Omnigraffle this for better documentation.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private float contactRadius;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * deadZoneRatio ...
 | 
	
		
			
				|  |  | +   * 
 | 
	
		
			
				|  |  | +   * TODO(munjaldesai): Write a simple explanation for this. Currently not easy
 | 
	
		
			
				|  |  | +   * to immediately comprehend it's meaning.
 | 
	
		
			
				|  |  | +   * 
 | 
	
		
			
				|  |  | +   * TODO(munjaldesai): Omnigraffle this for better documentation.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private float deadZoneRatio = Float.NaN;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * joystickRadius The center coordinates of the parent layout holding all the
 | 
	
		
			
				|  |  | +   * elements of the virtual joystick. The coordinates are relative to the
 | 
	
		
			
				|  |  | +   * immediate parent (mainLayout). Since the parent must be a square centerX =
 | 
	
		
			
				|  |  | +   * centerY = radius.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private float joystickRadius = Float.NaN;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * parentSize The length (width==height ideally) of a side of the parent
 | 
	
		
			
				|  |  | +   * container that holds the virtual joystick.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private float parentSize = Float.NaN;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * normalizingMultiplier Used to convert any distance from pixels to a
 | 
	
		
			
				|  |  | +   * normalized value between 0 and 1. 0 is the center of widget and 1 is the
 | 
	
		
			
				|  |  | +   * normalized distance to the {@link #outerRing} from the center of the
 | 
	
		
			
				|  |  | +   * widget.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private float normalizingMultiplier;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * currentRotationRange The (15 degree) green slice/arc used to indicate
 | 
	
		
			
				|  |  | +   * turn-in-place behavior.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private ImageView currentRotationRange;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * previousRotationRange The (30 degree) green slice/arc used to indicate
 | 
	
		
			
				|  |  | +   * turn-in-place behavior.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private ImageView previousRotationRange;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * turnInPlaceMode True when the virtual joystick is in turn-in-place mode.
 | 
	
		
			
				|  |  | +   * False otherwise.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private volatile boolean turnInPlaceMode;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * turnInPlaceStartTheta The orientation of the robot when the turn-in-place
 | 
	
		
			
				|  |  | +   * mode is initiated.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private float turnInPlaceStartTheta = Float.NaN;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * rightTurnOffset The rotation offset in degrees applied to
 | 
	
		
			
				|  |  | +   * {@link #currentRotationRange} and {@link #previousRotationRange} when the
 | 
	
		
			
				|  |  | +   * robot is turning clockwise (right) in turn-in-place mode.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private float rightTurnOffset;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * currentOrientation The orientation of the robot in degrees.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private volatile float currentOrientation;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * pointerId Used to keep track of the contact that initiated the interaction
 | 
	
		
			
				|  |  | +   * with the virtual joystick. All other contacts are ignored.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private int pointerId = INVALID_POINTER_ID;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * contactUpLocation The location of the primary contact when it is lifted off
 | 
	
		
			
				|  |  | +   * of the screen.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private Point contactUpLocation;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * previousVelocityMode True when the new contact position is within
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private boolean previousVelocityMode;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * magnetizedXAxis True when the contact has been snapped to the x-axis, false
 | 
	
		
			
				|  |  | +   * otherwise.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private boolean magnetizedXAxis;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Velocity commands are published when this is true. Not published otherwise.
 | 
	
		
			
				|  |  | +   * This is to prevent spamming velocity commands.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private volatile boolean publishVelocity;
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Used to publish velocity commands at a specific rate.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private Timer publisherTimer;
 | 
	
		
			
				|  |  | +  org.ros.message.geometry_msgs.Twist vel = new org.ros.message.geometry_msgs.Twist();
 | 
	
		
			
				|  |  | +  TimerTask timerTask = new TimerTask() {
 | 
	
		
			
				|  |  | +    @Override
 | 
	
		
			
				|  |  | +    public void run() {
 | 
	
		
			
				|  |  | +      if (publishVelocity) {
 | 
	
		
			
				|  |  | +        publisher.publish(vel);
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  };
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  public VirtualJoystickView(Context context) {
 | 
	
		
			
				|  |  | +    super(context);
 | 
	
		
			
				|  |  | +    // All the virtual joystick elements must be centered on the parent.
 | 
	
		
			
				|  |  | +    setGravity(Gravity.CENTER);
 | 
	
		
			
				|  |  | +    // Instantiate the elements from the layout XML file.
 | 
	
		
			
				|  |  | +    LayoutInflater.from(context).inflate(org.ros.rosjava.android.R.layout.virtual_joystick, this,
 | 
	
		
			
				|  |  | +        true);
 | 
	
		
			
				|  |  | +    initVirtualJoystick();
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Initialize the node and register the publisher and subscriber.
 | 
	
		
			
				|  |  | +   * 
 | 
	
		
			
				|  |  | +   * @param masterUri
 | 
	
		
			
				|  |  | +   *          The address of the master node.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private void initNode(final URI masterUri) {
 | 
	
		
			
				|  |  | +    try {
 | 
	
		
			
				|  |  | +      NodeConfiguration nodeConfiguration =
 | 
	
		
			
				|  |  | +          NodeConfiguration.newPublic(InetAddressFactory.newNonLoopback().getHostAddress()
 | 
	
		
			
				|  |  | +              .toString(), masterUri);
 | 
	
		
			
				|  |  | +      node = new DefaultNodeFactory().newNode("virtual_joystick", nodeConfiguration);
 | 
	
		
			
				|  |  | +      publisher = node.newPublisher("cmd_vel", "geometry_msgs/Twist");
 | 
	
		
			
				|  |  | +      node.newSubscriber("odom", "nav_msgs/Odometry", this);
 | 
	
		
			
				|  |  | +      publisherTimer.schedule(timerTask, 0, 80);
 | 
	
		
			
				|  |  | +    } catch (Exception e) {
 | 
	
		
			
				|  |  | +      if (node != null) {
 | 
	
		
			
				|  |  | +        node.getLog().fatal(e);
 | 
	
		
			
				|  |  | +      } else {
 | 
	
		
			
				|  |  | +        android.util.Log.e("VIRTUAL_JOYSTICK", e.toString());
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  @Override
 | 
	
		
			
				|  |  | +  public void onAnimationEnd(Animation animation) {
 | 
	
		
			
				|  |  | +    contactRadius = 0f;
 | 
	
		
			
				|  |  | +    normalizedMagnitude = 0f;
 | 
	
		
			
				|  |  | +    updateMagnitudeText();
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  @Override
 | 
	
		
			
				|  |  | +  public void onAnimationRepeat(Animation animation) {
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  @Override
 | 
	
		
			
				|  |  | +  public void onAnimationStart(Animation animation) {
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  @Override
 | 
	
		
			
				|  |  | +  public void onNewMessage(final Odometry message) {
 | 
	
		
			
				|  |  | +    double heading;
 | 
	
		
			
				|  |  | +    // For some reason the values of z and y seem to be interchanged. If they
 | 
	
		
			
				|  |  | +    // are not swapped then heading is always incorrect.
 | 
	
		
			
				|  |  | +    double w = message.pose.pose.orientation.w;
 | 
	
		
			
				|  |  | +    double x = message.pose.pose.orientation.x;
 | 
	
		
			
				|  |  | +    double y = message.pose.pose.orientation.z;
 | 
	
		
			
				|  |  | +    double z = message.pose.pose.orientation.y;
 | 
	
		
			
				|  |  | +    heading = Math.atan2(2 * y * w - 2 * x * z, x * x - y * y - z * z + w * w) * 180 / Math.PI;
 | 
	
		
			
				|  |  | +    // Negating the orientation to make the math for rotation in
 | 
	
		
			
				|  |  | +    // turn-in-place mode easy. Since the actual heading is irrelevant it does
 | 
	
		
			
				|  |  | +    // no harm.
 | 
	
		
			
				|  |  | +    currentOrientation = (float) -heading;
 | 
	
		
			
				|  |  | +    // Only update the orientation images if the turn-in-place mode is active.
 | 
	
		
			
				|  |  | +    if (turnInPlaceMode) {
 | 
	
		
			
				|  |  | +      post(new Runnable() {
 | 
	
		
			
				|  |  | +        @Override
 | 
	
		
			
				|  |  | +        public void run() {
 | 
	
		
			
				|  |  | +          updateTurnInPlaceRotation();
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +      postInvalidate();
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  @Override
 | 
	
		
			
				|  |  | +  public boolean onTouch(View v, MotionEvent event) {
 | 
	
		
			
				|  |  | +    final int action = event.getAction();
 | 
	
		
			
				|  |  | +    switch (action & MotionEvent.ACTION_MASK) {
 | 
	
		
			
				|  |  | +      case MotionEvent.ACTION_MOVE: {
 | 
	
		
			
				|  |  | +        // If the primary contact point is no longer on the screen then ignore
 | 
	
		
			
				|  |  | +        // the event.
 | 
	
		
			
				|  |  | +        if (pointerId != INVALID_POINTER_ID) {
 | 
	
		
			
				|  |  | +          // If the virtual joystick is in resume-previous-velocity mode.
 | 
	
		
			
				|  |  | +          if (previousVelocityMode) {
 | 
	
		
			
				|  |  | +            // And the current contact is close to the contact location prior to
 | 
	
		
			
				|  |  | +            // ContactUp.
 | 
	
		
			
				|  |  | +            if (inLastContactRange(event.getX(event.getActionIndex()),
 | 
	
		
			
				|  |  | +                event.getY(event.getActionIndex()))) {
 | 
	
		
			
				|  |  | +              // Then use the previous velocity.
 | 
	
		
			
				|  |  | +              onContactMove(contactUpLocation.x + joystickRadius, contactUpLocation.y
 | 
	
		
			
				|  |  | +                  + joystickRadius);
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            // Since the current contact is not close to the prior location.
 | 
	
		
			
				|  |  | +            else {
 | 
	
		
			
				|  |  | +              // Exit the resume-previous-velocity mode.
 | 
	
		
			
				|  |  | +              previousVelocityMode = false;
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +          // Since the resume-previous-velocity mode is not active generate
 | 
	
		
			
				|  |  | +          // velocities based on current contact position.
 | 
	
		
			
				|  |  | +          else {
 | 
	
		
			
				|  |  | +            onContactMove(event.getX(event.findPointerIndex(pointerId)),
 | 
	
		
			
				|  |  | +                event.getY(event.findPointerIndex(pointerId)));
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        break;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +      case MotionEvent.ACTION_DOWN: {
 | 
	
		
			
				|  |  | +        // Get the coordinates of the pointer that is initiating the
 | 
	
		
			
				|  |  | +        // interaction.
 | 
	
		
			
				|  |  | +        pointerId = event.getPointerId(event.getActionIndex());
 | 
	
		
			
				|  |  | +        onContactDown();
 | 
	
		
			
				|  |  | +        // If the current contact is close to the location of the contact prior
 | 
	
		
			
				|  |  | +        // to contactUp.
 | 
	
		
			
				|  |  | +        if (inLastContactRange(event.getX(event.getActionIndex()),
 | 
	
		
			
				|  |  | +            event.getY(event.getActionIndex()))) {
 | 
	
		
			
				|  |  | +          // Trigger resume-previous-velocity mode.
 | 
	
		
			
				|  |  | +          previousVelocityMode = true;
 | 
	
		
			
				|  |  | +          // The animation calculations/operations are performed in
 | 
	
		
			
				|  |  | +          // onContactMove(). If this is not called and the user's finger stays
 | 
	
		
			
				|  |  | +          // perfectly still after the down event, no operation is performed.
 | 
	
		
			
				|  |  | +          // Calling onContactMove avoids this.
 | 
	
		
			
				|  |  | +          onContactMove(contactUpLocation.x + joystickRadius, contactUpLocation.y + joystickRadius);
 | 
	
		
			
				|  |  | +        } else {
 | 
	
		
			
				|  |  | +          onContactMove(event.getX(event.getActionIndex()), event.getY(event.getActionIndex()));
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        break;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +      case MotionEvent.ACTION_POINTER_UP:
 | 
	
		
			
				|  |  | +      case MotionEvent.ACTION_UP: {
 | 
	
		
			
				|  |  | +        // Check if the contact that initiated the interaction is up.
 | 
	
		
			
				|  |  | +        if ((action & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT == pointerId) {
 | 
	
		
			
				|  |  | +          onContactUp();
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        break;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    return true;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Allows the user the option to turn on the auto-snap feature.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  public void EnableSnapping() {
 | 
	
		
			
				|  |  | +    magnetTheta = 10;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Allows the user the option to turn off the auto-snap feature.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  public void DisableSnapping() {
 | 
	
		
			
				|  |  | +    magnetTheta = 1;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Initialize the fields with values that can only be determined once the
 | 
	
		
			
				|  |  | +   * layout for the views has been determined.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  @Override
 | 
	
		
			
				|  |  | +  protected void onLayout(boolean changed, int l, int t, int r, int b) {
 | 
	
		
			
				|  |  | +    // Call the parent's onLayout to setup the views.
 | 
	
		
			
				|  |  | +    super.onLayout(changed, l, t, r, b);
 | 
	
		
			
				|  |  | +    // The parent container must be a square. A square container simplifies the
 | 
	
		
			
				|  |  | +    // code. A non-square container does not provide any benefit over a
 | 
	
		
			
				|  |  | +    // square.
 | 
	
		
			
				|  |  | +    if (mainLayout.getWidth() != mainLayout.getHeight()) {
 | 
	
		
			
				|  |  | +      // TODO(munjaldesai): Need to throw an exception/error. For now the
 | 
	
		
			
				|  |  | +      // touch events will not be processed.
 | 
	
		
			
				|  |  | +      this.setOnTouchListener(null);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    parentSize = mainLayout.getWidth();
 | 
	
		
			
				|  |  | +    if (parentSize < 200 || parentSize > 400) {
 | 
	
		
			
				|  |  | +      // TODO: Need to throw an exception for attempting to create
 | 
	
		
			
				|  |  | +      // a virtual joystick that is either too small or too big. For now the
 | 
	
		
			
				|  |  | +      // touch events will be processed.
 | 
	
		
			
				|  |  | +      this.setOnTouchListener(null);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    // Calculate the center coordinates (radius) of parent container
 | 
	
		
			
				|  |  | +    // (mainLayout).
 | 
	
		
			
				|  |  | +    joystickRadius = mainLayout.getWidth() / 2;
 | 
	
		
			
				|  |  | +    normalizingMultiplier = BOX_TO_CIRCLE_RATIO / (parentSize / 2);
 | 
	
		
			
				|  |  | +    // Calculate the radius of the center divet as a normalize value.
 | 
	
		
			
				|  |  | +    deadZoneRatio = THUMB_DIVET_RADIUS * normalizingMultiplier;
 | 
	
		
			
				|  |  | +    // Determine the font size for the text view showing linear velocity. 8.3%
 | 
	
		
			
				|  |  | +    // of the overall size seems to work well.
 | 
	
		
			
				|  |  | +    magnitudeText.setTextSize(parentSize / 12);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Scale and rotate the intensity circle instantaneously. The key difference
 | 
	
		
			
				|  |  | +   * between this method and {@link #animateIntensityCircle(float, long)} is
 | 
	
		
			
				|  |  | +   * that this method does not attach an animation listener and the animation is
 | 
	
		
			
				|  |  | +   * instantaneous.
 | 
	
		
			
				|  |  | +   * 
 | 
	
		
			
				|  |  | +   * @param endScale
 | 
	
		
			
				|  |  | +   *          The scale factor that must be attained at the end of the
 | 
	
		
			
				|  |  | +   *          animation.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private void animateIntensityCircle(float endScale) {
 | 
	
		
			
				|  |  | +    AnimationSet intensityCircleAnimation = new AnimationSet(true);
 | 
	
		
			
				|  |  | +    intensityCircleAnimation.setInterpolator(new LinearInterpolator());
 | 
	
		
			
				|  |  | +    intensityCircleAnimation.setFillAfter(true);
 | 
	
		
			
				|  |  | +    RotateAnimation rotateAnim;
 | 
	
		
			
				|  |  | +    rotateAnim = new RotateAnimation(contactTheta, contactTheta, joystickRadius, joystickRadius);
 | 
	
		
			
				|  |  | +    rotateAnim.setInterpolator(new LinearInterpolator());
 | 
	
		
			
				|  |  | +    rotateAnim.setDuration(0);
 | 
	
		
			
				|  |  | +    rotateAnim.setFillAfter(true);
 | 
	
		
			
				|  |  | +    intensityCircleAnimation.addAnimation(rotateAnim);
 | 
	
		
			
				|  |  | +    ScaleAnimation scaleAnim;
 | 
	
		
			
				|  |  | +    scaleAnim =
 | 
	
		
			
				|  |  | +        new ScaleAnimation(contactRadius, endScale, contactRadius, endScale, joystickRadius,
 | 
	
		
			
				|  |  | +            joystickRadius);
 | 
	
		
			
				|  |  | +    scaleAnim.setDuration(0);
 | 
	
		
			
				|  |  | +    scaleAnim.setFillAfter(true);
 | 
	
		
			
				|  |  | +    intensityCircleAnimation.addAnimation(scaleAnim);
 | 
	
		
			
				|  |  | +    // Apply the animation.
 | 
	
		
			
				|  |  | +    intensity.startAnimation(intensityCircleAnimation);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Scale and rotate the intensity circle over the specified duration. Unlike
 | 
	
		
			
				|  |  | +   * {@link #animateIntensityCircle(float)} this method registers an animation
 | 
	
		
			
				|  |  | +   * listener.
 | 
	
		
			
				|  |  | +   * 
 | 
	
		
			
				|  |  | +   * @param endScale
 | 
	
		
			
				|  |  | +   *          The scale factor that must be attained at the end of the
 | 
	
		
			
				|  |  | +   *          animation.
 | 
	
		
			
				|  |  | +   * @param duration
 | 
	
		
			
				|  |  | +   *          The duration in milliseconds the animation should take.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private void animateIntensityCircle(float endScale, long duration) {
 | 
	
		
			
				|  |  | +    AnimationSet intensityCircleAnimation = new AnimationSet(true);
 | 
	
		
			
				|  |  | +    intensityCircleAnimation.setInterpolator(new LinearInterpolator());
 | 
	
		
			
				|  |  | +    intensityCircleAnimation.setFillAfter(true);
 | 
	
		
			
				|  |  | +    // The listener is needed to set the magnitude text to 0 only after the
 | 
	
		
			
				|  |  | +    // animation is over.
 | 
	
		
			
				|  |  | +    intensityCircleAnimation.setAnimationListener(this);
 | 
	
		
			
				|  |  | +    RotateAnimation rotateAnim;
 | 
	
		
			
				|  |  | +    rotateAnim = new RotateAnimation(contactTheta, contactTheta, joystickRadius, joystickRadius);
 | 
	
		
			
				|  |  | +    rotateAnim.setInterpolator(new LinearInterpolator());
 | 
	
		
			
				|  |  | +    rotateAnim.setDuration(duration);
 | 
	
		
			
				|  |  | +    rotateAnim.setFillAfter(true);
 | 
	
		
			
				|  |  | +    intensityCircleAnimation.addAnimation(rotateAnim);
 | 
	
		
			
				|  |  | +    ScaleAnimation scaleAnim;
 | 
	
		
			
				|  |  | +    scaleAnim =
 | 
	
		
			
				|  |  | +        new ScaleAnimation(contactRadius, endScale, contactRadius, endScale, joystickRadius,
 | 
	
		
			
				|  |  | +            joystickRadius);
 | 
	
		
			
				|  |  | +    scaleAnim.setDuration(duration);
 | 
	
		
			
				|  |  | +    scaleAnim.setFillAfter(true);
 | 
	
		
			
				|  |  | +    intensityCircleAnimation.addAnimation(scaleAnim);
 | 
	
		
			
				|  |  | +    // Apply the animation.
 | 
	
		
			
				|  |  | +    intensity.startAnimation(intensityCircleAnimation);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Fade in and fade out the {@link #orientationWidget}s. The widget best
 | 
	
		
			
				|  |  | +   * aligned with the {@link #contactTheta} will be the brightest and the
 | 
	
		
			
				|  |  | +   * successive ones within {@link #ORIENTATION_TACK_FADE_RANGE} the will be
 | 
	
		
			
				|  |  | +   * faded out proportionally. The tacks out of that range will have alpha set
 | 
	
		
			
				|  |  | +   * to 0.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private void animateOrientationWidgets() {
 | 
	
		
			
				|  |  | +    float deltaTheta;
 | 
	
		
			
				|  |  | +    for (int i = 0; i < orientationWidget.length; i++) {
 | 
	
		
			
				|  |  | +      deltaTheta = differenceBetweenAngles(i * 15, contactTheta);
 | 
	
		
			
				|  |  | +      if (deltaTheta < ORIENTATION_TACK_FADE_RANGE) {
 | 
	
		
			
				|  |  | +        orientationWidget[i].setAlpha(1.0f - deltaTheta / ORIENTATION_TACK_FADE_RANGE);
 | 
	
		
			
				|  |  | +      } else {
 | 
	
		
			
				|  |  | +        orientationWidget[i].setAlpha(0.0f);
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * From http://actionsnippet.com/?p=1451. Calculates the difference between 2
 | 
	
		
			
				|  |  | +   * angles. The result is always the minimum difference between 2 angles (0<
 | 
	
		
			
				|  |  | +   * result <= 360).
 | 
	
		
			
				|  |  | +   * 
 | 
	
		
			
				|  |  | +   * @param angle0
 | 
	
		
			
				|  |  | +   *          One of 2 angles used to calculate difference. The order of
 | 
	
		
			
				|  |  | +   *          arguments does not matter. Must be in degrees.
 | 
	
		
			
				|  |  | +   * @param angle1
 | 
	
		
			
				|  |  | +   *          One of 2 angles used to calculate difference. The order of
 | 
	
		
			
				|  |  | +   *          arguments does not matter. Must be in degrees.
 | 
	
		
			
				|  |  | +   * @return The difference between the 2 arguments in degrees.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private float differenceBetweenAngles(float angle0, float angle1) {
 | 
	
		
			
				|  |  | +    return Math.abs((angle0 + 180 - angle1) % 360 - 180);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Sets {@link #turnInPlaceMode} to false indicating that the turn-in-place is
 | 
	
		
			
				|  |  | +   * no longer active. It also changes the alpha values appropriately.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private void endTurnInPlaceRotation() {
 | 
	
		
			
				|  |  | +    turnInPlaceMode = false;
 | 
	
		
			
				|  |  | +    currentRotationRange.setAlpha(0.0f);
 | 
	
		
			
				|  |  | +    previousRotationRange.setAlpha(0.0f);
 | 
	
		
			
				|  |  | +    intensity.setAlpha(1.0f);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Sets up the visual elements of the virtual joystick.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private void initVirtualJoystick() {
 | 
	
		
			
				|  |  | +    mainLayout =
 | 
	
		
			
				|  |  | +        (RelativeLayout) findViewById(org.ros.rosjava.android.R.id.virtual_joystick_layout);
 | 
	
		
			
				|  |  | +    magnitudeText = (TextView) findViewById(org.ros.rosjava.android.R.id.magnitude);
 | 
	
		
			
				|  |  | +    intensity = (ImageView) findViewById(org.ros.rosjava.android.R.id.intensity);
 | 
	
		
			
				|  |  | +    thumbDivet = (ImageView) findViewById(org.ros.rosjava.android.R.id.thumb_divet);
 | 
	
		
			
				|  |  | +    orientationWidget = new ImageView[24];
 | 
	
		
			
				|  |  | +    for (ImageView widget : orientationWidget) {
 | 
	
		
			
				|  |  | +      widget = new ImageView(getContext());
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    orientationWidget[0] = (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_0_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[1] = (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_15_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[2] = (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_30_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[3] = (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_45_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[4] = (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_60_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[5] = (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_75_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[6] = (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_90_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[7] =
 | 
	
		
			
				|  |  | +        (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_105_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[8] =
 | 
	
		
			
				|  |  | +        (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_120_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[9] =
 | 
	
		
			
				|  |  | +        (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_135_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[10] =
 | 
	
		
			
				|  |  | +        (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_150_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[11] =
 | 
	
		
			
				|  |  | +        (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_165_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[12] =
 | 
	
		
			
				|  |  | +        (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_180_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[13] =
 | 
	
		
			
				|  |  | +        (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_195_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[14] =
 | 
	
		
			
				|  |  | +        (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_210_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[15] =
 | 
	
		
			
				|  |  | +        (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_225_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[16] =
 | 
	
		
			
				|  |  | +        (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_240_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[17] =
 | 
	
		
			
				|  |  | +        (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_255_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[18] =
 | 
	
		
			
				|  |  | +        (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_270_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[19] =
 | 
	
		
			
				|  |  | +        (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_285_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[20] =
 | 
	
		
			
				|  |  | +        (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_300_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[21] =
 | 
	
		
			
				|  |  | +        (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_315_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[22] =
 | 
	
		
			
				|  |  | +        (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_330_degrees);
 | 
	
		
			
				|  |  | +    orientationWidget[23] =
 | 
	
		
			
				|  |  | +        (ImageView) findViewById(org.ros.rosjava.android.R.id.widget_345_degrees);
 | 
	
		
			
				|  |  | +    // Initially hide all the widgets.
 | 
	
		
			
				|  |  | +    for (ImageView tack : orientationWidget) {
 | 
	
		
			
				|  |  | +      tack.setAlpha(0.0f);
 | 
	
		
			
				|  |  | +      tack.setVisibility(INVISIBLE);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    // The value (radius) 40 is arbitrary, but small enough to work for the
 | 
	
		
			
				|  |  | +    // smallest sized virtual joystick. Once the layout is set a value is
 | 
	
		
			
				|  |  | +    // calculated based on the size of the virtual joystick.
 | 
	
		
			
				|  |  | +    magnitudeText.setTranslationX((float) (40 * Math.cos((90 + contactTheta) * Math.PI / 180.0)));
 | 
	
		
			
				|  |  | +    magnitudeText.setTranslationY((float) (40 * Math.sin((90 + contactTheta) * Math.PI / 180.0)));
 | 
	
		
			
				|  |  | +    // Hide the intensity circle.
 | 
	
		
			
				|  |  | +    animateIntensityCircle(0);
 | 
	
		
			
				|  |  | +    // Initially the orientationWidgets should point to 0 degrees.
 | 
	
		
			
				|  |  | +    contactTheta = 0;
 | 
	
		
			
				|  |  | +    animateOrientationWidgets();
 | 
	
		
			
				|  |  | +    currentRotationRange = (ImageView) findViewById(org.ros.rosjava.android.R.id.top_angle_slice);
 | 
	
		
			
				|  |  | +    previousRotationRange = (ImageView) findViewById(org.ros.rosjava.android.R.id.mid_angle_slice);
 | 
	
		
			
				|  |  | +    // Hide the slices/arcs used during the turn-in-place mode.
 | 
	
		
			
				|  |  | +    currentRotationRange.setAlpha(0.0f);
 | 
	
		
			
				|  |  | +    previousRotationRange.setAlpha(0.0f);
 | 
	
		
			
				|  |  | +    lastVelocityDivet =
 | 
	
		
			
				|  |  | +        (ImageView) findViewById(org.ros.rosjava.android.R.id.previous_velocity_divet);
 | 
	
		
			
				|  |  | +    contactUpLocation = new Point(0, 0);
 | 
	
		
			
				|  |  | +    for (ImageView tack : orientationWidget) {
 | 
	
		
			
				|  |  | +      tack.setVisibility(INVISIBLE);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    // Create a timer.
 | 
	
		
			
				|  |  | +    publisherTimer = new Timer();
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Update the virtual joystick to indicate a contact down has occurred.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private void onContactDown() {
 | 
	
		
			
				|  |  | +    // The divets should be completely opaque indicating
 | 
	
		
			
				|  |  | +    // the virtual joystick is active.
 | 
	
		
			
				|  |  | +    thumbDivet.setAlpha(1.0f);
 | 
	
		
			
				|  |  | +    magnitudeText.setAlpha(1.0f);
 | 
	
		
			
				|  |  | +    // Previous contact location need not be shown any more.
 | 
	
		
			
				|  |  | +    lastVelocityDivet.setAlpha(0.0f);
 | 
	
		
			
				|  |  | +    // Restore the orientation tacks.
 | 
	
		
			
				|  |  | +    for (ImageView tack : orientationWidget) {
 | 
	
		
			
				|  |  | +      tack.setVisibility(VISIBLE);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    publishVelocity = true;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Updates the virtual joystick layout based on the location of the contact.
 | 
	
		
			
				|  |  | +   * Generates the velocity messages. Switches in and out of turn-in-place.
 | 
	
		
			
				|  |  | +   * 
 | 
	
		
			
				|  |  | +   * @param x
 | 
	
		
			
				|  |  | +   *          The x coordinates of the contact relative to the parent container.
 | 
	
		
			
				|  |  | +   * @param y
 | 
	
		
			
				|  |  | +   *          The y coordinates of the contact relative to the parent container.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private void onContactMove(float x, float y) {
 | 
	
		
			
				|  |  | +    // Get the coordinates of the contact relative to the center of the main
 | 
	
		
			
				|  |  | +    // layout.
 | 
	
		
			
				|  |  | +    float thumbDivetX = x - joystickRadius;
 | 
	
		
			
				|  |  | +    float thumbDivetY = y - joystickRadius;
 | 
	
		
			
				|  |  | +    // Convert the coordinates from Cartesian to Polar.
 | 
	
		
			
				|  |  | +    contactTheta = (float) (Math.atan2(thumbDivetY, thumbDivetX) * 180 / Math.PI + 90);
 | 
	
		
			
				|  |  | +    contactRadius =
 | 
	
		
			
				|  |  | +        (float) Math.sqrt(thumbDivetX * thumbDivetX + thumbDivetY * thumbDivetY)
 | 
	
		
			
				|  |  | +            * normalizingMultiplier;
 | 
	
		
			
				|  |  | +    // Calculate the distance (0 to 1) from the center divet to the contact
 | 
	
		
			
				|  |  | +    // point.
 | 
	
		
			
				|  |  | +    normalizedMagnitude = (contactRadius - deadZoneRatio) / (1 - deadZoneRatio);
 | 
	
		
			
				|  |  | +    // Perform bounds checking.
 | 
	
		
			
				|  |  | +    if (contactRadius >= 1f) {
 | 
	
		
			
				|  |  | +      // Since the contact is outside the outer ring, reset the coordinate for
 | 
	
		
			
				|  |  | +      // the thumb divet to the on the outer ring.
 | 
	
		
			
				|  |  | +      thumbDivetX /= contactRadius;
 | 
	
		
			
				|  |  | +      thumbDivetY /= contactRadius;
 | 
	
		
			
				|  |  | +      // The magnitude should not exceed 1.
 | 
	
		
			
				|  |  | +      normalizedMagnitude = 1f;
 | 
	
		
			
				|  |  | +      contactRadius = 1f;
 | 
	
		
			
				|  |  | +    } else if (contactRadius < deadZoneRatio) {
 | 
	
		
			
				|  |  | +      // Since the contact is inside the dead zone snap the thumb divet to the
 | 
	
		
			
				|  |  | +      // dead zone. It should stay there till the contact gets outside the
 | 
	
		
			
				|  |  | +      // deadzone area.
 | 
	
		
			
				|  |  | +      thumbDivetX = 0;
 | 
	
		
			
				|  |  | +      thumbDivetY = 0;
 | 
	
		
			
				|  |  | +      // Prevent normalizedMagnitude going negative inside the deadzone.
 | 
	
		
			
				|  |  | +      normalizedMagnitude = 0f;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // Magnetize!
 | 
	
		
			
				|  |  | +    // If the contact is not snapped to the x axis.
 | 
	
		
			
				|  |  | +    if (!magnetizedXAxis) {
 | 
	
		
			
				|  |  | +      // Check if the contact should be snapped to either axis.
 | 
	
		
			
				|  |  | +      if ((contactTheta + 360) % 90 < magnetTheta) {
 | 
	
		
			
				|  |  | +        // If the current angle is within MAGNET_THETA degrees + 0, 90, 180, or
 | 
	
		
			
				|  |  | +        // 270 then subtract the additional degrees so that the current theta is
 | 
	
		
			
				|  |  | +        // 0, 90, 180, or 270.
 | 
	
		
			
				|  |  | +        contactTheta -= ((contactTheta + 360) % 90);
 | 
	
		
			
				|  |  | +      } else if ((contactTheta + 360) % 90 > (90 - magnetTheta)) {
 | 
	
		
			
				|  |  | +        // If the current angle is within MAGNET_THETA degrees - 0, 90, 180, or
 | 
	
		
			
				|  |  | +        // 270 then add the additional degrees so that the current theta is 0,
 | 
	
		
			
				|  |  | +        // 90, 180, or 270.
 | 
	
		
			
				|  |  | +        contactTheta += (90 - ((contactTheta + 360) % 90));
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +      // Indicate that the contact has been snapped to the x-axis.
 | 
	
		
			
				|  |  | +      if (floatCompare(contactTheta, 90) || floatCompare(contactTheta, 270)) {
 | 
	
		
			
				|  |  | +        magnetizedXAxis = true;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    } else {
 | 
	
		
			
				|  |  | +      // Use a wider range to keep the contact snapped in.
 | 
	
		
			
				|  |  | +      if (differenceBetweenAngles((contactTheta + 360) % 360, 90) < POST_LOCK_MAGNET_THETA) {
 | 
	
		
			
				|  |  | +        contactTheta = 90;
 | 
	
		
			
				|  |  | +      } else if (differenceBetweenAngles((contactTheta + 360) % 360, 270) < POST_LOCK_MAGNET_THETA) {
 | 
	
		
			
				|  |  | +        contactTheta = 270;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +      // Indicate that the contact is not snapped to the x-axis.
 | 
	
		
			
				|  |  | +      else {
 | 
	
		
			
				|  |  | +        magnetizedXAxis = false;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // Update the size and location (scale and rotation) of various elements.
 | 
	
		
			
				|  |  | +    animateIntensityCircle(contactRadius);
 | 
	
		
			
				|  |  | +    animateOrientationWidgets();
 | 
	
		
			
				|  |  | +    updateThumbDivet(thumbDivetX, thumbDivetY);
 | 
	
		
			
				|  |  | +    updateMagnitudeText();
 | 
	
		
			
				|  |  | +    // Publish the velocities.
 | 
	
		
			
				|  |  | +    publishVelocity(normalizedMagnitude * Math.cos(contactTheta * Math.PI / 180.0),
 | 
	
		
			
				|  |  | +        normalizedMagnitude * Math.sin(contactTheta * Math.PI / 180.0));
 | 
	
		
			
				|  |  | +    // Check if the turn-in-place mode needs to be activated/deactivated.
 | 
	
		
			
				|  |  | +    updateTurnInPlaceMode();
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Enable/Disable turn-in-place mode.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private void updateTurnInPlaceMode() {
 | 
	
		
			
				|  |  | +    if (!turnInPlaceMode) {
 | 
	
		
			
				|  |  | +      if (floatCompare(contactTheta, 270)) {
 | 
	
		
			
				|  |  | +        // If the user is turning left and the turn-in-place mode is not active
 | 
	
		
			
				|  |  | +        // then activate it for a left turn.
 | 
	
		
			
				|  |  | +        turnInPlaceMode = true;
 | 
	
		
			
				|  |  | +        rightTurnOffset = 0;
 | 
	
		
			
				|  |  | +      } else if (floatCompare(contactTheta, 90)) {
 | 
	
		
			
				|  |  | +        // If the user is turning right and the turn-in-place mode is not active
 | 
	
		
			
				|  |  | +        // then activate it for a right turn.
 | 
	
		
			
				|  |  | +        turnInPlaceMode = true;
 | 
	
		
			
				|  |  | +        rightTurnOffset = 15;
 | 
	
		
			
				|  |  | +      } else {
 | 
	
		
			
				|  |  | +        // Nothing to do while not in turn-in-place mode and not at 270/90.
 | 
	
		
			
				|  |  | +        return;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +      // Initiate the turn-in-place mode but wait some time before changing the
 | 
	
		
			
				|  |  | +      // images. This is to avoid the users getting seizures because of the
 | 
	
		
			
				|  |  | +      // quick changes every time they cross 270 or 90.
 | 
	
		
			
				|  |  | +      initiateTurnInPlace();
 | 
	
		
			
				|  |  | +      // Start a timer and if the user is still turning in place when the timer
 | 
	
		
			
				|  |  | +      // is up, then visually indicate entering turn-in-place mode.
 | 
	
		
			
				|  |  | +      new Timer().schedule(new TimerTask() {
 | 
	
		
			
				|  |  | +        @Override
 | 
	
		
			
				|  |  | +        public void run() {
 | 
	
		
			
				|  |  | +          post(new Runnable() {
 | 
	
		
			
				|  |  | +            @Override
 | 
	
		
			
				|  |  | +            public void run() {
 | 
	
		
			
				|  |  | +              if (turnInPlaceMode) {
 | 
	
		
			
				|  |  | +                currentRotationRange.setAlpha(1.0f);
 | 
	
		
			
				|  |  | +                previousRotationRange.setAlpha(1.0f);
 | 
	
		
			
				|  |  | +                intensity.setAlpha(0.2f);
 | 
	
		
			
				|  |  | +              }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +          });
 | 
	
		
			
				|  |  | +          postInvalidate();
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      }, TURN_IN_PLACE_CONFIRMATION_DELAY);
 | 
	
		
			
				|  |  | +    } else if (!(floatCompare(contactTheta, 270) || floatCompare(contactTheta, 90))) {
 | 
	
		
			
				|  |  | +      // If the user was in turn-in-place mode and is now no longer on the x
 | 
	
		
			
				|  |  | +      // axis, then exit turn-in-place mode.
 | 
	
		
			
				|  |  | +      endTurnInPlaceRotation();
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * The divets and the ring are made transparent to reflect that the virtual
 | 
	
		
			
				|  |  | +   * joystick is no longer active. The intensity circle is slowly scaled to 0.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private void onContactUp() {
 | 
	
		
			
				|  |  | +    // TODO(munjaldesai): The 1000 should eventually be replaced with a number
 | 
	
		
			
				|  |  | +    // that reflects the physical characteristics of the motor controller along
 | 
	
		
			
				|  |  | +    // with the latency associated with the connection.
 | 
	
		
			
				|  |  | +    animateIntensityCircle(0, (long) (normalizedMagnitude * 1000));
 | 
	
		
			
				|  |  | +    magnitudeText.setAlpha(0.4f);
 | 
	
		
			
				|  |  | +    // Place the lastVelocityDivet at the location of the last known contact.
 | 
	
		
			
				|  |  | +    lastVelocityDivet.setTranslationX(thumbDivet.getTranslationX());
 | 
	
		
			
				|  |  | +    lastVelocityDivet.setTranslationY(thumbDivet.getTranslationY());
 | 
	
		
			
				|  |  | +    lastVelocityDivet.setAlpha(0.4f);
 | 
	
		
			
				|  |  | +    contactUpLocation.x = (int) (thumbDivet.getTranslationX());
 | 
	
		
			
				|  |  | +    contactUpLocation.y = (int) (thumbDivet.getTranslationY());
 | 
	
		
			
				|  |  | +    // Move the thumb divet back to the center.
 | 
	
		
			
				|  |  | +    updateThumbDivet(0, 0);
 | 
	
		
			
				|  |  | +    // Reset the pointer id.
 | 
	
		
			
				|  |  | +    pointerId = INVALID_POINTER_ID;
 | 
	
		
			
				|  |  | +    // The robot should stop moving.
 | 
	
		
			
				|  |  | +    publishVelocity(0, 0);
 | 
	
		
			
				|  |  | +    // Turn-in-place should not be active anymore.
 | 
	
		
			
				|  |  | +    endTurnInPlaceRotation();
 | 
	
		
			
				|  |  | +    // Hide the orientation tacks.
 | 
	
		
			
				|  |  | +    for (ImageView tack : orientationWidget) {
 | 
	
		
			
				|  |  | +      tack.setVisibility(INVISIBLE);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    // Stop publishing the velocity since the contact is no longer on the
 | 
	
		
			
				|  |  | +    // screen.
 | 
	
		
			
				|  |  | +    publishVelocity = false;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Publish the velocity as a ROS Twist message.
 | 
	
		
			
				|  |  | +   * 
 | 
	
		
			
				|  |  | +   * @param linearV
 | 
	
		
			
				|  |  | +   *          The normalized linear velocity (-1 to 1).
 | 
	
		
			
				|  |  | +   * @param angularV
 | 
	
		
			
				|  |  | +   *          The normalized angular velocity (-1 to 1).
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private void publishVelocity(double linearV, double angularV) {
 | 
	
		
			
				|  |  | +    vel.linear.x = linearV;
 | 
	
		
			
				|  |  | +    vel.linear.y = 0;
 | 
	
		
			
				|  |  | +    vel.linear.z = 0;
 | 
	
		
			
				|  |  | +    vel.angular.x = 0;
 | 
	
		
			
				|  |  | +    vel.angular.y = 0;
 | 
	
		
			
				|  |  | +    vel.angular.z = -angularV;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Specify the location of the master node to which the velocity messages will
 | 
	
		
			
				|  |  | +   * be published. Unless this method is called no messages will be published.
 | 
	
		
			
				|  |  | +   * 
 | 
	
		
			
				|  |  | +   * @param masterUri
 | 
	
		
			
				|  |  | +   *          The location of the master node.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  public void setMasterUri(URI masterUri) {
 | 
	
		
			
				|  |  | +    // Setup the publisher first.
 | 
	
		
			
				|  |  | +    initNode(masterUri);
 | 
	
		
			
				|  |  | +    this.setOnTouchListener(this);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Called each time turn-in-place mode is initiated.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private void initiateTurnInPlace() {
 | 
	
		
			
				|  |  | +    // Record the orientation when the turn-in-place was initiated.
 | 
	
		
			
				|  |  | +    turnInPlaceStartTheta = (currentOrientation + 360) % 360;
 | 
	
		
			
				|  |  | +    RotateAnimation rotateAnim;
 | 
	
		
			
				|  |  | +    rotateAnim =
 | 
	
		
			
				|  |  | +        new RotateAnimation(rightTurnOffset, rightTurnOffset, joystickRadius, joystickRadius);
 | 
	
		
			
				|  |  | +    rotateAnim.setInterpolator(new LinearInterpolator());
 | 
	
		
			
				|  |  | +    rotateAnim.setDuration(0);
 | 
	
		
			
				|  |  | +    rotateAnim.setFillAfter(true);
 | 
	
		
			
				|  |  | +    currentRotationRange.startAnimation(rotateAnim);
 | 
	
		
			
				|  |  | +    rotateAnim = new RotateAnimation(15, 15, joystickRadius, joystickRadius);
 | 
	
		
			
				|  |  | +    rotateAnim.setInterpolator(new LinearInterpolator());
 | 
	
		
			
				|  |  | +    rotateAnim.setDuration(0);
 | 
	
		
			
				|  |  | +    rotateAnim.setFillAfter(true);
 | 
	
		
			
				|  |  | +    previousRotationRange.startAnimation(rotateAnim);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Update the linear velocity text view.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private void updateMagnitudeText() {
 | 
	
		
			
				|  |  | +    // Don't update when the user is turning in place.
 | 
	
		
			
				|  |  | +    if (!turnInPlaceMode) {
 | 
	
		
			
				|  |  | +      magnitudeText.setText(String.valueOf((int) (normalizedMagnitude * 100)) + "%");
 | 
	
		
			
				|  |  | +      magnitudeText.setTranslationX((float) (parentSize / 4 * Math.cos((90 + contactTheta)
 | 
	
		
			
				|  |  | +          * Math.PI / 180.0)));
 | 
	
		
			
				|  |  | +      magnitudeText.setTranslationY((float) (parentSize / 4 * Math.sin((90 + contactTheta)
 | 
	
		
			
				|  |  | +          * Math.PI / 180.0)));
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Based on the difference between the current orientation and the orientation
 | 
	
		
			
				|  |  | +   * when the turn-in-place mode was initiated, update the visuals.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private void updateTurnInPlaceRotation() {
 | 
	
		
			
				|  |  | +    final float currentTheta = (currentOrientation + 360) % 360;
 | 
	
		
			
				|  |  | +    float offsetTheta;
 | 
	
		
			
				|  |  | +    // Calculate the difference between the orientations.
 | 
	
		
			
				|  |  | +    offsetTheta = (turnInPlaceStartTheta - currentTheta + 360) % 360;
 | 
	
		
			
				|  |  | +    offsetTheta = 360 - offsetTheta;
 | 
	
		
			
				|  |  | +    // Show the current rotation amount.
 | 
	
		
			
				|  |  | +    magnitudeText.setText(String.valueOf((int) offsetTheta));
 | 
	
		
			
				|  |  | +    // Calculate theta in increments of 15 degrees. (0-14 => 0, 15-29=>15, etc).
 | 
	
		
			
				|  |  | +    offsetTheta = (int) (offsetTheta - (offsetTheta % 15));
 | 
	
		
			
				|  |  | +    // Rotate the 2 arcs based on the offset in orientation.
 | 
	
		
			
				|  |  | +    RotateAnimation rotateAnim;
 | 
	
		
			
				|  |  | +    rotateAnim =
 | 
	
		
			
				|  |  | +        new RotateAnimation(offsetTheta + rightTurnOffset, offsetTheta + rightTurnOffset,
 | 
	
		
			
				|  |  | +            joystickRadius, joystickRadius);
 | 
	
		
			
				|  |  | +    rotateAnim.setInterpolator(new LinearInterpolator());
 | 
	
		
			
				|  |  | +    rotateAnim.setDuration(0);
 | 
	
		
			
				|  |  | +    rotateAnim.setFillAfter(true);
 | 
	
		
			
				|  |  | +    currentRotationRange.startAnimation(rotateAnim);
 | 
	
		
			
				|  |  | +    rotateAnim =
 | 
	
		
			
				|  |  | +        new RotateAnimation(offsetTheta + 15, offsetTheta + 15, joystickRadius, joystickRadius);
 | 
	
		
			
				|  |  | +    rotateAnim.setInterpolator(new LinearInterpolator());
 | 
	
		
			
				|  |  | +    rotateAnim.setDuration(0);
 | 
	
		
			
				|  |  | +    rotateAnim.setFillAfter(true);
 | 
	
		
			
				|  |  | +    previousRotationRange.startAnimation(rotateAnim);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Moves the {@link #thumbDivet} to the specified coordinates (under the
 | 
	
		
			
				|  |  | +   * contact) and also orients it so that is facing the direction opposite to
 | 
	
		
			
				|  |  | +   * the center of the {@link #mainLayout}.
 | 
	
		
			
				|  |  | +   * 
 | 
	
		
			
				|  |  | +   * @param x
 | 
	
		
			
				|  |  | +   *          The x coordinate relative to the center of the {@link #mainLayout}
 | 
	
		
			
				|  |  | +   * @param y
 | 
	
		
			
				|  |  | +   *          The Y coordinate relative to the center of the {@link #mainLayout}
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private void updateThumbDivet(float x, float y) {
 | 
	
		
			
				|  |  | +    // Offset the specified coordinates to ensure that the center of the thumb
 | 
	
		
			
				|  |  | +    // divet is under the thumb.
 | 
	
		
			
				|  |  | +    thumbDivet.setTranslationX(-THUMB_DIVET_RADIUS);
 | 
	
		
			
				|  |  | +    thumbDivet.setTranslationY(-THUMB_DIVET_RADIUS);
 | 
	
		
			
				|  |  | +    // Set the orientation. This must be done before translation.
 | 
	
		
			
				|  |  | +    thumbDivet.setRotation(contactTheta);
 | 
	
		
			
				|  |  | +    thumbDivet.setTranslationX(x);
 | 
	
		
			
				|  |  | +    thumbDivet.setTranslationY(y);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Comparing 2 float values.
 | 
	
		
			
				|  |  | +   * 
 | 
	
		
			
				|  |  | +   * @param v1
 | 
	
		
			
				|  |  | +   * @param v2
 | 
	
		
			
				|  |  | +   * @return True if v1 and v2 and within {@value #FLOAT_EPSILON} of each other.
 | 
	
		
			
				|  |  | +   *         False otherwise.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private boolean floatCompare(float v1, float v2) {
 | 
	
		
			
				|  |  | +    if (Math.abs(v1 - v2) < FLOAT_EPSILON) {
 | 
	
		
			
				|  |  | +      return true;
 | 
	
		
			
				|  |  | +    } else {
 | 
	
		
			
				|  |  | +      return false;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  private boolean inLastContactRange(float x, float y) {
 | 
	
		
			
				|  |  | +    if (Math.sqrt((x - contactUpLocation.x - joystickRadius)
 | 
	
		
			
				|  |  | +        * (x - contactUpLocation.x - joystickRadius) + (y - contactUpLocation.y - joystickRadius)
 | 
	
		
			
				|  |  | +        * (y - contactUpLocation.y - joystickRadius)) < THUMB_DIVET_RADIUS) {
 | 
	
		
			
				|  |  | +      return true;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    return false;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 |