Skip to content

Commit

Permalink
Merge pull request #208 from NGMarmaduke/map-clusters
Browse files Browse the repository at this point in the history
Map clusters
  • Loading branch information
NGMarmaduke authored Mar 21, 2017
2 parents 0de2648 + d3b2862 commit 86d34ae
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 207 deletions.
2 changes: 1 addition & 1 deletion .storybook/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ var cssMap = webpackPostcssTools.makeVarMap(path.join(paths.globalsSrc, 'index.c

module.exports = {
module: {
noParse: /node_modules\/mapbox-gl\/dist\/mapbox-gl.js/,
noParse: /node_modules\/@appearhere\/mapbox-gl\/dist\/mapbox-gl.js/,
loaders: [
{
test: /\.css$/,
Expand Down
7 changes: 3 additions & 4 deletions components/Map/BaseMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ import mapboxgl from '../../utils/mapboxgl/mapboxgl';
import lngLat from '../../utils/propTypeValidations/lngLat';
import noop from '../../utils/noop';

import { LONDON } from '../../constants/coordinates';
import { DEFAULT_CENTER, DEFAULT_ZOOM } from '../../constants/mapbox';

import css from './BaseMap.css';


export default class BaseMap extends Component {
static propTypes = {
allowWrap: PropTypes.bool,
Expand All @@ -29,9 +28,9 @@ export default class BaseMap extends Component {

static defaultProps = {
allowWrap: true,
center: LONDON,
center: DEFAULT_CENTER,
mapboxStyle: mapStyle,
zoom: 11,
zoom: DEFAULT_ZOOM,
onClick: noop,
onMapLoad: noop,
onMoveEnd: noop,
Expand Down
91 changes: 72 additions & 19 deletions components/Map/MarkableMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,36 @@ import isEqual from 'lodash/fp/isEqual';
import uniqueId from 'lodash/fp/uniqueId';
import cx from 'classnames';

import lngLat from '../../utils/propTypeValidations/lngLat';
import minLngLatBounds from '../../utils/geoUtils/minLngLatBounds';
import mapboxgl from '../../utils/mapboxgl/mapboxgl';
import MarkerContainer from './MarkerContainer';
import BaseMap from './BaseMap';

import {
CLUSTER_RADIUS,
CLUSTER_MAX_ZOOM,
MARKER_SOURCE,
MARKER_LAYER,
CLUSTER_LAYER,
MOVE_TO_MARKER_MAX_LAT_OFFSET,
} from '../../constants/mapbox';

import css from './MarkableMap.css';

export default class MarkableMap extends Component {
static propTypes = {
// TODO: shapeOf prop type
markers: PropTypes.array,
markers: PropTypes.arrayOf(
PropTypes.shape({
id: React.PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
lngLat: lngLat.isRequired,
label: PropTypes.string.isRequired,
props: PropTypes.object,
})
),
MarkerComponent: PropTypes.func.isRequired,
autoFit: PropTypes.bool,
};
Expand All @@ -39,9 +58,9 @@ export default class MarkableMap extends Component {
};

componentDidMount() {
const { autoFit } = this.props;
const { autoFit, markers } = this.props;
this.updateMapboxMarkerSource();
if (autoFit) this.fitMarkers();
if (autoFit) this.fitMarkers(markers);
}

componentDidUpdate(prevProps) {
Expand All @@ -63,7 +82,7 @@ export default class MarkableMap extends Component {
return !prevMarker || !isEqual(prevMarker.lngLat, marker.lngLat);
});
const markerChange = prevMarkers.length !== markers.length || markersMoved;
if (markerChange) this.fitMarkers();
if (markerChange) this.fitMarkers(markers);
}
}

Expand All @@ -76,21 +95,30 @@ export default class MarkableMap extends Component {
handleMapLoad = () => {
const mapbox = this.getMaboxGL();

mapbox.addSource('markers', {
mapbox.addSource(MARKER_SOURCE, {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [],
},
cluster: true,
clusterRadius: 10,
clusterRadius: CLUSTER_RADIUS,
clusterMaxZoom: CLUSTER_MAX_ZOOM,
});
this.mapboxMarkerSource = mapbox.getSource(MARKER_SOURCE);

mapbox.addLayer({
id: 'markers',
id: MARKER_LAYER,
type: 'symbol',
source: 'markers',
filter: ['!=', 'active', true],
source: MARKER_SOURCE,
filter: [
'all',
['!=', 'active', true],
['!has', 'point_count'],
],
layout: {
'icon-allow-overlap': true,
'text-allow-overlap': true,
'icon-image': 'pin-{labellen}',
'text-field': '{label}',
'text-font': ['Open Sans Semibold', 'Arial Unicode MS Bold'],
Expand All @@ -103,11 +131,27 @@ export default class MarkableMap extends Component {
'text-color': '#FFFFFF',
},
});
this.mapboxMarkerSource = mapbox.getSource('markers');
mapbox.addLayer({
id: CLUSTER_LAYER,
type: 'symbol',
source: MARKER_SOURCE,
filter: ['has', 'point_count'],
layout: {
'icon-image': 'pin-cluster',
'text-field': '{point_count}',
'text-font': ['Open Sans Semibold', 'Arial Unicode MS Bold'],
'text-size': 14,
},
paint: {
'text-color': '#FFFFFF',
},
});

// When hovering on a marker change the cursor to a pointer
mapbox.on('mousemove', (e) => {
const features = mapbox.queryRenderedFeatures(e.point, { layers: ['markers'] });
const features = mapbox.queryRenderedFeatures(e.point, {
layers: [MARKER_LAYER, CLUSTER_LAYER],
});
mapbox.getCanvas().style.cursor = features.length ? 'pointer' : '';
});

Expand Down Expand Up @@ -139,24 +183,33 @@ export default class MarkableMap extends Component {
handleMapClick = (e) => {
const { originalEvent, point } = e;
if (originalEvent.target !== this.getMaboxGL().getCanvas()) return;
const markers = this.getMaboxGL().queryRenderedFeatures(point, { layers: ['markers'] });
const features = this.getMaboxGL().queryRenderedFeatures(point, { layers: [MARKER_LAYER] });

if (markers.length) {
const marker = markers[0];
if (features.length > 0) {
const marker = features[0];
this.moveToMarker(marker);
this.setState({ activeMarkerId: marker.properties.id });
} else {
const clusters = this.getMaboxGL().queryRenderedFeatures(point, {
layers: [CLUSTER_LAYER],
});
this.setState({ activeMarkerId: null });

if (clusters.length > 0) {
const { markers } = this.props;
const clusterMarkerIds = JSON.parse(clusters[0].properties.markerids);
const clusterMarkers = markers.filter(marker => clusterMarkerIds.indexOf(marker.id) !== -1);
this.fitMarkers(clusterMarkers);
}
}
};

fitMarkers = () => {
const { markers } = this.props;
fitMarkers = (markers) => {
if (!markers.length) return;

this.map.fitBounds(
minLngLatBounds(markers.map(marker => marker.lngLat)),
{ padding: 20, offset: [0, 20], maxZoom: 16 },
{ padding: { top: 20, bottom: 20, left: 50, right: 50 }, offset: [0, 20], maxZoom: 16 },
);
};

Expand All @@ -165,7 +218,7 @@ export default class MarkableMap extends Component {
const [markerLng, markerLat] = geometry.coordinates;
const zoom = this.getMaboxGL().getZoom();

const nextLat = markerLat + (80 / Math.pow(2, zoom));
const nextLat = markerLat + ((MOVE_TO_MARKER_MAX_LAT_OFFSET * 2) / Math.pow(2, zoom));
const nextCenter = new mapboxgl.LngLat(markerLng, nextLat).wrap();

this.map.easeTo({ center: nextCenter });
Expand Down
14 changes: 7 additions & 7 deletions components/Map/MarkableMap.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ it('it renders the active marker correctly', () => {
<MarkableMap
ref={ (c) => { component = c; } }
MarkerComponent={ SpaceMarker }
markers={ [{ id: 1, lngLat: [1, 0] }] }
markers={ [{ id: 1, lngLat: [1, 0], label: 'test' }] }
/>,
div
);
Expand All @@ -54,7 +54,7 @@ it('it autosizes the map correctly', () => {
ReactDOM.render(
<MarkableMap
MarkerComponent={ SpaceMarker }
markers={ [{ id: 1, lngLat: [1, 0] }] }
markers={ [{ id: 1, lngLat: [1, 0], label: 'test' }] }
autoFit
/>,
div
Expand All @@ -68,8 +68,8 @@ it('it autosizes the map correctly', () => {
<MarkableMap
MarkerComponent={ SpaceMarker }
markers={ [
{ id: 1, lngLat: [0, 0] },
{ id: 2, lngLat: [1, 1] },
{ id: 1, lngLat: [0, 0], label: 'test' },
{ id: 2, lngLat: [1, 1], label: 'test' },
] }
autoFit
/>,
Expand All @@ -87,8 +87,8 @@ it('it autosizes the map correctly', () => {
<MarkableMap
MarkerComponent={ SpaceMarker }
markers={ [
{ id: 1, lngLat: [0, 0] },
{ id: 2, lngLat: [1, 1] },
{ id: 1, lngLat: [0, 0], label: 'test' },
{ id: 2, lngLat: [1, 1], label: 'test' },
] }
autoFit
/>,
Expand All @@ -106,7 +106,7 @@ it('unmounts without crashing', () => {
ReactDOM.render(
<MarkableMap
MarkerComponent={ SpaceMarker }
markers={ [{ id: 1, lngLat: [1, 0] }] }
markers={ [{ id: 1, lngLat: [1, 0], label: 'test' }] }
/>,
div
);
Expand Down
2 changes: 1 addition & 1 deletion config/webpack.config.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ module.exports = {
moduleTemplates: ['*-loader']
},
module: {
noParse: /node_modules\/mapbox-gl\/dist\/mapbox-gl.js/,
noParse: /node_modules\/@appearhere\/mapbox-gl\/dist\/mapbox-gl.js/,
// First, run the linter.
// It's important to do this before Babel processes the JS.
preLoaders: [
Expand Down
2 changes: 1 addition & 1 deletion config/webpack.config.prod.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ module.exports = {
moduleTemplates: ['*-loader']
},
module: {
noParse: /node_modules\/mapbox-gl\/dist\/mapbox-gl.js/,
noParse: /node_modules\/@appearhere\/mapbox-gl\/dist\/mapbox-gl.js/,
// First, run the linter.
// It's important to do this before Babel processes the JS.
preLoaders: [
Expand Down
19 changes: 19 additions & 0 deletions constants/mapbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { LONDON } from './coordinates';

export const DEFAULT_ZOOM = 11;
export const DEFAULT_CENTER = LONDON;

export const CLUSTER_RADIUS = 60;
export const CLUSTER_MAX_ZOOM = 13;

export const MARKER_SOURCE = 'markers';
export const MARKER_LAYER = 'marker-layer';
export const CLUSTER_LAYER = 'cluster-layer';

/**
* The latitude offset when calculating the next map center, this offset ensures that the active
* marker content is centred vertically
*
* The value is the maximum offset applied when the zoom level is 0 i.e. the furthest possible zoom
*/
export const MOVE_TO_MARKER_MAX_LAT_OFFSET = 40;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"whatwg-fetch": "1.0.0"
},
"dependencies": {
"@appearhere/mapbox-gl": "^1.1.0",
"@appearhere/nuka-carousel": "^2.1.6",
"@appearhere/react-input-range": "^1.1.0",
"array-from": "^2.1.1",
Expand All @@ -103,7 +104,6 @@
"key-mirror": "^1.0.1",
"lodash": "^4.16.3",
"lost": "^7.1.0",
"mapbox-gl": "^0.32.1",
"moment": "^2.17.1",
"moment-range": "^3.0.2",
"object-fit-images": "^2.5.9",
Expand Down
2 changes: 1 addition & 1 deletion utils/mapboxgl/mapboxgl.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { canUseDOM } from 'exenv';

const mapboxgl = canUseDOM ? require('mapbox-gl/dist/mapbox-gl') : {};
const mapboxgl = canUseDOM ? require('@appearhere/mapbox-gl/dist/mapbox-gl') : {};

mapboxgl.accessToken = 'pk.eyJ1IjoiYXBwZWFyaGVyZSIsImEiOiJvUlJ0MWxNIn0.8_mzlmxdekKy99luyV4T7w';

Expand Down
Loading

0 comments on commit 86d34ae

Please sign in to comment.