From 1b88274aca4c95f60982d7aae85708712828d7ea Mon Sep 17 00:00:00 2001 From: efarooqui Date: Mon, 28 Sep 2020 13:40:28 -0700 Subject: [PATCH] Building with supercluster; working on map refactor --- package.json | 1 + src/components/Map.jsx | 69 +++++++++++++++++++++++++++++++- src/components/initialization.js | 69 ++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 src/components/initialization.js 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