Вот что мне предстояло сделать:
Первое что пришло мне на ум, поискать готовое решение для 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, где лежат все вошедшие точки.
Демо можно посмотреть здесь
Исходный код примера здесь
Комментарии (10)
SergeyVoyteshonok
01.08.2017 17:32Я занимаюсь разработкой SPA приложения на стеке react/redux для модерации контента, присылаемого пользователями.
С реактом понятно, а где redux? Чего это у вас напрямую работа со стейтом?
Tim152
01.08.2017 17:52Так это и не SPA приложение :) надеюсь вы поняли иронию.
Я не сторонник хранить «все» в redux, то что можно сделать через локальный стейт, я делаю через state, в продакшене вместо markersData, я прокидываю координаты из родительского контейнера в props, которые приходят из reduxSergeyVoyteshonok
01.08.2017 18:07Ну, на демке вполне себе SPA )). Мне просто интересно, большинство людей, когда используют сложные компоненты типа картографических фреймворков в связке react/redux, забивают на принципы redux.
raveclassic
01.08.2017 22:32Просто redux — не панацея, и применять его нужно аккуратно, там где нужны транзакционные апдейты app-wide состояния. Все остальное прекрасно уживается в local state.
bano-notit
02.08.2017 01:32Стоп стоп стоп… То есть вы хотите, чтобы я, при использовании редакса, все состояния выносил в редакс? Ну это бред! По сути редакс нужен только для каких-то глобальных вещей, типа текущего url, не знаю, состояния рендеринга или логина. Зачем всё приложение уведомлять о том, что на какой-то карте, которая к приложению по сути может вообще не иметь отношения, изменились какие-то маркеры? Конечно если это какая-нибудь тактическая игра.
SergeyVoyteshonok
02.08.2017 09:14Нет, я не говорю, что все должно быть в redux. Просто я вот занимаюсь ГИСами и у меня то, что отображается на карте и есть основная бизнес-логика, а не «изменились какие-то маркеры». Вот мне и интересно было, когда автор сказал про стек react/redux, как он работает со сложными картографическими компонентами, с которыми все общение обычно построено на функциях и событиях, и следование redux при работе с этими компонентами рождает гигантский оверхед. Мои исследовании в итоге привели меня к тому же варианту, который описал автор — чистый react без redux/mobx.
expeon
02.08.2017 14:12А какое у вас требование по количеству одновременно отображаемых маркеров было?
Tim152
02.08.2017 14:23Требований по количеству не было, если точки уже есть на фронте, то гугл карта вполне шустро работает с большим количеством маркеров, пинговать начинает сервер, тк ему эти точки нужно на фронт прислать, в итоге мы пришли к следующему взаимодействию: при изменении координат видимой области карты, фронт отсылает центр и радиус в котором необходимо искать пользователей, а бек возвращает найденных с сортировкой к центру, в итоге работает быстро и грузит только то что надо
Avdeev
Отличный туториал, спасибо за проделанную работу!
Tim152
спасибо :)