/**
* Tricorder: turn your phone into a tricorder.
*
* This is an Android implementation of a Star Trek tricorder, based on
* the phone's own sensors. It's also a demo project for sensor access.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2
* as published by the Free Software Foundation (see COPYING).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
package org.hermit.tricorder;
import org.hermit.android.core.SurfaceRunner;
import org.hermit.android.sound.Effect;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.view.MotionEvent;
import android.view.Surface;
/**
* A view which displays 3-axis sensor data in a variety of ways.
*
* This could be used, for example, to show the accelerometer or
* compass values.
*/
class TridataView
extends DataView
implements SensorEventListener
{
// ******************************************************************** //
// Constructor.
// ******************************************************************** //
/**
* Set up this view.
*
* @param context Parent application context.
* @param parent Parent surface.
* @param sman The SensorManager to get data from.
* @param sensor The ID of the sensor to read:
* Sensor.TYPE_XXX.
* @param unit The size of a unit of measure (for example,
* 1g of acceleration).
* @param range How many units big to make the graph.
* @param gridCol1 Colour for the graph grid in abs mode.
* @param plotCol1 Colour for the graph plot in abs mode.
* @param gridCol2 Colour for the graph grid in rel mode.
* @param plotCol2 Colour for the graph plot in rel mode.
* @param sound Sound to play while scanning.
*/
public TridataView(Tricorder context, SurfaceRunner parent,
SensorManager sman, int sensor,
float unit, float range,
int gridCol1, int plotCol1,
int gridCol2, int plotCol2, Effect sound)
{
super(context, parent);
// Get the UI strings.
title_vect_abs = parent.getRes(R.string.title_vect_abs);
title_mag_abs = parent.getRes(R.string.title_mag_abs);
title_num_abs = parent.getRes(R.string.title_num_abs);
title_xyz_abs = parent.getRes(R.string.title_xyz_abs);
title_vect_rel = parent.getRes(R.string.title_vect_rel);
title_mag_rel = parent.getRes(R.string.title_mag_rel);
title_num_rel = parent.getRes(R.string.title_num_rel);
title_xyz_rel = parent.getRes(R.string.title_xyz_rel);
appContext = context;
sensorManager = sman;
sensorId = sensor;
dataUnit = unit;
dataRange = range;
gridColour1 = gridCol1;
plotColour1 = plotCol1;
gridColour2 = gridCol2;
plotColour2 = plotCol2;
scanSound = sound;
processedValues = new float[3];
// Add the gravity 3-axis plot.
plotView = new AxisElement(parent, unit, range,
gridCol1, plotCol1,
new String[] { "XXXXXXXXXXXXXXXXXXXX" });
// Add the gravity magnitude chart.
chartView = new MagnitudeElement(parent, unit, range,
gridCol1, plotCol1,
new String[] { "XXXXXXXXXXXXXXXXXXXX" });
chartView.setScrolling(false);
// Add the numeric display.
numView = new Num3DElement(parent, gridCol1, plotCol1,
new String[] { "XXXXXXXXXXXXXXXXXXXX" });
xyzView = new MagnitudeElement(parent, 3, unit, range,
gridCol1, XYZ_PLOT_COLS,
new String[] { "XXXXXXXXXXXXXXXXXXXX" }, true);
xyzView.setScrolling(false);
viewEnabled = false;
scanEnabled = false;
setRelativeMode(false);
}
// ******************************************************************** //
// Geometry Management.
// ******************************************************************** //
/**
* This is called during layout when the size of this element has
* changed. This is where we first discover our size, so set
* our geometry to match.
*
* @param bounds The bounding rect of this element within
* its parent View.
*/
@Override
public void setGeometry(Rect bounds) {
super.setGeometry(bounds);
if (bounds.right - bounds.left < bounds.bottom - bounds.top)
layoutPortrait(bounds);
else
layoutLandscape(bounds);
plotBounds = plotView.getBounds();
numBounds = numView.getBounds();
}
/**
* Set up the layout of this view in portrait mode.
*
* @param bounds The bounding rect of this element within
* its parent View.
*/
private void layoutPortrait(Rect bounds) {
final int pad = getInterPadding();
final int h = bounds.bottom - bounds.top;
final int plotHeight = h / 3;
int res = h - plotHeight - pad * 2;
final int numHeight = res / 2;
final int chartHeight = res / 2;
int sx = bounds.left + pad;
int ex = bounds.right;
int y = bounds.top;
plotView.setGeometry(new Rect(sx, y, ex, y + plotHeight));
y += plotHeight + pad;
chartView.setGeometry(new Rect(sx, y, ex, y + chartHeight));
y += chartHeight + pad;
// The numeric and XYZ views are alternatives and go in the
// same place.
numView.setGeometry(new Rect(sx, y, ex, y + numHeight));
xyzView.setGeometry(new Rect(sx, y, ex, y + numHeight));
}
/**
* Set up the layout of this view in landscape mode.
*
* @param bounds The bounding rect of this element within
* its parent View.
*/
private void layoutLandscape(Rect bounds) {
final int pad = getInterPadding();
final int w = bounds.right - bounds.left;
final int h = bounds.bottom - bounds.top;
final int sx = bounds.left + pad;
final int ex = bounds.right;
int x = sx;
int y = bounds.top;
int plotWidth = (w - pad) / 3;
plotView.setGeometry(new Rect(x, y, x + plotWidth, y + h));
x += plotWidth + pad;
int chartHeight = (h - pad) / 2;
chartView.setGeometry(new Rect(x, y, ex, y + chartHeight));
y += chartHeight + pad;
// The numeric and XYZ views are alternatives and go in the
// same place.
numView.setGeometry(new Rect(x, y, ex, y + chartHeight));
xyzView.setGeometry(new Rect(x, y, ex, y + chartHeight));
}
/**
* Set the device rotation, so that we
* can adjust the sensor axes to match the screen axes.
*
* @param rotation Device rotation, as one of the
* Surface.ROTATION_XXX flags.
*/
public void setRotation(int rotation) {
switch (rotation) {
case Surface.ROTATION_0:
deviceTransformation = TRANSFORM_0;
break;
case Surface.ROTATION_90:
deviceTransformation = TRANSFORM_90;
break;
case Surface.ROTATION_180:
deviceTransformation = TRANSFORM_180;
break;
case Surface.ROTATION_270:
deviceTransformation = TRANSFORM_270;
break;
}
}
// ******************************************************************** //
// Configuration.
// ******************************************************************** //
/**
* Set the general scanning mode. This affects whichever views support
* it.
*
* @param continuous If true, scan all the time. Otherwise,
* scan only under user control.
*/
@Override
void setScanMode(boolean continuous) {
scanContinuously = continuous;
if (scanContinuously)
appContext.setAuxButton(R.string.lab_blank);
else
appContext.setAuxButton(R.string.lab_scan_start);
}
/**
* Set the general scanning mode. This affects whichever views support
* it.
*
* @param enable If true, play a sound while scanning
* under user control. Else don't.
*/
@Override
void setScanSound(boolean enable) {
scanPlaySound = enable;
}
/**
* Set or reset relative mode. In relative mode, we report all
* values relative to the values that were in force when we entered
* it. In absolute mode, we always report the absolute values.
*
* We set the header fields in each part of the display to reflect
* the mode.
*
* @param rel
*/
void setRelativeMode(boolean rel) {
relativeMode = rel;
relativeValues = null;
if (!relativeMode) {
plotView.setText(0, 0, title_vect_abs);
plotView.setDataColors(gridColour1, plotColour1);
chartView.setText(0, 0, title_mag_abs);
chartView.setDataColors(gridColour1, plotColour1);
numView.setText(0, 0, title_num_abs);
numView.setDataColors(gridColour1, plotColour1);
xyzView.setText(0, 0, title_xyz_abs);
xyzView.setDataColors(gridColour1, XYZ_PLOT_COLS);
} else {
plotView.setText(0, 0, title_vect_rel);
plotView.setDataColors(gridColour2, plotColour2);
chartView.setText(0, 0, title_mag_rel);
chartView.setDataColors(gridColour2, plotColour2);
numView.setText(0, 0, title_num_rel);
numView.setDataColors(gridColour2, plotColour2);
xyzView.setText(0, 0, title_xyz_rel);
xyzView.setDataColors(gridColour2, XYZ_PLOT_COLS);
}
}
// ******************************************************************** //
// State Management.
// ******************************************************************** //
/**
* Start this view. This notifies the view that it should start
* receiving and displaying data. The view will also get tick events
* starting here.
*/
@Override
void start() {
viewEnabled = true;
if (scanContinuously)
scanStart();
else
appContext.setAuxButton(R.string.lab_scan_start);
}
/**
* This view's aux button has been clicked. Toggle the scan mode.
* Does nothing in continuous scan mode.
*/
@Override
void auxButtonClick() {
if (!viewEnabled || scanContinuously)
return;
if (scanEnabled)
scanStop();
else
scanStart();
}
/**
* Stop this view. This notifies the view that it should stop
* receiving and displaying data, and generally stop using
* resources.
*/
@Override
void stop() {
scanStop();
viewEnabled = false;
}
private void scanStart() {
Sensor sensor = sensorManager.getDefaultSensor(sensorId);
if (sensor != null)
sensorManager.registerListener(this, sensor,
SensorManager.SENSOR_DELAY_GAME);
chartView.setScrolling(true);
xyzView.setScrolling(true);
appContext.setAuxButton(scanContinuously ?
R.string.lab_blank: R.string.lab_scan_stop);
if (scanSound != null && scanPlaySound && !scanContinuously)
scanSound.loop();
scanEnabled = true;
}
private void scanStop() {
sensorManager.unregisterListener(this);
chartView.setScrolling(false);
xyzView.setScrolling(false);
appContext.setAuxButton(scanContinuously ?
R.string.lab_blank: R.string.lab_scan_start);
if (scanSound != null)
scanSound.stop();
scanEnabled = false;
}
// ******************************************************************** //
// Data Management.
// ******************************************************************** //
/**
* Called when the accuracy of a sensor has changed.
*
* @param sensor The sensor being monitored.
* @param accuracy The new accuracy of this sensor.
*/
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// Don't need anything here.
}
/**
* Called when sensor values have changed.
*
* @param event The sensor event.
*/
public void onSensorChanged(SensorEvent event) {
try {
onSensorData(event.sensor.getType(), event.values);
} catch (Exception e) {
appContext.reportException(e);
}
}
/**
* Called when sensor values have changed. The length and contents
* of the values array vary depending on which sensor is being monitored.
*
* @param sensorId The ID of the sensor being monitored.
* @param values The new values for the sensor.
*/
@Override
public void onSensorData(int sensorId, float[] values) {
if (values.length < 3)
return;
synchronized (this) {
// If we're in relative mode, subtract the baseline values.
if (relativeMode) {
// First time through, set the baseline values.
if (relativeValues == null) {
relativeValues = new float[3];
relativeValues[0] = values[0];
relativeValues[1] = values[1];
relativeValues[2] = values[2];
}
values[0] -= relativeValues[0];
values[1] -= relativeValues[1];
values[2] -= relativeValues[2];
}
// Transform for the device orientation.
multiply(values, deviceTransformation, processedValues);
final float x = processedValues[0];
final float y = processedValues[1];
final float z = processedValues[2];
float m = 0.0f;
float az = 0.0f;
float alt = 0.0f;
// Calculate the magnitude.
m = (float) Math.sqrt(x*x + y*y + z*z);
// Calculate the azimuth and altitude.
az = (float) Math.toDegrees(Math.atan2(y, x));
az = 90 - az;
if (az < 0)
az += 360;
// sin alt = z / mag
alt = m == 0 ? 0 : (float) Math.toDegrees(Math.asin(z / m));
plotView.setValues(processedValues, m, az, alt);
chartView.setValue(m);
numView.setValues(processedValues, m, az, alt);
xyzView.setValue(processedValues);
}
}
/*
* Result[x] = vals[0]*tran[x][0] * vals[1]*tran[x][1] * vals[2]*tran[x][2].
*/
private static final void multiply(float[] vals, int[][] tran, float[] result) {
for (int x = 0; x < 3; ++x) {
float r = 0;
for (int y = 0; y < 3; ++y)
r += vals[y] * tran[x][y];
result[x] = r;
}
}
// ******************************************************************** //
// Input.
// ******************************************************************** //
/**
* Handle touch screen motion events.
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
@Override
public boolean handleTouchEvent(MotionEvent event) {
boolean done = false;
try {
final int x = (int) event.getX();
final int y = (int) event.getY();
final int action = event.getAction();
synchronized (this) {
if (action == MotionEvent.ACTION_DOWN) {
if (plotBounds != null && plotBounds.contains(x, y)) {
setRelativeMode(!relativeMode);
appContext.soundSecondary();
done = true;
} else if (numBounds != null && numBounds.contains(x, y)) {
// Toggle the X/Y/Z display.
showXyz = !showXyz;
appContext.soundSecondary();
done = true;
}
}
}
} catch (Exception e) {
appContext.reportException(e);
}
event.recycle();
return done;
}
// ******************************************************************** //
// View Drawing.
// ******************************************************************** //
/**
* This method is called to ask the view to draw itself.
*
* @param canvas Canvas to draw into.
* @param now Current system time in ms.
* @param bg Iff true, tell the gauge to draw its background
* first.
*/
@Override
public void draw(Canvas canvas, long now, boolean bg) {
super.draw(canvas, now, bg);
// Draw the elements.
plotView.draw(canvas, now, bg);
chartView.draw(canvas, now, bg);
if (showXyz)
xyzView.draw(canvas, now, bg);
else
numView.draw(canvas, now, bg);
}
// ******************************************************************** //
// Class Data.
// ******************************************************************** //
// Debugging tag.
@SuppressWarnings("unused")
private static final String TAG = "tricorder";
// Colours for an XYZ plot.
private static final int[] XYZ_PLOT_COLS =
new int[] { 0xffff0000, 0xff00ff00, 0xff0000ff };
// Co-ordinate transformations.
private static final int[][] TRANSFORM_0 = {
{ 1, 0, 0 },
{ 0, 1, 0 },
{ 0, 0, 1 },
};
private static final int[][] TRANSFORM_90 = {
{ 0, -1, 0 },
{ 1, 0, 0 },
{ 0, 0, 1 },
};
private static final int[][] TRANSFORM_180 = {
{ -1, 0, 0 },
{ 0, -1, 0 },
{ 0, 0, 1 },
};
private static final int[][] TRANSFORM_270 = {
{ 0, 1, 0 },
{ -1, 0, 0 },
{ 0, 0, 1 },
};
// ******************************************************************** //
// Private Data.
// ******************************************************************** //
// Application handle.
private Tricorder appContext;
// The sensor manager, which we use to interface to all sensors.
private SensorManager sensorManager;
// The ID of the sensor to read: Sensor.TYPE_XXX.
private int sensorId;
// The unit and range of the data. baseDataRange is the specified range
// for this display; dataRange is the current range under zooming.
public final float dataUnit;
public float dataRange;
// Current device orientation, as a matrix which can be used
// to correct sensor input.
private int[][] deviceTransformation = TRANSFORM_0;
// Flags for enabling the view -- when this view is displayed -- and
// scanning, when the user presses the scan button.
private boolean viewEnabled = false;
private boolean scanEnabled = false;
// Processed data values.
private float[] processedValues = null;
// If relativeMode is true, we're in relative mode. Display values
// relative to the values stored in relativeValues -- i.e. subtract
// relativeValues from future inputs.
private boolean relativeMode = false;
private float[] relativeValues = null;
// Colour of the graph grid and plot, in primary (absolute) and
// secondary (relative) modes.
private int gridColour1 = 0xff00ff00;
private int plotColour1 = 0xffff0000;
private int gridColour2 = 0xff00ff00;
private int plotColour2 = 0xffff0000;
// Sound to play while scanning. null if none.
private Effect scanSound = null;
// If true, scan continuously; else only when the user says.
private boolean scanContinuously = false;
// If true, play a sound while scanning under user control.
private boolean scanPlaySound = true;
// 3-axis plot, magnitude chart and numeric display for the data.
// numView and xyzView are two alternative modes for the bottom plot.
// We also keep the current bounds for each element.
private AxisElement plotView;
private Rect plotBounds;
private MagnitudeElement chartView;
private Num3DElement numView;
private Rect numBounds;
private MagnitudeElement xyzView;
// Do we show the XYZ display? If not, it's numView.
private boolean showXyz = false;
// Some useful strings.
private String title_vect_abs;
private String title_mag_abs;
private String title_num_abs;
private String title_xyz_abs;
private String title_vect_rel;
private String title_mag_rel;
private String title_num_rel;
private String title_xyz_rel;
}
|