4

I have a set of points coming from a webservice that need to be displayed on a map.

I have a current solution working nicely for most cases, using the well-known LatLngBounds.Builder, CameraUpdateFactory.newLatLngBounds and map.animateCamera.

I have some cases which give problems though: when the points are too far away, the map centers on max zoom level on the barycenter of those points. For example: I have 10 points in France and 2 points in Hawai. Maps centers more or less on the caribeans at min zoom level. Hence on screen I got nothing shown, the user has to scroll to actually see something is there.

So my question is:

is there a way to get the map to zoom out far enough so that I can see all points (that would be prefered)

Or: which would be the best way to filter out those cases where just a few points are very far away from the majority and pick a set of point to zoom on (in my example, I would choose to zoom on the 10 points in France and forget about the ones in Hawai).

Vincent Mimoun-Prat
  • 28,208
  • 16
  • 81
  • 124

4 Answers4

5

Put all the LatLng of the markers in the list and pass them to this method and at the last line in the newLatLngBounds(bounds, 50)) the 50 represent the padding between the map edge and the most outer marker in each side

public void centerIncidentRouteOnMap(List<LatLng> copiedPoints) {
        double minLat = Integer.MAX_VALUE;
        double maxLat = Integer.MIN_VALUE;
        double minLon = Integer.MAX_VALUE;
        double maxLon = Integer.MIN_VALUE;
        for (LatLng point : copiedPoints) {
            maxLat = Math.max(point.latitude, maxLat);
            minLat = Math.min(point.latitude, minLat);
            maxLon = Math.max(point.longitude, maxLon);
            minLon = Math.min(point.longitude, minLon);
        }
        final LatLngBounds bounds = new LatLngBounds.Builder().include(new LatLng(maxLat, maxLon)).include(new LatLng(minLat, minLon)).build();
        mapFragment.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 50));
    }
Omar HossamEldin
  • 3,033
  • 1
  • 25
  • 51
  • If he got problem with this, so please let him supply me with some LatLng points which reproduce the problem but i can't reproduce it. – Omar HossamEldin Sep 30 '13 at 23:41
  • Here are 4 points: Paris, Lyon, Marseille (in France) and Honolulu (Hawai). Now, I did what you mentionned, expect the map will center on mexico at min zoom level. So none of the markers will be visible. Instead, I'd like to ignore Honolulu and compute the bounds just on France. – Vincent Mimoun-Prat Oct 01 '13 at 11:58
  • @3amoura was not to be rude or anything, just wanted to let you know and it seems I was right – cYrixmorten Oct 01 '13 at 13:16
  • @MarvinLabs sorry i didn't understand your question at first i mean the part that you want to ignore if there are a long distant marker like "Honolulu" in your example – Omar HossamEldin Oct 01 '13 at 13:58
  • @cYrixmorten there is nothing my friend ;) it is ok, after understanding the question i suppose that your answer should work – Omar HossamEldin Oct 01 '13 at 14:00
  • @3amoura great :) hope so, atleast it works for my purpos but of course an error can have sneaked in after rewriting the code – cYrixmorten Oct 01 '13 at 14:21
4

Spotted an error in my previous code and decided to sit down and rewrite it.

I have done something similar before where I had ~4500 markers and wanted to select those within a certain distance of a specific location. Took that code and generalized it to be used with any kind of Markers.

All markersSelected markers with getSurroundingMarkers

The code that I will post below contains two methods that you can use:

selectLowDistanceMarkers

Measures distance between each and every marker and selects only those that does not have a long distance to any of the other markers. This requires a O(n+n^2) runtime due to the comparison between every marker and an examination afterwards.

getSurroundingMarkers

If you already know a postition that you would like to zoom in to, then this method does the same as above. This method is way less CPU heavy as it only has to do a O(n) run through all the markers and compare them to the given position.

private List<Marker> selectLowDistanceMarkers(List<Marker> markers,
        int maxDistanceMeters) {

    List<Marker> acceptedMarkers = new ArrayList<Marker>();

    if (markers == null) return acceptedMarkers;

    Map<Marker, Float> longestDist = new HashMap<Marker, Float>();

    for (Marker marker1 : markers) {

        // in this for loop we remember the max distance for each marker
        // think of a map with a flight company's routes from an airport
        // these lines is drawn for each airport
        // marker1 being the airport and marker2 destinations

        for (Marker marker2 : markers) {
            if (!marker1.equals(marker2)) {
                float distance = distBetween(marker1.getPosition(),
                        marker2.getPosition());
                if (longestDist.containsKey(marker1)) {
                    // possible we have a longer distance
                    if (distance > longestDist.get(marker1))
                        longestDist.put(marker1, distance);
                } else {
                    // first distance
                    longestDist.put(marker1, distance);
                }
            }
        }
    }


    // examine the distances collected
    for (Marker marker: longestDist.keySet()) {
        if (longestDist.get(marker) <= maxDistanceMeters) acceptedMarkers.add(marker);
    }

    return acceptedMarkers;
}

private List<Marker> getSurroundingMarkers(List<Marker> markers,
        LatLng origin, int maxDistanceMeters) {
    List<Marker> surroundingMarkers = surroundingMarkers = new ArrayList<Marker>();
    if (markers == null) return surroundingMarkers ;


        for (Marker marker : markers) {

            double dist = distBetween(origin, marker.getPosition());

            if (dist < getHydrantsLoadradius()) {
                surroundingMarkers.add(marker);
            }
        }


    return surroundingMarkers;
}

private float distBetween(LatLng pos1, LatLng pos2) {
    return distBetween(pos1.latitude, pos1.longitude, pos2.latitude,
            pos2.longitude);
}

/** distance in meters **/
private float distBetween(double lat1, double lng1, double lat2, double lng2) {
    double earthRadius = 3958.75;
    double dLat = Math.toRadians(lat2 - lat1);
    double dLng = Math.toRadians(lng2 - lng1);
    double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
            + Math.cos(Math.toRadians(lat1))
            * Math.cos(Math.toRadians(lat2)) * Math.sin(dLng / 2)
            * Math.sin(dLng / 2);
    double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    double dist = earthRadius * c;

    int meterConversion = 1609;

    return (float) (dist * meterConversion);
}

Again, use the well known LatLngBounds to determine how much you need to zoom after using one of the filtering algorithms above.

cYrixmorten
  • 7,110
  • 3
  • 25
  • 33
  • Will try and get back to you, thanks for the input :) – Vincent Mimoun-Prat Oct 01 '13 at 12:00
  • Cool :) just added some more text to explain my idea – cYrixmorten Oct 01 '13 at 12:28
  • Just spotted an error myself, stand by till I have corrected it – cYrixmorten Oct 01 '13 at 12:31
  • Done, report back if it causes troubles – cYrixmorten Oct 01 '13 at 13:05
  • I finally used my own algorithm, but yours gave me some ideas. As an optimisation to your algorithm, you don't have to compare all markers with all markers, your loop can be as in my algorithm, that lowers the complexity of the algorithm. Thanks. – Vincent Mimoun-Prat Oct 01 '13 at 19:58
  • I have not used the costly one myself as I know where I want to zoom in at. Seems like you found a good solution, glad I could help :) found this today, which also seems promising in some situations: http://stackoverflow.com/questions/18450081/add-markers-dynamically-on-google-maps-v2-for-android – cYrixmorten Oct 01 '13 at 20:12
0

Based on some ideas from cYrixmorten, I have simplified the problem because I know the map can accomodate at least 4000km of surface. So here is the function to build the list of ignored webcams (then I simply ignore that webcam for the camera bounds computation but still add the marker so that it is on the map if the user moves).

private List<Webcam> buildIgnoredWebcamsList(List<Webcam> webcams) {
    if (webcams == null || webcams.size() < 2) return Lists.newArrayList();

    int webcamCount = webcams.size();

    // Number of conflicts (distance > 4000 km) for the camera at index #
    float averageConflictCount = 0;
    int[] conflictCount = new int[webcamCount];
    Arrays.fill(conflictCount, 0);

    // Find number of conflicts between camera pairs
    float[] distance = new float[1];

    for (int i = 0; i < webcamCount - 1; ++i) {
        Webcam a = webcams.get(i);

                    // We don't have to start from 0, compare a and b only once
        for (int j = i + 1; j < webcamCount; ++j) {
            Webcam b = webcams.get(j);
            Location.distanceBetween(a.getLatitude(), a.getLongitude(), b.getLatitude(), b.getLongitude(), distance);

            // We have a conflict between a and b if they are more than 4000km away
            if (distance[0] > 4000 * 1000) {
                conflictCount[i] += 1;
                conflictCount[j] += 1;
                averageConflictCount += 2;
            }
        }
    }
    averageConflictCount /= webcamCount;

    // Exclude all webcams with a number of conflicts greater than the average
    List<Webcam> ignoredCamerasForBounds = Lists.newArrayList();

    for (int i = 0; i < webcamCount; ++i) {
        if (conflictCount[i] > averageConflictCount) {
            ignoredCamerasForBounds.add(webcams.get(i));
        }
    }

    return ignoredCamerasForBounds;
}
Vincent Mimoun-Prat
  • 28,208
  • 16
  • 81
  • 124
  • Discovered today: Untested, though seems Android Maps Extensions can do all sorts of clever stuff. One of them being to define clusters based on the distance between markers and load markers dynamically. If it is as easy as it looks then it is suddenly a no-brainer to select the biggest cluster and zoom in on that. – cYrixmorten Oct 01 '13 at 20:20
  • I already use clusterkraf, good library, but does not solve the problem as their algorithm computes clusters based on pixel distance. Ideally, it is true that we would basically need to compute clusters and only zoom on the most populated one. My solution does the trick for now and is pretty inexpensive, I'll do better later if needed. – Vincent Mimoun-Prat Oct 01 '13 at 21:10
  • Yeah, I will stick to mine too, the getSurroundingMarkers that is.. It was also just to make you aware of it, as I was not until today. Btw nice idea with the double forloop, that makes it only, what O(n+nlog(n)), anyway, much better :) – cYrixmorten Oct 01 '13 at 21:15
0
Display display = getWindowManager().getDefaultDisplay(); 
        Point size = new Point();
        display.getSize(size);
        int ancho = size.x;
        int alto =size.y;
List<LatLng> copiedPoints = new ArrayList<LatLng>();
        copiedPoints.add(origin);
        copiedPoints.add(dest);

centerIncidentRouteOnMap(copiedPoints, ancho, alto);

....

public void centerIncidentRouteOnMap(List<LatLng> copiedPoints, int ancho, int alto) {
    double minLat = Integer.MAX_VALUE;
    double maxLat = Integer.MIN_VALUE;
    double minLon = Integer.MAX_VALUE;
    double maxLon = Integer.MIN_VALUE;
    for (LatLng point : copiedPoints) {
        maxLat = Math.max(point.latitude, maxLat);
        minLat = Math.min(point.latitude, minLat);
        maxLon = Math.max(point.longitude, maxLon);
        minLon = Math.min(point.longitude, minLon);
    }
    final LatLngBounds bounds = new LatLngBounds.Builder().include(new LatLng(maxLat, maxLon)).include(new LatLng(minLat, minLon)).build();
    map.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds,ancho, alto, 50));
}