src/CalibrateiSpindel.cpp

Sat, 08 Jun 2024 15:54:30 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Sat, 08 Jun 2024 15:54:30 +0200
changeset 527
84091b9cb800
parent 512
1dfae8de6ca9
permissions
-rw-r--r--

Version 0.4.6a1. Added HLT equipment volume and deadspace settings. In EditProduct the target water selection is now sticky. Changed the water treatment tab. Added a row wich displays the salt adjustments. This can be selected between actual and target values. The treated water show can select between mash or sparge water. The total line will become the final water in the boil kettle. Database update function is expanded with the new settings. Added a popup message warning that the database is upgraded and user action is required for the equipment profiles.

/**
 * CalibrateiSpindel.cpp is part of bmsapp.
 *
 * bmsapp 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.
 *
 * bmsapp 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */
#include "CalibrateiSpindel.h"
#include "../ui/ui_CalibrateiSpindel.h"
#include "global.h"
#include "Utils.h"
#include "polyfit.h"
#include "MainWindow.h"

/*
 * The following MySQL query produces a table with historic real results:
 *
 * SELECT pr.code,
 *        pr.og,
 *        (SELECT angle FROM log_ispindel WHERE code=pr.code ORDER BY datetime LIMIT 1) as og_angle,
 *        pr.fg,
 *        (SELECT angle FROM log_ispindel WHERE code=pr.code ORDER BY datetime DESC LIMIT 1) as fg_angle
 *   FROM products AS pr
 *   WHERE pr.stage=11 AND pr.log_ispindel=1
 *   ORDER BY pr.fg DESC
 * ;
 */


CalibrateiSpindel::CalibrateiSpindel(int id, QWidget *parent) : QDialog(parent), ui(new Ui::CalibrateiSpindel)
{
    QSqlQuery query;

#ifdef DEBUG_MONITOR
    qDebug() << "CalibrateiSpindel record:" << id;
#endif

    ui->setupUi(this);
    this->recno = id;
    setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint);
    WindowTitle();

    query.prepare("SELECT node,alias,calibrate FROM mon_ispindels WHERE record = :recno");
    query.bindValue(":recno", this->recno);
    query.exec();
    if (query.next()) {

	_node = query.value("node").toString();
	_alias = query.value("alias").toString();
	ui->nameEdit->setText(_node+"/"+_alias);

	QJsonParseError parseError;
        const auto& json = query.value("calibrate").toString();

	if (!json.trimmed().isEmpty()) {
	    const auto& formattedJson = QString("%1").arg(json);
	    QJsonDocument jsonResponse = QJsonDocument::fromJson(formattedJson.toUtf8(),  &parseError);
	    if (parseError.error != QJsonParseError::NoError) {
                qWarning() << "Parse error: " << parseError.errorString() << "at" << parseError.offset ;
            } else {

	    	QJsonObject jsonObj = jsonResponse.object();
		QJsonArray polyData = jsonObj.value("polyData").toArray();
		for (int i = 0; i < polyData.size(); i++) {
		    Old[i] = New[i] = polyData.at(i).toDouble();
		}
		_data_old = QString("(%1 * x^3) + (%2 * x^2) + (%3 * x) + %4").arg(Old[0], 0, 'f', 9, '0').arg(Old[1], 0, 'f', 9, '0').arg(Old[2], 0, 'f', 9, '0').arg(Old[3], 0, 'f', 9, '0');
		ui->oldEdit->setText(_data_old);

		QJsonArray calData = jsonObj.value("calData").toArray();
		oldtotal = 0;
		for (int i = 0; i < calData.size(); i++) {
		    QJsonObject calObj = calData.at(i).toObject();
		    Calibrate c;
		    c.plato = calObj["plato"].toDouble();
		    c.angle = calObj["angle"].toDouble();
		    c.sg = Utils::plato_to_sg(c.plato);
		    nCal.append(c);
		    oCal.append(c);
		    oldtotal++;
		}
		newtotal = oldtotal;
	    }
	}

    }

    connect(ui->dataTable, SIGNAL(cellChanged(int, int)), this, SLOT(cell_Changed(int, int)));
    emit refreshTable();
}


void CalibrateiSpindel::refreshTable()
{
    QString w;
    QWidget* pWidget;
    QHBoxLayout* pLayout;
    double  d, x[12], y[12];
    bool gerror, aerror;

    qDebug() << "refreshTable" << oldtotal << newtotal;

    /*
     * During filling the table turn off the cellChanged signal because every cell that is filled
     * triggers the cellChanged signal. The QTableWidget has no better signal to use.
     */
    this->ignoreChanges = true;

    const QStringList labels({tr("SG"), tr("°Plato"), tr("Angle"), tr("Del")});
    ui->dataTable->setColumnCount(4);
    ui->dataTable->setColumnWidth(0, 100);	/* SG		*/
    ui->dataTable->setColumnWidth(1, 100);	/* °Plato	*/
    ui->dataTable->setColumnWidth(2, 100);	/* Tilt angle	*/
    ui->dataTable->setColumnWidth(3,  55);	/* Del button	*/
    ui->dataTable->setHorizontalHeaderLabels(labels);
    ui->dataTable->verticalHeader()->hide();
    ui->dataTable->setRowCount(newtotal);

    for (int i = 0; i < 12; i++) {
	x[i] = y[i] = 0;
    }

    std::sort(nCal.begin() , nCal.end(), [=]( const Calibrate& test1 , const Calibrate& test2 )->bool {
	return test2.angle < test1.angle;
    });
    this->dataHasErrors = false;

    for (int i = 0; i < newtotal; i++) {

	y[i] = nCal[i].plato;
	x[i] = nCal[i].angle;

	gerror = aerror = false;
	if ((nCal[i].angle < 10) || (nCal[i].angle > 80))
	    aerror = true;
	if (i == 0) {
	    if (nCal[0].plato <= nCal[1].plato)
	    	gerror = true;
	} else if (i == (newtotal -1)) {
	    if (nCal[i].plato != 0)
	    	gerror = true;
	} else {
	    if ((nCal[i].plato <= nCal[i + 1].plato) || (nCal[i].plato >= nCal[i - 1].plato))
		gerror = true;
	}
	if (gerror || aerror)
	    this->dataHasErrors = true;

	w = QString("%1").arg(nCal[i].sg, 1, 'f', 4, '0');
	QTableWidgetItem *item = new QTableWidgetItem(w);
	item->setTextAlignment(Qt::AlignCenter|Qt::AlignVCenter);
	if (gerror)
	    item->setForeground(QBrush(QColor(Qt::red)));
	ui->dataTable->setItem(i, 0, item);

	w = QString("%1").arg(nCal[i].plato, 1, 'f', 3, '0');
        item = new QTableWidgetItem(w);
        item->setTextAlignment(Qt::AlignCenter|Qt::AlignVCenter);
	if (gerror)
	    item->setForeground(QBrush(QColor(Qt::red)));
        ui->dataTable->setItem(i, 1, item);

	w = QString("%1").arg(nCal[i].angle, 1, 'f', 5, '0');
        item = new QTableWidgetItem(w);
        item->setTextAlignment(Qt::AlignCenter|Qt::AlignVCenter);
	if (aerror)
	    item->setForeground(QBrush(QColor(Qt::red)));
        ui->dataTable->setItem(i, 2, item);

	/* Add the Delete row button */
        pWidget = new QWidget();
        QPushButton* btn_del = new QPushButton();
        btn_del->setObjectName(QString("%1").arg(i));  /* Send row with the button */
        btn_del->setText(tr("Del"));
        connect(btn_del, SIGNAL(clicked()), this, SLOT(on_deleteRow_clicked()));
        pLayout = new QHBoxLayout(pWidget);
        pLayout->addWidget(btn_del);
        pLayout->setContentsMargins(5, 0, 5, 0);
        pWidget->setLayout(pLayout);
        ui->dataTable->setCellWidget(i, 3, pWidget);
    }
    int rc = Polyfit::polyfit(newtotal, x, y, 4, New);

    _data_new = QString("(%1 * x^3) + (%2 * x^2) + (%3 * x) + %4").arg(New[0], 0, 'f', 9, '0').arg(New[1], 0, 'f', 9, '0').arg(New[2], 0, 'f', 9, '0').arg(New[3], 0, 'f', 9, '0');
    ui->newEdit->setText(_data_new);

    /*
     * Check the new formula against the old formula.
     */
    this->textIsChanged = (_data_old.compare(_data_new) == 0) ? false:true;
    CalibrateiSpindel::WindowTitle();

    new_plot = new QLineSeries();
    old_plot = new QLineSeries();

    for (int i = 0; i < oldtotal; i++) {
	old_plot->append(oCal[i].angle, oCal[i].plato);
    }
    for (int i = 0; i < newtotal; i++) {
	new_plot->append(nCal[i].angle, nCal[i].plato);
    }

    old_plot->setName(tr("Old"));
    new_plot->setName(tr("New"));

    chart = new QChart();
    chart->setTitle(tr("Calibration plot"));
    chart->addSeries(old_plot);
    chart->addSeries(new_plot);

    QValueAxis *axisX = new QValueAxis;
    axisX->setRange(10, 80);
    axisX->setTickCount(8);
    axisX->setLabelFormat("%.0f");
    axisX->setTitleText(tr("Angle"));
    axisX->setLabelsFont(QFont("Helvetica", 8, QFont::Normal));
    chart->addAxis(axisX, Qt::AlignBottom);
    old_plot->attachAxis(axisX);
    new_plot->attachAxis(axisX);

    QValueAxis *axisY = new QValueAxis;
    axisY->setRange(0, 20);
    axisY->setTickCount(11);
    axisY->setLabelFormat("%.1f");
    axisY->setTitleText("Plato");
    axisY->setLabelsFont(QFont("Helvetica", 8, QFont::Normal));
    chart->addAxis(axisY, Qt::AlignLeft);
    old_plot->attachAxis(axisY);
    new_plot->attachAxis(axisY);

    ui->chartView->setRenderHint(QPainter::Antialiasing);
    ui->chartView->setChart(chart);

    this->ignoreChanges = false;
}


CalibrateiSpindel::~CalibrateiSpindel()
{
    delete ui;
    emit entry_changed();
}


void CalibrateiSpindel::on_quitButton_clicked()
{
    if (this->textIsChanged) {
        int rc = QMessageBox::warning(this, tr("iSpindel calibrate changed"), tr("The calibration data has been modified. Save changes?"),
                                QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Save);
        switch (rc) {
            case QMessageBox::Save:
		    	if (this->dataHasErrors) {
			    QMessageBox::warning(this, tr("iSpindel calibrate"), tr("Data is changed but has errors, not saving."));
			    return;	/* Return to the editor page */
			} else {
                            SaveData();
			}
                        break;  /* Saved and then Quit */
            case QMessageBox::Discard:
                        break;  /* Quit without Save */
            case QMessageBox::Cancel:
                        return; /* Return to the editor page */
        }
    }

    this->close();
    this->setResult(1);
}


void CalibrateiSpindel::SaveData()
{
    QSqlQuery query;

    if (this->textIsChanged) {
	/*
	 * Build json data
	 */
	QJsonObject calObj;
	QJsonArray coeff, poly;

	for (int i = 0; i < 4; i++) {
	    coeff.append(New[i]);
	}
	calObj.insert("polyData", coeff);
	for (int i = 0; i < newtotal; i++) {
	    QJsonObject obj;
	    obj.insert("plato", nCal[i].plato);
	    obj.insert("angle", nCal[i].angle);
	    poly.append(obj);
	}
	calObj.insert("calData", poly);

	query.prepare("UPDATE mon_ispindels SET calibrate=:calibrate WHERE record = :recno");
	query.bindValue(":calibrate", QJsonDocument(calObj).toJson(QJsonDocument::Compact) );
	query.bindValue(":recno", this->recno);
	query.exec();
	if (query.lastError().isValid()) {
            qWarning() << "CalibrateiSpindel" << query.lastError();
            QMessageBox::warning(this, tr("Database error"),
                        tr("MySQL error: %1\n%2\n%3")
                        .arg(query.lastError().nativeErrorCode())
                        .arg(query.lastError().driverText())
                        .arg(query.lastError().databaseText()));
        } else {
            qDebug() << "CalibrateiSpindel Saved";
        }
    }
}


void CalibrateiSpindel::on_deleteRow_clicked()
{
    QPushButton *pb = qobject_cast<QPushButton *>(QObject::sender());
    int row = pb->objectName().toInt();
    qDebug() << "Delete row" << row << newtotal;

    if (newtotal < 4) {
	QMessageBox::warning(this, tr("iSpindel calibrate"), tr("You cannot delete too many rows."));
	return;
    }

    nCal.removeAt(row);
    newtotal--;
    emit refreshTable();
}


void CalibrateiSpindel::on_addButton_clicked()
{
    qDebug() << "Add row" << newtotal;
    this->ignoreChanges = true;

    Calibrate c;
    c.plato = 10.0;
    c.angle = 50.0;
    c.sg = Utils::plato_to_sg(10.0);
    nCal.append(c);
    newtotal++;

    this->ignoreChanges = false;
    emit refreshTable();
}


void CalibrateiSpindel::cell_Changed(int nRow, int nCol)
{
    QString w;

    if (this->ignoreChanges)
        return;

    qDebug() << "Cell at row " + QString::number(nRow) + " column " + QString::number(nCol) + " was changed.";

    if (nCol == 0) {		// SG changed
	double d = ui->dataTable->item(nRow, 0)->text().toDouble();
	if (d < 1.000 || d > 1.100) {
	     QMessageBox::warning(this, tr("iSpindel calibrate"), tr("The SG must be between 1.000 and 1.100."));
	     return;
	}
	nCal[nRow].sg = d;
	nCal[nRow].plato = Utils::sg_to_plato(d);
    } else if (nCol == 1) {
	double d = ui->dataTable->item(nRow, 1)->text().toDouble();
	if (d < 0 || d > 25) {
	    QMessageBox::warning(this, tr("iSpindel calibrate"), tr("Plato must be between 0 and 25."));
	    return;
	}
	nCal[nRow].plato = d;
	nCal[nRow].sg = Utils::plato_to_sg(d);
    } else if (nCol == 2) {
	double d = ui->dataTable->item(nRow, 2)->text().toDouble();
	if (d < 10 || d > 80) {
	    QMessageBox::warning(this, tr("iSpindel calibrate"), tr("The tilt angles must be between 10 and 80."));
	    return;
	}
	nCal[nRow].angle = d;
    }

    emit refreshTable();
}


/*
 * Window header, mark any change with '**'
 */
void CalibrateiSpindel::WindowTitle()
{
    QString txt;

    txt = QString(tr("BMSapp - Calibrate iSpindel %1").arg(this->recno));

    if (this->textIsChanged) {
        txt.append((QString(" **")));
    }
    setWindowTitle(txt);
}

mercurial