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