diff --git a/README.md b/README.md
index 4815f11..cd67372 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,6 @@
* node v6 (https://nodejs.org)
## Quick Start
-* `npm install -g nodemon`
* `npm install`
* `npm run dev`
* Navigate browser to `http://localhost:3000`
@@ -18,6 +17,7 @@ See Guild https://github.com/lorenwest/node-config/wiki/Configuration-Files
|----|-----------|
|`PORT`| The port to listen|
|`GOOGLE_API_KEY`| The google api key see (https://developers.google.com/maps/documentation/javascript/get-api-key#key)|
+|`API_BASE_URL`| The base URL for Drone API |
## Install dependencies
diff --git a/config/default.js b/config/default.js
index 51a169d..d7ebe10 100644
--- a/config/default.js
+++ b/config/default.js
@@ -3,6 +3,11 @@
* Main config file
*/
module.exports = {
+ // below env variables are NOT visible in frontend
PORT: process.env.PORT || 3000,
+
+ // below env variables are visible in frontend
GOOGLE_API_KEY: process.env.GOOGLE_API_KEY || 'AIzaSyCrL-O319wNJK8kk8J_JAYsWgu6yo5YsDI',
+ API_BASE_URL: process.env.API_BASE_URL || 'https://kb-dsp-server-dev.herokuapp.com',
+ //API_BASE_URL: process.env.API_BASE_URL || 'http://localhost:5000',
};
diff --git a/package.json b/package.json
index 962848b..54340c9 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
"json-loader": "^0.5.4",
"lodash": "^4.16.4",
"moment": "^2.17.0",
+ "node-js-marker-clusterer": "^1.0.0",
"node-sass": "^3.7.0",
"postcss-flexboxfixer": "0.0.5",
"postcss-loader": "^0.13.0",
@@ -64,6 +65,7 @@
"redux-logger": "^2.6.1",
"redux-thunk": "^2.0.0",
"sass-loader": "^4.0.0",
+ "socket.io-client": "^1.7.1",
"style-loader": "^0.13.0",
"superagent": "^2.3.0",
"superagent-promise": "^1.1.0",
diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx
index 8a08c4c..27d3ec1 100644
--- a/src/components/Header/Header.jsx
+++ b/src/components/Header/Header.jsx
@@ -1,5 +1,6 @@
import React, { PropTypes } from 'react';
import CSSModules from 'react-css-modules';
+import { Link } from 'react-router';
import SearchInput from '../SearchInput';
import Dropdown from '../Dropdown';
import styles from './Header.scss';
@@ -28,11 +29,12 @@ export const Header = ({location, selectedCategory, categories, user, notificati
return (
- Dashboard
- Requests
+ Dashboard
+ Requests
My Drones
My Services
Analytics
+ Drone Traffic
);
diff --git a/src/routes/DronesMap/components/DronesMapView.jsx b/src/routes/DronesMap/components/DronesMapView.jsx
new file mode 100644
index 0000000..f69868c
--- /dev/null
+++ b/src/routes/DronesMap/components/DronesMapView.jsx
@@ -0,0 +1,76 @@
+import React, { PropTypes } from 'react';
+import CSSModules from 'react-css-modules';
+import MarkerClusterer from 'node-js-marker-clusterer';
+import styles from './DronesMapView.scss';
+
+const getIcon = (status) => {
+ switch (status) {
+ case 'in-motion':
+ return 'http://maps.google.com/mapfiles/ms/icons/blue-dot.png';
+ case 'idle-ready':
+ return 'http://maps.google.com/mapfiles/ms/icons/green-dot.png';
+ case 'idle-busy':
+ return 'http://maps.google.com/mapfiles/ms/icons/orange-dot.png';
+ default:
+ throw new Error(`invalid drone status ${status}`);
+ }
+};
+
+const getLatLng = ({currentLocation}) => ({lng: currentLocation[0], lat: currentLocation[1]});
+
+class DronesMapView extends React.Component {
+
+ componentDidMount() {
+ const { drones, mapSettings } = this.props;
+ this.map = new google.maps.Map(this.node, mapSettings);
+ const id2Marker = {};
+
+ const markers = drones.map((drone) => {
+ const marker = new google.maps.Marker({
+ clickable: false,
+ crossOnDrag: false,
+ cursor: 'pointer',
+ position: getLatLng(drone),
+ icon: getIcon(drone.status),
+ label: drone.name,
+ });
+ id2Marker[drone.id] = marker;
+ return marker;
+ });
+ this.id2Marker = id2Marker;
+ this.markerCluster = new MarkerClusterer(this.map, markers, { imagePath: '/img/m' });
+ }
+
+ componentWillReceiveProps(nextProps) {
+ const { drones } = nextProps;
+ drones.forEach((drone) => {
+ const marker = this.id2Marker[drone.id];
+ if (marker) {
+ marker.setPosition(getLatLng(drone));
+ marker.setLabel(drone.name);
+ }
+ });
+ this.markerCluster.repaint();
+ }
+
+ shouldComponentUpdate() {
+ // the whole logic is handled by google plugin
+ return false;
+ }
+
+ componentWillUnmount() {
+ this.props.disconnect();
+ }
+
+ render() {
+ return (this.node = node)} />;
+ }
+}
+
+DronesMapView.propTypes = {
+ drones: PropTypes.array.isRequired,
+ disconnect: PropTypes.func.isRequired,
+ mapSettings: PropTypes.object.isRequired,
+};
+
+export default CSSModules(DronesMapView, styles);
diff --git a/src/routes/DronesMap/components/DronesMapView.scss b/src/routes/DronesMap/components/DronesMapView.scss
new file mode 100644
index 0000000..c512305
--- /dev/null
+++ b/src/routes/DronesMap/components/DronesMapView.scss
@@ -0,0 +1,4 @@
+.map-view {
+ width: 100%;
+ height: calc(100vh - 60px - 42px - 50px); // header height - breadcrumb height - footer height
+}
diff --git a/src/routes/DronesMap/containers/DronesMapContainer.js b/src/routes/DronesMap/containers/DronesMapContainer.js
new file mode 100644
index 0000000..b95532b
--- /dev/null
+++ b/src/routes/DronesMap/containers/DronesMapContainer.js
@@ -0,0 +1,12 @@
+import { asyncConnect } from 'redux-connect';
+import {actions} from '../modules/DronesMap';
+
+import DronesMapView from '../components/DronesMapView';
+
+const resolve = [{
+ promise: ({ store }) => store.dispatch(actions.init()),
+}];
+
+const mapState = (state) => state.dronesMap;
+
+export default asyncConnect(resolve, mapState, actions)(DronesMapView);
diff --git a/src/routes/DronesMap/index.js b/src/routes/DronesMap/index.js
new file mode 100644
index 0000000..5b00c0b
--- /dev/null
+++ b/src/routes/DronesMap/index.js
@@ -0,0 +1,16 @@
+import { injectReducer } from '../../store/reducers';
+
+export default (store) => ({
+ path: 'drones-map',
+ name: 'DronesMap', /* Breadcrumb name */
+ staticName: true,
+ getComponent(nextState, cb) {
+ require.ensure([], (require) => {
+ const DronesMap = require('./containers/DronesMapContainer').default;
+ const reducer = require('./modules/DronesMap').default;
+
+ injectReducer(store, { key: 'dronesMap', reducer });
+ cb(null, DronesMap);
+ }, 'DronesMap');
+ },
+});
diff --git a/src/routes/DronesMap/modules/DronesMap.js b/src/routes/DronesMap/modules/DronesMap.js
new file mode 100644
index 0000000..19b9a29
--- /dev/null
+++ b/src/routes/DronesMap/modules/DronesMap.js
@@ -0,0 +1,84 @@
+import { handleActions } from 'redux-actions';
+import io from 'socket.io-client';
+import APIService from 'services/APIService';
+import config from '../../../../config/default';
+
+// Drones will be updated and map will be redrawn every 3s
+// Otherwise if drones are updated with high frequency (e.g. 0.5s), the map will be freezing
+const MIN_REDRAW_DIFF = 3000;
+
+// can't support more than 10k drones
+// map will be very slow
+const DRONE_LIMIT = 10000;
+
+let socket;
+let pendingUpdates = {};
+let lastUpdated = null;
+let updateTimeoutId;
+
+// ------------------------------------
+// Constants
+// ------------------------------------
+export const DRONES_LOADED = 'DronesMap/DRONES_LOADED';
+export const DRONES_UPDATED = 'DronesMap/DRONES_UPDATED';
+
+// ------------------------------------
+// Actions
+// ------------------------------------
+
+
+// load drones and initialize socket
+export const init = () => async(dispatch) => {
+ const { body: {items: drones} } = await APIService.searchDrones({limit: DRONE_LIMIT});
+ lastUpdated = new Date().getTime();
+ dispatch({ type: DRONES_LOADED, payload: {drones} });
+ socket = io(config.API_BASE_URL);
+ socket.on('dronepositionupdate', (drone) => {
+ pendingUpdates[drone.id] = drone;
+ if (updateTimeoutId) {
+ return;
+ }
+ updateTimeoutId = setTimeout(() => {
+ dispatch({ type: DRONES_UPDATED, payload: pendingUpdates });
+ pendingUpdates = {};
+ updateTimeoutId = null;
+ lastUpdated = new Date().getTime();
+ }, Math.max(MIN_REDRAW_DIFF - (new Date().getTime() - lastUpdated)), 0);
+ });
+};
+
+// disconnect socket
+export const disconnect = () => () => {
+ socket.disconnect();
+ socket = null;
+ clearTimeout(updateTimeoutId);
+ updateTimeoutId = null;
+ pendingUpdates = {};
+ lastUpdated = null;
+};
+
+export const actions = {
+ init,
+ disconnect,
+};
+
+// ------------------------------------
+// Reducer
+// ------------------------------------
+export default handleActions({
+ [DRONES_LOADED]: (state, { payload: {drones} }) => ({ ...state, drones }),
+ [DRONES_UPDATED]: (state, { payload: updates }) => ({
+ ...state,
+ drones: state.drones.map((drone) => {
+ const updated = updates[drone.id];
+ return updated || drone;
+ }),
+ }),
+}, {
+ drones: null,
+ // it will show the whole globe
+ mapSettings: {
+ zoom: 3,
+ center: { lat: 0, lng: 0 },
+ },
+});
diff --git a/src/routes/index.js b/src/routes/index.js
index 1068fb0..5ba1783 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -2,6 +2,7 @@ import CoreLayout from 'layouts/CoreLayout';
import ServiceRequestRoute from './ServiceRequest';
import DashboardRoute from './Dashboard';
import MyRequestRoute from './MyRequest';
+import DronesMapRoute from './DronesMap';
export const createRoutes = (store) => ({
path: '/',
@@ -18,6 +19,7 @@ export const createRoutes = (store) => ({
ServiceRequestRoute(store),
DashboardRoute(store),
MyRequestRoute(store),
+ DronesMapRoute(store),
],
});
diff --git a/src/services/APIService.js b/src/services/APIService.js
new file mode 100644
index 0000000..53e4e4b
--- /dev/null
+++ b/src/services/APIService.js
@@ -0,0 +1,22 @@
+import superagent from 'superagent';
+import superagentPromise from 'superagent-promise';
+import {API_BASE_URL} from '../../config/default';
+
+const request = superagentPromise(superagent, Promise);
+
+export default class APIService {
+
+ /**
+ * Search drones
+ * @param {Object} params
+ * @param {Number} params.limit the limit
+ * @param {Number} params.offset the offset
+ * @returns {{total: Number, items: Array}} the result
+ */
+ static searchDrones(params) {
+ return request
+ .get(`${API_BASE_URL}/api/v1/drones`)
+ .query(params)
+ .end();
+ }
+}
diff --git a/src/static/img/m1.png b/src/static/img/m1.png
new file mode 100644
index 0000000..329ff52
Binary files /dev/null and b/src/static/img/m1.png differ
diff --git a/src/static/img/m2.png b/src/static/img/m2.png
new file mode 100644
index 0000000..b999cbc
Binary files /dev/null and b/src/static/img/m2.png differ
diff --git a/src/static/img/m3.png b/src/static/img/m3.png
new file mode 100644
index 0000000..9f30b30
Binary files /dev/null and b/src/static/img/m3.png differ
diff --git a/src/static/img/m4.png b/src/static/img/m4.png
new file mode 100644
index 0000000..0d3f826
Binary files /dev/null and b/src/static/img/m4.png differ
diff --git a/src/static/img/m5.png b/src/static/img/m5.png
new file mode 100644
index 0000000..61387d2
Binary files /dev/null and b/src/static/img/m5.png differ
diff --git a/webpack.config.js b/webpack.config.js
index ffe7f77..a572ebb 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -99,6 +99,8 @@ module.exports = {
__COVERAGE__: !argv.watch && process.env.NODE_ENV === 'test',
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV),
+ GOOGLE_API_KEY: JSON.stringify(process.env.GOOGLE_API_KEY),
+ API_BASE_URL: JSON.stringify(process.env.API_BASE_URL),
},
}),
new HtmlWebpackPlugin({
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