Skip to content

Instantly share code, notes, and snippets.

@mbritton
Created April 16, 2017 17:46
Show Gist options
  • Save mbritton/74853d2a1678110f824bcb82d34ac078 to your computer and use it in GitHub Desktop.
Save mbritton/74853d2a1678110f824bcb82d34ac078 to your computer and use it in GitHub Desktop.
UI for map view of an application using Leaflet, React and Redux
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