378 lines
12 KiB
C++
378 lines
12 KiB
C++
/*
|
|
* ModelRailroadTimetablePlanner
|
|
* Copyright 2016-2023, Filippo Gentile
|
|
*
|
|
* This program 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.
|
|
*
|
|
* This program 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 "jobshelper.h"
|
|
|
|
#include "app/session.h"
|
|
|
|
#include <sqlite3pp/sqlite3pp.h>
|
|
|
|
#include <QDebug>
|
|
|
|
bool JobsHelper::createNewJob(sqlite3pp::database &db, db_id &outJobId, JobCategory cat)
|
|
{
|
|
sqlite3pp::command q_newJob(db, "INSERT INTO jobs(id,category,shift_id) VALUES(?,?,NULL)");
|
|
if (outJobId)
|
|
q_newJob.bind(1, outJobId);
|
|
else
|
|
q_newJob.bind(1); // Bind NULL
|
|
q_newJob.bind(2, int(cat));
|
|
|
|
sqlite3_mutex *mutex = sqlite3_db_mutex(db.db());
|
|
sqlite3_mutex_enter(mutex);
|
|
int rc = q_newJob.execute();
|
|
db_id jobId = db.last_insert_rowid();
|
|
sqlite3_mutex_leave(mutex);
|
|
q_newJob.reset();
|
|
|
|
if (rc != SQLITE_OK)
|
|
{
|
|
qWarning() << rc << db.error_msg();
|
|
outJobId = 0;
|
|
return false;
|
|
}
|
|
|
|
outJobId = jobId;
|
|
|
|
emit Session->jobAdded(jobId);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool JobsHelper::removeJob(sqlite3pp::database &db, db_id jobId)
|
|
{
|
|
sqlite3pp::query q(db, "SELECT shift_id FROM jobs WHERE id=?");
|
|
q.bind(1, jobId);
|
|
if (q.step() != SQLITE_ROW)
|
|
{
|
|
return false; // Job doesn't exist
|
|
}
|
|
db_id shiftId = q.getRows().get<db_id>(0);
|
|
q.reset();
|
|
|
|
if (shiftId != 0)
|
|
{
|
|
// Remove job from shift
|
|
emit Session->shiftJobsChanged(shiftId, jobId);
|
|
}
|
|
|
|
// Get stations in which job stopped or transited
|
|
QSet<db_id> stationsToUpdate;
|
|
q.prepare("SELECT station_id FROM stops WHERE job_id=?"
|
|
" UNION "
|
|
"SELECT station_id FROM old_stops WHERE job_id=?");
|
|
q.bind(1, jobId);
|
|
for (auto st : q)
|
|
{
|
|
stationsToUpdate.insert(st.get<db_id>(0));
|
|
}
|
|
|
|
// Get Rollingstock used by job
|
|
QSet<db_id> rsToUpdate;
|
|
q.prepare("SELECT coupling.rs_id FROM stops JOIN coupling ON coupling.stop_id=stops.id"
|
|
" WHERE stops.job_id=?"
|
|
" UNION "
|
|
"SELECT old_coupling.rs_id FROM old_stops JOIN old_coupling ON "
|
|
"old_coupling.stop_id=old_stops.id"
|
|
" WHERE old_stops.job_id=?");
|
|
q.bind(1, jobId);
|
|
for (auto rs : q)
|
|
{
|
|
rsToUpdate.insert(rs.get<db_id>(0));
|
|
}
|
|
|
|
// Remove job
|
|
db.execute("BEGIN TRANSACTION");
|
|
|
|
q.prepare("DELETE FROM stops WHERE job_id=?");
|
|
|
|
q.bind(1, jobId);
|
|
int ret = q.step();
|
|
q.reset();
|
|
|
|
if (ret == SQLITE_OK || ret == SQLITE_DONE)
|
|
{
|
|
// Remove possible left over from editing
|
|
q.prepare("DELETE FROM old_stops WHERE job_id=?");
|
|
q.bind(1, jobId);
|
|
ret = q.step();
|
|
q.reset();
|
|
}
|
|
|
|
if (ret == SQLITE_OK || ret == SQLITE_DONE)
|
|
{
|
|
q.prepare("DELETE FROM jobs WHERE id=?");
|
|
q.bind(1, jobId);
|
|
ret = q.step();
|
|
q.reset();
|
|
}
|
|
|
|
if (ret == SQLITE_OK || ret == SQLITE_DONE)
|
|
{
|
|
db.execute("COMMIT");
|
|
}
|
|
else
|
|
{
|
|
qDebug() << "Error while removing Job:" << jobId << ret << db.error_msg()
|
|
<< db.extended_error_code();
|
|
db.execute("ROLLBACK");
|
|
return false;
|
|
}
|
|
|
|
emit Session->jobRemoved(jobId);
|
|
|
|
// Refresh graphs and station views
|
|
emit Session->stationJobsPlanChanged(stationsToUpdate);
|
|
|
|
// Refresh Rollingstock views
|
|
emit Session->rollingStockPlanChanged(rsToUpdate);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool JobsHelper::removeAllJobs(sqlite3pp::database &db)
|
|
{
|
|
// Old
|
|
sqlite3pp::command cmd(db, "DELETE FROM old_coupling");
|
|
cmd.execute();
|
|
|
|
cmd.prepare("DELETE FROM old_stops");
|
|
cmd.execute();
|
|
|
|
// Current
|
|
cmd.prepare("DELETE FROM coupling");
|
|
cmd.execute();
|
|
|
|
cmd.prepare("DELETE FROM stops");
|
|
cmd.execute();
|
|
|
|
// Delete jobs
|
|
cmd.prepare("DELETE FROM jobs");
|
|
cmd.execute();
|
|
|
|
emit Session->jobRemoved(0);
|
|
|
|
return true;
|
|
}
|
|
|
|
QTime calcReversedTime(const QTime &start, const QTime &end, const QTime &value)
|
|
{
|
|
const int msecsFromStart = start.msecsTo(value);
|
|
return end.addMSecs(-msecsFromStart);
|
|
}
|
|
|
|
bool JobsHelper::copyStops(sqlite3pp::database &db, db_id fromJobId, db_id toJobId, int secsOffset,
|
|
bool copyRsOps, bool reversePath)
|
|
{
|
|
query q_getStops(db, "SELECT id,station_id,arrival,departure,type,"
|
|
"description,in_gate_conn,out_gate_conn,next_segment_conn_id"
|
|
" FROM stops WHERE job_id=? ORDER BY arrival ASC");
|
|
query q_getRsOp(db, "SELECT rs_id,operation FROM coupling WHERE stop_id=?");
|
|
|
|
command q_setStop(db, "INSERT INTO stops(id,job_id,station_id,arrival,departure,type,"
|
|
"description,in_gate_conn,out_gate_conn,next_segment_conn_id)"
|
|
"VALUES(NULL,?,?,?,?,?,?,?,?,?)");
|
|
command q_setRsOp(db, "INSERT INTO coupling(id,stop_id,rs_id,operation) VALUES(NULL,?,?,?)");
|
|
|
|
QSet<db_id> stationsToUpdate;
|
|
QSet<db_id> rsToUpdate;
|
|
|
|
QTime start, end;
|
|
if (reversePath)
|
|
{
|
|
// Get first departure and last arrival to compute reversed time
|
|
query q(db, "SELECT MIN(departure) FROM stops WHERE job_id=?");
|
|
q.bind(1, fromJobId);
|
|
q.step();
|
|
start = q.getRows().get<QTime>(0);
|
|
|
|
q.prepare("SELECT MAX(arrival) FROM stops WHERE job_id=?");
|
|
q.bind(1, fromJobId);
|
|
q.step();
|
|
end = q.getRows().get<QTime>(0);
|
|
}
|
|
|
|
// Store last next segment when reversing path
|
|
db_id lastNextSegmentConn = 0;
|
|
|
|
q_getStops.bind(1, fromJobId);
|
|
for (auto stop : q_getStops)
|
|
{
|
|
db_id origStopId = stop.get<db_id>(0);
|
|
db_id stationId = stop.get<db_id>(1);
|
|
QTime arrival = stop.get<QTime>(2);
|
|
QTime departure = stop.get<QTime>(3);
|
|
int type = stop.get<int>(4);
|
|
|
|
// Avoid memory copy
|
|
const unsigned char *description = sqlite3_column_text(q_getStops.stmt(), 5);
|
|
const int descrLen = sqlite3_column_bytes(q_getStops.stmt(), 5);
|
|
|
|
db_id in_gate_conn = stop.get<db_id>(6);
|
|
db_id out_gate_conn = stop.get<db_id>(7);
|
|
db_id next_seg_conn = stop.get<db_id>(8);
|
|
|
|
if (reversePath)
|
|
{
|
|
// Calculate reversed time
|
|
const QTime origArr = arrival;
|
|
const QTime origDep = departure;
|
|
|
|
// Arrival and departure get swapped
|
|
arrival = calcReversedTime(start, end, origDep);
|
|
departure = calcReversedTime(start, end, origArr);
|
|
|
|
// Swap current next segment with the one of previous stop
|
|
qSwap(lastNextSegmentConn, next_seg_conn);
|
|
|
|
// Swap gate connections
|
|
qSwap(in_gate_conn, out_gate_conn);
|
|
|
|
// First stop, set in_gate = out_gate so track matches
|
|
// TODO: this shouldn't be needed but seems to not cause harm
|
|
if (!in_gate_conn)
|
|
in_gate_conn = out_gate_conn;
|
|
|
|
// If we do not go past this station (Last stop) then we do not set out gate
|
|
if (!next_seg_conn)
|
|
out_gate_conn = 0;
|
|
}
|
|
|
|
// Apply time shift
|
|
arrival = arrival.addSecs(secsOffset);
|
|
departure = departure.addSecs(secsOffset);
|
|
|
|
q_setStop.bind(1, toJobId);
|
|
q_setStop.bindOrNull(2, stationId);
|
|
q_setStop.bind(3, arrival);
|
|
q_setStop.bind(4, departure);
|
|
q_setStop.bind(5, type);
|
|
// Pass SQLITE_STATIC because description is valid until next loop cycle, so avoid copy
|
|
sqlite3_bind_text(q_setStop.stmt(), 6, reinterpret_cast<const char *>(description),
|
|
descrLen, SQLITE_STATIC);
|
|
q_setStop.bindOrNull(7, in_gate_conn);
|
|
q_setStop.bindOrNull(8, out_gate_conn);
|
|
q_setStop.bindOrNull(9, next_seg_conn);
|
|
if (q_setStop.execute() != SQLITE_OK)
|
|
{
|
|
qWarning() << "JobsHelper::copyStops() error setting stop" << origStopId
|
|
<< "To:" << toJobId << secsOffset << db.error_msg();
|
|
continue; // Skip stop
|
|
}
|
|
db_id newStopId = db.last_insert_rowid();
|
|
q_setStop.reset();
|
|
|
|
if (copyRsOps)
|
|
{
|
|
q_getRsOp.bind(1, origStopId);
|
|
for (auto rs : q_getRsOp)
|
|
{
|
|
db_id rsId = rs.get<db_id>(0);
|
|
RsOp op = RsOp(rs.get<int>(1));
|
|
|
|
if (reversePath)
|
|
{
|
|
// Reverse operations (Couple -> Uncouple and viceversa)
|
|
if (op == RsOp::Coupled)
|
|
op = RsOp::Uncoupled;
|
|
else if (op == RsOp::Uncoupled)
|
|
op = RsOp::Coupled;
|
|
}
|
|
|
|
q_setRsOp.bind(1, newStopId);
|
|
q_setRsOp.bind(2, rsId);
|
|
q_setRsOp.bind(3, int(op));
|
|
q_setRsOp.execute();
|
|
q_setRsOp.reset();
|
|
|
|
// Store rollingstock ID to update it later
|
|
rsToUpdate.insert(rsId);
|
|
}
|
|
q_getRsOp.reset();
|
|
}
|
|
|
|
// Store station to update it later
|
|
stationsToUpdate.insert(stationId);
|
|
}
|
|
|
|
// Refresh graphs and station views
|
|
emit Session->stationJobsPlanChanged(stationsToUpdate);
|
|
|
|
// Refresh Rollingstock views
|
|
emit Session->rollingStockPlanChanged(rsToUpdate);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool JobsHelper::checkShiftsExist(sqlite3pp::database &db)
|
|
{
|
|
query q(db, "SELECT id FROM jobshifts LIMIT 1");
|
|
return q.step() == SQLITE_ROW;
|
|
}
|
|
|
|
JobStopDirectionHelper::JobStopDirectionHelper(sqlite3pp::database &db) :
|
|
mDb(db),
|
|
m_query(new sqlite3pp::query(mDb))
|
|
{
|
|
int ret = m_query->prepare("SELECT g1.side,g2.side"
|
|
" FROM stops"
|
|
" LEFT JOIN station_gate_connections c1 ON c1.id=stops.in_gate_conn"
|
|
" LEFT JOIN station_gate_connections c2 ON c2.id=stops.out_gate_conn"
|
|
" LEFT JOIN station_gates g1 ON g1.id=c1.gate_id"
|
|
" LEFT JOIN station_gates g2 ON g2.id=c2.gate_id"
|
|
" WHERE stops.id=?");
|
|
if (ret != SQLITE_OK)
|
|
qWarning() << "JobStopDirectionHelper cannot prepare query";
|
|
}
|
|
|
|
JobStopDirectionHelper::~JobStopDirectionHelper()
|
|
{
|
|
delete m_query;
|
|
m_query = nullptr;
|
|
}
|
|
|
|
utils::Side JobStopDirectionHelper::getStopOutSide(db_id stopId)
|
|
{
|
|
m_query->bind(1, stopId);
|
|
if (m_query->step() != SQLITE_ROW)
|
|
{
|
|
// Stop doesn't exist
|
|
return utils::Side::NSides;
|
|
}
|
|
|
|
utils::Side in_side = utils::Side::NSides;
|
|
utils::Side out_side = utils::Side::NSides;
|
|
|
|
auto r = m_query->getRows();
|
|
if (r.column_type(0) != SQLITE_NULL)
|
|
in_side = utils::Side(r.get<int>(0));
|
|
if (r.column_type(1) != SQLITE_NULL)
|
|
out_side = utils::Side(r.get<int>(1));
|
|
|
|
// Prefer out side
|
|
if (out_side != utils::Side::NSides)
|
|
return out_side;
|
|
|
|
// We only have in side, invert it
|
|
if (in_side == utils::Side::NSides)
|
|
return in_side;
|
|
|
|
return in_side == utils::Side::East ? utils::Side::West : utils::Side::East;
|
|
}
|