1 /* 2 * Copyright (C) 2010 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 17 package com.android.gallery3d.data; 18 19 import com.android.gallery3d.R; 20 import com.android.gallery3d.util.ReverseGeocoder; 21 import com.android.gallery3d.util.GalleryUtils; 22 23 import android.content.Context; 24 import android.os.Handler; 25 import android.os.Looper; 26 import android.widget.Toast; 27 28 import java.util.ArrayList; 29 30 class LocationClustering extends Clustering { 31 private static final String TAG = "LocationClustering"; 32 33 private static final int MIN_GROUPS = 1; 34 private static final int MAX_GROUPS = 20; 35 private static final int MAX_ITERATIONS = 30; 36 37 // If the total distance change is less than this ratio, stop iterating. 38 private static final float STOP_CHANGE_RATIO = 0.01f; 39 private Context mContext; 40 private ArrayList<ArrayList<SmallItem>> mClusters; 41 private ArrayList<String> mNames; 42 private String mNoLocationString; 43 private Handler mHandler; 44 45 private static class Point { 46 public Point(double lat, double lng) { 47 latRad = Math.toRadians(lat); 48 lngRad = Math.toRadians(lng); 49 } 50 public Point() {} 51 public double latRad, lngRad; 52 } 53 54 private static class SmallItem { 55 Path path; 56 double lat, lng; 57 } 58 59 public LocationClustering(Context context) { 60 mContext = context; 61 mNoLocationString = mContext.getResources().getString(R.string.no_location); 62 mHandler = new Handler(Looper.getMainLooper()); 63 } 64 65 @Override 66 public void run(MediaSet baseSet) { 67 final int total = baseSet.getTotalMediaItemCount(); 68 final SmallItem[] buf = new SmallItem[total]; 69 // Separate items to two sets: with or without lat-long. 70 final double[] latLong = new double[2]; 71 baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() { 72 public void consume(int index, MediaItem item) { 73 if (index < 0 || index >= total) return; 74 SmallItem s = new SmallItem(); 75 s.path = item.getPath(); 76 item.getLatLong(latLong); 77 s.lat = latLong[0]; 78 s.lng = latLong[1]; 79 buf[index] = s; 80 } 81 }); 82 83 final ArrayList<SmallItem> withLatLong = new ArrayList<SmallItem>(); 84 final ArrayList<SmallItem> withoutLatLong = new ArrayList<SmallItem>(); 85 final ArrayList<Point> points = new ArrayList<Point>(); 86 for (int i = 0; i < total; i++) { 87 SmallItem s = buf[i]; 88 if (s == null) continue; 89 if (GalleryUtils.isValidLocation(s.lat, s.lng)) { 90 withLatLong.add(s); 91 points.add(new Point(s.lat, s.lng)); 92 } else { 93 withoutLatLong.add(s); 94 } 95 } 96 97 ArrayList<ArrayList<SmallItem>> clusters = new ArrayList<ArrayList<SmallItem>>(); 98 99 int m = withLatLong.size(); 100 if (m > 0) { 101 // cluster the items with lat-long 102 Point[] pointsArray = new Point[m]; 103 pointsArray = points.toArray(pointsArray); 104 int[] bestK = new int[1]; 105 int[] index = kMeans(pointsArray, bestK); 106 107 for (int i = 0; i < bestK[0]; i++) { 108 clusters.add(new ArrayList<SmallItem>()); 109 } 110 111 for (int i = 0; i < m; i++) { 112 clusters.get(index[i]).add(withLatLong.get(i)); 113 } 114 } 115 116 ReverseGeocoder geocoder = new ReverseGeocoder(mContext); 117 mNames = new ArrayList<String>(); 118 boolean hasUnresolvedAddress = false; 119 mClusters = new ArrayList<ArrayList<SmallItem>>(); 120 for (ArrayList<SmallItem> cluster : clusters) { 121 String name = generateName(cluster, geocoder); 122 if (name != null) { 123 mNames.add(name); 124 mClusters.add(cluster); 125 } else { 126 // move cluster-i to no location cluster 127 withoutLatLong.addAll(cluster); 128 hasUnresolvedAddress = true; 129 } 130 } 131 132 if (withoutLatLong.size() > 0) { 133 mNames.add(mNoLocationString); 134 mClusters.add(withoutLatLong); 135 } 136 137 if (hasUnresolvedAddress) { 138 mHandler.post(new Runnable() { 139 public void run() { 140 Toast.makeText(mContext, R.string.no_connectivity, 141 Toast.LENGTH_LONG).show(); 142 } 143 }); 144 } 145 } 146 147 private static String generateName(ArrayList<SmallItem> items, 148 ReverseGeocoder geocoder) { 149 ReverseGeocoder.SetLatLong set = new ReverseGeocoder.SetLatLong(); 150 151 int n = items.size(); 152 for (int i = 0; i < n; i++) { 153 SmallItem item = items.get(i); 154 double itemLatitude = item.lat; 155 double itemLongitude = item.lng; 156 157 if (set.mMinLatLatitude > itemLatitude) { 158 set.mMinLatLatitude = itemLatitude; 159 set.mMinLatLongitude = itemLongitude; 160 } 161 if (set.mMaxLatLatitude < itemLatitude) { 162 set.mMaxLatLatitude = itemLatitude; 163 set.mMaxLatLongitude = itemLongitude; 164 } 165 if (set.mMinLonLongitude > itemLongitude) { 166 set.mMinLonLatitude = itemLatitude; 167 set.mMinLonLongitude = itemLongitude; 168 } 169 if (set.mMaxLonLongitude < itemLongitude) { 170 set.mMaxLonLatitude = itemLatitude; 171 set.mMaxLonLongitude = itemLongitude; 172 } 173 } 174 175 return geocoder.computeAddress(set); 176 } 177 178 @Override 179 public int getNumberOfClusters() { 180 return mClusters.size(); 181 } 182 183 @Override 184 public ArrayList<Path> getCluster(int index) { 185 ArrayList<SmallItem> items = mClusters.get(index); 186 ArrayList<Path> result = new ArrayList<Path>(items.size()); 187 for (int i = 0, n = items.size(); i < n; i++) { 188 result.add(items.get(i).path); 189 } 190 return result; 191 } 192 193 @Override 194 public String getClusterName(int index) { 195 return mNames.get(index); 196 } 197 198 // Input: n points 199 // Output: the best k is stored in bestK[0], and the return value is the 200 // an array which specifies the group that each point belongs (0 to k - 1). 201 private static int[] kMeans(Point points[], int[] bestK) { 202 int n = points.length; 203 204 // min and max number of groups wanted 205 int minK = Math.min(n, MIN_GROUPS); 206 int maxK = Math.min(n, MAX_GROUPS); 207 208 Point[] center = new Point[maxK]; // center of each group. 209 Point[] groupSum = new Point[maxK]; // sum of points in each group. 210 int[] groupCount = new int[maxK]; // number of points in each group. 211 int[] grouping = new int[n]; // The group assignment for each point. 212 213 for (int i = 0; i < maxK; i++) { 214 center[i] = new Point(); 215 groupSum[i] = new Point(); 216 } 217 218 // The score we want to minimize is: 219 // (sum of distance from each point to its group center) * sqrt(k). 220 float bestScore = Float.MAX_VALUE; 221 // The best group assignment up to now. 222 int[] bestGrouping = new int[n]; 223 // The best K up to now. 224 bestK[0] = 1; 225 226 float lastDistance = 0; 227 float totalDistance = 0; 228 229 for (int k = minK; k <= maxK; k++) { 230 // step 1: (arbitrarily) pick k points as the initial centers. 231 int delta = n / k; 232 for (int i = 0; i < k; i++) { 233 Point p = points[i * delta]; 234 center[i].latRad = p.latRad; 235 center[i].lngRad = p.lngRad; 236 } 237 238 for (int iter = 0; iter < MAX_ITERATIONS; iter++) { 239 // step 2: assign each point to the nearest center. 240 for (int i = 0; i < k; i++) { 241 groupSum[i].latRad = 0; 242 groupSum[i].lngRad = 0; 243 groupCount[i] = 0; 244 } 245 totalDistance = 0; 246 247 for (int i = 0; i < n; i++) { 248 Point p = points[i]; 249 float bestDistance = Float.MAX_VALUE; 250 int bestIndex = 0; 251 for (int j = 0; j < k; j++) { 252 float distance = (float) GalleryUtils.fastDistanceMeters( 253 p.latRad, p.lngRad, center[j].latRad, center[j].lngRad); 254 // We may have small non-zero distance introduced by 255 // floating point calculation, so zero out small 256 // distances less than 1 meter. 257 if (distance < 1) { 258 distance = 0; 259 } 260 if (distance < bestDistance) { 261 bestDistance = distance; 262 bestIndex = j; 263 } 264 } 265 grouping[i] = bestIndex; 266 groupCount[bestIndex]++; 267 groupSum[bestIndex].latRad += p.latRad; 268 groupSum[bestIndex].lngRad += p.lngRad; 269 totalDistance += bestDistance; 270 } 271 272 // step 3: calculate new centers 273 for (int i = 0; i < k; i++) { 274 if (groupCount[i] > 0) { 275 center[i].latRad = groupSum[i].latRad / groupCount[i]; 276 center[i].lngRad = groupSum[i].lngRad / groupCount[i]; 277 } 278 } 279 280 if (totalDistance == 0 || (Math.abs(lastDistance - totalDistance) 281 / totalDistance) < STOP_CHANGE_RATIO) { 282 break; 283 } 284 lastDistance = totalDistance; 285 } 286 287 // step 4: remove empty groups and reassign group number 288 int reassign[] = new int[k]; 289 int realK = 0; 290 for (int i = 0; i < k; i++) { 291 if (groupCount[i] > 0) { 292 reassign[i] = realK++; 293 } 294 } 295 296 // step 5: calculate the final score 297 float score = totalDistance * (float) Math.sqrt(realK); 298 299 if (score < bestScore) { 300 bestScore = score; 301 bestK[0] = realK; 302 for (int i = 0; i < n; i++) { 303 bestGrouping[i] = reassign[grouping[i]]; 304 } 305 if (score == 0) { 306 break; 307 } 308 } 309 } 310 return bestGrouping; 311 } 312 } 313