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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 1 addition & 2 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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">
Lines changed: 231 additions & 0 deletions
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);
Lines changed: 39 additions & 0 deletions
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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>

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy