Skip to content

Commit 29561cb

Browse files
author
gondzo
committed
Merge branch 'moveDronesMap' into dev
# Conflicts: # config/default.js # src/routes/index.js # src/services/APIService.js
2 parents 40c8d3c + f6bca5d commit 29561cb

File tree

17 files changed

+220
-4
lines changed

17 files changed

+220
-4
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
* node v6 (https://nodejs.org)
55

66
## Quick Start
7-
* `npm install -g nodemon`
87
* `npm install`
98
* `npm run dev`
109
* Navigate browser to `http://localhost:3000`
@@ -18,6 +17,7 @@ See Guild https://github.com/lorenwest/node-config/wiki/Configuration-Files
1817
|----|-----------|
1918
|`PORT`| The port to listen|
2019
|`GOOGLE_API_KEY`| The google api key see (https://developers.google.com/maps/documentation/javascript/get-api-key#key)|
20+
|`API_BASE_URL`| The base URL for Drone API |
2121

2222

2323
## Install dependencies

config/default.js

+3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
* Main config file
44
*/
55
module.exports = {
6+
// below env variables are NOT visible in frontend
67
PORT: process.env.PORT || 3000,
8+
9+
// below env variables are visible in frontend
710
GOOGLE_API_KEY: process.env.GOOGLE_API_KEY || 'AIzaSyCrL-O319wNJK8kk8J_JAYsWgu6yo5YsDI',
811
API_BASE_PATH: process.env.API_BASE_PATH || 'https://kb-dsp-server.herokuapp.com',
912
};

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"json-loader": "^0.5.4",
4545
"lodash": "^4.16.4",
4646
"moment": "^2.17.0",
47+
"node-js-marker-clusterer": "^1.0.0",
4748
"node-sass": "^3.7.0",
4849
"postcss-flexboxfixer": "0.0.5",
4950
"postcss-loader": "^0.13.0",
@@ -76,6 +77,7 @@
7677
"redux-logger": "^2.6.1",
7778
"redux-thunk": "^2.0.0",
7879
"sass-loader": "^4.0.0",
80+
"socket.io-client": "^1.7.1",
7981
"style-loader": "^0.13.0",
8082
"superagent": "^2.3.0",
8183
"superagent-promise": "^1.1.0",

src/components/Header/Header.jsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { PropTypes } from 'react';
22
import CSSModules from 'react-css-modules';
3+
import { Link } from 'react-router';
34
import SearchInput from '../SearchInput';
45
import Dropdown from '../Dropdown';
56
import styles from './Header.scss';
@@ -30,8 +31,8 @@ export const Header = ({location, selectedCategory, categories, user, notificati
3031
return (
3132
<li styleName="pages">
3233
<ul>
33-
<li className={currentRoute === 'Dashboard' ? 'active' : null}><a href="/dashboard">Dashboard</a></li>
34-
<li className={currentRoute === 'Requests' ? 'active' : null}><a href="/my-request">Requests</a></li>
34+
<li className={currentRoute === 'Dashboard' ? 'active' : null}><Link to="/dashboard">Dashboard</Link></li>
35+
<li className={currentRoute === 'Requests' ? 'active' : null}><Link to="/my-request">Requests</Link></li>
3536
<li className={currentRoute === 'MyDrones' ? 'active' : null}>My Drones</li>
3637
<li className={currentRoute === 'MyServices' ? 'active' : null}>My Services</li>
3738
<li className={currentRoute === 'Analytics' ? 'active' : null}>Analytics</li>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import React, { PropTypes } from 'react';
2+
import CSSModules from 'react-css-modules';
3+
import MarkerClusterer from 'node-js-marker-clusterer';
4+
import styles from './DronesMapView.scss';
5+
6+
const getIcon = (status) => {
7+
switch (status) {
8+
case 'in-motion':
9+
return 'http://maps.google.com/mapfiles/ms/icons/blue-dot.png';
10+
case 'idle-ready':
11+
return 'http://maps.google.com/mapfiles/ms/icons/green-dot.png';
12+
case 'idle-busy':
13+
return 'http://maps.google.com/mapfiles/ms/icons/orange-dot.png';
14+
default:
15+
throw new Error(`invalid drone status ${status}`);
16+
}
17+
};
18+
19+
const getLatLng = ({currentLocation}) => ({lng: currentLocation[0], lat: currentLocation[1]});
20+
21+
class DronesMapView extends React.Component {
22+
23+
componentDidMount() {
24+
const { drones, mapSettings } = this.props;
25+
this.map = new google.maps.Map(this.node, mapSettings);
26+
const id2Marker = {};
27+
28+
const markers = drones.map((drone) => {
29+
const marker = new google.maps.Marker({
30+
clickable: false,
31+
crossOnDrag: false,
32+
cursor: 'pointer',
33+
position: getLatLng(drone),
34+
icon: getIcon(drone.status),
35+
label: drone.name,
36+
});
37+
id2Marker[drone.id] = marker;
38+
return marker;
39+
});
40+
this.id2Marker = id2Marker;
41+
this.markerCluster = new MarkerClusterer(this.map, markers, { imagePath: '/img/m' });
42+
}
43+
44+
componentWillReceiveProps(nextProps) {
45+
const { drones } = nextProps;
46+
drones.forEach((drone) => {
47+
const marker = this.id2Marker[drone.id];
48+
if (marker) {
49+
marker.setPosition(getLatLng(drone));
50+
marker.setLabel(drone.name);
51+
}
52+
});
53+
this.markerCluster.repaint();
54+
}
55+
56+
shouldComponentUpdate() {
57+
// the whole logic is handled by google plugin
58+
return false;
59+
}
60+
61+
componentWillUnmount() {
62+
this.props.disconnect();
63+
}
64+
65+
render() {
66+
return <div styleName="map-view" ref={(node) => (this.node = node)} />;
67+
}
68+
}
69+
70+
DronesMapView.propTypes = {
71+
drones: PropTypes.array.isRequired,
72+
disconnect: PropTypes.func.isRequired,
73+
mapSettings: PropTypes.object.isRequired,
74+
};
75+
76+
export default CSSModules(DronesMapView, styles);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.map-view {
2+
width: 100%;
3+
height: calc(100vh - 60px - 42px - 50px); // header height - breadcrumb height - footer height
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { asyncConnect } from 'redux-connect';
2+
import {actions} from '../modules/DronesMap';
3+
4+
import DronesMapView from '../components/DronesMapView';
5+
6+
const resolve = [{
7+
promise: ({ store }) => store.dispatch(actions.init()),
8+
}];
9+
10+
const mapState = (state) => state.dronesMap;
11+
12+
export default asyncConnect(resolve, mapState, actions)(DronesMapView);

src/routes/DronesMap/index.js

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { injectReducer } from '../../store/reducers';
2+
3+
export default (store) => ({
4+
path: 'drones-map',
5+
name: 'DronesMap', /* Breadcrumb name */
6+
staticName: true,
7+
getComponent(nextState, cb) {
8+
require.ensure([], (require) => {
9+
const DronesMap = require('./containers/DronesMapContainer').default;
10+
const reducer = require('./modules/DronesMap').default;
11+
12+
injectReducer(store, { key: 'dronesMap', reducer });
13+
cb(null, DronesMap);
14+
}, 'DronesMap');
15+
},
16+
});
+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { handleActions } from 'redux-actions';
2+
import io from 'socket.io-client';
3+
import APIService from 'services/APIService';
4+
import config from '../../../../config/default';
5+
6+
// Drones will be updated and map will be redrawn every 3s
7+
// Otherwise if drones are updated with high frequency (e.g. 0.5s), the map will be freezing
8+
const MIN_REDRAW_DIFF = 3000;
9+
10+
// can't support more than 10k drones
11+
// map will be very slow
12+
const DRONE_LIMIT = 10000;
13+
14+
let socket;
15+
let pendingUpdates = {};
16+
let lastUpdated = null;
17+
let updateTimeoutId;
18+
19+
// ------------------------------------
20+
// Constants
21+
// ------------------------------------
22+
export const DRONES_LOADED = 'DronesMap/DRONES_LOADED';
23+
export const DRONES_UPDATED = 'DronesMap/DRONES_UPDATED';
24+
25+
// ------------------------------------
26+
// Actions
27+
// ------------------------------------
28+
29+
30+
// load drones and initialize socket
31+
export const init = () => async(dispatch) => {
32+
const { body: {items: drones} } = await APIService.searchDrones({limit: DRONE_LIMIT});
33+
lastUpdated = new Date().getTime();
34+
dispatch({ type: DRONES_LOADED, payload: {drones} });
35+
socket = io(config.API_BASE_PATH);
36+
socket.on('dronepositionupdate', (drone) => {
37+
pendingUpdates[drone.id] = drone;
38+
if (updateTimeoutId) {
39+
return;
40+
}
41+
updateTimeoutId = setTimeout(() => {
42+
dispatch({ type: DRONES_UPDATED, payload: pendingUpdates });
43+
pendingUpdates = {};
44+
updateTimeoutId = null;
45+
lastUpdated = new Date().getTime();
46+
}, Math.max(MIN_REDRAW_DIFF - (new Date().getTime() - lastUpdated)), 0);
47+
});
48+
};
49+
50+
// disconnect socket
51+
export const disconnect = () => () => {
52+
socket.disconnect();
53+
socket = null;
54+
clearTimeout(updateTimeoutId);
55+
updateTimeoutId = null;
56+
pendingUpdates = {};
57+
lastUpdated = null;
58+
};
59+
60+
export const actions = {
61+
init,
62+
disconnect,
63+
};
64+
65+
// ------------------------------------
66+
// Reducer
67+
// ------------------------------------
68+
export default handleActions({
69+
[DRONES_LOADED]: (state, { payload: {drones} }) => ({ ...state, drones }),
70+
[DRONES_UPDATED]: (state, { payload: updates }) => ({
71+
...state,
72+
drones: state.drones.map((drone) => {
73+
const updated = updates[drone.id];
74+
return updated || drone;
75+
}),
76+
}),
77+
}, {
78+
drones: null,
79+
// it will show the whole globe
80+
mapSettings: {
81+
zoom: 3,
82+
center: { lat: 0, lng: 0 },
83+
},
84+
});

src/routes/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import MissionPlanner from './MissionPlanner';
66
import MyRequestRoute from './MyRequest';
77
import MyRequestStatusRoute from './MyRequestStatus';
88
import StatusDetailRoute from './StatusDetail';
9+
import DronesMapRoute from './DronesMap';
910

1011
export const createRoutes = (store) => ({
1112
path: '/',
@@ -26,6 +27,7 @@ export const createRoutes = (store) => ({
2627
MyRequestRoute(store),
2728
MyRequestStatusRoute(store),
2829
StatusDetailRoute(store),
30+
DronesMapRoute(store),
2931
],
3032
});
3133

src/services/APIService.js

+15-1
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ export default class APIService {
498498
})).then(() => statusDetail[id]);
499499
}
500500

501-
static fetchMissionList() {
501+
static fetchMissionList() {
502502
return regAndAuth().then((authRes) => {
503503
const accessToken = authRes.body.accessToken;
504504

@@ -562,4 +562,18 @@ static fetchMissionList() {
562562
.then((res) => res.body);
563563
});
564564
}
565+
566+
/**
567+
* Search drones
568+
* @param {Object} params
569+
* @param {Number} params.limit the limit
570+
* @param {Number} params.offset the offset
571+
* @returns {{total: Number, items: Array}} the result
572+
*/
573+
static searchDrones(params) {
574+
return request
575+
.get(`${config.API_BASE_PATH}/api/v1/drones`)
576+
.query(params)
577+
.end();
578+
}
565579
}

src/static/img/m1.png

2.93 KB
Loading

src/static/img/m2.png

3.18 KB
Loading

src/static/img/m3.png

3.86 KB
Loading

src/static/img/m4.png

5.57 KB
Loading

src/static/img/m5.png

6.68 KB
Loading

webpack.config.js

+2
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ module.exports = {
100100
__COVERAGE__: !argv.watch && process.env.NODE_ENV === 'test',
101101
'process.env': {
102102
NODE_ENV: JSON.stringify(process.env.NODE_ENV),
103+
GOOGLE_API_KEY: JSON.stringify(process.env.GOOGLE_API_KEY),
104+
API_BASE_URL: JSON.stringify(process.env.API_BASE_URL),
103105
},
104106
}),
105107
new HtmlWebpackPlugin({

0 commit comments

Comments
 (0)