Привет, Хабр! Хочу рассказать о моем опыте разработки карты с кластеризованными маркерами на 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, где лежат все вошедшие точки.
Демо можно посмотреть здесь
Исходный код примера здесь
