diff --git a/src/Board.js b/src/Board.js index ab9e166a716261fc2a1baa421ad111f798602ace..3de574c3ba1236a8cd0eb9011231899c0f030be0 100755 --- a/src/Board.js +++ b/src/Board.js @@ -71,7 +71,7 @@ class Board extends React.Component { var component = node.getComponent(); return React.createElement(getComponentType(component), { - node: node, + id: node.getId(), ros: this.props.ros, store: this.props.store }, null); @@ -108,7 +108,7 @@ class Board extends React.Component { /** * Switch in and out of a modal. This function returns a fuction * that does the real switching for a given modal. - * + * * There currently, 2019/07/02, are two modals : readme and settings. * The former is showing information about a gadget and the latter * displays a form to set the configuration of the related gadget. @@ -123,7 +123,7 @@ class Board extends React.Component { // If no modal is active, display the relevant one; // otherwise, get out of it. if (!this.props.store.modal.enabled) { - this.props.store.displayModal(modeName, selectedTab.getComponent()); + this.props.store.displayModal(modeName, selectedTab.getId()); } else { this.props.store.exitModal(); } @@ -137,8 +137,8 @@ class Board extends React.Component { if (typeof selectedTab !== 'undefined') { const components = this.props.store.components; // Is there a store for this gadget? - if (selectedTab.getComponent() in components) { - const component = components[selectedTab.getComponent()]; + if (selectedTab.getId() in components) { + const component = components[selectedTab.getId()]; // is the active (selected) tab configurable? if ('configSchema' in component) { renderValues.buttons.push( diff --git a/src/HorizontalGauge/RosHGauge.js b/src/HorizontalGauge/RosHGauge.js index a14070cce05c4f7e7c32f011828c4fffe2410cca..73580bed1e2b18de8e9a5bcdec7ffdbd1a223006 100644 --- a/src/HorizontalGauge/RosHGauge.js +++ b/src/HorizontalGauge/RosHGauge.js @@ -30,23 +30,23 @@ class RosHGauge extends Component { super(props) this.datum = null; - this.props.store.components['h-gauge'] = new HGaugeStore(); - defaultConfig(this.props.store.components['h-gauge']); + this.props.store.components[this.props.id] = new HGaugeStore(); + defaultConfig(this.props.store.components[this.props.id]); } get topic() { - return this.props.store.components['h-gauge'].config.topic; + return this.props.store.components[this.props.id].config.topic; } get field() { - return this.props.store.components['h-gauge'].config.field; + return this.props.store.components[this.props.id].config.field; } get min() { - return this.props.store.components['h-gauge'].config.min; + return this.props.store.components[this.props.id].config.min; } get max() { - return this.props.store.components['h-gauge'].config.max; + return this.props.store.components[this.props.id].config.max; } - + componentDidMount() { if ('ros' in this.props && this.props.ros) { this.topicAutorun = autorun( reaction => { @@ -57,7 +57,7 @@ class RosHGauge extends Component { ros : this.props.ros, name: this.topic, }); - + // callback on the topic, that takes stores the value of a field // (defined in configuration) in 'this.datum'. this.listener.subscribe(function(message) { @@ -68,13 +68,13 @@ class RosHGauge extends Component { console.warn('RosHGauge expects to be passed a valid Ros object as property, got ', this.props.ros); } } - + componentWillUnmount() { if ('listener' in this) { this.listener.unsubscribe(); } } - + render() { return <HorizontalGauge value={this.datum} diff --git a/src/InteractionTrace/InteractionTrace.js b/src/InteractionTrace/InteractionTrace.js index 021e3df75175ec887fca6efec0b0831eabac283b..f340168c68f4441fe8439927d57029a5a433c59a 100644 --- a/src/InteractionTrace/InteractionTrace.js +++ b/src/InteractionTrace/InteractionTrace.js @@ -12,8 +12,6 @@ import { Backgrounds, Iteration, Actions, Mood, Legend } from './figureElements'; class InteractionTraceStore { - currentInteraction = null; - showInteraction = false; config = {}; configSchema = { title: "Interaction trace", @@ -55,8 +53,6 @@ class InteractionTraceStore { </div>); } decorate(InteractionTraceStore, { - currentInteraction: observable, - showInteraction: observable, config: observable, }) @@ -67,7 +63,7 @@ class InteractionTrace extends Component { super(props); this.interactionStore = new InteractionTraceStore(); - this.props.store.components['int-trace'] = this.interactionStore; + this.props.store.components[this.props.id] = this.interactionStore; // Fill the undefined fields of the configuration object with their default values. defaultConfig(this.interactionStore); @@ -100,7 +96,7 @@ class InteractionTrace extends Component { this.em = undefined; } - /** In debug mode, we add bounding boxes for the different areas of this figure */ + /** In debug mode, we add bounding boxes for the different areas of this figure */ get debug() { return this.interactionStore.config.debug; } @@ -308,7 +304,7 @@ class InteractionTrace extends Component { y: 4, xScale: xScale, minActions: this.nbPrimitiveActions, - interactionStore: this.interactionStore, + store: this.props.store.sharedData, }, { // Labels for all figure elements component: ComponentLabels, diff --git a/src/InteractionTrace/RosInteractionTrace.js b/src/InteractionTrace/RosInteractionTrace.js index fb0caea1cc806f5c47ef886e80271eb28a8a01cd..e03702b3715c6bf1e60026f64c60ef967dfdf86d 100644 --- a/src/InteractionTrace/RosInteractionTrace.js +++ b/src/InteractionTrace/RosInteractionTrace.js @@ -13,17 +13,17 @@ class RosInteractionTrace extends Component { required: ["trace", "boredom", "mood"], properties: { trace: { - title: "Topic for the trace", + title: "Topic for the trace", type: "string", default: "/algorithm/trace" }, boredom: { - title: "Topic for the boredom", + title: "Topic for the boredom", type: "string", default: "/algorithm/boredom" }, mood: { - title: "Topic for the mood", + title: "Topic for the mood", type: "string", default: "/algorithm/mood" }, @@ -37,31 +37,31 @@ class RosInteractionTrace extends Component { constructor(props) { super(props) - + this.data = []; } get traceTopic() { - return this.props.store.components['int-trace'].config.trace; + return this.props.store.components[this.props.id].config.trace; } get boredomTopic() { - return this.props.store.components['int-trace'].config.boredom; + return this.props.store.components[this.props.id].config.boredom; } get moodTopic() { - return this.props.store.components['int-trace'].config.mood; + return this.props.store.components[this.props.id].config.mood; } get lowerIdResets() { - return this.props.store.components['int-trace'].config.lowerIdResets; + return this.props.store.components[this.props.id].config.lowerIdResets; } - + componentDidMount() { // Append the configuration schema of this component to the one of InteractionTrace // Doing so, the configuration form will have all fields for both components. - appendConfigSchema(this.props.store.components['int-trace'], this.newSchema); - + appendConfigSchema(this.props.store.components[this.props.id], this.newSchema); + if ('ros' in this.props && this.props.ros) { // With this MobX autorun, if the topics are changed in the configuration, // we will automatically unsubscribe from the old ones and register to the @@ -101,7 +101,7 @@ class RosInteractionTrace extends Component { console.warn('RosInteractionTrace expects to be passed a valid Ros object as property, got ', this.props.ros); } } - + componentWillUnmount() { if ('trace_listener' in this) { this.trace_listener.unsubscribe(); @@ -113,7 +113,7 @@ class RosInteractionTrace extends Component { this.mood_listener.unsubscribe(); } } - + render() { return <InteractionTrace data={this.data} {...this.props}/> } diff --git a/src/InteractionTrace/figureElements.js b/src/InteractionTrace/figureElements.js index 4551d4c751c5ca1b8a334849cd395cf87d8fd551..dc0d55f6601361c14b41d0d9d268bcd85fe87861 100644 --- a/src/InteractionTrace/figureElements.js +++ b/src/InteractionTrace/figureElements.js @@ -12,7 +12,7 @@ export function Separators(props) { // elements.push(<rect x="0" y={y} width="100%" height={height} fill="white" stroke="none"/>); const line_y = props.yLayout.upper(i) + props.yLayout.domain.padding / 2; elements.push( - <line x1="0" y1={line_y} x2="100%" y2={line_y} + <line x1="0" y1={line_y} x2="100%" y2={line_y} key={i} className="separator"/>); } return elements; @@ -57,17 +57,16 @@ export function Actions(props) { if (interaction === undefined || interaction.enacted === undefined) { return null; } - + // These two methods will handle when the mouse hovers and leaves an action. As a result, the hovered interaction - // is stored in props.interactionStore (mobx). The intent is to display information of this interaction somewhere - // else in the application. - const interactionStore = props.interactionStore + // is stored in props.store (mobx). The intent is to display information of this interaction somewhere else in the + // application. const handleOver = () => { - props.interactionStore.currentInteraction = interaction; - interactionStore.showInteraction = true; + props.store.set('currentInteraction', interaction); + props.store.set('showInteraction', true); } const handleOut = () => { - interactionStore.showInteraction = false; + props.store.set('showInteraction', false); } const actions = cloneDeep(interaction.enacted.action_names); @@ -80,7 +79,7 @@ export function Actions(props) { actions.push('︙'); valences.push(0); } - + return ( <text key={actions.join('')+interaction.id} y="0" style={style} onMouseOver={handleOver} onMouseOut={handleOut}> @@ -182,17 +181,17 @@ export function Plots(props) { // d3's line generator const line = d3.line() .x(d => props.xScale(d.x)) // set the x values for the line generator - .y(d => props.yScale(d.y)) // set the y values for the line generator + .y(d => props.yScale(d.y)) // set the y values for the line generator .curve(d3.curveLinear); // don't apply smoothing to the line // .curve(d3.curveMonotoneX) // apply smoothing to the line - + // For each curve return Object.keys(props.data).map((name) => { const data = props.data[name].filter((point, i) => { return !(point === undefined || point.x === undefined || point.y === undefined) }); const colour = props.colour(name); - + // Add markers on the line (by default) const markers = data.map((point, i) => { return <circle className="dot" key={"dot-"+i} fill={colour} @@ -216,7 +215,7 @@ export function Plots(props) { */ export function Legend(props) { const em = props.em || 1; - + const names = Object.keys(props.data); const yScale = index => props.yOffset + index * em * 1.2; @@ -237,8 +236,8 @@ export function Legend(props) { </text>, <path key={"line" + name} className={"line " + name} d={line(data)} stroke={props.colour(name)} /> - + ]})} </g> ); -} \ No newline at end of file +} diff --git a/src/LatestInteraction/LatestInteraction.js b/src/LatestInteraction/LatestInteraction.js index 24aea55f137ae0e0ebe078b4703767fd50e75f03..d87d2c97e88649d0171bfb329b8f8f322eefac07 100644 --- a/src/LatestInteraction/LatestInteraction.js +++ b/src/LatestInteraction/LatestInteraction.js @@ -7,18 +7,18 @@ export const LatestInteraction = observer( function LatestInteraction(props) { let intended, enacted; let iteration = null; - if ('int-trace' in props.store.components && props.store.components['int-trace'].showInteraction) { - intended = props.store.components['int-trace'].currentInteraction.intended; - enacted = props.store.components['int-trace'].currentInteraction.enacted; - const iteration_id = props.store.components['int-trace'].currentInteraction.id; - iteration = [<p key="first"><span className="label">Iteration:</span> {iteration_id}</p>, <hr key="last"/>]; + if (props.store.sharedData.has('currentInteraction') && props.store.sharedData.get('showInteraction')) { + intended = props.store.sharedData.get('currentInteraction').intended; + enacted = props.store.sharedData.get('currentInteraction').enacted; + const iteration_id = props.store.sharedData.get('currentInteraction').id; + iteration = [<p key="first"><span className="label">Iteration:</span> {iteration_id}</p>, <hr key="last"/>]; } else { intended = props.intended; enacted = props.enacted; } /** - * Return the + * Return the * @param {Object} inter object representing the interaction to display (either intended or enacted) * @param {Number} i index of primitive action which row this is makeing */ @@ -27,7 +27,7 @@ function LatestInteraction(props) { return ( <td className={actionClassName(inter.primitive_valences[i])}> {inter.action_names[i]} ({inter.primitive_valences[i]}) - </td>); + </td>); } else { return null; } diff --git a/src/LatestInteraction/RosLastestInteraction.js b/src/LatestInteraction/RosLastestInteraction.js index 49031be36e3f24f7d3a1603db2f6ec13ef78834c..89f4995922deb3baf2b236dd84ab036c075fc3d3 100644 --- a/src/LatestInteraction/RosLastestInteraction.js +++ b/src/LatestInteraction/RosLastestInteraction.js @@ -24,7 +24,7 @@ class LatestInteractionStore { required: ["trace"], properties: { trace: { - title: "Topic for the trace", + title: "Topic for the trace", type: "string", default: "/algorithm/trace" }, @@ -40,9 +40,9 @@ class RosLatestInteraction extends Component { constructor(props) { super(props); - this.props.store.components['latest-interaction'] = new LatestInteractionStore(); + this.props.store.components[this.id] = new LatestInteractionStore(); // Fill the undefined fields of the configuration object with their default values. - defaultConfig(this.props.store.components['latest-interaction']); + defaultConfig(this.props.store.components[this.id]); this.latestInteraction = { intended: { @@ -57,8 +57,12 @@ class RosLatestInteraction extends Component { this.topicType = "april_messages/trace"; } + get id() { + return this.props.id; + } + get topicName() { - return this.props.store.components['latest-interaction'].config.trace; + return this.props.store.components[this.id].config.trace; } componentDidMount() { @@ -77,7 +81,7 @@ class RosLatestInteraction extends Component { name: this.topicName, messageType: this.topicType }); - + // Subscribe (and declare a callback) this.trace_listener.subscribe(message => { this.latestInteraction = message; diff --git a/src/LineChart/RosLineChart.js b/src/LineChart/RosLineChart.js index e90c012bbedf1129e7ddaa542850465313d612d6..b5f605d18d625aa3251c0d37e93485adcd43743e 100644 --- a/src/LineChart/RosLineChart.js +++ b/src/LineChart/RosLineChart.js @@ -26,7 +26,7 @@ class LineChartStore { type: "object", properties: { topic: { - title: "topic", + title: "topic", type: "string", default: "/std_msgs/int64" }, @@ -80,7 +80,7 @@ class RosLineChart extends Component { super(props); this.store = new LineChartStore(); - this.props.store.components['line-chart'] = this.store; + this.props.store.components[this.props.id] = this.store; // Fill the undefined fields of the configuration object with their default values. defaultConfig(this.store); @@ -90,7 +90,7 @@ class RosLineChart extends Component { /** * Extract a field from a ros message and add it to the state, for plotting. - * + * * If there are already maxPoints in storage, we remove the oldest data point. * @param {Object} curve has two entries, `field`, path to the field to be extracted from incoming messages * and `topic` the ROS topic from which we extract the field. diff --git a/src/MemoryRanked/MemoryRanked.js b/src/MemoryRanked/MemoryRanked.js index f39c5222760e49b96d548ce7f593c150110845b2..95f49c9777ef79ab82e838858f5bdffe0e79318b 100644 --- a/src/MemoryRanked/MemoryRanked.js +++ b/src/MemoryRanked/MemoryRanked.js @@ -93,28 +93,28 @@ class MemoryRanked extends Component { super(props); this.store = new MemoryStore(); - this.props.store.components['memory'] = this.store; + this.props.store.components[this.props.id] = this.store; // Fill the undefined fields of the configuration object with their default values. defaultConfig(this.store); - + // Use the margin convention practice this.margin = {top: 10, right: 20, bottom: 20, left: 20} - + // The content of each figure element is offset this much down, to leave room for the main labels this.yOffset = 50; - + // Will store the conversion from 1em in px for the LabelColumn. // This is a tricky trick to set the size of the LabelColumn to match with the longest chain of actions that // it will have to show. this.em = undefined; - + // The increment in Y coordinate between two entries in the memory this.vStep = 30; - + // // How frequent the horizontal guide lines should be put // // If set to 4, one guide line will be displayed every four line of data - // this.guideLinesStep = config.guideLinesStep; - + // this.guideLinesStep = config.guideLinesStep; + // // The highest number of actions displayed in one memory entry. // this.maxActionLength = config.maxActionLength; } @@ -126,11 +126,11 @@ class MemoryRanked extends Component { get maxActionLength() { return this.store.maxActionLength; } - + /** Get the font size of a node in the DOM. It tells us how big one em is in px. - * + * * This value is used to adjust the width of the LabelColumn based on the width of its content. - * + * * @param {DOMnode} node the DOM node from which we retrieve information */ getEmSize(node) { @@ -138,15 +138,15 @@ class MemoryRanked extends Component { this.em = parseFloat(getComputedStyle(node).fontSize); } } - - /** Y scale will use the randomly generated number + + /** Y scale will use the randomly generated number * @param {Number} i the index of a line of data * @return how many pixels down on the ordinate axis the line of data must be displayed */ yScale(i) { return i*this.vStep; } - + /** Generate guide lines every `this.guideLineCount` line of data * @return array of ordinate, one for each line to be added */ @@ -163,10 +163,10 @@ class MemoryRanked extends Component { // Here, 50 is the top margin used in all the columns of this component y.push(this.margin.top + 50 + this.yScale(this.guideLinesStep*i) + this.vStep / 2) } - + return y; } - + sort(data) { let sorter; // Make a sorting function based on the configuration or, by default, sort @@ -186,22 +186,22 @@ class MemoryRanked extends Component { return b.valence - a.valence; } } - + return data.sort(sorter); } - + render() { const data = this.sort(this.props.data); - - + + // Minimal required height for this figure const minHeight = this.margin.top + this.yOffset + this.yScale(data.length+2) + this.margin.bottom; const outerHeight = Math.max(minHeight, this.props.size.height); - + // width and height are the inner dimensions of the drawing const width = this.props.size.width - this.margin.left - this.margin.right, height = outerHeight - this.margin.top - this.margin.bottom; - + // Object that manages the correct spacing and placing of each figure element, in the horizontal direction. this.xLayout = new LinearLayout(this.props.size.width) .magnitudeAuto() @@ -210,7 +210,7 @@ class MemoryRanked extends Component { .fixedCell(this.maxActionLength*this.em*0.8 || 60) .fixedCell(50) .ratioCell(1); - + const figureElements = [ { // Actions column component: LabelColumn, @@ -251,29 +251,29 @@ class MemoryRanked extends Component { </div>; } } - + const viewbox = "0 0 " + (width + this.margin.left + this.margin.right) + " " + (height + this.margin.top + this.margin.bottom); - + return ( <svg viewBox={viewbox} width="100%" height={outerHeight} className="memory-ranked"> - + {/* Draw a reactangle around the drawing area of the SVG */} {/* <rect x="0" y="0" width="100%" height="100%" fill="none" stroke="red"/> */} - + {/* 1. The main figure elements */} {figureElements.map(this.makeSVGComponent.bind(this))} - + {/* 2. Vertical partition between the actions and the occurences */} <line className="partition" x1={this.xLayout.upper(0) + this.xLayout.padding()/2} y1={this.margin.top} x2={this.xLayout.upper(0) + this.xLayout.padding()/2} y2={this.margin.top + height}/> - + {/* 3. Vertical partition between the occurences and the valences */} <line className="partition" x1={this.xLayout.upper(1) + this.xLayout.padding()/2} y1={this.margin.top} x2={this.xLayout.upper(1) + this.xLayout.padding()/2} y2={this.margin.top + height}/> - + {/* 4. Now, horizontal guide lines, to help reading this visualisation */} {this.guideLines().map((yValue) => { return <line key={yValue+"guideline"} className="guideline" @@ -282,14 +282,14 @@ class MemoryRanked extends Component { })} </svg>); } - + /** * Create the react Components declared in declaration and place them at the required x and y coordinates. - * + * * *Note*: it passes an additional `width` attribute on to the elements so that they know how much horizontal * space is available for them. * @param {Object} declaration object defining what elements will be appended to the SVG. - * Expected attributes: + * Expected attributes: * - component: React component (function or class) to be added * - x: index of the element, on the horizontal axis * - all additional attributes are passed to the React Component @@ -304,10 +304,10 @@ class MemoryRanked extends Component { let props = Object.assign({}, declaration); delete props.x; delete props.y; delete props.component; props.width = this.xLayout.width(declaration.x); - + // Instanciate the component const component = React.createElement(declaration.component, props); - + return ( <g key={"SVG"+i} transform={"translate(" + x + "," + y + ")"}> {/* <rect x="0" y="0" width={valenceLocation.width} height={valenceLocation.height} fill="none" stroke="steelblue"/> */} @@ -320,5 +320,5 @@ decorate(MemoryRanked, { guideLinesStep: computed, maxActionLength: computed, }) - -export default MemoryRanked; \ No newline at end of file + +export default MemoryRanked; diff --git a/src/MemoryRanked/RosMemoryRanked.js b/src/MemoryRanked/RosMemoryRanked.js index 1fa39d09e15d8144820a8a42ba89033b2fe4414b..ef5d5e8301b0ac87950588b786699bb0bc6d9a95 100644 --- a/src/MemoryRanked/RosMemoryRanked.js +++ b/src/MemoryRanked/RosMemoryRanked.js @@ -29,13 +29,13 @@ class RosMemoryRanked extends Component { }; get topic() { - return this.props.store.components['memory'].config.topic; + return this.props.store.components[this.props.id].config.topic; } componentDidMount() { // Append the configuration schema of this component to the one of MemoryRanked // Doing so, the configuration form will have all fields for both components. - appendConfigSchema(this.props.store.components['memory'], this.newSchema); + appendConfigSchema(this.props.store.components[this.props.id], this.newSchema); if ('ros' in this.props && this.props.ros) { // With this MobX autorun, if the topics are changed in the configuration, diff --git a/src/Mood/RosMood.js b/src/Mood/RosMood.js index 71afde0b1b8bfe6e7d1be6f880e0517a113689cb..bf008d6d3f196c059e2922434e9f881a285ce681 100644 --- a/src/Mood/RosMood.js +++ b/src/Mood/RosMood.js @@ -37,7 +37,7 @@ class RosMood extends Component { super(props) this.store = new MoodStore(); - this.props.store.components['mood'] = this.store; + this.props.store.components[this.props.id] = this.store; // Fill the undefined fields of the configuration object with their default values. defaultConfig(this.store); diff --git a/src/VideoStream/index.js b/src/VideoStream/index.js index bc95c05191b8e8222286295bb46dc6f702d6e378..19122f57a9168c317f88a33b4b324e39d56bbe8d 100644 --- a/src/VideoStream/index.js +++ b/src/VideoStream/index.js @@ -44,7 +44,7 @@ class VideoStream extends Component { // Fill the undefined fields of the configuration object with their default values. defaultConfig(this.store); - this.props.store.components['video'] = this.store; + this.props.store.components[this.props.id] = this.store; // Automatically detect if the configured topic name is valid this.hasTopic = observable.box(false); diff --git a/src/index.js b/src/index.js index 73d63ae0e86e7f6b56f1f6815751746913e11138..7d2c6311c71fd49f4041c843b693b143c9f91121 100644 --- a/src/index.js +++ b/src/index.js @@ -17,6 +17,7 @@ import './Common.css' class ObservableStore { components = {} + sharedData = observable.map() modal = { enabled: false, mode: null, @@ -42,7 +43,7 @@ const Main = observer( class Main extends React.Component { constructor(props) { super(props); - + this.store = new ObservableStore(); this.ros = undefined; @@ -80,7 +81,7 @@ class Main extends React.Component { <Modal store={this.store}/> </div>); } - + return ( <div id="level-1"> <div id="menu">