Created
April 16, 2017 17:46
-
-
Save mbritton/74853d2a1678110f824bcb82d34ac078 to your computer and use it in GitHub Desktop.
UI for map view of an application using Leaflet, React and Redux
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { Component } from 'react' | |
import { ReactDOM, render } from 'react-dom' | |
import { MapComponent, Map, Marker, Popup, TileLayer, GeoJson, LayerGroup, FeatureGroup, Circle } from 'react-leaflet' | |
import { showElevationChart, setZoomSnap, progressEventAction, sendLatLons } from '../actions/applicationActions.js' | |
import { fetchCountyData } from '../actions/countyActions.js' | |
import { fetchRoutes, toggleSelectedRoute, loadRoute, hideRoutes, showRoutes } from '../actions/routesActions.js' | |
import { fetchTrailheadMarkers, getToolTip, setLeafletMarkers, doMarkerSelected } from '../actions/markersActions.js' | |
import { setMap, setMapControls, addRoute, clearPopups, clearRoute, routeLoaded, zoomToCounties, addZoomControl, doBalloon, | |
setCirclePosition, setCircleOpacity, deselectUnpinnedRoutes, zoomSelectedRoutes, addLabels } from '../actions/mapActions.js' | |
import { connect } from 'react-redux' | |
import { ATLBikeFeatureGroup } from './ATLBikeFeatureGroup.js' | |
import ATLBikeMarker from './ATLBikeMarker.js' | |
const mapStateToProps = (state) => { | |
return { | |
circleOpacity: state.map.circleOpacity, | |
countyBounds: state.county.countyBounds, | |
countyData: state.county.countyData, | |
routes: state.routes, | |
markers: state.markers.markers, | |
selectedMarker: state.markers.selectedMarker, | |
selectedRoute: state.routes.selectedRoute, | |
mode: state.application.mode, | |
toolTip: state.markers.toolTip, | |
map:state.map, | |
mapLoaded: state.map.mapLoaded, | |
atlBikeMarkers: state.markers.leafletMarkers, | |
trailheadIcon: state.markers.trailheadIcon, | |
circlePosition: state.map.circlePosition, | |
doMarkerSelected: state.markers.doMarkerSelected, | |
showElevationChart: state.application.showChart, | |
zoomSnap: state.application.zoomSnap, | |
pinnedRoutes: state.routes.pinnedRoutes, | |
unPinnedFileName: state.routes.unPinnedFileName, | |
point: state.application.point, | |
siteURL: state.application.siteURL | |
} | |
} | |
const mapDispatchToProps = (dispatch) => { | |
return { | |
addSCTToMap: (gpx) => { | |
dispatch(addRoute(gpx.target, true)) | |
}, | |
addRouteToMap: (gpx, isSCT, pinnedRoutes) => { | |
dispatch(addRoute(gpx, isSCT, pinnedRoutes)) | |
}, | |
addZoomControl: (zoomControl) => { | |
dispatch(addZoomControl(zoomControl)) | |
}, | |
setCirclePosition: (latLon) => { | |
dispatch(setCirclePosition(latLon)) | |
}, | |
clearPopups: () => { | |
dispatch(clearPopups()) | |
}, | |
fetchCountyData: () => { | |
dispatch(fetchCountyData()) | |
}, | |
fetchRoutes: () => { | |
dispatch(fetchRoutes()) | |
}, | |
fetchTrailheadMarkers: () => { | |
dispatch(fetchTrailheadMarkers()) | |
}, | |
setMap: (map) => { | |
dispatch(setMap(map)) | |
}, | |
removeRouteFromMap: (route) => { | |
dispatch(clearRoute(route)) | |
}, | |
routeLoaded: () => { | |
dispatch(routeLoaded()) | |
dispatch(toggleSelectedRoute()) | |
}, | |
zoomToCounties: (countyBounds) => { | |
dispatch(zoomToCounties(countyBounds)) | |
}, | |
setLeafletMarkers: (markers) => { | |
dispatch(setLeafletMarkers(markers)) | |
}, | |
doBalloon: (mapLayer) => { | |
dispatch(doBalloon(mapLayer)) | |
}, | |
selectMarker: (doIt) => { | |
dispatch(doMarkerSelected(doIt)) | |
}, | |
setCircleOpacity: (circleOpacity) => { | |
dispatch(setCircleOpacity(circleOpacity)) | |
}, | |
setMapControls: () => { | |
dispatch(setMapControls()) | |
}, | |
showChart: () => { | |
dispatch(showElevationChart()) | |
}, | |
setZoomSnap: (doZoomSnap) => { | |
dispatch(setZoomSnap(doZoomSnap)) | |
}, | |
setProgressEventAction: actObj => { | |
dispatch(progressEventAction(actObj)) | |
}, | |
loadRoute: (route) => { | |
dispatch(loadRoute(route)) | |
}, | |
deselectUnpinnedRoutes: (pinnedRoutes, selectedRoute) => { | |
dispatch(deselectUnpinnedRoutes(pinnedRoutes, selectedRoute)) | |
}, | |
zoomSelectedRoutes: () => { | |
dispatch(zoomSelectedRoutes()) | |
}, | |
addLabels: () => { | |
dispatch(addLabels()) | |
}, | |
hideRoutes: () => { | |
dispatch(hideRoutes()) | |
}, | |
showRoutes: () => { | |
dispatch(showRoutes()) | |
} | |
} | |
} | |
const styles = { | |
stroke: false, | |
color: '#fff', | |
weight: 1, | |
opacity: 1 | |
} | |
export class LeafletMap extends Component { | |
constructor(props) { | |
super(props) | |
this.options = { | |
previousPage:null, | |
previousCircleCenter:0, | |
markersLoaded:false, | |
tileURL: 'http://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', | |
selectedRouteFlag:true, | |
sctAdded:false, | |
markerFacade:null, | |
markerSelected:false | |
} | |
this.componentDidUpdate = this.componentDidUpdate.bind(this) | |
this.onEachCountyFeature = this.onEachCountyFeature.bind(this) | |
this.zoomToFeature = this.zoomToFeature.bind(this) | |
this.onMapClick = this.onMapClick.bind(this) | |
this.updateSelectedMarker = this.updateSelectedMarker.bind(this) | |
} | |
componentDidMount() { | |
this.props.setMap(this.refs['atlMap'].leafletElement) | |
} | |
componentDidUpdate() { | |
// Add Silver Comet route, always happens first | |
if (this.props.mapLoaded && !this.options.sctAdded) { | |
this.options.sctAdded = true | |
this.props.setProgressEventAction({type: 'loading', show: true}) | |
new L.GPX(this.props.siteURL + '/assets/gpx/sct_whole.gpx', { | |
async: true, | |
polyline_options: { | |
clickable: true, | |
color: 'rgba(242, 101, 34, 1)', | |
weight: 4, | |
opacity:1 | |
}, | |
marker_options: { | |
weight: 2, | |
startIconUrl: './assets/images/pin-icon-start.png', | |
endIconUrl: './assets/images/pin-icon-end.png', | |
shadowUrl: './assets/images/pin-shadow.png' | |
} | |
}).on('loaded', (response) => { | |
this.props.setProgressEventAction({type: 'loading', show: false}) | |
this.props.setZoomSnap(false) | |
this.props.setMapControls() | |
this.props.addSCTToMap(response) | |
}) | |
} | |
if (this.props.selectedRoute !== null && this.props.selectedRoute.loadedGPX !== undefined) { | |
let fg = null | |
let gpx2Load = this.props.selectedRoute.loadedGPX | |
let fileName = null | |
let pos = 0 | |
let arstr = [] | |
this.props.setZoomSnap(true) | |
// TODO: move to reducer | |
if (gpx2Load !== null) { | |
this.props.addRouteToMap(gpx2Load, false, this.props.pinnedRoutes) | |
} | |
this.props.showChart() | |
// Toggle selectedRoute and unPineddFileName back to null to avoid loop and re-add, respectively. | |
this.props.routeLoaded() | |
// What's the selected route? | |
this.props.deselectUnpinnedRoutes(this.props.pinnedRoutes, this.props.selectedRoute) | |
this.props.zoomSelectedRoutes() | |
this.props.addLabels() | |
} | |
// Add new route if a new one has been selected | |
if (this.props.selectedRoute !== null && this.props.selectedRoute.loadedGPX === undefined && this.props.map.routeAdded) { | |
this.props.loadRoute(this.props.selectedRoute) | |
} | |
// Routing | |
if (this.props.mode !== null && this.props.mode !== undefined && this.props.mode !== 'welcome') { | |
if (this.props.mode !== this.options.previousPage) { | |
this.props.mode === 'trailheads' ? this.props.hideRoutes() : null | |
if (this.props.mode === 'routes') { | |
this.props.showRoutes() | |
} | |
this.refs.atlMap.zoom = 10 | |
this.props.clearPopups() | |
} | |
this.options.previousPage = this.props.mode | |
} | |
// Add markers | |
if (this.props.markers.markers !== undefined && !this.options.markersLoaded) { | |
this.updateMarkers() | |
if (!this.props.map.zoomControl) { | |
this.props.addZoomControl(L.control.zoom({ | |
position:'bottomright' | |
})) | |
} | |
this.options.markersLoaded = true | |
} | |
if (this.props.selectedMarker !== null && this.props.selectedMarker !== undefined && this.props.doMarkerSelected === false) { | |
if (this.props.mode === 'trailheads') { | |
this.updateSelectedMarker() | |
this.props.selectMarker(true) | |
} | |
} | |
} | |
componentWillUnMount() { | |
// TODO: implement | |
} | |
componentWillMount() { | |
this.props.fetchCountyData() | |
this.props.fetchRoutes() | |
this.props.fetchTrailheadMarkers() | |
} | |
componentWillUpdate() {} | |
highlightCounty(event) { | |
const layer = event.target | |
layer.setStyle({ | |
weight: 1, | |
color: '#fff', | |
dashArray: '3', | |
fillOpacity: .07, | |
dashArray: '3' | |
}) | |
if (!L.Browser.ie && !L.Browser.opera) { | |
layer.bringToFront() | |
} | |
} | |
onEachCountyFeature(feature, layer) { | |
const _ = this | |
let label | |
if (feature !== undefined && layer !== undefined) { | |
label = new L.marker(layer.getBounds().getCenter(), { | |
icon: L.divIcon({ | |
className: 'county-label', | |
html: feature.properties.name, | |
iconSize: [100, 40] | |
}) | |
}).addTo(this.props.map.map) | |
layer.on({ | |
mouseover: function(e) { | |
// console.dir(e.target.toGeoJSON()); | |
//_.highlightCounty(e) | |
this.bringToBack() | |
}, | |
mouseout: function(e) { | |
//_.resetCountyHighlight(e) | |
this.bringToBack() | |
}, | |
click: function(e) { | |
// console.dir(e.target.toGeoJSON()); | |
_.zoomToFeature(e) | |
this.bringToBack() | |
} | |
}) | |
} | |
} | |
onMapClick() { | |
this.props.map.map.fitBounds(this.props.countyBounds) | |
} | |
resetCountyHighlight(evt) { | |
let layer = evt.target | |
layer.setStyle({ | |
color: '#fff', | |
fillOpacity: 0 | |
}) | |
if (!L.Browser.ie && !L.Browser.opera) { | |
layer.bringToFront() | |
} | |
} | |
styleGeoJson() { | |
return { | |
fillColor: '#fff', | |
weight: 1, | |
opacity: .5, | |
color: 'rgb(255, 255, 255)', | |
dashArray: '3', | |
fillOpacity: .1 | |
} | |
} | |
updateMarkers() { | |
let muiMarkers = [], muiMapLayer | |
for (var i=0; i<this.props.markers.markers.length; i++) { | |
let position = [this.props.markers.markers[i]['lat'],this.props.markers.markers[i]['lon']] | |
let toolTipTemplate = '<li>{name}</li>' | |
let items = [] | |
let toolTipData = { | |
type: this.props.markers.markers[i]['lat'], | |
name: this.props.markers.markers[i]['name'], | |
lat: this.props.markers.markers[i]['lat'], | |
lon: this.props.markers.markers[i]['lon'] | |
} | |
let toolTipStr = L.Util.template(toolTipTemplate, toolTipData) | |
muiMapLayer = <ATLBikeMarker key={i} position={position} icon={this.props.trailheadIcon} content={toolTipStr} /> | |
muiMarkers.push(muiMapLayer) | |
} | |
this.props.setLeafletMarkers(muiMarkers) | |
} | |
updateSelectedMarker() { | |
this.refs.atlMap.zoom = 10 | |
if (this.props.selectedMarker !== undefined) { | |
for (var i = 0; i < this.props.markers.markers.length; i++) { | |
var latlng = new L.LatLng(this.props.markers.markers[i]['lat'], this.props.markers.markers[i]['lon']) | |
if (latlng.lat === this.props.selectedMarker.lat && latlng.lng === this.props.selectedMarker.lon) { | |
this.props.setCirclePosition(latlng) | |
this.props.map.map.panTo(latlng) | |
} | |
} | |
} | |
} | |
zoomToFeature(evt) { | |
if (this.props.zoomSnap) { | |
this.props.map.map.fitBounds(evt.target.getBounds()) | |
} | |
} | |
render() { | |
return ( | |
<div> | |
<Map className={this.props.styleName} attributionControl={false} zoomControl={false} minZoom={9} zoom={10} ref="atlMap" bounds={this.props.countyBounds} onclick={this.onMapClick}> | |
<Circle ref="circle" zDepth={5} center={this.props.circlePosition} radius={2500} stroke={true} color='rgb(0, 188, 212)' weight="2" opacity={this.props.circleOpacity} fill={false} /> | |
<ATLBikeMarker ref="svMarker" position={this.props.point} icon={this.props.trailheadIcon} /> | |
<TileLayer opacity={1} id="tileLayer00" url={this.options.tileURL} /> | |
<ATLBikeFeatureGroup> | |
<GeoJson ref="featureg" onEachFeature={this.onEachCountyFeature} data={this.props.countyData} style={this.styleGeoJson}></GeoJson> | |
</ATLBikeFeatureGroup> | |
{this.props.atlBikeMarkers} | |
</Map> | |
</div> | |
) | |
} | |
} | |
const connData = connect( | |
mapStateToProps, | |
mapDispatchToProps | |
)(LeafletMap) | |
export default connData | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment