Skip to content

Commit f6bca5d

Browse files
lstkzgondzo
authored and
gondzo
committed
integrate map
1 parent 94182f9 commit f6bca5d

File tree

17 files changed

+228
-3
lines changed

17 files changed

+228
-3
lines changed

README.md

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +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',
11+
API_BASE_URL: process.env.API_BASE_URL || 'https://kb-dsp-server.herokuapp.com',
812
};

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"json-loader": "^0.5.4",
4242
"lodash": "^4.16.4",
4343
"moment": "^2.17.0",
44+
"node-js-marker-clusterer": "^1.0.0",
4445
"node-sass": "^3.7.0",
4546
"postcss-flexboxfixer": "0.0.5",
4647
"postcss-loader": "^0.13.0",
@@ -64,6 +65,7 @@
6465
"redux-logger": "^2.6.1",
6566
"redux-thunk": "^2.0.0",
6667
"sass-loader": "^4.0.0",
68+
"socket.io-client": "^1.7.1",
6769
"style-loader": "^0.13.0",
6870
"superagent": "^2.3.0",
6971
"superagent-promise": "^1.1.0",

src/components/Header/Header.jsx

Lines changed: 3 additions & 2 deletions
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';
@@ -28,8 +29,8 @@ export const Header = ({location, selectedCategory, categories, user, notificati
2829
return (
2930
<li styleName="pages">
3031
<ul>
31-
<li className={currentRoute === 'Dashboard' ? 'active' : null}><a href="/dashboard">Dashboard</a></li>
32-
<li className={currentRoute === 'Requests' ? 'active' : null}><a href="/my-request">Requests</a></li>
32+
<li className={currentRoute === 'Dashboard' ? 'active' : null}><Link to="/dashboard">Dashboard</Link></li>
33+
<li className={currentRoute === 'Requests' ? 'active' : null}><Link to="/my-request">Requests</Link></li>
3334
<li className={currentRoute === 'MyDrones' ? 'active' : null}>My Drones</li>
3435
<li className={currentRoute === 'MyServices' ? 'active' : null}>My Services</li>
3536
<li className={currentRoute === 'Analytics' ? 'active' : null}>Analytics</li>
Lines changed: 76 additions & 0 deletions
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);
Lines changed: 4 additions & 0 deletions
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+
}
Lines changed: 12 additions & 0 deletions
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

Lines changed: 16 additions & 0 deletions
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+
});
Lines changed: 84 additions & 0 deletions
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_URL);
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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import CoreLayout from 'layouts/CoreLayout';
22
import ServiceRequestRoute from './ServiceRequest';
33
import DashboardRoute from './Dashboard';
44
import MyRequestRoute from './MyRequest';
5+
import DronesMapRoute from './DronesMap';
56

67
export const createRoutes = (store) => ({
78
path: '/',
@@ -18,6 +19,7 @@ export const createRoutes = (store) => ({
1819
ServiceRequestRoute(store),
1920
DashboardRoute(store),
2021
MyRequestRoute(store),
22+
DronesMapRoute(store),
2123
],
2224
});
2325

src/services/APIService.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import superagent from 'superagent';
2+
import superagentPromise from 'superagent-promise';
3+
import {API_BASE_URL} from '../../config/default';
4+
5+
const request = superagentPromise(superagent, Promise);
6+
7+
export default class APIService {
8+
9+
/**
10+
* Search drones
11+
* @param {Object} params
12+
* @param {Number} params.limit the limit
13+
* @param {Number} params.offset the offset
14+
* @returns {{total: Number, items: Array}} the result
15+
*/
16+
static searchDrones(params) {
17+
return request
18+
.get(`${API_BASE_URL}/api/v1/drones`)
19+
.query(params)
20+
.end();
21+
}
22+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ module.exports = {
9999
__COVERAGE__: !argv.watch && process.env.NODE_ENV === 'test',
100100
'process.env': {
101101
NODE_ENV: JSON.stringify(process.env.NODE_ENV),
102+
GOOGLE_API_KEY: JSON.stringify(process.env.GOOGLE_API_KEY),
103+
API_BASE_URL: JSON.stringify(process.env.API_BASE_URL),
102104
},
103105
}),
104106
new HtmlWebpackPlugin({

0 commit comments

Comments
 (0)