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