",
19 | "license": "Apache-2.0",
20 | "bugs": {
21 | "url": "https://github.com/robostack/zethus/issues"
22 | },
23 | "dependencies": {
24 | "amphion": "npm:@robostack/amphion@^0.1.24",
25 | "brace": "^0.11.1",
26 | "classnames": "^2.2.6",
27 | "d3": "^5.16.0",
28 | "dagre-d3": "^0.6.4",
29 | "is-valid-http-url": "^1.0.3",
30 | "jsoneditor-react": "^2.0.0",
31 | "lodash": "^4.17.13",
32 | "mousetrap": "^1.6.5",
33 | "prop-types": "^15.7.2",
34 | "react-graceful-unmount": "^1.0.7",
35 | "react-markdown": "^4.3.1",
36 | "react-rnd": "^10.1.10",
37 | "react-router-dom": "^5.2.0",
38 | "react-select": "^3.1.0",
39 | "react-tagsinput": "^3.19.0",
40 | "react-virtualized": "^9.21.2",
41 | "roslib": "npm:@robostack/roslib@^1.1.0",
42 | "shortid": "^2.2.14",
43 | "stats-js": "^1.0.0",
44 | "store": "^2.0.12",
45 | "styled-components": "^4.3.2",
46 | "three": "^0.117.0"
47 | },
48 | "scripts": {
49 | "start": "webpack-dev-server --config webpack.app.dev.js --host 192.168.64.6",
50 | "build": "webpack --config webpack.app.prod.js",
51 | "build-lib": "webpack --config webpack.lib.prod.js",
52 | "lint": "eslint src/**/*.{js,jsx} --fix",
53 | "predeploy": "npm run build",
54 | "deploy": "gh-pages -d build",
55 | "prettier": "prettier src/**/* --write"
56 | },
57 | "browserslist": [
58 | ">0.2%",
59 | "not dead",
60 | "not ie <= 11",
61 | "not op_mini all"
62 | ],
63 | "devDependencies": {
64 | "@babel/core": "^7.10.2",
65 | "@babel/preset-env": "^7.10.2",
66 | "@babel/preset-react": "^7.10.1",
67 | "@types/d3": "^5.7.2",
68 | "babel-loader": "^8.1.0",
69 | "clean-webpack-plugin": "^3.0.0",
70 | "copy-webpack-plugin": "^5.1.1",
71 | "css-loader": "^3.6.0",
72 | "eslint": "^6.8.0",
73 | "eslint-config-airbnb": "^18.1.0",
74 | "eslint-config-prettier": "^6.11.0",
75 | "eslint-loader": "^3.0.4",
76 | "eslint-plugin-import": "^2.21.2",
77 | "eslint-plugin-jsx-a11y": "^6.2.1",
78 | "eslint-plugin-prettier": "^3.1.4",
79 | "eslint-plugin-react": "^7.20.0",
80 | "eslint-restricted-globals": "^0.2.0",
81 | "file-loader": "^4.3.0",
82 | "gh-pages": "^2.2.0",
83 | "html-webpack-plugin": "^3.2.0",
84 | "husky": "^3.1.0",
85 | "lint-staged": "^9.5.0",
86 | "node-sass": "^4.14.1",
87 | "npm-run-all": "^4.1.5",
88 | "prettier": "^1.19.1",
89 | "prettier-sort-destructure": "0.0.4",
90 | "react": "^16.13.1",
91 | "react-dom": "^16.13.1",
92 | "sass-loader": "^8.0.2",
93 | "style-loader": "^1.2.1",
94 | "stylelint-config-standard": "^19.0.0",
95 | "stylelint-prettier": "^1.1.2",
96 | "url-loader": "^2.3.0",
97 | "webpack": "^4.43.0",
98 | "webpack-cli": "^3.3.11",
99 | "webpack-dev-server": "^3.11.0"
100 | },
101 | "husky": {
102 | "hooks": {
103 | "pre-commit": "lint-staged"
104 | }
105 | },
106 | "lint-staged": {
107 | "*.{js,jsx}": [
108 | "eslint --fix",
109 | "prettier --write",
110 | "git add"
111 | ],
112 | "*.{css,scss}": [
113 | "prettier --write",
114 | "git add"
115 | ]
116 | },
117 | "peerDependencies": {
118 | "react": "^16.10.1",
119 | "react-dom": "^16.10.1"
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/public/EditorControls.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author qiao / https://github.com/qiao
3 | * @author mrdoob / http://mrdoob.com
4 | * @author alteredq / http://alteredqualia.com/
5 | * @author WestLangley / http://github.com/WestLangley
6 | */
7 |
8 | THREE.EditorControls = function ( object, domElement ) {
9 |
10 | domElement = ( domElement !== undefined ) ? domElement : document;
11 |
12 | // API
13 |
14 | this.enabled = true;
15 | this.center = new THREE.Vector3();
16 | this.panSpeed = 0.001;
17 | this.zoomSpeed = 0.1;
18 | this.rotationSpeed = 0.005;
19 |
20 | // internals
21 |
22 | var scope = this;
23 | var vector = new THREE.Vector3();
24 | var delta = new THREE.Vector3();
25 | var box = new THREE.Box3();
26 |
27 | var STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2 };
28 | var state = STATE.NONE;
29 |
30 | var center = this.center;
31 | var normalMatrix = new THREE.Matrix3();
32 | // normalMatrix.set(0, 0, -1, -1, 0, 0, 0, 1, 0);
33 | var pointer = new THREE.Vector2();
34 | var pointerOld = new THREE.Vector2();
35 | var spherical = new THREE.Spherical();
36 |
37 | // events
38 |
39 | var changeEvent = { type: 'change' };
40 |
41 | this.focus = function ( target ) {
42 |
43 | var distance;
44 |
45 | box.setFromObject( target );
46 |
47 | if ( box.isEmpty() === false ) {
48 |
49 | center.copy( box.getCenter() );
50 | distance = box.getBoundingSphere().radius;
51 |
52 | } else {
53 |
54 | // Focusing on an Group, AmbientLight, etc
55 |
56 | center.setFromMatrixPosition( target.matrixWorld );
57 | distance = 0.1;
58 |
59 | }
60 |
61 | delta.set( 0, 0, 1 );
62 | delta.applyQuaternion( object.quaternion );
63 | delta.multiplyScalar( distance * 4 );
64 |
65 | object.position.copy( center ).add( delta );
66 |
67 | scope.dispatchEvent( changeEvent );
68 |
69 | };
70 |
71 | this.pan = function ( delta ) {
72 |
73 | var distance = object.position.distanceTo( center );
74 |
75 | delta.multiplyScalar( distance * scope.panSpeed );
76 | delta.applyMatrix3( normalMatrix.getNormalMatrix( object.matrix ) );
77 |
78 | object.position.add( delta );
79 | center.add( delta );
80 |
81 | scope.dispatchEvent( changeEvent );
82 |
83 | };
84 |
85 | this.zoom = function ( delta ) {
86 |
87 | var distance = object.position.distanceTo( center );
88 |
89 | delta.multiplyScalar( distance * scope.zoomSpeed );
90 |
91 | if ( delta.length() > distance ) return;
92 |
93 | delta.applyMatrix3( normalMatrix.getNormalMatrix( object.matrix ) );
94 |
95 | object.position.add( delta );
96 |
97 | scope.dispatchEvent( changeEvent );
98 |
99 | };
100 |
101 | this.rotate = function ( delta ) {
102 |
103 | vector.copy( object.position ).sub( center );
104 |
105 | // spherical.setFromVector3( vector );
106 | spherical.setFromCartesianCoords( -1 * vector.x, vector.z, vector.y );
107 |
108 | spherical.theta += delta.x;
109 | spherical.phi += delta.y;
110 |
111 | spherical.makeSafe();
112 |
113 | const tempRelPosition = vector.setFromSpherical( spherical );
114 | vector.set(-1 * tempRelPosition.x, tempRelPosition.z, tempRelPosition.y);
115 |
116 | object.position.copy( center ).add( vector );
117 |
118 | object.lookAt( center );
119 |
120 | scope.dispatchEvent( changeEvent );
121 |
122 | };
123 |
124 | // mouse
125 |
126 | function onMouseDown( event ) {
127 |
128 | if ( scope.enabled === false ) return;
129 |
130 | if ( event.button === 0 ) {
131 |
132 | state = STATE.ROTATE;
133 |
134 | } else if ( event.button === 1 ) {
135 |
136 | state = STATE.ZOOM;
137 |
138 | } else if ( event.button === 2 ) {
139 |
140 | state = STATE.PAN;
141 |
142 | }
143 |
144 | pointerOld.set( event.clientX, event.clientY );
145 |
146 | domElement.addEventListener( 'mousemove', onMouseMove, false );
147 | domElement.addEventListener( 'mouseup', onMouseUp, false );
148 | domElement.addEventListener( 'mouseout', onMouseUp, false );
149 | domElement.addEventListener( 'dblclick', onMouseUp, false );
150 |
151 | }
152 |
153 | function onMouseMove( event ) {
154 |
155 | if ( scope.enabled === false ) return;
156 |
157 | pointer.set( event.clientX, event.clientY );
158 |
159 | var movementX = pointer.x - pointerOld.x;
160 | var movementY = pointer.y - pointerOld.y;
161 |
162 | if ( state === STATE.ROTATE ) {
163 |
164 | scope.rotate( delta.set( - movementX * scope.rotationSpeed, - movementY * scope.rotationSpeed, 0 ) );
165 |
166 | } else if ( state === STATE.ZOOM ) {
167 |
168 | scope.zoom( delta.set( 0, 0, movementY ) );
169 |
170 | } else if ( state === STATE.PAN ) {
171 |
172 | scope.pan( delta.set( - movementX, movementY, 0 ) );
173 |
174 | }
175 |
176 | pointerOld.set( event.clientX, event.clientY );
177 |
178 | }
179 |
180 | function onMouseUp( event ) {
181 |
182 | domElement.removeEventListener( 'mousemove', onMouseMove, false );
183 | domElement.removeEventListener( 'mouseup', onMouseUp, false );
184 | domElement.removeEventListener( 'mouseout', onMouseUp, false );
185 | domElement.removeEventListener( 'dblclick', onMouseUp, false );
186 |
187 | state = STATE.NONE;
188 |
189 | }
190 |
191 | function onMouseWheel( event ) {
192 |
193 | event.preventDefault();
194 |
195 | // Normalize deltaY due to https://bugzilla.mozilla.org/show_bug.cgi?id=1392460
196 | scope.zoom( delta.set( 0, 0, event.deltaY > 0 ? 1 : - 1 ) );
197 |
198 | }
199 |
200 | function contextmenu( event ) {
201 |
202 | event.preventDefault();
203 |
204 | }
205 |
206 | this.dispose = function () {
207 |
208 | domElement.removeEventListener( 'contextmenu', contextmenu, false );
209 | domElement.removeEventListener( 'mousedown', onMouseDown, false );
210 | domElement.removeEventListener( 'wheel', onMouseWheel, false );
211 |
212 | domElement.removeEventListener( 'mousemove', onMouseMove, false );
213 | domElement.removeEventListener( 'mouseup', onMouseUp, false );
214 | domElement.removeEventListener( 'mouseout', onMouseUp, false );
215 | domElement.removeEventListener( 'dblclick', onMouseUp, false );
216 |
217 | domElement.removeEventListener( 'touchstart', touchStart, false );
218 | domElement.removeEventListener( 'touchmove', touchMove, false );
219 |
220 | };
221 |
222 | domElement.addEventListener( 'contextmenu', contextmenu, false );
223 | domElement.addEventListener( 'mousedown', onMouseDown, false );
224 | domElement.addEventListener( 'wheel', onMouseWheel, false );
225 |
226 | // touch
227 |
228 | var touches = [ new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3() ];
229 | var prevTouches = [ new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3() ];
230 |
231 | var prevDistance = null;
232 |
233 | function touchStart( event ) {
234 |
235 | if ( scope.enabled === false ) return;
236 |
237 | switch ( event.touches.length ) {
238 |
239 | case 1:
240 | touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
241 | touches[ 1 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
242 | break;
243 |
244 | case 2:
245 | touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
246 | touches[ 1 ].set( event.touches[ 1 ].pageX, event.touches[ 1 ].pageY, 0 );
247 | prevDistance = touches[ 0 ].distanceTo( touches[ 1 ] );
248 | break;
249 |
250 | }
251 |
252 | prevTouches[ 0 ].copy( touches[ 0 ] );
253 | prevTouches[ 1 ].copy( touches[ 1 ] );
254 |
255 | }
256 |
257 |
258 | function touchMove( event ) {
259 |
260 | if ( scope.enabled === false ) return;
261 |
262 | event.preventDefault();
263 | event.stopPropagation();
264 |
265 | function getClosest( touch, touches ) {
266 |
267 | var closest = touches[ 0 ];
268 |
269 | for ( var i in touches ) {
270 |
271 | if ( closest.distanceTo( touch ) > touches[ i ].distanceTo( touch ) ) closest = touches[ i ];
272 |
273 | }
274 |
275 | return closest;
276 |
277 | }
278 |
279 | switch ( event.touches.length ) {
280 |
281 | case 1:
282 | touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
283 | touches[ 1 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
284 | scope.rotate( touches[ 0 ].sub( getClosest( touches[ 0 ], prevTouches ) ).multiplyScalar( - scope.rotationSpeed ) );
285 | break;
286 |
287 | case 2:
288 | touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
289 | touches[ 1 ].set( event.touches[ 1 ].pageX, event.touches[ 1 ].pageY, 0 );
290 | var distance = touches[ 0 ].distanceTo( touches[ 1 ] );
291 | scope.zoom( delta.set( 0, 0, prevDistance - distance ) );
292 | prevDistance = distance;
293 |
294 |
295 | var offset0 = touches[ 0 ].clone().sub( getClosest( touches[ 0 ], prevTouches ) );
296 | var offset1 = touches[ 1 ].clone().sub( getClosest( touches[ 1 ], prevTouches ) );
297 | offset0.x = - offset0.x;
298 | offset1.x = - offset1.x;
299 |
300 | scope.pan( offset0.add( offset1 ).multiplyScalar( 0.5 ) );
301 |
302 | break;
303 |
304 | }
305 |
306 | prevTouches[ 0 ].copy( touches[ 0 ] );
307 | prevTouches[ 1 ].copy( touches[ 1 ] );
308 |
309 | }
310 |
311 | domElement.addEventListener( 'touchstart', touchStart, false );
312 | domElement.addEventListener( 'touchmove', touchMove, false );
313 |
314 | };
315 |
316 | THREE.EditorControls.prototype = Object.create( THREE.EventDispatcher.prototype );
317 | THREE.EditorControls.prototype.constructor = THREE.EditorControls;
318 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/favicon.ico
--------------------------------------------------------------------------------
/public/forklift/forklift.stl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/forklift/forklift.stl
--------------------------------------------------------------------------------
/public/image/viz/viz-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-image.png
--------------------------------------------------------------------------------
/public/image/viz/viz-interactive-marker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-interactive-marker.png
--------------------------------------------------------------------------------
/public/image/viz/viz-laserscan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-laserscan.png
--------------------------------------------------------------------------------
/public/image/viz/viz-map.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-map.png
--------------------------------------------------------------------------------
/public/image/viz/viz-marker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-marker.png
--------------------------------------------------------------------------------
/public/image/viz/viz-markerarray.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-markerarray.png
--------------------------------------------------------------------------------
/public/image/viz/viz-odometry.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-odometry.png
--------------------------------------------------------------------------------
/public/image/viz/viz-path.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-path.png
--------------------------------------------------------------------------------
/public/image/viz/viz-point.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-point.png
--------------------------------------------------------------------------------
/public/image/viz/viz-pointcloud.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-pointcloud.png
--------------------------------------------------------------------------------
/public/image/viz/viz-pose.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-pose.png
--------------------------------------------------------------------------------
/public/image/viz/viz-posearray.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-posearray.png
--------------------------------------------------------------------------------
/public/image/viz/viz-range.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-range.png
--------------------------------------------------------------------------------
/public/image/viz/viz-robotmodel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-robotmodel.png
--------------------------------------------------------------------------------
/public/image/viz/viz-tf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-tf.png
--------------------------------------------------------------------------------
/public/image/viz/viz-wrench.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/image/viz/viz-wrench.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
12 | Zethus
13 |
14 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RoboStack/zethus/86233034d121bb4f3b7c8bb9e1618c2d145b7958/public/screenshot.png
--------------------------------------------------------------------------------
/public/zethus.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/zethus_full.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/zethus_mark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/chevron.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Chevron = () => (
4 |
15 | );
16 |
17 | export default Chevron;
18 |
--------------------------------------------------------------------------------
/src/components/connectionDot.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { RosStatusIndicator } from './styled';
4 |
5 | class ConnectionDot extends React.PureComponent {
6 | render() {
7 | const { status } = this.props;
8 | return ;
9 | }
10 | }
11 |
12 | export default ConnectionDot;
13 |
--------------------------------------------------------------------------------
/src/components/errorBoundary.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import React, { Component } from 'react';
3 | import { ButtonOutline, Flex } from './styled';
4 | import { downloadFile } from '../utils';
5 |
6 | const Wrapper = styled(Flex)`
7 | flex-direction: column;
8 | align-items: center;
9 | width: 100%;
10 | height: 100%;
11 | padding-top: 200px;
12 | `;
13 |
14 | const Icon = styled.svg`
15 | width: 200px;
16 | height: 200px;
17 | `;
18 |
19 | const Heading = styled.h1`
20 | font-size: 20px;
21 | margin-bottom: 0;
22 | `;
23 |
24 | const ButtonsWrapper = styled(Flex)`
25 | ${ButtonOutline} {
26 | margin: 0 10px;
27 | }
28 | `;
29 |
30 | class ErrorBoundary extends Component {
31 | constructor(props) {
32 | super(props);
33 | this.state = { error: null };
34 |
35 | this.downloadConfig = this.downloadConfig.bind(this);
36 | }
37 |
38 | componentDidCatch(error, errorInfo) {
39 | this.setState({ error });
40 | }
41 |
42 | downloadConfig() {
43 | const { configuration } = this.props;
44 | downloadFile(JSON.stringify(configuration, null, 2), 'zethus_config.json');
45 | }
46 |
47 | render() {
48 | const { error } = this.state;
49 | const { children, resetReload } = this.props;
50 | if (error) {
51 | return (
52 |
53 |
60 |
61 |
62 | We're sorry — something's gone wrong
63 | {error.message || 'An unknown error occured'}
64 |
65 |
66 | Download config
67 |
68 |
69 | Reset and reload
70 |
71 |
72 |
73 | );
74 | }
75 | return children;
76 | }
77 | }
78 |
79 | export default ErrorBoundary;
80 |
--------------------------------------------------------------------------------
/src/components/logo.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Logo = () => (
4 |
16 | );
17 |
18 | export default Logo;
19 |
--------------------------------------------------------------------------------
/src/components/optionRow.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { HalfWidth, StyledOptionRow } from './styled';
3 |
4 | const OptionRow = ({ label, children, separator }) => {
5 | return (
6 |
7 |
8 | {label}
9 | {separator || ':'}
10 |
11 | {children}
12 |
13 | );
14 | };
15 |
16 | export default OptionRow;
17 |
--------------------------------------------------------------------------------
/src/components/styled/constants.js:
--------------------------------------------------------------------------------
1 | export const COLOR_PRIMARY = '#dc1d30';
2 |
3 | export const COLOR_BLUE = '#013c89';
4 | export const COLOR_RED = '#dc1d30';
5 |
6 | export const COLOR_GREY_LIGHT_1 = '#f6f6f6';
7 | export const COLOR_GREY_LIGHT_2 = '#dddddd';
8 |
9 | export const COLOR_GREY_TEXT_1 = '#222222';
10 | export const COLOR_GREY_TEXT_2 = '#444444';
11 | export const COLOR_GREY_TEXT_3 = '#666666';
12 | export const COLOR_GREY_TEXT_4 = '#888888';
13 |
14 | export const FONT_SIZE_DEFAULT = '0.8rem';
15 | export const FONT_SIZE_S = '0.7rem';
16 |
17 | export const HEADER_HEIGHT_PX = 60;
18 |
--------------------------------------------------------------------------------
/src/components/styled/modal.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { Button, Flex, Paragraph } from './index';
3 | import {
4 | COLOR_GREY_LIGHT_1,
5 | COLOR_GREY_LIGHT_2,
6 | COLOR_GREY_TEXT_2,
7 | COLOR_PRIMARY,
8 | } from './constants';
9 |
10 | export const ModalWrapper = styled.div`
11 | position: absolute;
12 | z-index: 1000;
13 | top: 0;
14 | left: 0;
15 | right: 0;
16 | bottom: 0;
17 | background-color: rgba(255, 255, 255, 0.9);
18 | `;
19 |
20 | export const ModalContents = styled.div`
21 | width: 1000px;
22 | margin: 100px auto;
23 | height: 600px;
24 | background-color: #ffffff;
25 | box-shadow: 0 0 40px rgba(0, 0, 0, 0.08);
26 | border-radius: 8px;
27 | padding: 20px;
28 | display: flex;
29 | flex-direction: column;
30 | `;
31 |
32 | export const ModalTitle = styled.h2`
33 | margin-bottom: 6px;
34 | margin-top: 0;
35 | `;
36 |
37 | export const ModalActions = styled.div`
38 | display: flex;
39 | flex-shrink: 0;
40 | padding: 10px 0 0;
41 |
42 | button {
43 | margin-left: 10px;
44 | }
45 | `;
46 |
47 | export const TypeEmpty = styled(Paragraph)`
48 | padding-left: 30px;
49 | font-style: italic;
50 | `;
51 |
52 | export const TypeUnsupported = styled(Flex)`
53 | padding: 10px;
54 | opacity: 0.4;
55 | cursor: not-allowed;
56 | `;
57 |
58 | export const TypeContainer = styled.div`
59 | display: flex;
60 | flex-grow: 1;
61 | overflow: hidden;
62 | `;
63 |
64 | export const TypeSelection = styled.div`
65 | flex: 1 0 50%;
66 | overflow-y: auto;
67 | `;
68 |
69 | export const TypeInfo = styled.div`
70 | border-left: 1px solid ${COLOR_GREY_LIGHT_2};
71 | padding: 0 0 0 20px;
72 | flex: 1 0 50%;
73 | overflow-y: auto;
74 |
75 | h4 {
76 | margin-top: 10px;
77 | margin-bottom: 8px;
78 | font-size: 22px;
79 | }
80 |
81 | img {
82 | max-width: 100%;
83 | }
84 |
85 | & > div {
86 | margin-bottom: 8px;
87 | }
88 | `;
89 |
90 | export const TypeHeading = styled.h4`
91 | display: flex;
92 | align-items: center;
93 | text-transform: uppercase;
94 | margin-bottom: 5px;
95 | `;
96 |
97 | export const AddVizForm = styled.form`
98 | display: flex;
99 | flex-grow: 1;
100 | flex-shrink: 1;
101 | height: 0;
102 | flex-direction: column;
103 | `;
104 |
105 | export const TopicRow = styled(Button)`
106 | text-align: left;
107 | width: 100%;
108 | font-size: 16px;
109 | border: 0;
110 | background: transparent;
111 | padding: 10px;
112 | display: flex;
113 | align-items: center;
114 |
115 | .reactSelect {
116 | color: ${COLOR_GREY_TEXT_2};
117 | }
118 |
119 | &:hover {
120 | color: ${COLOR_PRIMARY};
121 | background-color: ${COLOR_GREY_LIGHT_1};
122 | .reactSelect {
123 | color: ${COLOR_GREY_TEXT_2};
124 | }
125 | }
126 |
127 | ${({ selected }) =>
128 | selected &&
129 | `
130 | color: ${COLOR_PRIMARY};
131 | background-color: ${COLOR_GREY_LIGHT_1};
132 | `}
133 | `;
134 |
135 | export const TypeRow = styled(TopicRow)`
136 | padding-left: 30px;
137 | `;
138 |
--------------------------------------------------------------------------------
/src/components/styled/viz.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components';
2 | import { Button, Container, Flex } from './index';
3 | import {
4 | COLOR_GREY_LIGHT_1,
5 | COLOR_GREY_LIGHT_2,
6 | COLOR_GREY_TEXT_4,
7 | COLOR_PRIMARY,
8 | } from './constants';
9 |
10 | export const VizImageClose = styled(Button)`
11 | cursor: pointer;
12 | background-color: transparent;
13 | border: 0;
14 | color: inherit;
15 | text-decoration: underline;
16 | font-size: inherit;
17 | font-family: inherit;
18 | &:hover {
19 | text-decoration: none;
20 | }
21 | `;
22 |
23 | export const SidebarVizContainer = styled(Container)`
24 | overflow-y: auto;
25 | `;
26 |
27 | export const VizItemContent = styled.div`
28 | margin-top: 8px;
29 | margin-left: 24px;
30 | `;
31 |
32 | export const VizItemIcon = styled.div`
33 | width: 20px;
34 | height: 20px;
35 | display: inline-block;
36 | background-color: ${COLOR_GREY_LIGHT_1};
37 | margin-right: 10px;
38 | border-radius: 4px;
39 | svg {
40 | width: 100%;
41 | }
42 | `;
43 |
44 | export const VizImageContainer = styled.div`
45 | background: black;
46 | display: flex;
47 | align-items: center;
48 | flex-direction: column;
49 | cursor: move;
50 | border-radius: 4px;
51 | overflow: hidden;
52 | height: 100%;
53 | width: 100%;
54 | border: 1px solid ${COLOR_GREY_LIGHT_2};
55 |
56 | img {
57 | width: 100%;
58 | height: 100%;
59 | object-fit: contain;
60 | }
61 |
62 | canvas {
63 | width: 100%;
64 | height: 100%;
65 | }
66 | `;
67 |
68 | export const RosStatus = styled(Flex)`
69 | margin-bottom: 10px;
70 | align-items: center;
71 | `;
72 |
73 | export const VizImageHeader = styled.div`
74 | background: #fff;
75 | display: flex;
76 | width: 100%;
77 | font-size: 14px;
78 | padding: 2px 5px;
79 | min-height: 25px;
80 | border-bottom: 1px solid ${COLOR_GREY_LIGHT_2};
81 | word-break: break-all;
82 | overflow: hidden;
83 | `;
84 |
85 | export const VizImageName = styled.div`
86 | pointer-events: none;
87 | user-select: none;
88 | `;
89 |
90 | export const VizItem = styled.div`
91 | padding: 10px 0;
92 | border-bottom: 1px solid ${COLOR_GREY_LIGHT_1};
93 | `;
94 |
95 | export const VizItemActions = styled.div`
96 | display: flex;
97 | margin-top: 8px;
98 |
99 | button {
100 | background: ${COLOR_GREY_LIGHT_1};
101 | border: 0;
102 | border-radius: 4px;
103 | color: ${COLOR_GREY_TEXT_4};
104 | padding: 5px 10px;
105 | font-size: 0.6rem;
106 | &:hover {
107 | background-color: ${COLOR_PRIMARY};
108 | color: #ffffff;
109 | }
110 | & + button {
111 | margin-left: 10px;
112 | }
113 | }
114 | `;
115 |
116 | export const VizItemCollapse = styled(Button)`
117 | background-color: transparent;
118 | border: 0;
119 | padding: 0;
120 | margin: 0 3px 0 0;
121 | width: 20px;
122 | height: 20px;
123 | svg {
124 | width: 14px;
125 | }
126 | ${({ collapsed }) =>
127 | collapsed &&
128 | css`
129 | transform: rotate(-90deg);
130 | `}
131 | `;
132 |
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom';
2 | import React from 'react';
3 |
4 | import Zethus from './zethus';
5 | import { GlobalStyle } from './components/styled';
6 |
7 | ReactDOM.render(
8 | <>
9 |
10 |
11 | >,
12 | document.getElementById('root'),
13 | );
14 |
--------------------------------------------------------------------------------
/src/panels/addModal/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 |
4 | import { TabsButton, TabsHeader } from '../../components/styled';
5 | import {
6 | ModalWrapper,
7 | ModalContents,
8 | ModalTitle,
9 | } from '../../components/styled/modal';
10 | import TabVizType from './tabVizType';
11 | import TabTopicName from './tabTopicName';
12 | import SelectedVizOptionsForm from './options';
13 | import { stopPropagation } from '../../utils';
14 |
15 | const tabs = {
16 | vizType: 'Visualization type',
17 | topicName: 'Topic name',
18 | };
19 |
20 | class AddModal extends React.Component {
21 | constructor(props) {
22 | super(props);
23 | this.state = {
24 | tabType: tabs.vizType,
25 | selectedViz: '',
26 | };
27 |
28 | this.updateTab = this.updateTab.bind(this);
29 | this.selectViz = this.selectViz.bind(this);
30 | }
31 |
32 | updateTab(tabType) {
33 | this.setState({
34 | tabType,
35 | });
36 | }
37 |
38 | selectViz(vizType, topicName, messageType) {
39 | this.setState({
40 | selectedViz: vizType
41 | ? {
42 | vizType,
43 | topicName,
44 | messageType,
45 | }
46 | : '',
47 | });
48 | }
49 |
50 | render() {
51 | const {
52 | addVisualization,
53 | closeModal,
54 | ros,
55 | rosParams,
56 | rosTopics,
57 | } = this.props;
58 | const { selectedViz, tabType } = this.state;
59 | return (
60 |
61 |
62 | Add Visualization
63 | {selectedViz ? (
64 | this.selectViz(null)}
69 | />
70 | ) : (
71 | <>
72 |
73 | {_.map(tabs, tabText => (
74 | this.updateTab(tabText)}
79 | >
80 | {tabText}
81 |
82 | ))}
83 |
84 | {tabType === tabs.vizType ? (
85 |
91 | ) : (
92 |
97 | )}
98 | >
99 | )}
100 |
101 |
102 | );
103 | }
104 | }
105 |
106 | export default AddModal;
107 |
--------------------------------------------------------------------------------
/src/panels/addModal/options.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CONSTANTS } from 'amphion';
3 | import RobotModelOptions from './optionsRobotModel';
4 | import GenericOptions from './optionsGeneric';
5 |
6 | const { VIZ_TYPE_ROBOTMODEL } = CONSTANTS;
7 |
8 | class SelectedVizOptionsForm extends React.PureComponent {
9 | render() {
10 | const {
11 | addVisualization,
12 | back,
13 | ros,
14 | selectedViz: { vizType: type },
15 | selectedViz,
16 | } = this.props;
17 | if (type === VIZ_TYPE_ROBOTMODEL) {
18 | return (
19 |
25 | );
26 | }
27 | return (
28 |
33 | );
34 | }
35 | }
36 |
37 | export default SelectedVizOptionsForm;
38 |
--------------------------------------------------------------------------------
/src/panels/addModal/optionsGeneric.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | ButtonPrimary,
4 | FlexGrow,
5 | Input,
6 | InputLabel,
7 | InputWrapper,
8 | } from '../../components/styled';
9 | import { ModalActions } from '../../components/styled/modal';
10 |
11 | class GenericOptions extends React.Component {
12 | constructor(props) {
13 | super(props);
14 | this.state = {
15 | name: props.selectedViz.vizType,
16 | };
17 | this.onSubmit = this.onSubmit.bind(this);
18 | this.updateName = this.updateName.bind(this);
19 | }
20 |
21 | updateName(e) {
22 | this.setState({
23 | name: e.target.value,
24 | });
25 | }
26 |
27 | onSubmit(e) {
28 | e.preventDefault();
29 | const { addVisualization, selectedViz } = this.props;
30 | const { name } = this.state;
31 | addVisualization({
32 | ...selectedViz,
33 | name,
34 | visible: true,
35 | });
36 | }
37 |
38 | render() {
39 | const { name } = this.state;
40 | const { back } = this.props;
41 | return (
42 |
56 | );
57 | }
58 | }
59 |
60 | export default GenericOptions;
61 |
--------------------------------------------------------------------------------
/src/panels/addModal/optionsRobotModel.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Amphion from 'amphion';
3 | import _ from 'lodash';
4 | import {
5 | Button,
6 | ButtonPrimary,
7 | FlexGrow,
8 | Input,
9 | InputLabel,
10 | InputWrapper,
11 | Paragraph,
12 | } from '../../components/styled';
13 | import { ModalActions, TypeHeading } from '../../components/styled/modal';
14 |
15 | const statuses = {
16 | loading: 0,
17 | loaded: 1,
18 | error: 2,
19 | };
20 |
21 | class RobotModelOptions extends React.Component {
22 | constructor(props) {
23 | super(props);
24 | this.state = {
25 | name: props.selectedViz.vizType,
26 | packages: {},
27 | status: statuses.loading,
28 | };
29 | this.onSubmit = this.onSubmit.bind(this);
30 | this.getPackages = this.getPackages.bind(this);
31 | this.updateName = this.updateName.bind(this);
32 | this.updatePackage = this.updatePackage.bind(this);
33 | }
34 |
35 | componentDidMount() {
36 | this.getPackages();
37 | }
38 |
39 | updateName(e) {
40 | this.setState({
41 | name: e.target.value,
42 | });
43 | }
44 |
45 | getURLEndpoint() {
46 | const urlSearchParams = new URLSearchParams(window.location.search);
47 | const urlParams = Object.fromEntries(urlSearchParams.entries());
48 | return urlParams.pkgs || `http://localhost:9090/ros/pkgs`;
49 | }
50 |
51 | getPackages() {
52 | const {
53 | ros,
54 | selectedViz: { topicName },
55 | } = this.props;
56 | try {
57 | const robotInstance = new Amphion.RobotModel(ros, topicName);
58 | robotInstance.getPackages(packages => {
59 | this.setState({
60 | packages: _.mapValues(
61 | _.keyBy(packages),
62 | p => `${this.getURLEndpoint()}/${p}`,
63 | ),
64 | status: statuses.loaded,
65 | });
66 | });
67 | } catch (e) {
68 | this.setState({
69 | status: statuses.error,
70 | });
71 | }
72 | }
73 |
74 | updatePackage(e) {
75 | const {
76 | dataset: { id: packageId },
77 | value,
78 | } = e.target;
79 | const { packages } = this.state;
80 | this.setState({
81 | packages: {
82 | ...packages,
83 | [packageId]: value,
84 | },
85 | });
86 | }
87 |
88 | onSubmit(e) {
89 | e.preventDefault();
90 | const { addVisualization, selectedViz } = this.props;
91 | const { name, packages } = this.state;
92 | addVisualization({
93 | ...selectedViz,
94 | name,
95 | packages,
96 | });
97 | }
98 |
99 | render() {
100 | const { name, packages, status } = this.state;
101 | const { back } = this.props;
102 | if (status === statuses.loading) {
103 | return Loading list of packages...;
104 | }
105 | if (status === statuses.error) {
106 | return (
107 |
108 | Error occured while fetching packages. Please{' '}
109 |
110 |
111 | );
112 | }
113 | return (
114 |
139 | );
140 | }
141 | }
142 |
143 | export default RobotModelOptions;
144 |
--------------------------------------------------------------------------------
/src/panels/addModal/tabTopicName.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 | import { vizOptions } from '../../utils/vizOptions';
4 | import { ButtonPrimary, FlexGrow } from '../../components/styled';
5 | import {
6 | AddVizForm,
7 | ModalActions,
8 | TopicRow,
9 | TypeContainer,
10 | TypeSelection,
11 | TypeUnsupported,
12 | } from '../../components/styled/modal';
13 |
14 | class TopicName extends React.Component {
15 | constructor(props) {
16 | super(props);
17 | this.state = {
18 | selectedViz: '',
19 | };
20 | this.selectViz = this.selectViz.bind(this);
21 | this.onSubmit = this.onSubmit.bind(this);
22 | }
23 |
24 | selectViz(vizType, topicName, messageType) {
25 | this.setState({
26 | selectedViz: {
27 | vizType,
28 | topicName,
29 | messageType,
30 | },
31 | });
32 | }
33 |
34 | onSubmit(e) {
35 | e.preventDefault();
36 | const { selectViz } = this.props;
37 | const {
38 | selectedViz: { messageType, topicName, vizType },
39 | } = this.state;
40 | selectViz(vizType, topicName, messageType);
41 | }
42 |
43 | render() {
44 | const { closeModal, rosTopics } = this.props;
45 | const { selectedViz } = this.state;
46 | return (
47 |
48 |
49 |
50 | {_.map(_.sortBy(rosTopics, 'name'), ({ name, messageType }) => {
51 | const vizOption = _.find(vizOptions, v =>
52 | _.includes(v.messageTypes, messageType),
53 | );
54 | return vizOption ? (
55 |
60 | this.selectViz(vizOption.type, name, messageType)
61 | }
62 | >
63 | {name}
64 | ({messageType})
65 |
66 | ) : (
67 |
68 | {name}
69 |
70 | (Unsupported type: {messageType})
71 |
72 | );
73 | })}
74 |
75 |
76 |
77 |
78 |
79 | Proceed
80 |
81 |
82 | Close
83 |
84 |
85 |
86 | );
87 | }
88 | }
89 |
90 | export default TopicName;
91 |
--------------------------------------------------------------------------------
/src/panels/addModal/tabVizType.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 | import { vizOptions } from '../../utils/vizOptions';
4 | import VizTypeItem from './vizTypeItem';
5 | import VizTypeDetails from './vizTypeDetails';
6 | import { ButtonPrimary, FlexGrow } from '../../components/styled';
7 | import {
8 | AddVizForm,
9 | ModalActions,
10 | TypeContainer,
11 | TypeInfo,
12 | TypeSelection,
13 | } from '../../components/styled/modal';
14 |
15 | class VizType extends React.PureComponent {
16 | constructor(props) {
17 | super(props);
18 | this.state = {
19 | selectedViz: '',
20 | };
21 | this.selectViz = this.selectViz.bind(this);
22 | this.onSubmit = this.onSubmit.bind(this);
23 | }
24 |
25 | selectViz(vizType, topicName, messageType) {
26 | this.setState({
27 | selectedViz: {
28 | vizType,
29 | topicName,
30 | messageType,
31 | },
32 | });
33 | }
34 |
35 | onSubmit(e) {
36 | e.preventDefault();
37 | const { selectViz } = this.props;
38 | const {
39 | selectedViz: { messageType, topicName, vizType },
40 | } = this.state;
41 | selectViz(vizType, topicName, messageType);
42 | }
43 |
44 | render() {
45 | const { selectedViz } = this.state;
46 | const { closeModal, rosParams, rosTopics } = this.props;
47 | return (
48 |
49 |
50 |
51 | {_.map(vizOptions, op => {
52 | return (
53 |
60 | _.includes(op.messageTypes, t.messageType),
61 | )}
62 | />
63 | );
64 | })}
65 |
66 |
67 | {selectedViz ? (
68 |
69 | ) : (
70 |
71 | No visualization selected.
72 |
73 | Please choose a visualization on the left to see details
74 |
75 | )}
76 |
77 |
78 |
79 |
80 |
81 | Proceed
82 |
83 |
84 | Close
85 |
86 |
87 |
88 | );
89 | }
90 | }
91 |
92 | export default VizType;
93 |
--------------------------------------------------------------------------------
/src/panels/addModal/vizTypeDetails.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 | import ReactMarkdown from 'react-markdown';
4 |
5 | import { vizOptions } from '../../utils/vizOptions';
6 | import { Anchor } from '../../components/styled';
7 |
8 | class VizTypeDetails extends React.PureComponent {
9 | render() {
10 | const { vizType } = this.props;
11 | const { description, docsLink, type } = _.find(
12 | vizOptions,
13 | v => v.type === vizType,
14 | );
15 | return (
16 | <>
17 | {type}
18 |
19 | View docs
20 | >
21 | );
22 | }
23 | }
24 |
25 | export default VizTypeDetails;
26 |
--------------------------------------------------------------------------------
/src/panels/addModal/vizTypeItem.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 | import isValidUrl from 'is-valid-http-url';
4 | import { CONSTANTS } from 'amphion';
5 | import Select from 'react-select';
6 | import { TypeEmpty, TypeHeading, TypeRow } from '../../components/styled/modal';
7 | import { VizItemIcon } from '../../components/styled/viz';
8 | import {
9 | VIZ_TYPE_DEPTHCLOUD_STREAM,
10 | VIZ_TYPE_IMAGE_STREAM,
11 | } from '../../utils/vizOptions';
12 | import { Input } from '../../components/styled';
13 |
14 | const { VIZ_TYPE_ROBOTMODEL } = CONSTANTS;
15 |
16 | const customStyles = {
17 | container: provided => ({
18 | ...provided,
19 | fontSize: '16px',
20 | width: '250px',
21 | }),
22 | control: provided => ({
23 | ...provided,
24 | border: '1px solid #dddddd',
25 | }),
26 | };
27 |
28 | class VizTypeItem extends React.PureComponent {
29 | orderTopics() {}
30 |
31 | render() {
32 | const {
33 | rosParams,
34 | selectedViz,
35 | selectViz,
36 | topics,
37 | vizDetails,
38 | } = this.props;
39 | const topicName = _.get(selectedViz, 'topicName');
40 | const isRobotmodel = _.get(selectedViz, 'vizType') === VIZ_TYPE_ROBOTMODEL;
41 | const isAdditionalTypeSelected = {
42 | [VIZ_TYPE_ROBOTMODEL]:
43 | _.get(selectedViz, 'vizType') === VIZ_TYPE_ROBOTMODEL,
44 | [VIZ_TYPE_DEPTHCLOUD_STREAM]:
45 | _.get(selectedViz, 'vizType') === VIZ_TYPE_DEPTHCLOUD_STREAM,
46 | [VIZ_TYPE_IMAGE_STREAM]:
47 | _.get(selectedViz, 'vizType') === VIZ_TYPE_IMAGE_STREAM,
48 | };
49 | if (vizDetails.type === VIZ_TYPE_ROBOTMODEL) {
50 | const params = rosParams.filter(param =>
51 | param.includes('robot_description'),
52 | );
53 | params.sort((a, b) => a.length - b.length);
54 | return (
55 |
56 |
57 | {vizDetails.icon}
58 | {vizDetails.type}
59 |
60 |
64 |
81 |
82 | );
83 | }
84 | if (
85 | vizDetails.type === VIZ_TYPE_DEPTHCLOUD_STREAM ||
86 | vizDetails.type === VIZ_TYPE_IMAGE_STREAM
87 | ) {
88 | return (
89 |
90 |
91 | {vizDetails.icon}
92 | {vizDetails.type}
93 |
94 |
98 | {
102 | if (!isValidUrl(e.target.value)) {
103 | return;
104 | }
105 | selectViz(vizDetails.type, e.target.value, '');
106 | }}
107 | />
108 |
109 |
110 | );
111 | }
112 | return (
113 |
114 |
115 | {vizDetails.icon}
116 | {vizDetails.type}
117 |
118 | {_.map(topics, topic => (
119 |
124 | selectViz(vizDetails.type, topic.name, topic.messageType)
125 | }
126 | >
127 | {topic.name}
128 |
129 | ))}
130 | {_.size(topics) === 0 && (
131 | No topics available for the visualization type
132 | )}
133 |
134 | );
135 | }
136 | }
137 |
138 | export default VizTypeItem;
139 |
--------------------------------------------------------------------------------
/src/panels/configurationModal/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { JsonEditor as Editor } from 'jsoneditor-react';
4 | import 'jsoneditor-react/es/editor.min.css';
5 | import ace from 'brace';
6 | import 'brace/mode/json';
7 | import 'brace/theme/xcode';
8 |
9 | import {
10 | ButtonPrimary,
11 | ButtonOutline,
12 | FlexGrow,
13 | } from '../../components/styled';
14 |
15 | import {
16 | ModalActions,
17 | ModalWrapper,
18 | ModalContents,
19 | ModalTitle,
20 | } from '../../components/styled/modal';
21 | import { COLOR_GREY_LIGHT_1 } from '../../components/styled/constants';
22 | import { downloadFile, stopPropagation } from '../../utils';
23 |
24 | const StyledEditor = styled.div`
25 | flex-grow: 1;
26 | flex-shrink: 1;
27 | background-color: ${COLOR_GREY_LIGHT_1};
28 | .jsoneditor {
29 | border: 0;
30 | border-radius: 4px;
31 | }
32 | .ace_content {
33 | background-color: ${COLOR_GREY_LIGHT_1};
34 | }
35 | .ace_editor {
36 | font-family: 'Source Code Pro', sans-serif;
37 | }
38 | .jsoneditor-text {
39 | background-color: ${COLOR_GREY_LIGHT_1};
40 | }
41 | `;
42 |
43 | const EditorWrapper = styled.div`
44 | display: flex;
45 | flex-direction: column;
46 | flex-grow: 1;
47 | flex-shrink: 1;
48 | position: relative;
49 | `;
50 |
51 | const DragHoverText = styled.div`
52 | position: absolute;
53 | top: 0;
54 | bottom: 0;
55 | left: 0;
56 | right: 0;
57 | background-color: rgba(255, 255, 255, 0.7);
58 | z-index: 1010;
59 | display: flex;
60 | align-items: center;
61 | justify-content: center;
62 | > p {
63 | text-align: center;
64 | }
65 | `;
66 |
67 | const overrideEventDefaults = event => {
68 | event.preventDefault();
69 | event.stopPropagation();
70 | };
71 |
72 | class ConfigurationModal extends React.Component {
73 | constructor(props) {
74 | super(props);
75 | this.state = {
76 | dragging: false,
77 | };
78 | this.dragEventCounter = 0;
79 | this.jsonEditor = null;
80 |
81 | this.downloadConfig = this.downloadConfig.bind(this);
82 | this.handleSubmit = this.handleSubmit.bind(this);
83 | this.dragenterListener = this.dragenterListener.bind(this);
84 | this.dragleaveListener = this.dragleaveListener.bind(this);
85 | this.dropListener = this.dropListener.bind(this);
86 | this.setEditorRef = this.setEditorRef.bind(this);
87 | }
88 |
89 | dragenterListener(event) {
90 | overrideEventDefaults(event);
91 | this.dragEventCounter++;
92 | if (event.dataTransfer.items && event.dataTransfer.items[0]) {
93 | this.setState({ dragging: true });
94 | }
95 | }
96 |
97 | dragleaveListener(event) {
98 | overrideEventDefaults(event);
99 | this.dragEventCounter--;
100 |
101 | if (this.dragEventCounter === 0) {
102 | this.setState({ dragging: false });
103 | }
104 | }
105 |
106 | dropListener(event) {
107 | overrideEventDefaults(event);
108 | this.dragEventCounter = 0;
109 | this.setState({ dragging: false });
110 |
111 | if (event.dataTransfer.files && event.dataTransfer.files[0]) {
112 | const f = event.dataTransfer.files[0];
113 | const reader = new FileReader();
114 |
115 | reader.onload = e => {
116 | try {
117 | const config = JSON.parse(e.target.result);
118 | this.jsonEditor.update(config);
119 | } catch (error) {
120 | // TODO: Add notifications. Show notification for invalid json
121 | console.log(error);
122 | }
123 | };
124 | reader.readAsText(f);
125 | }
126 | }
127 |
128 | componentDidMount() {
129 | window.addEventListener('dragover', event => {
130 | overrideEventDefaults(event);
131 | });
132 | window.addEventListener('drop', event => {
133 | overrideEventDefaults(event);
134 | });
135 | }
136 |
137 | componentWillUnmount() {
138 | window.removeEventListener('dragover', overrideEventDefaults);
139 | window.removeEventListener('drop', overrideEventDefaults);
140 | }
141 |
142 | downloadConfig() {
143 | const config = this.jsonEditor.get();
144 | downloadFile(JSON.stringify(config, null, 2), 'zethus_config.json');
145 | }
146 |
147 | handleSubmit() {
148 | const { updateConfiguration } = this.props;
149 | const config = this.jsonEditor.get();
150 | updateConfiguration(config);
151 | }
152 |
153 | setEditorRef(instance) {
154 | if (instance) {
155 | const { jsonEditor } = instance;
156 | this.jsonEditor = jsonEditor;
157 | } else {
158 | this.jsonEditor = null;
159 | }
160 | }
161 |
162 | render() {
163 | const { closeModal, configuration } = this.props;
164 | const { dragging } = this.state;
165 |
166 | return (
167 |
168 |
169 | Edit Configuration
170 |
179 |
189 | {dragging && (
190 |
191 | Drag & drop Configuration JSON file
192 |
193 | )}
194 |
195 |
196 |
197 | Download
198 |
199 |
200 |
201 | Update Configuration
202 |
203 | Cancel
204 |
205 |
206 |
207 | );
208 | }
209 | }
210 |
211 | export default ConfigurationModal;
212 |
--------------------------------------------------------------------------------
/src/panels/graphVisualizationModal/Tree.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import createAndPopulateGraph, { reposition } from './utils';
3 | import { defaultGraph, graphWithTopicNodes } from '../../utils';
4 | import { NODE_SELECT_VALUES } from './constants';
5 |
6 | class Tree extends Component {
7 | constructor(props) {
8 | super(props);
9 | this.graphBasedOnOptions = this.graphBasedOnOptions.bind(this);
10 | this.handleGraphResize = this.handleGraphResize.bind(this);
11 | }
12 |
13 | componentDidMount() {
14 | this.graphBasedOnOptions();
15 | window.addEventListener('resize', this.handleGraphResize);
16 | }
17 |
18 | handleGraphResize() {
19 | reposition(this.graph);
20 | }
21 |
22 | graphBasedOnOptions() {
23 | const { graph, nodeSelect } = this.props;
24 | let newGraph = null;
25 | switch (nodeSelect.value) {
26 | case NODE_SELECT_VALUES.NODES_ONLY: {
27 | newGraph = defaultGraph(graph);
28 | break;
29 | }
30 | case NODE_SELECT_VALUES.NODES_AND_TOPICS: {
31 | newGraph = graphWithTopicNodes(graph);
32 | break;
33 | }
34 | }
35 | this.graph = createAndPopulateGraph(newGraph, 'graph');
36 | }
37 |
38 | componentDidUpdate(prevProps) {
39 | const { graph, nodeSelect } = this.props;
40 | if (
41 | JSON.stringify(prevProps.graph) !== JSON.stringify(graph) ||
42 | JSON.stringify(prevProps.nodeSelect) !== JSON.stringify(nodeSelect)
43 | ) {
44 | this.graphBasedOnOptions();
45 | }
46 | }
47 |
48 | render() {
49 | return (
50 |
53 | );
54 | }
55 | }
56 |
57 | export default Tree;
58 |
--------------------------------------------------------------------------------
/src/panels/graphVisualizationModal/constants.js:
--------------------------------------------------------------------------------
1 | export const NODE_SELECT_VALUES = {
2 | NODES_ONLY: 0,
3 | NODES_AND_TOPICS: 1,
4 | };
5 |
6 | export const NODE_OPTIONS = [
7 | {
8 | value: NODE_SELECT_VALUES.NODES_ONLY,
9 | label: 'Nodes only',
10 | },
11 | {
12 | value: NODE_SELECT_VALUES.NODES_AND_TOPICS,
13 | label: 'Node/Topics (all)',
14 | },
15 | ];
16 |
--------------------------------------------------------------------------------
/src/panels/graphVisualizationModal/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import Tree from './Tree';
4 |
5 | import { ButtonPrimary } from '../../components/styled';
6 | import {
7 | ModalWrapper,
8 | ModalContents,
9 | ModalTitle,
10 | } from '../../components/styled/modal';
11 | import {
12 | stopPropagation,
13 | generateGraph,
14 | ROS_SOCKET_STATUSES,
15 | } from '../../utils';
16 | import API_CALL_STATUS from '../../utils/constants';
17 | import VisualizationHelperToolbar from './visualizationToolbar';
18 | import { NODE_OPTIONS } from './constants';
19 |
20 | const GraphContainer = styled.div`
21 | border: 1px solid red;
22 | display: flex;
23 | height: 90%;
24 | justify-content: center;
25 | align-items: center;
26 | position: relative;
27 | overflow: hidden;
28 | & > svg {
29 | width: 100%;
30 | height: 100%;
31 | z-index: 10 !important;
32 | }
33 |
34 | .node rect,
35 | .node circle,
36 | .node ellipse {
37 | stroke: #333;
38 | fill: #fff;
39 | stroke-width: 1px;
40 | }
41 | .edgePath path {
42 | stroke: #333;
43 | fill: #333;
44 | stroke-width: 1.5px;
45 | }
46 | .root-node {
47 | color: white;
48 | }
49 |
50 | /* This styles the title of the tooltip */
51 | .name {
52 | font-size: 1.5em;
53 | font-weight: bold;
54 | color: #60b1fc;
55 | margin: 0;
56 | }
57 |
58 | /* This styles the body of the tooltip */
59 | .description {
60 | font-size: 1.2em;
61 | }
62 | `;
63 |
64 | const ModalHeading = styled.div`
65 | display: flex;
66 | justify-content: space-between;
67 | align-items: center;
68 | `;
69 |
70 | const StyledModalContents = styled(ModalContents)`
71 | height: 90%;
72 | width: 90%;
73 | margin: auto;
74 | margin-top: 5vh;
75 | `;
76 |
77 | class ConfigurationModal extends React.Component {
78 | constructor(props) {
79 | super(props);
80 | this.state = {
81 | graph: null,
82 | status: API_CALL_STATUS.FETCHING,
83 | visualizationToolbarSettings: {
84 | debug: true,
85 | nodeSelect: NODE_OPTIONS[0],
86 | },
87 | };
88 | this.graphContainerRef = React.createRef();
89 | this.handleSubmit = this.handleSubmit.bind(this);
90 | this.createGraph = this.createGraph.bind(this);
91 | this.refreshGraph = this.refreshGraph.bind(this);
92 | this.returnContainerRef = this.returnContainerRef.bind(this);
93 | this.changeVisualizationToolbar = this.changeVisualizationToolbar.bind(
94 | this,
95 | );
96 | this.selectHandler = this.selectHandler.bind(this);
97 | }
98 |
99 | createGraph() {
100 | const { ros } = this.props;
101 | const p = generateGraph(ros);
102 | p.then(graph => {
103 | this.setState({ graph, status: API_CALL_STATUS.SUCCESSFUL });
104 | }).catch(err => {
105 | console.log(err);
106 | this.setState({
107 | status: API_CALL_STATUS.ERROR,
108 | });
109 | });
110 | }
111 |
112 | selectHandler(selectedOption) {
113 | this.setState(function({ visualizationToolbarSettings }) {
114 | return {
115 | visualizationToolbarSettings: {
116 | ...visualizationToolbarSettings,
117 | nodeSelect: selectedOption,
118 | },
119 | };
120 | });
121 | }
122 |
123 | changeVisualizationToolbar(e) {
124 | const {
125 | checked,
126 | dataset: { id },
127 | } = e.target;
128 | this.setState(function({ visualizationToolbarSettings }) {
129 | return {
130 | visualizationToolbarSettings: {
131 | ...visualizationToolbarSettings,
132 | [id]: checked,
133 | },
134 | };
135 | });
136 | }
137 |
138 | refreshGraph(e) {
139 | e.preventDefault();
140 | this.createGraph();
141 | }
142 |
143 | returnContainerRef() {
144 | return this.graphContainerRef;
145 | }
146 |
147 | componentDidMount() {
148 | this.createGraph();
149 | }
150 |
151 | handleSubmit() {
152 | const { updateConfiguration } = this.props;
153 | const config = this.jsonEditor.get();
154 | updateConfiguration(config);
155 | }
156 |
157 | render() {
158 | const { closeModal, rosStatus } = this.props;
159 | const {
160 | graph,
161 | status,
162 | visualizationToolbarSettings: { debug, nodeSelect },
163 | } = this.state;
164 |
165 | let data = null;
166 | if (status === API_CALL_STATUS.SUCCESSFUL) {
167 | data = (
168 |
174 | );
175 | } else if (status === API_CALL_STATUS.ERROR) {
176 | data = (
177 |
178 | Error{' '}
179 | Refresh
180 |
181 | );
182 | } else {
183 | data = Loading.
;
184 | }
185 | return (
186 |
187 |
188 |
189 | Graph
190 |
194 | {rosStatus === ROS_SOCKET_STATUSES.CONNECTED
195 | ? 'Refresh'
196 | : 'Websocket disconnected.'}
197 |
198 |
199 |
205 | {data}
206 |
207 |
208 | );
209 | }
210 | }
211 |
212 | export default ConfigurationModal;
213 |
--------------------------------------------------------------------------------
/src/panels/graphVisualizationModal/utils.js:
--------------------------------------------------------------------------------
1 | import DagreD3 from 'dagre-d3';
2 | import * as d3 from 'd3';
3 |
4 | // NOTE: Make into a class/pure functions.
5 |
6 | function createAndPopulateGraph(graph, targetElementId) {
7 | const g = new DagreD3.graphlib.Graph().setGraph({
8 | rankdir: 'LR',
9 | });
10 | const initialScale = 0.75;
11 | const svg = d3.select(`#${targetElementId}`);
12 | const inner = svg.select('g');
13 | // Create the renderer
14 | /* eslint-disable-next-line */
15 | const render = new DagreD3.render();
16 |
17 | // Set up zoom support
18 | const zoom = d3.zoom().on('zoom', function() {
19 | inner.attr('transform', d3.event.transform);
20 | });
21 | svg.call(zoom);
22 | graph.nodes.forEach(function(node) {
23 | g.setNode(node.id, { label: node.label, shape: node.type });
24 | });
25 |
26 | graph.edges.forEach(function(edge) {
27 | g.setEdge(edge.source.id, edge.target.id, { label: edge.value });
28 | });
29 |
30 | // Set some general styles
31 | g.nodes().forEach(function(v) {
32 | const node = g.node(v);
33 | node.rx = 5;
34 | node.ry = 5;
35 | });
36 | // Run the renderer. This is what draws the final graph.
37 | render(inner, g);
38 | svg.call(
39 | zoom.transform,
40 | d3.zoomIdentity
41 | .translate(
42 | (svg.attr('width') - g.graph().width * initialScale) / 2 +
43 | Number(svg.style('width').slice(0, -2)) / 2,
44 | Number(svg.style('height').slice(0, -2)) / 2 -
45 | (g.graph().height * initialScale) / 2,
46 | )
47 | .scale(initialScale),
48 | );
49 | svg.attr('height', g.graph().height * initialScale + 40);
50 |
51 | return {
52 | svg,
53 | g,
54 | initialScale,
55 | zoom,
56 | };
57 | }
58 |
59 | export function reposition({ svg, zoom, g, initialScale }) {
60 | svg.call(
61 | zoom.transform,
62 | d3.zoomIdentity
63 | .translate(
64 | (svg.attr('width') - g.graph().width * initialScale) / 2 +
65 | Number(svg.style('width').slice(0, -2)) / 2,
66 | Number(svg.style('height').slice(0, -2)) / 2 -
67 | (g.graph().height * initialScale) / 2,
68 | )
69 | .scale(initialScale),
70 | );
71 | svg.attr('height', g.graph().height * initialScale + 40);
72 | }
73 |
74 | export default createAndPopulateGraph;
75 |
--------------------------------------------------------------------------------
/src/panels/graphVisualizationModal/visualizationToolbar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import PropTypes from 'prop-types';
4 | import Select from 'react-select';
5 |
6 | import { Container } from '../../components/styled';
7 | import { NODE_OPTIONS } from './constants';
8 |
9 | const SelectStyled = styled(Select)`
10 | width: 200px !important;
11 | z-index: 101;
12 | `;
13 |
14 | const Wrapper = styled(Container)`
15 | padding-left: 0;
16 | `;
17 |
18 | function visualizationToolbar({
19 | changeVisualizationToolbar,
20 | debug,
21 | selectHandler,
22 | nodeSelect,
23 | }) {
24 | return (
25 |
26 |
33 |
34 | {/* Could be included in a different pr with all filtering options. */}
35 | {/* */}
46 |
47 | );
48 | }
49 |
50 | visualizationToolbar.propTypes = {
51 | debug: PropTypes.bool,
52 | };
53 |
54 | visualizationToolbar.defaultProps = {
55 | debug: true,
56 | };
57 |
58 | export default visualizationToolbar;
59 |
--------------------------------------------------------------------------------
/src/panels/header/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyledHeader, StyledLogo } from '../../components/styled';
3 | import Logo from '../../components/logo';
4 | import Toolbar from './toolbar';
5 |
6 | export default class Header extends React.PureComponent {
7 | render() {
8 | const { activeTool, selectTool } = this.props;
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/panels/header/tool.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyledTool, ToolHeading } from '../../components/styled';
3 |
4 | export default class Tool extends React.PureComponent {
5 | render() {
6 | const { active, data, selectTool } = this.props;
7 | const { icon, name, type } = data;
8 | return (
9 | selectTool(name, type)}>
10 | {icon(active)} {name}
11 |
12 | );
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/panels/header/toolbar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { map } from 'lodash';
3 | import { StyledToolbar } from '../../components/styled';
4 | import Tool from './tool';
5 | import { toolOptions } from '../../utils/toolbar';
6 |
7 | export default class Toolbar extends React.PureComponent {
8 | render() {
9 | const { activeTool, selectTool } = this.props;
10 | return (
11 |
12 | {map(toolOptions, option => (
13 |
19 | ))}
20 |
21 | );
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/panels/info/addInfoPanelModal.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { filter, get, isNil, map, size } from 'lodash';
3 | import TagsInput from 'react-tagsinput';
4 | import {
5 | AddInfoPanelModalTopics,
6 | ButtonPrimary,
7 | FlexGrow,
8 | } from '../../components/styled';
9 |
10 | import {
11 | ModalActions,
12 | ModalContents,
13 | ModalTitle,
14 | ModalWrapper,
15 | TopicRow,
16 | } from '../../components/styled/modal';
17 | import { stopPropagation } from '../../utils';
18 |
19 | class AddInfoPanelModal extends React.Component {
20 | constructor(props) {
21 | super(props);
22 | this.state = {
23 | selected: null,
24 | keys: [],
25 | };
26 |
27 | this.selectTopic = this.selectTopic.bind(this);
28 | this.closeModal = this.closeModal.bind(this);
29 | this.addInfoPanel = this.addInfoPanel.bind(this);
30 | this.handleFilterKeysChange = this.handleFilterKeysChange.bind(this);
31 | }
32 |
33 | handleFilterKeysChange(keys) {
34 | this.setState({ keys });
35 | }
36 |
37 | selectTopic(selected) {
38 | this.setState({ selected });
39 | }
40 |
41 | closeModal() {
42 | const { closeModal } = this.props;
43 | this.setState({ selected: null, keys: [] }, () => {
44 | closeModal();
45 | });
46 | }
47 |
48 | addInfoPanel(topic, keys) {
49 | const { closeModal, onAdd } = this.props;
50 | this.setState({ selected: null, keys: [] }, () => {
51 | onAdd(topic, keys);
52 | closeModal();
53 | });
54 | }
55 |
56 | render() {
57 | const { allTopics, open, topics } = this.props;
58 | const { keys, selected } = this.state;
59 |
60 | const topicsNamesSet = new Set(map(topics, topic => topic.name));
61 | const filteredTopics = filter(
62 | allTopics,
63 | topic => !topicsNamesSet.has(topic.name),
64 | );
65 |
66 | return open ? (
67 |
68 |
69 | Add Info Panel
70 |
71 | {size(allTopics) === 0
72 | ? 'No topics available'
73 | : map(filteredTopics, ({ name, messageType }) => (
74 | {
79 | this.selectTopic({ name, messageType });
80 | }}
81 | >
82 | {name}
83 | ({messageType})
84 |
85 | ))}
86 |
87 |
88 |
96 |
97 |
98 |
99 | this.addInfoPanel(selected, keys)}
102 | >
103 | Confirm
104 |
105 | Cancel
106 |
107 |
108 |
109 | ) : null;
110 | }
111 | }
112 |
113 | export default AddInfoPanelModal;
114 |
--------------------------------------------------------------------------------
/src/panels/info/content.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { isNil, omit, size, map } from 'lodash';
3 | import FormattedContent from './formattedContent';
4 | import { FilteredKeys, InfoPanelNoMessage } from '../../components/styled';
5 | import RawContent from './rawContent';
6 |
7 | const CONTENT_MANUAL_UPDATE_RATE = 500;
8 |
9 | class Content extends React.Component {
10 | constructor(props) {
11 | super(props);
12 | this.forceUpdateId = null;
13 | }
14 |
15 | componentDidMount() {
16 | this.forceUpdateId = setInterval(() => {
17 | this.forceUpdate();
18 | }, CONTENT_MANUAL_UPDATE_RATE);
19 | }
20 |
21 | componentWillUnmount() {
22 | clearInterval(this.forceUpdateId);
23 | }
24 |
25 | shouldComponentUpdate(nextProps) {
26 | const { raw, selected } = this.props;
27 | return nextProps.selected !== selected || nextProps.raw !== raw;
28 | }
29 |
30 | render() {
31 | const { messageBuffers, openAddInfoPanel, raw, selected } = this.props;
32 |
33 | const lastMessage =
34 | size(messageBuffers[selected.name]) === 0
35 | ? null
36 | : messageBuffers[selected.name][0];
37 |
38 | if (isNil(selected.name)) {
39 | return (
40 |
41 | Add an info panel to receive messages.
42 |
43 | );
44 | }
45 |
46 | if (size(messageBuffers[selected.name]) === 0) {
47 | return waiting for messages...;
48 | }
49 |
50 | const bufferClone = raw ? [...messageBuffers[selected.name]] : [];
51 | return (
52 | <>
53 | {size(selected.keys) > 0 && (
54 |
55 | {map(selected.keys, key => (
56 | {key}
57 | ))}
58 |
59 | )}
60 | {raw ? (
61 |
62 | ) : (
63 |
64 | )}
65 | >
66 | );
67 | }
68 | }
69 |
70 | export default Content;
71 |
--------------------------------------------------------------------------------
/src/panels/info/formattedContent.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { JsonEditor } from 'jsoneditor-react';
3 | import { isNil } from 'lodash';
4 |
5 | class FormattedContent extends React.PureComponent {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.jsonEditor = null;
10 | this.setRef = this.setRef.bind(this);
11 | }
12 |
13 | setRef(instance) {
14 | if (instance) {
15 | const { jsonEditor } = instance;
16 | this.jsonEditor = jsonEditor;
17 | }
18 | }
19 |
20 | componentDidUpdate(prevProps) {
21 | const { message } = this.props;
22 | if (!isNil(message) && prevProps.message !== message && this.jsonEditor) {
23 | this.jsonEditor.update(message);
24 | }
25 | }
26 |
27 | render() {
28 | const { message } = this.props;
29 | if (isNil(message)) {
30 | return null;
31 | }
32 | return (
33 |
42 | );
43 | }
44 | }
45 |
46 | export default FormattedContent;
47 |
--------------------------------------------------------------------------------
/src/panels/info/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ROSLIB from 'roslib';
3 | import {
4 | find,
5 | findIndex,
6 | forEach,
7 | get,
8 | isEqual,
9 | isNil,
10 | map,
11 | size,
12 | } from 'lodash';
13 | import {
14 | InfoPanel,
15 | InfoPanelAddButton,
16 | InfoPanelContentWrapper,
17 | InfoPanelHeader,
18 | InfoPanelHeaderControls,
19 | InfoPanelNoMessage,
20 | InfoPanelTab,
21 | InfoPanelTabsWrapper,
22 | } from '../../components/styled';
23 | import Content from './content';
24 | import { sanitizeMessage } from '../../utils/sanitize';
25 | import AddInfoPanelModal from './addInfoPanelModal';
26 |
27 | const MESSAGE_BUFFER_MAX_LENGTH = 1000;
28 | const compressionTypes = new Set([
29 | 'sensor_msgs/Image',
30 | 'sensor_msgs/PointCloud2',
31 | 'sensor_msgs/PointCloud',
32 | 'sensor_msgs/LaserScan',
33 | 'nav_msgs/Path',
34 | 'nav_msgs/OccupancyGrid',
35 | 'visualization_msgs/MarkerArray',
36 | 'geometry_msgs/Polygon',
37 | 'geometry_msgs/PolygonStamped',
38 | 'geometry_msgs/PoseArray',
39 | ]);
40 | const getTopicOptions = messageType => {
41 | if (compressionTypes.has(messageType)) {
42 | return {
43 | queue_length: 1,
44 | compression: 'cbor',
45 | };
46 | }
47 |
48 | return {
49 | queue_length: 1,
50 | };
51 | };
52 |
53 | class Info extends React.PureComponent {
54 | constructor(props) {
55 | super(props);
56 |
57 | const { topics } = props;
58 | this.topicInstances = {};
59 | this.messageBuffers = {};
60 |
61 | this.state = {
62 | selected: get(topics, '[0]', {}),
63 | raw: false,
64 | addModalOpen: false,
65 | };
66 |
67 | this.onMessage = this.onMessage.bind(this);
68 | this.onTabChange = this.onTabChange.bind(this);
69 | this.onRawClick = this.onRawClick.bind(this);
70 | this.toggleAddModal = this.toggleAddModal.bind(this);
71 | this.addInfoPanel = this.addInfoPanel.bind(this);
72 | }
73 |
74 | componentDidMount() {
75 | const { ros, topics } = this.props;
76 |
77 | forEach(topics, topic => {
78 | const topicInstance = new ROSLIB.Topic({
79 | ...topic,
80 | ros,
81 | ...getTopicOptions(topic.messageType),
82 | });
83 | topicInstance.subscribe(message => this.onMessage(topic, message));
84 | this.messageBuffers[topic.name] = [];
85 | this.topicInstances[topic.name] = topicInstance;
86 | });
87 | }
88 |
89 | static getDerivedStateFromProps(nextProps, prevState) {
90 | let { selected } = prevState;
91 | if (isNil(find(nextProps.topics, t => t.name === selected.name))) {
92 | selected = size(nextProps.topics) > 0 ? nextProps.topics[0] : {};
93 | }
94 | return {
95 | ...prevState,
96 | selected,
97 | topics: nextProps.topics,
98 | };
99 | }
100 |
101 | componentDidUpdate(prevProps) {
102 | const { ros, topics } = this.props;
103 |
104 | if (isEqual(prevProps.topics, topics)) {
105 | return;
106 | }
107 |
108 | const newTopicNames = new Set(map(topics, t => t.name));
109 | const oldTopicNames = new Set(map(prevProps.topics, t => t.name));
110 |
111 | forEach(prevProps.topics, topic => {
112 | if (!newTopicNames.has(topic.name)) {
113 | this.topicInstances[topic.name].unsubscribe();
114 | delete this.messageBuffers[topic.name];
115 | delete this.topicInstances[topic.name];
116 | }
117 | });
118 |
119 | forEach(topics, topic => {
120 | if (!oldTopicNames.has(topic.name)) {
121 | const topicInstance = new ROSLIB.Topic({
122 | ...topic,
123 | ros,
124 | ...getTopicOptions(topic.messageType),
125 | });
126 | topicInstance.subscribe(message => this.onMessage(topic, message));
127 | this.messageBuffers[topic.name] = [];
128 | this.topicInstances[topic.name] = topicInstance;
129 | }
130 | });
131 | }
132 |
133 | onMessage(topic, message) {
134 | const { name } = topic;
135 | const buffer = this.messageBuffers[name];
136 | if (size(buffer) === MESSAGE_BUFFER_MAX_LENGTH) {
137 | buffer.pop();
138 | }
139 |
140 | const sanitizedMessage = sanitizeMessage(topic, message);
141 | sanitizedMessage.timestamp = performance.now();
142 | buffer.unshift(sanitizedMessage);
143 | }
144 |
145 | onTabChange(e, topic) {
146 | const {
147 | collapsed,
148 | togglePanelCollapse,
149 | topics,
150 | updateInfoTabs,
151 | } = this.props;
152 | const action = e.target.getAttribute('data-action');
153 |
154 | if (action === 'close') {
155 | const topicsShallowClone = [...topics];
156 | const index = findIndex(topicsShallowClone, x => x.name === topic.name);
157 | topicsShallowClone.splice(index, 1);
158 | updateInfoTabs(topicsShallowClone);
159 | } else {
160 | this.setState({ selected: topic }, () => {
161 | if (collapsed) {
162 | togglePanelCollapse('info');
163 | }
164 | });
165 | }
166 | }
167 |
168 | onRawClick(e) {
169 | this.setState({ raw: e.target.checked || false });
170 | }
171 |
172 | toggleAddModal(addModalOpen) {
173 | this.props.refreshRosData();
174 | this.setState({ addModalOpen });
175 | }
176 |
177 | addInfoPanel(topic, keys) {
178 | const {
179 | collapsed,
180 | togglePanelCollapse,
181 | topics,
182 | updateInfoTabs,
183 | } = this.props;
184 | const topicsShallowClone = [...topics];
185 | topic.keys = keys;
186 | topicsShallowClone.push(topic);
187 | if (collapsed) {
188 | togglePanelCollapse('info');
189 | }
190 | setTimeout(() => updateInfoTabs(topicsShallowClone), 0);
191 | this.toggleAddModal(false);
192 | }
193 |
194 | render() {
195 | const { addModalOpen, raw, selected } = this.state;
196 | const {
197 | collapsed,
198 | rosTopics: allTopics,
199 | toggleGraphModal,
200 | togglePanelCollapse,
201 | topics,
202 | } = this.props;
203 |
204 | return (
205 | <>
206 |
207 |
208 |
209 | {map(topics, t => (
210 | this.onTabChange(e, t)}
214 | >
215 | {t.name}
216 | ⊗
217 |
218 | ))}
219 | this.toggleAddModal(true)}>
220 | +
221 |
222 |
223 |
224 |
225 | RQT Graph
226 |
227 |
231 | togglePanelCollapse('info')}>
232 | {collapsed ? 'Expand' : 'Collapse'} {collapsed ? '▲' : '▼'}
233 |
234 |
235 |
236 |
237 | this.toggleAddModal(true)}
242 | />
243 |
244 |
245 | this.toggleAddModal(false)}
250 | onAdd={this.addInfoPanel}
251 | />
252 | >
253 | );
254 | }
255 | }
256 |
257 | export default Info;
258 |
--------------------------------------------------------------------------------
/src/panels/info/rawContent.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { isNil, omit, size } from 'lodash';
3 | import { AutoSizer, List } from 'react-virtualized';
4 | import { RawContentRow, RawContentWrapper } from '../../components/styled';
5 |
6 | class RawContent extends React.Component {
7 | constructor(props) {
8 | super(props);
9 |
10 | this.rowRenderer = this.rowRenderer.bind(this);
11 | }
12 |
13 | static noRowsRenderer() {
14 | return null;
15 | }
16 |
17 | rowRenderer({ index }) {
18 | const { messages } = this.props;
19 | if (isNil(messages[index])) {
20 | return null;
21 | }
22 |
23 | return (
24 |
25 | {JSON.stringify(omit(messages[index], ['timestamp']))}
26 |
27 | );
28 | }
29 |
30 | render() {
31 | const { messages } = this.props;
32 | const rowHeight = 200;
33 | return (
34 |
35 |
36 | {({ width }) => (
37 |
47 | )}
48 |
49 |
50 | );
51 | }
52 | }
53 |
54 | export default RawContent;
55 |
--------------------------------------------------------------------------------
/src/panels/sidebar/globalOptions.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 | import styled from 'styled-components';
4 |
5 | import OptionRow from '../../components/optionRow';
6 | import { Container, Input, Select, TextButton } from '../../components/styled';
7 |
8 | const EditConfigButton = styled(TextButton)`
9 | font-size: 14px;
10 | `;
11 |
12 | class GlobalOptions extends React.PureComponent {
13 | constructor(props) {
14 | super(props);
15 | this.updateOptions = this.updateOptions.bind(this);
16 | }
17 |
18 | updateOptions(e) {
19 | const { updateGlobalOptions } = this.props;
20 | const {
21 | dataset: { id: optionId },
22 | value,
23 | } = e.target;
24 | updateGlobalOptions(optionId, value);
25 | }
26 |
27 | render() {
28 | const {
29 | framesList,
30 | globalOptions: {
31 | backgroundColor: {
32 | display: displayBackgroundColor,
33 | value: valueBackgroundColor,
34 | },
35 | display: displayOptions,
36 | fixedFrame: { display: displayFixedFrame, value: valueFixedFrame },
37 | grid: { display: displayGrid, size: valueGrid },
38 | },
39 | toggleConfigurationModal,
40 | } = this.props;
41 | if (!displayOptions) {
42 | return null;
43 | }
44 | return (
45 |
46 |
47 | Edit Configuration
48 |
49 | {displayBackgroundColor && (
50 |
51 |
57 |
58 | )}
59 | {displayGrid && {valueGrid}}
60 | {displayFixedFrame && (
61 |
62 |
73 |
74 | )}
75 |
76 | );
77 | }
78 | }
79 |
80 | export default GlobalOptions;
81 |
--------------------------------------------------------------------------------
/src/panels/sidebar/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 |
4 | import { ROS_SOCKET_STATUSES } from '../../utils';
5 | import { vizOptions } from '../../utils/vizOptions';
6 | import GlobalOptions from './globalOptions';
7 | import {
8 | ButtonPrimary,
9 | Container,
10 | Separator,
11 | SidebarCollapse,
12 | SidebarWrapper,
13 | StyledSidebar,
14 | } from '../../components/styled';
15 | import ConnectionDot from '../../components/connectionDot';
16 | import RosReconnectHandler from './rosReconnectHandler';
17 | import VizOptions from './vizOptions';
18 | import { RosStatus, SidebarVizContainer } from '../../components/styled/viz';
19 |
20 | class Sidebar extends React.Component {
21 | constructor(props) {
22 | super(props);
23 | this.state = {
24 | rosInput: props.rosEndpoint,
25 | };
26 | this.updateRosInput = this.updateRosInput.bind(this);
27 | this.onSubmit = this.onSubmit.bind(this);
28 | this.toggleSidebarOpen = this.toggleSidebarOpen.bind(this);
29 | }
30 |
31 | updateRosInput(e) {
32 | this.setState({
33 | rosInput: e.target.value,
34 | });
35 | }
36 |
37 | toggleSidebarOpen() {
38 | const { togglePanelCollapse } = this.props;
39 | togglePanelCollapse('sidebar');
40 | }
41 |
42 | onSubmit(e) {
43 | const {
44 | connectRos,
45 | disconnectRos,
46 | rosEndpoint,
47 | rosStatus,
48 | updateRosEndpoint,
49 | } = this.props;
50 | const { rosInput } = this.state;
51 | e.preventDefault();
52 | if (rosInput !== rosEndpoint) {
53 | updateRosEndpoint(rosInput);
54 | } else if (
55 | _.includes(
56 | [ROS_SOCKET_STATUSES.CONNECTED, ROS_SOCKET_STATUSES.CONNECTING],
57 | rosStatus,
58 | )
59 | ) {
60 | disconnectRos();
61 | } else {
62 | connectRos();
63 | }
64 | }
65 |
66 | render() {
67 | const {
68 | collapsedSidebar,
69 | connectRos,
70 | framesList,
71 | globalOptions,
72 | removeVisualization,
73 | rosInstance,
74 | rosStatus,
75 | rosTopics,
76 | toggleAddModal,
77 | toggleConfigurationModal,
78 | toggleVisibility,
79 | updateGlobalOptions,
80 | updateVizOptions,
81 | viewer,
82 | visualizations,
83 | vizInstances: vizInstancesSet,
84 | } = this.props;
85 |
86 | const vizInstances = [...vizInstancesSet];
87 |
88 | const { rosInput } = this.state;
89 | return (
90 |
91 |
92 |
93 |
94 |
95 |
96 | {rosStatus}.{' '}
97 |
101 |
102 |
103 |
109 |
110 |
111 | {rosStatus === ROS_SOCKET_STATUSES.CONNECTED && (
112 | <>
113 |
114 |
115 |
116 | Add Visualization
117 |
118 | {_.size(visualizations) === 0 && (
119 | No visualizations added to the scene
120 | )}
121 | {_.map(visualizations, vizItem => {
122 | const vizObject = _.find(
123 | vizOptions,
124 | v => v.type === vizItem.vizType,
125 | );
126 | if (!vizObject) {
127 | return null;
128 | }
129 | const topics = _.filter(rosTopics, t =>
130 | _.includes(vizObject.messageTypes, t.messageType),
131 | );
132 | const relatedTopics = _.filter(rosTopics, t =>
133 | _.includes(vizObject.additionalMessageTypes, t.messageType),
134 | );
135 | let vizInstance = _.filter(
136 | vizInstances,
137 | v => v.key === vizItem.key,
138 | );
139 | // TODO: This seems like a HACK but it was necessary to get the joints stuff to work when loading a fresh robot model
140 | if (vizInstance.length === 0) {
141 | vizInstance = [vizInstances[vizInstances.length - 1]];
142 | }
143 | return (
144 |
157 | );
158 | })}
159 |
160 | >
161 | )}
162 |
163 |
164 | {collapsedSidebar ? '▸' : '◂'}
165 |
166 |
167 | );
168 | }
169 | }
170 |
171 | export default Sidebar;
172 |
--------------------------------------------------------------------------------
/src/panels/sidebar/rosReconnectHandler.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { includes } from 'lodash';
3 |
4 | import { ROS_SOCKET_STATUSES } from '../../utils';
5 |
6 | // In seconds
7 | const MIN_TIMER_TIME = 5;
8 | const MAX_TIMER_TIME = 60;
9 | const TIMER_INCREMENT = 0;
10 | const ONE_SECOND = 1000;
11 |
12 | class RosReconnectHandler extends React.Component {
13 | constructor(props) {
14 | super(props);
15 | this.state = {
16 | timer: 0,
17 | };
18 | this.retryTime = MIN_TIMER_TIME;
19 | this.timerInstance = null;
20 |
21 | this.onTick = this.onTick.bind(this);
22 | this.startTimer = this.startTimer.bind(this);
23 | this.stopTimer = this.stopTimer.bind(this);
24 | }
25 |
26 | componentDidUpdate() {
27 | const { rosStatus } = this.props;
28 | switch (rosStatus) {
29 | case ROS_SOCKET_STATUSES.CONNECTED:
30 | this.retryTime = MIN_TIMER_TIME;
31 | break;
32 | case ROS_SOCKET_STATUSES.CONNECTING:
33 | this.stopTimer();
34 | break;
35 | case ROS_SOCKET_STATUSES.CONNECTION_ERROR:
36 | case ROS_SOCKET_STATUSES.INITIAL:
37 | default:
38 | if (!this.timerInstance && this.retryTime < MAX_TIMER_TIME) {
39 | this.retryTime += TIMER_INCREMENT;
40 | this.startTimer();
41 | }
42 | }
43 | }
44 |
45 | onTick() {
46 | const { timer } = this.state;
47 | const { connectRos } = this.props;
48 | if (timer === 0) {
49 | this.timerInstance = null;
50 | connectRos();
51 | } else {
52 | this.setState({
53 | timer: timer - 1,
54 | });
55 | this.timerInstance = setTimeout(this.onTick, ONE_SECOND);
56 | }
57 | }
58 |
59 | startTimer() {
60 | this.setState({
61 | timer: this.retryTime,
62 | });
63 | this.timerInstance = setTimeout(this.onTick, ONE_SECOND);
64 | }
65 |
66 | stopTimer() {
67 | if (this.timerInstance) {
68 | clearTimeout(this.timerInstance);
69 | this.timerInstance = null;
70 | this.setState({
71 | timer: 0,
72 | });
73 | }
74 | }
75 |
76 | render() {
77 | const { timer } = this.state;
78 | const { rosStatus } = this.props;
79 | if (
80 | includes(
81 | [ROS_SOCKET_STATUSES.CONNECTING, ROS_SOCKET_STATUSES.CONNECTED],
82 | rosStatus,
83 | )
84 | ) {
85 | return null;
86 | }
87 | return timer > 0 ? `Reconnecting in ${timer} seconds` : null;
88 | }
89 | }
90 |
91 | export default RosReconnectHandler;
92 |
--------------------------------------------------------------------------------
/src/panels/sidebar/vizOptions/arrow.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CONSTANTS } from 'amphion';
3 | import OptionRow from '../../../components/optionRow';
4 | import { Input } from '../../../components/styled';
5 |
6 | const { DEFAULT_OPTIONS_ARROW } = CONSTANTS;
7 |
8 | class ArrowOptions extends React.PureComponent {
9 | render() {
10 | const { options: propsOptions, updateOptions } = this.props;
11 |
12 | const { alpha, color, headLength, headRadius, shaftLength, shaftRadius } = {
13 | ...DEFAULT_OPTIONS_ARROW,
14 | ...propsOptions,
15 | };
16 |
17 | return (
18 | <>
19 |
20 |
27 |
28 |
29 |
30 |
38 |
39 |
40 |
41 |
49 |
50 |
51 |
52 |
60 |
61 |
62 |
63 |
71 |
72 |
73 |
74 |
82 |
83 | >
84 | );
85 | }
86 | }
87 |
88 | export default ArrowOptions;
89 |
--------------------------------------------------------------------------------
/src/panels/sidebar/vizOptions/axes.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CONSTANTS } from 'amphion';
3 | import OptionRow from '../../../components/optionRow';
4 | import { Input } from '../../../components/styled';
5 |
6 | const { DEFAULT_OPTIONS_AXES } = CONSTANTS;
7 |
8 | class AxesOptions extends React.PureComponent {
9 | render() {
10 | const { options: propsOptions, updateOptions } = this.props;
11 | const { axesLength, axesRadius } = {
12 | ...DEFAULT_OPTIONS_AXES,
13 | ...propsOptions,
14 | };
15 | return (
16 | <>
17 |
18 |
26 |
27 |
28 |
29 |
37 |
38 | >
39 | );
40 | }
41 | }
42 |
43 | export default AxesOptions;
44 |
--------------------------------------------------------------------------------
/src/panels/sidebar/vizOptions/colorTransformer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CONSTANTS } from 'amphion';
3 | import _ from 'lodash';
4 | import OptionRow from '../../../components/optionRow';
5 | import { Input, OptionContainer, Select } from '../../../components/styled';
6 |
7 | const { AXES, COLOR_TRANSFORMERS, INTENSITY_CHANNEL_OPTIONS } = CONSTANTS;
8 |
9 | const Intensity = props => {
10 | const {
11 | options: {
12 | autocomputeIntensityBounds,
13 | channelName,
14 | maxColor,
15 | maxIntensity,
16 | minColor,
17 | minIntensity,
18 | useRainbow,
19 | },
20 | updateOptions,
21 | } = props;
22 |
23 | return (
24 | <>
25 |
26 |
40 |
41 | {!useRainbow && (
42 | <>
43 |
44 |
51 |
52 |
53 |
60 |
61 | >
62 | )}
63 | {!autocomputeIntensityBounds && (
64 | <>
65 |
66 |
73 |
74 |
75 |
82 |
83 | >
84 | )}
85 | >
86 | );
87 | };
88 |
89 | const AxisColor = props => {
90 | const {
91 | options: { autocomputeValueBounds, axis, maxAxisValue, minAxisValue },
92 | updateOptions,
93 | } = props;
94 |
95 | return (
96 | <>
97 |
98 |
112 |
113 | {!autocomputeValueBounds && (
114 |
115 |
116 |
123 |
124 |
125 |
126 |
133 |
134 |
135 | )}
136 | >
137 | );
138 | };
139 |
140 | class ColorTransformer extends React.PureComponent {
141 | render() {
142 | const {
143 | options: { colorTransformer, flatColor },
144 | options,
145 | updateOptions,
146 | } = this.props;
147 |
148 | switch (colorTransformer) {
149 | case COLOR_TRANSFORMERS.INTENSITY:
150 | return ;
151 | case COLOR_TRANSFORMERS.AXIS_COLOR:
152 | return ;
153 | case COLOR_TRANSFORMERS.FLAT_COLOR:
154 | return (
155 |
156 |
163 |
164 | );
165 | default:
166 | return null;
167 | }
168 | }
169 | }
170 |
171 | export default ColorTransformer;
172 |
--------------------------------------------------------------------------------
/src/panels/sidebar/vizOptions/flatArrow.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CONSTANTS } from 'amphion';
3 |
4 | import OptionRow from '../../../components/optionRow';
5 | import { Input } from '../../../components/styled';
6 |
7 | const { DEFAULT_OPTIONS_FLATARROW } = CONSTANTS;
8 |
9 | class FlatArrowOptions extends React.PureComponent {
10 | render() {
11 | const { options: propsOptions, updateOptions } = this.props;
12 | const { alpha, arrowLength, color } = {
13 | ...DEFAULT_OPTIONS_FLATARROW,
14 | ...propsOptions,
15 | };
16 | return (
17 | <>
18 |
19 |
26 |
27 |
28 |
29 |
37 |
38 |
39 |
40 |
48 |
49 | >
50 | );
51 | }
52 | }
53 |
54 | export default FlatArrowOptions;
55 |
--------------------------------------------------------------------------------
/src/panels/sidebar/vizOptions/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import _ from 'lodash';
3 | import { CONSTANTS } from 'amphion';
4 | import VizSpecificOptions from './vizSpecificOption';
5 | import { Button, Select, StyledOptionRow } from '../../../components/styled';
6 | import OptionRow from '../../../components/optionRow';
7 | import {
8 | VizItem,
9 | VizItemActions,
10 | VizItemCollapse,
11 | VizItemContent,
12 | VizItemIcon,
13 | } from '../../../components/styled/viz';
14 | import Chevron from '../../../components/chevron';
15 | import {
16 | VIZ_TYPE_DEPTHCLOUD_STREAM,
17 | VIZ_TYPE_IMAGE_STREAM,
18 | } from '../../../utils/vizOptions';
19 |
20 | const {
21 | VIZ_TYPE_INTERACTIVEMARKER,
22 | VIZ_TYPE_ROBOTMODEL,
23 | VIZ_TYPE_TF,
24 | } = CONSTANTS;
25 |
26 | const VizOptions = ({
27 | options: { display, key, name, topicName, visible, vizType },
28 | options,
29 | topics,
30 | relatedTopics,
31 | vizInstance,
32 | vizObject: { icon },
33 | updateVizOptions,
34 | removeVisualization,
35 | toggleVisibility,
36 | }) => {
37 | const [collapsed, toggleCollapsed] = useState(false);
38 |
39 | if (_.isBoolean(display) && !display) {
40 | return null;
41 | }
42 |
43 | const updateVizOptionsWrapper = e => {
44 | if (vizType === VIZ_TYPE_INTERACTIVEMARKER) {
45 | updateVizOptions(key, {
46 | topicName: e.target.value,
47 | updateTopicName: undefined,
48 | feedbackTopicName: undefined,
49 | });
50 | return;
51 | }
52 | updateVizOptions(key, { topicName: e.target.value });
53 | };
54 |
55 | return (
56 |
57 |
58 | toggleCollapsed(!collapsed)}
61 | >
62 |
63 |
64 | {icon}
65 | {name}
66 |
67 | {!collapsed && (
68 |
69 | {!_.includes(
70 | [
71 | VIZ_TYPE_ROBOTMODEL,
72 | VIZ_TYPE_TF,
73 | VIZ_TYPE_DEPTHCLOUD_STREAM,
74 | VIZ_TYPE_IMAGE_STREAM,
75 | ],
76 | vizType,
77 | ) && (
78 |
79 |
84 |
85 | )}
86 |
93 |
94 |
97 |
100 |
101 |
102 | )}
103 |
104 | );
105 | };
106 |
107 | export default VizOptions;
108 |
--------------------------------------------------------------------------------
/src/panels/sidebar/vizOptions/interactiveMarkerOptions.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 | import { CONSTANTS } from 'amphion';
4 | import OptionRow from '../../../components/optionRow';
5 | import { Select } from '../../../components/styled';
6 |
7 | const {
8 | MESSAGE_TYPE_INTERACTIVEMARKER_FEEDBACK,
9 | MESSAGE_TYPE_INTERACTIVEMARKER_UPDATE,
10 | } = CONSTANTS;
11 |
12 | class InteractiveMarkerOptions extends React.PureComponent {
13 | constructor(props) {
14 | super(props);
15 |
16 | const {
17 | options: propsOptions,
18 | relatedTopics,
19 | updateVizOptions,
20 | } = this.props;
21 |
22 | const { key } = propsOptions;
23 |
24 | this.updateTopics = _.filter(
25 | relatedTopics,
26 | t => t.messageType === MESSAGE_TYPE_INTERACTIVEMARKER_UPDATE,
27 | );
28 | this.feedbackTopics = _.filter(
29 | relatedTopics,
30 | t => t.messageType === MESSAGE_TYPE_INTERACTIVEMARKER_FEEDBACK,
31 | );
32 |
33 | updateVizOptions(key, {
34 | updateTopicName: {
35 | name: this.updateTopics.length > 0 ? this.updateTopics[0].name : '',
36 | messageType: MESSAGE_TYPE_INTERACTIVEMARKER_UPDATE,
37 | },
38 | feedbackTopicName: {
39 | name: this.feedbackTopics.length > 0 ? this.feedbackTopics[0].name : '',
40 | messageType: MESSAGE_TYPE_INTERACTIVEMARKER_FEEDBACK,
41 | },
42 | });
43 | }
44 |
45 | render() {
46 | const { options: propsOptions, updateVizOptions } = this.props;
47 |
48 | const { feedbackTopicName, key, updateTopicName } = propsOptions;
49 |
50 | return (
51 | <>
52 |
53 |
71 |
72 |
73 |
91 |
92 | >
93 | );
94 | }
95 | }
96 |
97 | export default InteractiveMarkerOptions;
98 |
--------------------------------------------------------------------------------
/src/panels/sidebar/vizOptions/laserScan.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 | import { CONSTANTS } from 'amphion';
4 | import OptionRow from '../../../components/optionRow';
5 | import ColorTransformer from './colorTransformer';
6 | import { updateOptionsUtil } from '../../../utils';
7 | import { Input, Select } from '../../../components/styled';
8 |
9 | const {
10 | COLOR_TRANSFORMERS,
11 | DEFAULT_OPTIONS_LASERSCAN,
12 | LASERSCAN_STYLES,
13 | } = CONSTANTS;
14 |
15 | class LaserScanOptions extends React.PureComponent {
16 | constructor(props) {
17 | super(props);
18 | this.updateOptions = updateOptionsUtil.bind(this);
19 | }
20 |
21 | render() {
22 | const { options: propsOptions } = this.props;
23 |
24 | const options = {
25 | ...DEFAULT_OPTIONS_LASERSCAN,
26 | ...propsOptions,
27 | };
28 | const { alpha, colorTransformer, size, style } = options;
29 |
30 | return (
31 | <>
32 |
33 |
47 |
48 |
49 |
50 |
58 |
59 |
60 |
61 |
69 |
70 |
71 |
72 |
86 |
87 |
91 | >
92 | );
93 | }
94 | }
95 |
96 | export default LaserScanOptions;
97 |
--------------------------------------------------------------------------------
/src/panels/sidebar/vizOptions/map.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 | import { CONSTANTS } from 'amphion';
4 | import OptionRow from '../../../components/optionRow';
5 | import { updateOptionsUtil } from '../../../utils';
6 | import { Input, Select } from '../../../components/styled';
7 |
8 | const { DEFAULT_OPTIONS_MAP, MAP_COLOR_SCHEMES } = CONSTANTS;
9 |
10 | class MapOptions extends React.Component {
11 | constructor(props) {
12 | super(props);
13 | this.updateOptions = updateOptionsUtil.bind(this);
14 | }
15 |
16 | render() {
17 | const { options: propsOptions } = this.props;
18 | const options = {
19 | ...DEFAULT_OPTIONS_MAP,
20 | ...propsOptions,
21 | };
22 | const { alpha, colorScheme, drawBehind } = options;
23 | return (
24 | <>
25 |
26 |
34 |
35 |
36 |
50 |
51 |
52 |
59 |
60 | >
61 | );
62 | }
63 | }
64 |
65 | export default MapOptions;
66 |
--------------------------------------------------------------------------------
/src/panels/sidebar/vizOptions/marker.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 |
4 | import { CONSTANTS } from 'amphion';
5 | import OptionRow from '../../../components/optionRow';
6 | import { Input, OptionContainer } from '../../../components/styled';
7 |
8 | const { DEFAULT_OPTIONS_MARKER } = CONSTANTS;
9 |
10 | class MarkerOptions extends React.PureComponent {
11 | constructor(props) {
12 | super(props);
13 | this.updateNamespaceVisibility = this.updateNamespaceVisibility.bind(this);
14 | }
15 |
16 | updateNamespaceVisibility(e) {
17 | const {
18 | options: { key, namespaces },
19 | updateVizOptions,
20 | } = this.props;
21 | const {
22 | checked,
23 | dataset: { id: optionId },
24 | } = e.target;
25 | updateVizOptions(key, {
26 | namespaces: {
27 | ...namespaces,
28 | [optionId]: checked,
29 | },
30 | });
31 | }
32 |
33 | render() {
34 | const { options: propsOptions } = this.props;
35 |
36 | const { namespaces } = {
37 | ...DEFAULT_OPTIONS_MARKER,
38 | ...propsOptions,
39 | };
40 |
41 | if (_.size(_.compact(_.keys(namespaces))) === 0) {
42 | return null;
43 | }
44 |
45 | return (
46 | <>
47 | Namespaces:
48 |
49 | {_.map(namespaces, (checked, key) =>
50 | key ? (
51 |
52 |
59 |
60 | ) : null,
61 | )}
62 |
63 | >
64 | );
65 | }
66 | }
67 |
68 | export default MarkerOptions;
69 |
--------------------------------------------------------------------------------
/src/panels/sidebar/vizOptions/odometry.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 | import { CONSTANTS } from 'amphion';
4 | import OptionRow from '../../../components/optionRow';
5 | import ShapeOptions from './shape';
6 | import { updateOptionsUtil } from '../../../utils';
7 | import { Input, OptionContainer, Select } from '../../../components/styled';
8 |
9 | const {
10 | DEFAULT_OPTIONS_ODOMETRY,
11 | OBJECT_TYPE_ARROW,
12 | OBJECT_TYPE_AXES,
13 | } = CONSTANTS;
14 |
15 | class OdometryOptions extends React.PureComponent {
16 | constructor(props) {
17 | super(props);
18 | this.updateOptions = updateOptionsUtil.bind(this);
19 | }
20 |
21 | render() {
22 | const { options: propsOptions } = this.props;
23 | const options = {
24 | ...DEFAULT_OPTIONS_ODOMETRY,
25 | ...propsOptions,
26 | };
27 | const {
28 | angleTolerance,
29 | keep,
30 | positionTolerance,
31 | type: shapeType,
32 | } = options;
33 |
34 | return (
35 | <>
36 |
37 |
45 |
46 |
47 |
55 |
56 |
57 |
65 |
66 |
67 |
68 |
80 |
81 |
82 |
83 |
84 |
85 | >
86 | );
87 | }
88 | }
89 |
90 | export default OdometryOptions;
91 |
--------------------------------------------------------------------------------
/src/panels/sidebar/vizOptions/path.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CONSTANTS } from 'amphion';
3 | import OptionRow from '../../../components/optionRow';
4 | import { updateOptionsUtil } from '../../../utils';
5 | import { Input } from '../../../components/styled';
6 |
7 | const { DEFAULT_OPTIONS_PATH } = CONSTANTS;
8 |
9 | class PathOptions extends React.PureComponent {
10 | constructor(props) {
11 | super(props);
12 | this.updateOptions = updateOptionsUtil.bind(this);
13 | }
14 |
15 | render() {
16 | const { options: propsOptions } = this.props;
17 | const { alpha, color } = {
18 | ...DEFAULT_OPTIONS_PATH,
19 | ...propsOptions,
20 | };
21 | return (
22 | <>
23 |
24 |
31 |
32 |
33 |
41 |
42 | >
43 | );
44 | }
45 | }
46 |
47 | export default PathOptions;
48 |
--------------------------------------------------------------------------------
/src/panels/sidebar/vizOptions/point.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CONSTANTS } from 'amphion';
3 | import OptionRow from '../../../components/optionRow';
4 | import { updateOptionsUtil } from '../../../utils';
5 | import { Input } from '../../../components/styled';
6 |
7 | const { DEFAULT_OPTIONS_POINT } = CONSTANTS;
8 |
9 | class PointOptions extends React.PureComponent {
10 | constructor(props) {
11 | super(props);
12 | this.updateOptions = updateOptionsUtil.bind(this);
13 | }
14 |
15 | render() {
16 | const { options: propsOptions } = this.props;
17 | const { alpha, color, radius } = {
18 | ...DEFAULT_OPTIONS_POINT,
19 | ...propsOptions,
20 | };
21 | return (
22 | <>
23 |
24 |
32 |
33 |
34 |
41 |
42 |
43 |
51 |
52 | >
53 | );
54 | }
55 | }
56 |
57 | export default PointOptions;
58 |
--------------------------------------------------------------------------------
/src/panels/sidebar/vizOptions/pointcloud.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 | import { CONSTANTS } from 'amphion';
4 | import OptionRow from '../../../components/optionRow';
5 | import { updateOptionsUtil } from '../../../utils';
6 | import { Input, Select } from '../../../components/styled';
7 |
8 | const { DEFAULT_OPTIONS_POINTCLOUD, POINTCLOUD_COLOR_CHANNELS } = CONSTANTS;
9 |
10 | class PointCloudOptions extends React.PureComponent {
11 | constructor(props) {
12 | super(props);
13 | this.updateOptions = updateOptionsUtil.bind(this);
14 | }
15 |
16 | render() {
17 | const { options: propsOptions } = this.props;
18 |
19 | const options = {
20 | ...DEFAULT_OPTIONS_POINTCLOUD,
21 | ...propsOptions,
22 | };
23 | const { colorChannel, size, useRainbow } = options;
24 |
25 | return (
26 | <>
27 |
28 |
42 |
43 |
44 |
45 |
53 |
54 |
55 | {colorChannel === POINTCLOUD_COLOR_CHANNELS.INTENSITY && (
56 |
57 |
64 |
65 | )}
66 | >
67 | );
68 | }
69 | }
70 |
71 | export default PointCloudOptions;
72 |
--------------------------------------------------------------------------------
/src/panels/sidebar/vizOptions/pose.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 | import { CONSTANTS } from 'amphion';
4 | import OptionRow from '../../../components/optionRow';
5 | import ShapeOptions from './shape';
6 | import { updateOptionsUtil } from '../../../utils';
7 | import { OptionContainer, Select } from '../../../components/styled';
8 |
9 | const {
10 | DEFAULT_OPTIONS_POSE,
11 | OBJECT_TYPE_ARROW,
12 | OBJECT_TYPE_AXES,
13 | OBJECT_TYPE_FLAT_ARROW,
14 | VIZ_TYPE_POSE,
15 | VIZ_TYPE_POSEARRAY,
16 | } = CONSTANTS;
17 |
18 | const dropdownOptions = {
19 | [VIZ_TYPE_POSE]: [OBJECT_TYPE_ARROW, OBJECT_TYPE_AXES],
20 | [VIZ_TYPE_POSEARRAY]: [
21 | OBJECT_TYPE_ARROW,
22 | OBJECT_TYPE_AXES,
23 | OBJECT_TYPE_FLAT_ARROW,
24 | ],
25 | };
26 |
27 | class PoseOptions extends React.PureComponent {
28 | constructor(props) {
29 | super(props);
30 | this.updateOptions = updateOptionsUtil.bind(this);
31 | }
32 |
33 | render() {
34 | const { options: propsOptions } = this.props;
35 | const options = {
36 | ...DEFAULT_OPTIONS_POSE,
37 | ...propsOptions,
38 | };
39 | const { type: shapeType, vizType } = options;
40 |
41 | return (
42 | <>
43 |
44 |
56 |
57 |
58 |
59 |
60 | >
61 | );
62 | }
63 | }
64 |
65 | export default PoseOptions;
66 |
--------------------------------------------------------------------------------
/src/panels/sidebar/vizOptions/range.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CONSTANTS } from 'amphion';
3 | import OptionRow from '../../../components/optionRow';
4 | import { updateOptionsUtil } from '../../../utils';
5 | import { Input } from '../../../components/styled';
6 |
7 | const { DEFAULT_OPTIONS_RANGE } = CONSTANTS;
8 |
9 | class RangeOptions extends React.PureComponent {
10 | constructor(props) {
11 | super(props);
12 | this.updateOptions = updateOptionsUtil.bind(this);
13 | }
14 |
15 | render() {
16 | const { options: propsOptions } = this.props;
17 | const { alpha, color } = {
18 | ...DEFAULT_OPTIONS_RANGE,
19 | ...propsOptions,
20 | };
21 | return (
22 | <>
23 |
24 |
31 |
32 |
33 |
41 |
42 | >
43 | );
44 | }
45 | }
46 |
47 | export default RangeOptions;
48 |
--------------------------------------------------------------------------------
/src/panels/sidebar/vizOptions/robotModel.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { keys, map } from 'lodash';
3 | import { VizItem, VizItemCollapse } from '../../../components/styled/viz';
4 | import Chevron from '../../../components/chevron';
5 | import { Input, StyledOptionRow } from '../../../components/styled';
6 | import OptionRow from '../../../components/optionRow';
7 |
8 | class RobotModelLinksJoints extends React.PureComponent {
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | linksCollapsed: true,
13 | jointsCollapsed: true,
14 | };
15 |
16 | this.toggleCollapsed = this.toggleCollapsed.bind(this);
17 | }
18 |
19 | toggleCollapsed(name) {
20 | // eslint-disable-next-line react/destructuring-assignment
21 | const current = this.state[name];
22 | this.setState({ [name]: !current });
23 | }
24 |
25 | render() {
26 | const { jointsCollapsed, linksCollapsed } = this.state;
27 | const { vizInstance } = this.props;
28 |
29 | let joints = null;
30 | let links = null;
31 |
32 | if (vizInstance) {
33 | let v = vizInstance;
34 | const rest = null;
35 | if (Array.isArray(vizInstance)) [v] = vizInstance;
36 |
37 | const urdfObject = v ? v.urdfObject : null;
38 | joints = urdfObject ? urdfObject.joints : null;
39 | links = urdfObject ? urdfObject.links : null;
40 | }
41 |
42 | return (
43 | <>
44 |
45 |
46 | this.toggleCollapsed('linksCollapsed')}
49 | >
50 |
51 |
52 | Links
53 |
54 | {!linksCollapsed &&
55 | links &&
56 | map(keys(links), (name, index) => {
57 | const link = links[name];
58 | return (
59 |
60 | {
66 | const { checked } = e.target;
67 | if (checked) {
68 | link.show();
69 | } else {
70 | link.hide();
71 | }
72 | }}
73 | />
74 |
75 | );
76 | })}
77 |
78 |
79 | this.toggleCollapsed('jointsCollapsed')}
82 | >
83 |
84 |
85 | Joints
86 |
87 | {!jointsCollapsed &&
88 | joints &&
89 | map(keys(joints), (name, index) => {
90 | return ;
91 | })}
92 | >
93 | );
94 | }
95 | }
96 |
97 | export default RobotModelLinksJoints;
98 |
--------------------------------------------------------------------------------
/src/panels/sidebar/vizOptions/shape.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CONSTANTS } from 'amphion';
3 | import Arrow from './arrow';
4 | import FlatArrow from './flatArrow';
5 | import Axes from './axes';
6 |
7 | const {
8 | OBJECT_TYPE_ARROW,
9 | OBJECT_TYPE_AXES,
10 | OBJECT_TYPE_FLAT_ARROW,
11 | } = CONSTANTS;
12 |
13 | class ShapeOptions extends React.PureComponent {
14 | render() {
15 | const {
16 | options,
17 | options: { type: shapeType },
18 | updateOptions,
19 | } = this.props;
20 |
21 | switch (shapeType) {
22 | case OBJECT_TYPE_ARROW:
23 | return ;
24 | case OBJECT_TYPE_FLAT_ARROW:
25 | return ;
26 | case OBJECT_TYPE_AXES:
27 | return ;
28 | default:
29 | return null;
30 | }
31 | }
32 | }
33 |
34 | export default ShapeOptions;
35 |
--------------------------------------------------------------------------------
/src/panels/sidebar/vizOptions/vizSpecificOption.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CONSTANTS } from 'amphion';
3 | import LaserScanOptions from './laserScan';
4 | import MapOptions from './map';
5 | import MarkerOptions from './marker';
6 | import OdometryOptions from './odometry';
7 | import PathOptions from './path';
8 | import PoseOptions from './pose';
9 | import PointCloudOptions from './pointcloud';
10 | import RangeOptions from './range';
11 | import PointOptions from './point';
12 | import InteractiveMarkerOptions from './interactiveMarkerOptions';
13 | import WrenchOptions from './wrench';
14 | import RobotModelLinksJoints from './robotModel';
15 |
16 | const {
17 | VIZ_TYPE_IMAGE,
18 | VIZ_TYPE_INTERACTIVEMARKER,
19 | VIZ_TYPE_LASERSCAN,
20 | VIZ_TYPE_MAP,
21 | VIZ_TYPE_MARKER,
22 | VIZ_TYPE_MARKERARRAY,
23 | VIZ_TYPE_ODOMETRY,
24 | VIZ_TYPE_PATH,
25 | VIZ_TYPE_POINT,
26 | VIZ_TYPE_POINTCLOUD,
27 | VIZ_TYPE_POLYGON,
28 | VIZ_TYPE_POSE,
29 | VIZ_TYPE_POSEARRAY,
30 | VIZ_TYPE_RANGE,
31 | VIZ_TYPE_ROBOTMODEL,
32 | VIZ_TYPE_TF,
33 | VIZ_TYPE_WRENCH,
34 | } = CONSTANTS;
35 |
36 | const VizSpecificOptions = ({
37 | options: { vizType },
38 | options,
39 | topics,
40 | vizInstance,
41 | relatedTopics,
42 | updateVizOptions,
43 | }) => {
44 | switch (vizType) {
45 | case VIZ_TYPE_IMAGE:
46 | return null;
47 | case VIZ_TYPE_INTERACTIVEMARKER:
48 | return (
49 |
55 | );
56 | case VIZ_TYPE_LASERSCAN:
57 | return (
58 |
62 | );
63 | case VIZ_TYPE_MAP:
64 | return (
65 |
66 | );
67 | case VIZ_TYPE_MARKER:
68 | return (
69 |
70 | );
71 | case VIZ_TYPE_MARKERARRAY:
72 | return null;
73 | case VIZ_TYPE_ODOMETRY:
74 | return (
75 |
79 | );
80 | case VIZ_TYPE_PATH:
81 | return (
82 |
83 | );
84 | case VIZ_TYPE_POINT:
85 | return (
86 |
87 | );
88 | case VIZ_TYPE_POINTCLOUD:
89 | return (
90 |
94 | );
95 | case VIZ_TYPE_POLYGON:
96 | return null;
97 | case VIZ_TYPE_POSE:
98 | return (
99 |
100 | );
101 | case VIZ_TYPE_POSEARRAY:
102 | return null;
103 | case VIZ_TYPE_RANGE:
104 | return (
105 |
106 | );
107 | case VIZ_TYPE_ROBOTMODEL:
108 | return ;
109 | case VIZ_TYPE_TF:
110 | return null;
111 | case VIZ_TYPE_WRENCH:
112 | return (
113 |
114 | );
115 | default:
116 | return null;
117 | }
118 | };
119 |
120 | export default VizSpecificOptions;
121 |
--------------------------------------------------------------------------------
/src/panels/sidebar/vizOptions/wrench.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CONSTANTS } from 'amphion';
3 | import OptionRow from '../../../components/optionRow';
4 | import { updateOptionsUtil } from '../../../utils';
5 | import { Input } from '../../../components/styled';
6 |
7 | const { DEFAULT_OPTIONS_WRENCH } = CONSTANTS;
8 |
9 | class WrenchOptions extends React.PureComponent {
10 | constructor(props) {
11 | super(props);
12 | this.updateOptions = updateOptionsUtil.bind(this);
13 | }
14 |
15 | render() {
16 | const { options: propsOptions } = this.props;
17 | const {
18 | alpha,
19 | arrowWidth,
20 | forceArrowScale,
21 | forceColor,
22 | torqueArrowScale,
23 | torqueColor,
24 | } = {
25 | ...DEFAULT_OPTIONS_WRENCH,
26 | ...propsOptions,
27 | };
28 | return (
29 | <>
30 |
31 |
38 |
39 |
40 |
47 |
48 |
49 |
57 |
58 |
59 |
67 |
68 |
69 |
77 |
78 |
79 |
87 |
88 | >
89 | );
90 | }
91 | }
92 |
93 | export default WrenchOptions;
94 |
--------------------------------------------------------------------------------
/src/panels/sources/index.js:
--------------------------------------------------------------------------------
1 | import Amphion from 'amphion';
2 |
3 | const rosTopicDataSources = {};
4 |
5 | export const getOrCreateRosTopicDataSource = options => {
6 | const { topicName } = options;
7 | const existingSource = rosTopicDataSources[topicName];
8 | if (existingSource) {
9 | return existingSource;
10 | }
11 | rosTopicDataSources[existingSource] = new Amphion.RosTopicDataSource(options);
12 | return rosTopicDataSources[existingSource];
13 | };
14 |
--------------------------------------------------------------------------------
/src/panels/tools/index.jsx:
--------------------------------------------------------------------------------
1 | const Tools = () => {};
2 |
3 | export default Tools;
4 |
--------------------------------------------------------------------------------
/src/panels/viewer/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const StyledViewport = styled.div`
5 | width: 100%;
6 | height: 100%;
7 | position: relative;
8 |
9 | #viewportStats {
10 | position: absolute !important;
11 | top: auto !important;
12 | left: auto !important;
13 | right: 0 !important;
14 | bottom: 0 !important;
15 | }
16 | `;
17 |
18 | class Viewport extends React.PureComponent {
19 | constructor(props) {
20 | super(props);
21 | this.container = React.createRef();
22 |
23 | this.updateViewerOptions = this.updateViewerOptions.bind(this);
24 | }
25 |
26 | componentDidUpdate() {
27 | this.updateViewerOptions();
28 | }
29 |
30 | updateViewerOptions() {
31 | const {
32 | globalOptions: {
33 | backgroundColor: { value: backgroundColor },
34 | fixedFrame: { value: selectedFrame },
35 | grid: {
36 | centerlineColor: gridCenterlineColor,
37 | color: gridColor,
38 | divisions: gridDivisions,
39 | size: gridSize,
40 | },
41 | },
42 | viewer,
43 | } = this.props;
44 | viewer.updateOptions({
45 | backgroundColor,
46 | gridSize,
47 | gridDivisions,
48 | gridColor,
49 | gridCenterlineColor,
50 | selectedFrame,
51 | });
52 | }
53 |
54 | componentDidMount() {
55 | const { viewer } = this.props;
56 | const container = this.container.current;
57 | viewer.setContainer(container);
58 | this.updateViewerOptions();
59 | viewer.scene.stats.dom.id = 'viewportStats';
60 | container.appendChild(viewer.scene.stats.dom);
61 | }
62 |
63 | componentWillUnmount() {
64 | const { viewer } = this.props;
65 | viewer.destroy();
66 | }
67 |
68 | render() {
69 | return ;
70 | }
71 | }
72 |
73 | export default Viewport;
74 |
--------------------------------------------------------------------------------
/src/utils/common.js:
--------------------------------------------------------------------------------
1 | export const iconLineStyle = {
2 | fill: 'none',
3 | stroke: '#dc1d30',
4 | strokeLinecap: 'round',
5 | strokeLinejoin: 'round',
6 | strokeWidth: '1px',
7 | };
8 |
9 | export const iconFillStyle = {
10 | fill: '#dc1d30',
11 | };
12 |
13 | export const TOOL_TYPE_CONTROLS = 'TOOL_TYPE_CONTROLS';
14 | export const TOOL_TYPE_POINT = 'TOOL_TYPE_POINT';
15 | export const TOOL_TYPE_NAV_GOAL = 'TOOL_TYPE_NAV_GOAL';
16 | export const TOOL_TYPE_POSE_ESTIMATE = 'TOOL_TYPE_POSE_ESTIMATE';
17 |
18 | export const MESSAGE_TYPE_TOOL_POINT = 'geometry_msgs/PointStamped';
19 | export const MESSAGE_TYPE_TOOL_NAV_GOAL = 'geometry_msgs/PoseStamped';
20 | export const MESSAGE_TYPE_TOOL_POSE_ESTIMATE =
21 | 'geometry_msgs/PoseWithCovarianceStamped';
22 |
--------------------------------------------------------------------------------
/src/utils/constants.js:
--------------------------------------------------------------------------------
1 | const API_CALL_STATUS = {
2 | IDLE: 0,
3 | FETCHING: 1,
4 | SUCCESSFUL: 2,
5 | ERROR: 3,
6 | };
7 |
8 | export default API_CALL_STATUS;
9 |
--------------------------------------------------------------------------------
/src/utils/editorControls.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /**
3 | * @author qiao / https://github.com/qiao
4 | * @author mrdoob / http://mrdoob.com
5 | * @author alteredq / http://alteredqualia.com/
6 | * @author WestLangley / http://github.com/WestLangley
7 | */
8 |
9 | import {
10 | Box3,
11 | EventDispatcher,
12 | Matrix3,
13 | Sphere,
14 | Spherical,
15 | Vector2,
16 | Vector3,
17 | } from 'three/build/three.module';
18 |
19 | const EditorControls = function(object, domElement) {
20 | domElement = domElement !== undefined ? domElement : document;
21 |
22 | // API
23 |
24 | this.enabled = true;
25 | this.center = new Vector3();
26 | this.panSpeed = 0.002;
27 | this.zoomSpeed = 0.1;
28 | this.rotationSpeed = 0.005;
29 |
30 | // internals
31 |
32 | const scope = this;
33 | const vector = new Vector3();
34 | const delta = new Vector3();
35 | const box = new Box3();
36 |
37 | const STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2 };
38 | let state = STATE.NONE;
39 |
40 | const { center } = this;
41 | const normalMatrix = new Matrix3();
42 | const pointer = new Vector2();
43 | const pointerOld = new Vector2();
44 | const spherical = new Spherical();
45 | const sphere = new Sphere();
46 |
47 | // events
48 |
49 | const changeEvent = { type: 'change' };
50 |
51 | this.focus = function(target) {
52 | let distance;
53 |
54 | box.setFromObject(target);
55 |
56 | if (box.isEmpty() === false) {
57 | box.getCenter(center);
58 | distance = box.getBoundingSphere(sphere).radius;
59 | } else {
60 | // Focusing on an Group, AmbientLight, etc
61 |
62 | center.setFromMatrixPosition(target.matrixWorld);
63 | distance = 0.1;
64 | }
65 |
66 | delta.set(0, 0, 1);
67 | delta.applyQuaternion(object.quaternion);
68 | delta.multiplyScalar(distance * 4);
69 |
70 | object.position.copy(center).add(delta);
71 |
72 | scope.dispatchEvent(changeEvent);
73 | };
74 |
75 | this.pan = function(delta) {
76 | const distance = object.position.distanceTo(center);
77 |
78 | delta.multiplyScalar(distance * scope.panSpeed);
79 | delta.applyMatrix3(normalMatrix.getNormalMatrix(object.matrix));
80 |
81 | object.position.add(delta);
82 | center.add(delta);
83 |
84 | scope.dispatchEvent(changeEvent);
85 | };
86 |
87 | this.zoom = function(delta) {
88 | const distance = object.position.distanceTo(center);
89 |
90 | delta.multiplyScalar(distance * scope.zoomSpeed);
91 |
92 | if (delta.length() > distance) return;
93 |
94 | delta.applyMatrix3(normalMatrix.getNormalMatrix(object.matrix));
95 |
96 | object.position.add(delta);
97 |
98 | scope.dispatchEvent(changeEvent);
99 | };
100 |
101 | this.rotate = function(delta) {
102 | vector.copy(object.position).sub(center);
103 |
104 | // spherical.setFromVector3(vector);
105 | spherical.setFromCartesianCoords(-1 * vector.x, vector.z, vector.y);
106 |
107 | spherical.theta += delta.x * scope.rotationSpeed;
108 | spherical.phi += delta.y * scope.rotationSpeed;
109 |
110 | spherical.makeSafe();
111 |
112 | vector.setFromSpherical(spherical);
113 |
114 | object.position.copy(center).add(vector);
115 |
116 | object.lookAt(center);
117 |
118 | scope.dispatchEvent(changeEvent);
119 | };
120 |
121 | // mouse
122 |
123 | function onMouseDown(event) {
124 | if (scope.enabled === false) return;
125 |
126 | if (event.button === 0) {
127 | state = STATE.ROTATE;
128 | } else if (event.button === 1) {
129 | state = STATE.ZOOM;
130 | } else if (event.button === 2) {
131 | state = STATE.PAN;
132 | }
133 |
134 | pointerOld.set(event.clientX, event.clientY);
135 |
136 | domElement.addEventListener('mousemove', onMouseMove, false);
137 | domElement.addEventListener('mouseup', onMouseUp, false);
138 | domElement.addEventListener('mouseout', onMouseUp, false);
139 | domElement.addEventListener('dblclick', onMouseUp, false);
140 | }
141 |
142 | function onMouseMove(event) {
143 | if (scope.enabled === false) return;
144 |
145 | pointer.set(event.clientX, event.clientY);
146 |
147 | const movementX = pointer.x - pointerOld.x;
148 | const movementY = pointer.y - pointerOld.y;
149 |
150 | if (state === STATE.ROTATE) {
151 | scope.rotate(delta.set(-movementX, -movementY, 0));
152 | } else if (state === STATE.ZOOM) {
153 | scope.zoom(delta.set(0, 0, movementY));
154 | } else if (state === STATE.PAN) {
155 | scope.pan(delta.set(-movementX, movementY, 0));
156 | }
157 |
158 | pointerOld.set(event.clientX, event.clientY);
159 | }
160 |
161 | function onMouseUp(event) {
162 | domElement.removeEventListener('mousemove', onMouseMove, false);
163 | domElement.removeEventListener('mouseup', onMouseUp, false);
164 | domElement.removeEventListener('mouseout', onMouseUp, false);
165 | domElement.removeEventListener('dblclick', onMouseUp, false);
166 |
167 | state = STATE.NONE;
168 | }
169 |
170 | function onMouseWheel(event) {
171 | event.preventDefault();
172 |
173 | // Normalize deltaY due to https://bugzilla.mozilla.org/show_bug.cgi?id=1392460
174 | scope.zoom(delta.set(0, 0, event.deltaY > 0 ? 1 : -1));
175 | }
176 |
177 | function contextmenu(event) {
178 | event.preventDefault();
179 | }
180 |
181 | this.dispose = function() {
182 | domElement.removeEventListener('contextmenu', contextmenu, false);
183 | domElement.removeEventListener('mousedown', onMouseDown, false);
184 | domElement.removeEventListener('wheel', onMouseWheel, false);
185 |
186 | domElement.removeEventListener('mousemove', onMouseMove, false);
187 | domElement.removeEventListener('mouseup', onMouseUp, false);
188 | domElement.removeEventListener('mouseout', onMouseUp, false);
189 | domElement.removeEventListener('dblclick', onMouseUp, false);
190 |
191 | domElement.removeEventListener('touchstart', touchStart, false);
192 | domElement.removeEventListener('touchmove', touchMove, false);
193 | };
194 |
195 | domElement.addEventListener('contextmenu', contextmenu, false);
196 | domElement.addEventListener('mousedown', onMouseDown, false);
197 | domElement.addEventListener('wheel', onMouseWheel, false);
198 |
199 | // touch
200 |
201 | const touches = [new Vector3(), new Vector3(), new Vector3()];
202 | const prevTouches = [new Vector3(), new Vector3(), new Vector3()];
203 |
204 | let prevDistance = null;
205 |
206 | function touchStart(event) {
207 | if (scope.enabled === false) return;
208 |
209 | switch (event.touches.length) {
210 | case 1:
211 | touches[0]
212 | .set(event.touches[0].pageX, event.touches[0].pageY, 0)
213 | .divideScalar(window.devicePixelRatio);
214 | touches[1]
215 | .set(event.touches[0].pageX, event.touches[0].pageY, 0)
216 | .divideScalar(window.devicePixelRatio);
217 | break;
218 |
219 | case 2:
220 | touches[0]
221 | .set(event.touches[0].pageX, event.touches[0].pageY, 0)
222 | .divideScalar(window.devicePixelRatio);
223 | touches[1]
224 | .set(event.touches[1].pageX, event.touches[1].pageY, 0)
225 | .divideScalar(window.devicePixelRatio);
226 | prevDistance = touches[0].distanceTo(touches[1]);
227 | break;
228 | }
229 |
230 | prevTouches[0].copy(touches[0]);
231 | prevTouches[1].copy(touches[1]);
232 | }
233 |
234 | function touchMove(event) {
235 | if (scope.enabled === false) return;
236 |
237 | event.preventDefault();
238 | event.stopPropagation();
239 |
240 | function getClosest(touch, touches) {
241 | let closest = touches[0];
242 |
243 | for (const i in touches) {
244 | if (closest.distanceTo(touch) > touches[i].distanceTo(touch))
245 | closest = touches[i];
246 | }
247 |
248 | return closest;
249 | }
250 |
251 | switch (event.touches.length) {
252 | case 1:
253 | touches[0]
254 | .set(event.touches[0].pageX, event.touches[0].pageY, 0)
255 | .divideScalar(window.devicePixelRatio);
256 | touches[1]
257 | .set(event.touches[0].pageX, event.touches[0].pageY, 0)
258 | .divideScalar(window.devicePixelRatio);
259 | scope.rotate(
260 | touches[0]
261 | .sub(getClosest(touches[0], prevTouches))
262 | .multiplyScalar(-1),
263 | );
264 | break;
265 |
266 | case 2:
267 | touches[0]
268 | .set(event.touches[0].pageX, event.touches[0].pageY, 0)
269 | .divideScalar(window.devicePixelRatio);
270 | touches[1]
271 | .set(event.touches[1].pageX, event.touches[1].pageY, 0)
272 | .divideScalar(window.devicePixelRatio);
273 | var distance = touches[0].distanceTo(touches[1]);
274 | scope.zoom(delta.set(0, 0, prevDistance - distance));
275 | prevDistance = distance;
276 |
277 | var offset0 = touches[0]
278 | .clone()
279 | .sub(getClosest(touches[0], prevTouches));
280 | var offset1 = touches[1]
281 | .clone()
282 | .sub(getClosest(touches[1], prevTouches));
283 | offset0.x = -offset0.x;
284 | offset1.x = -offset1.x;
285 |
286 | scope.pan(offset0.add(offset1));
287 |
288 | break;
289 | }
290 |
291 | prevTouches[0].copy(touches[0]);
292 | prevTouches[1].copy(touches[1]);
293 | }
294 |
295 | domElement.addEventListener('touchstart', touchStart, false);
296 | domElement.addEventListener('touchmove', touchMove, false);
297 | };
298 |
299 | EditorControls.prototype = Object.create(EventDispatcher.prototype);
300 | EditorControls.prototype.constructor = EditorControls;
301 |
302 | export { EditorControls };
303 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import { CONSTANTS } from 'amphion';
2 | import _ from 'lodash';
3 | import { TF_MESSAGE_TYPES } from './vizOptions';
4 |
5 | const { DEFAULT_OPTIONS_SCENE } = CONSTANTS;
6 |
7 | export const ROS_SOCKET_STATUSES = {
8 | INITIAL: 'Not Connected',
9 | CONNECTING: 'Connecting',
10 | CONNECTED: 'Connected',
11 | CONNECTION_ERROR: 'Error in connection',
12 | };
13 |
14 | export const getTfTopics = rosTopics =>
15 | _.filter(rosTopics, t => _.includes(TF_MESSAGE_TYPES, t.messageType));
16 |
17 | export const stopPropagation = e => e.stopPropagation();
18 |
19 | export const downloadFile = (content, filename, options = {}) => {
20 | const element = document.createElement('a');
21 | element.setAttribute(
22 | 'href',
23 | `data:${options.mimetype || 'text/json'};charset=utf-8,${encodeURIComponent(
24 | content,
25 | )}`,
26 | );
27 | element.setAttribute('download', filename);
28 | element.style.display = 'none';
29 | document.body.appendChild(element);
30 | element.click();
31 | document.body.removeChild(element);
32 | };
33 |
34 | const getURLEndpoint = type => {
35 | const urlSearchParams = new URLSearchParams(window.location.search);
36 | const urlParams = Object.fromEntries(urlSearchParams.entries());
37 |
38 | if (type === 'bridge') {
39 | urlParams.bridge;
40 | }
41 | if (type === 'pkgs') {
42 | urlParams.pkgs;
43 | }
44 | return '';
45 | };
46 |
47 | export const DEFAULT_CONFIG = {
48 | panels: {
49 | sidebar: {
50 | display: true,
51 | collapsed: false,
52 | },
53 | header: {
54 | display: true,
55 | },
56 | info: {
57 | display: true,
58 | collapsed: true,
59 | },
60 | },
61 | ros: {
62 | endpoint:
63 | getURLEndpoint('bridge') || `ws://${window.location.host}/ros/bridge`,
64 | pkgsEndpoint:
65 | getURLEndpoint('pkgs') || `http://${window.location.host}/ros/pkgs`,
66 | },
67 | infoTabs: [],
68 | visualizations: [],
69 | globalOptions: {
70 | display: true,
71 | backgroundColor: {
72 | display: true,
73 | value: DEFAULT_OPTIONS_SCENE.backgroundColor,
74 | },
75 | fixedFrame: {
76 | display: true,
77 | value: 'world',
78 | },
79 | grid: {
80 | display: true,
81 | size: DEFAULT_OPTIONS_SCENE.gridSize,
82 | divisions: DEFAULT_OPTIONS_SCENE.gridDivisions,
83 | color: DEFAULT_OPTIONS_SCENE.gridColor,
84 | centerlineColor: DEFAULT_OPTIONS_SCENE.gridCenterlineColor,
85 | },
86 | },
87 | tools: {
88 | mode: 'controls',
89 | controls: {
90 | display: false,
91 | enabled: true,
92 | },
93 | measure: {
94 | display: false,
95 | },
96 | custom: [],
97 | },
98 | };
99 |
100 | export function updateOptionsUtil(e) {
101 | const {
102 | options: { key },
103 | updateVizOptions,
104 | } = this.props;
105 | const {
106 | checked,
107 | dataset: { id: optionId },
108 | value,
109 | } = e.target;
110 | updateVizOptions(key, {
111 | [optionId]: _.has(e.target, 'checked') ? checked : value,
112 | });
113 | }
114 |
115 | export function promisifyGetNodeDetails(ros, node) {
116 | return new Promise(function(res, rej) {
117 | try {
118 | ros.getNodeDetails(node, function({ publishing, subscribing }) {
119 | res({ publishing, subscribing, node });
120 | });
121 | } catch (err) {
122 | rej(err);
123 | }
124 | });
125 | }
126 |
127 | /**
128 | *
129 | * @param {Array} topics - a list of topics
130 | * @param {Object} nodeDetails - List of node details with node name, publishing topics and subsribing topics
131 | * @returns {auxGraphData} - For creating graph later based on options.
132 | */
133 | export function createAuxGraph(topics, nodeDetails) {
134 | const auxGraphData = {};
135 |
136 | topics.forEach(topic => {
137 | auxGraphData[topic] = { publishers: [], subscribers: [] };
138 | });
139 | nodeDetails.forEach(function({ publishing: pubs, subscribing: subs, node }) {
140 | pubs.forEach(topic => {
141 | auxGraphData[topic].publishers.push(node);
142 | });
143 | subs.forEach(topic => {
144 | auxGraphData[topic].subscribers.push(node);
145 | });
146 | });
147 |
148 | return auxGraphData;
149 | }
150 |
151 | export function defaultGraph(graph) {
152 | const edges = [];
153 | const { auxGraphData } = graph;
154 | _.each(_.keys(auxGraphData), t => {
155 | const { publishers } = auxGraphData[t];
156 | const { subscribers } = auxGraphData[t];
157 |
158 | _.each(publishers, pub => {
159 | _.each(subscribers, sub => {
160 | edges.push({
161 | source: { id: pub, label: pub },
162 | target: { id: sub, label: sub },
163 | value: t,
164 | });
165 | });
166 | });
167 | });
168 | return { nodes: graph.nodes, edges };
169 | }
170 |
171 | export function graphWithTopicNodes(graph) {
172 | const newNodes = [...graph.nodes];
173 | const edges = [];
174 | const { auxGraphData } = graph;
175 | // Adding topic as nodes
176 | _.keys(graph.auxGraphData).forEach(topicName => {
177 | newNodes.push({
178 | id: topicName + topicName,
179 | label: topicName,
180 | type: 'rect',
181 | });
182 | });
183 |
184 | _.each(_.keys(auxGraphData), t => {
185 | const { publishers } = auxGraphData[t];
186 | const { subscribers } = auxGraphData[t];
187 |
188 | _.each(publishers, pub => {
189 | edges.push({
190 | source: { id: pub, label: pub },
191 | target: { id: t + t, label: t },
192 | value: '',
193 | });
194 | });
195 |
196 | _.each(subscribers, sub => {
197 | edges.push({
198 | source: { id: t + t, label: t },
199 | target: { id: sub, label: sub },
200 | value: '',
201 | });
202 | });
203 | });
204 |
205 | return { nodes: newNodes, edges };
206 | }
207 |
208 | /**
209 | *
210 | * @param {*} ros - Ros reference
211 | * @returns {Promise} - graph object represents nodes and links as edges.
212 | */
213 | export function generateGraph(ros) {
214 | const graph = {};
215 |
216 | return new Promise(function(res, rej) {
217 | ros.getNodes(nodes => {
218 | graph.nodes = _.map(nodes, node => ({
219 | id: node,
220 | label: node,
221 | type: 'ellipse',
222 | }));
223 |
224 | ros.getTopics(function({ topics }) {
225 | Promise.all(
226 | nodes.map(function(node) {
227 | return promisifyGetNodeDetails(ros, node);
228 | }),
229 | )
230 | .then(function(data) {
231 | graph.auxGraphData = createAuxGraph(topics, data);
232 | res(graph);
233 | })
234 | .catch(function(err) {
235 | rej(err);
236 | });
237 | });
238 | });
239 | });
240 | }
241 |
--------------------------------------------------------------------------------
/src/utils/raycaster.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 | import { TOOL_TYPE } from './toolbar';
3 |
4 | export default class Raycaster extends THREE.Raycaster {
5 | constructor(camera, scene, domElement) {
6 | super();
7 | this.fixedFrame = 'base_link';
8 | this.mouse = new THREE.Vector2();
9 | this.camera = camera;
10 | this.scene = scene;
11 | this.domElement = domElement;
12 | this.activePlane = new THREE.Plane();
13 | this.intersection = new THREE.Vector3();
14 | this.mouseDown = new THREE.Vector3();
15 | this.tool = { name: 'Controls', type: TOOL_TYPE.TOOL_TYPE_CONTROLS };
16 | this.eventListeners = {};
17 | this.arrowHelper = new THREE.ArrowHelper(
18 | new THREE.Vector3(0, 1, 0),
19 | this.scene.position,
20 | 1,
21 | 0xffff00,
22 | 0.2,
23 | 0.2,
24 | );
25 | this.dirv1Cache = new THREE.Vector3(0, 1, 0);
26 | this.dirv2Cache = new THREE.Vector3();
27 | this.quaternionCache = new THREE.Quaternion();
28 | this.arrowHelper.line.material.linewidth = 2;
29 |
30 | this.addOrReplaceEventListener = this.addOrReplaceEventListener.bind(this);
31 | this.mouseUpListener = this.mouseUpListener.bind(this);
32 | this.mouseMoveListener = this.mouseMoveListener.bind(this);
33 | this.mouseDownListener = this.mouseDownListener.bind(this);
34 | this.translateToFixedFrame = this.translateToFixedFrame.bind(this);
35 |
36 | this.domElement.addEventListener('mouseup', this.mouseUpListener, false);
37 | this.domElement.addEventListener(
38 | 'mousedown',
39 | this.mouseDownListener,
40 | false,
41 | );
42 | }
43 |
44 | addOrReplaceEventListener(name, cb) {
45 | this.eventListeners[name] = cb;
46 | }
47 |
48 | setRayDirection(event) {
49 | const rect = this.domElement.getBoundingClientRect();
50 | const { clientHeight, clientWidth } = this.domElement;
51 | this.mouse.x = ((event.clientX - rect.left) / clientWidth) * 2 - 1;
52 | this.mouse.y = -((event.clientY - rect.top) / clientHeight) * 2 + 1;
53 | this.setFromCamera(this.mouse, this.camera);
54 | }
55 |
56 | mouseDownListener(event) {
57 | this.setRayDirection(event);
58 | this.activePlane.setFromNormalAndCoplanarPoint(
59 | this.camera.up,
60 | this.scene.position,
61 | );
62 | this.ray.intersectPlane(this.activePlane, this.intersection);
63 | if (!(this.intersection && this.eventListeners[this.tool.name])) {
64 | return;
65 | }
66 | this.translateToFixedFrame(this.intersection);
67 | this.mouseDown.copy(this.intersection);
68 |
69 | switch (this.tool.type) {
70 | case TOOL_TYPE.TOOL_TYPE_POSE_ESTIMATE:
71 | case TOOL_TYPE.TOOL_TYPE_NAV_GOAL: {
72 | this.arrowHelper.position.copy(this.intersection);
73 | this.arrowHelper.quaternion.set(0, 0, 0, 1);
74 | this.scene.add(this.arrowHelper);
75 | this.domElement.addEventListener(
76 | 'mousemove',
77 | this.mouseMoveListener,
78 | false,
79 | );
80 | break;
81 | }
82 | default:
83 | }
84 | }
85 |
86 | mouseMoveListener(event) {
87 | this.setRayDirection(event);
88 | this.ray.intersectPlane(this.activePlane, this.intersection);
89 | if (!(this.intersection && this.eventListeners[this.tool.name])) {
90 | return;
91 | }
92 | this.translateToFixedFrame(this.intersection);
93 | this.dirv2Cache
94 | .copy(this.intersection)
95 | .sub(this.mouseDown)
96 | .normalize();
97 | this.quaternionCache.setFromUnitVectors(this.dirv1Cache, this.dirv2Cache);
98 |
99 | this.arrowHelper.quaternion.copy(this.quaternionCache);
100 | }
101 |
102 | mouseUpListener() {
103 | this.domElement.removeEventListener(
104 | 'mousemove',
105 | this.mouseMoveListener,
106 | false,
107 | );
108 | this.scene.remove(this.arrowHelper);
109 |
110 | switch (this.tool.type) {
111 | case TOOL_TYPE.TOOL_TYPE_POINT: {
112 | this.eventListeners[this.tool.name](this.intersection, this.fixedFrame);
113 | break;
114 | }
115 | case TOOL_TYPE.TOOL_TYPE_POSE_ESTIMATE:
116 | case TOOL_TYPE.TOOL_TYPE_NAV_GOAL: {
117 | const { position, quaternion } = this.arrowHelper;
118 | const quaternionTransform = new THREE.Quaternion().setFromAxisAngle(
119 | this.camera.up,
120 | Math.PI / 2,
121 | );
122 | quaternion.premultiply(quaternionTransform);
123 | this.eventListeners[this.tool.name](
124 | position,
125 | quaternion,
126 | this.fixedFrame,
127 | );
128 | break;
129 | }
130 | case TOOL_TYPE.TOOL_TYPE_CONTROLS:
131 | default:
132 | }
133 | }
134 |
135 | translateToFixedFrame(point) {
136 | const frame = this.scene.getObjectByName(this.fixedFrame);
137 | if (frame) {
138 | frame.worldToLocal(point);
139 | }
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/utils/sanitize.js:
--------------------------------------------------------------------------------
1 | import { get, isNil, map, set, size } from 'lodash';
2 |
3 | const replacementMap = {
4 | 'sensor_msgs/Image': {
5 | key: 'data',
6 | },
7 | 'visualization_msgs/MarkerArray': {
8 | key: 'markers',
9 | validation: it => size(it) <= 1000,
10 | },
11 | 'nav_msgs/OccupancyGrid': {
12 | key: 'data',
13 | },
14 | 'nav_msgs/Path': {
15 | key: 'poses',
16 | validation: it => size(it) <= 1000,
17 | },
18 | 'sensor_msgs/PointCloud': {
19 | key: 'points',
20 | },
21 | 'sensor_msgs/PointCloud2': {
22 | key: 'data',
23 | },
24 | 'geometry_msgs/Polygon': {
25 | key: 'points',
26 | },
27 | 'geometry_msgs/PolygonStamped': {
28 | key: 'polygon.points',
29 | },
30 | 'geometry_msgs/PoseArray': {
31 | key: 'poses',
32 | validation: it => size(it) <= 1000,
33 | },
34 | };
35 |
36 | export const sanitizeMessage = (topic, message) => {
37 | // message is mutated for speed
38 | const { keys, messageType } = topic;
39 | const keysSet = new Set(keys);
40 | const filteredMessage = message;
41 | if (size(keys) !== 0) {
42 | map(filteredMessage, (value, key) => {
43 | if (!keysSet.has(key)) {
44 | delete filteredMessage[key];
45 | }
46 | });
47 | }
48 | if (isNil(replacementMap[messageType])) {
49 | return filteredMessage;
50 | }
51 | const { key, validation } = replacementMap[messageType];
52 | const value = get(filteredMessage, key);
53 | if (validation && value && validation(value)) {
54 | return filteredMessage;
55 | }
56 | // only set if already not filtered
57 | if (value) {
58 | set(message, key, '...');
59 | }
60 | return message;
61 | };
62 |
--------------------------------------------------------------------------------
/src/utils/toolPublisher.js:
--------------------------------------------------------------------------------
1 | import ROSLIB from 'roslib';
2 | import {
3 | TOOL_TYPE_NAV_GOAL,
4 | TOOL_TYPE_POINT,
5 | TOOL_TYPE_POSE_ESTIMATE,
6 | } from './common';
7 |
8 | export default class ToolPublisher {
9 | constructor(ros) {
10 | this.ros = ros;
11 | this.seq = {
12 | [TOOL_TYPE_POINT]: 0,
13 | [TOOL_TYPE_NAV_GOAL]: 0,
14 | [TOOL_TYPE_POSE_ESTIMATE]: 0,
15 | };
16 |
17 | this.pointToolPublisher = new ROSLIB.Topic({
18 | ros: this.ros,
19 | name: '/clicked_point',
20 | messageType: 'geometry_msgs/PointStamped',
21 | });
22 | this.navGoalToolPublisher = new ROSLIB.Topic({
23 | ros: this.ros,
24 | name: '/move_base_simple/goal',
25 | messageType: 'geometry_msgs/PoseStamped',
26 | });
27 | this.poseEstimateToolPublisher = new ROSLIB.Topic({
28 | ros: this.ros,
29 | name: '/initialpose',
30 | messageType: 'geometry_msgs/PoseWithCovarianceStamped',
31 | });
32 |
33 | this.pointToolPublisher.advertise();
34 | this.navGoalToolPublisher.advertise();
35 | this.poseEstimateToolPublisher.advertise();
36 |
37 | this.publishPointToolMessage = this.publishPointToolMessage.bind(this);
38 | this.publishNavGoalToolMessage = this.publishNavGoalToolMessage.bind(this);
39 | this.publishPoseEstimateToolMessage = this.publishPoseEstimateToolMessage.bind(
40 | this,
41 | );
42 | }
43 |
44 | publishPointToolMessage(point, frameId) {
45 | const message = new ROSLIB.Message({
46 | header: {
47 | seq: this.seq[TOOL_TYPE_POINT],
48 | stamp: {
49 | secs: Math.floor(Date.now() / 1000),
50 | nsecs: 0,
51 | },
52 | frame_id: frameId,
53 | },
54 | point,
55 | });
56 | this.pointToolPublisher.publish(message);
57 | this.seq[TOOL_TYPE_POINT]++;
58 | }
59 |
60 | publishNavGoalToolMessage(pose, frameId) {
61 | const message = new ROSLIB.Message({
62 | header: {
63 | seq: this.seq[TOOL_TYPE_NAV_GOAL],
64 | stamp: {
65 | secs: Math.floor(Date.now() / 1000),
66 | nsecs: 0,
67 | },
68 | frame_id: frameId,
69 | },
70 | pose,
71 | });
72 | this.navGoalToolPublisher.publish(message);
73 | this.seq[TOOL_TYPE_NAV_GOAL]++;
74 | }
75 |
76 | publishPoseEstimateToolMessage(pose, frameId) {
77 | // covariance being published here is meaningless
78 | // but we keep the same covariance as rviz does
79 | // for compatibility
80 | const covariance = new Array(36).fill(0);
81 | covariance[0] = 0.5 * 0.5;
82 | covariance[6 + 1] = 0.5 * 0.5;
83 | covariance[6 * 6 - 1] = ((Math.PI / 12.0) * Math.PI) / 12.0;
84 |
85 | const message = new ROSLIB.Message({
86 | header: {
87 | seq: this.seq[TOOL_TYPE_POSE_ESTIMATE],
88 | stamp: {
89 | secs: Math.floor(Date.now() / 1000),
90 | nsecs: 0,
91 | },
92 | frame_id: frameId,
93 | },
94 | pose: {
95 | pose,
96 | covariance,
97 | },
98 | });
99 | this.poseEstimateToolPublisher.publish(message);
100 | this.seq[TOOL_TYPE_POSE_ESTIMATE]++;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/utils/toolbar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | iconLineStyle,
4 | MESSAGE_TYPE_TOOL_NAV_GOAL,
5 | MESSAGE_TYPE_TOOL_POINT,
6 | MESSAGE_TYPE_TOOL_POSE_ESTIMATE,
7 | TOOL_TYPE_CONTROLS,
8 | TOOL_TYPE_NAV_GOAL,
9 | TOOL_TYPE_POINT,
10 | TOOL_TYPE_POSE_ESTIMATE,
11 | } from './common';
12 |
13 | const activeStyle = {
14 | ...iconLineStyle,
15 | strokeWidth: '1px',
16 | };
17 | const inactiveStyle = {
18 | ...activeStyle,
19 | stroke: '#000000',
20 | };
21 |
22 | export const TOOL_TYPE = {
23 | [TOOL_TYPE_CONTROLS]: TOOL_TYPE_CONTROLS,
24 | [TOOL_TYPE_POINT]: TOOL_TYPE_POINT,
25 | [TOOL_TYPE_NAV_GOAL]: TOOL_TYPE_NAV_GOAL,
26 | [TOOL_TYPE_POSE_ESTIMATE]: TOOL_TYPE_POSE_ESTIMATE,
27 | };
28 |
29 | export const toolOptions = [
30 | {
31 | name: 'Controls',
32 | type: TOOL_TYPE_CONTROLS,
33 | icon: active => (
34 |
42 | ),
43 | },
44 | {
45 | name: 'Pose Estimate',
46 | type: TOOL_TYPE_POSE_ESTIMATE,
47 | icon: active => (
48 |
63 | ),
64 | messageType: MESSAGE_TYPE_TOOL_POSE_ESTIMATE,
65 | },
66 | {
67 | name: 'Nav Goal',
68 | type: TOOL_TYPE_NAV_GOAL,
69 | icon: active => (
70 |
85 | ),
86 | messageType: MESSAGE_TYPE_TOOL_NAV_GOAL,
87 | },
88 | {
89 | name: 'Point',
90 | type: TOOL_TYPE_POINT,
91 | icon: active => (
92 |
100 | ),
101 | messageType: MESSAGE_TYPE_TOOL_POINT,
102 | },
103 | ];
104 |
--------------------------------------------------------------------------------
/src/zethus.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 | import shortid from 'shortid';
4 | import withGracefulUnmount from 'react-graceful-unmount';
5 | import store from 'store';
6 |
7 | import Panels from './panels';
8 |
9 | import { DEFAULT_CONFIG } from './utils';
10 | import ErrorBoundary from './components/errorBoundary';
11 |
12 | class Zethus extends React.Component {
13 | constructor(props) {
14 | super(props);
15 |
16 | const urlSearchParams = new URLSearchParams(window.location.search);
17 | const urlParams = Object.fromEntries(urlSearchParams.entries());
18 | const urlConfig = urlParams.config
19 | ? JSON.parse(urlParams.config)
20 | : undefined;
21 | this.zethusId = urlParams.zethusId ? urlParams.zethusId : undefined;
22 |
23 | const providedConfig =
24 | props.configuration || urlConfig || store.get('zethus_config') || {};
25 |
26 | // Empty object is required or the merge function mutates default config
27 | window.document.addEventListener('SetConfig', e => {
28 | this.updateConfiguration(e.config, e.replaceOnExisting || false);
29 | });
30 |
31 | this.state = {
32 | configuration: _.merge({}, DEFAULT_CONFIG, providedConfig),
33 | };
34 | this.updateVizOptions = this.updateVizOptions.bind(this);
35 | this.updateRosEndpoint = this.updateRosEndpoint.bind(this);
36 | this.updateGlobalOptions = this.updateGlobalOptions.bind(this);
37 | this.addVisualization = this.addVisualization.bind(this);
38 | this.removeVisualization = this.removeVisualization.bind(this);
39 | this.toggleVisibility = this.toggleVisibility.bind(this);
40 | this.updateConfiguration = this.updateConfiguration.bind(this);
41 | this.resetReload = this.resetReload.bind(this);
42 | }
43 |
44 | updateConfiguration(configuration, replaceOnExisting) {
45 | const { configuration: oldConfiguration } = this.state;
46 | let newConfiguration;
47 | if (replaceOnExisting) {
48 | newConfiguration = {
49 | ...oldConfiguration,
50 | ...configuration,
51 | };
52 | } else {
53 | newConfiguration = {
54 | ..._.merge(oldConfiguration, configuration),
55 | };
56 | }
57 |
58 | if (window.parent && this.zethusId) {
59 | const event = new CustomEvent(`ZethusUpdateConfig${this.zethusId}`, {
60 | detail: { config: newConfiguration },
61 | });
62 | window.parent.document.dispatchEvent(event);
63 | }
64 |
65 | this.setState({ configuration: newConfiguration });
66 | }
67 |
68 | updateVizOptions(key, options) {
69 | const {
70 | configuration: { visualizations },
71 | } = this.state;
72 | this.updateConfiguration(
73 | {
74 | visualizations: _.map(visualizations, v =>
75 | v.key === key ? { ...v, ...options } : v,
76 | ),
77 | },
78 | true,
79 | );
80 | }
81 |
82 | updateRosEndpoint(endpoint) {
83 | const {
84 | configuration: { ros },
85 | } = this.state;
86 | this.updateConfiguration({
87 | ros: {
88 | ...ros,
89 | endpoint,
90 | },
91 | });
92 | }
93 |
94 | componentWillUnmount() {
95 | const { configuration } = this.state;
96 | store.set('zethus_config', configuration);
97 | }
98 |
99 | updateGlobalOptions(path, option) {
100 | const {
101 | configuration: { globalOptions },
102 | } = this.state;
103 | const clonedGlobalOptions = _.cloneDeep(globalOptions);
104 | _.set(clonedGlobalOptions, path, option);
105 | this.updateConfiguration(
106 | {
107 | globalOptions: clonedGlobalOptions,
108 | },
109 | true,
110 | );
111 | }
112 |
113 | removeVisualization(e) {
114 | const {
115 | dataset: { id: vizId },
116 | } = e.target;
117 | const {
118 | configuration: { visualizations },
119 | } = this.state;
120 | this.updateConfiguration(
121 | {
122 | visualizations: _.filter(visualizations, v => v.key !== vizId),
123 | },
124 | true,
125 | );
126 | }
127 |
128 | toggleVisibility(e) {
129 | const {
130 | dataset: { id: vizId },
131 | } = e.target;
132 | const {
133 | configuration: { visualizations },
134 | } = this.state;
135 | this.updateConfiguration(
136 | {
137 | visualizations: _.map(visualizations, v =>
138 | v.key === vizId
139 | ? {
140 | ...v,
141 | visible: !!(_.isBoolean(v.visible) && !v.visible),
142 | }
143 | : v,
144 | ),
145 | },
146 | true,
147 | );
148 | }
149 |
150 | resetReload() {
151 | this.setState(
152 | {
153 | configuration: DEFAULT_CONFIG,
154 | },
155 | () => {
156 | window.location.reload();
157 | },
158 | );
159 | }
160 |
161 | addVisualization(vizOptions) {
162 | const {
163 | configuration: { visualizations },
164 | } = this.state;
165 | this.updateConfiguration({
166 | visualizations: [
167 | ...visualizations,
168 | {
169 | ...vizOptions,
170 | key: shortid.generate(),
171 | },
172 | ],
173 | });
174 | }
175 |
176 | render() {
177 | const { configuration } = this.state;
178 | return (
179 |
183 |
193 |
194 | );
195 | }
196 | }
197 |
198 | export default withGracefulUnmount(Zethus);
199 |
--------------------------------------------------------------------------------
/webpack.app.dev.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const CopyPlugin = require('copy-webpack-plugin');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
5 |
6 | module.exports = {
7 | entry: './src/index.jsx',
8 | mode: 'development',
9 | plugins: [
10 | new HtmlWebpackPlugin({
11 | template: './public/index.html',
12 | filename: 'index.html',
13 | templateParameters: {
14 | PUBLIC_URL: '',
15 | },
16 | }),
17 | new CopyPlugin([{ from: './public' }]),
18 | new CleanWebpackPlugin(),
19 | ],
20 | module: {
21 | rules: [
22 | {
23 | test: /\.css$/i,
24 | use: ['style-loader', 'css-loader'],
25 | },
26 | {
27 | test: /\.s[ac]ss$/i,
28 | use: ['style-loader', 'css-loader', 'sass-loader'],
29 | },
30 | {
31 | test: /\.(png|jpe?g|gif|svg)$/i,
32 | use: [
33 | {
34 | loader: 'file-loader',
35 | },
36 | ],
37 | },
38 | {
39 | test: /\.(js|jsx)$/i,
40 | exclude: /node_modules/,
41 | loader: 'babel-loader',
42 | options: {
43 | presets: ['@babel/preset-env'],
44 | },
45 | },
46 | ],
47 | },
48 | resolve: {
49 | extensions: ['.js', '.jsx'],
50 | alias: {
51 | three: path.resolve('./node_modules/three'),
52 | },
53 | },
54 | devServer: {
55 | compress: true,
56 | hot: true,
57 | port: 3000,
58 | quiet: false,
59 | noInfo: false,
60 | stats: {
61 | assets: false,
62 | children: false,
63 | chunks: false,
64 | chunkModules: false,
65 | colors: true,
66 | entrypoints: false,
67 | hash: false,
68 | modules: false,
69 | timings: false,
70 | version: false,
71 | },
72 | },
73 | };
74 |
--------------------------------------------------------------------------------
/webpack.app.prod.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const CopyPlugin = require('copy-webpack-plugin');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
6 |
7 | const PUBLIC_URL = require('./package').homepage;
8 |
9 | module.exports = {
10 | entry: './src/index.jsx',
11 | mode: 'production',
12 | devtool: 'source-map',
13 | optimization: {
14 | splitChunks: {
15 | chunks: 'async',
16 | minSize: 30000,
17 | maxSize: 0,
18 | minChunks: 1,
19 | maxAsyncRequests: 5,
20 | maxInitialRequests: 3,
21 | automaticNameDelimiter: '~',
22 | automaticNameMaxLength: 30,
23 | name: true,
24 | cacheGroups: {
25 | vendors: {
26 | test: /[\\/]node_modules[\\/]/,
27 | priority: -10,
28 | },
29 | default: {
30 | minChunks: 2,
31 | priority: -20,
32 | reuseExistingChunk: true,
33 | },
34 | },
35 | },
36 | },
37 | plugins: [
38 | new webpack.HashedModuleIdsPlugin(),
39 | new HtmlWebpackPlugin({
40 | template: './public/index.html',
41 | filename: 'index.html',
42 | templateParameters: {
43 | PUBLIC_URL,
44 | },
45 | }),
46 | new CopyPlugin([{ from: './public' }]),
47 | new CleanWebpackPlugin(),
48 | ],
49 | module: {
50 | rules: [
51 | {
52 | test: /\.css$/i,
53 | use: ['style-loader', 'css-loader'],
54 | },
55 | {
56 | test: /\.s[ac]ss$/i,
57 | use: ['style-loader', 'css-loader', 'sass-loader'],
58 | },
59 | {
60 | test: /\.(png|jpe?g|gif|svg)$/i,
61 | use: [
62 | {
63 | loader: 'file-loader',
64 | },
65 | ],
66 | },
67 | {
68 | test: /\.(js|jsx)$/i,
69 | exclude: /node_modules/,
70 | loader: 'babel-loader',
71 | options: {
72 | presets: ['@babel/preset-env'],
73 | },
74 | },
75 | ],
76 | },
77 | resolve: {
78 | extensions: ['.js', '.jsx'],
79 | alias: {
80 | three: path.resolve('./node_modules/three'),
81 | },
82 | },
83 | output: {
84 | filename: 'index.[hash].js',
85 | path: path.resolve(__dirname, 'build'),
86 | },
87 | };
88 |
--------------------------------------------------------------------------------
/webpack.lib.prod.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const CopyPlugin = require('copy-webpack-plugin');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
6 |
7 | const PUBLIC_URL = require('./package').homepage;
8 |
9 | module.exports = {
10 | entry: {
11 | zethus: './src/zethus.jsx',
12 | panels: './src/panels/index.jsx',
13 | },
14 | mode: 'production',
15 | devtool: 'source-map',
16 | optimization: {
17 | splitChunks: {
18 | chunks: 'async',
19 | minSize: 30000,
20 | maxSize: 0,
21 | minChunks: 1,
22 | maxAsyncRequests: 5,
23 | maxInitialRequests: 3,
24 | automaticNameDelimiter: '~',
25 | automaticNameMaxLength: 30,
26 | name: true,
27 | cacheGroups: {
28 | vendors: {
29 | test: /[\\/]node_modules[\\/]/,
30 | priority: -10,
31 | },
32 | default: {
33 | minChunks: 2,
34 | priority: -20,
35 | reuseExistingChunk: true,
36 | },
37 | },
38 | },
39 | },
40 | plugins: [
41 | new webpack.HashedModuleIdsPlugin(),
42 | new HtmlWebpackPlugin({
43 | template: './public/index.html',
44 | filename: 'index.html',
45 | templateParameters: {
46 | PUBLIC_URL,
47 | },
48 | }),
49 | new CopyPlugin([{ from: './public' }]),
50 | new CleanWebpackPlugin(),
51 | ],
52 | module: {
53 | rules: [
54 | {
55 | test: /\.css$/i,
56 | use: ['style-loader', 'css-loader'],
57 | },
58 | {
59 | test: /\.s[ac]ss$/i,
60 | use: ['style-loader', 'css-loader', 'sass-loader'],
61 | },
62 | {
63 | test: /\.(png|jpe?g|gif|svg)$/i,
64 | use: [
65 | {
66 | loader: 'file-loader',
67 | },
68 | ],
69 | },
70 | {
71 | test: /\.(js|jsx)$/i,
72 | exclude: /node_modules/,
73 | loader: 'babel-loader',
74 | options: {
75 | presets: ['@babel/preset-env'],
76 | },
77 | },
78 | ],
79 | },
80 | resolve: {
81 | extensions: ['.js', '.jsx'],
82 | alias: {
83 | three: path.resolve('./node_modules/three'),
84 | },
85 | },
86 | output: {
87 | filename: '[name].umd.js',
88 | libraryTarget: 'umd',
89 | path: path.resolve(__dirname, 'build-lib'),
90 | },
91 | };
92 |
--------------------------------------------------------------------------------