From 206035d8f9aa71598eadcc923e4c687af5704f86 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Wed, 28 Feb 2018 15:02:53 -0600 Subject: [PATCH] Update map search to use autosuggest and geocoding (#140) In the previous version of the map search, the crossings visible on the map would simply be filtered out by the search query. This update changes that so: * Setting focus on the search bar replaces the nearby crossings with suggestions * These suggestions include Crossings, Communities, and Geocoded locations * Selecting a suggested crossing: * Centers the map and zooms in on the crossing * Shows the crossing details in the sidebar * Selecting a suggested community: * Filters the map to only show crossings in that community * Sets the map bounds to fit the community * Selecting a geocoded location * Centers the map and zooms in on the location Libraries used: * [react-autosuggest](https://github.com/moroshko/react-autosuggest) * [mapbox-sdk-js](https://github.com/mapbox/mapbox-sdk-js) (for geocoding) --- frontend/package.json | 2 + .../CrossingListItem/CrossingListItem.js | 2 +- .../CrossingStatusHistoryItem.js | 7 +- .../PublicCrossingListItem.js | 2 +- .../Shared/CrossingMapPage/CrossingMapPage.js | 92 ++++-- .../CrossingMapPage/CrossingMapSearchBar.js | 261 ++++++++++++++++-- .../CrossingMapPage/CrossingMapSearchBar.scss | 75 ++++- .../CrossingMapSearchCrossingSuggestions.js | 31 +++ .../CrossingMapPage/CrossingMapSidebar.js | 170 +++++++----- .../CrossingMapPage/CrossingMapSidebar.scss | 21 +- .../CrossingSidebarNearbyCrossingItem.js | 62 +++++ .../CrossingSidebarSearchResultItem.js | 56 ---- .../queries/suggestCrossingsQuery.js | 23 ++ .../src/components/Shared/Map/CrossingMap.js | 96 ++++++- .../Shared/Map/CrossingStaticMap.js | 4 +- frontend/src/components/Shared/StatusIcon.js | 13 + frontend/src/constants/StatusConstants.js | 2 +- frontend/yarn.lock | 45 +++ 18 files changed, 744 insertions(+), 220 deletions(-) create mode 100644 frontend/src/components/Shared/CrossingMapPage/CrossingMapSearchCrossingSuggestions.js create mode 100644 frontend/src/components/Shared/CrossingMapPage/CrossingSidebarNearbyCrossingItem.js delete mode 100644 frontend/src/components/Shared/CrossingMapPage/CrossingSidebarSearchResultItem.js create mode 100644 frontend/src/components/Shared/CrossingMapPage/queries/suggestCrossingsQuery.js create mode 100644 frontend/src/components/Shared/StatusIcon.js diff --git a/frontend/package.json b/frontend/package.json index e667c066..91f4772d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "formatcoords": "^1.1.3", "get-graphql-schema": "^2.1.0", "jwt-decode": "^2.2.0", + "mapbox": "^1.0.0-beta9", "mapbox-gl": "^0.40.0", "mobile-detect": "^1.4.1", "moment": "^2.18.1", @@ -24,6 +25,7 @@ "prop-types": "^15.6.0", "react": "^16.0.0-rc.3", "react-apollo": "^1.4.2", + "react-autosuggest": "^9.3.3", "react-container-query": "^0.9.1", "react-csv": "^1.0.8", "react-dom": "^16.0.0-rc.3", diff --git a/frontend/src/components/Dashboard/CrossingListPage/CrossingListItem/CrossingListItem.js b/frontend/src/components/Dashboard/CrossingListPage/CrossingListItem/CrossingListItem.js index 4339d1fc..d6384409 100644 --- a/frontend/src/components/Dashboard/CrossingListPage/CrossingListItem/CrossingListItem.js +++ b/frontend/src/components/Dashboard/CrossingListPage/CrossingListItem/CrossingListItem.js @@ -520,7 +520,7 @@ class CrossingListItem extends React.Component {
- Status: {statusConstants.strings[this.state.selectedStatus]} + Status: {statusConstants.statusNames[this.state.selectedStatus]}
- {shouldDisplay.status} diff --git a/frontend/src/components/Public/CrossingListItem/PublicCrossingListItem.js b/frontend/src/components/Public/CrossingListItem/PublicCrossingListItem.js index 180653b0..23a03537 100644 --- a/frontend/src/components/Public/CrossingListItem/PublicCrossingListItem.js +++ b/frontend/src/components/Public/CrossingListItem/PublicCrossingListItem.js @@ -55,7 +55,7 @@ class PublicCrossingListItem extends React.Component {
- Status: {statusConstants.strings[crossing.latestStatusId]} + Status: {statusConstants.statusNames[crossing.latestStatusId]}
diff --git a/frontend/src/components/Shared/CrossingMapPage/CrossingMapPage.js b/frontend/src/components/Shared/CrossingMapPage/CrossingMapPage.js index 41fc80ef..d26627ca 100644 --- a/frontend/src/components/Shared/CrossingMapPage/CrossingMapPage.js +++ b/frontend/src/components/Shared/CrossingMapPage/CrossingMapPage.js @@ -23,28 +23,13 @@ class CrossingMapPage extends Component { super(props); // If we have a current user, we're on the dashboard, we should get their viewport - const envelope = this.props.currentUser - ? JSON.parse( - this.props.currentUser.communityByCommunityId.viewportgeojson, - ) - : JSON.parse( - `{"type":"Polygon","coordinates":[[[-98.086914,30.148464],[-98.086914,30.433285],[-97.615974,30.433285],[-97.615974,30.148464],[-98.086914,30.148464]]]}`, - ); - - // I actually like doing the padding here because it keeps the data/view separation - const viewport = [ - [ - Math.min(...envelope.coordinates[0].map(arr => arr[0])) - 0.1, - Math.min(...envelope.coordinates[0].map(arr => arr[1])) - 0.1, - ], - [ - Math.max(...envelope.coordinates[0].map(arr => arr[0])) + 0.1, - Math.max(...envelope.coordinates[0].map(arr => arr[1])) + 0.1, - ], - ]; + const viewportgeojson = this.props.currentUser + ? this.props.currentUser.communityByCommunityId.viewportgeojson + : `{"type":"Polygon","coordinates":[[[-98.086914,30.148464],[-98.086914,30.433285],[-97.615974,30.433285],[-97.615974,30.148464],[-98.086914,30.148464]]]}`; + + const viewportAndCenter = this.getViewportAndCenter(viewportgeojson); this.state = { - viewport: viewport, selectedCrossingId: null, selectedCrossingStatus: null, fullscreen: false, @@ -55,9 +40,39 @@ class CrossingMapPage extends Component { showCaution: true, showLongterm: true, visibleCrossings: [], + selectedLocationCoordinates: null, + selectedCommunity: null, + viewport: viewportAndCenter.viewport, + center: viewportAndCenter.center, + mapCenter: viewportAndCenter.center, }; } + getViewportAndCenter = viewportgeojson => { + const envelope = JSON.parse(viewportgeojson); + + const viewport = [ + [ + Math.min(...envelope.coordinates[0].map(arr => arr[0])) - 0.1, + Math.min(...envelope.coordinates[0].map(arr => arr[1])) - 0.1, + ], + [ + Math.max(...envelope.coordinates[0].map(arr => arr[0])) + 0.1, + Math.max(...envelope.coordinates[0].map(arr => arr[1])) + 0.1, + ], + ]; + + const center = { + lng: (viewport[0][0] + viewport[1][0]) / 2, + lat: (viewport[0][1] + viewport[1][1]) / 2, + }; + + return { + viewport: viewport, + center: center, + }; + }; + formatSearchQuery(query) { return `%${query.replace(/ /g, '%')}%`; } @@ -98,15 +113,40 @@ class CrossingMapPage extends Component { this.setState({ showLongterm: !this.state.showLongterm }); }; + getMapCenter = center => { + this.setState({ mapCenter: center }); + }; + + setSelectedLocationCoordinates = coordinates => { + this.setState({ selectedLocationCoordinates: coordinates }); + }; + + setSelectedCommunity = community => { + this.setState({ selectedCommunity: community }); + if (community && community.viewportgeojson) { + const viewportAndCenter = this.getViewportAndCenter( + community.viewportgeojson, + ); + this.setState({ + viewport: viewportAndCenter.viewport, + center: viewportAndCenter.center, + mapCenter: viewportAndCenter.mapCenter, + }); + } + }; + render() { const { viewport, + mapCenter, selectedCrossingId, selectedCrossingStatus, searchQuery, formattedSearchQuery, visibleCrossings, selectedCrossingName, + selectedLocationCoordinates, + selectedCommunity, } = this.state; const { currentUser } = this.props; const allCommunities = @@ -132,6 +172,7 @@ class CrossingMapPage extends Component { searchQuery={searchQuery} searchQueryUpdated={this.searchQueryUpdated} selectedCrossingName={selectedCrossingName} + setSelectedCommunity={this.setSelectedCommunity} /> )} {params.fullsize && ( @@ -162,6 +203,11 @@ class CrossingMapPage extends Component { toggleShowLongterm={this.toggleShowLongterm} visibleCrossings={visibleCrossings} allCommunities={allCommunities} + center={mapCenter} + setSelectedLocationCoordinates={ + this.setSelectedLocationCoordinates + } + setSelectedCommunity={this.setSelectedCommunity} /> )}
{!params.fullsize && @@ -211,6 +262,7 @@ const allCommunities = gql` nodes { id name + viewportgeojson } } } diff --git a/frontend/src/components/Shared/CrossingMapPage/CrossingMapSearchBar.js b/frontend/src/components/Shared/CrossingMapPage/CrossingMapSearchBar.js index cf583672..49e7298a 100644 --- a/frontend/src/components/Shared/CrossingMapPage/CrossingMapSearchBar.js +++ b/frontend/src/components/Shared/CrossingMapPage/CrossingMapSearchBar.js @@ -1,50 +1,261 @@ import React, { Component } from 'react'; import 'components/Shared/CrossingMapPage/CrossingMapSearchBar.css'; import FontAwesome from 'react-fontawesome'; +import Autosuggest from 'react-autosuggest'; +import MapboxClient from 'mapbox'; +import CrossingMapSearchCrossingSuggestions from 'components/Shared/CrossingMapPage/CrossingMapSearchCrossingSuggestions'; + +const mapboxClient = new MapboxClient( + 'pk.eyJ1IjoiY3Jvd2VhdHgiLCJhIjoiY2o1NDFvYmxkMHhkcDMycDF2a3pseDFpZiJ9.UcnizcFDleMpv5Vbv8Rngw', +); + +// When suggestion is clicked, Autosuggest needs to populate the input +// based on the clicked suggestion. Teach Autosuggest how to calculate the +// input value for every given suggestion. +const getSuggestionValue = suggestion => { + return suggestion.place_name || suggestion.name; +}; + +// Use your imagination to render suggestions. +const Suggestion = suggestion => ( +
+
+ {suggestion.__typename === 'Crossing' && ( + + )} + {suggestion.__typename === 'Community' && ( + + )} + {suggestion.type === 'Feature' && ( + + )} +
+
+ {suggestion.place_name || suggestion.name} +
+
+); + +const renderSectionTitle = section => { + return null; +}; + +const getSectionSuggestions = section => { + return section.suggestions; +}; + +const formatSearchQuery = query => { + return `%${query.replace(/ /g, '%')}%`; +}; class CrossingMapSearchBar extends Component { + constructor() { + super(); + + let autosuggestInput; + + this.state = { + typedValue: '', + selectedValue: '', + mapboxSuggestions: [], + crossingSuggestions: [], + communitySuggestions: [], + }; + } + + onChange = (event, { newValue, method }) => { + if (method === 'type') { + this.setState({ + typedValue: newValue, + selectedValue: null, + }); + } else if (method === 'escape') { + this.setState({ + selectedValue: null, + }); + } else if (method === 'enter' || method === 'click') { + this.setState({ + selectedValue: newValue, + typedValue: newValue, + }); + } else if (method === 'down' || method === 'up') { + this.setState({ + selectedValue: newValue, + }); + } + }; + + onSuggestionSelected = ( + event, + { suggestion, suggestionValue, suggestionIndex, sectionIndex, method }, + ) => { + // If we've selected a crossing, center in on it + if (suggestion.__typename === 'Crossing') { + this.props.selectCrossing(suggestion.id); + } + + // If we've selected a mapbox location, center on it + if (suggestion.type === 'Feature') { + this.props.setSelectedLocationCoordinates(suggestion.center); + } + + // If we've selected a commuity, set the map bounds and filter + if (suggestion.__typename === 'Community') { + this.props.setSelectedCommunity(suggestion); + } + + // Unfocus the search bar + this.autosuggestInput.blur(); + }; + + // Autosuggest will call this function every time you need to update suggestions. + onSuggestionsFetchRequested = ({ value }) => { + const { center, communityId, communities } = this.props; + + const inputValue = value.trim().toLowerCase(); + const inputLength = inputValue.length; + + // Get the suggestions from the mapbox geocoder + if (inputLength > 2) { + mapboxClient.geocodeForward( + inputValue, + { + proximity: { latitude: center.lat, longitude: center.lng }, + // Hardcoding this for now to get better results + // TODO: Design a better solution for accurate geocode results + bbox: [-100, 27, -94, 34], + }, + (err, res) => { + this.setState({ mapboxSuggestions: res.features }); + }, + ); + } else { + this.setState({ mapboxSuggestions: [] }); + } + + // If we aren't filtering by community, get the communities + if (!communityId) { + const communitySuggestions = communities + .filter(c => c.name.toLowerCase().includes(inputValue)) + .slice(0, 4); + this.setState({ communitySuggestions: communitySuggestions }); + } + }; + + // Autosuggest will call this function every time you need to clear suggestions. + onSuggestionsClearRequested = () => { + this.setState({ + mapboxSuggestions: [], + communitySuggestions: [], + }); + }; + clearSearch = () => { this.props.searchQueryUpdated({ target: { value: '' } }); this.props.selectCrossing(null, null); + this.props.setSelectedCommunity(null); + this.setState({ typedValue: '', selectedValue: null }); + }; + + updateCrossingSuggestions = suggestions => { + this.setState({ + crossingSuggestions: suggestions, + }); + }; + + onInputFocus = () => { + this.props.toggleSearchFocus(true); + }; + + onInputBlur = () => { + this.props.toggleSearchFocus(false); }; render() { + const { selectedCrossingId, communityId } = this.props; + const { - searchQuery, - selectedCrossingId, - searchQueryUpdated, - selectedCrossingName, - } = this.props; + typedValue, + selectedValue, + mapboxSuggestions, + crossingSuggestions, + communitySuggestions, + } = this.state; + + const suggestions = [ + { + title: 'Communities', + suggestions: communitySuggestions, + }, + { + title: 'Crossings', + suggestions: crossingSuggestions, + }, + { + title: 'Locations', + suggestions: mapboxSuggestions, + }, + ]; + + const value = selectedValue ? selectedValue : typedValue; + + // Autosuggest will pass through all these props to the input. + const inputProps = { + placeholder: 'Search...', + value, + onChange: this.onChange, + onFocus: this.onInputFocus, + onBlur: this.onInputBlur, + }; + + const formattedQuery = formatSearchQuery(typedValue); return (
+
- SEARCH FOR A PLACE, AREA, OR CROSSING + SEARCH FOR A PLACE, COMMUNITY, OR CROSSING
- {selectedCrossingId && ( -
- {selectedCrossingName} -
- )} - {!selectedCrossingId && ( -
- + {selectedCrossingId && ( +
+ Back to Search +
+ )} + {!selectedCrossingId && ( + { + if (autosuggest !== null) { + this.autosuggestInput = autosuggest.input; + } + }} + suggestions={suggestions} + multiSection={true} + getSectionSuggestions={getSectionSuggestions} + renderSectionTitle={renderSectionTitle} + onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} + onSuggestionsClearRequested={this.onSuggestionsClearRequested} + onSuggestionSelected={this.onSuggestionSelected} + onSuggestionHighlighted={this.onSuggestionHighlighted} + getSuggestionValue={getSuggestionValue} + renderSuggestion={Suggestion} + inputProps={inputProps} + shouldRenderSuggestions={() => true} + focusInputOnSuggestionClick={false} /> -
- )} - {!selectedCrossingId && ( -
- -
- )} + )} +
({ + variables: { + search: ownProps.searchQuery, + communityId: ownProps.communityId, + }, + }), +})(CrossingMapSearchCrossingSuggestions); diff --git a/frontend/src/components/Shared/CrossingMapPage/CrossingMapSidebar.js b/frontend/src/components/Shared/CrossingMapPage/CrossingMapSidebar.js index b27324a0..f0fa26e3 100644 --- a/frontend/src/components/Shared/CrossingMapPage/CrossingMapSidebar.js +++ b/frontend/src/components/Shared/CrossingMapPage/CrossingMapSidebar.js @@ -1,11 +1,23 @@ import React, { Component } from 'react'; import SelectedCrossingContainer from 'components/Shared/CrossingMapPage/SelectedCrossingContainer'; import CrossingMapSearchBar from 'components/Shared/CrossingMapPage/CrossingMapSearchBar'; -import CrossingSidebarSearchResultItem from 'components/Shared/CrossingMapPage/CrossingSidebarSearchResultItem'; +import CrossingSidebarNearbyCrossingItem from 'components/Shared/CrossingMapPage/CrossingSidebarNearbyCrossingItem'; import 'components/Shared/CrossingMapPage/CrossingMapSidebar.css'; import FontAwesome from 'react-fontawesome'; import classnames from 'classnames'; +const FilterCheckbox = ({defaultChecked, onClick, title}) => ( + +) + class CrossingMapSidebar extends Component { constructor(props) { super(props); @@ -13,6 +25,7 @@ class CrossingMapSidebar extends Component { this.state = { visible: true, showFilters: false, + searchFocused: false, }; } @@ -24,8 +37,12 @@ class CrossingMapSidebar extends Component { this.setState({ showFilters: !this.state.showFilters }); }; + toggleSearchFocus = focused => { + this.setState({ searchFocused: focused }); + }; + render() { - const { visible } = this.state; + const { visible, searchFocused } = this.state; const { toggleShowOpen, toggleShowClosed, @@ -43,6 +60,9 @@ class CrossingMapSidebar extends Component { visibleCrossings, allCommunities, selectedCrossingName, + center, + setSelectedLocationCoordinates, + setSelectedCommunity, } = this.props; return ( @@ -55,83 +75,83 @@ class CrossingMapSidebar extends Component { searchQuery={searchQuery} searchQueryUpdated={searchQueryUpdated} selectedCrossingName={selectedCrossingName} + center={center} + setSelectedLocationCoordinates={setSelectedLocationCoordinates} + toggleSearchFocus={this.toggleSearchFocus} + communities={allCommunities} + communityId={currentUser && currentUser.communityId} + setSelectedCommunity={setSelectedCommunity} /> - {selectedCrossingId && ( - - )} -
-
-
- {this.state.showFilters ? ( - - ) : ( - - )}{' '} - FILTER -
-
-
- {this.state.showFilters && ( -
- - - - + )} +
+
+
+ {this.state.showFilters ? ( + + ) : ( + + )}{' '} + FILTER +
+
+
+ {this.state.showFilters && ( +
+ + + + +
+ )} +
+ {visibleCrossings.map(c => ( + + ))} +
)} -
- {visibleCrossings.map(c => ( - - ))} -
)}
selectCrossing(crossingId)} + > +
+ +
+
+
+ {statusNames[statusId]} +
+
+ {crossingName} +
+
+ {allCommunities && + communityIds + .map(id => allCommunities.find(c => c.id === id).name) + .join(', ')} +
+
+
+
+ {moment(latestStatus).calendar(null, { + lastDay: '[Yesterday]', + sameDay: '[Today]', + sameElse: 'MM/DD/YYYY', + })} +
+
+ {moment(latestStatus).format('h:mm A')} +
+
+
+ ); + } +} + +export default CrossingSidebarNearbyCrossingItem; diff --git a/frontend/src/components/Shared/CrossingMapPage/CrossingSidebarSearchResultItem.js b/frontend/src/components/Shared/CrossingMapPage/CrossingSidebarSearchResultItem.js deleted file mode 100644 index 42b44224..00000000 --- a/frontend/src/components/Shared/CrossingMapPage/CrossingSidebarSearchResultItem.js +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import 'components/Shared/CrossingMapPage/CrossingMapPage.css'; -import { strings, statusIcons } from 'constants/StatusConstants'; -import moment from 'moment'; - -class CrossingSidebarSearchResultItem extends React.Component { - render() { - const { - latestStatus, - statusId, - crossingName, - communityIds, - allCommunities, - } = this.props; - - return ( -
-
- {strings[statusId]} -
-
-
- {strings[statusId]} -
-
- {crossingName} -
-
- {allCommunities && - communityIds - .map(id => allCommunities.find(c => c.id === id).name) - .join(', ')} -
-
-
-
- {moment(latestStatus).calendar(null, { - lastDay: '[Yesterday]', - sameDay: '[Today]', - sameElse: 'MM/DD/YYYY', - })} -
-
- {moment(latestStatus).format('h:mm A')} -
-
-
- ); - } -} - -export default CrossingSidebarSearchResultItem; diff --git a/frontend/src/components/Shared/CrossingMapPage/queries/suggestCrossingsQuery.js b/frontend/src/components/Shared/CrossingMapPage/queries/suggestCrossingsQuery.js new file mode 100644 index 00000000..7782f9a4 --- /dev/null +++ b/frontend/src/components/Shared/CrossingMapPage/queries/suggestCrossingsQuery.js @@ -0,0 +1,23 @@ +import gql from 'graphql-tag'; + +const suggestCrossings = gql` + query suggestCrossings($search: String, $communityId: Int) { + searchCrossings( + search: $search + communityId: $communityId + showOpen: true + showClosed: true + showCaution: true + showLongterm: true + first: 5 + ) { + nodes { + id + name + geojson + } + } + } +`; + +export default suggestCrossings; diff --git a/frontend/src/components/Shared/Map/CrossingMap.js b/frontend/src/components/Shared/Map/CrossingMap.js index b00f3a6c..5ffdc193 100644 --- a/frontend/src/components/Shared/Map/CrossingMap.js +++ b/frontend/src/components/Shared/Map/CrossingMap.js @@ -19,17 +19,29 @@ class CrossingMap extends React.Component { selectedCrossingId: -1, // Mapbox filters don't support null values selectedCrossing: null, selectedCrossingCoordinates: null, + selectedLocationCoordinates: null, firstLoadComplete: false, - center: [ - (this.props.viewport[0][0] + this.props.viewport[1][0]) / 2, - (this.props.viewport[0][1] + this.props.viewport[1][1]) / 2, - ], }; componentWillReceiveProps(nextProps) { + // If we've selected a crossing if (nextProps.selectedCrossingId !== this.props.selectedCrossingId) { if (nextProps.selectedCrossingId) { this.setState({ selectedCrossingId: nextProps.selectedCrossingId }); + const crossing = + this.props.openCrossings.searchCrossings.nodes.find( + c => c.id === nextProps.selectedCrossingId, + ) || + this.props.closedCrossings.searchCrossings.nodes.find( + c => c.id === nextProps.selectedCrossingId, + ) || + this.props.cautionCrossings.searchCrossings.nodes.find( + c => c.id === nextProps.selectedCrossingId, + ) || + this.props.cautionCrossings.searchCrossings.nodes.find( + c => c.id === nextProps.selectedCrossingId, + ); + this.selectCrossing(crossing); } else { this.setState({ selectedCrossingId: -1 }); this.setState({ selectedCrossing: null }); @@ -46,6 +58,19 @@ class CrossingMap extends React.Component { this.setState({ selectedCrossing: selectedCrossing }); } + // If we are selecting a location, fly to it + if ( + nextProps.selectedLocationCoordinates !== + this.state.selectedLocationCoordinates + ) { + this.setState({ + selectedLocationCoordinates: nextProps.selectedLocationCoordinates, + }); + if (nextProps.selectedLocationCoordinates) { + this.flyTo(nextProps.selectedLocationCoordinates); + } + } + // This is a slightly strange litle fix here, we used to check loading in render, and not render the map until it loaded // that worked well for a single query, but led to the map disappearing on search. I then updated it to hide the crossing // layers instead of hiding the whole map on load, but this led to the map not correctly filling the containing div. By checking @@ -72,6 +97,10 @@ class CrossingMap extends React.Component { this.addGeoLocateControl(map); this.addCrossingClickHandlers(map); this.addUpdateVisibleCrossingHandlers(map); + + // update the map page center on map move + map.on('moveend', this.getMapCenter); + // disable map rotation using right click + drag map.dragRotate.disable(); @@ -104,6 +133,23 @@ class CrossingMap extends React.Component { map.on('data', this.updateVisibleCrossings); } + getMapCenter = () => { + const { map } = this.state; + const center = map.getCenter(); + + this.props.getMapCenter(center); + }; + + flyTo = point => { + const { map } = this.state; + if (map) { + map.flyTo({ + center: point, + zoom: 13, + }); + } + }; + updateVisibleCrossings = e => { if (e.type === 'data' && !e.isSourceLoaded) return; @@ -148,13 +194,35 @@ class CrossingMap extends React.Component { map.on('click', this.onMapClick); } + selectCrossing = crossing => { + const coordinates = JSON.parse(crossing.geojson).coordinates; + + const mapCrossing = { + crossingId: crossing.id, + crossingName: crossing.name, + crossingStatus: crossing.latestStatusId, + geojson: crossing.geojson, + }; + + this.setState({ + selectedCrossingCoordinates: coordinates, + selectedCrossing: mapCrossing, + }); + this.flyTo(coordinates); + this.props.selectCrossing( + crossing.id, + crossing.latestStatusId, + crossing.name, + ); + }; + onCrossingClick = crossing => { this.setState({ selectedCrossingId: crossing.properties.crossingId }); this.setState({ selectedCrossing: crossing.properties }); this.setState({ selectedCrossingCoordinates: crossing.geometry.coordinates, }); - this.setState({ center: crossing.geometry.coordinates }); + this.flyTo(crossing.geometry.coordinates); this.props.selectCrossing( crossing.properties.crossingId, crossing.properties.crossingStatus, @@ -214,7 +282,13 @@ class CrossingMap extends React.Component { ? this.props.longtermCrossings.searchCrossings.nodes : null; - const { showOpen, showClosed, showCaution, showLongterm } = this.props; + const { + showOpen, + showClosed, + showCaution, + showLongterm, + center, + } = this.props; return ( {!isLoading && showOpen && ( @@ -471,7 +545,7 @@ export default compose( ownProps.currentUser && ownProps.currentUser.role !== 'floods_super_admin' ? ownProps.currentUser.communityId - : null, + : ownProps.selectedCommunityId, }, }), }), @@ -488,7 +562,7 @@ export default compose( ownProps.currentUser && ownProps.currentUser.role !== 'floods_super_admin' ? ownProps.currentUser.communityId - : null, + : ownProps.selectedCommunityId, }, }), }), @@ -505,7 +579,7 @@ export default compose( ownProps.currentUser && ownProps.currentUser.role !== 'floods_super_admin' ? ownProps.currentUser.communityId - : null, + : ownProps.selectedCommunityId, }, }), }), @@ -522,7 +596,7 @@ export default compose( ownProps.currentUser && ownProps.currentUser.role !== 'floods_super_admin' ? ownProps.currentUser.communityId - : null, + : ownProps.selectedCommunityId, }, }), }), diff --git a/frontend/src/components/Shared/Map/CrossingStaticMap.js b/frontend/src/components/Shared/Map/CrossingStaticMap.js index bebb1ae0..00a62396 100644 --- a/frontend/src/components/Shared/Map/CrossingStaticMap.js +++ b/frontend/src/components/Shared/Map/CrossingStaticMap.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import ReactMapboxGl, { Marker } from 'react-mapbox-gl'; import mapboxstyle from 'components/Shared/Map/mapboxstyle.json'; -import { statusIcons } from 'constants/StatusConstants'; +import StatusIcon from 'components/Shared/StatusIcon'; const Map = ReactMapboxGl({ accessToken: null, interactive: false }); @@ -23,7 +23,7 @@ class CrossingStaticMap extends Component { onStyleLoad={this.onStyleLoad} > - {status} + ); diff --git a/frontend/src/components/Shared/StatusIcon.js b/frontend/src/components/Shared/StatusIcon.js new file mode 100644 index 00000000..dd372189 --- /dev/null +++ b/frontend/src/components/Shared/StatusIcon.js @@ -0,0 +1,13 @@ +import React from 'react'; + +import { statusNames, statusIcons } from 'constants/StatusConstants'; + +export default function StatusIcon({ statusId, ...props }) { + return ( + {statusNames[statusId]} + ); +} diff --git a/frontend/src/constants/StatusConstants.js b/frontend/src/constants/StatusConstants.js index eaa474c2..b40e90c9 100644 --- a/frontend/src/constants/StatusConstants.js +++ b/frontend/src/constants/StatusConstants.js @@ -12,7 +12,7 @@ export const CLOSED = 2; export const CAUTION = 3; export const LONGTERM = 4; -export const strings = { +export const statusNames = { 1: 'Open', 2: 'Closed', 3: 'Caution', diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 264a1006..affde866 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -5863,6 +5863,13 @@ mapbox-gl@^0.40.0: vt-pbf "^3.0.1" webworkify "^1.4.0" +mapbox@^1.0.0-beta9: + version "1.0.0-beta9" + resolved "https://registry.yarnpkg.com/mapbox/-/mapbox-1.0.0-beta9.tgz#580bbacd9990bbe10f2f729ff4031a3b898d27a4" + dependencies: + es6-promise "^4.0.5" + rest "^2.0.0" + mapnik-vector-tile@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/mapnik-vector-tile/-/mapnik-vector-tile-1.4.0.tgz#f46742514cd3ad3554c5d640614804fe9beb49e5" @@ -6397,6 +6404,10 @@ object-assign@4.1.1, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^ version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" +object-assign@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" + object-hash@^1.1.4: version "1.1.8" resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.1.8.tgz#28a659cf987d96a4dabe7860289f3b5326c4a03c" @@ -7359,6 +7370,22 @@ react-apollo@^1.4.2: object-assign "^4.0.1" prop-types "^15.5.8" +react-autosuggest@^9.3.3: + version "9.3.3" + resolved "https://registry.yarnpkg.com/react-autosuggest/-/react-autosuggest-9.3.3.tgz#400a9173d291380daa625a599dcbf5cf1c908d01" + dependencies: + prop-types "^15.5.10" + react-autowhatever "^10.1.0" + shallow-equal "^1.0.0" + +react-autowhatever@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/react-autowhatever/-/react-autowhatever-10.1.0.tgz#41f6d69382437d3447a0a3c8913bb8ca2feaabc1" + dependencies: + prop-types "^15.5.8" + react-themeable "^1.1.0" + section-iterator "^2.0.0" + react-container-query@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/react-container-query/-/react-container-query-0.9.1.tgz#57136d98c1ada18084b27cb45a0006a9f03a95d6" @@ -7635,6 +7662,12 @@ react-test-renderer@^16.0.0-rc.3: fbjs "^0.8.9" object-assign "^4.1.0" +react-themeable@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/react-themeable/-/react-themeable-1.1.0.tgz#7d4466dd9b2b5fa75058727825e9f152ba379a0e" + dependencies: + object-assign "^3.0.0" + react-transition-group@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.2.0.tgz#b51fc921b0c3835a7ef7c571c79fc82c73e9204f" @@ -8087,6 +8120,10 @@ resolve@^1.1.5, resolve@^1.1.6, resolve@^1.2.0, resolve@^1.3.2, resolve@~1.4.0: dependencies: path-parse "^1.0.5" +rest@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/rest/-/rest-2.0.0.tgz#6dfadf66a405c49cfbd5b4bd25b59fd29cd861bc" + restore-cursor@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" @@ -8214,6 +8251,10 @@ scss-tokenizer@^0.2.3: js-base64 "^2.1.8" source-map "^0.4.2" +section-iterator@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/section-iterator/-/section-iterator-2.0.0.tgz#bf444d7afeeb94ad43c39ad2fb26151627ccba2a" + seedrandom@^2.4.2: version "2.4.3" resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-2.4.3.tgz#2438504dad33917314bff18ac4d794f16d6aaecc" @@ -8325,6 +8366,10 @@ shallow-copy@~0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/shallow-copy/-/shallow-copy-0.0.1.tgz#415f42702d73d810330292cc5ee86eae1a11a170" +shallow-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.0.0.tgz#508d1838b3de590ab8757b011b25e430900945f7" + shallowequal@0.2.x, shallowequal@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-0.2.2.tgz#1e32fd5bcab6ad688a4812cb0cc04efc75c7014e"