UNPKG

@hackoregon/component-library

Version:

Official repo for Hack Oregon React component library

328 lines (302 loc) 8.59 kB
/* eslint-disable no-nested-ternary */ import React, { Component } from "react"; import MapGL, { NavigationControl, Marker, FlyToInterpolator } from "react-map-gl"; import Dimensions from "react-dimensions"; import { css } from "emotion"; import PropTypes from "prop-types"; import createRef from "create-react-ref/lib/createRef"; import Geocoder from "react-map-gl-geocoder"; import "mapbox-gl/dist/mapbox-gl.css"; import { isEqual } from "lodash"; import mapboxgl from "./mapboxgl"; const MAPBOX_TOKEN = "pk.eyJ1IjoiaGFja29yZWdvbiIsImEiOiJjamk0MGZhc2cwNDl4M3FsdHAwaG54a3BnIn0.Fq1KA0IUwpeKQlFIoaEn_Q"; const CIVIC_LIGHT = "mapbox://styles/hackoregon/cjiazbo185eib2srytwzleplg"; const CIVIC_DARK = "mapbox://styles/mapbox/dark-v9"; const mapWrapper = css` margin: 0 auto; padding: 0; width: 100%; height: 100%; `; const navControl = css` position: absolute; left: 0; z-index: 1; `; class BaseMap extends Component { constructor(props) { super(props); this.state = { viewport: { longitude: props.initialLongitude, latitude: props.initialLatitude, zoom: props.initialZoom, minZoom: 6, maxZoom: 20, pitch: props.initialPitch, bearing: 0, scrollZoom: true }, tooltipInfo: null, x: null, y: null, mounted: false }; this.mapRef = createRef(); } componentDidMount() { // Geocoder requires a ref to the map component this.setState({ mounted: true }); } componentWillReceiveProps(props) { const { updateViewport } = props; if (updateViewport) { const updatedViewportProps = { zoom: props.initialZoom, pitch: props.initialPitch, longitude: props.initialLongitude, latitude: props.initialLatitude }; // Remove all keys that have null/undefined values to keep defaults Object.keys(updatedViewportProps).forEach(key => { if (updatedViewportProps[key] == null) { delete updatedViewportProps[key]; } }); this.setState(prevState => ({ viewport: { ...prevState.viewport, ...updatedViewportProps } })); } } componentDidUpdate(prevProps) { const { mapboxData: previousMapboxData, mapboxLayerOptions: previousMapboxLayerOptions } = prevProps; const { mapboxData, mapboxDataId, mapboxLayerOptions, mapboxLayerId } = this.props; if (!isEqual(previousMapboxData, mapboxData)) { const map = this.mapRef.current.getMap(); map.getSource(mapboxDataId).setData(mapboxData); } if (!isEqual(previousMapboxLayerOptions, mapboxLayerOptions)) { const map = this.mapRef.current.getMap(); const updatedProperties = Object.keys(mapboxLayerOptions).filter( m => !isEqual(previousMapboxLayerOptions[m], mapboxLayerOptions[m]) ); updatedProperties.forEach(p => map.setPaintProperty(mapboxLayerId, p, mapboxLayerOptions[p]) ); } } onHover = ({ object, x, y }) => { this.setState({ tooltipInfo: object, x, y }); }; onViewportChange = viewport => { this.setState(prevState => ({ viewport: { ...prevState.viewport, ...viewport } })); }; render() { const { viewport, tooltipInfo, x, y, mounted } = this.state; const { height, containerHeight, containerWidth, civicMapStyle, mapboxToken, navigation, geocoder, locationMarker, geocoderOptions, geocoderOnChange, mapGLOptions, children, useContainerHeight, onBaseMapClick, mapboxData, mapboxDataId, mapboxLayerType, mapboxLayerOptions, mapboxLayerId, locationMarkerCoord, animate, animationDuration, scaleBar, scaleBarOptions } = this.props; viewport.width = containerWidth || 500; viewport.height = useContainerHeight ? containerHeight : height; const childrenLayers = React.Children.map(children, child => { return React.cloneElement(child, { viewport, tooltipInfo, x, y, onHover: info => this.onHover(info) }); }); const onMapLoad = () => { const map = this.mapRef.current.getMap(); if (scaleBar) { map.addControl( new mapboxgl.ScaleControl({ maxWidth: scaleBarOptions.maxWidth, unit: scaleBarOptions.units }) ); } if (!mapboxData || !mapboxLayerType || !mapboxLayerOptions) return; map.addSource(mapboxDataId, { type: "geojson", data: mapboxData }); map.addLayer( { id: mapboxLayerId, type: mapboxLayerType, source: mapboxDataId, paint: mapboxLayerOptions }, "waterway-label" ); }; const baseMapboxStyleURL = civicMapStyle === "light" ? CIVIC_LIGHT : civicMapStyle === "dark" ? CIVIC_DARK : CIVIC_LIGHT; const animationProps = !animate ? {} : { transitionDuration: animationDuration, transitionInterpolator: new FlyToInterpolator() }; return ( <div className={mapWrapper}> <MapGL className="MapGL" {...viewport} {...animationProps} mapStyle={baseMapboxStyleURL} mapboxApiAccessToken={mapboxToken} onViewportChange={newViewport => this.onViewportChange(newViewport)} ref={this.mapRef} {...mapGLOptions} onClick={onBaseMapClick} onLoad={onMapLoad} > <div className={navControl}> {navigation && ( <NavigationControl className="NavigationControl" onViewportChange={newViewport => this.onViewportChange(newViewport) } /> )} </div> {locationMarker && ( <Marker latitude={locationMarkerCoord.latitude} longitude={locationMarkerCoord.longitude} offsetLeft={-20} offsetTop={-10} > <span role="img" aria-label="star"></span> </Marker> )} {geocoder && mounted && ( <Geocoder mapRef={{ current: this.mapRef.current }} mapboxApiAccessToken={mapboxToken} onViewportChange={newViewport => { this.onViewportChange(newViewport); // eslint-disable-next-line no-unused-expressions !!geocoderOnChange && geocoderOnChange(newViewport); }} options={{ ...geocoderOptions }} /> )} {childrenLayers} </MapGL> </div> ); } } BaseMap.propTypes = { initialLongitude: PropTypes.number, initialLatitude: PropTypes.number, initialZoom: PropTypes.number, initialPitch: PropTypes.number, height: PropTypes.number, containerHeight: PropTypes.number, containerWidth: PropTypes.number, mapboxToken: PropTypes.string, civicMapStyle: PropTypes.string, navigation: PropTypes.bool, locationMarker: PropTypes.bool, locationMarkerCoord: PropTypes.shape({ latitude: PropTypes.number, longitude: PropTypes.number }), geocoder: PropTypes.bool, geocoderOptions: PropTypes.shape({}), geocoderOnChange: PropTypes.func, mapGLOptions: PropTypes.shape({}), children: PropTypes.node, useContainerHeight: PropTypes.bool, updateViewport: PropTypes.bool, animate: PropTypes.bool, animationDuration: PropTypes.number, onBaseMapClick: PropTypes.func, mapboxDataId: PropTypes.string, mapboxData: PropTypes.shape({ type: PropTypes.string, features: PropTypes.arrayOf(PropTypes.shape({})) }), mapboxLayerType: PropTypes.string, mapboxLayerId: PropTypes.string, mapboxLayerOptions: PropTypes.shape({}), scaleBar: PropTypes.bool, scaleBarOptions: PropTypes.shape({ maxWidth: PropTypes.number, units: PropTypes.string }) }; BaseMap.defaultProps = { mapboxToken: MAPBOX_TOKEN, navigation: true, geocoder: false, useContainerHeight: false, updateViewport: true, animate: false, initialLongitude: -122.6765, initialLatitude: 45.5231, initialZoom: 9.5, initialPitch: 0, height: 500, locationMarkerCoord: { latitude: 0, longitude: 0 }, animationDuration: 1000, scaleBar: false }; export default Dimensions()(BaseMap);