Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

integration of /pin-clusters #575

Merged
merged 2 commits into from
May 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/components/PinMap/ClusterMarker.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import PropTypes from 'proptypes';
import { Marker } from 'react-leaflet';
import { divIcon, point } from 'leaflet';

function markerClass(count) {
if (count < 100) return 'small';
if (count < 1000) return 'medium';
return 'large';
}

function abbreviatedCount(count) {
if (count < 1000) return count;
if (count < 10000) return `${(count / 1000).toFixed(1)}K`;
if (count < 1000000) return `${(count / 1000).toFixed(0)}K`;
return `${(count / 1000000).toFixed(1)}M`;
}

const ClusterMarker = ({
position,
count,
onClick,
}) => {
const markerIcon = divIcon({
html: `<div><span>${abbreviatedCount(count)}</span></div>`,
className: `marker-cluster marker-cluster-${markerClass(count)}`,
iconSize: point(40, 40),
});

return (
<Marker position={position} onClick={onClick} icon={markerIcon} />
);
};

export default ClusterMarker;

ClusterMarker.propTypes = {
position: PropTypes.node.isRequired,
count: PropTypes.number.isRequired,
onClick: PropTypes.func.isRequired,
};
166 changes: 105 additions & 61 deletions src/components/PinMap/PinMap.jsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getPinInfoRequest } from '@reducers/data';
import { updateMapPosition } from '@reducers/ui';
import PinPopup from '@components/PinMap/PinPopup';
import CustomMarker from '@components/PinMap/CustomMarker';
import ClusterMarker from '@components/PinMap/ClusterMarker';
import {
Map,
TileLayer,
Rectangle,
Tooltip,
LayersControl,
LayerGroup,
ZoomControl,
withLeaflet,
} from 'react-leaflet';
import Choropleth from 'react-leaflet-choropleth';
import MarkerClusterGroup from 'react-leaflet-markercluster';
import HeatmapLayer from 'react-leaflet-heatmap-layer';
import PropTypes from 'proptypes';
import COLORS from '@styles/COLORS';
Expand Down Expand Up @@ -46,6 +48,7 @@ class PinMap extends Component {
ready: false,
width: null,
height: null,
heatmapVisible: false,
};
this.container = React.createRef();
}
Expand Down Expand Up @@ -93,6 +96,14 @@ class PinMap extends Component {
this.setState({ bounds });
}

updatePosition = ({ target: map }) => {
const { updatePosition } = this.props;
updatePosition({
zoom: map.getZoom(),
bounds: map.getBounds(),
});
}

onEachRegionFeature = (feature, layer) => {
// Popup text when clicking on a region
const popupText = `
Expand All @@ -114,65 +125,77 @@ class PinMap extends Component {

renderMarkers = () => {
const {
data,
pinClusters,
getPinInfo,
pinsInfo,
} = this.props;

if (data) {
return data.map(d => {
if (d.latitude && d.longitude) {
const {
latitude,
longitude,
srnumber,
requesttype,
} = d;
const position = [latitude, longitude];
const {
status,
createddate,
updateddate,
closeddate,
address,
ncname,
} = pinsInfo[srnumber] || {};
const { displayName, color, abbrev } = REQUEST_TYPES[requesttype];

const popup = (
<PinPopup
displayName={displayName}
color={color}
abbrev={abbrev}
address={address}
createdDate={createddate}
updatedDate={updateddate}
closedDate={closeddate}
status={status}
ncName={ncname}
/>
);

if (pinClusters) {
return pinClusters.map(({
id,
count,
latitude,
longitude,
expansion_zoom: expansionZoom,
srnumber,
requesttype,
}) => {
const position = [latitude, longitude];

if (count > 1) {
return (
<CustomMarker
key={srnumber}
<ClusterMarker
key={id}
position={position}
onClick={() => {
if (!pinsInfo[srnumber]) {
getPinInfo(srnumber);
}
count={count}
onClick={({ latlng }) => {
this.map.flyTo(latlng, expansionZoom);
}}
color={color}
icon="map-marker-alt"
size="3x"
style={{ textShadow: '1px 0px 3px rgba(0,0,0,1.0), -1px 0px 3px rgba(0,0,0,1.0)' }}
>
{popup}
</CustomMarker>
/>
);
}

return null;
const {
status,
createddate,
updateddate,
closeddate,
address,
ncname,
} = pinsInfo[srnumber] || {};
const { displayName, color, abbrev } = REQUEST_TYPES[requesttype];

const popup = (
<PinPopup
displayName={displayName}
color={color}
abbrev={abbrev}
address={address}
createdDate={createddate}
updatedDate={updateddate}
closedDate={closeddate}
status={status}
ncName={ncname}
/>
);

return (
<CustomMarker
key={srnumber}
position={position}
onClick={() => {
if (!pinsInfo[srnumber]) {
getPinInfo(srnumber);
}
}}
color={color}
icon="map-marker-alt"
size="3x"
style={{ textShadow: '1px 0px 3px rgba(0,0,0,1.0), -1px 0px 3px rgba(0,0,0,1.0)' }}
>
{popup}
</CustomMarker>
);
});
}

Expand Down Expand Up @@ -202,9 +225,10 @@ class PinMap extends Component {
geoJSON,
width,
height,
heatmapVisible,
} = this.state;

const { data } = this.props;
const { heatmap } = this.props;

return (
<>
Expand All @@ -215,6 +239,21 @@ class PinMap extends Component {
bounds={bounds}
style={{ width, height }}
zoomControl={false}
whenReady={e => {
this.map = e.target;
this.updatePosition(e);
}}
onMoveend={this.updatePosition}
onOverlayadd={({ name }) => {
if (name === 'Heatmap') {
this.setState({ heatmapVisible: true });
}
}}
onOverlayremove={({ name }) => {
if (name === 'Heatmap') {
this.setState({ heatmapVisible: false });
}
}}
>
<ZoomControl position="topright" />
<LayersControl
Expand Down Expand Up @@ -259,24 +298,24 @@ class PinMap extends Component {
)
}
<Overlay checked name="Markers">
<MarkerClusterGroup
maxClusterRadius={40}
>
<LayerGroup>
{this.renderMarkers()}
</MarkerClusterGroup>
</LayerGroup>
</Overlay>
<Overlay name="Heatmap">
{/* intensityExtractor is required and requires a callback as the value.
* The heatmap is working with an empty callback but we'll probably
* improve functionality post-MVP by generating a heatmap list
* on the backend. */}
{/* The heatmapVisible test prevents the component from doing
* unnecessary calculations when the heatmap isn't visible */}
Comment on lines +310 to +311
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, good catch.

<HeatmapLayer
max={1}
points={data}
points={heatmapVisible ? heatmap : []}
radius={20}
blur={25}
longitudeExtractor={m => m.longitude}
latitudeExtractor={m => m.latitude}
longitudeExtractor={m => m[1]}
latitudeExtractor={m => m[0]}
intensityExtractor={() => 1}
/>
</Overlay>
Expand Down Expand Up @@ -324,22 +363,27 @@ class PinMap extends Component {

const mapDispatchToProps = dispatch => ({
getPinInfo: srnumber => dispatch(getPinInfoRequest(srnumber)),
updatePosition: position => dispatch(updateMapPosition(position)),
});

const mapStateToProps = state => ({
data: state.data.pins,
pinsInfo: state.data.pinsInfo,
pinClusters: state.data.pinClusters,
heatmap: state.data.heatmap,
});

PinMap.propTypes = {
data: PropTypes.arrayOf(PropTypes.shape({})),
pinsInfo: PropTypes.shape({}),
pinClusters: PropTypes.arrayOf(PropTypes.shape({})),
heatmap: PropTypes.arrayOf(PropTypes.array),
getPinInfo: PropTypes.func.isRequired,
updatePosition: PropTypes.func.isRequired,
};

PinMap.defaultProps = {
data: undefined,
pinsInfo: {},
pinClusters: [],
heatmap: [],
};

export default connect(mapStateToProps, mapDispatchToProps)(PinMap);
2 changes: 1 addition & 1 deletion src/components/chartExtras/NumberOfRequests.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const NumberOfRequests = ({
);

const mapStateToProps = state => ({
numRequests: state.data.pins.length,
numRequests: Object.values(state.data.counts.type).reduce((p, c) => p + c, 0),
});

export default connect(mapStateToProps)(NumberOfRequests);
Expand Down
14 changes: 11 additions & 3 deletions src/components/common/Loader.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'proptypes';
import { connect } from 'react-redux';
import { MENU_TABS } from '@components/common/CONSTANTS';

const Loader = ({
isLoading,
Expand All @@ -17,9 +18,16 @@ const Loader = ({
);
};

const mapStateToProps = state => ({
isLoading: state.data.isLoading || state.comparisonData.isLoading,
});
const mapStateToProps = state => {
const { activeTab } = state.ui.menu;
return {
isLoading: (
state.comparisonData.isLoading
|| (state.data.isMapLoading && activeTab === MENU_TABS.MAP)
|| (state.data.isVisLoading && activeTab === MENU_TABS.VISUALIZATIONS)
),
};
};

export default connect(mapStateToProps)(Loader);

Expand Down
Loading