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.text.format.DateFormat;
     21 import android.text.format.DateUtils;
     22 
     23 import com.android.gallery3d.common.Utils;
     24 import com.android.gallery3d.util.GalleryUtils;
     25 
     26 import java.util.ArrayList;
     27 import java.util.Collections;
     28 import java.util.Comparator;
     29 
     30 public class TimeClustering extends Clustering {
     31     @SuppressWarnings("unused")
     32     private static final String TAG = "TimeClustering";
     33 
     34     // If 2 items are greater than 25 miles apart, they will be in different
     35     // clusters.
     36     private static final int GEOGRAPHIC_DISTANCE_CUTOFF_IN_MILES = 20;
     37 
     38     // Do not want to split based on anything under 1 min.
     39     private static final long MIN_CLUSTER_SPLIT_TIME_IN_MS = 60000L;
     40 
     41     // Disregard a cluster split time of anything over 2 hours.
     42     private static final long MAX_CLUSTER_SPLIT_TIME_IN_MS = 7200000L;
     43 
     44     // Try and get around 9 clusters (best-effort for the common case).
     45     private static final int NUM_CLUSTERS_TARGETED = 9;
     46 
     47     // Try and merge 2 clusters if they are both smaller than min cluster size.
     48     // The min cluster size can range from 8 to 15.
     49     private static final int MIN_MIN_CLUSTER_SIZE = 8;
     50     private static final int MAX_MIN_CLUSTER_SIZE = 15;
     51 
     52     // Try and split a cluster if it is bigger than max cluster size.
     53     // The max cluster size can range from 20 to 50.
     54     private static final int MIN_MAX_CLUSTER_SIZE = 20;
     55     private static final int MAX_MAX_CLUSTER_SIZE = 50;
     56 
     57     // Initially put 2 items in the same cluster as long as they are within
     58     // 3 cluster frequencies of each other.
     59     private static int CLUSTER_SPLIT_MULTIPLIER = 3;
     60 
     61     // The minimum change factor in the time between items to consider a
     62     // partition.
     63     // Example: (Item 3 - Item 2) / (Item 2 - Item 1).
     64     private static final int MIN_PARTITION_CHANGE_FACTOR = 2;
     65 
     66     // Make the cluster split time of a large cluster half that of a regular
     67     // cluster.
     68     private static final int PARTITION_CLUSTER_SPLIT_TIME_FACTOR = 2;
     69 
     70     private Context mContext;
     71     private ArrayList<Cluster> mClusters;
     72     private String[] mNames;
     73     private Cluster mCurrCluster;
     74 
     75     private long mClusterSplitTime =
     76             (MIN_CLUSTER_SPLIT_TIME_IN_MS + MAX_CLUSTER_SPLIT_TIME_IN_MS) / 2;
     77     private long mLargeClusterSplitTime =
     78             mClusterSplitTime / PARTITION_CLUSTER_SPLIT_TIME_FACTOR;
     79     private int mMinClusterSize = (MIN_MIN_CLUSTER_SIZE + MAX_MIN_CLUSTER_SIZE) / 2;
     80     private int mMaxClusterSize = (MIN_MAX_CLUSTER_SIZE + MAX_MAX_CLUSTER_SIZE) / 2;
     81 
     82 
     83     private static final Comparator<SmallItem> sDateComparator =
     84             new DateComparator();
     85 
     86     private static class DateComparator implements Comparator<SmallItem> {
     87         @Override
     88         public int compare(SmallItem item1, SmallItem item2) {
     89             return -Utils.compare(item1.dateInMs, item2.dateInMs);
     90         }
     91     }
     92 
     93     public TimeClustering(Context context) {
     94         mContext = context;
     95         mClusters = new ArrayList<Cluster>();
     96         mCurrCluster = new Cluster();
     97     }
     98 
     99     @Override
    100     public void run(MediaSet baseSet) {
    101         final int total = baseSet.getTotalMediaItemCount();
    102         final SmallItem[] buf = new SmallItem[total];
    103         final double[] latLng = new double[2];
    104 
    105         baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
    106             @Override
    107             public void consume(int index, MediaItem item) {
    108                 if (index < 0 || index >= total) return;
    109                 SmallItem s = new SmallItem();
    110                 s.path = item.getPath();
    111                 s.dateInMs = item.getDateInMs();
    112                 item.getLatLong(latLng);
    113                 s.lat = latLng[0];
    114                 s.lng = latLng[1];
    115                 buf[index] = s;
    116             }
    117         });
    118 
    119         ArrayList<SmallItem> items = new ArrayList<SmallItem>(total);
    120         for (int i = 0; i < total; i++) {
    121             if (buf[i] != null) {
    122                 items.add(buf[i]);
    123             }
    124         }
    125 
    126         Collections.sort(items, sDateComparator);
    127 
    128         int n = items.size();
    129         long minTime = 0;
    130         long maxTime = 0;
    131         for (int i = 0; i < n; i++) {
    132             long t = items.get(i).dateInMs;
    133             if (t == 0) continue;
    134             if (minTime == 0) {
    135                 minTime = maxTime = t;
    136             } else {
    137                 minTime = Math.min(minTime, t);
    138                 maxTime = Math.max(maxTime, t);
    139             }
    140         }
    141 
    142         setTimeRange(maxTime - minTime, n);
    143 
    144         for (int i = 0; i < n; i++) {
    145             compute(items.get(i));
    146         }
    147 
    148         compute(null);
    149 
    150         int m = mClusters.size();
    151         mNames = new String[m];
    152         for (int i = 0; i < m; i++) {
    153             mNames[i] = mClusters.get(i).generateCaption(mContext);
    154         }
    155     }
    156 
    157     @Override
    158     public int getNumberOfClusters() {
    159         return mClusters.size();
    160     }
    161 
    162     @Override
    163     public ArrayList<Path> getCluster(int index) {
    164         ArrayList<SmallItem> items = mClusters.get(index).getItems();
    165         ArrayList<Path> result = new ArrayList<Path>(items.size());
    166         for (int i = 0, n = items.size(); i < n; i++) {
    167             result.add(items.get(i).path);
    168         }
    169         return result;
    170     }
    171 
    172     @Override
    173     public String getClusterName(int index) {
    174         return mNames[index];
    175     }
    176 
    177     private void setTimeRange(long timeRange, int numItems) {
    178         if (numItems != 0) {
    179             int meanItemsPerCluster = numItems / NUM_CLUSTERS_TARGETED;
    180             // Heuristic to get min and max cluster size - half and double the
    181             // desired items per cluster.
    182             mMinClusterSize = meanItemsPerCluster / 2;
    183             mMaxClusterSize = meanItemsPerCluster * 2;
    184             mClusterSplitTime = timeRange / numItems * CLUSTER_SPLIT_MULTIPLIER;
    185         }
    186         mClusterSplitTime = Utils.clamp(mClusterSplitTime, MIN_CLUSTER_SPLIT_TIME_IN_MS, MAX_CLUSTER_SPLIT_TIME_IN_MS);
    187         mLargeClusterSplitTime = mClusterSplitTime / PARTITION_CLUSTER_SPLIT_TIME_FACTOR;
    188         mMinClusterSize = Utils.clamp(mMinClusterSize, MIN_MIN_CLUSTER_SIZE, MAX_MIN_CLUSTER_SIZE);
    189         mMaxClusterSize = Utils.clamp(mMaxClusterSize, MIN_MAX_CLUSTER_SIZE, MAX_MAX_CLUSTER_SIZE);
    190     }
    191 
    192     private void compute(SmallItem currentItem) {
    193         if (currentItem != null) {
    194             int numClusters = mClusters.size();
    195             int numCurrClusterItems = mCurrCluster.size();
    196             boolean geographicallySeparateItem = false;
    197             boolean itemAddedToCurrentCluster = false;
    198 
    199             // Determine if this item should go in the current cluster or be the
    200             // start of a new cluster.
    201             if (numCurrClusterItems == 0) {
    202                 mCurrCluster.addItem(currentItem);
    203             } else {
    204                 SmallItem prevItem = mCurrCluster.getLastItem();
    205                 if (isGeographicallySeparated(prevItem, currentItem)) {
    206                     mClusters.add(mCurrCluster);
    207                     geographicallySeparateItem = true;
    208                 } else if (numCurrClusterItems > mMaxClusterSize) {
    209                     splitAndAddCurrentCluster();
    210                 } else if (timeDistance(prevItem, currentItem) < mClusterSplitTime) {
    211                     mCurrCluster.addItem(currentItem);
    212                     itemAddedToCurrentCluster = true;
    213                 } else if (numClusters > 0 && numCurrClusterItems < mMinClusterSize
    214                         && !mCurrCluster.mGeographicallySeparatedFromPrevCluster) {
    215                     mergeAndAddCurrentCluster();
    216                 } else {
    217                     mClusters.add(mCurrCluster);
    218                 }
    219 
    220                 // Creating a new cluster and adding the current item to it.
    221                 if (!itemAddedToCurrentCluster) {
    222                     mCurrCluster = new Cluster();
    223                     if (geographicallySeparateItem) {
    224                         mCurrCluster.mGeographicallySeparatedFromPrevCluster = true;
    225                     }
    226                     mCurrCluster.addItem(currentItem);
    227                 }
    228             }
    229         } else {
    230             if (mCurrCluster.size() > 0) {
    231                 int numClusters = mClusters.size();
    232                 int numCurrClusterItems = mCurrCluster.size();
    233 
    234                 // The last cluster may potentially be too big or too small.
    235                 if (numCurrClusterItems > mMaxClusterSize) {
    236                     splitAndAddCurrentCluster();
    237                 } else if (numClusters > 0 && numCurrClusterItems < mMinClusterSize
    238                         && !mCurrCluster.mGeographicallySeparatedFromPrevCluster) {
    239                     mergeAndAddCurrentCluster();
    240                 } else {
    241                     mClusters.add(mCurrCluster);
    242                 }
    243                 mCurrCluster = new Cluster();
    244             }
    245         }
    246     }
    247 
    248     private void splitAndAddCurrentCluster() {
    249         ArrayList<SmallItem> currClusterItems = mCurrCluster.getItems();
    250         int numCurrClusterItems = mCurrCluster.size();
    251         int secondPartitionStartIndex = getPartitionIndexForCurrentCluster();
    252         if (secondPartitionStartIndex != -1) {
    253             Cluster partitionedCluster = new Cluster();
    254             for (int j = 0; j < secondPartitionStartIndex; j++) {
    255                 partitionedCluster.addItem(currClusterItems.get(j));
    256             }
    257             mClusters.add(partitionedCluster);
    258             partitionedCluster = new Cluster();
    259             for (int j = secondPartitionStartIndex; j < numCurrClusterItems; j++) {
    260                 partitionedCluster.addItem(currClusterItems.get(j));
    261             }
    262             mClusters.add(partitionedCluster);
    263         } else {
    264             mClusters.add(mCurrCluster);
    265         }
    266     }
    267 
    268     private int getPartitionIndexForCurrentCluster() {
    269         int partitionIndex = -1;
    270         float largestChange = MIN_PARTITION_CHANGE_FACTOR;
    271         ArrayList<SmallItem> currClusterItems = mCurrCluster.getItems();
    272         int numCurrClusterItems = mCurrCluster.size();
    273         int minClusterSize = mMinClusterSize;
    274 
    275         // Could be slightly more efficient here but this code seems cleaner.
    276         if (numCurrClusterItems > minClusterSize + 1) {
    277             for (int i = minClusterSize; i < numCurrClusterItems - minClusterSize; i++) {
    278                 SmallItem prevItem = currClusterItems.get(i - 1);
    279                 SmallItem currItem = currClusterItems.get(i);
    280                 SmallItem nextItem = currClusterItems.get(i + 1);
    281 
    282                 long timeNext = nextItem.dateInMs;
    283                 long timeCurr = currItem.dateInMs;
    284                 long timePrev = prevItem.dateInMs;
    285 
    286                 if (timeNext == 0 || timeCurr == 0 || timePrev == 0) continue;
    287 
    288                 long diff1 = Math.abs(timeNext - timeCurr);
    289                 long diff2 = Math.abs(timeCurr - timePrev);
    290 
    291                 float change = Math.max(diff1 / (diff2 + 0.01f), diff2 / (diff1 + 0.01f));
    292                 if (change > largestChange) {
    293                     if (timeDistance(currItem, prevItem) > mLargeClusterSplitTime) {
    294                         partitionIndex = i;
    295                         largestChange = change;
    296                     } else if (timeDistance(nextItem, currItem) > mLargeClusterSplitTime) {
    297                         partitionIndex = i + 1;
    298                         largestChange = change;
    299                     }
    300                 }
    301             }
    302         }
    303         return partitionIndex;
    304     }
    305 
    306     private void mergeAndAddCurrentCluster() {
    307         int numClusters = mClusters.size();
    308         Cluster prevCluster = mClusters.get(numClusters - 1);
    309         ArrayList<SmallItem> currClusterItems = mCurrCluster.getItems();
    310         int numCurrClusterItems = mCurrCluster.size();
    311         if (prevCluster.size() < mMinClusterSize) {
    312             for (int i = 0; i < numCurrClusterItems; i++) {
    313                 prevCluster.addItem(currClusterItems.get(i));
    314             }
    315             mClusters.set(numClusters - 1, prevCluster);
    316         } else {
    317             mClusters.add(mCurrCluster);
    318         }
    319     }
    320 
    321     // Returns true if a, b are sufficiently geographically separated.
    322     private static boolean isGeographicallySeparated(SmallItem itemA, SmallItem itemB) {
    323         if (!GalleryUtils.isValidLocation(itemA.lat, itemA.lng)
    324                 || !GalleryUtils.isValidLocation(itemB.lat, itemB.lng)) {
    325             return false;
    326         }
    327 
    328         double distance = GalleryUtils.fastDistanceMeters(
    329             Math.toRadians(itemA.lat),
    330             Math.toRadians(itemA.lng),
    331             Math.toRadians(itemB.lat),
    332             Math.toRadians(itemB.lng));
    333         return (GalleryUtils.toMile(distance) > GEOGRAPHIC_DISTANCE_CUTOFF_IN_MILES);
    334     }
    335 
    336     // Returns the time interval between the two items in milliseconds.
    337     private static long timeDistance(SmallItem a, SmallItem b) {
    338         return Math.abs(a.dateInMs - b.dateInMs);
    339     }
    340 }
    341 
    342 class SmallItem {
    343     Path path;
    344     long dateInMs;
    345     double lat, lng;
    346 }
    347 
    348 class Cluster {
    349     @SuppressWarnings("unused")
    350     private static final String TAG = "Cluster";
    351     private static final String MMDDYY_FORMAT = "MMddyy";
    352 
    353     // This is for TimeClustering only.
    354     public boolean mGeographicallySeparatedFromPrevCluster = false;
    355 
    356     private ArrayList<SmallItem> mItems = new ArrayList<SmallItem>();
    357 
    358     public Cluster() {
    359     }
    360 
    361     public void addItem(SmallItem item) {
    362         mItems.add(item);
    363     }
    364 
    365     public int size() {
    366         return mItems.size();
    367     }
    368 
    369     public SmallItem getLastItem() {
    370         int n = mItems.size();
    371         return (n == 0) ? null : mItems.get(n - 1);
    372     }
    373 
    374     public ArrayList<SmallItem> getItems() {
    375         return mItems;
    376     }
    377 
    378     public String generateCaption(Context context) {
    379         int n = mItems.size();
    380         long minTimestamp = 0;
    381         long maxTimestamp = 0;
    382 
    383         for (int i = 0; i < n; i++) {
    384             long t = mItems.get(i).dateInMs;
    385             if (t == 0) continue;
    386             if (minTimestamp == 0) {
    387                 minTimestamp = maxTimestamp = t;
    388             } else {
    389                 minTimestamp = Math.min(minTimestamp, t);
    390                 maxTimestamp = Math.max(maxTimestamp, t);
    391             }
    392         }
    393         if (minTimestamp == 0) return "";
    394 
    395         String caption;
    396         String minDay = DateFormat.format(MMDDYY_FORMAT, minTimestamp)
    397                 .toString();
    398         String maxDay = DateFormat.format(MMDDYY_FORMAT, maxTimestamp)
    399                 .toString();
    400 
    401         if (minDay.substring(4).equals(maxDay.substring(4))) {
    402             // The items are from the same year - show at least as
    403             // much granularity as abbrev_all allows.
    404             caption = DateUtils.formatDateRange(context, minTimestamp,
    405                     maxTimestamp, DateUtils.FORMAT_ABBREV_ALL);
    406 
    407             // Get a more granular date range string if the min and
    408             // max timestamp are on the same day and from the
    409             // current year.
    410             if (minDay.equals(maxDay)) {
    411                 int flags = DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_SHOW_DATE;
    412                 // Contains the year only if the date does not
    413                 // correspond to the current year.
    414                 String dateRangeWithOptionalYear = DateUtils.formatDateTime(
    415                         context, minTimestamp, flags);
    416                 String dateRangeWithYear = DateUtils.formatDateTime(
    417                         context, minTimestamp, flags | DateUtils.FORMAT_SHOW_YEAR);
    418                 if (!dateRangeWithOptionalYear.equals(dateRangeWithYear)) {
    419                     // This means both dates are from the same year
    420                     // - show the time.
    421                     // Not enough room to display the time range.
    422                     // Pick the mid-point.
    423                     long midTimestamp = (minTimestamp + maxTimestamp) / 2;
    424                     caption = DateUtils.formatDateRange(context, midTimestamp,
    425                             midTimestamp, DateUtils.FORMAT_SHOW_TIME | flags);
    426                 }
    427             }
    428         } else {
    429             // The items are not from the same year - only show
    430             // month and year.
    431             int flags = DateUtils.FORMAT_NO_MONTH_DAY
    432                     | DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_SHOW_DATE;
    433             caption = DateUtils.formatDateRange(context, minTimestamp,
    434                     maxTimestamp, flags);
    435         }
    436 
    437         return caption;
    438     }
    439 }
    440