Commit 0bd07302 authored by Dorian Goepp's avatar Dorian Goepp

Upgrade the configuration system for gadgets

The configuration of each gadget is now managed with MobX. It means that we get rid of the mechanisms of FlexLayout and that we can configure items that are not displayed in FlexLayout.
Also, The configuration and readme mode are now displayed as a modal overlay over the whole web page. This is still WIP, but much better on some aspects than before.

Limitation : for now, if two tabs are instantiated with the same gadget inside, the configuration would be common to both. It clearly should be fixed.
parent 70e9dd70
......@@ -67,6 +67,23 @@ html,body
border:1px solid #ddd;
}
#overlay {
background: rgb(156, 156, 151);
height: 100%;
width: 100%;
opacity: .9;
top: 0;
left: 0;
position: absolute;
padding: 0;
transition: opacity .5s;
}
#overlay:hover {
opacity: .9;
transition: opacity .5s;
}
.flexlayout__layout {
left: 0;
top: 0;
......
This diff is collapsed.
import React, {Component} from 'react'
import {decorate, observable, computed, autorun} from 'mobx'
import {observer} from 'mobx-react'
import RosLib from 'roslib'
import {get} from 'lodash/object'
import {defaultConfig} from '../utils/configurationHelpers'
import HorizontalGauge from './index'
class HGaugeStore {
readme = <p className="about">This gauge represents a <b>measure</b> ranging from 0 to 1.</p>;
configSchema = {
title: "Horizontal Gauge",
type: "object",
required: ["topic", "field", "min", "max"],
properties: {
topic: {type: "string", title: "ROS topic (absolute name)",
default: "/predictions/interaction_involvement"},
field: {type: "string", title: "the field to be read from the message",
default: "prediction_of_head_orientation.list[0].values[1]"},
min: {type: "number", title: "Lower end of the data range", default: 0},
max: {type: "number", title: "Upper end of the data range", default: 1},
}
};
}
const RosHGauge = observer(
class RosHGauge extends Component {
constructor(props) {
super(props)
this.state = {datum: null}
this.datum = null;
this.props.store.components['h-gauge'] = new HGaugeStore();
defaultConfig(this.props.store.components['h-gauge']);
}
static get modes() {
return {
readme: <p className="about">This gauge represents a <b>measure</b> ranging from 0 to 1.</p>,
settingsSchema: {
title: "Horizontal Gauge",
type: "object",
required: ["topic", "field", "min", "max"],
properties: {
topic: {type: "string", title: "ROS topic (absolute name)",
default: "/predictions/interaction_involvement"},
field: {type: "string", title: "the field to be read from the message",
default: "prediction_of_head_orientation.list[0].values[1]"},
min: {type: "number", title: "Lower end of the data range", default: 0},
max: {type: "number", title: "Upper end of the data range", default: 1},
}
},
};
get topic() {
return this.props.store.components['h-gauge'].config.topic;
}
get field() {
return this.props.store.components['h-gauge'].config.field;
}
get min() {
return this.props.store.components['h-gauge'].config.min;
}
get max() {
return this.props.store.components['h-gauge'].config.max;
}
componentDidMount() {
if ('ros' in this.props && this.props.ros) {
const config = this.props.node.getConfig();
this.listener = new RosLib.Topic({
ros : this.props.ros,
name: config.topic,
});
this.listener.subscribe(function(message) {
this.setState({datum: get(message, config.field)});
}.bind(this));
}else {
this.topicAutorun = autorun( reaction => {
if ('listener' in this) {
this.listener.unsubscribe();
}
this.listener = new RosLib.Topic({
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) {
this.datum = get(message, this.field);
}.bind(this));
})
} else {
console.warn('RosHGauge expects to be passed a valid Ros object as property, got ', this.props.ros);
}
}
......@@ -53,14 +76,19 @@ class RosHGauge extends Component {
}
render() {
const config = this.props.node.getConfig();
return <HorizontalGauge
value={this.state.datum}
min={config.min}
max={config.max}
value={this.datum}
min={this.min}
max={this.max}
{...this.props}/>;
}
}
})
decorate(RosHGauge, {
datum: observable,
topic: computed,
field: computed,
min: computed,
max: computed,
})
export default RosHGauge
\ No newline at end of file
import React from 'react'
import HorizontalGauge from './HozirontalGauge'
import RosHGauge from './RosHGauge'
import {Modal} from '../utils/modeHandler'
export default HorizontalGauge;
function modal(props){
return <Modal component={RosHGauge} {...props}/>;
}
export {modal as RosHGauge};
\ No newline at end of file
export {RosHGauge};
\ No newline at end of file
import React, {Component} from 'react'
import {decorate, observable} from 'mobx'
import {decorate, observable, computed} from 'mobx'
import {observer} from 'mobx-react'
import * as d3 from 'd3'
import {defaultConfig} from '../utils/configurationHelpers'
import LinearLayout from '../utils/LinearLayout'
import { extent } from '../utils/extent';
import { getBoredom, getIValence, getEValence } from './accessors';
......@@ -13,10 +14,50 @@ import { Backgrounds, Iteration, Actions, Mood,
class InteractionTraceStore {
currentInteraction = null;
showInteraction = false;
config = {};
configSchema = {
title: "Interaction trace",
type: "object",
properties: {
maxBoredom: {
title: "Maximal expected value for the boredom (used for scaling)",
type: "integer",
default: 2,
},
nbPrimitiveActions: {
title: "How many primitive actions are displayed",
type: "integer",
default: 3,
},
widthInEm: {
title: "The width of each column in em units",
type: "number",
default: 2.5,
},
debug: {
title: "Debug mode",
type: "boolean",
default: false
}
}
};
readme = (
<div className="about">
<h1>Trace of the reasoning process performed by the agent</h1>
<p>The <b>mood</b> represents the effect of the <b>valence</b> on the mood of the agent.</p>
<h2>Boredom</h2>
<p>
Observe the evolution of the <b>interactions</b> performed by the agent. <b>Intended</b> interactions are the
interactions the agent plans to perform whereas <b>enacted</b> interactions are the interactions actually
performed in the physical world of the agent. An interaction is composed of an <b>action</b> and a
<b>valence</b>, the valence being dependent of the response from the environment.
</p>
</div>);
}
decorate(InteractionTraceStore, {
currentInteraction: observable,
showInteraction: observable,
config: observable,
})
const InteractionTrace = observer(
......@@ -27,10 +68,11 @@ class InteractionTrace extends Component {
this.interactionStore = new InteractionTraceStore();
this.props.store.components['int-trace'] = this.interactionStore;
// Fill the undefined fields of the configuration object with their default values.
defaultConfig(this.interactionStore);
// Use the margin convention practice
// Use the margin convention practice of D3
this.margin = {top: 20, right: 30, bottom: 20, left: 20};
// this.margin = {top: 0, right: 0, bottom: 0, left: 0};
// Blanck space between each element of the figure
this.spacing = {v: 20, h: 15};
......@@ -54,38 +96,37 @@ class InteractionTrace extends Component {
.ratioCell(0.5) // Row for the valences (takes a ratio of the avalable space)
.fixedCell(75); // Row for the actions (temporary value, updated later in function of this.em)
const config = this.props.node.getConfig();
// Value for the boredom at which we plot a red line (showing max value)
this.maxBoredom = config.maxBoredom;
// How many primitive actions (at least) we want to display
this.nbPrimitiveActions = config.nbPrimitiveActions;
// 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.
// Will store the conversion from 1em in px. We have to assume that this computation is approximative.
this.em = undefined;
// In debug mode, we add bounding boxes for the different areas of this figure
this.debug = config.debug;
// Other parameter: how many em the minimal column width has to be (function minXWidth).
}
// The smallest allowed space around each point on the x axis.
minXWidth() {
return 2.5*this.em;
/** In debug mode, we add bounding boxes for the different areas of this figure */
get debug() {
return this.interactionStore.config.debug;
}
/** A red line, showing the maximum accepted boredom level, will be displayed using this value */
get maxBoredom() {
return this.interactionStore.config.maxBoredom;
}
/** How many primitive actions (at least) we want to display */
get nbPrimitiveActions() {
return this.interactionStore.config.nbPrimitiveActions;
}
/** The smallest allowed space around each point on the x axis. */
get minXWidth() {
return this.interactionStore.config.widthInEm * this.em;
}
/** 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.
* This value is used to adjust the width of the columns in the trace, based on the size of their content (which has text).
*
* @param {DOMnode} node the DOM node from which we retrieve information
*/
getEmSize(node) {
if (node) {
this.em = parseFloat(getComputedStyle(node).fontSize);
}
if (node) {
this.em = parseFloat(getComputedStyle(node).fontSize);
}
}
/**
......@@ -96,7 +137,7 @@ class InteractionTrace extends Component {
* the second is a function that will return true iff the datum we give it belongs to the domain.
*/
filter(data) {
const maxNumberOfInteractions = Math.floor(this.xLayout.width(2) / this.minXWidth()) || 0;
const maxNumberOfInteractions = Math.floor(this.xLayout.width(2) / this.minXWidth) || 0;
const highestId = d3.max(data, (datum => datum.id)) || 0;
const xDomain = [highestId-maxNumberOfInteractions, highestId];
const domainFilter = (datum) => (datum.id >= xDomain[0] && datum.id <= xDomain[1]);
......@@ -343,6 +384,12 @@ class InteractionTrace extends Component {
}
}
)
decorate(InteractionTrace, {
debug: computed,
maxBoredom: computed,
nbPrimitiveActions: computed,
em: observable,
});
export default InteractionTrace;
class MakeSVGComponents extends Component {
......
import React, {Component} from 'react'
import {Modal} from '../utils/modeHandler'
import {observable, decorate} from 'mobx'
import {observable, decorate, computed, autorun} from 'mobx'
import {observer} from 'mobx-react'
import sizeMe from 'react-sizeme'
import RosLib from 'roslib'
import {appendConfigSchema} from '../utils/configurationHelpers'
import InteractionTrace from './InteractionTrace'
const RosInteractionTrace = observer(
class RosInteractionTrace extends Component {
newSchema = {
required: ["trace", "boredom", "mood"],
properties: {
trace: {
title: "Topic for the trace",
type: "string",
default: "/algorithm/trace"
},
boredom: {
title: "Topic for the boredom",
type: "string",
default: "/algorithm/boredom"
},
mood: {
title: "Topic for the mood",
type: "string",
default: "/algorithm/mood"
},
lowerIdResets: {
title: "Reset the display when a new interaction arrives with an ID lower than the latest one?",
type: "boolean",
default: true,
}
}
};
constructor(props) {
super(props)
this.data = [];
}
static get modes() {
return {
readme: (
<div className="about">
<h1>Trace of the reasoning process performed by the agent</h1>
<p>The <b>mood</b> represents the effect of the <b>valence</b> on the mood of the agent.</p>
<h2>Boredom</h2>
<p>
Observe the evolution of the <b>interactions</b> performed by the agent. <b>Intended</b> interactions are the
interactions the agent plans to perform whereas <b>enacted</b> interactions are the interactions actually
performed in the physical world of the agent. An interaction is composed of an <b>action</b> and a
<b>valence</b>, the valence being dependent of the response from the environment.
</p>
</div>),
settingsSchema: {
title: "Interaction trace",
type: "object",
required: ["trace", "boredom", "mood"],
properties: {
trace: {
title: "Topic for the trace",
type: "string",
default: "/algorithm/trace"
},
boredom: {
title: "Topic for the boredom",
type: "string",
default: "/algorithm/boredom"
},
mood: {
title: "Topic for the mood",
type: "string",
default: "/algorithm/mood"
},
lowerIdResets: {
title: "Reset the display when a new interaction arrives with an ID lower than the latest one?",
type: "boolean",
default: true,
},
maxBoredom: {
title: "Maximal expected value for the boredom (used for scaling)",
type: "integer",
default: 2,
},
nbPrimitiveActions: {
title: "How many primitive actions are displayed",
type: "integer",
default: 3,
},
debug: {
title: "Debug mode",
type: "boolean",
default: false
}
}
}
};
get traceTopic() {
return this.props.store.components['int-trace'].config.trace;
}
get boredomTopic() {
return this.props.store.components['int-trace'].config.boredom;
}
get moodTopic() {
return this.props.store.components['int-trace'].config.mood;
}
get lowerIdResets() {
return this.props.store.components['int-trace'].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);
if ('ros' in this.props && this.props.ros) {
const config = this.props.node.getConfig();
this.trace_listener = new RosLib.Topic({
ros : this.props.ros,
name: config.trace,
messageType: "april_messages/trace"
});
this.trace_listener.subscribe(this.interaction_to_data.bind(this));
// With this MobX autorun, if the topics are changed in the configuration,
// we will automatically unsubscribe from the old ones and register to the
// new topic names.
this.topicAutorun = autorun( reaction => {
if ('trace_listener' in this) {
this.trace_listener.unsubscribe();
}
this.trace_listener = new RosLib.Topic({
ros : this.props.ros,
name: this.traceTopic,
messageType: "april_messages/trace"
});
this.trace_listener.subscribe(this.interaction_to_data.bind(this));
this.boredom_listener = new RosLib.Topic({
ros : this.props.ros,
name: config.boredom,
messageType: "april_messages/boredom"
});
this.boredom_listener.subscribe(this.boredom_to_data.bind(this));
if ('boredom_listener' in this) {
this.boredom_listener.unsubscribe();
}
this.boredom_listener = new RosLib.Topic({
ros : this.props.ros,
name: this.boredomTopic,
messageType: "april_messages/boredom"
});
this.boredom_listener.subscribe(this.boredom_to_data.bind(this));
this.mood_listener = new RosLib.Topic({
ros : this.props.ros,
name: config.mood,
messageType: "april_messages/mood"
if ('mood_listener' in this) {
this.mood_listener.unsubscribe();
}
this.mood_listener = new RosLib.Topic({
ros : this.props.ros,
name: this.moodTopic,
messageType: "april_messages/mood"
});
this.mood_listener.subscribe(this.mood_to_data.bind(this));
});
this.mood_listener.subscribe(this.mood_to_data.bind(this));
} else {
console.warn('RosInteractionTrace expects to be passed a valid Ros object as property, got ', this.props.ros);
}
......@@ -156,8 +156,7 @@ class RosInteractionTrace extends Component {
*/
mergeDataInState(newDatum) {
// Empty the data and get the first interaction of an experiment
const config = this.props.node.getConfig();
if (config.lowerIdResets && this.data.length > 0 && newDatum.id < this.data[this.data.length-1].id) {
if (this.lowerIdResets && this.data.length > 0 && newDatum.id < this.data[this.data.length-1].id) {
this.data = [];
}
......@@ -173,11 +172,10 @@ class RosInteractionTrace extends Component {
)
decorate(RosInteractionTrace, {
data: observable,
// lowerIdResets: observable,
traceTopic: computed,
boredomTopic: computed,
moodTopic: computed,
lowerIdResets: computed,
})
const ModalRosInteractionTrace = observer(function(props) {
return <Modal component={RosInteractionTrace} {...props}/>
});
export default sizeMe({monitorHeight: true})(ModalRosInteractionTrace);
\ No newline at end of file
export default sizeMe({monitorHeight: true})(RosInteractionTrace);
\ No newline at end of file
import React, {Component} from 'react'
import RosLib from 'roslib'
import {autorun, decorate, observable} from 'mobx'
import {observer} from 'mobx-react'
import {Modal} from '../utils/modeHandler'
import {defaultConfig} from '../utils/configurationHelpers'
import {LatestInteraction} from './LatestInteraction'
class LatestInteractionStore {
readme = (
<div className="about">
<p>
<b>Last action</b> done by the agent and its <b>valence</b>.
</p>
<p>
If you place your mouse pointer over an <b>action</b> in the interaction trace, this widget will display
the <b>full interaction</b>.
</p>
</div>);
config = {};
configSchema = {
title: "Interaction trace",
type: "object",
required: ["trace"],
properties: {
trace: {
title: "Topic for the trace",
type: "string",
default: "/algorithm/trace"
},
}
};
}
decorate(LatestInteractionStore, {
config: observable,
})
const RosLatestInteraction = observer(
class RosLatestInteraction extends Component {
constructor(props) {
super(props)
super(props);
this.state = {intended: {
action_names: [],
primitive_valences: []
}, enacted: {
action_names: [],
primitive_valences: []
}};
this.trace = {
topic: "/algorithm/trace",
type: "april_messages/trace"
}
}
this.props.store.components['latest-interaction'] = new LatestInteractionStore();
// Fill the undefined fields of the configuration object with their default values.
defaultConfig(this.props.store.components['latest-interaction']);
static get modes() {
return {
readme: (
<div className="about">
<p>
<b>Last action</b> done by the agent and its <b>valence</b>.
</p>
<p>
If you place your mouse pointer over an <b>action</b> in the interaction trace, this widget will display
the <b>full interaction</b>.
</p>
</div>),
settingsSchema: {
title: "Interaction trace",
type: "object",
required: ["trace"],
properties: {
trace: {
title: "Topic for the trace",
type: "string",
default: "/algorithm/trace"
},
}
this.latestInteraction = {
intended: {
action_names: [],
primitive_valences: []
},
enacted: {