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