blob: 4bc0369954caf1cefc24d49776c9646b6d085b71 [file] [log] [blame]
Ruei-sung Linf0f78442012-08-13 19:04:29 -07001/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.bordeaux.services;
18
Ruei-sung Lin5d42ffa2012-08-23 16:01:57 -070019import android.content.Context;
Ruei-sung Linf0f78442012-08-13 19:04:29 -070020import android.location.Location;
21import android.text.format.Time;
22import android.util.Log;
23
24import java.util.ArrayList;
Ruei-sung Lin5d42ffa2012-08-23 16:01:57 -070025import java.util.HashMap;
Ruei-sung Linf0f78442012-08-13 19:04:29 -070026import java.util.HashSet;
Ruei-sung Lin5d42ffa2012-08-23 16:01:57 -070027import java.util.List;
28import java.util.Map;
Ruei-sung Linf0f78442012-08-13 19:04:29 -070029
30/**
31 * ClusterManager incrementally indentify representitve clusters from the input location
32 * stream. Clusters are updated online using leader based clustering algorithm. The input
33 * locations initially are kept by the clusters. Periodially, a cluster consolidating
34 * procedure is carried out to refine the cluster centers. After consolidation, the
35 * location data are released.
36 */
37public class ClusterManager {
38
39 private static String TAG = "ClusterManager";
40
Ruei-sung Lin83954e82012-08-28 18:00:48 -070041 private static float LOCATION_CLUSTER_RADIUS = 50; // meter
Ruei-sung Linf0f78442012-08-13 19:04:29 -070042
Ruei-sung Lin83954e82012-08-28 18:00:48 -070043 private static float SEMANTIC_CLUSTER_RADIUS = 100; // meter
Ruei-sung Linf0f78442012-08-13 19:04:29 -070044
Ruei-sung Lin78a66d92012-08-29 10:30:03 -070045 // Consoliate location clusters (and check for new semantic clusters)
46 // every 30 minutes (1800 seconds).
47 private static final long CONSOLIDATE_INTERVAL = 1800;
Ruei-sung Linf0f78442012-08-13 19:04:29 -070048
Ruei-sung Lin78a66d92012-08-29 10:30:03 -070049 // Prune away clusters that are stayed for less than 3 minutes (180 seconds)
50 private static long LOCATION_CLUSTER_THRESHOLD = 180;
Ruei-sung Linf0f78442012-08-13 19:04:29 -070051
Ruei-sung Lin78a66d92012-08-29 10:30:03 -070052 // A location cluster can be labeled as a semantic cluster if it has been
53 // stayed for at least 10 minutes (600 seconds) within a day.
54 private static final long SEMANTIC_CLUSTER_THRESHOLD = 600; // seconds
Ruei-sung Lin83954e82012-08-28 18:00:48 -070055
Ruei-sung Lin78a66d92012-08-29 10:30:03 -070056 // Reset location cluters every 6 hours (21600 seconds).
57 private static final long LOCATION_REFRESH_PERIOD = 21600; // seconds
Ruei-sung Lin5d42ffa2012-08-23 16:01:57 -070058
59 private static String UNKNOWN_LOCATION = "Unknown Location";
60
61 private static String HOME = "Home";
62
63 private static String OFFICE = "Office";
Ruei-sung Linf0f78442012-08-13 19:04:29 -070064
65 private Location mLastLocation = null;
66
Ruei-sung Lin78a66d92012-08-29 10:30:03 -070067 private long mClusterDuration;
68
Ruei-sung Linf0f78442012-08-13 19:04:29 -070069 private long mTimeRef = 0;
70
71 private long mSemanticClusterCount = 0;
72
Ruei-sung Lin78a66d92012-08-29 10:30:03 -070073 private ArrayList<LocationCluster> mLocationClusters = new ArrayList<LocationCluster>();
Ruei-sung Linf0f78442012-08-13 19:04:29 -070074
75 private ArrayList<SemanticCluster> mSemanticClusters = new ArrayList<SemanticCluster>();
76
Ruei-sung Lin5d42ffa2012-08-23 16:01:57 -070077 private AggregatorRecordStorage mStorage;
78
79 private static String SEMANTIC_TABLE = "SemanticTable";
80
81 private static String SEMANTIC_ID = "ID";
82
Ruei-sung Lin83954e82012-08-28 18:00:48 -070083 private static final String SEMANTIC_LONGITUDE = "Longitude";
Ruei-sung Lin5d42ffa2012-08-23 16:01:57 -070084
Ruei-sung Lin83954e82012-08-28 18:00:48 -070085 private static final String SEMANTIC_LATITUDE = "Latitude";
Ruei-sung Lin5d42ffa2012-08-23 16:01:57 -070086
Ruei-sung Lin83954e82012-08-28 18:00:48 -070087 private static final String[] SEMANTIC_COLUMNS =
88 new String[]{ SEMANTIC_ID,
89 SEMANTIC_LONGITUDE,
90 SEMANTIC_LATITUDE,
91 TimeStatsAggregator.WEEKEND,
92 TimeStatsAggregator.WEEKDAY,
93 TimeStatsAggregator.MORNING,
94 TimeStatsAggregator.NOON,
95 TimeStatsAggregator.AFTERNOON,
96 TimeStatsAggregator.EVENING,
97 TimeStatsAggregator.NIGHT,
98 TimeStatsAggregator.LATENIGHT };
99
100 private static final int mFeatureValueStart = 3;
101 private static final int mFeatureValueEnd = 10;
Ruei-sung Lin5d42ffa2012-08-23 16:01:57 -0700102
103 public ClusterManager(Context context) {
104 mStorage = new AggregatorRecordStorage(context, SEMANTIC_TABLE, SEMANTIC_COLUMNS);
105
106 loadSemanticClusters();
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700107 }
108
109 public void addSample(Location location) {
110 float bestClusterDistance = Float.MAX_VALUE;
111 int bestClusterIndex = -1;
112 long lastDuration;
Ruei-sung Lin83954e82012-08-28 18:00:48 -0700113 long currentTime = location.getTime() / 1000; // measure time in seconds
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700114
115 if (mLastLocation != null) {
116 // get the duration spent in the last location
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700117 long duration = (location.getTime() - mLastLocation.getTime()) / 1000;
118 mClusterDuration += duration;
119
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700120 Log.v(TAG, "sample duration: " + duration +
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700121 ", number of clusters: " + mLocationClusters.size());
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700122
123 // add the last location to cluster.
124 // first find the cluster it belongs to.
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700125 for (int i = 0; i < mLocationClusters.size(); ++i) {
126 float distance = mLocationClusters.get(i).distanceToCenter(mLastLocation);
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700127 Log.v(TAG, "clulster " + i + " is within " + distance + " meters");
128 if (distance < bestClusterDistance) {
129 bestClusterDistance = distance;
130 bestClusterIndex = i;
131 }
132 }
133
134 // add the location to the selected cluster
135 if (bestClusterDistance < LOCATION_CLUSTER_RADIUS) {
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700136 mLocationClusters.get(bestClusterIndex).addSample(mLastLocation, duration);
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700137 } else {
138 // if it is far away from all existing clusters, create a new cluster.
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700139 LocationCluster cluster = new LocationCluster(mLastLocation, duration);
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700140 // move the center of the new cluster if its covering region overlaps
141 // with an existing cluster.
142 if (bestClusterDistance < 2 * LOCATION_CLUSTER_RADIUS) {
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700143 Log.v(TAG, "move away activated");
144 cluster.moveAwayCluster(mLocationClusters.get(bestClusterIndex),
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700145 ((float) 2 * LOCATION_CLUSTER_RADIUS));
146 }
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700147 mLocationClusters.add(cluster);
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700148 }
149 } else {
150 mTimeRef = currentTime;
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700151
152 if (mLocationClusters.isEmpty()) {
153 mClusterDuration = 0;
154 }
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700155 }
156
157 long collectDuration = currentTime - mTimeRef;
158 Log.e(TAG, "collect duration: " + collectDuration);
159 if (collectDuration > CONSOLIDATE_INTERVAL) {
160 // TODO : conslidation takes time. move this to a separate thread later.
161 consolidateClusters(collectDuration);
162 mTimeRef = currentTime;
163 }
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700164 mLastLocation = location;
165 }
166
167 private void consolidateClusters(long duration) {
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700168 LocationCluster cluster;
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700169 for (int i = mLocationClusters.size() - 1; i >= 0; --i) {
170 cluster = mLocationClusters.get(i);
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700171 cluster.consolidate(duration);
172
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700173 // remove transit clusters
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700174 if (!cluster.passThreshold(LOCATION_CLUSTER_THRESHOLD)) {
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700175 mLocationClusters.remove(cluster);
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700176 }
177 }
178
179 // merge clusters whose regions are overlapped. note that after merge
Ruei-sung Lin5d42ffa2012-08-23 16:01:57 -0700180 // cluster center changes but cluster size remains unchanged.
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700181 for (int i = mLocationClusters.size() - 1; i >= 0; --i) {
182 cluster = mLocationClusters.get(i);
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700183 for (int j = i - 1; j >= 0; --j) {
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700184 float distance = mLocationClusters.get(j).distanceToCluster(cluster);
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700185 if (distance < LOCATION_CLUSTER_RADIUS) {
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700186 mLocationClusters.get(j).absorbCluster(cluster);
187 mLocationClusters.remove(cluster);
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700188 }
189 }
190 }
Ruei-sung Lin5d42ffa2012-08-23 16:01:57 -0700191
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700192 // check if new semantic clusters are found
193 if (findNewSemanticClusters() &&
194 mClusterDuration < LOCATION_REFRESH_PERIOD) {
195 saveSemanticClusters();
196 }
Ruei-sung Lin5d42ffa2012-08-23 16:01:57 -0700197
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700198 if (mClusterDuration > LOCATION_REFRESH_PERIOD) {
199 updateSemanticClusters();
200 mClusterDuration = 0;
201 }
202 }
203
204 private boolean findNewSemanticClusters() {
205 // select candidate location clusters
206 ArrayList<LocationCluster> candidates = new ArrayList<LocationCluster>();
207 for (LocationCluster cluster : mLocationClusters) {
208 if (!cluster.hasSemanticId() &&
209 cluster.passThreshold(SEMANTIC_CLUSTER_THRESHOLD)) {
210 candidates.add(cluster);
211 }
212 }
213
214 // assign each candidate to a semantic cluster
215 boolean foundNewClusters = false;
216 for (LocationCluster candidate : candidates) {
217 // find the closest semantic cluster
218 float bestClusterDistance = Float.MAX_VALUE;
219 SemanticCluster bestCluster = null;
220 for (SemanticCluster cluster : mSemanticClusters) {
221 float distance = cluster.distanceToCluster(candidate);
222 Log.v(TAG, "distance to semantic cluster: " + cluster.getSemanticId());
223
224 if (distance < bestClusterDistance) {
225 bestClusterDistance = distance;
226 bestCluster = cluster;
227 }
228 }
229
230 // if candidate doesn't belong to any semantic cluster, create a new
231 // semantic cluster
232 if (bestClusterDistance > SEMANTIC_CLUSTER_RADIUS) {
233 // if it is far away from all existing clusters, create a new cluster.
234 bestCluster = new SemanticCluster(candidate, mSemanticClusterCount++);
235 String id = bestCluster.getSemanticId();
236
237 // Add new semantic cluster to the list.
238 mSemanticClusters.add(bestCluster);
239
240 foundNewClusters = true;
241 }
242 candidate.setSemanticId(bestCluster.getSemanticId());
243 }
244 candidates.clear();
245 return foundNewClusters;
246 }
247
248 private void updateSemanticClusters() {
249 synchronized (mSemanticClusters) {
250 // initialize semanticMap
251 HashMap<String, ArrayList<BaseCluster> > semanticMap =
252 new HashMap<String, ArrayList<BaseCluster> >();
253 for (SemanticCluster cluster : mSemanticClusters) {
254 String semanticId = cluster.getSemanticId();
255 semanticMap.put(cluster.getSemanticId(), new ArrayList<BaseCluster>());
256 semanticMap.get(semanticId).add(cluster);
257 }
258
259 // assign each candidate to a semantic cluster
260 for (LocationCluster cluster : mLocationClusters) {
261 if (cluster.hasSemanticId()) {
262 semanticMap.get(cluster.getSemanticId()).add(cluster);
263 }
264 }
265 // reset location clusters.
266 mLocationClusters.clear();
267 mLastLocation = null;
268 mTimeRef = 0;
269
270 // use candidates semantic cluster
271 BaseCluster newCluster = new BaseCluster();
272 for (ArrayList<BaseCluster> clusterList : semanticMap.values()) {
273 SemanticCluster semanticCluster = (SemanticCluster) clusterList.get(0);
274
275 if (clusterList.size() > 1) {
276 newCluster.setCluster(clusterList.get(1));
277 for (int i = 2; i < clusterList.size(); i++) {
278 newCluster.absorbCluster(clusterList.get(i));
279 }
280 semanticCluster.absorbCluster(newCluster);
281 } else {
282 // cluster with no new candidate
283 Log.e(TAG, "semantic cluster with no new location clusters: " +
284 semanticCluster);
285 }
286 }
287 }
Ruei-sung Lin5d42ffa2012-08-23 16:01:57 -0700288 saveSemanticClusters();
289 }
290
Ruei-sung Lin5d42ffa2012-08-23 16:01:57 -0700291 private void loadSemanticClusters() {
292 List<Map<String, String> > allData = mStorage.getAllData();
Ruei-sung Lin83954e82012-08-28 18:00:48 -0700293 HashMap<String, Long> histogram = new HashMap<String, Long>();
Ruei-sung Lin5d42ffa2012-08-23 16:01:57 -0700294
Wei Hua9c3a7dc2012-08-28 13:46:31 -0700295 synchronized (mSemanticClusters) {
296 mSemanticClusters.clear();
297 for (Map<String, String> map : allData) {
298 String semanticId = map.get(SEMANTIC_ID);
299 double longitude = Double.valueOf(map.get(SEMANTIC_LONGITUDE));
300 double latitude = Double.valueOf(map.get(SEMANTIC_LATITUDE));
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700301 SemanticCluster cluster =
302 new SemanticCluster(semanticId, longitude, latitude);
Ruei-sung Lin83954e82012-08-28 18:00:48 -0700303
Wei Hua9c3a7dc2012-08-28 13:46:31 -0700304 histogram.clear();
305 for (int i = mFeatureValueStart; i <= mFeatureValueEnd; i++) {
306 String featureValue = SEMANTIC_COLUMNS[i];
307 if (map.containsKey(featureValue)) {
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700308 histogram.put(featureValue, Long.valueOf(map.get(featureValue)));
Wei Hua9c3a7dc2012-08-28 13:46:31 -0700309 }
Ruei-sung Lin83954e82012-08-28 18:00:48 -0700310 }
Wei Hua9c3a7dc2012-08-28 13:46:31 -0700311 cluster.setHistogram(histogram);
Wei Hua9c3a7dc2012-08-28 13:46:31 -0700312 mSemanticClusters.add(cluster);
Ruei-sung Lin83954e82012-08-28 18:00:48 -0700313 }
Wei Hua9c3a7dc2012-08-28 13:46:31 -0700314 mSemanticClusterCount = mSemanticClusters.size();
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700315 Log.e(TAG, "load " + mSemanticClusterCount + " semantic clusters.");
Ruei-sung Lin5d42ffa2012-08-23 16:01:57 -0700316 }
Ruei-sung Lin5d42ffa2012-08-23 16:01:57 -0700317 }
318
319 private void saveSemanticClusters() {
320 HashMap<String, String> rowFeatures = new HashMap<String, String>();
Ruei-sung Lin5d42ffa2012-08-23 16:01:57 -0700321
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700322 mStorage.removeAllData();
Wei Hua9c3a7dc2012-08-28 13:46:31 -0700323 synchronized (mSemanticClusters) {
Wei Hua9c3a7dc2012-08-28 13:46:31 -0700324 for (SemanticCluster cluster : mSemanticClusters) {
325 rowFeatures.clear();
326 rowFeatures.put(SEMANTIC_ID, cluster.getSemanticId());
Ruei-sung Lin5d42ffa2012-08-23 16:01:57 -0700327
Wei Hua9c3a7dc2012-08-28 13:46:31 -0700328 rowFeatures.put(SEMANTIC_LONGITUDE,
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700329 String.valueOf(cluster.getCenterLongitude()));
Wei Hua9c3a7dc2012-08-28 13:46:31 -0700330 rowFeatures.put(SEMANTIC_LATITUDE,
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700331 String.valueOf(cluster.getCenterLatitude()));
Ruei-sung Lin83954e82012-08-28 18:00:48 -0700332
Wei Hua9c3a7dc2012-08-28 13:46:31 -0700333 HashMap<String, Long> histogram = cluster.getHistogram();
334 for (Map.Entry<String, Long> entry : histogram.entrySet()) {
335 rowFeatures.put(entry.getKey(), String.valueOf(entry.getValue()));
336 }
337 mStorage.addData(rowFeatures);
Ruei-sung Lin78a66d92012-08-29 10:30:03 -0700338 Log.e(TAG, "saving semantic cluster: " + rowFeatures);
Ruei-sung Lin83954e82012-08-28 18:00:48 -0700339 }
Ruei-sung Lin5d42ffa2012-08-23 16:01:57 -0700340 }
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700341 }
342
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700343 public String getSemanticLocation() {
Ruei-sung Linc7c9cf12012-08-28 11:35:19 -0700344 String label = LocationStatsAggregator.UNKNOWN_LOCATION;
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700345
Ruei-sung Linc7c9cf12012-08-28 11:35:19 -0700346 // instead of using the last location, try acquiring the latest location.
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700347 if (mLastLocation != null) {
Ruei-sung Lin5d42ffa2012-08-23 16:01:57 -0700348 // TODO: use fast neatest neighbor search speed up location search
Wei Hua9c3a7dc2012-08-28 13:46:31 -0700349 synchronized (mSemanticClusters) {
350 for (SemanticCluster cluster: mSemanticClusters) {
351 if (cluster.distanceToCenter(mLastLocation) < SEMANTIC_CLUSTER_RADIUS) {
352 return cluster.getSemanticId();
353 }
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700354 }
355 }
356 }
357 return label;
358 }
Wei Hua9c3a7dc2012-08-28 13:46:31 -0700359
360 public List<String> getClusterNames() {
361 ArrayList<String> clusters = new ArrayList<String>();
362 synchronized (mSemanticClusters) {
363 for (SemanticCluster cluster: mSemanticClusters) {
364 clusters.add(cluster.getSemanticId());
365 }
366 }
367 return clusters;
368 }
Ruei-sung Linf0f78442012-08-13 19:04:29 -0700369}