/* * Copyright (C) 2013 Maciej Górski * * 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 pl.mg6.android.maps.extensions.impl; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import pl.mg6.android.maps.extensions.ClusteringSettings; import pl.mg6.android.maps.extensions.Marker; import pl.mg6.android.maps.extensions.utils.SphericalMercator; import android.support.v4.util.LongSparseArray; import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; import com.google.android.gms.maps.model.VisibleRegion; class GridClusteringStrategy extends BaseClusteringStrategy { private static final boolean DEBUG_GRID = false; private DebugHelper debugHelper; private boolean addMarkersDynamically; private double baseClusterSize; private IGoogleMap map; private Map<DelegatingMarker, ClusterMarker> markers; private double clusterSize; private int zoom; private int[] visibleClusters = new int[4]; private LongSparseArray<ClusterMarker> clusters = new LongSparseArray<ClusterMarker>(); private List<ClusterMarker> cache = new ArrayList<ClusterMarker>(); private ClusterRefresher refresher; public GridClusteringStrategy(ClusteringSettings settings, IGoogleMap map, List<DelegatingMarker> markers, ClusterRefresher refresher) { super(settings, map); this.addMarkersDynamically = settings.isAddMarkersDynamically(); this.baseClusterSize = settings.getClusterSize(); this.map = map; this.markers = new HashMap<DelegatingMarker, ClusterMarker>(); for (DelegatingMarker m : markers) { if (m.isVisible()) { this.markers.put(m, null); } } this.refresher = refresher; this.zoom = Math.round(map.getCameraPosition().zoom); this.clusterSize = calculateClusterSize(zoom); recalculate(); } @Override public void cleanup() { for (int i = 0; i < clusters.size(); i++) { ClusterMarker cluster = clusters.valueAt(i); cluster.cleanup(); } clusters.clear(); markers.clear(); refresher.cleanup(); if (DEBUG_GRID) { if (debugHelper != null) { debugHelper.cleanup(); } } super.cleanup(); } @Override public void onCameraChange(CameraPosition cameraPosition) { zoom = Math.round(cameraPosition.zoom); double clusterSize = calculateClusterSize(zoom); if (this.clusterSize != clusterSize) { this.clusterSize = clusterSize; recalculate(); } else if (addMarkersDynamically) { addMarkersInVisibleRegion(); } if (DEBUG_GRID) { if (debugHelper == null) { debugHelper = new DebugHelper(); } debugHelper.drawDebugGrid(map, clusterSize); } } @Override public void onAdd(DelegatingMarker marker) { if (!marker.isVisible()) { return; } addMarker(marker); } private void addMarker(DelegatingMarker marker) { LatLng position = marker.getPosition(); long clusterId = calculateClusterId(position); ClusterMarker cluster = findClusterById(clusterId); cluster.add(marker); markers.put(marker, cluster); if (!addMarkersDynamically || isPositionInVisibleClusters(position)) { refresh(cluster); } } private boolean isPositionInVisibleClusters(LatLng position) { int y = convLat(position.latitude); int x = convLng(position.longitude); int[] b = visibleClusters; return b[0] <= y && y <= b[2] && (b[1] <= x && x <= b[3] || b[1] > b[3] && (b[1] <= x || x <= b[3])); } @Override public void onRemove(DelegatingMarker marker) { if (!marker.isVisible()) { return; } removeMarker(marker); } private void removeMarker(DelegatingMarker marker) { ClusterMarker cluster = markers.remove(marker); if (cluster != null) { cluster.remove(marker); refresh(cluster); } } @Override public void onPositionChange(DelegatingMarker marker) { if (!marker.isVisible()) { return; } ClusterMarker oldCluster = markers.get(marker); if (oldCluster != null && isMarkerInCluster(marker, oldCluster)) { refresh(oldCluster); } else { if (oldCluster != null) { oldCluster.remove(marker); refresh(oldCluster); } addMarker(marker); } } @Override public Marker map(com.google.android.gms.maps.model.Marker original) { for (int i = 0; i < clusters.size(); i++) { ClusterMarker cluster = clusters.valueAt(i); if (original.equals(cluster.getVirtual())) { return cluster; } } return null; } @Override public List<Marker> getDisplayedMarkers() { List<Marker> displayedMarkers = new ArrayList<Marker>(); for (int i = 0; i < clusters.size(); i++) { ClusterMarker cluster = clusters.valueAt(i); Marker displayedMarker = cluster.getDisplayedMarker(); if (displayedMarker != null) { displayedMarkers.add(displayedMarker); } } return displayedMarkers; } @Override public float getMinZoomLevelNotClustered(Marker marker) { if (!markers.containsKey(marker)) { throw new UnsupportedOperationException("marker is not visible or is a cluster"); } int zoom = 0; while (zoom <= 25 && hasCollision(marker, zoom)) { zoom++; } if (zoom > 25) { return Float.POSITIVE_INFINITY; } return zoom; } private boolean hasCollision(Marker marker, int zoom) { double clusterSize = calculateClusterSize(zoom); LatLng position = marker.getPosition(); int x = (int) (SphericalMercator.scaleLongitude(position.longitude) / clusterSize); int y = (int) (SphericalMercator.scaleLatitude(position.latitude) / clusterSize); for (DelegatingMarker m : markers.keySet()) { if (m.equals(marker)) { continue; } LatLng mPosition = m.getPosition(); int mX = (int) (SphericalMercator.scaleLongitude(mPosition.longitude) / clusterSize); if (x != mX) { continue; } int mY = (int) (SphericalMercator.scaleLatitude(mPosition.latitude) / clusterSize); if (y == mY) { return true; } } return false; } private boolean isMarkerInCluster(DelegatingMarker marker, ClusterMarker cluster) { long clusterId = cluster.getClusterId(); long markerClusterId = calculateClusterId(marker.getPosition()); return clusterId == markerClusterId; } private ClusterMarker findClusterById(long clusterId) { ClusterMarker cluster = clusters.get(clusterId); if (cluster == null) { if (cache.size() > 0) { cluster = cache.remove(cache.size() - 1); } else { cluster = new ClusterMarker(this); } cluster.setClusterId(clusterId); clusters.put(clusterId, cluster); } return cluster; } @Override public void onVisibilityChangeRequest(DelegatingMarker marker, boolean visible) { if (visible) { addMarker(marker); } else { removeMarker(marker); marker.changeVisible(false); } } private void refresh(ClusterMarker cluster) { refresher.refresh(cluster); } private void recalculate() { for (int i = 0; i < clusters.size(); i++) { ClusterMarker cluster = clusters.valueAt(i); cluster.reset(); cache.add(cluster); } clusters.clear(); if (addMarkersDynamically) { calculateVisibleClusters(); } for (DelegatingMarker marker : markers.keySet()) { addMarker(marker); } refresher.refreshAll(); } private void addMarkersInVisibleRegion() { calculateVisibleClusters(); for (DelegatingMarker marker : markers.keySet()) { LatLng position = marker.getPosition(); if (isPositionInVisibleClusters(position)) { ClusterMarker cluster = markers.get(marker); refresh(cluster); } } refresher.refreshAll(); } private void calculateVisibleClusters() { IProjection projection = map.getProjection(); VisibleRegion visibleRegion = projection.getVisibleRegion(); LatLngBounds bounds = visibleRegion.latLngBounds; visibleClusters[0] = convLat(bounds.southwest.latitude); visibleClusters[1] = convLng(bounds.southwest.longitude); visibleClusters[2] = convLat(bounds.northeast.latitude); visibleClusters[3] = convLng(bounds.northeast.longitude); } private long calculateClusterId(LatLng position) { long y = convLat(position.latitude); long x = convLng(position.longitude); long ret = (y << 32) + x; return ret; } private int convLat(double lat) { return (int) (SphericalMercator.scaleLatitude(lat) / clusterSize); } private int convLng(double lng) { return (int) (SphericalMercator.scaleLongitude(lng) / clusterSize); } private double calculateClusterSize(int zoom) { return baseClusterSize / (1 << zoom); } }