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 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