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