/* * 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.ClusterOptions; import pl.mg6.android.maps.extensions.ClusterOptionsProvider; import pl.mg6.android.maps.extensions.ClusteringSettings; import pl.mg6.android.maps.extensions.ClusteringSettings.IconDataProvider; import pl.mg6.android.maps.extensions.Marker; import pl.mg6.android.maps.extensions.utils.SphericalMercator; 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.MarkerOptions; import com.google.android.gms.maps.model.VisibleRegion; class GridClusteringStrategy implements ClusteringStrategy { private static final boolean DEBUG_GRID = false; private DebugHelper debugHelper; private final MarkerOptions markerOptions = new MarkerOptions(); private boolean addMarkersDynamically; private double baseClusterSize; private IGoogleMap map; private Map<DelegatingMarker, ClusterMarker> markers; private double clusterSize; private int oldZoom, zoom; private int[] visibleClusters = new int[4]; private Map<ClusterKey, ClusterMarker> clusters = new HashMap<ClusterKey, ClusterMarker>(); private ClusterRefresher refresher; private ClusterOptionsProvider clusterOptionsProvider; private IconDataProvider iconDataProvider; public GridClusteringStrategy(ClusteringSettings settings, IGoogleMap map, List<DelegatingMarker> markers, ClusterRefresher refresher) { this.clusterOptionsProvider = settings.getClusterOptionsProvider(); this.iconDataProvider = settings.getIconDataProvider(); this.addMarkersDynamically = settings.isAddMarkersDynamically(); this.baseClusterSize = settings.getClusterSize(); this.map = map; this.markers = new HashMap<DelegatingMarker, ClusterMarker>(); this.refresher = refresher; this.zoom = Math.round(map.getCameraPosition().zoom); this.clusterSize = calculateClusterSize(zoom); addVisibleMarkers(markers); } @Override public void cleanup() { for (ClusterMarker cluster : clusters.values()) { cluster.cleanup(); } clusters.clear(); markers.clear(); refresher.cleanup(); if (DEBUG_GRID) { if (debugHelper != null) { debugHelper.cleanup(); } } } @Override public void onCameraChange(CameraPosition cameraPosition) { oldZoom = zoom; 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 onClusterGroupChange(DelegatingMarker marker) { if (!marker.isVisible()) { return; } ClusterMarker oldCluster = markers.get(marker); if (oldCluster != null) { oldCluster.remove(marker); refresh(oldCluster); } addMarker(marker); } @Override public void onAdd(DelegatingMarker marker) { if (!marker.isVisible()) { return; } addMarker(marker); } private void addMarker(DelegatingMarker marker) { int clusterGroup = marker.getClusterGroup(); if (clusterGroup < 0) { markers.put(marker, null); marker.changeVisible(true); } else { LatLng position = marker.getPosition(); ClusterKey key = calculateClusterKey(clusterGroup, position); ClusterMarker cluster = findClusterById(key); 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) { oldCluster.remove(marker); refresh(oldCluster); } addMarker(marker); } @Override public Marker map(com.google.android.gms.maps.model.Marker original) { for (ClusterMarker cluster : clusters.values()) { if (original.equals(cluster.getVirtual())) { return cluster; } } return null; } @Override public List<Marker> getDisplayedMarkers() { List<Marker> displayedMarkers = new ArrayList<Marker>(); for (ClusterMarker cluster : clusters.values()) { Marker displayedMarker = cluster.getDisplayedMarker(); if (displayedMarker != null) { displayedMarkers.add(displayedMarker); } } for (DelegatingMarker marker : markers.keySet()) { if (markers.get(marker) == null) { displayedMarkers.add(marker); } } 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 ClusterMarker findClusterById(ClusterKey key) { ClusterMarker cluster = clusters.get(key); if (cluster == null) { cluster = new ClusterMarker(this); clusters.put(key, cluster); } return cluster; } @Override public void onVisibilityChangeRequest(DelegatingMarker marker, boolean visible) { if (visible) { addMarker(marker); } else { removeMarker(marker); marker.changeVisible(false); } } @Override public void onShowInfoWindow(DelegatingMarker marker) { if (!marker.isVisible()) { return; } ClusterMarker cluster = markers.get(marker); if (cluster == null) { marker.forceShowInfoWindow(); } else if (cluster.getMarkersInternal().size() == 1) { cluster.refresh(); marker.forceShowInfoWindow(); } } private void refresh(ClusterMarker cluster) { if (cluster != null) { refresher.refresh(cluster); } } private void addVisibleMarkers(List<DelegatingMarker> markers) { if (addMarkersDynamically) { calculateVisibleClusters(); } for (DelegatingMarker marker : markers) { if (marker.isVisible()) { addMarker(marker); } } refresher.refreshAll(); } private void recalculate() { if (addMarkersDynamically) { calculateVisibleClusters(); } if (zoomedIn()) { splitClusters(); } else { joinClusters(); } refresher.refreshAll(); } private boolean zoomedIn() { return zoom > oldZoom; } private void splitClusters() { Map<ClusterKey, ClusterMarker> newClusters = new HashMap<ClusterKey, ClusterMarker>(); for (ClusterMarker cluster : clusters.values()) { List<DelegatingMarker> ms = cluster.getMarkersInternal(); if (ms.isEmpty()) { cluster.removeVirtual(); continue; } ClusterKey[] clusterIds = new ClusterKey[ms.size()]; boolean allSame = true; for (int j = 0; j < ms.size(); j++) { clusterIds[j] = calculateClusterKey(ms.get(j).getClusterGroup(), ms.get(j).getPosition()); if (!clusterIds[j].equals(clusterIds[0])) { allSame = false; } } if (allSame) { newClusters.put(clusterIds[0], cluster); if (addMarkersDynamically && isPositionInVisibleClusters(cluster.getMarkersInternal().get(0).getPosition())) { refresh(cluster); } } else { cluster.removeVirtual(); for (int j = 0; j < ms.size(); j++) { cluster = newClusters.get(clusterIds[j]); if (cluster == null) { cluster = new ClusterMarker(this); newClusters.put(clusterIds[j], cluster); if (!addMarkersDynamically || isPositionInVisibleClusters(ms.get(j).getPosition())) { refresh(cluster); } } cluster.add(ms.get(j)); markers.put(ms.get(j), cluster); } } } clusters = newClusters; } private void joinClusters() { Map<ClusterKey, ClusterMarker> newClusters = new HashMap<ClusterKey, ClusterMarker>(); Map<ClusterKey, List<ClusterMarker>> oldClusters = new HashMap<ClusterKey, List<ClusterMarker>>(); for (ClusterMarker cluster : clusters.values()) { List<DelegatingMarker> ms = cluster.getMarkersInternal(); if (ms.isEmpty()) { cluster.removeVirtual(); continue; } ClusterKey clusterId = calculateClusterKey(ms.get(0).getClusterGroup(), ms.get(0).getPosition()); List<ClusterMarker> clusterList = oldClusters.get(clusterId); if (clusterList == null) { clusterList = new ArrayList<ClusterMarker>(); oldClusters.put(clusterId, clusterList); } clusterList.add(cluster); } for (ClusterKey key : oldClusters.keySet()) { List<ClusterMarker> clusterList = oldClusters.get(key); if (clusterList.size() == 1) { ClusterMarker cluster = clusterList.get(0); newClusters.put(key, cluster); if (addMarkersDynamically && isPositionInVisibleClusters(cluster.getMarkersInternal().get(0).getPosition())) { refresh(cluster); } } else { ClusterMarker cluster = new ClusterMarker(this); newClusters.put(key, cluster); if (!addMarkersDynamically || isPositionInVisibleClusters(clusterList.get(0).getMarkersInternal().get(0).getPosition())) { refresh(cluster); } for (ClusterMarker old : clusterList) { old.removeVirtual(); List<DelegatingMarker> ms = old.getMarkersInternal(); for (DelegatingMarker m : ms) { cluster.add(m); markers.put(m, cluster); } } } } clusters = newClusters; } 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 ClusterKey calculateClusterKey(int group, LatLng position) { int y = convLat(position.latitude); int x = convLng(position.longitude); return new ClusterKey(group, y, x); } 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); } com.google.android.gms.maps.model.Marker createMarker(List<Marker> markers, LatLng position) { markerOptions.position(position); if (clusterOptionsProvider != null) { ClusterOptions opts = clusterOptionsProvider.getClusterOptions(markers); markerOptions.icon(opts.getIcon()); markerOptions.anchor(opts.getAnchorU(), opts.getAnchorV()); markerOptions.flat(opts.isFlat()); markerOptions.infoWindowAnchor(opts.getInfoWindowAnchorU(), opts.getInfoWindowAnchorV()); markerOptions.rotation(opts.getRotation()); } else { MarkerOptions opts = iconDataProvider.getIconData(markers.size()); markerOptions.icon(opts.getIcon()); markerOptions.anchor(opts.getAnchorU(), opts.getAnchorV()); markerOptions.flat(opts.isFlat()); markerOptions.infoWindowAnchor(opts.getInfoWindowAnchorU(), opts.getInfoWindowAnchorV()); markerOptions.rotation(opts.getRotation()); } return map.addMarker(markerOptions); } private static class ClusterKey { private final int group; private final int latitudeId; private final int longitudeId; public ClusterKey(int group, int latitudeId, int longitudeId) { this.group = group; this.latitudeId = latitudeId; this.longitudeId = longitudeId; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } ClusterKey that = (ClusterKey) o; if (group != that.group) { return false; } if (latitudeId != that.latitudeId) { return false; } if (longitudeId != that.longitudeId) { return false; } return true; } @Override public int hashCode() { int result = group; result = 31 * result + latitudeId; result = 31 * result + longitudeId; return result; } } }