diff options
Diffstat (limited to 'src/com/android/gallery3d/data/LocationClustering.java')
-rw-r--r-- | src/com/android/gallery3d/data/LocationClustering.java | 304 |
1 files changed, 304 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/data/LocationClustering.java b/src/com/android/gallery3d/data/LocationClustering.java new file mode 100644 index 000000000..3cb1399e5 --- /dev/null +++ b/src/com/android/gallery3d/data/LocationClustering.java @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.data; + +import com.android.gallery3d.R; +import com.android.gallery3d.util.ReverseGeocoder; +import com.android.gallery3d.util.GalleryUtils; + +import android.content.Context; +import android.widget.Toast; + +import java.util.ArrayList; + +class LocationClustering extends Clustering { + private static final String TAG = "LocationClustering"; + + private static final int MIN_GROUPS = 1; + private static final int MAX_GROUPS = 20; + private static final int MAX_ITERATIONS = 30; + + // If the total distance change is less than this ratio, stop iterating. + private static final float STOP_CHANGE_RATIO = 0.01f; + private Context mContext; + private ArrayList<ArrayList<SmallItem>> mClusters; + private ArrayList<String> mNames; + private String mNoLocationString; + + private static class Point { + public Point(double lat, double lng) { + latRad = Math.toRadians(lat); + lngRad = Math.toRadians(lng); + } + public Point() {} + public double latRad, lngRad; + } + + private static class SmallItem { + Path path; + double lat, lng; + } + + public LocationClustering(Context context) { + mContext = context; + mNoLocationString = mContext.getResources().getString(R.string.no_location); + } + + @Override + public void run(MediaSet baseSet) { + final int total = baseSet.getTotalMediaItemCount(); + final SmallItem[] buf = new SmallItem[total]; + // Separate items to two sets: with or without lat-long. + final double[] latLong = new double[2]; + baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() { + public void consume(int index, MediaItem item) { + if (index < 0 || index >= total) return; + SmallItem s = new SmallItem(); + s.path = item.getPath(); + item.getLatLong(latLong); + s.lat = latLong[0]; + s.lng = latLong[1]; + buf[index] = s; + } + }); + + final ArrayList<SmallItem> withLatLong = new ArrayList<SmallItem>(); + final ArrayList<SmallItem> withoutLatLong = new ArrayList<SmallItem>(); + final ArrayList<Point> points = new ArrayList<Point>(); + for (int i = 0; i < total; i++) { + SmallItem s = buf[i]; + if (s == null) continue; + if (GalleryUtils.isValidLocation(s.lat, s.lng)) { + withLatLong.add(s); + points.add(new Point(s.lat, s.lng)); + } else { + withoutLatLong.add(s); + } + } + + ArrayList<ArrayList<SmallItem>> clusters = new ArrayList<ArrayList<SmallItem>>(); + + int m = withLatLong.size(); + if (m > 0) { + // cluster the items with lat-long + Point[] pointsArray = new Point[m]; + pointsArray = points.toArray(pointsArray); + int[] bestK = new int[1]; + int[] index = kMeans(pointsArray, bestK); + + for (int i = 0; i < bestK[0]; i++) { + clusters.add(new ArrayList<SmallItem>()); + } + + for (int i = 0; i < m; i++) { + clusters.get(index[i]).add(withLatLong.get(i)); + } + } + + ReverseGeocoder geocoder = new ReverseGeocoder(mContext); + mNames = new ArrayList<String>(); + boolean hasUnresolvedAddress = false; + mClusters = new ArrayList<ArrayList<SmallItem>>(); + for (ArrayList<SmallItem> cluster : clusters) { + String name = generateName(cluster, geocoder); + if (name != null) { + mNames.add(name); + mClusters.add(cluster); + } else { + // move cluster-i to no location cluster + withoutLatLong.addAll(cluster); + hasUnresolvedAddress = true; + } + } + + if (withoutLatLong.size() > 0) { + mNames.add(mNoLocationString); + mClusters.add(withoutLatLong); + } + + if (hasUnresolvedAddress) { + Toast.makeText(mContext, R.string.no_connectivity, + Toast.LENGTH_LONG).show(); + } + } + + private static String generateName(ArrayList<SmallItem> items, + ReverseGeocoder geocoder) { + ReverseGeocoder.SetLatLong set = new ReverseGeocoder.SetLatLong(); + + int n = items.size(); + for (int i = 0; i < n; i++) { + SmallItem item = items.get(i); + double itemLatitude = item.lat; + double itemLongitude = item.lng; + + if (set.mMinLatLatitude > itemLatitude) { + set.mMinLatLatitude = itemLatitude; + set.mMinLatLongitude = itemLongitude; + } + if (set.mMaxLatLatitude < itemLatitude) { + set.mMaxLatLatitude = itemLatitude; + set.mMaxLatLongitude = itemLongitude; + } + if (set.mMinLonLongitude > itemLongitude) { + set.mMinLonLatitude = itemLatitude; + set.mMinLonLongitude = itemLongitude; + } + if (set.mMaxLonLongitude < itemLongitude) { + set.mMaxLonLatitude = itemLatitude; + set.mMaxLonLongitude = itemLongitude; + } + } + + return geocoder.computeAddress(set); + } + + @Override + public int getNumberOfClusters() { + return mClusters.size(); + } + + @Override + public ArrayList<Path> getCluster(int index) { + ArrayList<SmallItem> items = mClusters.get(index); + ArrayList<Path> result = new ArrayList<Path>(items.size()); + for (int i = 0, n = items.size(); i < n; i++) { + result.add(items.get(i).path); + } + return result; + } + + @Override + public String getClusterName(int index) { + return mNames.get(index); + } + + // Input: n points + // Output: the best k is stored in bestK[0], and the return value is the + // an array which specifies the group that each point belongs (0 to k - 1). + private static int[] kMeans(Point points[], int[] bestK) { + int n = points.length; + + // min and max number of groups wanted + int minK = Math.min(n, MIN_GROUPS); + int maxK = Math.min(n, MAX_GROUPS); + + Point[] center = new Point[maxK]; // center of each group. + Point[] groupSum = new Point[maxK]; // sum of points in each group. + int[] groupCount = new int[maxK]; // number of points in each group. + int[] grouping = new int[n]; // The group assignment for each point. + + for (int i = 0; i < maxK; i++) { + center[i] = new Point(); + groupSum[i] = new Point(); + } + + // The score we want to minimize is: + // (sum of distance from each point to its group center) * sqrt(k). + float bestScore = Float.MAX_VALUE; + // The best group assignment up to now. + int[] bestGrouping = new int[n]; + // The best K up to now. + bestK[0] = 1; + + float lastDistance = 0; + float totalDistance = 0; + + for (int k = minK; k <= maxK; k++) { + // step 1: (arbitrarily) pick k points as the initial centers. + int delta = n / k; + for (int i = 0; i < k; i++) { + Point p = points[i * delta]; + center[i].latRad = p.latRad; + center[i].lngRad = p.lngRad; + } + + for (int iter = 0; iter < MAX_ITERATIONS; iter++) { + // step 2: assign each point to the nearest center. + for (int i = 0; i < k; i++) { + groupSum[i].latRad = 0; + groupSum[i].lngRad = 0; + groupCount[i] = 0; + } + totalDistance = 0; + + for (int i = 0; i < n; i++) { + Point p = points[i]; + float bestDistance = Float.MAX_VALUE; + int bestIndex = 0; + for (int j = 0; j < k; j++) { + float distance = (float) GalleryUtils.fastDistanceMeters( + p.latRad, p.lngRad, center[j].latRad, center[j].lngRad); + // We may have small non-zero distance introduced by + // floating point calculation, so zero out small + // distances less than 1 meter. + if (distance < 1) { + distance = 0; + } + if (distance < bestDistance) { + bestDistance = distance; + bestIndex = j; + } + } + grouping[i] = bestIndex; + groupCount[bestIndex]++; + groupSum[bestIndex].latRad += p.latRad; + groupSum[bestIndex].lngRad += p.lngRad; + totalDistance += bestDistance; + } + + // step 3: calculate new centers + for (int i = 0; i < k; i++) { + if (groupCount[i] > 0) { + center[i].latRad = groupSum[i].latRad / groupCount[i]; + center[i].lngRad = groupSum[i].lngRad / groupCount[i]; + } + } + + if (totalDistance == 0 || (Math.abs(lastDistance - totalDistance) + / totalDistance) < STOP_CHANGE_RATIO) { + break; + } + lastDistance = totalDistance; + } + + // step 4: remove empty groups and reassign group number + int reassign[] = new int[k]; + int realK = 0; + for (int i = 0; i < k; i++) { + if (groupCount[i] > 0) { + reassign[i] = realK++; + } + } + + // step 5: calculate the final score + float score = totalDistance * (float) Math.sqrt(realK); + + if (score < bestScore) { + bestScore = score; + bestK[0] = realK; + for (int i = 0; i < n; i++) { + bestGrouping[i] = reassign[grouping[i]]; + } + if (score == 0) { + break; + } + } + } + return bestGrouping; + } +} |