1
0
Fork 0
mirror of https://git.rwth-aachen.de/acs/public/villas/web/ synced 2025-03-09 00:00:01 +01:00

Add gauge widget color zones

This commit is contained in:
Markus Grigull 2017-09-18 00:29:55 +02:00
parent 7b51cfc3f1
commit 7825d119de
9 changed files with 346 additions and 64 deletions

View file

@ -0,0 +1,131 @@
/**
* File: edit-widget-color-zones-control.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 20.08.2017
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React from 'react';
import { FormGroup, ControlLabel, Button, Glyphicon } from 'react-bootstrap';
import Table from '../table';
import TableColumn from '../table-column';
class EditWidgetColorZonesControl extends React.Component {
constructor(props) {
super(props);
this.state = {
widget: {
zones: []
},
selectedZones: []
};
}
componentWillReceiveProps(nextProps) {
this.setState({ widget: nextProps.widget });
}
addZone = () => {
// add row
const widget = this.state.widget;
widget.zones.push({ strokeStyle: 'ffffff', min: 0, max: 100 });
this.setState({ widget });
this.sendEvent(widget);
}
removeZones = () => {
// remove zones
const widget = this.state.widget;
this.state.selectedZones.forEach(row => {
widget.zones.splice(row, 1);
});
this.setState({ selectedZones: [], widget });
this.sendEvent(widget);
}
changeCell = (event, row, column) => {
// change row
const widget = this.state.widget;
if (column === 1) {
widget.zones[row].strokeStyle = event.target.value;
} else if (column === 2) {
widget.zones[row].min = event.target.value;
} else if (column === 3) {
widget.zones[row].max = event.target.value;
}
this.setState({ widget });
this.sendEvent(widget);
}
sendEvent(widget) {
// create event
const event = {
target: {
id: 'zones',
value: widget.zones
}
};
this.props.handleChange(event);
}
checkedCell = (row, event) => {
// update selected rows
const selectedZones = this.state.selectedZones;
if (event.target.checked) {
if (selectedZones.indexOf(row) === -1) {
selectedZones.push(row);
}
} else {
let index = selectedZones.indexOf(row);
if (row > -1) {
selectedZones.splice(index, 1);
}
}
this.setState({ selectedZones });
}
render() {
return <FormGroup>
<ControlLabel>Color zones</ControlLabel>
<Table data={this.state.widget.zones}>
<TableColumn width="20" checkbox onChecked={this.checkedCell} />
<TableColumn title="Color" dataKey="strokeStyle" inlineEditable onInlineChange={this.changeCell} />
<TableColumn title="Minimum" dataKey="min" inlineEditable onInlineChange={this.changeCell} />
<TableColumn title="Maximum" dataKey="max" inlineEditable onInlineChange={this.changeCell} />
</Table>
<Button onClick={this.addZone} disabled={!this.props.widget.colorZones}><Glyphicon glyph="plus" /> Add</Button>
<Button onClick={this.removeZones} disabled={!this.props.widget.colorZones}><Glyphicon glyph="minus" /> Remove</Button>
</FormGroup>;
}
}
export default EditWidgetColorZonesControl;

View file

@ -32,6 +32,8 @@ import EditWidgetOrientation from './edit-widget-orientation';
import EditWidgetAspectControl from './edit-widget-aspect-control';
import EditWidgetTextSizeControl from './edit-widget-text-size-control';
import EditWidgetCheckboxControl from './edit-widget-checkbox-control';
import EditWidgetColorZonesControl from './edit-widget-color-zones-control';
import EditWidgetMinMaxControl from './edit-widget-min-max-control';
export default function createControls(widgetType = null, widget = null, sessionToken = null, files = null, validateForm, simulation, handleChange) {
// Use a list to concatenate the controls according to the widget type
@ -79,7 +81,10 @@ export default function createControls(widgetType = null, widget = null, session
dialogControls.push(
<EditWidgetTextControl key={1} widget={widget} controlId={'name'} label={'Text'} placeholder={'Enter text'} validate={id => validateForm(id)} handleChange={e => handleChange(e)} />,
<EditWidgetSimulatorControl key={2} widget={widget} validate={(id) => validateForm(id)} simulation={simulation} handleChange={(e) => gaugeBoundOnChange(e) } />,
<EditWidgetSignalControl key={3} widget={widget} validate={(id) => validateForm(id)} simulation={simulation} handleChange={(e) => handleChange(e)} />
<EditWidgetSignalControl key={3} widget={widget} validate={(id) => validateForm(id)} simulation={simulation} handleChange={(e) => handleChange(e)} />,
<EditWidgetCheckboxControl key={4} widget={widget} controlId="colorZones" text="Show color zones" handleChange={e => handleChange(e)} />,
<EditWidgetColorZonesControl key={5} widget={widget} handleChange={e => handleChange(e)} />,
<EditWidgetMinMaxControl key={6} widget={widget} controlId="value" handleChange={e => handleChange(e)} />
);
break;
case 'PlotTable':

View file

@ -0,0 +1,64 @@
/**
* File: edit-widget-min-max-control.js
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
* Date: 30.08.2017
*
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React from 'react';
import { FormGroup, FormControl, ControlLabel, Checkbox, Table } from 'react-bootstrap';
class EditWidgetMinMaxControl extends React.Component {
constructor(props) {
super(props);
const widget = {};
widget[props.controlID + "UseMinMax"] = false;
widget[props.controlId + "Min"] = 0;
widget[props.controlId + "Max"] = 1;
this.state = {
widget
};
}
componentWillReceiveProps(nextProps) {
this.setState({ widget: nextProps.widget });
}
render() {
return <FormGroup>
<ControlLabel>{this.props.label}</ControlLabel>
<Checkbox id={this.props.controlId + "UseMinMax"} checked={this.state.widget[this.props.controlId + "UseMinMax"] || ''} onChange={e => this.props.handleChange(e)}>Enable min-max</Checkbox>
<Table>
<tbody>
<tr>
<td>
Min: <FormControl type="number" id={this.props.controlId + "Min"} disabled={!this.state.widget[this.props.controlId + "UseMinMax"]} placeholder="Minimum value" value={this.state.widget[this.props.controlId + 'Min']} onChange={e => this.props.handleChange(e)} />
</td>
<td>
Max: <FormControl type="number" id={this.props.controlId + "Max"} disabled={!this.state.widget[this.props.controlId + "UseMinMax"]} placeholder="Maximum value" value={this.state.widget[this.props.controlId + 'Max']} onChange={e => this.props.handleChange(e)} />
</td>
</tr>
</tbody>
</Table>
</FormGroup>;
}
}
export default EditWidgetMinMaxControl;

View file

@ -53,7 +53,7 @@ class EditWidgetDialog extends React.Component {
assignAspectRatio(changeObject, fileId) {
// get aspect ratio of file
let file = this.props.files.find(element => element._id === fileId);
const file = this.props.files.find(element => element._id === fileId);
// scale width to match aspect
const aspectRatio = file.dimensions.width / file.dimensions.height;
@ -94,6 +94,8 @@ class EditWidgetDialog extends React.Component {
changeObject = this.assignAspectRatio(changeObject, e.target.value);
} else if (e.target.type === 'checkbox') {
changeObject[e.target.id] = e.target.checked;
} else if (e.target.type === 'number') {
changeObject[e.target.id] = Number(e.target.value);
} else {
changeObject[e.target.id] = e.target.value;
}

View file

@ -33,7 +33,9 @@ class TableColumn extends Component {
dataIndex: false,
inlineEditable: false,
clickable: false,
labelKey: null
labelKey: null,
checkbox: false,
checkboxKey: ''
};
render() {

View file

@ -20,7 +20,7 @@
******************************************************************************/
import React, { Component } from 'react';
import { Table, Button, Glyphicon, FormControl, Label } from 'react-bootstrap';
import { Table, Button, Glyphicon, FormControl, Label, Checkbox } from 'react-bootstrap';
import { Link } from 'react-router-dom';
//import TableColumn from './table-column';
@ -96,6 +96,12 @@ class CustomTable extends Component {
cell.push(<Button bsClass='table-control-button' onClick={() => child.props.onDelete(index)}><Glyphicon glyph='remove' /></Button>);
}
if (child.props.checkbox) {
const checkboxKey = this.props.checkboxKey;
cell.push(<Checkbox className="table-control-checkbox" inline checked={checkboxKey ? data[checkboxKey] : null} onChange={e => child.props.onChecked(index, e)}></Checkbox>);
}
return cell;
}

View file

@ -105,10 +105,15 @@ class WidgetFactory {
case 'Gauge':
widget.simulator = defaultSimulator;
widget.signal = 0;
widget.minWidth = 200;
widget.minWidth = 100;
widget.minHeight = 150;
widget.width = 200;
widget.width = 150;
widget.height = 150;
widget.colorZones = false;
widget.zones = [];
widget.valueMin = 0;
widget.valueMax = 1;
widget.valueUseMinMax = false;
break;
case 'Box':
widget.minWidth = 50;

View file

@ -18,62 +18,31 @@ class WidgetGauge extends Component {
this.gauge = null;
this.state = {
value: 0
value: 0,
minValue: 0,
maxValue: 1
};
}
staticLabels(widget_height) {
let label_font_size = Math.floor(widget_height * 0.055); // font scaling factor, integer for performance
return {
font: label_font_size + 'px "Helvetica Neue"',
labels: [0.0, 0.1, 0.5, 0.9, 1.0],
color: "#000000",
fractionDigits: 1
}
}
computeGaugeOptions(widget_height) {
return {
angle: -0.25,
lineWidth: 0.2,
pointer: {
length: 0.6,
strokeWidth: 0.035
},
radiusScale: 0.9,
colorStart: '#6EA2B0',
colorStop: '#6EA2B0',
strokeColor: '#E0E0E0',
highDpiSupport: true,
staticLabels: this.staticLabels(widget_height)
};
}
componentDidMount() {
const opts = this.computeGaugeOptions(this.props.widget.height);
this.gauge = new Gauge(this.gaugeCanvas).setOptions(opts);
this.gauge.maxValue = 1;
this.gauge.setMinValue(0);
this.gauge = new Gauge(this.gaugeCanvas).setOptions(this.computeGaugeOptions(this.props.widget));
this.gauge.maxValue = this.state.maxValue;
this.gauge.setMinValue(this.state.minValue);
this.gauge.animationSpeed = 30;
this.gauge.set(this.state.value);
}
shouldComponentUpdate(nextProps, nextState) {
// Check if size changed, resize labels if it did (the canvas itself is scaled with css)
if (this.props.widget.height !== nextProps.widget.height) {
this.updateAfterResize(nextProps.widget.height);
}
// signal component update only if the value changed
return this.state.value !== nextState.value;
this.updateLabels(this.state.minValue, this.state.maxValue);
}
componentWillReceiveProps(nextProps) {
// update value
const simulator = nextProps.widget.simulator;
if (nextProps.data == null || nextProps.data[simulator.node][simulator.simulator] == null || nextProps.data[simulator.node][simulator.simulator].values == null) {
if (nextProps.data == null || nextProps.data[simulator.node] == null
|| nextProps.data[simulator.node][simulator.simulator] == null
|| nextProps.data[simulator.node][simulator.simulator].length === 0
|| nextProps.data[simulator.node][simulator.simulator].values.length === 0
|| nextProps.data[simulator.node][simulator.simulator].values[0].length === 0) {
this.setState({ value: 0 });
return;
}
@ -83,36 +52,129 @@ class WidgetGauge extends Component {
// Take just 3 decimal positions
// Note: Favor this method over Number.toFixed(n) in order to avoid a type conversion, since it returns a String
if (signal != null) {
const new_value = Math.round( signal[signal.length - 1].y * 1e3 ) / 1e3;
if (this.state.value !== new_value) {
this.setState({ value: new_value });
const value = Math.round( signal[signal.length - 1].y * 1e3 ) / 1e3;
if (this.state.value !== value && value != null) {
this.setState({ value });
// update min-max if needed
let updateLabels = false;
let minValue = this.state.minValue;
let maxValue = this.state.maxValue;
if (nextProps.widget.valueUseMinMax) {
if (this.state.minValue > nextProps.widget.valueMin) {
minValue = nextProps.widget.valueMin;
this.setState({ minValue });
this.gauge.setMinValue(minValue);
updateLabels = true;
}
if (this.state.maxValue < nextProps.widget.valueMax) {
maxValue = nextProps.widget.valueMax;
this.setState({ maxValue });
this.gauge.maxValue = maxValue;
updateLabels = true;
}
}
if (updateLabels === false) {
// check if min/max changed
if (minValue > this.gauge.minValue) {
minValue = this.gauge.minValue;
updateLabels = true;
this.setState({ minValue });
}
if (maxValue < this.gauge.maxValue) {
maxValue = this.gauge.maxValue;
updateLabels = true;
this.setState({ maxValue });
}
}
if (updateLabels) {
this.updateLabels(minValue, maxValue);
}
// update gauge's value
this.gauge.set(new_value);
this.gauge.set(value);
}
}
}
updateAfterResize(newHeight) {
// Update labels after resize
this.gauge.setOptions({ staticLabels: this.staticLabels(newHeight) });
updateLabels(minValue, maxValue, force) {
// calculate labels
const labels = [];
const labelCount = 5;
const labelStep = (maxValue - minValue) / (labelCount - 1);
for (let i = 0; i < labelCount; i++) {
labels.push(minValue + labelStep * i);
}
// calculate zones
let zones = this.props.widget.colorZones ? this.props.widget.zones : null;
if (zones != null) {
// adapt range 0-100 to actual min-max
const step = (maxValue - minValue) / 100;
zones = zones.map(zone => {
return Object.assign({}, zone, { min: (zone.min * step) + +minValue, max: zone.max * step + +minValue, strokeStyle: '#' + zone.strokeStyle });
});
console.log(zones);
}
this.gauge.setOptions({
staticLabels: {
font: '10px "Helvetica Neue"',
labels,
color: "#000000",
fractionDigits: 1
},
staticZones: zones
});
}
computeGaugeOptions(widget) {
return {
angle: -0.25,
lineWidth: 0.2,
pointer: {
length: 0.6,
strokeWidth: 0.035
},
radiusScale: 0.8,
colorStart: '#6EA2B0',
colorStop: '#6EA2B0',
strokeColor: '#E0E0E0',
highDpiSupport: true,
limitMax: false,
limitMin: false
};
}
render() {
var componentClass = this.props.editing ? "gauge-widget editing" : "gauge-widget";
var signalType = null;
const componentClass = this.props.editing ? "gauge-widget editing" : "gauge-widget";
let signalType = null;
if (this.props.simulation) {
var simulationModel = this.props.simulation.models.filter((model) => model.simulator.node === this.props.widget.simulator.node && model.simulator.simulator === this.props.widget.simulator.simulator)[0];
const simulationModel = this.props.simulation.models.filter((model) => model.simulator.node === this.props.widget.simulator.node && model.simulator.simulator === this.props.widget.simulator.simulator)[0];
signalType = (simulationModel != null && simulationModel.length > 0) ? simulationModel.mapping[this.props.widget.signal].type : '';
}
return (
<div className={ componentClass }>
<div className="gauge-name">{ this.props.widget.name }</div>
<canvas ref={ (node) => this.gaugeCanvas = node } />
<div className="gauge-unit">{ signalType }</div>
<div className="gauge-value">{ this.state.value }</div>
<div className={componentClass}>
<div className="gauge-name">{this.props.widget.name}</div>
<canvas ref={node => this.gaugeCanvas = node} />
<div className="gauge-unit">{signalType}</div>
<div className="gauge-value">{this.state.value}</div>
</div>
);
}

View file

@ -247,6 +247,11 @@ body {
color: #888;
}
.table-control-checkbox input {
position: relative !important;
margin-top: 0 !important;
}
.unselectable {
-webkit-touch-callout: none !important; /* iOS Safari */
-webkit-user-select: none !important; /* Safari */