From fd01748d5c66740b4163ee47152824495e4fa2ab Mon Sep 17 00:00:00 2001 From: Kyle Bowerman Date: Tue, 15 Nov 2016 12:17:46 -0600 Subject: [PATCH 1/2] 233317 submission --- README.md | 9 +- package.json | 2 + src/App.js | 2 - src/App.test.js | 8 - src/components/maps/GoogleMap.js | 2 +- src/components/mission/EditMissionPlanner.js | 355 -------------- .../mission/EditMissionPlannerWrapper.js | 35 -- src/components/mission/InfoWindow.js | 298 ------------ src/components/mission/MissionPlanner.js | 439 ++++++++++-------- src/components/mission/MissionPlannerList.js | 78 ---- .../mission/MissionPlannerWrapper.js | 34 -- src/config/index.js | 20 + src/index.js | 12 +- src/styles/App.css | 29 +- 14 files changed, 304 insertions(+), 1019 deletions(-) delete mode 100644 src/App.test.js delete mode 100644 src/components/mission/EditMissionPlanner.js delete mode 100644 src/components/mission/EditMissionPlannerWrapper.js delete mode 100644 src/components/mission/InfoWindow.js delete mode 100644 src/components/mission/MissionPlannerList.js delete mode 100644 src/components/mission/MissionPlannerWrapper.js diff --git a/README.md b/README.md index d0ff7c0..dda2174 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,16 @@ react-app --- # Local Deployment -Copy `envSample` as `.env`. -Install node dependencies using `npm install` -Run the development server using `npm run start` +- Copy `envSample` as `.env` and configure variables described in section below +- Install node dependencies using `npm install` +- Run the development server using `npm run start` +- `http://localhost:3000` should be opened in the browser after succesfull server run # Local Configuration Variables You can edit them in `.env` file. +- Set `REACT_APP_GOOGLE_API_KEY` to google API key which can be obtained [here](https://console.developers.google.com/flows/enableapi?apiid=maps_backend%2Cgeocoding_backend%2Cdirections_backend%2Cdistance_matrix_backend%2Celevation_backend%2Cplaces_backend&reusekey=true) +- Set `REACT_APP_AUTH0_CLIEND_ID` and `REACT_APP_AUTH0_DOMAIN` which can be obtained at [Auth0](https://auth0.com/) # Heroku Deployment ``` diff --git a/package.json b/package.json index 656f6f2..aaa8cee 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,8 @@ "version": "0.1.0", "private": true, "devDependencies": { + "enzyme": "^2.6.0", + "react-addons-test-utils": "^15.3.2", "react-scripts": "0.7.0" }, "dependencies": { diff --git a/src/App.js b/src/App.js index c33984f..6f53270 100644 --- a/src/App.js +++ b/src/App.js @@ -59,8 +59,6 @@ class App extends Component { Drones Planner Logout - test: {process.env.FOO} - test2: {process.env.BIZ} {child} diff --git a/src/App.test.js b/src/App.test.js deleted file mode 100644 index b84af98..0000000 --- a/src/App.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './App'; - -it('renders without crashing', () => { - const div = document.createElement('div'); - ReactDOM.render(, div); -}); diff --git a/src/components/maps/GoogleMap.js b/src/components/maps/GoogleMap.js index f490ea0..f2c7aad 100644 --- a/src/components/maps/GoogleMap.js +++ b/src/components/maps/GoogleMap.js @@ -69,7 +69,7 @@ class GoogleMap extends Component { componentWillUnmount() { const _self = this; // remove all markers - _self.markers.forEach((single) => { + _self.markers && _self.markers.forEach((single) => { single.setMap(null); }); } diff --git a/src/components/mission/EditMissionPlanner.js b/src/components/mission/EditMissionPlanner.js deleted file mode 100644 index 31cfd16..0000000 --- a/src/components/mission/EditMissionPlanner.js +++ /dev/null @@ -1,355 +0,0 @@ -/** - * Copyright (c) 2016 Topcoder Inc, All rights reserved. - */ - -/** - * The root mission planner component - * - * @author TCSCODER - * @version 1.0.0 - */ -import React, { Component } from 'react'; -import ReactDOM from 'react-dom'; -import circleGreen from '../../i/circle_green.svg'; -import InfoWindow from './InfoWindow.js'; -import { Grid, Row, Col, ButtonToolbar, Button } from 'react-bootstrap'; -import { ToastContainer, ToastMessage } from 'react-toastr'; - -import MissionApi from '../../api/Mission.js'; -import config from '../../config'; -import { hashHistory } from 'react-router'; - -const ToastMessageFactory = React.createFactory(ToastMessage.animation); - -class EditMissionPlanner extends Component { - - constructor(props) { - super(props); - this.handleMapClick = this.handleMapClick.bind(this); - this.handleMarkerClick = this.handleMarkerClick.bind(this); - this.handleMissionItemUpdate = this.handleMissionItemUpdate.bind(this); - this.doHandleMarkerClick = this.doHandleMarkerClick.bind(this); - this.clearAll = this.clearAll.bind(this); - this.save = this.save.bind(this); - this.addPoint = this.addPoint.bind(this); - this.missionApi = new MissionApi(config.api.basePath, props.auth); - this.state = { - // the path markers - markers: [], - missionItems: [], - idSequence: 0 - } - } - - clearAll() { - this.poly.setMap(null); - this.poly = null; - this.state.markers.forEach((single) => { - single.setMap(null); - }); - this.setState({ markers: [], missionItems: [], idSequence: 0 }); - } - - save(event) { - event.preventDefault(); - const _self = this; - if (_self.state.missionItems.length === 0) { - _self.toastContainer.warning('', - 'Add some waypoints before saving a mission', { - timeOut: 3000, - preventDuplicates:true - }); - } else if (!_self.state.missionName) { - _self.toastContainer.warning('', - 'Enter a mission name', { - timeOut: 3000, - preventDuplicates:true - }); - } else { - // save the mission - _self.missionApi.update(_self.props.missionId, _self.state.missionName, _self.state.missionItems, _self.state.plannedHomePosition).then(() => { - _self.toastContainer.success('', - 'Mission updated', { - timeOut: 1000, - preventDuplicates:false - }); - setTimeout(() => { - hashHistory.push('/list'); - }, 2000); - }); - } - } - - /** - * Handle the mission item update fired from info window component - * @param {Number} id the id of mission item in mission items array - * @param {Object} missionItem the updated mission item - */ - handleMissionItemUpdate(id, missionItem) { - if (id === 0) { - this.setState({ plannedHomePosition: missionItem }); - } else { - const missionItems = this.state.missionItems; - missionItems.splice(id - 1, 1, missionItem); - this.setState({ missionItems: missionItems }); - } - } - - /** - * Actual marker click handler - * @param {MouseEvent} event the MouseEvent object fired by google map api - * @param {Marker} marker the maker which is clicked - * @param {Object} item the mission item object - */ - doHandleMarkerClick(event, marker, item) { - const _self = this; - const google = window.google; - const div = document.createElement('div'); - const id = marker.get('id'); - ReactDOM.render(_self.renderInfoWindow(marker.getLabel().text, { lat: marker.getPosition().lat(), - lng: marker.getPosition().lng() }, item, id), div); - const infoWindow = new google.maps.InfoWindow({ - content: div, - maxWidth: 400 - }); - infoWindow.open(_self.map, marker); - } - - /** - * Attach the click event on marker and handle the click event on the marker - * - * @param {object} event the propogated event - * @param {Object} item the mission item object - */ - handleMarkerClick(marker, item) { - const _self = this; - marker.addListener('click', (event) => { - _self.doHandleMarkerClick(event, marker, item); - }); - } - - /** - * Render the info window for the specified type i.e, H, T, W - */ - renderInfoWindow(type, position, item, id) { - return ( - - ); - } - - initPolyline() { - const google = window.google; - const _self = this; - if (!_self.poly) { - _self.poly = new google.maps.Polyline({ - strokeColor: '#ff794d', - strokeOpacity: 1.0, - strokeWeight: 2 - }); - _self.poly.setMap(_self.map); - } - } - - getMarkerOpts(idSequence, lat, lng) { - const _self = this; - const google = window.google; - const markerOpts = { - position: new google.maps.LatLng(lat, lng), - cursor: 'pointer', - map: _self.map, - icon: circleGreen - }; - if (idSequence === 0) { - // add the home - markerOpts.label = { - color: '#759e57', - text: 'H', - fontWeight: '800' - }; - } else if (idSequence === 1) { - // add the takeoff marker - markerOpts.label = { - color: '#759e57', - text: 'T', - fontWeight: '800' - }; - } else { - // add general waypoint marker - markerOpts.label = { - color: '#759e57', - text: `${idSequence}`, - fontWeight: '800' - }; - } - return markerOpts; - } - - getMissionItem(idSequence, lat, lng, alt) { - if (idSequence !== 0) { - return { - autoContinue: true, - command: idSequence === 1 ? 22 : 16, - coordinate: [lat, lng, alt], - frame: 3, - id: idSequence, - param1: 0.000000, - param2: 0.000000, - param3: 0.000000, - param4: 0.000000, - type: 'missionItem' - } - } - return { - autoContinue: true, - command: 16, - coordinate: [lat, lng, alt], - frame: 0, - id: idSequence, - param1: 0.000000, - param2: 0.000000, - param3: 0.000000, - param4: 0.000000, - type: 'missionItem' - } - } - - addPoint(latLng, alt) { - const google = window.google; - const _self = this; - _self.initPolyline(); - const path = _self.poly.getPath(); - const markers = _self.state.markers; - let idSequence = _self.state.idSequence; - const markerOpts = _self.getMarkerOpts(idSequence, latLng.lat(), latLng.lng()); - const marker = new google.maps.Marker(markerOpts); - marker.set('id', idSequence); - const missionItems = _self.state.missionItems; - const missionItem = _self.getMissionItem(idSequence, latLng.lat(), latLng.lng(), alt); - if (idSequence !== 0) { - // if id sequence is 0 than it is home point, so home point is not added to mission items. - missionItems.push(missionItem); - } else { - _self.setState({ plannedHomePosition: missionItem }); - } - idSequence += 1; - _self.handleMarkerClick(marker, missionItem); - markers.push(marker); - _self.setState({ markers: markers, idSequence: idSequence, missionItems: missionItems }); - if (idSequence !== 1) { - path.push(latLng); - } - } - - /** - * Handle the click event on the map - * @param {object} event the propogated event - */ - handleMapClick(event) { - this.addPoint(event.latLng, 25.000000); - } - - componentWillReceiveProps(nextProps) { - if (this.props.loaded === false && nextProps.loaded === true) { - this.loadMap(); - } - } - - shouldComponentUpdate() { - // never update the map, rendering is delegated to google api - return false; - } - - componentDidMount() { - if (this.props.loaded === true) { - this.loadMap(); - } - } - - componentWillUnmount() { - const _self = this; - if (_self.poly) { - _self.poly.setMap(null); - _self.poly = null; - } - // remove all markers - _self.state.markers.forEach((single) => { - single.setMap(null); - }); - _self.map = null; - } - - loadInitialPoints(mission) { - const _self = this; - const google = window.google; - _self.initPolyline(); - const path = _self.poly.getPath(); - const markers = _self.state.markers; - - // add planned home position marker - const markerOpts = _self.getMarkerOpts(0, mission.plannedHomePosition.coordinate[0], - mission.plannedHomePosition.coordinate[1]); - const marker = new google.maps.Marker(markerOpts); - marker.set('id', 0); - _self.handleMarkerClick(marker, mission.plannedHomePosition); - markers.push(marker); - - mission.missionItems.forEach((single, index) => { - const markerOpts = _self.getMarkerOpts(index + 1, single.coordinate[0], single.coordinate[1]); - const marker = new google.maps.Marker(markerOpts); - marker.set('id', index + 1); - _self.handleMarkerClick(marker, single); - markers.push(marker); - path.push(new google.maps.LatLng(single.coordinate[0], single.coordinate[1])); - }); - _self.setState({ markers: markers, idSequence: mission.missionItems.length + 1, - missionItems: mission.missionItems, - plannedHomePosition: mission.plannedHomePosition, missionName: mission.missionName }); - } - - loadMap() { - const _self = this; - const google = window.google; - _self.missionApi.getSingle(_self.props.missionId).then((mission) => { - _self.map = new google.maps.Map(_self.mapElement, { - center: { - lat: _self.props.lat, - lng: _self.props.lng - }, - zoom: _self.props.zoom, - }); - // add click listener on map - _self.map.addListener('click', this.handleMapClick); - _self.loadInitialPoints(mission); - }); - } - - render() { - return ( - - - - this.toastContainer = element} className="toast-top-right" /> - - - - - - - - List All missions - - - - - -
this.mapElement = element } /> - - - - ); - } -} - -export default EditMissionPlanner; diff --git a/src/components/mission/EditMissionPlannerWrapper.js b/src/components/mission/EditMissionPlannerWrapper.js deleted file mode 100644 index f0d97ad..0000000 --- a/src/components/mission/EditMissionPlannerWrapper.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2016 Topcoder Inc, All rights reserved. - */ - -/** - * The edit mission planner wrapper component - * - * @author TCSCODER - * @version 1.0.0 - */ -import React, { Component } from 'react'; -import EditMissionPlanner from './EditMissionPlanner.js'; - -class EditMissionPlannerWrapper extends Component { - constructor(props) { - super(props); - this.state = { - // represents the map options - mapOpts: { - lat: 42.010, - lng: -96.824, - zoom: 9 - }, - id: props.params.id - } - } - - render() { - return ( - - ); - } -} - -export default EditMissionPlannerWrapper; diff --git a/src/components/mission/InfoWindow.js b/src/components/mission/InfoWindow.js deleted file mode 100644 index a2dac22..0000000 --- a/src/components/mission/InfoWindow.js +++ /dev/null @@ -1,298 +0,0 @@ -/** - * Copyright (c) 2016 Topcoder Inc, All rights reserved. - */ - -/** - * The mission planner info window component - * - * @author TCSCODER - * @version 1.0.0 - */ -import React, { Component } from 'react'; -import { Grid, Row, Col, Form, FormControl, InputGroup, ControlLabel, FormGroup } from 'react-bootstrap'; -import '../../styles/App.css'; -import Select from 'react-select'; - -import 'react-select/dist/react-select.css'; -import commands from './data/commands.js'; -import frames from './data/frames.js'; - -class InfoWindow extends Component { - - constructor(props) { - super(props); - this.getSelectedCommand = this.getSelectedCommand.bind(this); - this.getSequence = this.getSequence.bind(this); - this.handleCommandChange = this.handleCommandChange.bind(this); - this.handleFrameChange = this.handleFrameChange.bind(this); - - // set initial state - this.state = { - type: props.type, - command: props.command, - frame: props.frame || 3, - lat: props.position.lat, - lng: props.position.lng, - altitude: props.altitude, - param1: props.param1, - param2: props.param2, - param3: props.param3, - param4: props.param4 - } - } - - getSelectedCommand() { - if (this.state.type === 'H') { - return 'Planned Home Position'; - } else if (this.state.type === 'T') { - return 'Takeoff'; - } else if (this.state.type === 'W') { - return 'Waypoint'; - } else { - return ''; - } - } - - getSequence() { - if (this.state.type !== 'H' && this.state.type !== 'T') { - return this.props.id; - } - return this.state.type; - } - - /** - * Get the float representation of the value - * @param {String} value the value to parse - */ - getFloatValue(value) { - if (value) { - try { - return parseFloat(value, 10).toFixed(6); - } catch (e) { - // value is not a number, return 0 - return parseFloat(0, 10).toFixed(6); - } - } - return parseFloat(0, 10).toFixed(6); - } - - updateState(name, event, cb) { - const change = { }; - change[name] = this.getFloatValue(event.target.value); - this.setState(change, cb); - } - - getMissionItem() { - const missionItem = { - autoContinue: true, - command: this.state.command, - coordinate: [this.state.lat, this.state.lng, this.state.altitude], - frame: this.state.frame, - id: this.props.id, - param1: this.state.param1, - param2: this.state.param2, - param3: this.state.param3, - param4: this.state.param4, - type: 'missionItem' - }; - return missionItem; - } - - handleCommandChange(val) { - this.setState({ command: val.value }, () => { - // when type is home this can never fire, so compute mission item for other commands - const missionItem = this.getMissionItem(); - this.props.onUpdate(this.props.id, missionItem); - }); - } - - handleFrameChange(val) { - this.setState({ frame: val.value }, () => { - // when type is home this can never fire, so compute mission item for other commands - const missionItem = this.getMissionItem(); - this.props.onUpdate(this.props.id, missionItem); - }); - } - - handleHomeChange(name, event) { - this.updateState(name, event, () => { - const missionItem = this.getMissionItem(); - this.props.onUpdate(this.props.id, missionItem); - }); - } - - handlePointChange(name, event) { - this.updateState(name, event, () => { - const missionItem = this.getMissionItem(); - this.props.onUpdate(this.props.id, missionItem); - }); - } - - render() { - const isHome = this.props.type === 'H'; - return ( - - - {this.getSequence()} - {this.getSelectedCommand()} - - { isHome === false && - - -

Provides advanced access to all commands. Be very careful!

- -
- } - { isHome === true ? ( -
- - -

Planned home position. Actual home position set by vehicle

- -
-
- - - Latitude: - - - - this.homeLatElement = homeLatElem} - value={this.state.lat} onChange={this.handleHomeChange.bind(this, 'lat')} /> - - - - - - Longitude: - - - - this.homeLngElement = homeLngElem} - value={this.state.lng} onChange={this.handleHomeChange.bind(this, 'lng')} /> - - - - - - Altitude: - - - - this.homeAltElement = homeAltElem} - value={this.state.altitude} onChange={this.handleHomeChange.bind(this, 'altitude')} /> - m - - - -
-
- ) : ( -
- - - - - -
- - - Param1: - - - - this.param1Element = param1Elem} - onChange={this.handlePointChange.bind(this, 'param1')} /> - - - - - - Param2: - - - - this.param2Element = param2Elem} - onChange={this.handlePointChange.bind(this, 'param2')} /> - - - - - - Param3: - - - - this.param3Element = param3Elem} - onChange={this.handlePointChange.bind(this, 'param3')} /> - - - - - - Param4: - - - - this.param4Element = param4Elem} - onChange={this.handlePointChange.bind(this, 'param4')} /> - - - - - - Lat/X: - - - - this.latitudeElement = latElem} - onChange={this.handlePointChange.bind(this, 'lat')} /> - - - - - - Lon/Y: - - - - this.longitudeElement = lngElem} - onChange={this.handlePointChange.bind(this, 'lng')} /> - - - - - - Alt/Z: - - - - this.altitudeElement = altElem} - onChange={this.handlePointChange.bind(this, 'altitude')} /> - m - - - -
-
- )} -
- ); - } -} - -export default InfoWindow; diff --git a/src/components/mission/MissionPlanner.js b/src/components/mission/MissionPlanner.js index 30a9183..624f4df 100644 --- a/src/components/mission/MissionPlanner.js +++ b/src/components/mission/MissionPlanner.js @@ -3,63 +3,112 @@ */ /** - * The root mission planner component + * Mission planner component * * @author TCSCODER * @version 1.0.0 */ +import config from '../../config'; import React, { Component } from 'react'; -import ReactDOM from 'react-dom'; -import circleGreen from '../../i/circle_green.svg'; -import InfoWindow from './InfoWindow.js'; +import MissionPointList from './MissionPointList.js'; +import MissionMap from './MissionMap.js'; import { Form, FormGroup, Grid, Row, Col, ButtonToolbar, Button } from 'react-bootstrap'; +import TextInput from '../ui/TextInput'; import { ToastContainer, ToastMessage } from 'react-toastr'; import MissionApi from '../../api/Mission.js'; -import config from '../../config'; import { hashHistory } from 'react-router'; +import _ from 'lodash'; const ToastMessageFactory = React.createFactory(ToastMessage.animation); -import TextInput from '../ui/TextInput'; class MissionPlanner extends Component { constructor(props) { super(props); + + this.missionApi = new MissionApi(config.api.basePath, props.auth); + + // handlers this.handleMapClick = this.handleMapClick.bind(this); this.handleMarkerClick = this.handleMarkerClick.bind(this); - this.handleMissionItemUpdate = this.handleMissionItemUpdate.bind(this); - this.doHandleMarkerClick = this.doHandleMarkerClick.bind(this); + this.handleMissionItemCancel = this.handleMissionItemCancel.bind(this); + this.handleMissionItemSave = this.handleMissionItemSave.bind(this); + this.handleMissionItemDelete = this.handleMissionItemDelete.bind(this); + this.handleMissionItemHeaderClick = this.handleMissionItemHeaderClick.bind(this); this.clearAll = this.clearAll.bind(this); this.save = this.save.bind(this); - this.missionApi = new MissionApi(config.api.basePath, props.auth); + this.addPoint = this.addPoint.bind(this); + + // initial state this.state = { - // the path markers - markers: [], + missionId: this.props.id, + missionName: '', missionItems: [], - idSequence: 0 + openedMissionItems: [], // opened 'tabs' in the mission list panel on the right sidebar + centerMapOnUpdate: false // whether to center map after points are updated + } + } + + /** + * Load data when component in mounted + */ + componentDidMount() { + this.loadMission(); + } + + /** + * Loads existed mission from the server + */ + loadMission() { + if ( this.state.missionId ) { + this.missionApi.getSingle(this.state.missionId).then((mission) => { + const missionItems = mission.missionItems; + + // add planned home position to missionItems list + missionItems.unshift(mission.plannedHomePosition); + + // init markers for all missionItems and extend missionItems + missionItems.forEach((missionItem) => { + // add unique id + missionItem.keyId = _.uniqueId(); + }); + + this.setState({ + missionName: mission.missionName, + missionItems: missionItems, + centerMapOnUpdate: true + }); + }); } } + /** + * Clear all mission poitns + */ clearAll() { - this.poly.setMap(null); - this.poly = null; - this.state.markers.forEach((single) => { - single.setMap(null); - }); - this.setState({ markers: [], missionItems: [], idSequence: 0 }); + this.setState({ missionItems: [], openedMissionItems: [], centerMapOnUpdate: false }); } + /** + * Callback when mission name is valid + * @param {string} value mission name from text field + */ onValidMissionName(value) { - this.setState({ missionName: value }); + this.setState({ missionName: value, centerMapOnUpdate: false }); } + /** + * Save current mission + * @param {Event} event + */ save(event) { event.preventDefault(); const _self = this; - if (_self.state.missionItems.length === 0) { + + if (_self.state.missionItems.length < 2) { _self.toastContainer.warning('', - 'Add some waypoints before saving a mission', { + 'Add at least two waypoints before saving a mission', { timeOut: 3000, preventDuplicates:true }); @@ -69,208 +118,202 @@ class MissionPlanner extends Component { timeOut: 3000, preventDuplicates:true }); + } else if (_self.state.openedMissionItems.length) { + _self.toastContainer.warning('', + 'Cancel or Save currently edited waypoints first', { + timeOut: 3000, + preventDuplicates:true + }); } else { + // prepeare missionItems for saving + const missionItems = _self.state.missionItems.map((missionItem) => { + var clone = _.clone(missionItem); + delete clone.keyId; + return clone; + }); + // save the mission - _self.missionApi.save(_self.state.missionName, _self.state.missionItems, _self.state.plannedHomePosition).then(() => { - _self.toastContainer.success('', - 'Mission saved', { - timeOut: 1000, - preventDuplicates:false + if ( _self.state.missionId ) { + _self.missionApi.update(_self.state.missionId, _self.state.missionName, missionItems.slice(1), missionItems[0]).then(() => { + _self.onSaveSuccess('Mission updated'); }); - setTimeout(() => { - hashHistory.push('/list'); - }, 2000); - }); + } else { + _self.missionApi.save(_self.state.missionName, missionItems.slice(1), missionItems[0]).then(() => { + _self.onSaveSuccess('Mission saved'); + }); + } } } /** - * Handle the mission item update fired from info window component - * @param {Number} id the id of mission item in mission items array - * @param {Object} missionItem the updated mission item + * Callback when mission is succesfully save + * @param {string} messageText Text of the messge to show */ - handleMissionItemUpdate(id, missionItem) { - if (id === 0) { - this.setState({ plannedHomePosition: missionItem }); - } else { - const missionItems = this.state.missionItems; - missionItems.splice(id - 1, 1, missionItem); - this.setState({ missionItems: missionItems }); + onSaveSuccess(messageText) { + this.toastContainer.success('', + messageText, { + timeOut: 1000, + preventDuplicates:false + } + ); + + setTimeout(() => { + hashHistory.push('/list'); + }, 2000); + } + + /** + * Returns mission item index in mission item list identified by keyId + * @param {Object} missionItem mission item we are looking for + * @param {Array} missionItems mission item list + * @return {Number} mission item index + */ + getMissionItemIndex(missionItem, missionItems) { + let missionItemIndex = -1; + + for ( let tmlItem of missionItems ) { + missionItemIndex++ + if ( tmlItem.keyId === missionItem.keyId ) { + break; + } } + + return missionItemIndex; } /** - * Actual marker click handler - * @param {MouseEvent} event the MouseEvent object fired by google map api - * @param {Marker} marker the maker which is clicked + * Handle the mission item update is canceled fired from WissionPointListItem component + * @param {Object} missionItem edited but canceled value of missionItem */ - doHandleMarkerClick(event, marker) { - const _self = this; - const google = window.google; - const div = document.createElement('div'); - const id = marker.get('id'); - ReactDOM.render(_self.renderInfoWindow(marker.getLabel().text, { lat: marker.getPosition().lat(), - lng: marker.getPosition().lng() }, id), div); - const infoWindow = new google.maps.InfoWindow({ - content: div, - maxWidth: 400 - }); - infoWindow.open(_self.map, marker); + handleMissionItemCancel(missionItem) { + this.closeMissionItemPanel(missionItem); } /** - * Attach the click event on marker and handle the click event on the marker - * @param {object} event the propogated event + * Handle the mission item update fired from info window component + * @param {Object} missionItem the updated mission item */ - handleMarkerClick(marker) { - const _self = this; - marker.addListener('click', (event) => { - _self.doHandleMarkerClick(event, marker); + handleMissionItemSave(missionItem) { + this.setState((prevState) => { + const missionItems = _.clone(prevState.missionItems); + const missionItemIndex = this.getMissionItemIndex(missionItem, missionItems); + + missionItems.splice(missionItemIndex, 1, missionItem); + + return { missionItems: missionItems, centerMapOnUpdate: false } }); + this.closeMissionItemPanel(missionItem); } - getCommandValue(type) { - return (type === 'H' || type !== 'T') ? 16 : 22; + /** + * Handle the mission item delete fired from info window component on delete button press + * @param {Object} missionItem the id of mission item in mission items array + */ + handleMissionItemDelete(missionItem) { + this.setState((prevState) => { + let missionItems = _.clone(prevState.missionItems); + const missionItemIndex = this.getMissionItemIndex(missionItem, missionItems); + + missionItems.splice(missionItemIndex, 1); + missionItems = missionItems.map((missionItem, index) => { + // tekeoff point + if ( index === 1 ) { + missionItem.command = config.takeoffMissionItemCommand; + } + missionItem.id = index; + return missionItem; + }); + + return { missionItems: missionItems, centerMapOnUpdate: false } + }); + this.closeMissionItemPanel(missionItem); } /** - * Render the info window for the specified type i.e, H, T, W + * Handle mission item header click in right panel + * @param {Object} missionItem the mission item object */ - renderInfoWindow(type, position, id) { - return ( - - ); + handleMissionItemHeaderClick(missionItem) { + this.openMissionItemPanel(missionItem); } /** - * Handle the click event on the map - * @param {object} event the propogated event + * Actual marker click handler + * @param {MouseEvent} event the MouseEvent object fired by google map api + * @param {Object} missionItem the mission item object */ - handleMapClick(event) { - const google = window.google; - const _self = this; - if (!_self.poly) { - _self.poly = new google.maps.Polyline({ - strokeColor: '#ff794d', - strokeOpacity: 1.0, - strokeWeight: 2 - }); - _self.poly.setMap(_self.map); - } - const path = _self.poly.getPath(); - const markers = _self.state.markers; - let idSequence = this.state.idSequence; - const markerOpts = { - position: event.latLng, - cursor: 'pointer', - map: _self.map, - icon: circleGreen - }; - if (idSequence === 0) { - // add the home - markerOpts.label = { - color: '#759e57', - text: 'H', - fontWeight: '800' - }; - } else if (idSequence === 1) { - // add the takeoff marker - markerOpts.label = { - color: '#759e57', - text: 'T', - fontWeight: '800' - }; - } else { - // add general waypoint marker - markerOpts.label = { - color: '#759e57', - text: `${idSequence}`, - fontWeight: '800' - }; - } - const marker = new google.maps.Marker(markerOpts); - marker.set('id', idSequence); - const missionItems = this.state.missionItems; - if (idSequence !== 0) { - // if id sequence is 0 than it is home point, so home point is not added to mission items. - missionItems.push({ - autoContinue: true, - command: idSequence === 1 ? 22 : 16, - coordinate: [event.latLng.lat(), event.latLng.lng(), 25.000000], - frame: 3, - id: idSequence, - param1: 0.000000, - param2: 0.000000, - param3: 0.000000, - param4: 0.000000, - type: 'missionItem' - }); - } else { - this.setState({ plannedHomePosition: { - autoContinue: true, - command: 16, - coordinate: [event.latLng.lat(), event.latLng.lng(), 25.000000], - frame: 0, - id: idSequence, - param1: 0.000000, - param2: 0.000000, - param3: 0.000000, - param4: 0.000000, - type: 'missionItem' - }}); - } - idSequence += 1; - this.handleMarkerClick(marker); - markers.push(marker); - _self.setState({ markers: markers, idSequence: idSequence, missionItems: missionItems }); - if (idSequence !== 1) { - path.push(event.latLng); - } + handleMarkerClick(event, missionItem) { + this.openMissionItemPanel(missionItem); } - shouldComponentUpdate() { - // never update the map, rendering is delegated to google api - return false; + /** + * Open mission item panel + * @param {Object} missionItem the mission item object + */ + openMissionItemPanel(missionItem) { + this.setState((prevState) => { + if ( prevState.openedMissionItems.indexOf(missionItem.keyId) < 0 ) { + const openedMissionItems = _.clone(prevState.openedMissionItems); + openedMissionItems.push(missionItem.keyId); + return { openedMissionItems: openedMissionItems, centerMapOnUpdate: false }; + } + }); } - componentWillReceiveProps(nextProps) { - if (this.props.loaded === false && nextProps.loaded === true) { - this.loadMap(); - } + /** + * Close mission item panel + * @param {Object} missionItem the mission item object + */ + closeMissionItemPanel(missionItem) { + this.setState((prevState) => { + let missionItemIndex = prevState.openedMissionItems.indexOf(missionItem.keyId) + if ( missionItemIndex > -1 ) { + const openedMissionItems = _.clone(prevState.openedMissionItems); + openedMissionItems.splice(missionItemIndex, 1) + return { openedMissionItems: openedMissionItems, centerMapOnUpdate: false }; + } + }); } - componentDidMount() { - if (this.props.loaded === true) { - this.loadMap(); - } + /** + * Create missionItem + * @param {Integer} missionItemIndex mission item index in the list + * @param {Number} lat Latitude + * @param {Number} lng Longitude + * @param {Number} alt Altitude + * @return {Object} missionItem + */ + getMissionItem(missionItemIndex, lat, lng, alt) { + return _.extend({}, config.defaultMissionItem, { + keyId: _.uniqueId(), + command: missionItemIndex === 1 ? config.takeoffMissionItemCommand : config.defaultMissionItem.command, + coordinate: [lat, lng, alt], + id: missionItemIndex + }); } - componentWillUnmount() { - const _self = this; - if (_self.poly) { - _self.poly.setMap(null); - _self.poly = null; - } - // remove all markers - _self.state.markers.forEach((single) => { - single.setMap(null); + /** + * Add new point to mission item list + * @param {Object} latLng coordinates of the point + * @param {[type]} alt altitude of the point + */ + addPoint(latLng, alt) { + this.setState((prevState) => { + const missionItems = _.clone(prevState.missionItems); + const missionItemIndex = missionItems.length; + const missionItem = this.getMissionItem(missionItemIndex, latLng.lat(), latLng.lng(), alt); + missionItems.push(missionItem); + + return { missionItems: missionItems, centerMapOnUpdate: false } }); - _self.map = null; } - loadMap() { - const _self = this; - const google = window.google; - _self.map = new google.maps.Map(_self.mapElement, { - center: { - lat: _self.props.lat, - lng: _self.props.lng - }, - zoom: _self.props.zoom, - }); - // add click listener on map - _self.map.addListener('click', this.handleMapClick); + /** + * Handle the click event on the map + * @param {object} event the propogated event + */ + handleMapClick(event) { + this.addPoint(event.latLng, config.defaultMissionItem.coordinate[2]); } render() { @@ -283,26 +326,30 @@ class MissionPlanner extends Component { - -
- - - - - - - - + { !this.state.missionId && + +
+ + + + + + + + + } + {this.state.missionId && } List All missions
- + -
this.mapElement = element } /> + + diff --git a/src/components/mission/MissionPlannerList.js b/src/components/mission/MissionPlannerList.js deleted file mode 100644 index 69998a8..0000000 --- a/src/components/mission/MissionPlannerList.js +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright (c) 2016 Topcoder Inc, All rights reserved. - */ - -/** - * The missions list component - * - * @author TCSCODER - * @version 1.0.0 - */ -import React, { Component } from 'react'; -import MissionApi from '../../api/Mission.js'; -import config from '../../config'; -import { Grid, Row, Col, Table } from 'react-bootstrap'; -import MissionListItem from './MissionListItem.js'; - -class MissionPlannerList extends Component { - - constructor(props) { - super(props); - this.missionApi = new MissionApi(config.api.basePath, props.auth); - this.state = { - missions: [] - } - } - - componentDidMount() { - this.missionApi.getAll().then((missions) => { - this.setState({ missions: missions }); - }); - } - - refresh() { - this.missionApi.getAll().then((missions) => { - this.setState({ missions: missions }); - }); - } - - render() { - const rows = []; - this.state.missions.forEach((single) => { - rows.push(); - }); - const table = ( - - - - - - - - - - - {rows} - -
Mission Name
- ); - const notFound = ( -

No missions found

- ); - let content = notFound; - if (rows && rows.length > 0) { - content = table; - } - return ( - - - - {content} - - - - ); - } -} - -export default MissionPlannerList; diff --git a/src/components/mission/MissionPlannerWrapper.js b/src/components/mission/MissionPlannerWrapper.js deleted file mode 100644 index d4d26d0..0000000 --- a/src/components/mission/MissionPlannerWrapper.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) 2016 Topcoder Inc, All rights reserved. - */ - -/** - * The mission planner wrapper component - * - * @author TCSCODER - * @version 1.0.0 - */ -import React, { Component } from 'react'; -import MissionPlanner from './MissionPlanner.js'; - -class MissionPlannerWrapper extends Component { - constructor(props) { - super(props); - this.state = { - // represents the map options - mapOpts: { - lat: 42.010, - lng: -96.824, - zoom: 9 - } - } - } - - render() { - return ( - - ); - } -} - -export default MissionPlannerWrapper; diff --git a/src/config/index.js b/src/config/index.js index 0da7803..f94842a 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -21,6 +21,26 @@ const config = { }, AUTH0_CLIEND_ID: process.env.REACT_APP_AUTH0_CLIEND_ID, AUTH0_DOMAIN: process.env.REACT_APP_AUTH0_DOMAIN, + + defaultGoogleMapConfig: { + center: { + lat: 42.010, + lng: -96.824, + }, + zoom: 9, + }, + takeoffMissionItemCommand: 22, + defaultMissionItem: { + autoContinue: true, + command: 16, + coordinate: [0, 0, 0], + frame: 3, + param1: 0.000000, + param2: 0.000000, + param3: 0.000000, + param4: 0.000000, + type: 'missionItem' + } }; export default config; diff --git a/src/index.js b/src/index.js index 6d545fe..b16a5f9 100644 --- a/src/index.js +++ b/src/index.js @@ -16,9 +16,9 @@ import './styles/index.css'; import { Router, Route, hashHistory, IndexRedirect } from 'react-router'; import Login from './components/auth/Login.js'; import GoogleApiComponent from './components/maps/GoogleApiComponent.js'; -import MissionPlannerWrapper from './components/mission/MissionPlannerWrapper.js'; -import EditMissionPlannerWrapper from './components/mission/EditMissionPlannerWrapper.js'; -import MissionPlannerList from './components/mission/MissionPlannerList.js'; +import MissionPlannerCreate from './components/mission/MissionPlannerCreate.js'; +import MissionPlannerEdit from './components/mission/MissionPlannerEdit.js'; +import MissionList from './components/mission/MissionList.js'; import AuthService from './components/auth/utils/AuthService.js'; const auth = new AuthService(config.AUTH0_CLIEND_ID, config.AUTH0_DOMAIN); @@ -76,9 +76,9 @@ const routes = ( - - - + + + diff --git a/src/styles/App.css b/src/styles/App.css index fa7f11a..59a688c 100644 --- a/src/styles/App.css +++ b/src/styles/App.css @@ -17,14 +17,12 @@ } #map-container { height: 100%; + overflow: hidden; } .col-centered { margin: 0 auto; float: none; } -.info-window-container { - max-width: 400px; -} .gm-style-iw { overflow: visible !important; } @@ -38,4 +36,29 @@ } .full-width { width: 100%; +} + +.mission-point-list { + background-color: #fff; + float: right; + height: 100%; + overflow: auto; + width: 400px; +} + +.mission-point-list .panel { + margin-bottom: 2px; +} + +.mission-point-list .panel-title > a { + display: block; + margin: -15px -10px; + padding: 15px 10px; +} + +.mission-point-list .panel-title > a:after { + clear: both; + content: ''; + display: table; + width: 100%; } \ No newline at end of file From 66275256db707081aa94cdb06bd3b9a5be1ae790 Mon Sep 17 00:00:00 2001 From: Kyle Bowerman Date: Tue, 15 Nov 2016 12:27:17 -0600 Subject: [PATCH 2/2] new files added --- src/__tests__/App.test.js | 21 ++ src/components/auth/__tests__/Login.test.js | 19 ++ src/components/auth/__tests__/Signup.test.js | 15 + .../maps/__tests__/GoogleApiComponent.test.js | 16 + .../maps/__tests__/GoogleMap.test.js | 16 + src/components/mission/MissionList.js | 78 +++++ src/components/mission/MissionMap.js | 210 +++++++++++++ .../mission/MissionPlannerCreate.js | 21 ++ src/components/mission/MissionPlannerEdit.js | 21 ++ src/components/mission/MissionPointList.js | 48 +++ .../mission/MissionPointListItem.js | 279 ++++++++++++++++++ .../mission/__tests__/MissionList.test.js | 17 ++ .../mission/__tests__/MissionListItem.test.js | 22 ++ .../mission/__tests__/MissionMap.test.js | 44 +++ .../mission/__tests__/MissionPlanner.test.js | 17 ++ .../__tests__/MissionPlannerCreate.test.js | 17 ++ .../__tests__/MissionPlannerEdit.test.js | 18 ++ .../__tests__/MissionPointList.test.js | 44 +++ .../__tests__/MissionPointListItem.test.js | 34 +++ .../ui/__tests__/EmailInput.test.js | 16 + src/components/ui/__tests__/Password.test.js | 16 + src/components/ui/__tests__/TextInput.test.js | 18 ++ 22 files changed, 1007 insertions(+) create mode 100644 src/__tests__/App.test.js create mode 100644 src/components/auth/__tests__/Login.test.js create mode 100644 src/components/auth/__tests__/Signup.test.js create mode 100644 src/components/maps/__tests__/GoogleApiComponent.test.js create mode 100644 src/components/maps/__tests__/GoogleMap.test.js create mode 100644 src/components/mission/MissionList.js create mode 100644 src/components/mission/MissionMap.js create mode 100644 src/components/mission/MissionPlannerCreate.js create mode 100644 src/components/mission/MissionPlannerEdit.js create mode 100644 src/components/mission/MissionPointList.js create mode 100644 src/components/mission/MissionPointListItem.js create mode 100644 src/components/mission/__tests__/MissionList.test.js create mode 100644 src/components/mission/__tests__/MissionListItem.test.js create mode 100644 src/components/mission/__tests__/MissionMap.test.js create mode 100644 src/components/mission/__tests__/MissionPlanner.test.js create mode 100644 src/components/mission/__tests__/MissionPlannerCreate.test.js create mode 100644 src/components/mission/__tests__/MissionPlannerEdit.test.js create mode 100644 src/components/mission/__tests__/MissionPointList.test.js create mode 100644 src/components/mission/__tests__/MissionPointListItem.test.js create mode 100644 src/components/ui/__tests__/EmailInput.test.js create mode 100644 src/components/ui/__tests__/Password.test.js create mode 100644 src/components/ui/__tests__/TextInput.test.js diff --git a/src/__tests__/App.test.js b/src/__tests__/App.test.js new file mode 100644 index 0000000..0ad3217 --- /dev/null +++ b/src/__tests__/App.test.js @@ -0,0 +1,21 @@ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-addons-test-utils' + +import App from '../App'; + +describe('App Component', function () { + + it('renders without crash', () => { + let component = ReactTestUtils.renderIntoDocument(); + }); + +}); \ No newline at end of file diff --git a/src/components/auth/__tests__/Login.test.js b/src/components/auth/__tests__/Login.test.js new file mode 100644 index 0000000..955180d --- /dev/null +++ b/src/components/auth/__tests__/Login.test.js @@ -0,0 +1,19 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-addons-test-utils' + +import Login from '../Login'; + +describe('Login Component', function () { + + it('renders without crash', () => { + let shallowRenderer = ReactTestUtils.createRenderer(); + let component = shallowRenderer.render(); + }); + +}); \ No newline at end of file diff --git a/src/components/auth/__tests__/Signup.test.js b/src/components/auth/__tests__/Signup.test.js new file mode 100644 index 0000000..fecdc51 --- /dev/null +++ b/src/components/auth/__tests__/Signup.test.js @@ -0,0 +1,15 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-addons-test-utils' + +import Signup from '../Signup'; + +describe('Signup Component', function () { + + it('renders without crash', () => { + let shallowRenderer = ReactTestUtils.createRenderer(); + let component = shallowRenderer.render(); + }); + +}); \ No newline at end of file diff --git a/src/components/maps/__tests__/GoogleApiComponent.test.js b/src/components/maps/__tests__/GoogleApiComponent.test.js new file mode 100644 index 0000000..262bf28 --- /dev/null +++ b/src/components/maps/__tests__/GoogleApiComponent.test.js @@ -0,0 +1,16 @@ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-addons-test-utils' + +import GoogleApiComponent from '../GoogleApiComponent'; + +describe('GoogleApiComponent Component', function () { + + it('renders without crash', () => { + let component = ReactTestUtils.renderIntoDocument(); + }); + +}); \ No newline at end of file diff --git a/src/components/maps/__tests__/GoogleMap.test.js b/src/components/maps/__tests__/GoogleMap.test.js new file mode 100644 index 0000000..7d1758c --- /dev/null +++ b/src/components/maps/__tests__/GoogleMap.test.js @@ -0,0 +1,16 @@ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-addons-test-utils' + +import GoogleMap from '../GoogleMap'; + +describe('GoogleMap Component', function () { + + it('renders without crash', () => { + let component = ReactTestUtils.renderIntoDocument(); + }); + +}); \ No newline at end of file diff --git a/src/components/mission/MissionList.js b/src/components/mission/MissionList.js new file mode 100644 index 0000000..a2f5e74 --- /dev/null +++ b/src/components/mission/MissionList.js @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2016 Topcoder Inc, All rights reserved. + */ + +/** + * The missions list component + * + * @author TCSCODER + * @version 1.0.0 + */ +import React, { Component } from 'react'; +import MissionApi from '../../api/Mission.js'; +import config from '../../config'; +import { Grid, Row, Col, Table } from 'react-bootstrap'; +import MissionListItem from './MissionListItem.js'; + +class MissionList extends Component { + + constructor(props) { + super(props); + this.missionApi = new MissionApi(config.api.basePath, props.auth); + this.state = { + missions: [] + } + } + + componentDidMount() { + this.missionApi.getAll().then((missions) => { + this.setState({ missions: missions }); + }); + } + + refresh() { + this.missionApi.getAll().then((missions) => { + this.setState({ missions: missions }); + }); + } + + render() { + const rows = []; + this.state.missions.forEach((single) => { + rows.push(); + }); + const table = ( + + + + + + + + + + + {rows} + +
Mission Name
+ ); + const notFound = ( +

No missions found

+ ); + let content = notFound; + if (rows && rows.length > 0) { + content = table; + } + return ( + + + + {content} + + + + ); + } +} + +export default MissionList; diff --git a/src/components/mission/MissionMap.js b/src/components/mission/MissionMap.js new file mode 100644 index 0000000..d8ed5c2 --- /dev/null +++ b/src/components/mission/MissionMap.js @@ -0,0 +1,210 @@ +/** + * Copyright (c) 2016 Topcoder Inc, All rights reserved. + */ + +/** + * Mission Map component + * + * @author TCSCODER + * @version 1.0.0 + */ +import config from '../../config'; +import React, { Component } from 'react'; + +import circleGreen from '../../i/circle_green.svg'; + +class MissionMap extends Component { + + constructor(props) { + super(props); + + this.markers = []; + } + + getMarkerOpts(markerIndex, lat, lng) { + const google = window.google; + + const markerOpts = { + position: new google.maps.LatLng(lat, lng), + cursor: 'pointer', + map: this.map, + icon: circleGreen, + label: { + color: '#759e57', + text: markerIndex === 0 ? 'H' : (markerIndex === 1 ? 'T' : markerIndex + ''), + fontWeight: '800' + } + }; + + return markerOpts; + } + + loadMap() { + const google = window.google; + + // init map + this.map = new google.maps.Map(this.mapElement, config.defaultGoogleMapConfig); + this.map.addListener('click', this.props.onMapClick); + + this.initPolyline(); + } + + initPolyline() { + const google = window.google; + + if ( !this.poly ) { + this.poly = new google.maps.Polyline({ + strokeColor: '#ff794d', + strokeOpacity: 1.0, + strokeWeight: 2 + }); + this.poly.setMap(this.map); + } + } + + createMarker(missionItem) { + const google = window.google; + this.initPolyline(); // to make sure we inited it could be deleted after 100% testing + const path = this.poly.getPath(); + + // init marker + const markerOpts = this.getMarkerOpts(missionItem.id, missionItem.coordinate[0], missionItem.coordinate[1]); + const marker = new google.maps.Marker(markerOpts); + + marker.set('id', missionItem.id); + marker.addListener('click', (event) => { + this.props.onMarkerClick(event, missionItem); + }); + marker.keyId = missionItem.keyId; + this.markers.push(marker); + missionItem.id && path.push(new google.maps.LatLng(missionItem.coordinate[0], missionItem.coordinate[1])); + } + + removeMarker(marker) { + const markerIndex = this.markers.indexOf(marker); + const removedMarker = this.markers.splice(markerIndex, 1); + + this.markers = this.markers.map((marker, index) => { + // takeoff point + if ( index === 1 ) { + marker.setLabel('T'); + } + // waypoints + if ( index > 1 ) { + marker.setLabel(index + ''); + } + return marker; + }); + removedMarker.pop().setMap(null); + this.poly.getPath().removeAt(markerIndex - 1); + } + + syncMarkers() { + // add new markers + if ( this.props.missionItems.length > this.markers.length ) { + for ( let i = this.markers.length; i < this.props.missionItems.length; i++ ) { + this.createMarker(this.props.missionItems[i]); + } + } + + // remove markers + if ( this.props.missionItems.length < this.markers.length ) { + // remove from the end of array + for ( let i = this.markers.length - 1; i >= 0; i-- ) { + if ( this.isMissionItemRemoved(this.markers[i]) ) { + this.removeMarker(this.markers[i]); + } + } + } + + // update existed marker positions + this.updateMarkerPositions(); + + // center map if need + if ( this.props.centerOnUpdate ) { + this.centerMap(); + } + } + + updateMarkerPositions() { + const google = window.google; + + for ( let i = 0; i < this.markers.length; i++ ) { + let marker = this.markers[i]; + let markerPosition = marker.getPosition(); + let missionItem = this.props.missionItems[i]; + + if ( markerPosition.lat() !== missionItem.coordinate[0] || markerPosition.lng() !== missionItem.coordinate[1] ) { + // update marker position + marker.setPosition(new google.maps.LatLng(missionItem.coordinate[0], missionItem.coordinate[1])); + // update line + i && this.poly.getPath().setAt(i - 1, new google.maps.LatLng(missionItem.coordinate[0], missionItem.coordinate[1])); + } + } + } + + isMissionItemRemoved(marker) { + let isRemoved = true; + + for ( let missionItem of this.props.missionItems ) { + if ( missionItem.keyId === marker.keyId ) { + isRemoved = false; + break; + } + } + + return isRemoved; + } + + // center and zoom map so all markers can be seen + centerMap() { + const google = window.google; + + if ( this.markers.length ) { + const markersBounds = new google.maps.LatLngBounds(); + + for ( let marker of this.markers ) { + markersBounds.extend(marker.getPosition()); + } + + this.map.setCenter(markersBounds.getCenter()); + this.map.fitBounds(markersBounds); + } + } + + componentDidMount() { + if (this.props.loaded === true) { + this.loadMap(); + } + } + + componentWillReceiveProps(nextProps) { + if (this.props.loaded === false && nextProps.loaded === true) { + this.loadMap(); + } + } + + componentDidUpdate() { + if (this.props.loaded === true) { + this.syncMarkers(); + } + } + + componentWillUnmount() { + if (this.poly) { + this.poly.setMap(null); + this.poly = null; + } + // remove all markers + this.markers.forEach((single) => { + single.setMap(null); + }); + this.map = null; + } + + render() { + return
this.mapElement = element } /> + } +} + +export default MissionMap; diff --git a/src/components/mission/MissionPlannerCreate.js b/src/components/mission/MissionPlannerCreate.js new file mode 100644 index 0000000..a67076c --- /dev/null +++ b/src/components/mission/MissionPlannerCreate.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2016 Topcoder Inc, All rights reserved. + */ + +/** + * Mission planner create component + * + * @author TCSCODER + * @version 1.0.0 + */ +import React, { Component } from 'react'; +import MissionPlanner from './MissionPlanner.js'; + +class MissionPlannerCreate extends Component { + + render() { + return + } +} + +export default MissionPlannerCreate; diff --git a/src/components/mission/MissionPlannerEdit.js b/src/components/mission/MissionPlannerEdit.js new file mode 100644 index 0000000..0a1bba3 --- /dev/null +++ b/src/components/mission/MissionPlannerEdit.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2016 Topcoder Inc, All rights reserved. + */ + +/** + * Mission planner edit component + * + * @author TCSCODER + * @version 1.0.0 + */ +import React, { Component } from 'react'; +import MissionPlanner from './MissionPlanner.js'; + +class MissionPlannerEdit extends Component { + + render() { + return + } +} + +export default MissionPlannerEdit; diff --git a/src/components/mission/MissionPointList.js b/src/components/mission/MissionPointList.js new file mode 100644 index 0000000..388aa27 --- /dev/null +++ b/src/components/mission/MissionPointList.js @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2016 Topcoder Inc, All rights reserved. + */ + +/** + * List of Mission points component + * + * @author TCSCODER + * @version 1.0.0 + */ +import React, { Component } from 'react'; +import { Grid, Row, Col, Alert } from 'react-bootstrap'; +import MissionPointListItem from './MissionPointListItem.js'; + +class MissionPointList extends Component { + + render() { + return ( + + + + { + !this.props.missionItems.length ? + + Please, add at least two waypoints on the map. + + : + this.props.missionItems.map((item, index) => { + return -1} + /> + }) + } + + + + ); + } +} + +export default MissionPointList; diff --git a/src/components/mission/MissionPointListItem.js b/src/components/mission/MissionPointListItem.js new file mode 100644 index 0000000..093f543 --- /dev/null +++ b/src/components/mission/MissionPointListItem.js @@ -0,0 +1,279 @@ +/** + * Copyright (c) 2016 Topcoder Inc, All rights reserved. + */ + +/** + * Mission item in the list of Mission points component + * + * @author TCSCODER + * @version 1.0.0 + */ +import config from '../../config'; +import React, { Component } from 'react'; +import { Row, Col, Form, FormControl, InputGroup, ControlLabel, FormGroup, Button, Panel, ButtonToolbar } from 'react-bootstrap'; + +import 'react-select/dist/react-select.css'; +import Select from 'react-select'; + +import _ from 'lodash'; + +import commands from './data/commands.js'; +import frames from './data/frames.js'; + +class MissionPointListItem extends Component { + + constructor(props) { + super(props); + + this.handleCancelClick = this.handleCancelClick.bind(this); + this.handleSaveClick = this.handleSaveClick.bind(this); + this.handleDeleteClick = this.handleDeleteClick.bind(this); + this.handlePanelHeaderClick = this.handlePanelHeaderClick.bind(this); + this.handleCommandChange = this.handleCommandChange.bind(this); + this.handleFrameChange = this.handleFrameChange.bind(this); + + // set initial state + this.state = this.converMissionItemToState(this.props.missionItem); + + } + + getSelectedCommand() { + if (this.props.missionItemIndex === 0) { + return 'Planned Home Position'; + } else if (this.props.missionItemIndex === 1) { + return 'Takeoff'; + } else { + return 'Waypoint'; + } + } + + getSelectedlabel() { + if (this.props.missionItemIndex === 0) { + return 'H'; + } else if (this.props.missionItemIndex === 1) { + return 'T'; + } else { + return this.props.missionItemIndex; + } + } + + convertStateToMissionItem() { + let missionItem = _.clone(this.state); + + missionItem.coordinate = [missionItem.lat, missionItem.lng, missionItem.alt]; + delete missionItem['lat']; + delete missionItem['lng']; + delete missionItem['alt']; + + return missionItem; + } + + converMissionItemToState(missionItem) { + var state = _.extend({ + lat: this.props.missionItem.coordinate[0], + lng: this.props.missionItem.coordinate[1], + alt: this.props.missionItem.coordinate[2] + }, missionItem); + delete state['coordinate']; + + return state; + } + + handleCancelClick() { + // just in case we could need it send state before it was canceled + this.props.onCancel(this.convertStateToMissionItem()); + // reset state from initial missionItem + this.setState(this.converMissionItemToState(this.props.missionItem)); + } + + handleSaveClick() { + this.props.onSave(this.convertStateToMissionItem()); + } + + handleDeleteClick() { + this.props.onDelete(this.convertStateToMissionItem()); + } + + handlePanelHeaderClick(event) { + this.props.onPanelHeaderClick(this.convertStateToMissionItem()); + } + + handleCommandChange(val) { + this.setState({ command: val.value }); + } + + handleFrameChange(val) { + this.setState({ frame: val.value }); + } + + handleFloatChange(name, event) { + const value = event.target.value; + + if ( value.match(/^-?\d*(\.\d*)?$/) ) { + const changeState = {}; + changeState[name] = value; + this.setState(changeState); + } + } + + handleFloatBlur(name, event) { + const value = event.target.value; + const changeState = {}; + + changeState[name] = this.getFloatValue(value); + this.setState(changeState); + } + + /** + * Get the float representation of the value + * @param {String} value the value to parse + */ + getFloatValue(value) { + if (value) { + try { + return parseFloat(value, 10); + } catch (e) { + // value is not a number, return 0 + return parseFloat(0, 10); + } + } + + return parseFloat(0, 10); + } + + componentWillReceiveProps(nextProps) { + if (this.props.missionItemIndex !== 1 && nextProps.missionItemIndex === 1) { + this.setState({command: config.takeoffMissionItemCommand}) + } + } + + render() { + const isHome = this.props.missionItemIndex === 0; + + return ( + + {this.getSelectedlabel()} + {this.getSelectedCommand()} + + } collapsible expanded={this.props.isOpened}> +
+ + + { isHome ?

Planned home position. Actual home position set by vehicle

+ :

Provides advanced access to all commands. Be very careful!

} + +
+ { !isHome && +
+ + + + + + + + + Param1: + + + + + + + + + + Param2: + + + + + + + + + + Param3: + + + + + + + + + + Param4: + + + + + + + +
+ } + + + Latitude: + + + + + + + + + + Longitude: + + + + + + + + + + Altitude: + + + + + m + + + + + + { !isHome && } + + + + + + + + +
+
+ ); + } +} + +export default MissionPointListItem; diff --git a/src/components/mission/__tests__/MissionList.test.js b/src/components/mission/__tests__/MissionList.test.js new file mode 100644 index 0000000..d73de28 --- /dev/null +++ b/src/components/mission/__tests__/MissionList.test.js @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-addons-test-utils' + +import MissionList from '../MissionList'; + +describe('MissionList Component', function () { + + it('renders without crash', () => { + let shallowRenderer = ReactTestUtils.createRenderer(); + let component = shallowRenderer.render(); + }); + +}); \ No newline at end of file diff --git a/src/components/mission/__tests__/MissionListItem.test.js b/src/components/mission/__tests__/MissionListItem.test.js new file mode 100644 index 0000000..94e7870 --- /dev/null +++ b/src/components/mission/__tests__/MissionListItem.test.js @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-addons-test-utils' + +import MissionListItem from '../MissionListItem'; + +describe('MissionListItem Component', function () { + + it('renders without crash', () => { + let shallowRenderer = ReactTestUtils.createRenderer(); + let component = shallowRenderer.render(); + }); + +}); \ No newline at end of file diff --git a/src/components/mission/__tests__/MissionMap.test.js b/src/components/mission/__tests__/MissionMap.test.js new file mode 100644 index 0000000..a698d4a --- /dev/null +++ b/src/components/mission/__tests__/MissionMap.test.js @@ -0,0 +1,44 @@ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-addons-test-utils' + +import MissionMap from '../MissionMap'; + +import _ from 'lodash'; + +const missionItem = { + keyId: '0', + id: 0, + autoContinue: true, + command: 16, + coordinate: [0, 0, 0], + frame: 3, + param1: 0.000000, + param2: 0.000000, + param3: 0.000000, + param4: 0.000000, + type: 'missionItem' +} + +const missionItems = [0, 1, 2, 3].map((missionItemIndex) => { + return _.extend({}, missionItem, { + keyId: missionItemIndex + '', + id: missionItemIndex, + command: missionItemIndex === 1 ? 22 : 16 + }) +}); + +describe('MissionMap Component', function () { + + it('renders without crash', () => { + let component = ReactTestUtils.renderIntoDocument(); + }); + +}); \ No newline at end of file diff --git a/src/components/mission/__tests__/MissionPlanner.test.js b/src/components/mission/__tests__/MissionPlanner.test.js new file mode 100644 index 0000000..e021bfc --- /dev/null +++ b/src/components/mission/__tests__/MissionPlanner.test.js @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-addons-test-utils' + +import MissionPlanner from '../MissionPlanner'; + +describe('MissionPlanner Component', function () { + + it('renders without crash', () => { + let shallowRenderer = ReactTestUtils.createRenderer(); + let component = shallowRenderer.render(); + }); + +}); \ No newline at end of file diff --git a/src/components/mission/__tests__/MissionPlannerCreate.test.js b/src/components/mission/__tests__/MissionPlannerCreate.test.js new file mode 100644 index 0000000..f6dc301 --- /dev/null +++ b/src/components/mission/__tests__/MissionPlannerCreate.test.js @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-addons-test-utils' + +import MissionPlannerCreate from '../MissionPlannerCreate'; + +describe('MissionPlannerCreate Component', function () { + + it('renders without crash', () => { + let shallowRenderer = ReactTestUtils.createRenderer(); + let component = shallowRenderer.render(); + }); + +}); \ No newline at end of file diff --git a/src/components/mission/__tests__/MissionPlannerEdit.test.js b/src/components/mission/__tests__/MissionPlannerEdit.test.js new file mode 100644 index 0000000..a322d36 --- /dev/null +++ b/src/components/mission/__tests__/MissionPlannerEdit.test.js @@ -0,0 +1,18 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-addons-test-utils' + +import MissionPlannerEdit from '../MissionPlannerEdit'; + +describe('MissionPlannerEdit Component', function () { + + it('renders without crash', () => { + let shallowRenderer = ReactTestUtils.createRenderer(); + let component = shallowRenderer.render(); + }); + +}); \ No newline at end of file diff --git a/src/components/mission/__tests__/MissionPointList.test.js b/src/components/mission/__tests__/MissionPointList.test.js new file mode 100644 index 0000000..81254d2 --- /dev/null +++ b/src/components/mission/__tests__/MissionPointList.test.js @@ -0,0 +1,44 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-addons-test-utils' + +import MissionPointList from '../MissionPointList'; + +import _ from 'lodash'; + +const missionItem = { + keyId: '0', + id: 0, + autoContinue: true, + command: 16, + coordinate: [0, 0, 0], + frame: 3, + param1: 0.000000, + param2: 0.000000, + param3: 0.000000, + param4: 0.000000, + type: 'missionItem' +} + +const missionItems = [0, 1, 2, 3].map((missionItemIndex) => { + return _.extend({}, missionItem, { + keyId: missionItemIndex + '', + id: missionItemIndex, + command: missionItemIndex === 1 ? 22 : 16 + }) +}); + +describe('MissionPointList Component', function () { + + it('renders without crash', () => { + let component = ReactTestUtils.renderIntoDocument(); + }); + +}); \ No newline at end of file diff --git a/src/components/mission/__tests__/MissionPointListItem.test.js b/src/components/mission/__tests__/MissionPointListItem.test.js new file mode 100644 index 0000000..6d87673 --- /dev/null +++ b/src/components/mission/__tests__/MissionPointListItem.test.js @@ -0,0 +1,34 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-addons-test-utils' + +import MissionPointListItem from '../MissionPointListItem'; + +describe('MissionPointListItem Component', function () { + + it('renders without crash', () => { + let component = ReactTestUtils.renderIntoDocument(); + }); + +}); \ No newline at end of file diff --git a/src/components/ui/__tests__/EmailInput.test.js b/src/components/ui/__tests__/EmailInput.test.js new file mode 100644 index 0000000..56ec204 --- /dev/null +++ b/src/components/ui/__tests__/EmailInput.test.js @@ -0,0 +1,16 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-addons-test-utils' + +import EmailInput from '../EmailInput'; + +describe('EmailInput Component', function () { + + it('renders without crash', () => { + let shallowRenderer = ReactTestUtils.createRenderer(); + let component = shallowRenderer.render(); + }); + +}); \ No newline at end of file diff --git a/src/components/ui/__tests__/Password.test.js b/src/components/ui/__tests__/Password.test.js new file mode 100644 index 0000000..0adf441 --- /dev/null +++ b/src/components/ui/__tests__/Password.test.js @@ -0,0 +1,16 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-addons-test-utils' + +import Password from '../Password'; + +describe('Password Component', function () { + + it('renders without crash', () => { + let shallowRenderer = ReactTestUtils.createRenderer(); + let component = shallowRenderer.render(); + }); + +}); \ No newline at end of file diff --git a/src/components/ui/__tests__/TextInput.test.js b/src/components/ui/__tests__/TextInput.test.js new file mode 100644 index 0000000..8133c13 --- /dev/null +++ b/src/components/ui/__tests__/TextInput.test.js @@ -0,0 +1,18 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-addons-test-utils' + +import TextInput from '../TextInput'; + +describe('TextInput Component', function () { + + it('renders without crash', () => { + let shallowRenderer = ReactTestUtils.createRenderer(); + let component = shallowRenderer.render(); + }); + +}); \ No newline at end of file