• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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