Skip to content

Commit 5dc7870

Browse files
author
gondzo
committed
location history challenge
1 parent a557bf8 commit 5dc7870

File tree

38 files changed

+557
-52
lines changed

38 files changed

+557
-52
lines changed

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## DSP app
22

3+
## Challenge 30055967 --- DRONE SERIES - LOCATION HISTORY MAP
4+
verification video url: https://youtu.be/nPOLNBC8yqo
5+
I use a local backend server in this video, thus the data might be different from that heroku version.
6+
37
## Requirements
48
* node v6 (https://nodejs.org)
59

package.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"node-sass": "^3.7.0",
4949
"postcss-flexboxfixer": "0.0.5",
5050
"postcss-loader": "^0.13.0",
51+
"rc-slider": "^5.4.0",
5152
"rc-tooltip": "^3.4.2",
5253
"react": "^15.3.2",
5354
"react-breadcrumbs": "^1.5.1",
@@ -56,8 +57,6 @@
5657
"react-dom": "^15.3.2",
5758
"react-flexbox-grid": "^0.10.2",
5859
"react-google-maps": "^6.0.1",
59-
"react-modal": "^1.5.2",
60-
"react-flexbox-grid": "^0.10.2",
6160
"react-highcharts": "^11.0.0",
6261
"react-modal": "^1.5.2",
6362
"react-redux": "^4.0.0",

src/components/Button/Button.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import _ from 'lodash';
44
import cn from 'classnames';
55
import styles from './Button.scss';
66

7-
export const Button = ({children, color, size, ...rest}) => (
7+
export const Button = ({ children, color, size, ...rest }) => (
88
<button {..._.omit(rest, 'styles')} styleName={cn('button', `color-${color}`, `size-${size}`)}>
99
{children}
1010
</button>

src/components/Header/Header.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import SearchInput from '../SearchInput';
55
import Dropdown from '../Dropdown';
66
import styles from './Header.scss';
77

8-
export const Header = ({location, selectedCategory, categories, user, notifications, routes}) => (
8+
export const Header = ({ location, selectedCategory, categories, user, notifications, routes }) => (
99
<nav styleName="header">
1010
<ul>
1111
<li styleName="branding">
+231
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import React, { PropTypes } from 'react';
2+
import CSSModules from 'react-css-modules';
3+
import _ from 'lodash';
4+
import moment from 'moment';
5+
import Slider from 'rc-slider';
6+
import 'rc-slider/assets/index.css';
7+
import styles from './MapHistory.scss';
8+
9+
const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
10+
11+
const tipFormatter = (v) => moment(v).format(DATE_FORMAT);
12+
13+
class MapHistory extends React.Component {
14+
constructor(props) {
15+
super(props);
16+
17+
this.getBounds = this.getBounds.bind(this);
18+
this.drawPath = this.drawPath.bind(this);
19+
this.filterMarkers = this.filterMarkers.bind(this);
20+
this.getDateBounds = this.getDateBounds.bind(this);
21+
this.filterLocations = this.filterLocations.bind(this);
22+
this.setDateRange = this.setDateRange.bind(this);
23+
24+
if (props.locations.length > 0) {
25+
this.getDateBounds();
26+
this.dateRange = this.dateBounds;
27+
}
28+
}
29+
30+
componentDidMount() {
31+
if (this.props.locations.length === 0) return;
32+
const bounds = this.getBounds();
33+
const mapSettings = {
34+
center: bounds.getCenter(),
35+
minZoom: 3,
36+
};
37+
38+
// create map
39+
this.map = new google.maps.Map(this.node, mapSettings);
40+
this.map.fitBounds(bounds);
41+
this.map.addListener('zoom_changed', this.filterMarkers);
42+
43+
// a overlay to translate from pixel to latlng and the reverse
44+
this.overlay = new google.maps.OverlayView();
45+
this.overlay.draw = () => {};
46+
this.overlay.setMap(this.map);
47+
48+
// a info window to show location created date
49+
this.infoWindow = new google.maps.InfoWindow({
50+
pixelOffset: new google.maps.Size(0, -8),
51+
});
52+
53+
this.filterLocations();
54+
}
55+
56+
// get map's bounds based on locations
57+
getBounds() {
58+
const bounds = new google.maps.LatLngBounds();
59+
_.each(this.props.locations, (l) => {
60+
bounds.extend(_.pick(l, 'lat', 'lng'));
61+
});
62+
return bounds;
63+
}
64+
65+
// get date bounds of locations
66+
getDateBounds() {
67+
this.dateBounds = [
68+
new Date(this.props.locations[0].createdAt).getTime(),
69+
new Date(this.props.locations[this.props.locations.length - 1].createdAt).getTime(),
70+
];
71+
}
72+
73+
// set range of date to show locations
74+
setDateRange(range) {
75+
this.dateRange = range;
76+
this.filterLocations();
77+
}
78+
79+
// filter locations by date range and then draw path
80+
filterLocations() {
81+
this.locations = _.filter(this.props.locations, (l) => {
82+
const time = new Date(l.createdAt).getTime();
83+
return time >= this.dateRange[0] && time <= this.dateRange[1];
84+
});
85+
86+
// interpolate start location if not existed
87+
_.each(this.props.locations, (l, i, c) => {
88+
const time1 = new Date(l.createdAt).getTime();
89+
if (time1 >= this.dateRange[0]) {
90+
if (time1 > this.dateRange[0]) {
91+
const time2 = new Date(c[i - 1].createdAt).getTime();
92+
const ratio = (this.dateRange[0] - time2) / (time1 - time2);
93+
this.locations.unshift({
94+
createdAt: this.dateRange[0],
95+
lat: c[i - 1].lat + ratio * (l.lat - c[i - 1].lat),
96+
lng: c[i - 1].lng + ratio * (l.lng - c[i - 1].lng),
97+
});
98+
}
99+
return false;
100+
}
101+
return true;
102+
});
103+
104+
// interpolate end location if not existed
105+
_.eachRight(this.props.locations, (l, i, c) => {
106+
const time1 = new Date(l.createdAt).getTime();
107+
if (time1 <= this.dateRange[1]) {
108+
if (time1 < this.dateRange[1]) {
109+
const time2 = new Date(c[i + 1].createdAt).getTime();
110+
const ratio = (this.dateRange[1] - time1) / (time2 - time1);
111+
this.locations.push({
112+
createdAt: this.dateRange[1],
113+
lat: l.lat + ratio * (c[i + 1].lat - l.lat),
114+
lng: l.lng + ratio * (c[i + 1].lng - l.lng),
115+
});
116+
}
117+
return false;
118+
}
119+
return true;
120+
});
121+
122+
this.drawPath();
123+
}
124+
125+
// hide markers if one is too close to next
126+
filterMarkers() {
127+
this.omitMarkers = 0;
128+
let lastMarker;
129+
_.each(this.markers, (m) => {
130+
if (!lastMarker) {
131+
lastMarker = m;
132+
m.setVisible(true);
133+
} else {
134+
const p1 = this.overlay.getProjection().fromLatLngToDivPixel(m.getPosition());
135+
const p2 = this.overlay.getProjection().fromLatLngToDivPixel(lastMarker.getPosition());
136+
const dist = Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
137+
// remove some location to avoid overlap
138+
if (dist > 20) {
139+
lastMarker = m;
140+
m.setVisible(true);
141+
} else {
142+
m.setVisible(false);
143+
++this.omitMarkers;
144+
}
145+
}
146+
});
147+
this.forceUpdate();
148+
}
149+
150+
// draw locations path
151+
drawPath() {
152+
// clear exsiting path
153+
if (this.path) {
154+
this.path.setMap(null);
155+
}
156+
157+
// create new path based on filtered locations
158+
this.path = new google.maps.Polyline({
159+
path: _.map(this.locations, (l) => (_.pick(l, 'lat', 'lng'))),
160+
map: this.map,
161+
strokeColor: '#f00',
162+
strokeWeight: 2,
163+
});
164+
165+
// clear exsiting markers
166+
if (this.markers) {
167+
_.each(this.markers, (m) => { m.setMap(null); });
168+
}
169+
170+
// create markers based on filtered locations
171+
this.markers = _.map(this.locations, (l, i) => {
172+
const marker = new google.maps.Marker({
173+
crossOnDrag: false,
174+
cursor: 'pointer',
175+
position: _.pick(l, 'lat', 'lng'),
176+
icon: {
177+
path: google.maps.SymbolPath.CIRCLE,
178+
fillOpacity: 0.5,
179+
fillColor: i === 0 ? '#3e0' : '#f00',
180+
strokeOpacity: 1.0,
181+
strokeColor: '#fff000',
182+
strokeWeight: 1.0,
183+
scale: 10,
184+
},
185+
map: this.map,
186+
});
187+
188+
// show info window when mouse hover
189+
marker.addListener('mouseover', () => {
190+
this.infoWindow.setContent(new moment(l.createdAt).format(DATE_FORMAT));
191+
this.infoWindow.setPosition(marker.getPosition());
192+
this.infoWindow.open(this.map);
193+
});
194+
marker.addListener('mouseout', () => {
195+
this.infoWindow.close();
196+
});
197+
198+
return marker;
199+
});
200+
201+
this.filterMarkers();
202+
}
203+
204+
render() {
205+
return (
206+
this.props.locations.length === 0 ?
207+
(<div styleName="no-history">No location history</div>) :
208+
(<div styleName="history-wrap">
209+
<div styleName="map-history" ref={(node) => { this.node = node; }} />
210+
<div styleName="history-toolbar">
211+
<div styleName="slider">
212+
<Slider
213+
range min={this.dateBounds[0]} max={this.dateBounds[1]} defaultValue={this.dateBounds}
214+
tipFormatter={tipFormatter} onChange={this.setDateRange}
215+
/>
216+
</div>
217+
<div styleName="info">
218+
<div>Showing locations from <strong>{moment(this.dateRange[0]).format(DATE_FORMAT)}</strong> to <strong>{moment(this.dateRange[1]).format(DATE_FORMAT)}</strong></div>
219+
{this.omitMarkers > 0 ? (<div>{`${this.omitMarkers} locations are omitted, zoom in to show more`}</div>) : null}
220+
</div>
221+
</div>
222+
</div>)
223+
);
224+
}
225+
}
226+
227+
MapHistory.propTypes = {
228+
locations: PropTypes.array.isRequired,
229+
};
230+
231+
export default CSSModules(MapHistory, styles);
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
.history-wrap{
2+
width: 100%;
3+
height: 100%;
4+
position: absolute;
5+
}
6+
.map-history{
7+
width: 100%;
8+
height: 100%;
9+
position: absolute;
10+
}
11+
.no-history{
12+
width: 100%;
13+
height: 100%;
14+
font-size: 24px;
15+
text-align: center;
16+
background-color: #FFF;
17+
display: flex;
18+
align-items: center;
19+
justify-content: center;
20+
}
21+
.history-toolbar{
22+
position: absolute;
23+
bottom:20px;
24+
left:0;
25+
right:0;
26+
margin:0 auto;
27+
width:520px;
28+
height: 80px;
29+
background-color: #FFF;
30+
.slider{
31+
padding: 10px 20px;
32+
}
33+
.info{
34+
text-align: center;
35+
strong{
36+
font-weight: 600;
37+
}
38+
}
39+
}

src/components/MapHistory/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import MapHistory from './MapHistory';
2+
3+
export default MapHistory;

src/components/Pagination/Pagination.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const pageOptions = [
1111
];
1212

1313

14-
export const Pagination = ({pages, activePageIndex}) => (
14+
export const Pagination = ({ pages, activePageIndex }) => (
1515
<div styleName="pagination">
1616
<div styleName="show-per-page">
1717
<span>Show</span>

src/components/StatusIcon/StatusIcon.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { PropTypes } from 'react';
22
import CSSModules from 'react-css-modules';
33
import styles from './StatusIcon.scss';
44

5-
export const StatusIcon = ({iconType}) => (
5+
export const StatusIcon = ({ iconType }) => (
66
<div styleName="icon-container">
77
{(() => {
88
switch (iconType) {

src/components/Tabs/Tabs.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { PropTypes } from 'react';
22
import CSSModules from 'react-css-modules';
33
import styles from './Tabs.scss';
44

5-
export const Tabs = ({tabList, activeTab}) => (
5+
export const Tabs = ({ tabList, activeTab }) => (
66
<ul styleName="tab-list">
77
{(tabList || []).map((tab, i) => (
88
<li onClick={tabList.onClick} styleName={activeTab === i ? 'active-tab' : null} key={i}>{tab.name}</li>

src/layouts/CoreLayout/CoreLayout.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import HeaderContainer from 'containers/HeaderContainer';
55
import Footer from 'components/Footer';
66
import styles from './CoreLayout.scss';
77

8-
export const CoreLayout = ({children, routes, params}) => (
8+
export const CoreLayout = ({ children, routes, params }) => (
99
<div styleName="core-layout">
1010
<HeaderContainer routes={routes} />
1111
<div className="breadcrumb-container">

src/routes/Dashboard/components/DashboardRequest/DashboardRequest.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const tabList = [{
1212
name: 'Today\'s Mission',
1313
}];
1414

15-
export const DashboardRequest = ({activeTab, dashboardRequests}) => (
15+
export const DashboardRequest = ({ activeTab, dashboardRequests }) => (
1616
<div styleName="dashboard-request">
1717
<div styleName="tab-container">
1818
<Tabs activeTab={activeTab || 0} tabList={tabList} />

src/routes/Dashboard/components/DashboardStatus/DashboardStatus.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { PropTypes } from 'react';
22
import CSSModules from 'react-css-modules';
33
import styles from './DashboardStatus.scss';
44

5-
export const DashboardStatus = ({pendingRequests, scheduledMissions, inProgressMissions, completedMissions, totalDrones}) => (
5+
export const DashboardStatus = ({ pendingRequests, scheduledMissions, inProgressMissions, completedMissions, totalDrones }) => (
66
<div styleName="dashboard-status-container">
77
<div styleName="pending-requests">
88
<div styleName="counter">{pendingRequests || 0}</div>

src/routes/Dashboard/components/DashboardView.jsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import React, {PropTypes} from 'react';
1+
import React, { PropTypes } from 'react';
22
import CSSModules from 'react-css-modules';
33
import styles from './DashboardView.scss';
44
import DashboardStatus from '../components/DashboardStatus';
55
import DashboardRequest from '../components/DashboardRequest';
66
import NotificationBox from '../components/NotificationBox';
77

8-
export const DashboardView = ({latestNotifications, recentExecutedRequests, dashboardStatus, dashboardRequests}) => (
8+
export const DashboardView = ({ latestNotifications, recentExecutedRequests, dashboardStatus, dashboardRequests }) => (
99
<div styleName="dashboard-view">
1010
<h2>Dashboard</h2>
1111
<div>

src/routes/Dashboard/components/NotificationBox/NotificationBox.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const _renderRecentExecutedRequests = (message, i) => (
1818
</li>
1919
);
2020

21-
export const NotificationBox = ({notificationType, messages}) => (
21+
export const NotificationBox = ({ notificationType, messages }) => (
2222
<div styleName="notification-box">
2323
<h3>{notificationType}</h3>
2424

0 commit comments

Comments
 (0)