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
-
-
-
-
- ) : (
-
-
-
-
-
-
-
-
-
-
-
-
-
- )}
-
- );
- }
-}
-
-export default InfoWindow;
diff --git a/src/components/mission/MissionPlannerList.js b/src/components/mission/MissionList.js
similarity index 95%
rename from src/components/mission/MissionPlannerList.js
rename to src/components/mission/MissionList.js
index 69998a8..a2f5e74 100644
--- a/src/components/mission/MissionPlannerList.js
+++ b/src/components/mission/MissionList.js
@@ -14,7 +14,7 @@ import config from '../../config';
import { Grid, Row, Col, Table } from 'react-bootstrap';
import MissionListItem from './MissionListItem.js';
-class MissionPlannerList extends Component {
+class MissionList extends Component {
constructor(props) {
super(props);
@@ -75,4 +75,4 @@ class MissionPlannerList extends Component {
}
}
-export default MissionPlannerList;
+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/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/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/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/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}>
+
+
+ );
+ }
+}
+
+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
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