From 1b88274aca4c95f60982d7aae85708712828d7ea Mon Sep 17 00:00:00 2001 From: efarooqui Date: Mon, 28 Sep 2020 13:40:28 -0700 Subject: [PATCH 01/26] 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 From 9c59dd12a09d26d8acc6cd32ca26710ae3f396df Mon Sep 17 00:00:00 2001 From: efarooqui Date: Tue, 29 Sep 2020 09:35:55 -0700 Subject: [PATCH 02/26] Clustering working upon zoom; loading cluster data appropriately initially; need to style and add markers --- src/components/Map.jsx | 99 +++++++++++++++++++----------------------- src/store/initial.js | 3 +- 2 files changed, 47 insertions(+), 55 deletions(-) diff --git a/src/components/Map.jsx b/src/components/Map.jsx index a76860b..e92d38f 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -19,6 +19,7 @@ import DefsMarkers from './presentational/Map/DefsMarkers.jsx' // NB: important constants for map, TODO: make statics const supportedMapboxMap = ['streets', 'satellite'] const defaultToken = 'your_token' +const clusterId = 'clusters' class Map extends React.Component { constructor () { @@ -30,7 +31,8 @@ class Map extends React.Component { this.index = null this.state = { mapTransformX: 0, - mapTransformY: 0 + mapTransformY: 0, + clusters: [] } this.styleLocation = this.styleLocation.bind(this) } @@ -42,6 +44,9 @@ class Map extends React.Component { } componentWillReceiveProps (nextProps) { + if (hash(nextProps.domain.locations) !== hash(this.props.domain.locations)) { + this.loadClusterData(nextProps.domain.locations) + } // Set appropriate zoom for narrative const { bounds } = nextProps.app.map if (hash(bounds) !== hash(this.props.app.map.bounds) && @@ -77,6 +82,13 @@ class Map extends React.Component { .setMaxZoom(mapConf.maxZoom) .setMaxBounds(mapConf.maxBounds) + // Initialize supercluster index + const index = new Supercluster({ + radius: mapConf.clusterRadius, + maxZoom: mapConf.maxZoom, + minZoom: mapConf.minZoom + }) + let firstLayer if ((supportedMapboxMap.indexOf(this.props.ui.tiles) !== -1) && process.env.MAPBOX_TOKEN && process.env.MAPBOX_TOKEN !== defaultToken) { @@ -96,53 +108,42 @@ 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('moveend', () => { + this.update() + this.alignLayers() + }) + map.on('move zoomend viewreset', () => 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') }) window.addEventListener('resize', () => { this.alignLayers() }) 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); + getMapDetails () { + const bounds = this.map.getBounds() + const bbox = [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()] + const zoom = this.map.getZoom() + return [bbox, zoom] + } + + update () { + const [bbox, zoom] = this.getMapDetails() + this.setState({ + clusters: this.index.getClusters(bbox, zoom) + }) + } + + loadClusterData (locations) { + if (locations && locations.length !== 0) { + const geoJSON = locations.map(this.locationToGeoJSON) + if (this.index) { + this.index.load(geoJSON) + this.update() + } + } } alignLayers () { @@ -171,13 +172,14 @@ class Map extends React.Component { } locationToGeoJSON (location) { - const { x, y } = this.projectPoint([location.latitude, location.longitude]) const feature = { type: 'Feature', - properties: {}, + properties: { + cluster: false + }, geometry: { type: 'Point', - coordinates: [x, y] + coordinates: [location.longitude, location.latitude] } } return feature @@ -245,15 +247,6 @@ 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 @@ -306,7 +299,6 @@ 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 ? ( @@ -316,7 +308,6 @@ class Map extends React.Component { {this.renderShapes()} {this.renderNarratives()} {this.renderEvents()} - {/* {this.renderClusters()} */} {this.renderSelected()} ) : null diff --git a/src/store/initial.js b/src/store/initial.js index 1b9ab6d..1842230 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -52,7 +52,8 @@ const initial = { minZoom: 6, maxZoom: 18, bounds: null, - maxBounds: [[180, -180], [-180, 180]] + maxBounds: [[180, -180], [-180, 180]], + clusterRadius: 100, }, timeline: { dimensions: { From 297dd83436c2470b8577a3c0a64fbfd29e40b507 Mon Sep 17 00:00:00 2001 From: efarooqui Date: Tue, 29 Sep 2020 10:45:08 -0700 Subject: [PATCH 03/26] Individual points showing with cluster logic; need to define styling and appearance for larger clusters --- src/components/Map.jsx | 26 +++++++++++++++----------- src/store/initial.js | 2 +- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/components/Map.jsx b/src/components/Map.jsx index e92d38f..792b1ee 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -32,6 +32,7 @@ class Map extends React.Component { this.state = { mapTransformX: 0, mapTransformY: 0, + indexLoaded: false, clusters: [] } this.styleLocation = this.styleLocation.bind(this) @@ -131,18 +132,18 @@ class Map extends React.Component { update () { const [bbox, zoom] = this.getMapDetails() - this.setState({ - clusters: this.index.getClusters(bbox, zoom) - }) + if (this.index && this.state.indexLoaded) { + this.setState({ + clusters: this.index.getClusters(bbox, zoom) + }) + } } loadClusterData (locations) { - if (locations && locations.length !== 0) { - const geoJSON = locations.map(this.locationToGeoJSON) - if (this.index) { - this.index.load(geoJSON) - this.update() - } + if (locations && locations.length !== 0 && this.index) { + this.index.load(locations.map(this.locationToGeoJSON)) + this.setState({indexLoaded: true}) + this.update() } } @@ -175,7 +176,8 @@ class Map extends React.Component { const feature = { type: 'Feature', properties: { - cluster: false + cluster: false, + id: location.label }, geometry: { type: 'Point', @@ -261,11 +263,13 @@ class Map extends React.Component { } renderEvents () { + const individualClusters = this.state.clusters.filter(cl => !cl.properties.cluster) + const filteredLocations = individualClusters.map(cl => this.props.domain.locations.find(location => location.label === cl.properties.id)) return ( Date: Tue, 29 Sep 2020 18:48:40 -0700 Subject: [PATCH 04/26] Creating cluster styling and component to handle cluster visualization and mapping --- src/components/Map.jsx | 1 + .../presentational/Map/Clusters.jsx | 67 +++++++++++++++++++ src/scss/map.scss | 14 ++++ 3 files changed, 82 insertions(+) create mode 100644 src/components/presentational/Map/Clusters.jsx diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 792b1ee..5dcafbf 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -303,6 +303,7 @@ class Map extends React.Component { render () { const { isShowingSites } = this.props.app.flags + // console.info(this.state.clusters) const classes = this.props.app.narrative ? 'map-wrapper narrative-mode' : 'map-wrapper' const innerMap = this.map ? ( diff --git a/src/components/presentational/Map/Clusters.jsx b/src/components/presentational/Map/Clusters.jsx new file mode 100644 index 0000000..6752491 --- /dev/null +++ b/src/components/presentational/Map/Clusters.jsx @@ -0,0 +1,67 @@ +import React from 'react' +import { Portal } from 'react-portal' +import colors from '../../../common/global.js' +import { calcOpacity } from '../../../common/utilities' + +function ClusterEvents ({ + projectPoint, + styleCluster, + onSelect, + svg, + clusters, + radius +}) { + function renderClusterBySize (cluster) { + + } + + + function renderCluster (cluster) { + /** + { + geometry: { + coordinates: [longitude, latitude] + }, + properties: { + cluster: true|false, + cluster_id: int, + point_count: int, + point_count_abbreviated: int + }, + type: "Feature" + } + */ + const { coordinates } = cluster.geometry + const [longitude, latitude] = coordinates + if (!latitude || !longitude) return null + const { x, y } = projectPoint([latitude, longitude]) + + const customStyles = styleCluster ? styleCluster(cluster) : null + const extraRender = () => ( + + {customStyles[1]} + + ) + + return ( + + {renderClusterBySize(cluster)} + {extraRender ? extraRender() : null} + + ) + } + + return ( + + + {clusters.map(renderCluster)} + + + ) +} + +export default ClusterEvents diff --git a/src/scss/map.scss b/src/scss/map.scss index c492967..6e3121a 100644 --- a/src/scss/map.scss +++ b/src/scss/map.scss @@ -175,6 +175,10 @@ cursor: pointer; } +.cluster-event { + cursor: pointer; +} + .location-event-marker { pointer-events: all !important; fill: $event_default; @@ -185,6 +189,16 @@ } } +.cluster-event-marker { + pointer-events: all !important; + fill: $event_default; + stroke-width: 0; + + &.red { + fill: red; + } +} + .narrative-step-arrow { pointer-events: all !important; } From aa87db6b846bd07da611afa459bbf298255a3328 Mon Sep 17 00:00:00 2001 From: efarooqui Date: Wed, 30 Sep 2020 10:53:15 -0700 Subject: [PATCH 05/26] Working cluster visualization; need to modify styling for clusters for size and work out color based on events --- src/common/utilities.js | 4 +++ src/components/Map.jsx | 22 +++++++++++- .../presentational/Map/Clusters.jsx | 35 ++++++++++++++++--- src/store/initial.js | 2 +- 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/common/utilities.js b/src/common/utilities.js index dac0f4a..3690363 100644 --- a/src/common/utilities.js +++ b/src/common/utilities.js @@ -168,6 +168,10 @@ export function calcOpacity (num) { return base + (Math.min(0.5, 0.08 * (num - 1))) } +export function calcClusterSize (pointCount, numClusters) { + return Math.min(50, 10 + (pointCount / numClusters) * 20) +} + export const dateMin = function () { return Array.prototype.slice.call(arguments).reduce(function (a, b) { return a < b ? a : b diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 5dcafbf..f31b389 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -12,6 +12,7 @@ import 'leaflet' import Sites from './presentational/Map/Sites.jsx' import Shapes from './presentational/Map/Shapes.jsx' import Events from './presentational/Map/Events.jsx' +import Clusters from './presentational/Map/Clusters.jsx' import SelectedEvents from './presentational/Map/SelectedEvents.jsx' import Narratives from './presentational/Map/Narratives' import DefsMarkers from './presentational/Map/DefsMarkers.jsx' @@ -262,6 +263,10 @@ class Map extends React.Component { return [null, null] } + styleCluster (cluster) { + return [null, null] + } + renderEvents () { const individualClusters = this.state.clusters.filter(cl => !cl.properties.cluster) const filteredLocations = individualClusters.map(cl => this.props.domain.locations.find(location => location.label === cl.properties.id)) @@ -282,6 +287,20 @@ class Map extends React.Component { ) } + renderClusters () { + const allClusters = this.state.clusters.filter(cl => cl.properties.cluster) + return ( + {}} + /> + ) + } + renderSelected () { return ( @@ -313,6 +332,7 @@ class Map extends React.Component { {this.renderShapes()} {this.renderNarratives()} {this.renderEvents()} + {this.renderClusters()} {this.renderSelected()} ) : null diff --git a/src/components/presentational/Map/Clusters.jsx b/src/components/presentational/Map/Clusters.jsx index 6752491..c738520 100644 --- a/src/components/presentational/Map/Clusters.jsx +++ b/src/components/presentational/Map/Clusters.jsx @@ -1,18 +1,43 @@ import React from 'react' import { Portal } from 'react-portal' import colors from '../../../common/global.js' -import { calcOpacity } from '../../../common/utilities' +import { calcOpacity, calcClusterSize } from '../../../common/utilities' function ClusterEvents ({ projectPoint, styleCluster, - onSelect, + // onSelect, svg, clusters, - radius + numClusters, }) { function renderClusterBySize (cluster) { + const { point_count, cluster_id } = cluster.properties + const size = calcClusterSize(point_count, numClusters) + + const width = `${size}px` + const height = `${size}px` + + const styles = ({ + fill: 'blue', + stroke: colors.darkBackground, + strokeWidth: 0, + fillOpacity: calcOpacity(point_count), + width, + height + }) + + return ( + + + + ) } @@ -47,7 +72,7 @@ function ClusterEvents ({ {renderClusterBySize(cluster)} {extraRender ? extraRender() : null} @@ -57,7 +82,7 @@ function ClusterEvents ({ return ( - + {clusters.map(renderCluster)} diff --git a/src/store/initial.js b/src/store/initial.js index 623cc9f..d811cbd 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -53,7 +53,7 @@ const initial = { maxZoom: 18, bounds: null, maxBounds: [[180, -180], [-180, 180]], - clusterRadius: 40, + clusterRadius: 50, }, timeline: { dimensions: { From 5020e738118bfb68eaeace8298965064befc7d7c Mon Sep 17 00:00:00 2001 From: efarooqui Date: Wed, 30 Sep 2020 11:21:37 -0700 Subject: [PATCH 06/26] Cluster sizes showing up appropriately and unclustering upon zoom; need to nail down styling and add onSelect for clusters --- src/common/utilities.js | 2 +- src/components/Map.jsx | 1 - .../presentational/Map/Clusters.jsx | 23 ++++++++----------- src/scss/map.scss | 4 ++-- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/common/utilities.js b/src/common/utilities.js index 3690363..e78bfdc 100644 --- a/src/common/utilities.js +++ b/src/common/utilities.js @@ -169,7 +169,7 @@ export function calcOpacity (num) { } export function calcClusterSize (pointCount, numClusters) { - return Math.min(50, 10 + (pointCount / numClusters) * 20) + return Math.min(50, 10 + (pointCount / numClusters) * 10) } export const dateMin = function () { diff --git a/src/components/Map.jsx b/src/components/Map.jsx index f31b389..add532c 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -322,7 +322,6 @@ class Map extends React.Component { render () { const { isShowingSites } = this.props.app.flags - console.info(this.state.clusters) const classes = this.props.app.narrative ? 'map-wrapper narrative-mode' : 'map-wrapper' const innerMap = this.map ? ( diff --git a/src/components/presentational/Map/Clusters.jsx b/src/components/presentational/Map/Clusters.jsx index c738520..c1ab621 100644 --- a/src/components/presentational/Map/Clusters.jsx +++ b/src/components/presentational/Map/Clusters.jsx @@ -12,30 +12,25 @@ function ClusterEvents ({ numClusters, }) { function renderClusterBySize (cluster) { - const { point_count, cluster_id } = cluster.properties - - const size = calcClusterSize(point_count, numClusters) - - const width = `${size}px` - const height = `${size}px` + const { point_count: pointCount, cluster_id: clusterId } = cluster.properties const styles = ({ - fill: 'blue', + fill: colors.fallbackEventColor, stroke: colors.darkBackground, strokeWidth: 0, - fillOpacity: calcOpacity(point_count), - width, - height + fillOpacity: calcOpacity(pointCount), }) return ( - + />} ) } diff --git a/src/scss/map.scss b/src/scss/map.scss index 6e3121a..9d23009 100644 --- a/src/scss/map.scss +++ b/src/scss/map.scss @@ -191,8 +191,8 @@ .cluster-event-marker { pointer-events: all !important; - fill: $event_default; - stroke-width: 0; + // fill: $event_default; + // stroke-width: 0; &.red { fill: red; From 5333f492f8722c49d80c34b1f4580cb4d144e30f Mon Sep 17 00:00:00 2001 From: efarooqui Date: Wed, 30 Sep 2020 13:07:29 -0700 Subject: [PATCH 07/26] Getting appropriate clusterZoom; flyto moving to incorrect location on map --- src/common/utilities.js | 4 ++-- src/components/Map.jsx | 11 ++++++++++- src/components/presentational/Map/Clusters.jsx | 8 ++++++-- src/store/initial.js | 4 ++-- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/common/utilities.js b/src/common/utilities.js index e78bfdc..8a8ed67 100644 --- a/src/common/utilities.js +++ b/src/common/utilities.js @@ -74,8 +74,8 @@ export function insetSourceFrom (allSources) { if (!event.sources) { sources = [] } else { - sources = event.sources.map(src => { - const id = typeof src === 'object' ? src.id : src + sources = event.sources.map(id => { + // const id = typeof src === 'object' ? src.id : src return allSources.hasOwnProperty(id) ? allSources[id] : null }) } diff --git a/src/components/Map.jsx b/src/components/Map.jsx index add532c..492d152 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -27,6 +27,7 @@ class Map extends React.Component { super() this.projectPoint = this.projectPoint.bind(this) this.locationToGeoJSON = this.locationToGeoJSON.bind(this) + this.onClusterSelect = this.onClusterSelect.bind(this) this.svgRef = React.createRef() this.map = null this.index = null @@ -188,6 +189,14 @@ class Map extends React.Component { return feature } + onClusterSelect (e) { + const { id } = e.target + const { longitude, latitude } = e.target.attributes + const { x, y } = this.projectPoint([latitude.value, longitude.value]) + const expansionZoom = Math.max(this.index.getClusterExpansionZoom(parseInt(id)), this.index.options.minZoom) + this.map.flyTo([x, y], expansionZoom) + } + getClientDims () { const boundingClient = document.querySelector(`#${this.props.ui.dom.map}`).getBoundingClientRect() @@ -296,7 +305,7 @@ class Map extends React.Component { projectPoint={this.projectPoint} clusters={allClusters} numClusters={allClusters.length} - // onSelect={() => {}} + onSelect={this.onClusterSelect} /> ) } diff --git a/src/components/presentational/Map/Clusters.jsx b/src/components/presentational/Map/Clusters.jsx index c1ab621..f7164ab 100644 --- a/src/components/presentational/Map/Clusters.jsx +++ b/src/components/presentational/Map/Clusters.jsx @@ -6,13 +6,15 @@ import { calcOpacity, calcClusterSize } from '../../../common/utilities' function ClusterEvents ({ projectPoint, styleCluster, - // onSelect, + onSelect, svg, clusters, numClusters, }) { function renderClusterBySize (cluster) { const { point_count: pointCount, cluster_id: clusterId } = cluster.properties + const { coordinates } = cluster.geometry + const [longitude, latitude] = coordinates const styles = ({ fill: colors.fallbackEventColor, @@ -26,6 +28,8 @@ function ClusterEvents ({ { onSelect(e)} > {renderClusterBySize(cluster)} {extraRender ? extraRender() : null} diff --git a/src/store/initial.js b/src/store/initial.js index d811cbd..b26b85d 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -49,11 +49,11 @@ const initial = { map: { anchor: [31.356397, 34.784818], startZoom: 11, - minZoom: 6, + minZoom: 2, maxZoom: 18, bounds: null, maxBounds: [[180, -180], [-180, 180]], - clusterRadius: 50, + clusterRadius: 20, }, timeline: { dimensions: { From ed236e971621d67804896fd8a3b8f7f5096465e8 Mon Sep 17 00:00:00 2001 From: efarooqui Date: Wed, 30 Sep 2020 14:01:08 -0700 Subject: [PATCH 08/26] Working onCluster select; zooming in and watching clusters break down into individual clusters and points --- src/components/Map.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 492d152..25e6754 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -192,9 +192,8 @@ class Map extends React.Component { onClusterSelect (e) { const { id } = e.target const { longitude, latitude } = e.target.attributes - const { x, y } = this.projectPoint([latitude.value, longitude.value]) const expansionZoom = Math.max(this.index.getClusterExpansionZoom(parseInt(id)), this.index.options.minZoom) - this.map.flyTo([x, y], expansionZoom) + this.map.flyTo(new L.LatLng(latitude.value, longitude.value), expansionZoom) } getClientDims () { From 9357b5e28179d47fe27c0efb39759f2de275abe7 Mon Sep 17 00:00:00 2001 From: efarooqui Date: Thu, 1 Oct 2020 15:35:46 -0700 Subject: [PATCH 09/26] Broken onClick clusters working after validating longitude and latitude and only allowing locations to contain valid coordinates --- src/common/utilities.js | 8 ++++++++ src/components/Layout.js | 5 ++--- src/components/Map.jsx | 41 ++++++++++++++++++++++------------------ src/selectors/index.js | 6 ++++-- src/store/initial.js | 4 ++-- 5 files changed, 39 insertions(+), 25 deletions(-) diff --git a/src/common/utilities.js b/src/common/utilities.js index 8a8ed67..c77377c 100644 --- a/src/common/utilities.js +++ b/src/common/utilities.js @@ -172,6 +172,14 @@ export function calcClusterSize (pointCount, numClusters) { return Math.min(50, 10 + (pointCount / numClusters) * 10) } +export function isLatitude(lat) { + return !!lat && isFinite(lat) && Math.abs(lat) <= 90; +} + +export function isLongitude(lng) { + return !!lng && isFinite(lng) && Math.abs(lng) <= 180; +} + export const dateMin = function () { return Array.prototype.slice.call(arguments).reduce(function (a, b) { return a < b ? a : b diff --git a/src/components/Layout.js b/src/components/Layout.js index 8ef21e7..5930283 100644 --- a/src/components/Layout.js +++ b/src/components/Layout.js @@ -239,7 +239,6 @@ class Dashboard extends React.Component { render () { const { actions, app, domain, ui, features } = this.props - if (isMobile || window.innerWidth < 600) { const msg = 'This platform is not suitable for mobile. Please re-visit the site on a device with a larger screen.' return ( @@ -260,7 +259,7 @@ class Dashboard extends React.Component { } return ( -
+
this.update()) map.on('move zoomend viewreset', () => 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') }) window.addEventListener('resize', () => { this.alignLayers() }) this.map = map - this.index = index } getMapDetails () { @@ -143,7 +144,26 @@ class Map extends React.Component { loadClusterData (locations) { if (locations && locations.length !== 0 && this.index) { - this.index.load(locations.map(this.locationToGeoJSON)) + const convertedLocations = locations.reduce((acc, loc) => { + const { longitude, latitude } = loc + const validCoordinates = !!latitude && !!longitude + if (validCoordinates) { + const feature = { + type: 'Feature', + properties: { + cluster: false, + id: loc.label + }, + geometry: { + type: 'Point', + coordinates: [longitude, latitude] + } + } + acc.push(feature) + } + return acc + }, []) + this.index.load(convertedLocations) this.setState({indexLoaded: true}) this.update() } @@ -174,21 +194,6 @@ class Map extends React.Component { } } - locationToGeoJSON (location) { - const feature = { - type: 'Feature', - properties: { - cluster: false, - id: location.label - }, - geometry: { - type: 'Point', - coordinates: [location.longitude, location.latitude] - } - } - return feature - } - onClusterSelect (e) { const { id } = e.target const { longitude, latitude } = e.target.attributes diff --git a/src/selectors/index.js b/src/selectors/index.js index 4b5d2b6..26cd124 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -1,5 +1,5 @@ import { createSelector } from 'reselect' -import { insetSourceFrom, dateMin, dateMax } from '../common/utilities' +import { insetSourceFrom, dateMin, dateMax, isLatitude, isLongitude } from '../common/utilities' import { isTimeRangedIn } from './helpers' import { FILTER_MODE, NARRATIVE_MODE } from '../common/constants' @@ -64,7 +64,6 @@ export const selectEvents = createSelector( if (isActiveTime && isActiveFilter && isActiveCategory) { acc[event.id] = { ...event } } - return acc }, []) }) @@ -159,6 +158,9 @@ export const selectLocations = createSelector( (events) => { const activeLocations = {} events.forEach(event => { + const { latitude, longitude } = event + if (!isLatitude(latitude) || !isLongitude(longitude)) return + const location = `${event.location}$_${event.latitude}_${event.longitude}` if (activeLocations[location]) { diff --git a/src/store/initial.js b/src/store/initial.js index b26b85d..a3b6bd3 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -11,7 +11,7 @@ const initial = { */ domain: { events: [], - locations: [], + // locations: [], categories: [], associations: [], sources: {}, @@ -49,7 +49,7 @@ const initial = { map: { anchor: [31.356397, 34.784818], startZoom: 11, - minZoom: 2, + minZoom: 0, maxZoom: 18, bounds: null, maxBounds: [[180, -180], [-180, 180]], From 47b9a66816f66fa092f95f2b4744e12b227707c1 Mon Sep 17 00:00:00 2001 From: efarooqui Date: Mon, 5 Oct 2020 09:45:17 -0700 Subject: [PATCH 10/26] Working on select that selects all events in a cluster; need to configure UI to both zoom and select events --- src/common/utilities.js | 4 ++++ src/components/Map.jsx | 15 ++++++++++++++- src/store/initial.js | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/common/utilities.js b/src/common/utilities.js index c77377c..78d3443 100644 --- a/src/common/utilities.js +++ b/src/common/utilities.js @@ -180,6 +180,10 @@ export function isLongitude(lng) { return !!lng && isFinite(lng) && Math.abs(lng) <= 180; } +export function mapClustersToLocations(clusters, locations) { + return clusters.map(cl => locations.find(location => location.label === cl.properties.id)) +} + export const dateMin = function () { return Array.prototype.slice.call(arguments).reduce(function (a, b) { return a < b ? a : b diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 6ce2248..8940d17 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -17,6 +17,8 @@ import SelectedEvents from './presentational/Map/SelectedEvents.jsx' import Narratives from './presentational/Map/Narratives' import DefsMarkers from './presentational/Map/DefsMarkers.jsx' +import { mapClustersToLocations } from '../common/utilities' + // NB: important constants for map, TODO: make statics const supportedMapboxMap = ['streets', 'satellite'] const defaultToken = 'your_token' @@ -197,8 +199,19 @@ class Map extends React.Component { onClusterSelect (e) { const { id } = e.target const { longitude, latitude } = e.target.attributes + + // const clusterLeaves = this.index.getLeaves(parseInt(id), Infinity, 0) + // const leavesToLocations = mapClustersToLocations(clusterLeaves, this.props.domain.locations) + + // const locationEvents = leavesToLocations.reduce((acc, loc) => { + // loc.events.forEach(evt => acc.push(evt)) + // return acc + // }, []) + const expansionZoom = Math.max(this.index.getClusterExpansionZoom(parseInt(id)), this.index.options.minZoom) this.map.flyTo(new L.LatLng(latitude.value, longitude.value), expansionZoom) + + // this.props.methods.onSelect(locationEvents) } getClientDims () { @@ -282,7 +295,7 @@ class Map extends React.Component { renderEvents () { const individualClusters = this.state.clusters.filter(cl => !cl.properties.cluster) - const filteredLocations = individualClusters.map(cl => this.props.domain.locations.find(location => location.label === cl.properties.id)) + const filteredLocations = mapClustersToLocations(individualClusters, this.props.domain.locations) return ( Date: Tue, 6 Oct 2020 10:19:09 -0700 Subject: [PATCH 11/26] Removed logic for selecting all cards on cluster select; putting out PR to get feedback --- src/components/Map.jsx | 11 ----------- src/components/Timeline.jsx | 1 - src/components/presentational/Timeline/Events.js | 2 +- src/store/initial.js | 2 +- 4 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 8940d17..dfc9743 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -199,19 +199,8 @@ class Map extends React.Component { onClusterSelect (e) { const { id } = e.target const { longitude, latitude } = e.target.attributes - - // const clusterLeaves = this.index.getLeaves(parseInt(id), Infinity, 0) - // const leavesToLocations = mapClustersToLocations(clusterLeaves, this.props.domain.locations) - - // const locationEvents = leavesToLocations.reduce((acc, loc) => { - // loc.events.forEach(evt => acc.push(evt)) - // return acc - // }, []) - const expansionZoom = Math.max(this.index.getClusterExpansionZoom(parseInt(id)), this.index.options.minZoom) this.map.flyTo(new L.LatLng(latitude.value, longitude.value), expansionZoom) - - // this.props.methods.onSelect(locationEvents) } getClientDims () { diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index 61e91aa..8c6adef 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -304,7 +304,6 @@ class Timeline extends React.Component { const extraStyle = { ...heightStyle, ...foldedStyle } const contentHeight = { height: dims.contentHeight } const { categories } = this.props.domain - return (
onSelect(event), dims, - highlights: features.HIGHLIGHT_GROUPS ? getHighlights(event.filters[features.HIGHLIGHT_GROUPS.filterIndexIndicatingGroup]) : [], + highlights: features.HIGHLIGHT_GROUPS ? getHighlights(event.associations[features.HIGHLIGHT_GROUPS.filterIndexIndicatingGroup]) : [], features }) } diff --git a/src/store/initial.js b/src/store/initial.js index 9e38b0e..ade50e9 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -53,7 +53,7 @@ const initial = { maxZoom: 20, bounds: null, maxBounds: [[180, -180], [-180, 180]], - clusterRadius: 20, + clusterRadius: 30, }, timeline: { dimensions: { From 9dd5eabd3ad3f84389071df20a5d2f594d3cc8f5 Mon Sep 17 00:00:00 2001 From: efarooqui Date: Tue, 6 Oct 2020 10:31:17 -0700 Subject: [PATCH 12/26] Linting fixes --- src/common/utilities.js | 12 ++-- src/components/Layout.js | 4 +- src/components/Map.jsx | 5 +- src/components/initialization.js | 69 ------------------- .../presentational/Map/Clusters.jsx | 5 +- src/store/initial.js | 2 +- 6 files changed, 13 insertions(+), 84 deletions(-) delete mode 100644 src/components/initialization.js diff --git a/src/common/utilities.js b/src/common/utilities.js index 78d3443..4c74d30 100644 --- a/src/common/utilities.js +++ b/src/common/utilities.js @@ -172,16 +172,16 @@ export function calcClusterSize (pointCount, numClusters) { return Math.min(50, 10 + (pointCount / numClusters) * 10) } -export function isLatitude(lat) { - return !!lat && isFinite(lat) && Math.abs(lat) <= 90; +export function isLatitude (lat) { + return !!lat && isFinite(lat) && Math.abs(lat) <= 90 } -export function isLongitude(lng) { - return !!lng && isFinite(lng) && Math.abs(lng) <= 180; +export function isLongitude (lng) { + return !!lng && isFinite(lng) && Math.abs(lng) <= 180 } -export function mapClustersToLocations(clusters, locations) { - return clusters.map(cl => locations.find(location => location.label === cl.properties.id)) +export function mapClustersToLocations (clusters, locations) { + return clusters.map(cl => locations.find(location => location.label === cl.properties.id)) } export const dateMin = function () { diff --git a/src/components/Layout.js b/src/components/Layout.js index 5930283..b6ea66f 100644 --- a/src/components/Layout.js +++ b/src/components/Layout.js @@ -259,7 +259,7 @@ class Dashboard extends React.Component { } return ( -
+
' + 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 diff --git a/src/components/presentational/Map/Clusters.jsx b/src/components/presentational/Map/Clusters.jsx index f7164ab..ad441b1 100644 --- a/src/components/presentational/Map/Clusters.jsx +++ b/src/components/presentational/Map/Clusters.jsx @@ -9,7 +9,7 @@ function ClusterEvents ({ onSelect, svg, clusters, - numClusters, + numClusters }) { function renderClusterBySize (cluster) { const { point_count: pointCount, cluster_id: clusterId } = cluster.properties @@ -20,7 +20,7 @@ function ClusterEvents ({ fill: colors.fallbackEventColor, stroke: colors.darkBackground, strokeWidth: 0, - fillOpacity: calcOpacity(pointCount), + fillOpacity: calcOpacity(pointCount) }) return ( @@ -39,7 +39,6 @@ function ClusterEvents ({ ) } - function renderCluster (cluster) { /** { diff --git a/src/store/initial.js b/src/store/initial.js index ade50e9..b8080c4 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -53,7 +53,7 @@ const initial = { maxZoom: 20, bounds: null, maxBounds: [[180, -180], [-180, 180]], - clusterRadius: 30, + clusterRadius: 30 }, timeline: { dimensions: { From c86c4fba0238a6dfd56cd8f28c5472387e2f5823 Mon Sep 17 00:00:00 2001 From: efarooqui Date: Tue, 6 Oct 2020 10:34:29 -0700 Subject: [PATCH 13/26] Removing commented out sections --- src/scss/map.scss | 2 -- src/store/initial.js | 1 - 2 files changed, 3 deletions(-) diff --git a/src/scss/map.scss b/src/scss/map.scss index 9d23009..bc33fc4 100644 --- a/src/scss/map.scss +++ b/src/scss/map.scss @@ -191,8 +191,6 @@ .cluster-event-marker { pointer-events: all !important; - // fill: $event_default; - // stroke-width: 0; &.red { fill: red; diff --git a/src/store/initial.js b/src/store/initial.js index b8080c4..7aa4090 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -11,7 +11,6 @@ const initial = { */ domain: { events: [], - // locations: [], categories: [], associations: [], sources: {}, From 3dc13f334ba43fa55124d859df3cbc2be34adf51 Mon Sep 17 00:00:00 2001 From: efarooqui Date: Wed, 7 Oct 2020 10:19:03 -0700 Subject: [PATCH 14/26] Working filters with added check to make sure empty locations dont create clusters --- src/common/utilities.js | 6 +++++- src/components/Map.jsx | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/common/utilities.js b/src/common/utilities.js index 4c74d30..3e094ef 100644 --- a/src/common/utilities.js +++ b/src/common/utilities.js @@ -181,7 +181,11 @@ export function isLongitude (lng) { } export function mapClustersToLocations (clusters, locations) { - return clusters.map(cl => locations.find(location => location.label === cl.properties.id)) + return clusters.reduce((acc, cl) => { + const foundLocation = locations.find(location => location.label === cl.properties.id) + if (foundLocation) acc.push(foundLocation) + return acc + }, []) } export const dateMin = function () { diff --git a/src/components/Map.jsx b/src/components/Map.jsx index aa9c5d0..c0ab746 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -144,7 +144,7 @@ class Map extends React.Component { } loadClusterData (locations) { - if (locations && locations.length !== 0 && this.index) { + if (locations && locations.length > 0 && this.index) { const convertedLocations = locations.reduce((acc, loc) => { const { longitude, latitude } = loc const validCoordinates = !!latitude && !!longitude @@ -167,6 +167,8 @@ class Map extends React.Component { this.index.load(convertedLocations) this.setState({ indexLoaded: true }) this.update() + } else { + this.setState({ clusters: [] }) } } From d649ba99ed333f4494949560a9b408b8cf1e8a1b Mon Sep 17 00:00:00 2001 From: efarooqui Date: Wed, 7 Oct 2020 17:41:09 -0700 Subject: [PATCH 15/26] Configuring maxbounds in config to allow pan to not snap out of center --- src/components/Map.jsx | 1 + src/store/initial.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Map.jsx b/src/components/Map.jsx index c0ab746..9271841 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -78,6 +78,7 @@ class Map extends React.Component { * Creates a Leaflet map and a tilelayer for the map background */ const { map: mapConf } = this.props.app + console.info(mapConf.maxBounds) const map = L.map(this.props.ui.dom.map) .setView(mapConf.anchor, mapConf.startZoom) diff --git a/src/store/initial.js b/src/store/initial.js index 7aa4090..8c2cfd6 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -48,8 +48,8 @@ const initial = { map: { anchor: [31.356397, 34.784818], startZoom: 11, - minZoom: 0, - maxZoom: 20, + minZoom: 2, + maxZoom: 16, bounds: null, maxBounds: [[180, -180], [-180, 180]], clusterRadius: 30 From b5bb7472acd316eb33333ac2f84f04e87606f6fc Mon Sep 17 00:00:00 2001 From: efarooqui Date: Tue, 13 Oct 2020 11:04:07 -0700 Subject: [PATCH 16/26] Rewrote opacity alg for clusters --- src/common/utilities.js | 8 ++++++-- src/components/Map.jsx | 3 +-- .../presentational/Map/Clusters.jsx | 20 ++++++++++++++----- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/common/utilities.js b/src/common/utilities.js index 3e094ef..459083d 100644 --- a/src/common/utilities.js +++ b/src/common/utilities.js @@ -168,8 +168,12 @@ export function calcOpacity (num) { return base + (Math.min(0.5, 0.08 * (num - 1))) } -export function calcClusterSize (pointCount, numClusters) { - return Math.min(50, 10 + (pointCount / numClusters) * 10) +export function calcClusterOpacity (pointCount, totalPoints) { + return Math.min(0.8, 0.08 + (pointCount / totalPoints) * 50) +} + +export function calcClusterSize (pointCount, totalPoints) { + return Math.min(50, 10 + (pointCount / totalPoints) * 75) } export function isLatitude (lat) { diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 9271841..febedd6 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -78,7 +78,7 @@ class Map extends React.Component { * Creates a Leaflet map and a tilelayer for the map background */ const { map: mapConf } = this.props.app - console.info(mapConf.maxBounds) + const map = L.map(this.props.ui.dom.map) .setView(mapConf.anchor, mapConf.startZoom) @@ -312,7 +312,6 @@ class Map extends React.Component { styleCluster={this.styleCluster} projectPoint={this.projectPoint} clusters={allClusters} - numClusters={allClusters.length} onSelect={this.onClusterSelect} /> ) diff --git a/src/components/presentational/Map/Clusters.jsx b/src/components/presentational/Map/Clusters.jsx index ad441b1..5a2b414 100644 --- a/src/components/presentational/Map/Clusters.jsx +++ b/src/components/presentational/Map/Clusters.jsx @@ -1,26 +1,36 @@ import React from 'react' import { Portal } from 'react-portal' import colors from '../../../common/global.js' -import { calcOpacity, calcClusterSize } from '../../../common/utilities' +import { calcClusterOpacity, calcClusterSize } from '../../../common/utilities' function ClusterEvents ({ projectPoint, styleCluster, onSelect, svg, - clusters, - numClusters + clusters }) { + function calculateTotalPoints () { + return clusters.reduce((total, cl) => { + if (cl && cl.properties) { + total += cl.properties.point_count + } + return total + }, 0) + } + function renderClusterBySize (cluster) { const { point_count: pointCount, cluster_id: clusterId } = cluster.properties const { coordinates } = cluster.geometry const [longitude, latitude] = coordinates + const totalPoints = calculateTotalPoints() + const styles = ({ fill: colors.fallbackEventColor, stroke: colors.darkBackground, strokeWidth: 0, - fillOpacity: calcOpacity(pointCount) + fillOpacity: calcClusterOpacity(pointCount, totalPoints) }) return ( @@ -32,7 +42,7 @@ function ClusterEvents ({ latitude={latitude} cx='0' cy='0' - r={calcClusterSize(pointCount, numClusters)} + r={calcClusterSize(pointCount, totalPoints)} style={styles} />} From 32da7f01059205694ac163d5984ed89793fb65f9 Mon Sep 17 00:00:00 2001 From: efarooqui Date: Wed, 14 Oct 2020 21:05:58 -0700 Subject: [PATCH 17/26] Modified cluster styling to have gradients --- src/components/Map.jsx | 10 ++++++++++ src/components/presentational/Map/Clusters.jsx | 9 +++++---- src/components/presentational/Map/DefsClusters.jsx | 13 +++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 src/components/presentational/Map/DefsClusters.jsx diff --git a/src/components/Map.jsx b/src/components/Map.jsx index febedd6..9c78ca5 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -16,6 +16,7 @@ import Clusters from './presentational/Map/Clusters.jsx' import SelectedEvents from './presentational/Map/SelectedEvents.jsx' import Narratives from './presentational/Map/Narratives' import DefsMarkers from './presentational/Map/DefsMarkers.jsx' +import DefsClusters from './presentational/Map/DefsClusters.jsx' import { mapClustersToLocations } from '../common/utilities' @@ -328,6 +329,14 @@ class Map extends React.Component { ) } + renderClusterGradients () { + return ( + + + + ) + } + renderMarkers () { return ( @@ -343,6 +352,7 @@ class Map extends React.Component { {this.renderTiles()} {this.renderMarkers()} + {this.renderClusterGradients()} {isShowingSites ? this.renderSites() : null} {this.renderShapes()} {this.renderNarratives()} diff --git a/src/components/presentational/Map/Clusters.jsx b/src/components/presentational/Map/Clusters.jsx index 5a2b414..2e55de3 100644 --- a/src/components/presentational/Map/Clusters.jsx +++ b/src/components/presentational/Map/Clusters.jsx @@ -27,10 +27,10 @@ function ClusterEvents ({ const totalPoints = calculateTotalPoints() const styles = ({ - fill: colors.fallbackEventColor, - stroke: colors.darkBackground, - strokeWidth: 0, - fillOpacity: calcClusterOpacity(pointCount, totalPoints) + // fill: colors.fallbackEventColor, + // stroke: colors.darkBackground, + // strokeWidth: 0, + fillOpacity: calcClusterOpacity(pointCount, totalPoints), }) return ( @@ -44,6 +44,7 @@ function ClusterEvents ({ cy='0' r={calcClusterSize(pointCount, totalPoints)} style={styles} + fill="url('#myGradient')" />} ) diff --git a/src/components/presentational/Map/DefsClusters.jsx b/src/components/presentational/Map/DefsClusters.jsx new file mode 100644 index 0000000..146966e --- /dev/null +++ b/src/components/presentational/Map/DefsClusters.jsx @@ -0,0 +1,13 @@ +import React from 'react' + +const DefsClusters = () => ( + + + + + {/* */} + + +) + +export default DefsClusters From 2aaa1752ad8d6764640cb4f764ccf0290125d561 Mon Sep 17 00:00:00 2001 From: efarooqui Date: Wed, 14 Oct 2020 22:27:50 -0700 Subject: [PATCH 18/26] Changed back to normal opacities instead of radial gradients; could add a var that's read from the config to determine styling: radial gradient || normal opacity --- src/common/utilities.js | 4 ++-- src/components/presentational/Map/Clusters.jsx | 8 ++++---- src/components/presentational/Map/DefsClusters.jsx | 7 +++---- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/common/utilities.js b/src/common/utilities.js index 459083d..898ee56 100644 --- a/src/common/utilities.js +++ b/src/common/utilities.js @@ -169,11 +169,11 @@ export function calcOpacity (num) { } export function calcClusterOpacity (pointCount, totalPoints) { - return Math.min(0.8, 0.08 + (pointCount / totalPoints) * 50) + return Math.min(0.85, 0.08 + (pointCount / totalPoints) * 50) } export function calcClusterSize (pointCount, totalPoints) { - return Math.min(50, 10 + (pointCount / totalPoints) * 75) + return Math.min(50, 10 + (pointCount / totalPoints) * 250) } export function isLatitude (lat) { diff --git a/src/components/presentational/Map/Clusters.jsx b/src/components/presentational/Map/Clusters.jsx index 2e55de3..6da97ef 100644 --- a/src/components/presentational/Map/Clusters.jsx +++ b/src/components/presentational/Map/Clusters.jsx @@ -27,9 +27,9 @@ function ClusterEvents ({ const totalPoints = calculateTotalPoints() const styles = ({ - // fill: colors.fallbackEventColor, - // stroke: colors.darkBackground, - // strokeWidth: 0, + fill: colors.fallbackEventColor, + stroke: colors.darkBackground, + strokeWidth: 0, fillOpacity: calcClusterOpacity(pointCount, totalPoints), }) @@ -44,7 +44,7 @@ function ClusterEvents ({ cy='0' r={calcClusterSize(pointCount, totalPoints)} style={styles} - fill="url('#myGradient')" + // fill="url('#clusterGradient')" />} ) diff --git a/src/components/presentational/Map/DefsClusters.jsx b/src/components/presentational/Map/DefsClusters.jsx index 146966e..91ed1ac 100644 --- a/src/components/presentational/Map/DefsClusters.jsx +++ b/src/components/presentational/Map/DefsClusters.jsx @@ -2,10 +2,9 @@ import React from 'react' const DefsClusters = () => ( - - - - {/* */} + + + ) From 3bf80bce00f27fca7fe7475b965f82fdc4a5c28d Mon Sep 17 00:00:00 2001 From: efarooqui Date: Thu, 15 Oct 2020 09:23:19 -0700 Subject: [PATCH 19/26] Modified clusters to take in radial gradient as input if specified --- src/common/utilities.js | 2 +- src/components/Map.jsx | 6 ++++-- src/components/presentational/Map/Clusters.jsx | 4 ++-- src/components/presentational/Map/DefsClusters.jsx | 2 +- src/store/initial.js | 3 +++ 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/common/utilities.js b/src/common/utilities.js index 898ee56..2fb70f9 100644 --- a/src/common/utilities.js +++ b/src/common/utilities.js @@ -173,7 +173,7 @@ export function calcClusterOpacity (pointCount, totalPoints) { } export function calcClusterSize (pointCount, totalPoints) { - return Math.min(50, 10 + (pointCount / totalPoints) * 250) + return Math.min(50, 10 + (pointCount / totalPoints) * 150) } export function isLatitude (lat) { diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 9c78ca5..89e2c91 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -313,6 +313,7 @@ class Map extends React.Component { styleCluster={this.styleCluster} projectPoint={this.projectPoint} clusters={allClusters} + isRadial={this.props.ui.radial} onSelect={this.onClusterSelect} /> ) @@ -352,7 +353,7 @@ class Map extends React.Component { {this.renderTiles()} {this.renderMarkers()} - {this.renderClusterGradients()} + {this.props.ui.radial ? this.renderClusterGradients(): null} {isShowingSites ? this.renderSites() : null} {this.renderShapes()} {this.renderNarratives()} @@ -399,7 +400,8 @@ function mapStateToProps (state) { narratives: state.ui.style.narratives, mapSelectedEvents: state.ui.style.selectedEvents, shapes: state.ui.style.shapes, - eventRadius: state.ui.eventRadius + eventRadius: state.ui.eventRadius, + radial: state.ui.style.clusters.radial }, features: selectors.getFeatures(state) } diff --git a/src/components/presentational/Map/Clusters.jsx b/src/components/presentational/Map/Clusters.jsx index 6da97ef..cbd8cc4 100644 --- a/src/components/presentational/Map/Clusters.jsx +++ b/src/components/presentational/Map/Clusters.jsx @@ -7,6 +7,7 @@ function ClusterEvents ({ projectPoint, styleCluster, onSelect, + isRadial, svg, clusters }) { @@ -27,7 +28,7 @@ function ClusterEvents ({ const totalPoints = calculateTotalPoints() const styles = ({ - fill: colors.fallbackEventColor, + fill: isRadial ? "url('#clusterGradient')" : colors.fallbackEventColor, stroke: colors.darkBackground, strokeWidth: 0, fillOpacity: calcClusterOpacity(pointCount, totalPoints), @@ -44,7 +45,6 @@ function ClusterEvents ({ cy='0' r={calcClusterSize(pointCount, totalPoints)} style={styles} - // fill="url('#clusterGradient')" />} ) diff --git a/src/components/presentational/Map/DefsClusters.jsx b/src/components/presentational/Map/DefsClusters.jsx index 91ed1ac..577ab42 100644 --- a/src/components/presentational/Map/DefsClusters.jsx +++ b/src/components/presentational/Map/DefsClusters.jsx @@ -4,7 +4,7 @@ const DefsClusters = () => ( - + ) diff --git a/src/store/initial.js b/src/store/initial.js index 8c2cfd6..eeeed43 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -121,6 +121,9 @@ const initial = { strokeWidth: 3, opacity: 0.9 } + }, + clusters: { + radial: false } }, dom: { From 9200019d1e60d81d1bab50d945ffdc0eb68a673b Mon Sep 17 00:00:00 2001 From: efarooqui Date: Mon, 19 Oct 2020 09:11:08 -0700 Subject: [PATCH 20/26] Linting fixes --- src/components/Map.jsx | 2 +- src/components/presentational/Map/Clusters.jsx | 2 +- src/components/presentational/Map/DefsClusters.jsx | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 89e2c91..46aa135 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -353,7 +353,7 @@ class Map extends React.Component { {this.renderTiles()} {this.renderMarkers()} - {this.props.ui.radial ? this.renderClusterGradients(): null} + {this.props.ui.radial ? this.renderClusterGradients() : null} {isShowingSites ? this.renderSites() : null} {this.renderShapes()} {this.renderNarratives()} diff --git a/src/components/presentational/Map/Clusters.jsx b/src/components/presentational/Map/Clusters.jsx index cbd8cc4..62ccaf9 100644 --- a/src/components/presentational/Map/Clusters.jsx +++ b/src/components/presentational/Map/Clusters.jsx @@ -31,7 +31,7 @@ function ClusterEvents ({ fill: isRadial ? "url('#clusterGradient')" : colors.fallbackEventColor, stroke: colors.darkBackground, strokeWidth: 0, - fillOpacity: calcClusterOpacity(pointCount, totalPoints), + fillOpacity: calcClusterOpacity(pointCount, totalPoints) }) return ( diff --git a/src/components/presentational/Map/DefsClusters.jsx b/src/components/presentational/Map/DefsClusters.jsx index 577ab42..1cf264b 100644 --- a/src/components/presentational/Map/DefsClusters.jsx +++ b/src/components/presentational/Map/DefsClusters.jsx @@ -2,9 +2,9 @@ import React from 'react' const DefsClusters = () => ( - - - + + + ) From c310d15579f021e134c4479f595cf4d674ef8db0 Mon Sep 17 00:00:00 2001 From: efarooqui Date: Mon, 19 Oct 2020 13:32:35 -0700 Subject: [PATCH 21/26] Created utility function isIdentical; moved DefsClusters into Clusters file --- src/common/utilities.js | 12 ++++++ src/components/Map.jsx | 39 +++++++------------ .../presentational/Map/Clusters.jsx | 14 ++++++- .../presentational/Map/DefsClusters.jsx | 12 ------ 4 files changed, 38 insertions(+), 39 deletions(-) delete mode 100644 src/components/presentational/Map/DefsClusters.jsx diff --git a/src/common/utilities.js b/src/common/utilities.js index 2fb70f9..ad76526 100644 --- a/src/common/utilities.js +++ b/src/common/utilities.js @@ -1,4 +1,6 @@ import moment from 'moment' +import hash from 'object-hash' + let { DATE_FMT, TIME_FMT } = process.env if (!DATE_FMT) DATE_FMT = 'MM/DD/YYYY' @@ -159,6 +161,10 @@ export function selectTypeFromPathWithPoster (path, poster) { return { type: typeForPath(path), path, poster } } +export function isIdentical (obj1, obj2) { + return hash(obj1) === hash(obj2) +} + export function calcOpacity (num) { /* Events have opacity 0.5 by default, and get added to according to how many * other events there are in the same render. The idea here is that the @@ -169,10 +175,16 @@ export function calcOpacity (num) { } export function calcClusterOpacity (pointCount, totalPoints) { + /* Clusters represent multiple events within a specific radius. The darker the cluster, + the larger the number of underlying events. We use a multiplication factor (50) here as well + to ensure that the larger clusters have an appropriately darker shading. */ return Math.min(0.85, 0.08 + (pointCount / totalPoints) * 50) } export function calcClusterSize (pointCount, totalPoints) { + /* The larger the cluster size, the higher the count of points that the cluster represents. + Just like with opacity, we use a multiplication factor to ensure that clusters with higher point + counts appear larger. */ return Math.min(50, 10 + (pointCount / totalPoints) * 150) } diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 46aa135..231a7a2 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -6,7 +6,6 @@ import Supercluster from 'supercluster' import { connect } from 'react-redux' import * as selectors from '../selectors' -import hash from 'object-hash' import 'leaflet' import Sites from './presentational/Map/Sites.jsx' @@ -16,9 +15,9 @@ import Clusters from './presentational/Map/Clusters.jsx' import SelectedEvents from './presentational/Map/SelectedEvents.jsx' import Narratives from './presentational/Map/Narratives' import DefsMarkers from './presentational/Map/DefsMarkers.jsx' -import DefsClusters from './presentational/Map/DefsClusters.jsx' +import LoadingOverlay from '../components/Overlay/Loading' -import { mapClustersToLocations } from '../common/utilities' +import { mapClustersToLocations, isIdentical } from '../common/utilities' // NB: important constants for map, TODO: make statics const supportedMapboxMap = ['streets', 'satellite'] @@ -31,7 +30,7 @@ class Map extends React.Component { this.onClusterSelect = this.onClusterSelect.bind(this) this.svgRef = React.createRef() this.map = null - this.index = null + this.superclusterIndex = null this.state = { mapTransformX: 0, mapTransformY: 0, @@ -48,16 +47,16 @@ class Map extends React.Component { } componentWillReceiveProps (nextProps) { - if (hash(nextProps.domain.locations) !== hash(this.props.domain.locations)) { + if (!isIdentical(nextProps.domain.locations, this.props.domain.locations)) { this.loadClusterData(nextProps.domain.locations) } // Set appropriate zoom for narrative const { bounds } = nextProps.app.map - if (hash(bounds) !== hash(this.props.app.map.bounds) && + if (!isIdentical(bounds, this.props.app.map.bounds) && bounds !== null) { this.map.fitBounds(bounds) } else { - if (hash(nextProps.app.selected) !== hash(this.props.app.selected)) { + if (!isIdentical(nextProps.app.selected, this.props.app.selected)) { // Fly to first of events selected const eventPoint = (nextProps.app.selected.length > 0) ? nextProps.app.selected[0] : null @@ -88,14 +87,12 @@ class Map extends React.Component { .setMaxBounds(mapConf.maxBounds) // Initialize supercluster index - const index = new Supercluster({ + this.superclusterIndex = new Supercluster({ radius: mapConf.clusterRadius, maxZoom: mapConf.maxZoom, minZoom: mapConf.minZoom }) - this.index = index - let firstLayer if ((supportedMapboxMap.indexOf(this.props.ui.tiles) !== -1) && process.env.MAPBOX_TOKEN && process.env.MAPBOX_TOKEN !== defaultToken) { @@ -138,15 +135,15 @@ class Map extends React.Component { update () { const [bbox, zoom] = this.getMapDetails() - if (this.index && this.state.indexLoaded) { + if (this.superclusterIndex && this.state.indexLoaded) { this.setState({ - clusters: this.index.getClusters(bbox, zoom) + clusters: this.superclusterIndex.getClusters(bbox, zoom) }) } } loadClusterData (locations) { - if (locations && locations.length > 0 && this.index) { + if (locations && locations.length > 0 && this.superclusterIndex) { const convertedLocations = locations.reduce((acc, loc) => { const { longitude, latitude } = loc const validCoordinates = !!latitude && !!longitude @@ -166,7 +163,7 @@ class Map extends React.Component { } return acc }, []) - this.index.load(convertedLocations) + this.superclusterIndex.load(convertedLocations) this.setState({ indexLoaded: true }) this.update() } else { @@ -202,7 +199,7 @@ class Map extends React.Component { onClusterSelect (e) { const { id } = e.target const { longitude, latitude } = e.target.attributes - const expansionZoom = Math.max(this.index.getClusterExpansionZoom(parseInt(id)), this.index.options.minZoom) + const expansionZoom = Math.max(this.superclusterIndex.getClusterExpansionZoom(parseInt(id)), this.superclusterIndex.options.minZoom) this.map.flyTo(new L.LatLng(latitude.value, longitude.value), expansionZoom) } @@ -330,14 +327,6 @@ class Map extends React.Component { ) } - renderClusterGradients () { - return ( - - - - ) - } - renderMarkers () { return ( @@ -353,7 +342,6 @@ class Map extends React.Component { {this.renderTiles()} {this.renderMarkers()} - {this.props.ui.radial ? this.renderClusterGradients() : null} {isShowingSites ? this.renderSites() : null} {this.renderShapes()} {this.renderNarratives()} @@ -391,7 +379,8 @@ function mapStateToProps (state) { map: state.app.map, narrative: state.app.associations.narrative, flags: { - isShowingSites: state.app.flags.isShowingSites + isShowingSites: state.app.flags.isShowingSites, + isFetchingDomain: state.app.flags.isFetchingDomain } }, ui: { diff --git a/src/components/presentational/Map/Clusters.jsx b/src/components/presentational/Map/Clusters.jsx index 62ccaf9..0f59688 100644 --- a/src/components/presentational/Map/Clusters.jsx +++ b/src/components/presentational/Map/Clusters.jsx @@ -3,6 +3,15 @@ import { Portal } from 'react-portal' import colors from '../../../common/global.js' import { calcClusterOpacity, calcClusterSize } from '../../../common/utilities' +const DefsClusters = () => ( + + + + + + +) + function ClusterEvents ({ projectPoint, styleCluster, @@ -27,12 +36,12 @@ function ClusterEvents ({ const totalPoints = calculateTotalPoints() - const styles = ({ + const styles = { fill: isRadial ? "url('#clusterGradient')" : colors.fallbackEventColor, stroke: colors.darkBackground, strokeWidth: 0, fillOpacity: calcClusterOpacity(pointCount, totalPoints) - }) + } return ( @@ -92,6 +101,7 @@ function ClusterEvents ({ return ( + {isRadial ? : null} {clusters.map(renderCluster)} diff --git a/src/components/presentational/Map/DefsClusters.jsx b/src/components/presentational/Map/DefsClusters.jsx deleted file mode 100644 index 1cf264b..0000000 --- a/src/components/presentational/Map/DefsClusters.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' - -const DefsClusters = () => ( - - - - - - -) - -export default DefsClusters From 7dee3dacd1eb63581dfe5a2fd29b796801866315 Mon Sep 17 00:00:00 2001 From: efarooqui Date: Mon, 19 Oct 2020 13:33:42 -0700 Subject: [PATCH 22/26] Some linting fixes --- src/common/utilities.js | 9 ++++----- src/components/Map.jsx | 1 - 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/common/utilities.js b/src/common/utilities.js index ad76526..371509e 100644 --- a/src/common/utilities.js +++ b/src/common/utilities.js @@ -1,7 +1,6 @@ import moment from 'moment' import hash from 'object-hash' - let { DATE_FMT, TIME_FMT } = process.env if (!DATE_FMT) DATE_FMT = 'MM/DD/YYYY' if (!TIME_FMT) TIME_FMT = 'HH:mm' @@ -175,16 +174,16 @@ export function calcOpacity (num) { } export function calcClusterOpacity (pointCount, totalPoints) { - /* Clusters represent multiple events within a specific radius. The darker the cluster, + /* Clusters represent multiple events within a specific radius. The darker the cluster, the larger the number of underlying events. We use a multiplication factor (50) here as well to ensure that the larger clusters have an appropriately darker shading. */ return Math.min(0.85, 0.08 + (pointCount / totalPoints) * 50) } export function calcClusterSize (pointCount, totalPoints) { - /* The larger the cluster size, the higher the count of points that the cluster represents. - Just like with opacity, we use a multiplication factor to ensure that clusters with higher point - counts appear larger. */ + /* The larger the cluster size, the higher the count of points that the cluster represents. + Just like with opacity, we use a multiplication factor to ensure that clusters with higher point + counts appear larger. */ return Math.min(50, 10 + (pointCount / totalPoints) * 150) } diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 231a7a2..f4a22d1 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -15,7 +15,6 @@ import Clusters from './presentational/Map/Clusters.jsx' import SelectedEvents from './presentational/Map/SelectedEvents.jsx' import Narratives from './presentational/Map/Narratives' import DefsMarkers from './presentational/Map/DefsMarkers.jsx' -import LoadingOverlay from '../components/Overlay/Loading' import { mapClustersToLocations, isIdentical } from '../common/utilities' From 70f89a2a5df152cf59c8ca95a93a0f0bb9708e9f Mon Sep 17 00:00:00 2001 From: efarooqui Date: Mon, 19 Oct 2020 15:29:39 -0700 Subject: [PATCH 23/26] Injecting loading overlay into map for cases where domain hasnt been loaded yet --- src/components/Map.jsx | 10 +++++++++- src/components/Overlay/Loading.js | 1 - 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/Map.jsx b/src/components/Map.jsx index f4a22d1..f2fe486 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -15,6 +15,7 @@ import Clusters from './presentational/Map/Clusters.jsx' import SelectedEvents from './presentational/Map/SelectedEvents.jsx' import Narratives from './presentational/Map/Narratives' import DefsMarkers from './presentational/Map/DefsMarkers.jsx' +import LoadingOverlay from '../components/Overlay/Loading' import { mapClustersToLocations, isIdentical } from '../common/utilities' @@ -335,7 +336,7 @@ class Map extends React.Component { } render () { - const { isShowingSites } = this.props.app.flags + const { isShowingSites, isFetchingDomain } = this.props.app.flags const classes = this.props.app.narrative ? 'map-wrapper narrative-mode' : 'map-wrapper' const innerMap = this.map ? ( @@ -356,6 +357,11 @@ class Map extends React.Component { tabIndex='0' >
+ {innerMap}
) @@ -376,6 +382,8 @@ function mapStateToProps (state) { selected: selectors.selectSelected(state), highlighted: state.app.highlighted, map: state.app.map, + language: state.app.language, + loading: state.app.loading, narrative: state.app.associations.narrative, flags: { isShowingSites: state.app.flags.isShowingSites, diff --git a/src/components/Overlay/Loading.js b/src/components/Overlay/Loading.js index 37bd110..c678add 100644 --- a/src/components/Overlay/Loading.js +++ b/src/components/Overlay/Loading.js @@ -4,7 +4,6 @@ import copy from '../../common/data/copy.json' const LoadingOverlay = ({ isLoading, language }) => { let classes = 'loading-overlay' classes += (!isLoading) ? ' hidden' : '' - return (
From e03fc472e04f5ca34998d573b219b03ece97d3d5 Mon Sep 17 00:00:00 2001 From: efarooqui Date: Mon, 19 Oct 2020 21:00:31 -0700 Subject: [PATCH 24/26] Removed line --- src/components/Overlay/Loading.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Overlay/Loading.js b/src/components/Overlay/Loading.js index c678add..37bd110 100644 --- a/src/components/Overlay/Loading.js +++ b/src/components/Overlay/Loading.js @@ -4,6 +4,7 @@ import copy from '../../common/data/copy.json' const LoadingOverlay = ({ isLoading, language }) => { let classes = 'loading-overlay' classes += (!isLoading) ? ' hidden' : '' + return (
From cd19910b064e6aa4bf8c45eea1f4a51dff632dc4 Mon Sep 17 00:00:00 2001 From: efarooqui Date: Tue, 20 Oct 2020 13:28:58 -0700 Subject: [PATCH 25/26] Loads index when locations are finally available in props; added through componentDidUpdate method --- src/common/utilities.js | 1 - src/components/Map.jsx | 38 +++++++++++++++++++++++--------------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/common/utilities.js b/src/common/utilities.js index 371509e..fc52cf1 100644 --- a/src/common/utilities.js +++ b/src/common/utilities.js @@ -76,7 +76,6 @@ export function insetSourceFrom (allSources) { sources = [] } else { sources = event.sources.map(id => { - // const id = typeof src === 'object' ? src.id : src return allSources.hasOwnProperty(id) ? allSources[id] : null }) } diff --git a/src/components/Map.jsx b/src/components/Map.jsx index f2fe486..dd42c96 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -17,7 +17,7 @@ import Narratives from './presentational/Map/Narratives' import DefsMarkers from './presentational/Map/DefsMarkers.jsx' import LoadingOverlay from '../components/Overlay/Loading' -import { mapClustersToLocations, isIdentical } from '../common/utilities' +import { mapClustersToLocations, isIdentical, isLatitude, isLongitude } from '../common/utilities' // NB: important constants for map, TODO: make statics const supportedMapboxMap = ['streets', 'satellite'] @@ -28,6 +28,7 @@ class Map extends React.Component { super() this.projectPoint = this.projectPoint.bind(this) this.onClusterSelect = this.onClusterSelect.bind(this) + this.loadClusterData = this.loadClusterData.bind(this) this.svgRef = React.createRef() this.map = null this.superclusterIndex = null @@ -73,24 +74,30 @@ class Map extends React.Component { } } + componentDidUpdate (prevState, prevProps) { + if (prevState.domain.locations.length > 0 && this.state.clusters.length === 0) { + this.loadClusterData(prevState.domain.locations) + } + } + initializeMap () { /** * Creates a Leaflet map and a tilelayer for the map background */ - const { map: mapConf } = this.props.app + const { map: mapConfig } = this.props.app const map = L.map(this.props.ui.dom.map) - .setView(mapConf.anchor, mapConf.startZoom) - .setMinZoom(mapConf.minZoom) - .setMaxZoom(mapConf.maxZoom) - .setMaxBounds(mapConf.maxBounds) + .setView(mapConfig.anchor, mapConfig.startZoom) + .setMinZoom(mapConfig.minZoom) + .setMaxZoom(mapConfig.maxZoom) + .setMaxBounds(mapConfig.maxBounds) // Initialize supercluster index this.superclusterIndex = new Supercluster({ - radius: mapConf.clusterRadius, - maxZoom: mapConf.maxZoom, - minZoom: mapConf.minZoom + radius: mapConfig.clusterRadius, + maxZoom: mapConfig.maxZoom, + minZoom: mapConfig.minZoom }) let firstLayer @@ -114,10 +121,10 @@ class Map extends React.Component { map.zoomControl.remove() map.on('moveend', () => { - this.update() + this.updateClusters() this.alignLayers() }) - map.on('load', () => this.update()) + map.on('move zoomend viewreset', () => 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') }) @@ -133,7 +140,7 @@ class Map extends React.Component { return [bbox, zoom] } - update () { + updateClusters () { const [bbox, zoom] = this.getMapDetails() if (this.superclusterIndex && this.state.indexLoaded) { this.setState({ @@ -146,7 +153,7 @@ class Map extends React.Component { if (locations && locations.length > 0 && this.superclusterIndex) { const convertedLocations = locations.reduce((acc, loc) => { const { longitude, latitude } = loc - const validCoordinates = !!latitude && !!longitude + const validCoordinates = isLatitude(latitude) && isLongitude(longitude) if (validCoordinates) { const feature = { type: 'Feature', @@ -165,7 +172,7 @@ class Map extends React.Component { }, []) this.superclusterIndex.load(convertedLocations) this.setState({ indexLoaded: true }) - this.update() + this.updateClusters() } else { this.setState({ clusters: [] }) } @@ -337,7 +344,7 @@ class Map extends React.Component { render () { const { isShowingSites, isFetchingDomain } = this.props.app.flags - const classes = this.props.app.narrative ? 'map-wrapper narrative-mode' : 'map-wrapper' + const classes = this.props.app.narrative ? 'map-wrapper narrative-mode' : 'map-wrapper' const innerMap = this.map ? ( {this.renderTiles()} @@ -372,6 +379,7 @@ function mapStateToProps (state) { return { domain: { locations: selectors.selectLocations(state), + // clusters: selectors.selectClusters(state), narratives: selectors.selectNarratives(state), categories: selectors.getCategories(state), sites: selectors.selectSites(state), From 5e2982786faba08a03db84a0e1c9d9e30711727a Mon Sep 17 00:00:00 2001 From: efarooqui Date: Tue, 20 Oct 2020 15:15:04 -0700 Subject: [PATCH 26/26] Linting fixes --- src/components/Map.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Map.jsx b/src/components/Map.jsx index dd42c96..5aa680b 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -344,7 +344,7 @@ class Map extends React.Component { render () { const { isShowingSites, isFetchingDomain } = this.props.app.flags - const classes = this.props.app.narrative ? 'map-wrapper narrative-mode' : 'map-wrapper' + const classes = this.props.app.narrative ? 'map-wrapper narrative-mode' : 'map-wrapper' const innerMap = this.map ? ( {this.renderTiles()} @@ -379,7 +379,6 @@ function mapStateToProps (state) { return { domain: { locations: selectors.selectLocations(state), - // clusters: selectors.selectClusters(state), narratives: selectors.selectNarratives(state), categories: selectors.getCategories(state), sites: selectors.selectSites(state),