diff --git a/package.json b/package.json
index 0f98cb7..3d753f1 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
"redux": "^3.6.0",
"redux-thunk": "^2.2.0",
"reselect": "^3.0.1",
+ "supercluster": "^7.1.0",
"video-react": "^0.13.1"
},
"devDependencies": {
diff --git a/src/components/Map.jsx b/src/components/Map.jsx
index ce971be..a76860b 100644
--- a/src/components/Map.jsx
+++ b/src/components/Map.jsx
@@ -1,6 +1,7 @@
/* global L */
import React from 'react'
import { Portal } from 'react-portal'
+import Supercluster from 'supercluster'
import { connect } from 'react-redux'
import * as selectors from '../selectors'
@@ -23,8 +24,10 @@ class Map extends React.Component {
constructor () {
super()
this.projectPoint = this.projectPoint.bind(this)
+ this.locationToGeoJSON = this.locationToGeoJSON.bind(this)
this.svgRef = React.createRef()
this.map = null
+ this.index = null
this.state = {
mapTransformX: 0,
mapTransformY: 0
@@ -93,7 +96,7 @@ class Map extends React.Component {
map.keyboard.disable()
map.zoomControl.remove()
-
+ map.on('moveend', () => this.updateClusters());
map.on('move zoomend viewreset moveend', () => this.alignLayers())
map.on('zoomstart', () => { if (this.svgRef.current !== null) this.svgRef.current.classList.add('hide') })
map.on('zoomend', () => { if (this.svgRef.current !== null) this.svgRef.current.classList.remove('hide') })
@@ -102,6 +105,46 @@ class Map extends React.Component {
this.map = map
}
+ // createClusterIcon(feature, latlng) {
+ // if (!feature.properties.cluster) return L.marker(latlng);
+
+ // const count = feature.properties.point_count;
+ // const size =
+ // count < 100 ? 'small' :
+ // count < 1000 ? 'medium' : 'large';
+ // const icon = L.divIcon({
+ // html: `
${ feature.properties.point_count_abbreviated }
`,
+ // className: `marker-cluster marker-cluster-${ size}`,
+ // iconSize: L.point(40, 40)
+ // });
+ // return L.marker(latlng, {icon});
+ // }
+
+ initializeSupercluster (locations) {
+ const { map: mapConf } = this.props.app
+ if (locations.length === 0) return
+ const geoJSON = locations.map(this.locationToGeoJSON)
+ // initialize supercluster
+ const index = new Supercluster({
+ radius: 40,
+ maxZoom: mapConf.maxZoom,
+ minZoom: mapConf.minZoom
+ }).load(geoJSON)
+ // Empty Layer Group that will receive the clusters data on the fly.
+ var markers = L.geoJSON(geoJSON, {}).addTo(this.map);
+ markers.id = 'clusters'
+ this.index = index
+ }
+
+ updateClusters () {
+ var bounds = this.map.getBounds();
+ var bbox = [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()];
+ var zoom = this.map.getZoom();
+ var clusters = this.index.getClusters(bbox, zoom);
+ // markers.clearLayers();
+ // markers.addData(clusters);
+ }
+
alignLayers () {
const mapNode = document.querySelector('.leaflet-map-pane')
if (mapNode === null) return { transformX: 0, transformY: 0 }
@@ -127,6 +170,19 @@ class Map extends React.Component {
}
}
+ locationToGeoJSON (location) {
+ const { x, y } = this.projectPoint([location.latitude, location.longitude])
+ const feature = {
+ type: 'Feature',
+ properties: {},
+ geometry: {
+ type: 'Point',
+ coordinates: [x, y]
+ }
+ }
+ return feature
+ }
+
getClientDims () {
const boundingClient = document.querySelector(`#${this.props.ui.dom.map}`).getBoundingClientRect()
@@ -189,6 +245,15 @@ class Map extends React.Component {
)
}
+ renderClusters () {
+ if (this.index === null) return
+ var bounds = this.map.getBounds();
+ var bbox = [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()];
+ var zoom = this.map.getZoom();
+ var clusters = this.index.getClusters(bbox, zoom);
+ console.info(this.map)
+ console.info('CLUSTERS: ', clusters)
+ }
/**
* Determines additional styles on the for each location.
* A location consists of an array of events (see selectors). The function
@@ -241,6 +306,7 @@ class Map extends React.Component {
render () {
const { isShowingSites } = this.props.app.flags
+ this.initializeSupercluster(this.props.domain.locations)
const classes = this.props.app.narrative ? 'map-wrapper narrative-mode' : 'map-wrapper'
const innerMap = this.map ? (
@@ -250,6 +316,7 @@ class Map extends React.Component {
{this.renderShapes()}
{this.renderNarratives()}
{this.renderEvents()}
+ {/* {this.renderClusters()} */}
{this.renderSelected()}
) : null
diff --git a/src/components/initialization.js b/src/components/initialization.js
new file mode 100644
index 0000000..8c0b605
--- /dev/null
+++ b/src/components/initialization.js
@@ -0,0 +1,69 @@
+var map = L.map('map').setView([0, 0], 0);
+
+// Empty Layer Group that will receive the clusters data on the fly.
+var markers = L.geoJSON(null, {
+ pointToLayer: createClusterIcon
+}).addTo(map);
+
+// Update the displayed clusters after user pan / zoom.
+map.on('moveend', update);
+
+function update() {
+ // if (!ready) return;
+ var bounds = map.getBounds();
+ var bbox = [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()];
+ var zoom = map.getZoom();
+ var clusters = index.getClusters(bbox, zoom);
+ markers.clearLayers();
+ markers.addData(clusters);
+}
+
+// Zoom to expand the cluster clicked by user.
+markers.on('click', function(e) {
+ var clusterId = e.layer.feature.properties.cluster_id;
+ var center = e.latlng;
+ var expansionZoom;
+ if (clusterId) {
+ expansionZoom = index.getClusterExpansionZoom(clusterId);
+ map.flyTo(center, expansionZoom);
+ }
+});
+
+// Retrieve Points data.
+var placesUrl = 'https://cdn.rawgit.com/mapbox/supercluster/v4.0.1/test/fixtures/places.json';
+var index;
+var ready = false;
+
+jQuery.getJSON(placesUrl, function(geojson) {
+ // Initialize the supercluster index.
+ index = supercluster({
+ radius: 60,
+ extent: 256,
+ maxZoom: 18
+ }).load(geojson.features); // Expects an array of Features.
+
+ ready = true;
+ update();
+});
+
+function createClusterIcon(feature, latlng) {
+ if (!feature.properties.cluster) return L.marker(latlng);
+
+ var count = feature.properties.point_count;
+ var size =
+ count < 100 ? 'small' :
+ count < 1000 ? 'medium' : 'large';
+ var icon = L.divIcon({
+ html: '' + feature.properties.point_count_abbreviated + '
',
+ className: 'marker-cluster marker-cluster-' + size,
+ iconSize: L.point(40, 40)
+ });
+
+ return L.marker(latlng, {
+ icon: icon
+ });
+}
+
+L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ attribution: '© OpenStreetMap contributors'
+}).addTo(map);
\ No newline at end of file