Permalink
Browse files

Updated comments, ready for initial release.

  • Loading branch information...
1 parent f1e159a commit 547b922737c2034d1aca0a0046e66213f8d09985 @jwf committed Feb 8, 2011
Showing with 1,787 additions and 773 deletions.
  1. +55 −18 src/ca/mcgill/hs/HSAndroid.java
  2. +2 −0 src/ca/mcgill/hs/classifiers/AccelerometerLingeringFilter.java
  3. +127 −16 src/ca/mcgill/hs/classifiers/NativeClassifier.java
  4. +49 −0 src/ca/mcgill/hs/classifiers/TimeDelayEmbeddingClassifier.java
  5. +14 −1 src/ca/mcgill/hs/classifiers/location/DBHelpers.java
  6. +9 −0 src/ca/mcgill/hs/classifiers/location/DebugHelper.java
  7. +23 −4 src/ca/mcgill/hs/classifiers/location/GPSClusterer.java
  8. +34 −14 src/ca/mcgill/hs/classifiers/location/GPSLocation.java
  9. +50 −16 src/ca/mcgill/hs/classifiers/location/GPSLocationSet.java
  10. +8 −3 src/ca/mcgill/hs/classifiers/location/GPSObservation.java
  11. +31 −6 src/ca/mcgill/hs/classifiers/location/Location.java
  12. +79 −16 src/ca/mcgill/hs/classifiers/location/LocationSet.java
  13. +85 −35 src/ca/mcgill/hs/classifiers/location/MotionStateClusterer.java
  14. +20 −0 src/ca/mcgill/hs/classifiers/location/Observation.java
  15. +44 −38 src/ca/mcgill/hs/classifiers/location/SignificantLocationClusterer.java
  16. +23 −5 src/ca/mcgill/hs/classifiers/location/WifiClusterer.java
  17. +46 −10 src/ca/mcgill/hs/classifiers/location/WifiLocation.java
  18. +51 −45 src/ca/mcgill/hs/classifiers/location/WifiLocationSet.java
  19. +39 −11 src/ca/mcgill/hs/classifiers/location/WifiObservation.java
  20. +6 −1 src/ca/mcgill/hs/graph/MagnitudeGraph.java
  21. +4 −2 src/ca/mcgill/hs/graph/MagnitudeGraphView.java
  22. +10 −11 src/ca/mcgill/hs/graph/NewActivityNotificationLauncher.java
  23. +3 −1 src/ca/mcgill/hs/hardware/Sensor.java
  24. +51 −0 src/ca/mcgill/hs/network/LogServerClient.java
  25. +59 −34 src/ca/mcgill/hs/plugin/BluetoothLogger.java
  26. +4 −3 src/ca/mcgill/hs/plugin/DataPacket.java
  27. +42 −35 src/ca/mcgill/hs/plugin/FileOutput.java
  28. +48 −24 src/ca/mcgill/hs/plugin/GPSLogger.java
  29. +20 −22 src/ca/mcgill/hs/plugin/GSMLogger.java
  30. +31 −13 src/ca/mcgill/hs/plugin/InputPlugin.java
  31. +43 −9 src/ca/mcgill/hs/plugin/LocationClusterer.java
  32. +42 −26 src/ca/mcgill/hs/plugin/OutputPlugin.java
  33. +3 −2 src/ca/mcgill/hs/plugin/Plugin.java
  34. +23 −0 src/ca/mcgill/hs/plugin/PluginFactory.java
  35. +5 −13 src/ca/mcgill/hs/plugin/ScreenOutput.java
  36. +44 −26 src/ca/mcgill/hs/plugin/SensorLogger.java
  37. +118 −70 src/ca/mcgill/hs/plugin/TDEClassifierPlugin.java
  38. +13 −10 src/ca/mcgill/hs/plugin/TestMagOutputPlugin.java
  39. +43 −33 src/ca/mcgill/hs/plugin/WifiLogger.java
  40. +10 −1 src/ca/mcgill/hs/prefs/DeleteUnUploadedFileManager.java
  41. +19 −4 src/ca/mcgill/hs/prefs/FileManager.java
  42. +35 −42 src/ca/mcgill/hs/prefs/HSAndroidPreferences.java
  43. +6 −8 src/ca/mcgill/hs/prefs/InputPluginPreferences.java
  44. +12 −1 src/ca/mcgill/hs/prefs/ManageModelsFileManager.java
  45. +4 −8 src/ca/mcgill/hs/prefs/OutputPluginPreferences.java
  46. +55 −25 src/ca/mcgill/hs/prefs/PreferenceFactory.java
  47. +58 −39 src/ca/mcgill/hs/prefs/SeekBarPreference.java
  48. +3 −1 src/ca/mcgill/hs/serv/HSServAutoStart.java
  49. +41 −29 src/ca/mcgill/hs/serv/HSService.java
  50. +4 −0 src/ca/mcgill/hs/serv/LogFileUploaderService.java
  51. +8 −0 src/ca/mcgill/hs/serv/LowBatteryMonitor.java
  52. +6 −0 src/ca/mcgill/hs/util/ActivityIndex.java
  53. +29 −27 src/ca/mcgill/hs/util/LRUCache.java
  54. +24 −11 src/ca/mcgill/hs/util/Log.java
  55. +31 −2 src/ca/mcgill/hs/widget/LingeringNotificationWidget.java
  56. +5 −2 src/ca/mcgill/hs/widget/LocationLabelerDialog.java
  57. +36 −0 src/ca/mcgill/hs/widget/LocationStatusWidget.java
View
73 src/ca/mcgill/hs/HSAndroid.java
@@ -10,6 +10,7 @@
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
+import android.os.Debug;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@@ -26,15 +27,27 @@
* is launched manually on the phone by the user, and is from where the
* background services can be manually started and stopped, and where the
* preferences and settings can be changed.
+ *
+ * @author Jordan Frank <jordan.frank@cs.mcgill.ca>
*/
public class HSAndroid extends Activity {
private static Button serviceSwitch;
private Intent serviceIntent;
+ /**
+ * Whether to start the service automatically when the application is
+ * loaded.
+ */
private boolean autoStartAppStart = false;
+ /**
+ * Whether to use Debug.MethodTracing to collect profiling information. Make
+ * sure that this is false for a release version!
+ */
+ private static final boolean doProfiling = false;
+
public static final String HSANDROID_PREFS_NAME = "HSAndroidPrefs";
private static Context context = null;
private static TableLayout freeSpace = null;
@@ -45,19 +58,33 @@
@SuppressWarnings("unused")
private static final String TAG = "HSAndroid";
- public static String getAppString(final int resId) {
- return context.getString(resId);
+ /**
+ * Fetches a resource string for the specified id from the application
+ * context.
+ *
+ * @param resourceId
+ * for the string to be fetched.
+ * @return String corresponding to the resourceId
+ */
+ public static String getAppString(final int resourceId) {
+ return context.getString(resourceId);
}
+ /**
+ * Returns a handle to the free space in the main screen where plugins can
+ * add messages or widgets.
+ *
+ * @return Handle to the free space on the main application layout.
+ */
public static TableLayout getFreeSpace() {
return freeSpace;
}
/**
- * Updates the main starting button. This is required due to the nature of
- * Activities in the Android API. In order to correctly get the state of the
- * service to update the button text, this method cannot be called from
- * within the Activity.
+ * Updates the main starting button according to whether the service is
+ * running or not. Should be called whenever the state of the service is
+ * changed. Probably a way to do this using Intents or events, but for now
+ * we rely on it being called manually.
*/
public static void updateButton() {
if (serviceSwitch != null) {
@@ -70,7 +97,9 @@ public static void updateButton() {
}
/**
- * Sets up the preferences, i.e. get Activity preferences.
+ * Retrieves the current state of the main application preferences, which
+ * control whether log data is stored in a file and whether the service
+ * automatically starts when the application is loaded.
*/
private void getPrefs() {
final SharedPreferences prefs = PreferenceFactory
@@ -84,7 +113,10 @@ private void getPrefs() {
/**
* This method is called when the activity is first created. It is the entry
- * point for the application.
+ * point for the application. Here we set up some important member variables
+ * such as the application context, the plugin factory, the input and output
+ * plugins, and the application preferences. We also initialize the GUI
+ * here.
*/
@Override
public void onCreate(final Bundle savedInstanceState) {
@@ -100,7 +132,8 @@ public void onCreate(final Bundle savedInstanceState) {
HSService.initializeInputPlugins();
HSService.initializeOutputPlugins();
- // // final Sensor s = new Sensor();
+ // Testing code for new sensor interface, disabled for now:
+ // final Sensor s = new Sensor();
// Log.d(TAG, "Sensor.androidInit: " + Sensor.androidInit());
// Log.d(TAG, "Bundle: " + Sensor.androidOpen());
// Log.d(TAG, "Sensor.sensorsModuleInit: " +
@@ -121,11 +154,10 @@ public void onCreate(final Bundle savedInstanceState) {
//
// Log.d(TAG, "Sensor.sensorsDataUnInit: " +
// Sensor.sensorsDataUninit());
-
// s.sensors_module_get_next_sensor(new Object(), 1);
-
// Sensor.sensors_module_init();
+ /* Set up the GUI */
setContentView(R.layout.main);
freeSpace = (TableLayout) findViewById(R.id.free_space);
@@ -139,24 +171,29 @@ public void onCreate(final Bundle savedInstanceState) {
@Override
public void onClick(final View v) {
if (!HSService.isRunning()) { // NOT RUNNING
- // Debug.startMethodTracing("hsandroid");
+ if (doProfiling) {
+ Debug.startMethodTracing(TAG);
+ }
startService(serviceIntent);
} else { // RUNNING
stopService(serviceIntent);
- // Debug.stopMethodTracing();
+ if (doProfiling) {
+ Debug.stopMethodTracing();
+ }
}
+ updateButton();
}
});
- // Auto App Start
if (autoStartAppStart) {
+ // Start the service on application start.
startService(serviceIntent);
}
}
/**
- * This method is called whenever the user wants to access the settings
- * menu.
+ * Called when the user access the application's options menu, sets up the
+ * two icons that appear.
*/
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
@@ -168,8 +205,8 @@ public boolean onCreateOptionsMenu(final Menu menu) {
}
/**
- * This method is used to parse the selection of options items. These items
- * include: - Preferences (settings)
+ * Loads the appropriate preferences screen depending on which option was
+ * selected from the options menu.
*/
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
View
2 src/ca/mcgill/hs/classifiers/AccelerometerLingeringFilter.java
@@ -9,6 +9,8 @@
* Very simple filter for accelerometer values. Maintains a moving window of
* accelerometer magnitude values and computes whether the average value is
* greater than some threshold. Can act as a very simple test for motion.
+ *
+ * @author Jordan Frank <jordan.frank@cs.mcgill.ca>
*/
public class AccelerometerLingeringFilter {
View
143 src/ca/mcgill/hs/classifiers/NativeClassifier.java
@@ -8,7 +8,7 @@
/**
* Wraps the native C code for the classifier.
*
- * @author jfrank8
+ * @author Jordan Frank <jordan.frank@cs.mcgill.ca>
*
*/
public class NativeClassifier {
@@ -17,43 +17,154 @@
System.loadLibrary("humansense");
}
+ /**
+ * The original GTM algorithm (Frank et al., AAAI'10), which treats each
+ * step in the segment individually, that is that nearest neighbours are
+ * recomputed at each segment.
+ */
+ public static final int GTMALGORITHM_INDEP_STEPS = 1;
+
+ /**
+ * More brittle, but faster algorithm that matches the entire trace, that is
+ * the nearest neighbours are only computed for the starting point of the
+ * segment, and then the next points in the model are used as the
+ * "neighbours" for the next points in the trace, rather than being
+ * recomputed as in GTMALGORITHM_INDEP_STEPS. This algorithm should only be
+ * used if the segment length parameter is small.
+ */
+ public static final int GTMALGORITHM_FULL_MATCH = 2;
+
+ /**
+ * Experimental algorithm, needs more testing. Doesn't just find nearest
+ * neighbours in the model, but finds the closest point (approximate) on any
+ * line-segment in the model. Then this point along the line-segment is
+ * used, and a point midway on the next line segment is used as the next
+ * point. This should lead to better results, and is still fairly fast, but
+ * it's unclear that the approximations made in the implementation are
+ * sound.
+ */
+ public static final int GTMALGORITHM_SEGMENT_MATCH = 3;
+
+ /**
+ * Closes and frees the memory for the nearest-neighbour data structure.
+ */
public native void annClose();
+ /**
+ * Computes the squared distance between points p and q, each having dim
+ * dimensions.
+ *
+ * @param dim
+ * The dimension of the points.
+ * @param p
+ * First point.
+ * @param q
+ * Second point.
+ * @return Squared interpoint distance.
+ */
public native float annDist(int dim, float[] p, float[] q);
- // Builds a model from data in in_file with embedding dimension m,
- // PCA reduced dimension p, and delay time d. Model is saved in a file
- // constructed from in_file with .dmp appended.
+ /**
+ * Builds a time-delay embedding model from data in in_file with embedding
+ * dimension m, PCA reduced dimension p, and delay time d. Model is saved in
+ * a file constructed from in_file with .dmp appended.
+ *
+ * @param in_file
+ * File name containing a single column of data points, from
+ * which the model is built.
+ * @param m
+ * Initial embedding dimension.
+ * @param p
+ * Number of principal components to use for final model.
+ * @param d
+ * Delay time in samples.
+ */
public native void buildTree(String in_file, int m, int p, int d);
- // Classifies the data in the array in, starting from index offset, and
- // returned values are stored in output, which must be an array of length
- // getNumModels()
+ /**
+ * Classifies the data in the array in, starting from index offset, and
+ * returned values are stored in output, which must be an array of length
+ * getNumModels().
+ *
+ * @param in
+ * Data to be classified.
+ * @param startIndex
+ * Starting index from which to classify for array in.
+ * @param out
+ * Scores, one per model. out must be preallocated with size
+ * equal to the number of models.
+ */
public native void classifySample(float[] in, int startIndex, float[] out);
- // Loads models from models_file, then classifies data from in_file,
- // storing the class-likelihoods in out_file.
+ /**
+ * Classifies an entire trajectory in the file specified by in_file, using
+ * the models from models_file, and storing the scores in out_file.
+ *
+ * @param in_file
+ * A file containing a single column of values representing the
+ * trajectory to be classified.
+ * @param out_file
+ * The file that will contain the scores. If the file exists, it
+ * will be overwritten.
+ * @param models_file
+ * A file containing the file names of the model files, one per
+ * line.
+ */
public native void classifyTrajectory(String in_file, String out_file,
String models_file);
- // Cleans up any loaded models.
+ /**
+ * Deletes any models and frees the memory allocated to them.
+ */
public native void deleteModels();
- // Returns a tab-separated list of model names
+ /**
+ * Returns a tab-separated list of model names
+ *
+ * @return A tab-separated list of model names.
+ */
public native String getModelNames();
- // Returns the number of loaded models
+ /**
+ * Returns the number of loaded models
+ *
+ * @return The number of loaded models
+ */
public native int getNumModels();
- // Returns the minimum number of samples that must be passed to the
- // classifySamples.
- // It is the maximum of the window sizes required for all of the models.
+ /**
+ * Returns the minimum number of samples that must be passed to the
+ * classifySamples. It is the maximum of the window sizes required for all
+ * of the models.
+ *
+ * @return The minimum allowable size of an array that can be passed to
+ * classifySamples
+ */
public native int getWindowSize();
- // Loads models from models_file.
+ /**
+ * Loads the models specified in models_file, and initializes the classifier
+ * parameters.
+ *
+ * @param models_file
+ * A file containing the model files, one per line.
+ * @param numNeighbours
+ * The number of neighbours used in the nearest-neighbours step
+ * of the classifier.
+ * @param matchSteps
+ * The length of the sequence that is compared by the classifier
+ * to compute the score.
+ */
public native void loadModels(String models_file, int numNeighbours,
int matchSteps);
+ /**
+ * Select the algorithm used by the classifier.
+ *
+ * @param algNum
+ * Algorithm number, must be one of GTMALGORITHM_INDEP_STEPS,
+ * GTMALGORITHM_FULL_MATCH, or GTMALGORITHM_SEGMENT_MATCH.
+ */
public native void setAlgorithmNumber(int algNum);
}
View
49 src/ca/mcgill/hs/classifiers/TimeDelayEmbeddingClassifier.java
@@ -7,6 +7,13 @@
import java.io.File;
+/**
+ * Wraps the functionality of the NativeClassifier, and makes it much easier to
+ * use by a plugin.
+ *
+ * @author Jordan Frank <jordan.frank@cs.mcgill.ca>
+ *
+ */
public class TimeDelayEmbeddingClassifier {
@SuppressWarnings("unused")
private static final String TAG = "TimeDelayEmbeddingClassifier";
@@ -23,6 +30,14 @@
private int numLoadedModels;
+ /**
+ * Adds a sample to the data buffer. Returns the index in the buffer at
+ * which the data was added.
+ *
+ * @param sample
+ * Data element to be added to the buffer.
+ * @return Index in the buffer where the data can be found.
+ */
public int addSample(final float sample) {
int index;
synchronized (buffer) {
@@ -41,11 +56,38 @@ public int addSample(final float sample) {
return index;
}
+ /**
+ * Just wraps the buildTree method of the native classifier.
+ *
+ * @param modelFile
+ * The file containing the data. This file must contain a single
+ * column of floating point values. The model will be stored in a
+ * file with the same name but with the suffix .dmp appended to
+ * it.
+ * @param m
+ * Initial embedding dimension.
+ * @param p
+ * Number of principal components to use for final model.
+ * @param d
+ * Time delay in samples.
+ *
+ * @see NativeClassifier#buildTree
+ */
public void buildModel(final String modelFile, final int m, final int p,
final int d) {
nativeClassifier.buildTree(modelFile, m, p, d);
}
+ /**
+ * Classifies the data in the buffer starting at the specified index.
+ *
+ * @param index
+ * The starting index in the buffer.
+ * @return The scores from each of the models. This will be an array
+ * containing the same number of values as there are models, and the
+ * order of the values correspond to the order of the models
+ * returned by {@link #getLoadedModelNames()}
+ */
public float[] classify(final int index) {
if (index < bufferMidPoint) {
return null;
@@ -86,6 +128,13 @@ public int getNumModels() {
return nativeClassifier.getNumModels();
}
+ /**
+ * Loads a set of models
+ *
+ * @param models
+ * A file containing the filenames of the models to load, one per
+ * line.
+ */
public void loadModels(final File models) {
nativeClassifier.loadModels(models.getAbsolutePath(), 2, 16);
nativeClassifier.setAlgorithmNumber(1);
View
15 src/ca/mcgill/hs/classifiers/location/DBHelpers.java
@@ -12,9 +12,22 @@
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
+/**
+ * Some helpful routines for managing the SQLite database files
+ *
+ * @author Jordan Frank <jordan.frank@cs.mcgill.ca>
+ *
+ */
public final class DBHelpers {
- /** Fast & simple file copy. */
+ /**
+ * Fast & simple file copy.
+ *
+ * @param source
+ * The file to be copied.
+ * @param dest
+ * The destination file.
+ * */
public static void copy(final File source, final File dest)
throws IOException {
FileChannel in = null, out = null;
View
9 src/ca/mcgill/hs/classifiers/location/DebugHelper.java
@@ -7,7 +7,16 @@
import java.io.PrintStream;
+/**
+ * Some helpful routines for debugging the database-related code.
+ *
+ * @author Jordan Frank <jordan.frank@cs.mcgill.ca>
+ *
+ */
public class DebugHelper {
+ /**
+ * Controls whether we want to log verbose debugging information.
+ */
public static final boolean DEBUG = false;
public static PrintStream out;
static {
View
27 src/ca/mcgill/hs/classifiers/location/GPSClusterer.java
@@ -9,13 +9,27 @@
import android.content.Context;
+//TODO: Make this work.
+/**
+ * Clusters GPS data based on locations in which the user spends a significant
+ * amount of time. Doesn't work yet!
+ *
+ * @author Jordan Frank <jordan.frank@cs.mcgill.ca>
+ *
+ */
public class GPSClusterer {
private final GPSLocationSet locations;
private final MotionStateClusterer pool;
+ /**
+ * Doesn't work yet.
+ *
+ * @param dbFile
+ * @param context
+ */
public GPSClusterer(final File dbFile, final Context context) {
locations = new GPSLocationSet(dbFile);
- pool = new MotionStateClusterer(locations, context);
+ pool = new MotionStateClusterer(locations);
}
/**
@@ -25,9 +39,14 @@ public void close() {
locations.close();
}
- // Returns a list the same length as timestamps that contains the votes
- // for each event. A positive value indicates motion, while a negative value
- // indicates stationarity.
+ /**
+ * Adds a new observation to the cluster pool.
+ *
+ * @param timestamp
+ * Timestamp in milliseconds associated with this observation.
+ * @param observation
+ * The {@link GPSObservation} to be clustered.
+ */
public void cluster(final double timestamp, final GPSObservation observation) {
pool.addObservation(timestamp, observation);
}
View
48 src/ca/mcgill/hs/classifiers/location/GPSLocation.java
@@ -13,6 +13,8 @@
/**
* Refers to an GPS location. Collections GPS observations to determine its
* average location.
+ *
+ * @author Jordan Frank <jordan.frank@cs.mcgill.ca>
*/
public class GPSLocation extends Location {
@@ -25,29 +27,37 @@
* @param timestamp
* The timestamp of the GPSLocation.
* @throws SQLException
+ * Indicates there is some problem accessing the database.
+ * @see Location#Location(SQLiteDatabase, double)
*/
public GPSLocation(final SQLiteDatabase db, final double timestamp) {
super(db, timestamp);
}
+ /**
+ * Creates a GPSLocation with the specified id.
+ *
+ * @param db
+ * @param id
+ * @see Location#Location(SQLiteDatabase, long)
+ */
public GPSLocation(final SQLiteDatabase db, final long id) {
super(db, id);
}
+ /**
+ * Adds a new observation to the location.
+ *
+ * @param o
+ * The new {@link GPSObservation} to add to this location.
+ */
@Override
public void addObservation(final Observation o) {
final GPSObservation observation = (GPSObservation) o;
DebugHelper.out.println("\tObservation being added to location "
+ getId());
DebugHelper.out.println("\t" + observation.toString());
- // conn.createStatement().executeUpdate(
- // "INSERT OR IGNORE INTO " + GPSLocationSet.LOCATIONS_TABLE +
- // " VALUES (" +
- // getId() + ", " +
- // getTimestamp() + ", " +
- // "NULL,0,0,0,0,0,0,1);"
- // );
db.execSQL("UPDATE " + GPSLocationSet.LOCATIONS_TABLE + " SET "
+ "latitude_total=" + observation.latitude + ", "
+ "latitude_weights=" + (1.0 / observation.accuracy) + ", "
@@ -58,6 +68,11 @@ public void addObservation(final Observation o) {
@Override
public double distanceFrom(final Location other) {
+ /**
+ * The distance is Euclidean distance between the weighted means of the
+ * observations associated with each location, where the weights
+ * correspond to the accuracy of the GPS readings for the observations.
+ */
final Cursor cursor = db.rawQuery(""
+ "SELECT latitude_average,longitude_average FROM "
+ GPSLocationSet.LOCATIONS_TABLE + " WHERE location_id="
@@ -78,7 +93,9 @@ public double distanceFrom(final Location other) {
}
/**
- * Returns the average latitude of the location.
+ * Returns the weighted average of the latitudes in the observations
+ * associated with this location. The weights correspond to the accuracies
+ * of the GPS observations.
*
* @return The average latitude of the location.
*/
@@ -97,7 +114,9 @@ public double getLatitude() {
}
/**
- * Returns the average longitude of the location.
+ * Returns the weighted average of the longitudes in the observations
+ * associated with this location. The weights correspond to the accuracies
+ * of the GPS observations.
*
* @return The average longitude of the location.
*/
@@ -118,11 +137,12 @@ public double getLongitude() {
@Override
public Observation getObservations() {
Observation observation = null;
- final Cursor cursor = db
- .rawQuery(
- "SELECT strftime('%s',timestamp)-strftime('%S',timestamp)+strftime('%f',timestamp),latitude_weights,latitude_average,longitude_average FROM "
- + GPSLocationSet.LOCATIONS_TABLE
- + " WHERE location_id=" + getId() + ";", null);
+ final Cursor cursor = db.rawQuery("SELECT "
+ + "strftime('%s',timestamp) - " + "strftime('%S',timestamp) + "
+ + "strftime('%f',timestamp),"
+ + "latitude_weights, latitude_average, longitude_average "
+ + "FROM " + GPSLocationSet.LOCATIONS_TABLE
+ + " WHERE location_id=" + getId() + ";", null);
try {
cursor.moveToNext();
observation = new GPSObservation(cursor.getDouble(0),
View
66 src/ca/mcgill/hs/classifiers/location/GPSLocationSet.java
@@ -11,29 +11,49 @@
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
-import ca.mcgill.hs.util.Log;
import ca.mcgill.hs.util.LRUCache;
+import ca.mcgill.hs.util.Log;
/**
* A set containing GPS locations.
+ *
+ * @author Jordan Frank <jordan.frank@cs.mcgill.ca>
*/
public class GPSLocationSet extends LocationSet {
+ /**
+ * The number of locations to cache.
+ */
+ private static final int CACHE_SIZE = 100;
+ /**
+ * Used to cache the last 100 most recently used locations, saves on
+ * database queries.
+ */
private final LRUCache<Long, GPSLocation> locationCache = new LRUCache<Long, GPSLocation>(
- 100);
+ CACHE_SIZE);
private static final boolean TIME_BASED_WINDOW = false;
- // Window length, in samples.
+ /**
+ * Window length, in samples.
+ */
private static final int WINDOW_LENGTH = 20;
- // Delta from the paper. This value represents the percentage of the points
- // in the pool that must neighbours of a point for it to be considered to be
- // part of a cluster
+ /**
+ * Delta from the paper. This value represents the percentage of the points
+ * in the pool that must neighbours of a point for it to be considered to be
+ * part of a cluster.
+ */
private static final float DELTA = 0.8f;
private static final String TAG = "GPSLocationSet";
+ /**
+ * Constructor, connects to the database for GPS locations.
+ *
+ * @param dbFile
+ * The SQLite database file for the GPS locations.
+ */
public GPSLocationSet(final File dbFile) {
final boolean dbExists = dbFile.exists();
@@ -53,6 +73,8 @@ public GPSLocationSet(final File dbFile) {
public long add(final Location loc) {
final GPSLocation location = (GPSLocation) loc;
+ cacheLocation(location);
+
// Now get potential neighbours
final Collection<Long> possibleNeighbours = new LinkedList<Long>();
final double lat = location.getLatitude();
@@ -94,9 +116,13 @@ public long add(final Location loc) {
return location.getId();
}
- @Override
- public void cacheLocation(final Location location) {
- locationCache.put(location.getId(), (GPSLocation) location);
+ /**
+ * Adds a location to the cache.
+ *
+ * @params location The location to be added to the cache.
+ */
+ private void cacheLocation(final GPSLocation location) {
+ locationCache.put(location.getId(), location);
}
public void close() {
@@ -109,6 +135,14 @@ public void close() {
}
}
+ /**
+ * Creates the initial GPS Clustering database. Note that if the database
+ * already contains information, it will be lost, as the tables are dropped
+ * before being created.
+ *
+ * @param db
+ * The database in which to create the tables.
+ */
private void createDatabase(final SQLiteDatabase db) {
/* Locations Table */
db.execSQL("DROP TABLE IF EXISTS " + LOCATIONS_TABLE);
@@ -134,13 +168,13 @@ private void createDatabase(final SQLiteDatabase db) {
+ "UPDATE "
+ LOCATIONS_TABLE
+ " SET "
- + "latitude_total=OLD.latitude_total+NEW.latitude_total*NEW.latitude_weights, "
- + "latitude_weights=OLD.latitude_weights+NEW.latitude_weights, "
- + "latitude_average=(OLD.latitude_total+NEW.latitude_total*NEW.latitude_weights)/(OLD.latitude_weights+NEW.latitude_weights), "
- + "longitude_total=OLD.longitude_total+NEW.longitude_total*NEW.longitude_weights, "
- + "longitude_weights=OLD.longitude_weights+NEW.longitude_weights, "
- + "longitude_average=(OLD.longitude_total+NEW.longitude_total*NEW.longitude_weights)/(OLD.longitude_weights+NEW.longitude_weights) "
- + "WHERE location_id=NEW.location_id; " + "END;");
+ + "latitude_total = OLD.latitude_total + NEW.latitude_total*NEW.latitude_weights, "
+ + "latitude_weights = OLD.latitude_weights + NEW.latitude_weights, "
+ + "latitude_average = (OLD.latitude_total + NEW.latitude_total * NEW.latitude_weights) / (OLD.latitude_weights + NEW.latitude_weights), "
+ + "longitude_total = OLD.longitude_total + NEW.longitude_total * NEW.longitude_weights, "
+ + "longitude_weights = OLD.longitude_weights + NEW.longitude_weights, "
+ + "longitude_average = (OLD.longitude_total + NEW.longitude_total * NEW.longitude_weights) / (OLD.longitude_weights + NEW.longitude_weights) "
+ + "WHERE location_id = NEW.location_id; " + "END;");
db.execSQL("CREATE INDEX idx_" + LOCATIONS_TABLE + "_latitude ON "
+ LOCATIONS_TABLE + " (latitude_average);");
db.execSQL("CREATE INDEX idx_" + LOCATIONS_TABLE + "_longitude ON "
View
11 src/ca/mcgill/hs/classifiers/location/GPSObservation.java
@@ -13,6 +13,9 @@
// protected final float speed;
+ /**
+ * Constructs a new GPSObservation with the given parameters.
+ */
public GPSObservation(final double timestamp, final int accuracy,
final double latitude, final double longitude) { // , float speed) {
this.timestamp = timestamp;
@@ -24,16 +27,18 @@ public GPSObservation(final double timestamp, final int accuracy,
@Override
public double distanceFrom(final Observation other) {
-
- // compute distance
+ /*
+ * Distance is Euclidean. Maybe better to use geodesic distances, but
+ * we're really only concerned with locations that are really close to
+ * each other.
+ */
final GPSObservation o = (GPSObservation) other;
return Math.sqrt((latitude - o.latitude) * (latitude - o.latitude)
+ (longitude - o.longitude) * (longitude - o.longitude));
}
@Override
public double getEPS() {
- // TODO Auto-generated method stub
return 1E-5;
}
View
37 src/ca/mcgill/hs/classifiers/location/Location.java
@@ -147,7 +147,7 @@ public void addNeighbours(final Collection<Long> ids) {
public abstract double distanceFrom(Location other);
/**
- * Creates a new location record
+ * Creates a new location record.
*
* @return The id of the new empty location that has been created.
*/
@@ -158,14 +158,17 @@ protected long generateNewLocationId() {
return new_id;
}
+ /**
+ * @return The id of this location.
+ */
public long getId() {
return location_id;
}
/**
* Returns this location's neighbours.
*
- * @return This location's neighbours.
+ * @return A list of this location's neighbours' ids.
*/
public List<Long> getNeighbours() {
if (neighbours == null) {
@@ -208,6 +211,9 @@ public long getNumMerged() {
return num_merged;
}
+ /**
+ * @return The number of neighbours for this location.
+ */
public long getNumNeighbours() {
if (num_neighbours >= 0) {
return num_neighbours;
@@ -226,6 +232,9 @@ public long getNumNeighbours() {
return num_neighbours;
}
+ /**
+ * @return The observations that comprise this location.
+ */
public abstract Observation getObservations();
/**
@@ -282,7 +291,7 @@ public void removeNeighbour(final long id) {
* Removes a set of neighbours from the location.
*
* @param neighboursToRemove
- * A set of the location's neighbours.
+ * A set location ids to remove from the list of neighbours.
*/
public void removeNeighbours(final Collection<Long> neighboursToRemove) {
if (neighbours == null) {
@@ -318,13 +327,29 @@ public void removeNeighbours(final Collection<Long> neighboursToRemove) {
}
- public void setNumMerged(final long l) {
- this.num_merged = l;
+ /**
+ * Used to keep track of the number of locations that have been merged to
+ * form the current location. This is used when determining how many
+ * neighbours a point has, since if two points have been merged to form a
+ * location, then that location should count as two neighbours.
+ *
+ * @param num
+ * The number of locations that have been merged to form this
+ * current location.
+ */
+ public void setNumMerged(final long num) {
+ this.num_merged = num;
db.execSQL("UPDATE " + LocationSet.LOCATIONS_TABLE
+ " SET num_merged=? WHERE location_id=?", new String[] {
- Long.toString(l), Long.toString(location_id) });
+ Long.toString(num), Long.toString(location_id) });
}
+ /**
+ * Sets the timestamp for this location
+ *
+ * @param timestamp
+ * The new timestamp, in milliseconds.
+ */
public void setTimestamp(final double timestamp) {
this.timestamp = timestamp;
db.execSQL("UPDATE " + LocationSet.LOCATIONS_TABLE
View
95 src/ca/mcgill/hs/classifiers/location/LocationSet.java
@@ -13,12 +13,14 @@
public abstract class LocationSet {
+ // Table Names
public static final String LOCATIONS_TABLE = "locations";
public static final String NEIGHBOURS_TABLE = "neighbours";
public static final String LABELS_TABLE = "labels";
public static final String CLUSTERS_TABLE = "clusters";
public static final String SQLITE_DATE_FORMAT = "'%Y-%m-%d %H:%M:%f'";
+
/**
* Delta from the paper. This value represents the percentage of the points
* in the pool that must neighbours of a point for it to be considered to be
@@ -38,13 +40,27 @@
*/
public abstract long add(Location point);
+ /**
+ * Adds a location to a cluster.
+ *
+ * @param locationId
+ * The id of the location to be added to the cluster.
+ * @param clusterId
+ * The id of the cluster to add the location to.
+ */
public void addToCluster(final long locationId, final long clusterId) {
db.execSQL("REPLACE INTO " + CLUSTERS_TABLE + " VALUES (" + locationId
+ "," + clusterId + ", NULL);");
}
- public abstract void cacheLocation(Location location);
-
+ /**
+ * Relabels a cluster with a new id. This is used when merging clusters.
+ *
+ * @param oldId
+ * Old cluster id.
+ * @param newId
+ * New cluster id.
+ */
public void changeClusterId(final long oldId, final long newId) {
if (oldId == newId) {
return;
@@ -55,6 +71,11 @@ public void changeClusterId(final long oldId, final long newId) {
+ " WHERE cluster_id=" + oldId + ";");
}
+ /**
+ * Returns the ids of all clusters in the database.
+ *
+ * @return A collection of cluster ids.
+ */
public Collection<Long> getAllClusters() {
final Collection<Long> clusters = new LinkedList<Long>();
final Cursor cursor = db.rawQuery("SELECT DISTINCT cluster_id FROM "
@@ -69,6 +90,11 @@ public void changeClusterId(final long oldId, final long newId) {
return clusters;
}
+ /**
+ * Returns the ids of all locations in the database.
+ *
+ * @return A collection of location ids.
+ */
public Collection<Long> getAllLocations() {
final Collection<Long> locations = new LinkedList<Long>();
final Cursor cursor = db.rawQuery("SELECT location_id FROM "
@@ -84,16 +110,17 @@ public void changeClusterId(final long oldId, final long newId) {
}
/**
- * Gets the cluster id for a particular location_id.
+ * Gets the cluster id for a particular location id.
*
- * @param location_id
+ * @param locationId
+ * The location id.
* @return Cluster id, or -1 if point is not in a cluster.
*/
- public long getClusterId(final long location_id) {
+ public long getClusterId(final long locationId) {
long cluster_id = -1;
final Cursor cursor = db.rawQuery("SELECT cluster_id FROM "
+ CLUSTERS_TABLE + " WHERE location_id=?;", new String[] { Long
- .toString(location_id) });
+ .toString(locationId) });
try {
if (cursor.moveToNext()) {
cluster_id = cursor.getLong(0);
@@ -109,25 +136,25 @@ public long getClusterId(final long location_id) {
* of locations
*
* @param locations
- * the set of locations to check
- * @return the cluster ids that occur in the collection of locations.
+ * The set of locations to check
+ * @return The cluster ids that occur in the collection of locations.
*/
public Collection<Long> getClusterIds(final Collection<Long> locations) {
- final StringBuilder location_ids = new StringBuilder();
+ final StringBuilder locationIds = new StringBuilder();
boolean first = true;
- for (final long location_id : locations) {
+ for (final long locationId : locations) {
if (first) {
- location_ids.append(location_id);
+ locationIds.append(locationId);
first = false;
} else {
- location_ids.append("," + location_id);
+ locationIds.append("," + locationId);
}
}
final Collection<Long> clusters = new LinkedList<Long>();
final Cursor cursor = db.rawQuery("SELECT DISTINCT cluster_id FROM "
+ CLUSTERS_TABLE + " WHERE location_id IN ("
- + location_ids.toString() + ")", null);
+ + locationIds.toString() + ")", null);
try {
while (cursor.moveToNext()) {
clusters.add(cursor.getLong(0));
@@ -138,13 +165,26 @@ public long getClusterId(final long location_id) {
return clusters;
}
- public abstract Location getLocation(long location_id);
+ /**
+ * Retrieves a location from the database.
+ *
+ * @param locationId
+ * The location id to retrieve.
+ */
+ public abstract Location getLocation(long locationId);
- public Collection<Long> getLocationsForCluster(final long cluster_id) {
+ /**
+ * Gets all of the locations contained in the specified cluster
+ *
+ * @param clusterId
+ * The id of the cluster.
+ * @return A collection of location ids.
+ */
+ public Collection<Long> getLocationsForCluster(final long clusterId) {
final Collection<Long> ids = new LinkedList<Long>();
final Cursor cursor = db.rawQuery("SELECT location_id FROM "
+ CLUSTERS_TABLE + " WHERE cluster_id=?", new String[] { Long
- .toString(cluster_id) });
+ .toString(clusterId) });
try {
while (cursor.moveToNext()) {
ids.add(cursor.getLong(0));
@@ -155,6 +195,11 @@ public long getClusterId(final long location_id) {
return ids;
}
+ /**
+ * Returns the next available cluster id.
+ *
+ * @return An available cluster id.
+ */
public long getNewClusterId() {
long cluster_id = -1;
final Cursor cursor = db.rawQuery("SELECT MAX(cluster_id) FROM "
@@ -169,11 +214,29 @@ public long getNewClusterId() {
return cluster_id;
}
+ /**
+ * @return The window length, in samples or seconds, depending on the value
+ * of {@link #usesTimeBasedWindow()}.
+ */
public abstract int getWindowLength();
+ /**
+ * Creates a new empty location with the specified timestamp.
+ *
+ * @param timestamp
+ * Timestamp, in milliseconds, associated with the new location.
+ */
public abstract Location newLocation(double timestamp);
+ /**
+ * Returns the percentage of the window that must be considered stationary
+ * for a new location to be created.
+ */
public abstract float pctOfWindowRequiredToBeStationary();
+ /**
+ * Returns true if the algorithm is using a fixed time length for the
+ * window, or false if the window contains a fixed number of samples.
+ */
public abstract boolean usesTimeBasedWindow();
}
View
120 src/ca/mcgill/hs/classifiers/location/MotionStateClusterer.java
@@ -16,14 +16,25 @@
import java.util.Timer;
import java.util.TimerTask;
-import android.content.Context;
import android.os.Environment;
+import ca.mcgill.hs.HSAndroid;
import ca.mcgill.hs.R;
import ca.mcgill.hs.util.Log;
+/**
+ * Uses modified DBScan clustering to determine the motion state given a
+ * sequence of observations.
+ *
+ * @author Jordan Frank <jordan.frank@cs.mcgill.ca>
+ *
+ */
public class MotionStateClusterer {
- // Contains the timestamp for the observation as well as the index in the
- // distance matrix for that observation.
+ /**
+ * Contains the timestamp for the observation as well as the index in the
+ * distance matrix for that observation.
+ *
+ * @author Jordan Frank <jordan.frank@cs.mcgill.ca>
+ */
private static final class Tuple {
public final double timestamp;
public final int index;
@@ -38,7 +49,7 @@ public Tuple(final double timestamp, final int index) {
private BufferedWriter outputLog = null;
- private static final String RESET_MOVEMENT_STATE_TIMER_NAME = "reset movement state timer";
+ private static final String RESET_MOVEMENT_STATE_TIMER_NAME = "RMSTimer";
private static final String TAG = "MotionStateClusterer";
@@ -47,40 +58,57 @@ public Tuple(final double timestamp, final int index) {
private Timer resetMovementTimer;
private long currentCluster = -1;
- // Maintain a queue of the Tuples for the observations that are
- // currently being clustered.
+ /**
+ * Maintain a queue of the Tuples for the observations that are currently
+ * being clustered.
+ */
private final LinkedList<Tuple> pool = new LinkedList<Tuple>();
- // Window length, in seconds.
+ /**
+ * Whether to use a window covering a fixed time period or a fixed number of
+ * samples.
+ */
private final boolean TIME_BASED_WINDOW;
+ /**
+ * Window length, in either seconds or samples, depending on the value of
+ * TIME_BASED_WINDOW.
+ */
private final int WINDOW_LENGTH;
- // Delta from the paper. This value represents the percentage of the points
- // in the pool that must neighbours of a point for it to be considered to be
- // part of a cluster
+ /**
+ * Delta from the paper. This value represents the percentage of the points
+ * in the pool that must neighbours of a point for it to be considered to be
+ * part of a cluster.
+ */
private final float DELTA;
- // Maintain a lookup table between timestamps and observations, which
- // are sets of measurements taken at an instance in time.
+ /**
+ * Maintain a lookup table between timestamps and observations, which are
+ * sets of measurements taken at an instance in time.
+ */
private final HashMap<Double, Observation> observations;
private final double[][] distMatrix;
- // True if the previous window was labelled as stationary.
+ /** True if the previous window was labelled as stationary. */
private boolean previouslyMoving = true;
- private boolean currentlyMoving = false;
- // True if the current window was labelled as stationary.
- // private boolean curStationaryStatus = false;
+ /** True if the current window was labelled as stationary. */
+ private boolean currentlyMoving = false;
private final SimpleDateFormat dfm = new SimpleDateFormat(
"yy-MM-dd-HH:mm:ss");
private int timerDelay = 1000 * RESET_MOVEMENT_STATE_TIME_IN_SECONDS;
- public MotionStateClusterer(final LocationSet locations,
- final Context context) {
+ /**
+ * Creates a new motion state clusterer for a given set of locations.
+ *
+ * @param locations
+ * The {@link LocationSet} for storing the locations.
+ */
+ public MotionStateClusterer(final LocationSet locations) {
TIME_BASED_WINDOW = locations.usesTimeBasedWindow();
WINDOW_LENGTH = locations.getWindowLength();
DELTA = locations.pctOfWindowRequiredToBeStationary();
@@ -102,8 +130,8 @@ public MotionStateClusterer(final LocationSet locations,
final SimpleDateFormat dfm = new SimpleDateFormat("yy-MM-dd-HHmmss");
final File recent_dir = new File(Environment
- .getExternalStorageDirectory(), (String) context.getResources()
- .getText(R.string.recent_file_path));
+ .getExternalStorageDirectory(), HSAndroid
+ .getAppString(R.string.recent_file_path));
final File f = new File(recent_dir, dfm.format(d) + "-clusters.log");
try {
outputLog = new BufferedWriter(new FileWriter(f));
@@ -114,8 +142,10 @@ public MotionStateClusterer(final LocationSet locations,
}
- // Adds a new observation to the pool, does some clustering, and then
- // returns the statuses of each point in the pool.
+ /**
+ * Adds a new observation to the pool, does some clustering, and then
+ * returns the statuses of each point in the pool.
+ */
public void addObservation(final double timestamp,
final Observation observation) {
@@ -146,6 +176,10 @@ public void addObservation(final double timestamp,
cluster(observation.getEPS());
}
+ /**
+ * Closes the clusterer and optionally writes statistics about the
+ * clustering.
+ */
public void close() {
try {
if (outputLog != null) {
@@ -169,8 +203,14 @@ public void close() {
}
}
- protected void cluster(final double eps) {
- // Perform the clustering
+ /**
+ * Perform the clustering on the locations currently in the pool.
+ *
+ * @param eps
+ * Epsilon from the clustering paper; the maximum distance
+ * between two points for them to be considered neighbours.
+ */
+ private void cluster(final double eps) {
final int pool_size = pool.size();
final boolean[] clusterStatus = new boolean[WINDOW_LENGTH];
for (int i = 0; i < WINDOW_LENGTH; i++) {
@@ -282,12 +322,13 @@ public void run() {
}
}
- // Deletes the oldest observation from the pool and returns the index
- // for that point in the distance matrix, so that it can be reused.
+ /**
+ * Deletes the oldest observation from the pool and returns the index for
+ * that point in the distance matrix, so that it can be reused.
+ */
private void deleteOldObservations(final double timestamp) {
-
- // Delete anything older than WINDOW_LENGTH seconds
if (TIME_BASED_WINDOW) {
+ // Delete anything older than WINDOW_LENGTH seconds.
while (pool.size() > 0
&& (timestamp - pool.getFirst().timestamp > WINDOW_LENGTH || pool
.size() >= WINDOW_LENGTH)) {
@@ -302,9 +343,8 @@ private void deleteOldObservations(final double timestamp) {
}
}
- // Only delete the one oldest observation (ie., fixed length sliding
- // window)
else {
+ // Only delete the one oldest observation.
if (!pool.isEmpty() && pool.size() >= WINDOW_LENGTH) {
final Tuple first = pool.getFirst();
observations.remove(first.timestamp);
@@ -318,7 +358,7 @@ private void deleteOldObservations(final double timestamp) {
}
}
- // Returns the first available index in the distance matrix.
+ /** Returns the first available index in the distance matrix. */
public int getAvailableIndex() {
int idx = 0;
while (distMatrix[0][idx] >= 0) {
@@ -327,22 +367,32 @@ public int getAvailableIndex() {
return idx;
}
+ /** Returns some clustering statistics */
public String getClusterStatus() {
return slClusterer.toString();
}
+ /**
+ * Returns the window length in either seconds or samples, depending on the
+ * value of TIME_BASED_WINDOW.
+ */
public int getMaxTime() {
return WINDOW_LENGTH;
}
+ /**
+ * Returns the most recently assigned cluster id.
+ *
+ * @return The id of the most recently assigned cluster.
+ */
public long getMostRecentClusterId() {
return currentCluster;
}
- public int getPoolSize() {
- return pool.size();
- }
-
+ /**
+ * @return True if the most recent observation was deemed moving, or false
+ * if it was deemed stationary.
+ */
public boolean lastObservationWasMoving() {
return currentlyMoving;
}
View
20 src/ca/mcgill/hs/classifiers/location/Observation.java
@@ -5,9 +5,29 @@
*/
package ca.mcgill.hs.classifiers.location;
+/**
+ * Contains observations, derived from sensor data. Observations form the basis
+ * of {@link Location}s.
+ *
+ * @author Jordan Frank <jordan.frank@cs.mcgill.ca>
+ *
+ */
public abstract class Observation {
+ /**
+ * Computes the distance between this observation and another.
+ *
+ * @param other
+ * The observation against which to compare this one.
+ * @return A distance value.
+ */
public abstract double distanceFrom(Observation other);
+ /**
+ * Epsilon is the maximum distance between two points for them to be
+ * considered neighbours.
+ *
+ * @return Epsilon
+ */
public abstract double getEPS();
}
View
82 src/ca/mcgill/hs/classifiers/location/SignificantLocationClusterer.java
@@ -10,43 +10,41 @@
import java.util.List;
/**
- * This class clusters a set of locations, which are really points at which the
- * user was stationary for a brief period of time, into places where the user
- * visits regularly.
+ * This class clusters a set of locations at which the user was stationary for a
+ * brief period of time into places where the user visits regularly.
+ *
+ * @author Jordan Frank <jordan.frank@cs.mcgill.ca>
*
*/
public class SignificantLocationClusterer {
- // Delta from the paper. This value represents the percentage of the points
- // in the pool that must neighbours of a point for it to be considered to be
- // part of a cluster
- // private static final float DELTA = 0.90f;
-
- /** The minimum number of points required to be clustered. */
+ /** The minimum number of neighbours required to form a cluster */
private static final int MIN_PTS = 10;
- /** A set of neighbour points. */
+ /** The set of neighbour points. */
private final LocationSet pool;
/**
- * Constructs a significant location clusterer using the specified
- * NeighbourPointSet.
+ * Constructs a significant location clusterer using the specified set of
+ * neighbour points.
*
* @param set
- * The set to use and modify.
+ * The set that contains the locations. This set will be updated
+ * with new locations and clusters.
*/
public SignificantLocationClusterer(final LocationSet set) {
this.pool = set;
}
/**
- * Adds all the cluster IDs of <tt>point</tt>'s neighbours to the set passed
- * as parameter.
+ * Updates the set of clusters that need to be merged by adding all clusters
+ * that the neighbours of <tt>location</tt> belong to to the set of clusters
+ * that need to be merged.
*
* @param location
* The point whose neighbours' clusters must be added.
* @param clustersToMerge
- * The set of clusters.
+ * The set of clusters to be merged. This set gets updated.
*/
private void addNeighbouringClusters(final Location location,
final Collection<Long> clustersToMerge) {
@@ -65,16 +63,16 @@ private void addNeighbouringClusters(final Location location,
}
/**
- * Adds the point's neighbours to the specified cluster.
+ * Adds a locations' neighbours to the specified cluster.
*
- * @param point
- * The point whose neighbours must be moved.
+ * @param location
+ * The point whose neighbours must be added.
* @param clusterId
- * The cluster to move them to.
+ * The cluster id that the neighbours will belong to.
*/
- private void addNeighboursToCluster(final Location point,
+ private void addNeighboursToCluster(final Location location,
final long clusterId) {
- final List<Long> neighbours = point.getNeighbours();
+ final List<Long> neighbours = location.getNeighbours();
for (final long neighbour_id : neighbours) {
DebugHelper.out.println("\tAdding neighbour " + neighbour_id
+ " to cluster " + clusterId + ": ");
@@ -84,14 +82,12 @@ private void addNeighboursToCluster(final Location point,
}
/**
- * Adds a neighbour point to the pool of observed points.
+ * Adds a new location to the pool of observed locations.
*
* @param location
* The new location to be added.
*/
public long addNewLocation(Location location) {
- pool.cacheLocation(location);
-
// Add the point to the pool, compute neighbours.
final long new_id = pool.add(location);
if (new_id != location.getId()) {
@@ -100,10 +96,20 @@ public long addNewLocation(Location location) {
return assignToCluster(location);
}
+ /**
+ * Does the work of adding a location to a cluster. This checks to see if
+ * the location should be added to a cluster, and also checks its neighbours
+ * to see if they should be added to a cluster. The location may already be
+ * assigned to a cluster, in which case its neighbours are also merged into
+ * that cluster if necessary.
+ *
+ * @param location
+ * The location to cluster. May or may not already have a cluster
+ * id assigned to it.
+ * @return The id of the cluster, or -1 if the location is not a part of a
+ * cluster.
+ */
public long assignToCluster(final Location location) {
- // if (location.getId() > 4) {
- // System.exit(0);
- // }
final long location_id = location.getId();
long cluster_id = pool.getClusterId(location_id);
DebugHelper.out.println("Clustering location " + location_id
@@ -182,18 +188,18 @@ public long assignToCluster(final Location location) {
}
/**
- * Creates a new cluster centered around the specified point. Returns the ID
- * of the newly formed cluster.
+ * Creates a new cluster centered around the specified location. Returns the
+ * ID of the newly formed cluster.
*
- * @param point
- * The point centering the new cluster.
- * @return The ID of the newly formed cluster.
+ * @param location
+ * The location representing the new cluster.
+ * @return The id of the newly formed cluster.
*/
- private long createNewCluster(final Location point) {
+ private long createNewCluster(final Location location) {
final long clusterID = pool.getNewClusterId();
- pool.addToCluster(point.getId(), clusterID);
- DebugHelper.out.println("\tAdded " + point.getId() + " to new cluster "
- + clusterID);
+ pool.addToCluster(location.getId(), clusterID);
+ DebugHelper.out.println("\tAdded " + location.getId()
+ + " to new cluster " + clusterID);
return clusterID;
}
@@ -223,7 +229,7 @@ public String toString() {
if (noise == (locations.size() - sum)) {
result += " (Validated)";
} else {
- result += " (Invalid, Actually " + noise + " Noise Points)";
+ result += " (Invalid, Should be " + noise + " Noise Points)";
}
return result;
}
View
28 src/ca/mcgill/hs/classifiers/location/WifiClusterer.java
@@ -11,30 +11,48 @@
private final WifiLocationSet locations;
private final MotionStateClusterer pool;
+ /**
+ * Constructs a new clusterer for wifi signals. Requires the context in
+ * order to access the databases for this application.
+ *
+ * @param context
+ * The application context.
+ */
public WifiClusterer(final Context context) {
locations = new WifiLocationSet(context);
- pool = new MotionStateClusterer(locations, context);
+ pool = new MotionStateClusterer(locations);
}
/**
- * Close must be called or we leave the database in a bad state.
+ * Closes the clusterer. It is important that this method be called or the
+ * database may not be properly closed.
*/
public void close() {
pool.close();
locations.close();
}
- // Returns a list the same length as timestamps that contains the votes
- // for each event. A positive value indicates motion, while a negative value
- // indicates stationarity.
+ /**
+ * Adds a new observation to the pool, and attempts to cluster it.
+ *
+ * @param observation
+ * The new observation to add to the cluster pool.
+ */
public void cluster(final WifiObservation observation) {
pool.addObservation(observation.getTimeStamp(), observation);
}
+ /**
+ * @return The id of the most recently assigned cluster.
+ */
public long getCurrentCluster() {
return pool.getMostRecentClusterId();
}
+ /**
+ * @return True if the last observation was deemed moving, or False if the
+ * last observation was deemed stationary.
+ */
public boolean isMoving() {
return pool.lastObservationWasMoving();
}
View
56 src/ca/mcgill/hs/classifiers/location/WifiLocation.java
@@ -17,9 +17,11 @@
/**
* A location characterized by observable wifi base stations and the measured
* signal strength corresponding to each base station. Distance is computed by
- * measuring the difference between signal strengths for base stations that are
- * common to both locations, with a requirement that the locations have a
- * substantial fraction of the base stations in common.
+ * measuring the difference between signal strengths for base stations (WAPs)
+ * that are common to both locations, with a requirement that the locations have
+ * a substantial fraction of the base stations in common.
+ *
+ * @author Jordan Frank <jordan.frank@cs.mcgill.ca>
*/
public class WifiLocation extends Location {
@@ -45,10 +47,26 @@
*/
public static final double MERGE_DIST = 3.0;
+ /**
+ * Constructs a new location with a specified timestamp.
+ *
+ * @param db
+ * The database in which locations are stored.
+ * @param timestamp
+ * The timestamp, in milliseconds, for the new location.
+ */
public WifiLocation(final SQLiteDatabase db, final double timestamp) {
super(db, timestamp);
}
+ /**
+ * Constructs a new location with a specified id.
+ *
+ * @param db
+ * The database in which locations are stored.
+ * @param id
+ * The id for the new location.
+ */
public WifiLocation(final SQLiteDatabase db, final long id) {
super(db, id);
}
@@ -72,10 +90,10 @@ public void addObservation(final Observation obs) {
final SQLiteStatement updateStrengthStmt = db
.compileStatement("UPDATE "
+ WifiLocationSet.OBSERVATIONS_TABLE
- + " SET strength=strength+?, count=count+1, average_strength=(strength+?)/(count+1) WHERE location_id=? AND wap_id=?");
+ + " SET strength=strength+?, count=count+1, "
+ + "average_strength=(strength+?)/(count+1) "
+ + "WHERE location_id=? AND wap_id=?");
updateStrengthStmt.bindLong(3, my_id);
- // DebugHelper.out.println("Here with " + observation.num_observations
- // + " observations.");
try {
db.beginTransaction();
for (final Entry<Integer, Integer> entry : observation.measurements
@@ -106,6 +124,13 @@ public void addObservation(final Observation obs) {
@Override
public double distanceFrom(final Location other) {
+ /*
+ * Distance between two locations is computed by root mean squared
+ * differences between signal strengths for all WAPs that are observed
+ * from both locations. If the ratio of the number of WAPs that are
+ * common to both locations to the minimum number of WAPs observed from
+ * either location is less than ETA, a distance of INFINITY is returned.
+ */
final WifiLocation location = (WifiLocation) other;
double dist = 0.0;
int num_common = 0;
@@ -142,7 +167,7 @@ public double distanceFrom(final Location other) {
* Returns the average strength of this observation at the specified WAP ID.
*
* @param wap_id
- * The WAP ID to examine.
+ * The WAP id to examine.
* @return The average strength of this observation at the specified WAP ID.
*/
public double getAvgStrength(final int wap_id) {
@@ -160,13 +185,20 @@ public double getAvgStrength(final int wap_id) {
return avgStrength;
}
+ /**
+ * @return The number of observations associated with this location.
+ */
public int getNumObservations() {
if (num_observations < 0) {
updateNumObservations();
}
return num_observations;
}
+ /**
+ * @return The set of all WAP ids associated with the observations for this
+ * location.
+ */
public Set<Integer> getObservableWAPs() {
final Set<Integer> wap_ids = new HashSet<Integer>();
final Cursor cursor = db.rawQuery("SELECT wap_id FROM "
@@ -184,16 +216,16 @@ public int getNumObservations() {
@Override
public Observation getObservations() {
- final WifiObservation observation = new WifiObservation(getTimestamp(),
- 35);
final Cursor cursor = db.rawQuery(
"SELECT wap_id,average_strength FROM "
+ WifiLocationSet.OBSERVATIONS_TABLE
+ " WHERE location_id=?", new String[] { Long
.toString(getId()) });
+ final WifiObservation observation = new WifiObservation(getTimestamp(),
+ cursor.getCount());
try {
while (cursor.moveToNext()) {
- observation.addObservation(cursor.getInt(0), cursor.getInt(1));
+ observation.addMeasurement(cursor.getInt(0), cursor.getInt(1));
}
} finally {
cursor.close();
@@ -220,6 +252,10 @@ public String toString() {
return sb.toString();
}
+ /**
+ * Updates the number of observations associated with this location. We
+ * cache this value, to save on database queries, as it is frequently used.
+ */
private void updateNumObservations() {
final Cursor cursor = db.rawQuery("SELECT COUNT(*) FROM "
+ WifiLocationSet.OBSERVATIONS_TABLE + " WHERE location_id=?",
View
96 src/ca/mcgill/hs/classifiers/location/WifiLocationSet.java
@@ -14,19 +14,22 @@
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
-import android.database.sqlite.SQLiteStatement;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import ca.mcgill.hs.util.LRUCache;
import ca.mcgill.hs.util.Log;
/**
- * Manages a set of Wifi-based Location fingerprints
+ * Manages a set of Wifi-based Locations
+ *
+ * @author Jordan Frank <jordan.frank@cs.mcgill.ca>
*/
public final class WifiLocationSet extends LocationSet {
/**
* Handles the creation and updating of the schema for the wifi location
* database.
+ *
+ * @author Jordan Frank <jordan.frank@cs.mcgill.ca>
*/
private static class WifiDatabaseHelper extends SQLiteOpenHelper {
@@ -101,22 +104,26 @@ public void onUpgrade(final SQLiteDatabase db, final int oldVersion,
private static final String TAG = "WifiLocationSet";
- /**
- * Database table names.
- */
+ // Database table names.
public static final String OBSERVATIONS_TABLE = "observations";
public static final String WAPS_TABLE = "waps";
/**
- * Used to cache the most recent locations, avoids some database calls. We
- * currently cache the last 100 most recently used locations.
+ * The number of locations to cache.
+ */
+ private static final int CACHE_SIZE = 100;
+ /**
+ * Used to cache the last 100 most recently used locations, saves on
+ * database queries.
*/
private final LRUCache<Long, WifiLocation> locationCache = new LRUCache<Long, WifiLocation>(
- 100);
+ CACHE_SIZE);
/**
* Do not use a time-based window, as samples can often come in at irregular
- * intervals.
+ * intervals. This could use some tuning; it might work better if we use a
+ * time-based window but use the wifi-scanning interval value to pick a good
+ * time window.
*/
private static final boolean TIME_BASED_WINDOW = false;
@@ -128,29 +135,23 @@ public void onUpgrade(final SQLiteDatabase db, final int oldVersion,
/**
* Delta from the paper. This value represents the percentage of the points
* in the pool that must neighbours of a point for it to be considered to be
- * part of a cluster
+ * part of a cluster.
*/
private static final float DELTA = 0.8f;
private final WifiDatabaseHelper dbHelper;
/**
- * Prepared statement for retrieving the number of merged locations. This is
- * used quite frequently but we have not actually benchmarked prepared
- * statements, and it is possible that this doesn't speed anything up.
+ * Controls whether we copy the database from the sdcard before opening it.
+ * This allows us, for debugging purposes, to modify the database offline
+ * and place it on the sdcard, to be used the next time the application is
+ * started.
*/
- SQLiteStatement retrieveNumMergedStmt = null;
private final boolean copyFromSDCard = false;
public WifiLocationSet(final Context context) {
if (copyFromSDCard) {
try {
- /*
- * We currently copy the database from the sdcard. This is
- * mainly used for debugging so that we can make changes to the
- * database offline and then use those changes next time we
- * start the service. This should eventually be removed.
- */
DBHelpers.copy(new File(
"/sdcard/hsandroidapp/data/wificlusters.db"), new File(
"/data/data/ca.mcgill.hs/databases/wificlusters.db"));
@@ -164,7 +165,7 @@ public WifiLocationSet(final Context context) {
// Uncomment for in-memory database
// db = SQLiteDatabase.create(null);
- // uncomment to recreate databases
+ // uncomment to delete and recreate all of the tables.
// dbHelper.onCreate(db);
/*
@@ -178,20 +179,22 @@ public WifiLocationSet(final Context context) {
* Does a little optimization when the service is started. Can't hurt.
*/
db.execSQL("ANALYZE");
- retrieveNumMergedStmt = db.compileStatement("SELECT num_merged FROM "
- + LocationSet.LOCATIONS_TABLE + " WHERE location_id=?");
}
@Override
public long add(final Location loc) {
final WifiLocation location = (WifiLocation) loc;
final long location_id = location.getId();
+ // Add the location to the cache.
+ cacheLocation(location);
+
/*
* Compute a threshold for minumum number of waps that must be common to
* be considered a neighbour. Note that if the neighbour has more waps,
* then it might have to be pruned out later. This is just used to
- * retrieve from the database a set of potential neighbours.
+ * retrieve from the database a set of potential neighbours, to quickly
+ * check if the location is a potential neighbour.
*/
final Long temp_threshold = (long) (location.getNumObservations() * WifiLocation.ETA);
@@ -215,12 +218,9 @@ public long add(final Location loc) {
} finally {
cursor.close();
}
- // Remove current location from the neighbours list.
- try {
- possibleNeighbours.remove(location_id);
- } catch (final IndexOutOfBoundsException e) {
- // Ignore.
- }
+ // Make sure the current location isn't in the list of neighbours.
+ possibleNeighbours.remove(location_id);
+
Log.d(TAG, "Adding " + possibleNeighbours.size()
+ " possible neighbours.");
@@ -236,7 +236,7 @@ public long add(final Location loc) {
DebugHelper.out.println("\tDistance between " + location.getId()
+ " and " + neighbour_id + " is " + dist);
- // Merge location if it is very close to a possible neighbour.
+ // Keep track of minimum distance to any neighbour
if (dist < min_dist) {
min_dist = dist;
nearest_neighbour_id = neighbour_id;
@@ -261,9 +261,13 @@ public long add(final Location loc) {
return location_id;
}
- @Override
- public void cacheLocation(final Location location) {
- locationCache.put(location.getId(), (WifiLocation) location);
+ /**
+ * Adds a location to the cache.
+ *
+ * @params location The location to be added to the cache.
+ */
+ private void cacheLocation(final WifiLocation location) {
+ locationCache.put(location.getId(), location);
}
public void close() {
@@ -289,6 +293,12 @@ public void close() {
}
}
+ /**
+ *
+ * @param id
+ * Location id to display
+ * @return Description of the location.
+ */
public String displayLocation(final long id) {
WifiLocation loc = null;
loc = new WifiLocation(db, id);
@@ -350,18 +360,14 @@ public Location newLocation(final double timestamp) {
return new WifiLocation(db, timestamp);
}
- /**
- * Returns the DELTA parameter, or the fraction of the buffer that must be
- * part of a cluster in order for the user to be considered stationary.
- */
@Override
public float pctOfWindowRequiredToBeStationary() {
return DELTA;
}
/**
* Returns the number of locations that have been merged into this location.
- * This should be used for computing the number of "neighborus" of a
+ * This should be used for computing the number of "neighbours" of a
* location, because if this is >1, then it means that this location
* actually represents more than one location, and should count as more than
* one neighbour.
@@ -372,14 +378,14 @@ public float pctOfWindowRequiredToBeStationary() {
* location.
*/
public long retrieveNumMerged(final long location_id) {
- retrieveNumMergedStmt.bindLong(0, location_id);
- return retrieveNumMergedStmt.simpleQueryForLong();
+ WifiLocation location = locationCache.get(location_id);
+ if (location == null) {
+ location = new WifiLocation(db, location_id);
+ cacheLocation(location);
+ }
+ return location.getNumMerged();
}
- /**
- * Returns a value specifying whether the MotionStateClassifier should use a
- * time-based window.
- */
@Override
public boolean usesTimeBasedWindow() {
return TIME_BASED_WINDOW;
View
50 src/ca/mcgill/hs/classifiers/location/WifiObservation.java
@@ -9,26 +9,52 @@
import java.util.Map;
import java.util.Map.Entry;
+/**
+ * A Wifi observation consists of a set of signal strength measurements. Each
+ * measurement consists of a WAP id (BSSID) and rssi. Observations can then be
+ * compared by comparing the signal strengths of common WAPs.
+ *
+ * @author Jordan Frank <jordan.frank@cs.mcgill.ca>