Skip to content

Commit

Permalink
Merge pull request #896 from sharetribe/mapbox-map
Browse files Browse the repository at this point in the history
Mapbox Map component
  • Loading branch information
kpuputti authored Aug 15, 2018
2 parents 04e85c8 + 85ed665 commit 5e9fb02
Show file tree
Hide file tree
Showing 10 changed files with 359 additions and 64 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ way to update this template, but currently, we follow a pattern:

---
## Upcoming version 2018-08-XX
* [add] Map component (used in ListingPage) using Mapbox instead of Google Maps
[#896](https://github.com/sharetribe/flex-template-web/pull/896)
* [add] Listing availability
[#868](https://github.com/sharetribe/flex-template-web/pull/868), [#873](https://github.com/sharetribe/flex-template-web/pull/873), [#891](https://github.com/sharetribe/flex-template-web/pull/891) & [#892](https://github.com/sharetribe/flex-template-web/pull/892)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { withGoogleMap, GoogleMap, Marker, Circle } from 'react-google-maps';
import config from '../../config';

/**
* DynamicMap uses withGoogleMap HOC.
* DynamicGoogleMap uses withGoogleMap HOC.
* It handles some of the google map initialization states.
*/
const DynamicMap = withGoogleMap(props => {
const DynamicGoogleMap = withGoogleMap(props => {
const { center, zoom, address, coordinatesConfig } = props;

const { markerURI, anchorX, anchorY, width, height } = coordinatesConfig.customMarker || {};
Expand All @@ -34,6 +34,11 @@ const DynamicMap = withGoogleMap(props => {

const circle = <Circle {...circleProps} />;

const controlPosition =
typeof window !== 'undefined' && typeof window.google !== 'undefined'
? window.google.maps.ControlPosition.LEFT_TOP
: 5;

return (
<GoogleMap
defaultZoom={zoom}
Expand All @@ -45,22 +50,28 @@ const DynamicMap = withGoogleMap(props => {
// Disable zooming by scrolling
scrollwheel: false,
// Fullscreen control toggle
fullscreenControl: true,
fullscreenControl: false,
// Street View control
streetViewControl: false,
// Zoom control position
zoomControlOptions: {
position: controlPosition,
},
}}
>
{coordinatesConfig.fuzzy ? circle : marker}
</GoogleMap>
);
});

DynamicMap.defaultProps = {
DynamicGoogleMap.defaultProps = {
address: '',
center: null,
zoom: config.coordinates.fuzzy ? config.coordinates.fuzzyDefaultZoomLevel : 11,
coordinatesConfig: config.coordinates,
};

DynamicMap.propTypes = {
DynamicGoogleMap.propTypes = {
address: string,
center: shape({
lat: number.isRequired,
Expand All @@ -70,4 +81,4 @@ DynamicMap.propTypes = {
coordinatesConfig: object,
};

export default DynamicMap;
export default DynamicGoogleMap;
167 changes: 167 additions & 0 deletions src/components/Map/DynamicMapboxMap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import React, { Component } from 'react';
import { string, shape, number, object } from 'prop-types';
import uniqueId from 'lodash/uniqueId';
import { circlePolyline } from '../../util/maps';
import config from '../../config';

const mapMarker = coordinatesConfig => {
const { customMarker } = coordinatesConfig;
if (customMarker) {
const element = document.createElement('div');
element.style.backgroundImage = `url(${customMarker.markerURI})`;
element.style.width = `${customMarker.width}px`;
element.style.height = `${customMarker.height}px`;
return new window.mapboxgl.Marker({ element });
} else {
return new window.mapboxgl.Marker();
}
};

const circleLayer = (center, coordinatesConfig, layerId) => {
const { fillColor, fillOpacity } = coordinatesConfig.circleOptions;
const path = circlePolyline(center, coordinatesConfig.coordinateOffset).map(([lat, lng]) => [
lng,
lat,
]);
return {
id: layerId,
type: 'fill',
source: {
type: 'geojson',
data: {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [path],
},
},
},
paint: {
'fill-color': fillColor,
'fill-opacity': fillOpacity,
},
};
};

const generateFuzzyLayerId = () => {
return uniqueId('fuzzy_layer_');
};

class DynamicMapboxMap extends Component {
constructor(props) {
super(props);

this.mapContainer = null;
this.map = null;
this.centerMarker = null;
this.fuzzyLayerId = generateFuzzyLayerId();

this.updateFuzzyCirclelayer = this.updateFuzzyCirclelayer.bind(this);
}
componentDidMount() {
const { center, zoom, coordinatesConfig } = this.props;
const position = [center.lng, center.lat];

this.map = new window.mapboxgl.Map({
container: this.mapContainer,
style: 'mapbox://styles/mapbox/streets-v10',
center: position,
zoom,
scrollZoom: false,
});
this.map.addControl(new window.mapboxgl.NavigationControl(), 'top-left');

if (coordinatesConfig.fuzzy) {
this.map.on('load', () => {
this.map.addLayer(circleLayer(center, coordinatesConfig, this.fuzzyLayerId));
});
} else {
this.centerMarker = mapMarker(coordinatesConfig);
this.centerMarker.setLngLat(position).addTo(this.map);
}
}
componentWillUnmount() {
if (this.map) {
this.centerMarker = null;
this.map.remove();
this.map = null;
}
}
componentDidUpdate(prevProps) {
if (!this.map) {
return;
}

const { center, zoom, coordinatesConfig } = this.props;
const { lat, lng } = center;
const position = [lng, lat];

// zoom change
if (zoom !== prevProps.zoom) {
this.map.setZoom(this.props.zoom);
}

const centerChanged = lat !== prevProps.center.lat || lng !== prevProps.center.lng;

// center marker change
if (this.centerMarker && centerChanged) {
this.centerMarker.setLngLat(position);
this.map.setCenter(position);
}

// fuzzy circle change
if (coordinatesConfig.fuzzy && centerChanged) {
if (this.map.loaded()) {
this.updateFuzzyCirclelayer();
} else {
this.map.on('load', this.updateFuzzyCirclelayer);
}
}

// NOTE: coordinatesConfig changes are not handled
}
updateFuzzyCirclelayer() {
if (!this.map) {
// map already removed
return;
}
const { center, coordinatesConfig } = this.props;
const { lat, lng } = center;
const position = [lng, lat];

this.map.removeLayer(this.fuzzyLayerId);

// We have to use a different layer id to avoid Mapbox errors
this.fuzzyLayerId = generateFuzzyLayerId();
this.map.addLayer(circleLayer(center, coordinatesConfig, this.fuzzyLayerId));

this.map.setCenter(position);
}
render() {
const { containerClassName, mapClassName } = this.props;
return (
<div className={containerClassName}>
<div className={mapClassName} ref={el => (this.mapContainer = el)} />
</div>
);
}
}

DynamicMapboxMap.defaultProps = {
address: '',
center: null,
zoom: config.coordinates.fuzzy ? config.coordinates.fuzzyDefaultZoomLevel : 11,
coordinatesConfig: config.coordinates,
};

DynamicMapboxMap.propTypes = {
address: string, // not used
center: shape({
lat: number.isRequired,
lng: number.isRequired,
}).isRequired,
zoom: number,
coordinatesConfig: object,
};

export default DynamicMapboxMap;
6 changes: 6 additions & 0 deletions src/components/Map/GoogleMap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { default as DynamicMap } from './DynamicGoogleMap';
export { default as StaticMap } from './StaticGoogleMap';

export const isMapsLibLoaded = () => {
return typeof window !== 'undefined' && window.google && window.google.maps;
};
17 changes: 8 additions & 9 deletions src/components/Map/Map.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { bool, number, object, string } from 'prop-types';
import classNames from 'classnames';
import { propTypes } from '../../util/types';
import config from '../../config';
import { StaticMap, DynamicMap, isMapsLibLoaded } from './GoogleMap';
// import { StaticMap, DynamicMap, isMapsLibLoaded } from './MapboxMap';

import DynamicMap from './DynamicMap';
import StaticMap from './StaticMap';
import css from './Map.css';

export class Map extends Component {
Expand Down Expand Up @@ -34,24 +34,23 @@ export class Map extends Component {
}

const location = coordinatesConfig.fuzzy ? obfuscatedCenter : center;
const centerLocationForGoogleMap = { lat: location.lat, lng: location.lng };

const isMapsLibLoaded = typeof window !== 'undefined' && window.google && window.google.maps;

return !isMapsLibLoaded ? (
return !isMapsLibLoaded() ? (
<div className={classes} />
) : useStaticMap ? (
<StaticMap
center={centerLocationForGoogleMap}
center={location}
zoom={zoom}
address={address}
coordinatesConfig={coordinatesConfig}
/>
) : (
<DynamicMap
containerElement={<div className={classes} onClick={this.onMapClicked} />}
containerElement={<div className={classes} />}
mapElement={<div className={mapClasses} />}
center={centerLocationForGoogleMap}
containerClassName={classes}
mapClassName={mapClasses}
center={location}
zoom={zoom}
address={address}
coordinatesConfig={coordinatesConfig}
Expand Down
6 changes: 6 additions & 0 deletions src/components/Map/MapboxMap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { default as DynamicMap } from './DynamicMapboxMap';
export { default as StaticMap } from './StaticMapboxMap';

export const isMapsLibLoaded = () => {
return typeof window !== 'undefined' && window.mapboxgl;
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,13 @@ import isEqual from 'lodash/isEqual';
import polyline from '@mapbox/polyline';
import { encodeLatLng, stringify } from '../../util/urlHelpers';
import { lazyLoadWithDimensions } from '../../util/contextHelpers';
import { circlePolyline } from '../../util/maps';
import config from '../../config';

const DEFAULT_COLOR = 'FF0000';
const DEFAULT_STROKE_OPACITY = 0.3;
const DEFAULT_FILL_OPACITY = 0.2;

// Return polyline encoded list of points forming a circle
// This algorithm is based on
// https://stackoverflow.com/questions/7316963/drawing-a-circle-google-static-maps
const getEncodedGMapCirclePoints = (lat, lng, rad, detail = 8) => {
const R = 6371;
const pi = Math.PI;

const _lat = lat * pi / 180;
const _lng = lng * pi / 180;
const d = rad / 1000 / R;

let points = [];
for (let i = 0; i <= 360; i += detail) {
const brng = i * pi / 180;

let pLat = Math.asin(
Math.sin(_lat) * Math.cos(d) + Math.cos(_lat) * Math.sin(d) * Math.cos(brng)
);
const pLng =
(_lng +
Math.atan2(
Math.sin(brng) * Math.sin(d) * Math.cos(_lat),
Math.cos(d) - Math.sin(_lat) * Math.sin(pLat)
)) *
180 /
pi;
pLat = pLat * 180 / pi;

points.push([pLat.toFixed(6), pLng.toFixed(6)]);
}

return polyline.encode(points);
};

// Extract color from string. Given value should be either with '#' (e.g. #FFFFFF') or without it.
const formatColorFromString = color => {
if (typeof color === 'string' && /^#[0-9A-F]{6}$/i.test(color)) {
Expand Down Expand Up @@ -80,7 +47,6 @@ const drawFuzzyCircle = (coordinatesConfig, center) => {
}

const { fillColor, fillOpacity, strokeColor, strokeWeight } = coordinatesConfig.circleOptions;
const { lat, lng } = center;

const circleRadius = coordinatesConfig.coordinateOffset || 500;
const circleStrokeWeight = strokeWeight || 1;
Expand All @@ -89,8 +55,8 @@ const drawFuzzyCircle = (coordinatesConfig, center) => {
const circleFill = formatColorFromString(fillColor);
const circleFillOpacity = convertOpacity(fillOpacity || DEFAULT_FILL_OPACITY);

//Encoded polyline string
const encodedPolyline = getEncodedGMapCirclePoints(lat, lng, circleRadius, 8);
// Encoded polyline string
const encodedPolyline = polyline.encode(circlePolyline(center, circleRadius));

const polylineGraphicTokens = [
`color:0x${circleStrokeColor}${circleStrokeOpacity}`,
Expand All @@ -108,7 +74,7 @@ const customMarker = (options, lat, lng) => {
return [`anchor:${anchorX},${anchorY}`, `icon:${markerURI}`, `${lat},${lng}`].join('|');
};

class StaticMap extends Component {
class StaticGoogleMap extends Component {
shouldComponentUpdate(nextProps, prevState) {
// Do not draw the map unless center, zoom or dimensions change
// We want to prevent unnecessary calls to Google Maps APIs due
Expand Down Expand Up @@ -147,7 +113,7 @@ class StaticMap extends Component {
}
}

StaticMap.defaultProps = {
StaticGoogleMap.defaultProps = {
className: null,
rootClassName: null,
address: '',
Expand All @@ -156,7 +122,7 @@ StaticMap.defaultProps = {
coordinatesConfig: config.coordinates,
};

StaticMap.propTypes = {
StaticGoogleMap.propTypes = {
className: string,
rootClassName: string,
address: string,
Expand All @@ -174,4 +140,4 @@ StaticMap.propTypes = {
}).isRequired,
};

export default lazyLoadWithDimensions(StaticMap, { maxWidth: '640px' });
export default lazyLoadWithDimensions(StaticGoogleMap, { maxWidth: '640px' });
Loading

0 comments on commit 5e9fb02

Please sign in to comment.