Pull to refresh

Google Maps clustering

Reading time7 min
Views47K
Если вы занимаетесь разработкой приложений, использующих Google Maps, то вполне можете столкнуться с ситуацией, изображенной на картинке слева. И, если вы считаете, что картинка справа выглядит лучше, то вам сюда.

image

Итак, какие проблемы возникают при работе с большим количеством маркеров:
  • При количестве маркеров >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<AbstractMarker> clusterManager;
//...
clusterManager = new ClusterManager<AbstractMarker>(this.getApplicationContext(), getMap());

И теперь остается только добавить маркеры в этот объект с помощью метода clusterManager.addItem(AbstractMarker marker);.

С помощью метода cluster объекта класса ClusterManager можно вызвать рекластеризацию (например, после добавления элементов). Чтобы автоматически вызывать этот метод при изменении положения камеры (при изменении приближения) необходимо присвоить карте обработчик OnCameraChangeListener.

map.setOnCameraChangeListener(clusterManager);


Что же, теперь наша карта почти красивая. Почему почти? Потому что вместо фотографий водителей, вы увидите только маркеры по умолчанию не исключено, что так будет лучше, а по нажатию на маркер ничего не будет происходить, даже если написали InfoWindowAdapter. Вот теперь и будем улучшать внешний вид, пользуясь предоставляемыми средствами.

Шаг 3. Алгоритм

Рассмотрим алгоритмы кластеризации. Существуют различные алгоритмы, можно выделить 2 основных:

Grid-based Clustering — видимая область карты делится на квадраты, в центр квадрата помещается один кластер.
  • Поддерживает удаление элементов.
  • Работает достаточно быстро.
  • Выглядит не очень красиво, но если маркеры распределены равномерно, то ничего плохого в этом алгоритме нет




Distance-based Clustering — алгоритм по умолчанию. Основывается на вычисление центров наибольшей концентрации маркеров; кластеры не имеют фиксированных границ.
  • Не поддерживает удаление элементов.
  • Работает медленнее (более сложные вычисления).
  • Красиво располагает маркеры в соответствии с положением на карте.




Надеюсь, Google не обидится на меня за взятые с документации картинки.

Вряд ли появится существенная нужда писать собственный алгоритм, но если уж сильно хочется, то вам придется реализовать интерфейс Algorithm.

Шаг 4. Изменение свойств объекта clusterManager

Как я сказал, фотографии наших водителей сменились на стандартные маркеры. Не порядок. К счастью, это легко исправить (и не только это). У объекта clusterManager есть метод setRenderer, который позволяет задать класс, который будет управлять кластерами / маркерами перед выводом на карту и многое другое. По умолчанию используется класс DefaultClusterRenderer, в котором многие фишки уже реализованы правильно. Поэтому наилучшим подходом будет унаследоваться от этого класса и переопределить нужные методы:

public class OwnIconRendered extends DefaultClusterRenderer<AbstractMarker> {

	public OwnIconRendered(Context context, GoogleMap map,
			ClusterManager<AbstractMarker> clusterManager) {
		super(context, map, clusterManager);
	}
	
	@Override
	protected void onBeforeClusterItemRendered(AbstractMarker item,
			MarkerOptions markerOptions) {
		markerOptions.icon(item.getMarker().getIcon());
	}	
	
}

И присвоим нашему clusterManager этот класс:

clusterManager.setRenderer(new OwnIconRendered(
    getApplicationContext(), getMap(), clusterManager));


Мне нужно только поменять иконку у маркеров, поэтому я переопределил только метод onBeforeClusterItemRendered, который позволяет настроить элементы кластеров перед выводом на карту. Но выбор методов для переопределения достаточно велик:



Просто как отдельный факт, в процессе кластеризации немаловажную роль играет класс MarkerManager, который по сути предоставляет интерфейс для работы с коллекцией маркеров и кластеров.

И давайте сделаем последний штрих: по нажатию на маркер будем стандартно выводить информацию о текущем маркере с помощью метода toString (мы его переопределяли), а по нажатию на кластер будем выводить, сколько торговых точек и грузовиков находятся в этом регионе.
Во-первых, нам понадобятся объекты выбранных маркера и кластера:

private AbstractMarker chosenMarker;
private Cluster<AbstractMarker> chosenCluster;


Необходимо добавить listeners к clusterManager:

clusterManager.setOnClusterItemClickListener(new ClusterManager.OnClusterItemClickListener<AbstractMarker>() {
	@Override
	public boolean onClusterItemClick(AbstractMarker item) {
		chosenMarker = item;
		return false;
	}
});

clusterManager.setOnClusterClickListener(new ClusterManager.OnClusterClickListener<AbstractMarker>() {
	@Override
	public boolean onClusterClick(Cluster<AbstractMarker> cluster) {
		chosenCluster = cluster;
		return false;
	}
});


Нужно также сделать clusterManager слушателем события OnMarkerClickListener:

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;
	}	
}


Таким образом, мы создали красивую и достаточно функциональную карту.

Надеюсь, данная статья была для вас полезной. Она является лишь результатом изучения библиотеки и личных изысканий, поэтому ссылки на ресурсы оставлять не буду. Спасибо, что дочитали до конца!

P.S. Присоединяйтесь к крупнейшему в мире сообществу Android разработчиков в Slack.
Tags:
Hubs:
Total votes 29: ↑27 and ↓2+25
Comments7

Articles