[Из песочницы] Google Maps clustering
Если вы занимаетесь разработкой приложений, использующих Google Maps, то вполне можете столкнуться с ситуацией, изображенной на картинке слева. И, если вы считаете, что картинка справа выглядит лучше, то вам сюда.Итак, какие проблемы возникают при работе с большим количеством маркеров:
При количестве маркеров >5000–10000 карта начинает жестоко тормозить. Внешний вид карты, заполненной маркерами, совсем не радует глаз. Какое же существует решение этой проблемы? Ответ — кластеризация. Кластер — объединение нескольких однородных элементов, которое может рассматриваться как самостоятельная единица, обладающая определёнными свойствами. Соответственно, мы должны объединять маркеры по территориальному признаку и заменять их одним маркером. И, к счастью, все это уже реализовано.Помимо стандартной библиотеки для работы с картами, Google предоставляет замечательную библиотеку Google Maps Android Marker Clustering Utility. Эта библиотека поставляется отдельно и позволяет компоновать маркеры.
Рассмотрим наиболее сложный пример, когда у есть несколько типов маркеров, каждый со своей иконкой и необходимыми действиями по нажатию (например, вывод InfoWindow с текстом).
Кому интереснее читать документацию
Задача Пусть перед нами поставлена следующая задача: дана торговая сеть, у нее есть некоторое количество точек и большое количество грузовиков, перевозящих товар, которые способны отчитываться о своем местоположении. Нужно наладить мониторинг положения грузовиков и торговых точек. У маркеров грузовиков должна быть иконка с лицом водителя. Заказчик суровый, увидел картинку с кластерами и сказал, чтобы все было так, а не иначе.Шаг 1. Подготовка Первый шаг весьма логичен — нужно описать абстрактный класс маркера, от которого будут наследоваться все остальные. Чтобы возможно было использовать кластеризацию, этот класс маркера должен уметь возвращать свое местоположение. Для этого необходимо реализовать интерфейс ClusterItem. Тогда возможный код абстрактного класса: public abstract class AbstractMarker implements ClusterItem { protected double latitude; protected double longitude;
protected MarkerOptions marker;
@Override public LatLng getPosition () { return new LatLng (latitude, longitude); }
protected AbstractMarker (double latitude, double longitude) { setLatitude (latitude); setLongitude (longitude); }
@Override public abstract String toString ();
public abstract MarkerOptions getMarker () { return marker; }
public void setMarker (MarkerOptions marker) { this.marker = marker; } //others getters & setters } Далее находится не слишком важный код классов маркеров торговых точек и грузовиков, но вдруг кому-то интересно.
TradeMarker, TruckMarker public class TradeMarker extends AbstractMarker {
private static BitmapDescriptor shopIcon = null;
private String description;
public TradeMarker (String description, double latitude, double longitude) { super (latitude, longitude); setDescription (description); setBitmapDescriptor (); setMarker (new MarkerOptions () .position (new LatLng (getLatitude (), getLongitude ())) .title (») .icon (shopIcon)); }
public static void setBitmapDescriptor () { if (shopIcon == null) shopIcon = BitmapDescriptorFactory. fromResource (R.drawable.trademarker); }
public String toString () { return «Trade place:» + getDescription (); }
public String getDescription () { return description; }
public void setDescription (String description) { this.description = description; } } public class TruckMarker extends AbstractMarker {
private String name; private String aim;
public TruckMarker (String name, String aim, double latitude, double longitude, BitmapDescriptor photo) { super (latitude, longitude); setName (name); setAim (aim); setMarker (new MarkerOptions () .position (new LatLng (getLatitude (), getLongitude ())) .title (») .icon (photo)); }
public String toString () { return «Name:» + getName () + »\n» + «Aim:» + getAim (); }
public String getName () { return name; }
public void setName (String name) { this.name = name; }
public String getAim () { return aim; }
public void setAim (String aim) { this.aim = aim; } } Шаг 2. Простейшая кластеризация Что же, подготовка завершена, теперь пора настроить кластеризацию. Наиболее важным здесь является класс ClusterManager, который позволяет гибко настроить кластеризацию. Класс ClusterManager является обобщенным, в качестве параметра ему передается класс, реализующий интерфейс ClusterItem. В качестве параметров при инициализации ему подаются контекст приложения и объект GoogleMap.Таким образом, для нашего кода это будет выглядеть примерно так:
private ClusterManager
map.setOnCameraChangeListener (clusterManager); Что же, теперь наша карта почти красивая. Почему почти? Потому что вместо фотографий водителей, вы увидите только маркеры по умолчанию не исключено, что так будет лучше, а по нажатию на маркер ничего не будет происходить, даже если написали InfoWindowAdapter. Вот теперь и будем улучшать внешний вид, пользуясь предоставляемыми средствами.
Шаг 3. Алгоритм Рассмотрим алгоритмы кластеризации. Существуют различные алгоритмы, можно выделить 2 основных: Grid-based Clustering — видимая область карты делится на квадраты, в центр квадрата помещается один кластер.
Поддерживает удаление элементов. Работает достаточно быстро. Выглядит не очень красиво, но если маркеры распределены равномерно, то ничего плохого в этом алгоритме нет
Distance-based Clustering — алгоритм по умолчанию. Основывается на вычисление центров наибольшей концентрации маркеров; кластеры не имеют фиксированных границ.
Не поддерживает удаление элементов. Работает медленнее (более сложные вычисления). Красиво располагает маркеры в соответствии с положением на карте.
Надеюсь, Google не обидится на меня за взятые с документации картинки.
Вряд ли появится существенная нужда писать собственный алгоритм, но если уж сильно хочется, то вам придется реализовать интерфейс Algorithm.
Шаг 4. Изменение свойств объекта clusterManager
Как я сказал, фотографии наших водителей сменились на стандартные маркеры. Не порядок. К счастью, это легко исправить (и не только это). У объекта clusterManager есть метод setRenderer, который позволяет задать класс, который будет управлять кластерами / маркерами перед выводом на карту и многое другое. По умолчанию используется класс DefaultClusterRenderer, в котором многие фишки уже реализованы правильно. Поэтому наилучшим подходом будет унаследоваться от этого класса и переопределить нужные методы:
public class OwnIconRendered extends DefaultClusterRenderer
public OwnIconRendered (Context context, GoogleMap map,
ClusterManager
Просто как отдельный факт, в процессе кластеризации немаловажную роль играет класс MarkerManager, который по сути предоставляет интерфейс для работы с коллекцией маркеров и кластеров.
И давайте сделаем последний штрих: по нажатию на маркер будем стандартно выводить информацию о текущем маркере с помощью метода toString (мы его переопределяли), а по нажатию на кластер будем выводить, сколько торговых точек и грузовиков находятся в этом регионе.Во-первых, нам понадобятся объекты выбранных маркера и кластера:
private AbstractMarker chosenMarker;
private Cluster
clusterManager.setOnClusterItemClickListener (new ClusterManager.OnClusterItemClickListener
clusterManager.setOnClusterClickListener (new ClusterManager.OnClusterClickListener
map.setOnMarkerClickListener (clusterManager); Теперь необходимо присвоить InfoWindowAdapter всем маркерам и всем кластерам. Для этого можно воспользоваться методами получения коллекций кластеров маркеров у объекта clusterManager. Также не забудем присвоить InfoWindowAdapter карте:
clusterManager.getMarkerCollection ().setOnInfoWindowAdapter (new MarkerInfoWindowAdapter ());
clusterManager.getClusterMarkerCollection ().setOnInfoWindowAdapter (new ClusterInfoWindow ());
map.setInfoWindowAdapter (clusterManager.getMarkerManager ()); Ну и для примера как может выглядеть класс ClusterInfoWindow:
private class ClusterInfoWindow implements InfoWindowAdapter {
@Override public View getInfoContents (Marker arg0) { return null; }
@Override public View getInfoWindow (Marker marker) { if (chosenCluster!= null) { View v = getLayoutInflater ().inflate (R.layout.cluster_window, null); TextView info = (TextView) v.findViewById (R.id.clusterTitle); int[] markerTypesCount = new int[2]; Arrays.fill (markerTypesCount, 0); for (AbstractMarker abstractMarker: chosenCluster.getItems ()) { if (abstractMarker instanceof TradeMarker) markerTypesCount[0] += 1; else if (abstractMarker instanceof TruckMarker) markerTypesCount[1] += 1; } info.setText («Trade places:» + markerTypesCount[0] + »\n» + «Truck:» + markerTypesCount[1] + »\n»);
return v; } return null; } } Таким образом, мы создали красивую и достаточно функциональную карту.
Надеюсь, данная статья была для вас полезной. Она является лишь результатом изучения библиотеки и личных изысканий, поэтому ссылки на ресурсы оставлять не буду. Спасибо, что дочитали до конца!