Skip to content

Commit 3b8c8fd

Browse files
author
gondzo
committed
Merge branch 'locationHistory' into dev
# Conflicts: # package.json # src/components/Header/Header.jsx # src/routes/Dashboard/containers/DashboardContainer.js # src/routes/DronesMap/components/DronesMapView.jsx # src/routes/DronesMap/containers/DronesMapContainer.js # src/routes/DronesMap/modules/DronesMap.js # src/routes/MissionPlanner/modules/MissionPlanner.js # src/routes/MyRequest/containers/MyRequestContainer.js # src/services/APIService.js # webpack.config.js
2 parents ff07fec + 5dc7870 commit 3b8c8fd

File tree

12 files changed

+518
-16
lines changed

12 files changed

+518
-16
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"postcss-flexboxfixer": "0.0.5",
6262
"postcss-loader": "^0.13.0",
6363
"rc-calendar": "^7.5.1",
64+
"rc-slider": "^5.4.0",
6465
"rc-tooltip": "^3.4.2",
6566
"react": "^15.3.2",
6667
"react-breadcrumbs": "^1.5.1",
@@ -71,7 +72,6 @@
7172
"react-dom": "^15.3.2",
7273
"react-flexbox-grid": "^0.10.2",
7374
"react-google-maps": "^6.0.1",
74-
"react-modal": "^1.5.2",
7575
"react-dropdown": "^1.2.0",
7676
"react-icheck": "^0.3.6",
7777
"react-input-range": "^0.9.3",
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;

0 commit comments

Comments
 (0)