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.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 {
     47         public Point(double lat, double lng) {
     48             latRad = Math.toRadians(lat);
     49             lngRad = Math.toRadians(lng);
     50         }
     51         public Point() {}
     52         public double latRad, lngRad;
     53     }
     54 
     55     private static class SmallItem {
     56         Path path;
     57         double lat, lng;
     58     }
     59 
     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
     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 
    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
    182     public int getNumberOfClusters() {
    183         return mClusters.size();
    184     }
    185 
    186     @Override
    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
    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).
    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