Привет, Хабр! Хочу рассказать о моем опыте разработки карты с кластеризованными маркерами на google maps api и React.js. Кластеризация — это группировка близлежащих маркеров, меток, точек в один кластер. Это помогает улучшить UX и отобразить данные визуально понятнее, чем куча наехавших друг на друга точек. Компания, в которой я работаю, создает уникальный продукт для СМИ, это мобильное приложение, смысл которого заключается в съемке фото/видео/стрим материалов и возможности получить отличную компенсацию от СМИ в том случае, если редакция использует ваш материал в публикации. Я занимаюсь разработкой SPA приложения на стеке react/redux для модерации контента, присылаемого пользователями. Недавно передо мной встала задача сделать интерактивную карту на которой можно было бы увидеть местоположение пользователей и отправить им push уведомление, если поблизости происходит интересное событие.
Вот что мне предстояло сделать:
Первое что пришло мне на ум, поискать готовое решение для react.js. Я нашел 2 топовых библиотеки google-map-react и react-google-maps. Они представляют собой обертки над стандартным API Google maps, представленные в виде компонент для react.js. Мой выбор пал на google-map-react потому-что она позволяла использовать в качестве маркера любой JSX элемент, напомню что стандартные средства google maps api позволяют использовать в качестве маркера изображение и svg элемент, в сети есть решения, описывающие хитрую вставку html конструкций в качестве маркера, но google-map-react представляет это из коробки.
Едем дальше, на макете видно что если маркеры находятся близко к друг другу, они объединяются в групповой маркер — это и есть кластеризация. В readme google-map-react я нашел пример кластеризации, но он был реализован с помощью recompose — это утилита, которая создает обертку над function components и higher-order components. Создатели пишут чтобы мы думали что это некий lodash для реакта. Но тем, кто незнаком с recompose врятли сразу будет все понятно, поэтому я адаптировал этот пример и убрал лишнюю зависимость.
Для начала зададим свойства для google-map-react и state компоненты, отрендерим карту с заранее подготовленными маркерами:
(api key получаем здесь)
Маркеров на карте не будет, так как массив this.state.clusters пустой. Для объединения маркеров в группу используем библиотеку supercluster
Для примера сгенерируем точки с координатами:
При каждом изменении масштаба/центра карты будем пересчитывать кластеры:
В методе getClusters мы скармливаем сгенерированные точки в supercluster, и на выходе получаем кластеры. Таким образом supercluster просто объединил лежащие рядом координаты точек и выдал новую точку со своими координатами и массивом points, где лежат все вошедшие точки.
Демо можно посмотреть здесь
Исходный код примера здесь
Вот что мне предстояло сделать:
Первое что пришло мне на ум, поискать готовое решение для react.js. Я нашел 2 топовых библиотеки google-map-react и react-google-maps. Они представляют собой обертки над стандартным API Google maps, представленные в виде компонент для react.js. Мой выбор пал на google-map-react потому-что она позволяла использовать в качестве маркера любой JSX элемент, напомню что стандартные средства google maps api позволяют использовать в качестве маркера изображение и svg элемент, в сети есть решения, описывающие хитрую вставку html конструкций в качестве маркера, но google-map-react представляет это из коробки.
Едем дальше, на макете видно что если маркеры находятся близко к друг другу, они объединяются в групповой маркер — это и есть кластеризация. В readme google-map-react я нашел пример кластеризации, но он был реализован с помощью recompose — это утилита, которая создает обертку над function components и higher-order components. Создатели пишут чтобы мы думали что это некий lodash для реакта. Но тем, кто незнаком с recompose врятли сразу будет все понятно, поэтому я адаптировал этот пример и убрал лишнюю зависимость.
Для начала зададим свойства для google-map-react и state компоненты, отрендерим карту с заранее подготовленными маркерами:
(api key получаем здесь)
const MAP = {
defaultZoom: 8,
defaultCenter: { lat: 60.814305, lng: 47.051773 },
options: {
maxZoom: 19,
},
};
state = {
mapOptions: {
center: MAP.defaultCenter,
zoom: MAP.defaultZoom,
},
clusters: [],
};
//JSX
<GoogleMapReact
defaultZoom={MAP.defaultZoom}
defaultCenter={MAP.defaultCenter}
options={MAP.options}
onChange={this.handleMapChange}
yesIWantToUseGoogleMapApiInternals
bootstrapURLKeys={{ key: 'yourkey' }}
>
{this.state.clusters.map(item => {
if (item.numPoints === 1) {
return (
<Marker
key={item.id}
lat={item.points[0].lat}
lng={item.points[0].lng}
/>
);
}
return (
<ClusterMarker
key={item.id}
lat={item.lat}
lng={item.lng}
points={item.points}
/>
);
})}
</GoogleMapReact>
Маркеров на карте не будет, так как массив this.state.clusters пустой. Для объединения маркеров в группу используем библиотеку supercluster
Для примера сгенерируем точки с координатами:
const TOTAL_COUNT = 200;
export const susolvkaCoords = { lat: 60.814305, lng: 47.051773 };
export const markersData = [...Array(TOTAL_COUNT)]
.fill(0) // fill(0) for loose mode
.map((__, index) => ({
id: index,
lat:
susolvkaCoords.lat +
0.01 *
index *
Math.sin(30 * Math.PI * index / 180) *
Math.cos(50 * Math.PI * index / 180) +
Math.sin(5 * index / 180),
lng:
susolvkaCoords.lng +
0.01 *
index *
Math.cos(70 + 23 * Math.PI * index / 180) *
Math.cos(50 * Math.PI * index / 180) +
Math.sin(5 * index / 180),
}));
При каждом изменении масштаба/центра карты будем пересчитывать кластеры:
handleMapChange = ({ center, zoom, bounds }) => {
this.setState(
{
mapOptions: {
center,
zoom,
bounds,
},
},
() => {
this.createClusters(this.props);
}
);
};
createClusters = props => {
this.setState({
clusters: this.state.mapOptions.bounds
? this.getClusters(props).map(({ wx, wy, numPoints, points }) => ({
lat: wy,
lng: wx,
numPoints,
id: `${numPoints}_${points[0].id}`,
points,
}))
: [],
});
};
getClusters = () => {
const clusters = supercluster(markersData, {
minZoom: 0,
maxZoom: 16,
radius: 60,
});
return clusters(this.state.mapOptions);
};
В методе getClusters мы скармливаем сгенерированные точки в supercluster, и на выходе получаем кластеры. Таким образом supercluster просто объединил лежащие рядом координаты точек и выдал новую точку со своими координатами и массивом points, где лежат все вошедшие точки.
Демо можно посмотреть здесь
Исходный код примера здесь