From 707c0f24b3d9cc1d5fcc15f8e478fd2027563c5e Mon Sep 17 00:00:00 2001 From: Chris Henrick Date: Sun, 28 Jan 2018 16:39:16 -0800 Subject: [PATCH 01/12] fix error of geojson poly detection in LeafletMap --- src/components/LeafletMap/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/LeafletMap/index.js b/src/components/LeafletMap/index.js index 6317b66..65b3f04 100644 --- a/src/components/LeafletMap/index.js +++ b/src/components/LeafletMap/index.js @@ -108,7 +108,7 @@ class LeafletMap extends Component { this.props.fetchGeoPolygons(geo); } - if (geojson.features.length) { + if (geojson && geojson.features && geojson.features.length) { if ( (geojson.geoName !== this.props.geojson.geoName) || (geo !== 'citywide' && this.props.geo === 'citywide') || From ae31283f31fe138c1284e5a7652719ab31ec6902 Mon Sep 17 00:00:00 2001 From: Chris Henrick Date: Sun, 28 Jan 2018 17:00:37 -0800 Subject: [PATCH 02/12] added start of location search component --- scss/components/_search.scss | 13 +++++++++++ scss/main.scss | 1 + src/components/App.js | 2 ++ src/components/Search/index.js | 42 ++++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 scss/components/_search.scss create mode 100644 src/components/Search/index.js diff --git a/scss/components/_search.scss b/scss/components/_search.scss new file mode 100644 index 0000000..4e0f2a2 --- /dev/null +++ b/scss/components/_search.scss @@ -0,0 +1,13 @@ +.Search { + width: 300px; + height: 38px + $padding-10 * 2; + position: absolute; + top: $margin-5 + $app-header-height; + left: $margin-25 + $zoom-controls-width + $padding-10; + z-index: 5; + padding-right: 10px; + + input { + width: 100%; + } +} diff --git a/scss/main.scss b/scss/main.scss index 254a2e9..b4058c0 100644 --- a/scss/main.scss +++ b/scss/main.scss @@ -37,3 +37,4 @@ @import 'components/modal'; @import 'components/small-device-message'; @import 'components/about-copy'; +@import 'components/search'; diff --git a/src/components/App.js b/src/components/App.js index f56d9dd..15903eb 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -10,6 +10,7 @@ import OptionsFiltersConnected from '../containers/OptionsFiltersConnected'; import ModalConnected from '../containers/ModalConnected'; import SmallDeviceMessage from './SmallDeviceMessage'; import LoadingIndicator from './LoadingIndicator'; +import Search from './Search'; class App extends Component { constructor() { @@ -96,6 +97,7 @@ class App extends Component { : [ , + , { + const { inputText } = this.state; + + e.preventDefault(); + + if (inputText && inputText.length) { + // TO DO + } + } + + handleChange = (e) => { + this.setState({ + inputText: e.target.value, + }); + } + + render() { + const { inputText } = this.state; + + return ( +
+
+ +
+
+ ); + } +} + +export default Search; From 4cf041719d5d27305bbb9ce3bd4f820a58d6d1a0 Mon Sep 17 00:00:00 2001 From: Chris Henrick Date: Sun, 28 Jan 2018 17:01:49 -0800 Subject: [PATCH 03/12] added actions & reducer for handling geocoding --- src/actions/async_actions.js | 49 ++++++++++++++++++++++++++++- src/constants/action_types.js | 5 +++ src/constants/app_config.js | 3 ++ src/reducers/geocodeReducer.js | 57 ++++++++++++++++++++++++++++++++++ src/reducers/index.js | 2 ++ 5 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 src/reducers/geocodeReducer.js diff --git a/src/actions/async_actions.js b/src/actions/async_actions.js index 5a474e5..3442185 100644 --- a/src/actions/async_actions.js +++ b/src/actions/async_actions.js @@ -1,6 +1,6 @@ import { polyfill } from 'es6-promise'; import fetch from 'isomorphic-fetch'; -import { cartoSQLQuery } from '../constants/app_config'; +import { cartoSQLQuery, geocodingK } from '../constants/app_config'; import * as actions from '../constants/action_types'; import { configureStatsSQL, @@ -171,3 +171,50 @@ export const fetchGeoPolygons = (geo) => { .catch(error => dispatch(receiveGeoPolygonsError(error))); }; }; + + +// address geocode +// we are about to make a GET request to geocode a location +const locationGeocodeRequest = searchTerm => ({ + type: actions.LOCATION_GEOCODE_REQUEST, + searchTerm +}); + +// we have JSON data representing the geocoded location +const locationGeocodeSuccess = json => ({ + type: actions.LOCATION_GEOCODE_SUCCESS, + json +}); + +// we encountered an error geocoding the location +export const locationGeocodeError = error => ({ + type: actions.LOCATION_GEOCODE_ERROR, + error +}); + +/* + * Redux Thunk action creator to fetch geocoded JSON for a given location / address + * @param {string} location: A URI encoded string representing an address, + * e.g. "1600+Amphitheatre+Parkway,+Mountain+View,+CA" +*/ +export const fetchLocationGeocode = (searchTerm) => { + const searchTermEncoded = encodeURIComponent(searchTerm); + const viewportBias = encodeURIComponent('40.485604,-74.284058|40.935303,-73.707275'); + const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${searchTermEncoded}&bounds=${viewportBias}&key=${geocodingK}`; + + return (dispatch) => { + dispatch(locationGeocodeRequest(searchTerm)); + return fetch(url) + .then(res => res.json()) + .then((json) => { + const { results, status } = json; + // catch a non-successful geocode result that was returned in the response + if (!results || !results.length || status !== 'OK') { + dispatch(locationGeocodeError('Address not found, please try again.')); + } else { + dispatch(locationGeocodeSuccess(results[0])); + } + }) + .catch(error => dispatch(locationGeocodeError(error))); + }; +}; diff --git a/src/constants/action_types.js b/src/constants/action_types.js index 2e513be..2f47a44 100644 --- a/src/constants/action_types.js +++ b/src/constants/action_types.js @@ -39,3 +39,8 @@ export const GEO_POLYGONS_ERROR = 'GEO_POLYGONS_ERROR'; export const MODAL_OPENED = 'MODAL_OPENED'; export const MODAL_CLOSED = 'MODAL_CLOSED'; + +// Geocoding a search location +export const LOCATION_GEOCODE_REQUEST = 'LOCATION_GEOCODE_REQUEST'; +export const LOCATION_GEOCODE_SUCCESS = 'LOCATION_GEOCODE_SUCCESS'; +export const LOCATION_GEOCODE_ERROR = 'LOCATION_GEOCODE_ERROR'; diff --git a/src/constants/app_config.js b/src/constants/app_config.js index 8896728..cbfb29c 100644 --- a/src/constants/app_config.js +++ b/src/constants/app_config.js @@ -57,3 +57,6 @@ export const labelFormats = { nypd_precinct: 'NYPD Precinct {}', intersection: '{}', }; + +// TO DO: replace this with a key on Chekpeds / GreenInfo account +export const geocodingK = 'AIzaSyCgATLAbiGUrmZSIaJsCZTewG9Zu32jxus'; diff --git a/src/reducers/geocodeReducer.js b/src/reducers/geocodeReducer.js new file mode 100644 index 0000000..b76e54d --- /dev/null +++ b/src/reducers/geocodeReducer.js @@ -0,0 +1,57 @@ +import { + LOCATION_GEOCODE_REQUEST, + LOCATION_GEOCODE_SUCCESS, + LOCATION_GEOCODE_ERROR, +} from '../constants/action_types'; + +const defaultState = { + isFetching: false, + searchTerm: '', + result: null, + error: null +}; + +const parseGeocodeResult = (result) => { + const { formatted_address, geometry } = result; + const addressLabel = formatted_address.replace(/,\s+(USA|Canada|Mexico)\s*$/, ''); + + return { + addressFormatted: addressLabel, + coordinates: [geometry.location.lat, geometry.location.lng], + }; +}; + +/* + * Geocoding Reducer + * @param {object} state: default reducer state + * @param {object} action: redux action creator +*/ +export default (state = defaultState, action) => { + switch (action.type) { + case LOCATION_GEOCODE_REQUEST: + return { + ...state, + isFetching: true, + searchTerm: action.searchTerm, + result: null + }; + + case LOCATION_GEOCODE_SUCCESS: + return { + ...state, + error: null, + isFetching: false, + result: parseGeocodeResult(action.json) + }; + + case LOCATION_GEOCODE_ERROR: + return { + ...state, + isFetching: false, + error: action.error + }; + + default: + return state; + } +}; diff --git a/src/reducers/index.js b/src/reducers/index.js index 8f56360..eb28b0a 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -12,6 +12,7 @@ import crashesDateRange from './crashes_date_range_reducer'; import crashesMaxDate from './crashes_max_date_reducer'; import crashStats from './crash_stats_reducer'; import modal from './modal_reducer'; +import geocoding from './geocodeReducer'; // breakpoints for redux-responsive store // taken from scss/skeleton/base/variables @@ -39,6 +40,7 @@ const rootReducer = combineReducers({ filterArea, filterContributingFactor, filterType, + geocoding, modal, routing: routerReducer, yearRange, From 9461f5006bb1d3658196b25d582f65ad57d39b6f Mon Sep 17 00:00:00 2001 From: Chris Henrick Date: Sun, 28 Jan 2018 17:22:18 -0800 Subject: [PATCH 04/12] made search a connected component --- src/actions/index.js | 1 + src/components/App.js | 2 +- src/components/Search/SearchForm.js | 57 +++++++++++++++++++++++++ src/components/Search/index.js | 66 ++++++++++++----------------- src/containers/SearchConnected.js | 17 ++++++++ 5 files changed, 102 insertions(+), 41 deletions(-) create mode 100644 src/components/Search/SearchForm.js create mode 100644 src/containers/SearchConnected.js diff --git a/src/actions/index.js b/src/actions/index.js index ccc4bd4..9a88b0e 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -4,6 +4,7 @@ export { fetchCrashStatsData, fetchCrashesDateRange, fetchCrashesMaxDate, fetchGeoPolygons, + fetchLocationGeocode } from './async_actions'; export { startDateChange, endDateChange } from './filter_by_date_actions'; export { diff --git a/src/components/App.js b/src/components/App.js index 15903eb..cd8e566 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -10,7 +10,7 @@ import OptionsFiltersConnected from '../containers/OptionsFiltersConnected'; import ModalConnected from '../containers/ModalConnected'; import SmallDeviceMessage from './SmallDeviceMessage'; import LoadingIndicator from './LoadingIndicator'; -import Search from './Search'; +import Search from '../containers/SearchConnected'; class App extends Component { constructor() { diff --git a/src/components/Search/SearchForm.js b/src/components/Search/SearchForm.js new file mode 100644 index 0000000..476c968 --- /dev/null +++ b/src/components/Search/SearchForm.js @@ -0,0 +1,57 @@ +import React, { Component, PropTypes } from 'react'; + +class Search extends Component { + static propTypes = { + error: PropTypes.string, + isFetching: PropTypes.bool.isRequired, + searchTerm: PropTypes.string, + result: PropTypes.shape({ + addressFormatted: PropTypes.string, + coordinates: PropTypes.arrayOf(PropTypes.number) + }), + fetchLocationGeocode: PropTypes.func.isRequired, + } + + static defaultProps = { + error: null, + result: null, + searchTerm: null + } + + state = { + inputText: '', + } + + handleSubmit = (e) => { + const { inputText } = this.state; + + e.preventDefault(); + + if (inputText && inputText.length) { + this.props.fetchLocationGeocode(inputText); + } + } + + handleChange = (e) => { + this.setState({ + inputText: e.target.value, + }); + } + + render() { + const { inputText } = this.state; + + return ( +
+ +
+ ); + } +} + +export default Search; diff --git a/src/components/Search/index.js b/src/components/Search/index.js index 61802e3..7f9e224 100644 --- a/src/components/Search/index.js +++ b/src/components/Search/index.js @@ -1,42 +1,28 @@ -import React, { Component } from 'react'; - -class Search extends Component { - state = { - inputText: '', - } - - handleSubmit = (e) => { - const { inputText } = this.state; - - e.preventDefault(); - - if (inputText && inputText.length) { - // TO DO - } - } - - handleChange = (e) => { - this.setState({ - inputText: e.target.value, - }); - } - - render() { - const { inputText } = this.state; - - return ( -
-
- -
-
- ); - } -} +import React, { PropTypes } from 'react'; + +import SearchForm from './SearchForm'; + +const Search = props => ( +
+ +
+); + +Search.propTypes = { + error: PropTypes.string, + isFetching: PropTypes.bool.isRequired, + searchTerm: PropTypes.string, + result: PropTypes.shape({ + addressFormatted: PropTypes.string, + coordinates: PropTypes.arrayOf(PropTypes.number) + }), + fetchLocationGeocode: PropTypes.func.isRequired, +}; + +Search.defaultProps = { + error: null, + result: null, + searchTerm: null +}; export default Search; diff --git a/src/containers/SearchConnected.js b/src/containers/SearchConnected.js new file mode 100644 index 0000000..5ab850f --- /dev/null +++ b/src/containers/SearchConnected.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; + +import { fetchLocationGeocode } from '../actions'; + +import Search from '../components/Search'; + +const mapStateToProps = ({ geocoding }) => { + const { error, isFetching, searchTerm, result } = geocoding; + return { + error, + isFetching, + result, + searchTerm, + }; +}; + +export default connect(mapStateToProps, { fetchLocationGeocode })(Search); From 5fe0727887010452838984de94184c64130a4d33 Mon Sep 17 00:00:00 2001 From: Chris Henrick Date: Sun, 28 Jan 2018 18:01:57 -0800 Subject: [PATCH 05/12] added search result readout component --- scss/components/_search-form.scss | 9 +++++ scss/components/_search-results.scss | 15 ++++++++ scss/components/_search.scss | 5 +-- scss/main.scss | 2 + src/components/Search/SearchForm.js | 5 ++- src/components/Search/SearchResults.js | 52 ++++++++++++++++++++++++++ src/components/Search/index.js | 5 +++ 7 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 scss/components/_search-form.scss create mode 100644 scss/components/_search-results.scss create mode 100644 src/components/Search/SearchResults.js diff --git a/scss/components/_search-form.scss b/scss/components/_search-form.scss new file mode 100644 index 0000000..1010a7e --- /dev/null +++ b/scss/components/_search-form.scss @@ -0,0 +1,9 @@ +.SearchForm { + width: 100%; + position: relative; + + input { + width: 100%; + margin: 0; + } +} diff --git a/scss/components/_search-results.scss b/scss/components/_search-results.scss new file mode 100644 index 0000000..50d5339 --- /dev/null +++ b/scss/components/_search-results.scss @@ -0,0 +1,15 @@ +.SearchResults { + width: 100%; + position: relative; + padding-right: 10px; + + p { + margin-bottom: 10px; + } + + button { + &:last-child { + margin-left: 10px; + } + } +} diff --git a/scss/components/_search.scss b/scss/components/_search.scss index 4e0f2a2..01fdbed 100644 --- a/scss/components/_search.scss +++ b/scss/components/_search.scss @@ -6,8 +6,5 @@ left: $margin-25 + $zoom-controls-width + $padding-10; z-index: 5; padding-right: 10px; - - input { - width: 100%; - } + background-color: transparent; } diff --git a/scss/main.scss b/scss/main.scss index b4058c0..45df137 100644 --- a/scss/main.scss +++ b/scss/main.scss @@ -38,3 +38,5 @@ @import 'components/small-device-message'; @import 'components/about-copy'; @import 'components/search'; +@import 'components/search-results'; +@import 'components/search-form'; diff --git a/src/components/Search/SearchForm.js b/src/components/Search/SearchForm.js index 476c968..059c7c5 100644 --- a/src/components/Search/SearchForm.js +++ b/src/components/Search/SearchForm.js @@ -29,6 +29,9 @@ class Search extends Component { if (inputText && inputText.length) { this.props.fetchLocationGeocode(inputText); + this.setState({ + inputText: '', + }); } } @@ -42,7 +45,7 @@ class Search extends Component { const { inputText } = this.state; return ( -
+ { + const { error, isFetching, result } = this.props; + + if (isFetching) { + return (

searching...

); + } + + if (error) { + return (

{error}

); + } + + if (result) { + return [ +

Filter crashes by this location?

, +

{result.addressFormatted}

, + , + + ]; + } + + return null; + } + + render() { + return ( +
+ {this.showSearchResult()} +
+ ); + } +} + +export default SearchResults; diff --git a/src/components/Search/index.js b/src/components/Search/index.js index 7f9e224..ea330ee 100644 --- a/src/components/Search/index.js +++ b/src/components/Search/index.js @@ -1,10 +1,15 @@ import React, { PropTypes } from 'react'; import SearchForm from './SearchForm'; +import SearchResults from './SearchResults'; const Search = props => (
+ { + (props.error || props.result) && + + }
); From 6a3fca4807c6db8eb80ae5b96e8053f37887802c Mon Sep 17 00:00:00 2001 From: Chris Henrick Date: Sun, 28 Jan 2018 19:23:19 -0800 Subject: [PATCH 06/12] added actions for filter by location & clear search --- src/actions/filter_by_location_actions.js | 15 ++++++++++++ src/actions/index.js | 1 + src/components/Search/SearchResults.js | 29 +++++++++++++++++++++-- src/components/Search/index.js | 2 ++ src/constants/action_types.js | 3 +++ src/containers/SearchConnected.js | 12 ++++++++-- src/reducers/geocodeReducer.js | 4 ++++ 7 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 src/actions/filter_by_location_actions.js diff --git a/src/actions/filter_by_location_actions.js b/src/actions/filter_by_location_actions.js new file mode 100644 index 0000000..9bbdb6e --- /dev/null +++ b/src/actions/filter_by_location_actions.js @@ -0,0 +1,15 @@ +import { + CLEAR_LOCATION_GEOCODE, + FILTER_BY_LOCATION, +} from '../constants/action_types'; + +export const clearLocationGeocode = () => ({ + type: CLEAR_LOCATION_GEOCODE, +}); + +export const filterByLocation = latLon => ({ + type: FILTER_BY_LOCATION, + latLon +}); + +export default clearLocationGeocode; diff --git a/src/actions/index.js b/src/actions/index.js index 9a88b0e..f63730e 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -20,3 +20,4 @@ export { } from './filter_by_area_actions'; export filterByContributingFactor from './filter_contributing_factor_actions'; export { openModal, closeModal } from './modal_actions'; +export { clearLocationGeocode, filterByLocation } from './filter_by_location_actions'; diff --git a/src/components/Search/SearchResults.js b/src/components/Search/SearchResults.js index 58cd2ae..af81df5 100644 --- a/src/components/Search/SearchResults.js +++ b/src/components/Search/SearchResults.js @@ -2,7 +2,9 @@ import React, { Component, PropTypes } from 'react'; class SearchResults extends Component { static propTypes = { + clearLocationGeocode: PropTypes.func.isRequired, error: PropTypes.string, + filterByLocation: PropTypes.func.isRequired, isFetching: PropTypes.bool.isRequired, searchTerm: PropTypes.string, result: PropTypes.shape({ @@ -17,6 +19,17 @@ class SearchResults extends Component { searchTerm: null } + handleFilterResult = (e) => { + e.preventDefault(); + const { result } = this.props; + this.props.filterByLocation(result.coordinates); + } + + handleSearchAgain = (e) => { + e.preventDefault(); + this.props.clearLocationGeocode(); + } + showSearchResult = () => { const { error, isFetching, result } = this.props; @@ -32,8 +45,20 @@ class SearchResults extends Component { return [

Filter crashes by this location?

,

{result.addressFormatted}

, - , - + , + ]; } diff --git a/src/components/Search/index.js b/src/components/Search/index.js index ea330ee..e4f9f23 100644 --- a/src/components/Search/index.js +++ b/src/components/Search/index.js @@ -21,7 +21,9 @@ Search.propTypes = { addressFormatted: PropTypes.string, coordinates: PropTypes.arrayOf(PropTypes.number) }), + clearLocationGeocode: PropTypes.func.isRequired, fetchLocationGeocode: PropTypes.func.isRequired, + filterByLocation: PropTypes.func.isRequired, }; Search.defaultProps = { diff --git a/src/constants/action_types.js b/src/constants/action_types.js index 2f47a44..19514ba 100644 --- a/src/constants/action_types.js +++ b/src/constants/action_types.js @@ -5,6 +5,8 @@ export const FILTER_BY_AREA_TYPE = 'FILTER_BY_AREA_TYPE'; export const FILTER_BY_AREA_IDENTIFIER = 'FILTER_BY_AREA_IDENTIFIER'; export const FILTER_BY_AREA_CUSTOM = 'FILTER_BY_AREA_CUSTOM'; +export const FILTER_BY_LOCATION = 'FILTER_BY_LOCATION'; + export const TOGGLE_CUSTOM_AREA_DRAW = 'TOGGLE_CUSTOM_AREA_DRAW'; export const FILTER_BY_TYPE_INJURY = 'FILTER_BY_TYPE_INJURY'; @@ -44,3 +46,4 @@ export const MODAL_CLOSED = 'MODAL_CLOSED'; export const LOCATION_GEOCODE_REQUEST = 'LOCATION_GEOCODE_REQUEST'; export const LOCATION_GEOCODE_SUCCESS = 'LOCATION_GEOCODE_SUCCESS'; export const LOCATION_GEOCODE_ERROR = 'LOCATION_GEOCODE_ERROR'; +export const CLEAR_LOCATION_GEOCODE = 'CLEAR_LOCATION_GEOCODE'; diff --git a/src/containers/SearchConnected.js b/src/containers/SearchConnected.js index 5ab850f..8b9defc 100644 --- a/src/containers/SearchConnected.js +++ b/src/containers/SearchConnected.js @@ -1,6 +1,10 @@ import { connect } from 'react-redux'; -import { fetchLocationGeocode } from '../actions'; +import { + fetchLocationGeocode, + clearLocationGeocode, + filterByLocation, +} from '../actions'; import Search from '../components/Search'; @@ -14,4 +18,8 @@ const mapStateToProps = ({ geocoding }) => { }; }; -export default connect(mapStateToProps, { fetchLocationGeocode })(Search); +export default connect(mapStateToProps, { + fetchLocationGeocode, + clearLocationGeocode, + filterByLocation, +})(Search); diff --git a/src/reducers/geocodeReducer.js b/src/reducers/geocodeReducer.js index b76e54d..1ffd5e1 100644 --- a/src/reducers/geocodeReducer.js +++ b/src/reducers/geocodeReducer.js @@ -1,4 +1,5 @@ import { + CLEAR_LOCATION_GEOCODE, LOCATION_GEOCODE_REQUEST, LOCATION_GEOCODE_SUCCESS, LOCATION_GEOCODE_ERROR, @@ -51,6 +52,9 @@ export default (state = defaultState, action) => { error: action.error }; + case CLEAR_LOCATION_GEOCODE: + return defaultState; + default: return state; } From da1f869806e30e5a4acd6bfcd96ce893ba18dde2 Mon Sep 17 00:00:00 2001 From: Chris Henrick Date: Sun, 28 Jan 2018 19:56:12 -0800 Subject: [PATCH 07/12] added behavior to set map view to search result & display marker --- src/components/LeafletMap/index.js | 27 +++++++++++++++++++++++++-- src/containers/LeafletMapConnected.js | 4 +++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/components/LeafletMap/index.js b/src/components/LeafletMap/index.js index 65b3f04..1d52c3b 100644 --- a/src/components/LeafletMap/index.js +++ b/src/components/LeafletMap/index.js @@ -58,13 +58,13 @@ class LeafletMap extends Component { // if loading a geo but no identifier, fetch geo-polygons to start up // if loading both/neither, that's already handled by standard props updates - if (geo && !identifier) { + if (geo && geo !== 'citywide' && !identifier) { this.props.fetchGeoPolygons(geo); } } componentWillReceiveProps(nextProps) { - const { geo, geojson, identifier, drawEnabeled } = nextProps; + const { geo, geojson, identifier, drawEnabeled, searchResult } = nextProps; if (identifier !== this.props.identifier) { // user filtered by a specific geography, so hide the GeoJSON boundary overlay @@ -131,6 +131,24 @@ class LeafletMap extends Component { this.customFilterClearPoly(); this.customFilterEnableDraw(); } + + // Handle Address Search Result + // user searched for a street address, zoom and center the map, add a marker + if (searchResult && + (JSON.stringify(searchResult) !== JSON.stringify(this.props.searchResult)) + ) { + const { coordinates, addressFormatted } = searchResult; + this.map.setView(coordinates, 16); + this.searchMarker = L.marker(coordinates) + .bindPopup(`

${addressFormatted}

`) + .addTo(this.map); + } + + // remove marker if user cleared search result or applied filter by location + if (!searchResult && this.props.searchResult) { + this.map.removeLayer(this.searchMarker); + this.searchMarker = null; + } } shouldComponentUpdate() { @@ -454,6 +472,7 @@ LeafletMap.defaultProps = { identifier: '', lngLats: [], geojson: {}, + searchResult: null, }; LeafletMap.propTypes = { @@ -494,6 +513,10 @@ LeafletMap.propTypes = { }).isRequired, noInjuryFatality: PropTypes.bool.isRequired }).isRequired, + searchResult: PropTypes.shape({ + addressFormatted: PropTypes.string, + result: PropTypes.arrayOf(PropTypes.number) + }), }; export default LeafletMap; diff --git a/src/containers/LeafletMapConnected.js b/src/containers/LeafletMapConnected.js index aef306f..101a54e 100644 --- a/src/containers/LeafletMapConnected.js +++ b/src/containers/LeafletMapConnected.js @@ -3,11 +3,12 @@ import { connect } from 'react-redux'; import { filterByAreaIdentifier, filterByAreaCustom, fetchGeoPolygons } from '../actions/'; import LeafletMap from '../components/LeafletMap/'; -const mapStateToProps = ({ filterDate, filterType, filterArea }, ownProps) => { +const mapStateToProps = ({ filterDate, filterType, filterArea, geocoding }, ownProps) => { const { startDate, endDate } = filterDate; const { location: { query } } = ownProps; const { lat, lng, zoom } = query; const { geo, geojson, identifier, lngLats, drawEnabeled } = filterArea; + const { result } = geocoding; return { zoom: zoom ? Number(zoom) : undefined, lat: lat ? Number(lat) : undefined, @@ -20,6 +21,7 @@ const mapStateToProps = ({ filterDate, filterType, filterArea }, ownProps) => { geojson, identifier, lngLats, + searchResult: result, }; }; From f2d7b9b64e8c4384a6de39cb2df06ba05d6dbcb2 Mon Sep 17 00:00:00 2001 From: Chris Henrick Date: Sun, 28 Jan 2018 20:09:26 -0800 Subject: [PATCH 08/12] start of sql for lat lon buffer spatial join --- src/constants/sql_queries.js | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/constants/sql_queries.js b/src/constants/sql_queries.js index c6c205b..3acff2b 100644 --- a/src/constants/sql_queries.js +++ b/src/constants/sql_queries.js @@ -221,6 +221,29 @@ const filterByCustomAreaClause = (lonLatArray) => { return ''; }; + +const filterByLocationSQL = ({ lat, lon }) => { + if (lat && lon) { + return sls` + AND + ST_Contains( + ST_Buffer( + ST_Transform( + ST_SetSRID( + ST_MakePoint(${lon}, ${lat}), + 4326 + ), + 3785 + ), + 65 + ), + c.the_geom_webmercator + ) + `; + } + return ''; +}; + /* ********************************** MAP **************************************** */ @@ -232,7 +255,7 @@ const filterByCustomAreaClause = (lonLatArray) => { // @param {string} harm: crash type, one of 'ALL', 'cyclist', 'motorist', 'ped' // @param {string} persona: crash type, of of 'ALL', 'fatality', 'injury', 'no inj/fat' export const configureMapSQL = (params) => { - const { startDate, endDate, filterType, geo, identifier, lngLats } = params; + const { startDate, endDate, filterType, geo, identifier, lngLats, lat, lon } = params; return sls` SELECT * FROM @@ -259,6 +282,7 @@ export const configureMapSQL = (params) => { ${filterByCustomAreaClause(lngLats)} ${filterByTypeWhereClause(filterType)} ${filterByIdentifierWhereClause(identifier, geo)} + ${filterByLocationSQL(lat, lon)} AND c.the_geom IS NOT NULL GROUP BY From 17d9596a8d654976bc94812bebcc9f0744dc34c8 Mon Sep 17 00:00:00 2001 From: Gregor Allensworth Date: Tue, 30 Jan 2018 08:01:36 -0800 Subject: [PATCH 09/12] #76 geocoder UI color + border --- scss/components/_search-form.scss | 8 ++++++++ scss/components/_search-results.scss | 2 ++ scss/components/_search.scss | 5 ++--- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/scss/components/_search-form.scss b/scss/components/_search-form.scss index 1010a7e..b7e154d 100644 --- a/scss/components/_search-form.scss +++ b/scss/components/_search-form.scss @@ -5,5 +5,13 @@ input { width: 100%; margin: 0; + height: 35px; + + border-radius: 0; + background-color: $btn-bg; + border-color: $marine-light; + color: $marine-light; } + + margin-bottom: 0; } diff --git a/scss/components/_search-results.scss b/scss/components/_search-results.scss index 50d5339..59c5b39 100644 --- a/scss/components/_search-results.scss +++ b/scss/components/_search-results.scss @@ -3,6 +3,8 @@ position: relative; padding-right: 10px; + border: 1px solid $marine-light; + p { margin-bottom: 10px; } diff --git a/scss/components/_search.scss b/scss/components/_search.scss index 01fdbed..b6a7f8f 100644 --- a/scss/components/_search.scss +++ b/scss/components/_search.scss @@ -1,9 +1,8 @@ .Search { width: 300px; - height: 38px + $padding-10 * 2; position: absolute; - top: $margin-5 + $app-header-height; - left: $margin-25 + $zoom-controls-width + $padding-10; + top: $margin-5 + $app-header-height - 10; + left: $margin-25 + $zoom-controls-width + $padding-10 - 10; z-index: 5; padding-right: 10px; background-color: transparent; From 85e4da8f5713df0972df29ca1dd989879b28cbfb Mon Sep 17 00:00:00 2001 From: Gregor Allensworth Date: Tue, 30 Jan 2018 08:11:28 -0800 Subject: [PATCH 10/12] #76 geocoder results panel X & tweaks --- scss/components/_search-results.scss | 23 +++++++++++++++++++---- src/components/Search/SearchResults.js | 12 +++--------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/scss/components/_search-results.scss b/scss/components/_search-results.scss index 59c5b39..5cbf615 100644 --- a/scss/components/_search-results.scss +++ b/scss/components/_search-results.scss @@ -9,9 +9,24 @@ margin-bottom: 10px; } - button { - &:last-child { - margin-left: 10px; - } + p.address { + font-size: 14px; + } + + button.close { + float: right; + + height: 20px; + padding: 0 5px; + margin: 5px 5px 10px 10px; + font-size: 11px; + line-height: 11px; + background-color: $off-white; + border-color: transparent; + color: $marine-light; + text-align: left; + text-transform: capitalize; + letter-spacing: 0.5px; + border: none; } } diff --git a/src/components/Search/SearchResults.js b/src/components/Search/SearchResults.js index af81df5..be19225 100644 --- a/src/components/Search/SearchResults.js +++ b/src/components/Search/SearchResults.js @@ -25,7 +25,7 @@ class SearchResults extends Component { this.props.filterByLocation(result.coordinates); } - handleSearchAgain = (e) => { + closeThisPanel = (e) => { e.preventDefault(); this.props.clearLocationGeocode(); } @@ -43,21 +43,15 @@ class SearchResults extends Component { if (result) { return [ + ,

Filter crashes by this location?

, -

{result.addressFormatted}

, +

{result.addressFormatted}

, , - ]; } From f65458336a51e924d3f37245e4217b49b3cd4293 Mon Sep 17 00:00:00 2001 From: Gregor Allensworth Date: Tue, 30 Jan 2018 08:38:52 -0800 Subject: [PATCH 11/12] #76 geocoder marker adds circle / geoPolygonStyle constant --- src/components/LeafletMap/index.js | 19 ++++++++++++++----- src/constants/app_config.js | 22 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/components/LeafletMap/index.js b/src/components/LeafletMap/index.js index 1d52c3b..9cc8aff 100644 --- a/src/components/LeafletMap/index.js +++ b/src/components/LeafletMap/index.js @@ -3,7 +3,15 @@ import momentPropTypes from 'react-moment-proptypes'; import sls from 'single-line-string'; import { configureMapSQL } from '../../constants/sql_queries'; -import { basemapURL, cartoUser, crashDataFieldNames, labelFormats } from '../../constants/app_config'; +import { + basemapURL, + cartoUser, + crashDataFieldNames, + labelFormats, + geoPolygonStyle, + intersectionCircleRadius, + intersectionCircleStyle +} from '../../constants/app_config'; import { boroughs, configureLayerSource, crashDataChanged } from '../../constants/api'; import ZoomControls from './ZoomControls'; @@ -142,12 +150,16 @@ class LeafletMap extends Component { this.searchMarker = L.marker(coordinates) .bindPopup(`

${addressFormatted}

`) .addTo(this.map); + this.searchCircle = L.circle(coordinates, intersectionCircleRadius, intersectionCircleStyle) + .addTo(this.map); } // remove marker if user cleared search result or applied filter by location if (!searchResult && this.props.searchResult) { this.map.removeLayer(this.searchMarker); + this.map.removeLayer(this.searchCircle); this.searchMarker = null; + this.searchCircle = null; } } @@ -369,10 +381,7 @@ class LeafletMap extends Component { function handleMouseover(e) { const layer = e.target; - layer.setStyle({ - fillColor: '#105b63', - fillOpacity: 1 - }); + layer.setStyle(geoPolygonStyle); self.revealFilterAreaTooltip(geo, e); diff --git a/src/constants/app_config.js b/src/constants/app_config.js index cbfb29c..eb2ac5a 100644 --- a/src/constants/app_config.js +++ b/src/constants/app_config.js @@ -58,5 +58,27 @@ export const labelFormats = { intersection: '{}', }; +// GeoJSON polyfons for clicking specific areas +// the style for drawing them onto the map +export const geoPolygonStyle = { + fillColor: '#105b63', + fillOpacity: 1 +}; + +// API key for geocoding // TO DO: replace this with a key on Chekpeds / GreenInfo account export const geocodingK = 'AIzaSyCgATLAbiGUrmZSIaJsCZTewG9Zu32jxus'; + +// geocoder search: the radius of the "preview" circle, and its visual style +export const intersectionCircleRadius = 27.4; // 90ft in meters +export const intersectionCircleStyle = { + /* + color: '#105b63', + opacity: 1, + weight: 2, + */ + stroke: false, + fillColor: '#105b63', + fillOpacity: 0.5, + clickable: false, +}; From 0eb2da2039a0e9363fa8f7f23589ecbf0ebd4527 Mon Sep 17 00:00:00 2001 From: Gregor Allensworth Date: Tue, 30 Jan 2018 09:22:52 -0800 Subject: [PATCH 12/12] #76 geocoder results panel now clearly shows search radius --- src/components/LeafletMap/index.js | 6 ++++-- src/components/Search/SearchResults.js | 4 +++- src/constants/app_config.js | 7 ++++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/components/LeafletMap/index.js b/src/components/LeafletMap/index.js index 9cc8aff..12f0e9b 100644 --- a/src/components/LeafletMap/index.js +++ b/src/components/LeafletMap/index.js @@ -9,7 +9,7 @@ import { crashDataFieldNames, labelFormats, geoPolygonStyle, - intersectionCircleRadius, + intersectionCircleRadiusMeters, intersectionCircleStyle } from '../../constants/app_config'; import { boroughs, configureLayerSource, crashDataChanged } from '../../constants/api'; @@ -150,7 +150,9 @@ class LeafletMap extends Component { this.searchMarker = L.marker(coordinates) .bindPopup(`

${addressFormatted}

`) .addTo(this.map); - this.searchCircle = L.circle(coordinates, intersectionCircleRadius, intersectionCircleStyle) + this.searchCircle = L.circle(coordinates, + intersectionCircleRadiusMeters, + intersectionCircleStyle) .addTo(this.map); } diff --git a/src/components/Search/SearchResults.js b/src/components/Search/SearchResults.js index be19225..15af2ff 100644 --- a/src/components/Search/SearchResults.js +++ b/src/components/Search/SearchResults.js @@ -1,5 +1,7 @@ import React, { Component, PropTypes } from 'react'; +import { intersectionCircleRadiusFeet } from '../../constants/app_config'; + class SearchResults extends Component { static propTypes = { clearLocationGeocode: PropTypes.func.isRequired, @@ -44,7 +46,7 @@ class SearchResults extends Component { if (result) { return [ , -

Filter crashes by this location?

, +

Filter crashes within {intersectionCircleRadiusFeet} feet of this location?

,

{result.addressFormatted}

,