Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Address Search and Crashes Filter #81

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
17 changes: 17 additions & 0 deletions scss/components/_search-form.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.SearchForm {
width: 100%;
position: relative;

input {
width: 100%;
margin: 0;
height: 35px;

border-radius: 0;
background-color: $btn-bg;
border-color: $marine-light;
color: $marine-light;
}

margin-bottom: 0;
}
32 changes: 32 additions & 0 deletions scss/components/_search-results.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.SearchResults {
width: 100%;
position: relative;
padding-right: 10px;

border: 1px solid $marine-light;

p {
margin-bottom: 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;
}
}
9 changes: 9 additions & 0 deletions scss/components/_search.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.Search {
width: 300px;
position: absolute;
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;
}
3 changes: 3 additions & 0 deletions scss/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,6 @@
@import 'components/modal';
@import 'components/small-device-message';
@import 'components/about-copy';
@import 'components/search';
@import 'components/search-results';
@import 'components/search-form';
49 changes: 48 additions & 1 deletion src/actions/async_actions.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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)));
};
};
15 changes: 15 additions & 0 deletions src/actions/filter_by_location_actions.js
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions src/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export { fetchCrashStatsData,
fetchCrashesDateRange,
fetchCrashesMaxDate,
fetchGeoPolygons,
fetchLocationGeocode
} from './async_actions';
export { startDateChange, endDateChange } from './filter_by_date_actions';
export {
Expand All @@ -19,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';
2 changes: 2 additions & 0 deletions src/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 '../containers/SearchConnected';

class App extends Component {
constructor() {
Expand Down Expand Up @@ -96,6 +97,7 @@ class App extends Component {
<SmallDeviceMessage /> :
[
<AppHeader key="app-header" openModal={openModal} />,
<Search key="search-ui" />,
<LeafletMapConnected
key="leaflet-map"
location={location}
Expand Down
50 changes: 42 additions & 8 deletions src/components/LeafletMap/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
intersectionCircleRadiusMeters,
intersectionCircleStyle
} from '../../constants/app_config';
import { boroughs, configureLayerSource, crashDataChanged } from '../../constants/api';

import ZoomControls from './ZoomControls';
Expand Down Expand Up @@ -58,13 +66,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
Expand Down Expand Up @@ -108,7 +116,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') ||
Expand All @@ -131,6 +139,30 @@ 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(`<p>${addressFormatted}</p>`)
.addTo(this.map);
this.searchCircle = L.circle(coordinates,
intersectionCircleRadiusMeters,
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;
}
}

shouldComponentUpdate() {
Expand Down Expand Up @@ -351,10 +383,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);

Expand Down Expand Up @@ -454,6 +483,7 @@ LeafletMap.defaultProps = {
identifier: '',
lngLats: [],
geojson: {},
searchResult: null,
};

LeafletMap.propTypes = {
Expand Down Expand Up @@ -494,6 +524,10 @@ LeafletMap.propTypes = {
}).isRequired,
noInjuryFatality: PropTypes.bool.isRequired
}).isRequired,
searchResult: PropTypes.shape({
addressFormatted: PropTypes.string,
result: PropTypes.arrayOf(PropTypes.number)
}),
};

export default LeafletMap;
60 changes: 60 additions & 0 deletions src/components/Search/SearchForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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);
this.setState({
inputText: '',
});
}
}

handleChange = (e) => {
this.setState({
inputText: e.target.value,
});
}

render() {
const { inputText } = this.state;

return (
<form className="SearchForm" onSubmit={this.handleSubmit}>
<input
type="text"
placeholder="Search by address or intersection"
value={inputText}
onChange={this.handleChange}
/>
</form>
);
}
}

export default Search;
Loading