From c6d3b29dcfa94b604883b7313057d3b7b63d7731 Mon Sep 17 00:00:00 2001 From: Dorian Goepp <dorian.goepp@gmail.com> Date: Tue, 20 Aug 2019 14:07:45 +0200 Subject: [PATCH] Allow for different instances of a gadget to have their own configuration --- src/Board.js | 10 +-- src/HorizontalGauge/RosHGauge.js | 20 ++--- src/InteractionTrace/InteractionTrace.js | 4 +- src/InteractionTrace/RosInteractionTrace.js | 26 +++---- .../RosLastestInteraction.js | 14 ++-- src/LineChart/RosLineChart.js | 6 +- src/MemoryRanked/MemoryRanked.js | 74 +++++++++---------- src/MemoryRanked/RosMemoryRanked.js | 4 +- src/Mood/RosMood.js | 2 +- src/VideoStream/index.js | 2 +- 10 files changed, 83 insertions(+), 79 deletions(-) diff --git a/src/Board.js b/src/Board.js index ab9e166..3de574c 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 a14070c..73580be 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 021e3df..d1620cb 100644 --- a/src/InteractionTrace/InteractionTrace.js +++ b/src/InteractionTrace/InteractionTrace.js @@ -67,7 +67,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 +100,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; } diff --git a/src/InteractionTrace/RosInteractionTrace.js b/src/InteractionTrace/RosInteractionTrace.js index fb0caea..e03702b 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/LatestInteraction/RosLastestInteraction.js b/src/LatestInteraction/RosLastestInteraction.js index 49031be..89f4995 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 e90c012..b5f605d 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 f39c522..95f49c9 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 1fa39d0..ef5d5e8 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 71afde0..bf008d6 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 bc95c05..19122f5 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); -- GitLab