@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) {
      mHandler.post(
          new Runnable() {
            public void run() {
              Toast.makeText(mContext, R.string.no_connectivity, Toast.LENGTH_LONG).show();
            }
          });
    }
  }
  // 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;
  }