mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-08 03:18:36 +03:00
Building with supercluster; working on map refactor
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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: `<div><span>${ feature.properties.point_count_abbreviated }</span></div>`,
|
||||
// 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 <circle> 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 ? (
|
||||
<React.Fragment>
|
||||
@@ -250,6 +316,7 @@ class Map extends React.Component {
|
||||
{this.renderShapes()}
|
||||
{this.renderNarratives()}
|
||||
{this.renderEvents()}
|
||||
{/* {this.renderClusters()} */}
|
||||
{this.renderSelected()}
|
||||
</React.Fragment>
|
||||
) : null
|
||||
|
||||
69
src/components/initialization.js
Normal file
69
src/components/initialization.js
Normal file
@@ -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: '<div><span>' + feature.properties.point_count_abbreviated + '</span></div>',
|
||||
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: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
Reference in New Issue
Block a user