Skip to content

935 map #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
6 changes: 4 additions & 2 deletions src/components/Header/Header.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -28,11 +29,12 @@ export const Header = ({location, selectedCategory, categories, user, notificati
return (
<li styleName="pages">
<ul>
<li className={currentRoute === 'Dashboard' ? 'active' : null}><a href="/dashboard">Dashboard</a></li>
<li className={currentRoute === 'Requests' ? 'active' : null}><a href="/my-request">Requests</a></li>
<li className={currentRoute === 'Dashboard' ? 'active' : null}><Link to="/dashboard">Dashboard</Link></li>
<li className={currentRoute === 'Requests' ? 'active' : null}><Link to="/my-request">Requests</Link></li>
<li className={currentRoute === 'MyDrones' ? 'active' : null}>My Drones</li>
<li className={currentRoute === 'MyServices' ? 'active' : null}>My Services</li>
<li className={currentRoute === 'Analytics' ? 'active' : null}>Analytics</li>
<li className={currentRoute === 'DroneMap' ? 'active' : null}><Link to="/drones-map">Drone Traffic</Link></li>
</ul>
</li>
);
Expand Down
76 changes: 76 additions & 0 deletions src/routes/DronesMap/components/DronesMapView.jsx
Original file line number Diff line number Diff line change
@@ -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 <div styleName="map-view" ref={(node) => (this.node = node)} />;
}
}

DronesMapView.propTypes = {
drones: PropTypes.array.isRequired,
disconnect: PropTypes.func.isRequired,
mapSettings: PropTypes.object.isRequired,
};

export default CSSModules(DronesMapView, styles);
4 changes: 4 additions & 0 deletions src/routes/DronesMap/components/DronesMapView.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.map-view {
width: 100%;
height: calc(100vh - 60px - 42px - 50px); // header height - breadcrumb height - footer height
}
12 changes: 12 additions & 0 deletions src/routes/DronesMap/containers/DronesMapContainer.js
Original file line number Diff line number Diff line change
@@ -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);
16 changes: 16 additions & 0 deletions src/routes/DronesMap/index.js
Original file line number Diff line number Diff line change
@@ -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');
},
});
84 changes: 84 additions & 0 deletions src/routes/DronesMap/modules/DronesMap.js
Original file line number Diff line number Diff line change
@@ -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 },
},
});
2 changes: 2 additions & 0 deletions src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: '/',
Expand All @@ -18,6 +19,7 @@ export const createRoutes = (store) => ({
ServiceRequestRoute(store),
DashboardRoute(store),
MyRequestRoute(store),
DronesMapRoute(store),
],
});

Expand Down
22 changes: 22 additions & 0 deletions src/services/APIService.js
Original file line number Diff line number Diff line change
@@ -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();
}
}
Binary file added src/static/img/m1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/static/img/m2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/static/img/m3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/static/img/m4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/static/img/m5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down