Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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