JobCrossingChecker: new checker for crossings and passings

Initial implementation
This commit is contained in:
Filippo Gentile 2022-11-01 22:55:32 +01:00
parent 3b969aafce
commit 6dacc44880
12 changed files with 558 additions and 0 deletions

View File

@ -1,4 +1,5 @@
add_subdirectory(jobeditor)
add_subdirectory(jobs_checker)
add_subdirectory(jobsmanager)
set(MR_TIMETABLE_PLANNER_SOURCES

View File

@ -0,0 +1,6 @@
add_subdirectory(crossing)
set(MR_TIMETABLE_PLANNER_SOURCES
${MR_TIMETABLE_PLANNER_SOURCES}
PARENT_SCOPE
)

View File

@ -0,0 +1,17 @@
set(MR_TIMETABLE_PLANNER_SOURCES
${MR_TIMETABLE_PLANNER_SOURCES}
jobs/jobs_checker/crossing/job_crossing_data.cpp
jobs/jobs_checker/crossing/job_crossing_data.h
jobs/jobs_checker/crossing/jobcrossingchecker.cpp
jobs/jobs_checker/crossing/jobcrossingchecker.h
jobs/jobs_checker/crossing/jobcrossingmodel.cpp
jobs/jobs_checker/crossing/jobcrossingmodel.h
jobs/jobs_checker/crossing/jobcrossingtask.cpp
jobs/jobs_checker/crossing/jobcrossingtask.h
PARENT_SCOPE
)

View File

@ -0,0 +1,51 @@
#include "job_crossing_data.h"
JobCrossingErrorMap::JobCrossingErrorMap()
{
}
void JobCrossingErrorMap::removeJob(db_id jobId)
{
auto job = map.find(jobId);
if(job == map.end())
return; //Not contained in map
//Remove all errors referencing to us
for(const JobCrossingErrorData& err : qAsConst(job->errors))
{
auto otherJob = map.find(err.otherJob.jobId);
if(otherJob == map.end())
continue; //Maybe already remove, skip
//Remove all errors regarding job in otherJob
std::remove_if(otherJob->errors.begin(),
otherJob->errors.end(),
[jobId](const JobCrossingErrorData& otherErr) -> bool
{
return otherErr.otherJob.jobId == jobId;
});
if(otherJob->errors.isEmpty())
{
//otherJob has no errors, remove it
map.erase(otherJob);
}
}
//Remove job
map.erase(job);
}
void JobCrossingErrorMap::merge(const ErrorMap &results)
{
for(const JobCrossingErrorList& list : results)
{
//First clear Job
removeJob(list.job.jobId);
//Then add new errors if not empty (already duplicated)
if(!list.errors.isEmpty())
map.insert(list.job.jobId, list);
}
}

View File

@ -0,0 +1,93 @@
#ifndef JOB_CROSS_DATA_H
#define JOB_CROSS_DATA_H
#ifdef ENABLE_BACKGROUND_MANAGER
#include <QMap>
#include <QTime>
#include "utils/types.h"
struct JobCrossingErrorData
{
db_id jobId;
db_id stopId;
db_id stationId;
JobStopEntry otherJob;
QTime arrival, departure;
QTime otherArr, otherDep;
QString stationName;
enum Type
{
NoError = 0,
JobCrossing, //NOTE: arrival refers to next job stop so it comes after departure (same for otherArr/Dep)
JobPassing
};
Type type;
};
struct JobCrossingErrorList
{
JobEntry job;
QVector<JobCrossingErrorData> errors;
inline int childCount() const { return errors.size(); }
inline const JobCrossingErrorData *ptrForRow(int row) const { return &errors.at(row); }
};
/*!
* \brief The JobCrossingErrorMap class
*
* Each job crossing or passing involves 2 jobs.
* This map contains duplicated errors from the point of view
* of both jobs.
*/
class JobCrossingErrorMap
{
public:
typedef QMap<db_id, JobCrossingErrorList> ErrorMap;
JobCrossingErrorMap();
inline int topLevelCount() const { return map.size(); }
inline const JobCrossingErrorList *getTopLevelAtRow(int row) const
{
if(row >= topLevelCount())
return nullptr;
return &(map.constBegin() + row).value();
}
inline const JobCrossingErrorList *getParent(JobCrossingErrorData *child) const
{
auto it = map.constFind(child->jobId);
if(it == map.constEnd())
return nullptr;
return &it.value();
}
inline int getParentRow(JobCrossingErrorData *child) const
{
auto it = map.constFind(child->jobId);
if(it == map.constEnd())
return -1;
return std::distance(map.constBegin(), it);
}
void removeJob(db_id jobId);
void merge(const ErrorMap& results);
public:
ErrorMap map;
};
#endif // ENABLE_BACKGROUND_MANAGER
#endif // JOB_CROSS_DATA_H

View File

@ -0,0 +1,46 @@
#ifdef ENABLE_BACKGROUND_MANAGER
#include "jobcrossingchecker.h"
#include "jobcrossingtask.h"
#include "jobcrossingmodel.h"
JobCrossingChecker::JobCrossingChecker(sqlite3pp::database &db, QObject *parent) :
IBackgroundChecker(db, parent)
{
eventType = int(JobCrossingResultEvent::_Type);
errorsModel = new JobCrossingModel(this);
}
QString JobCrossingChecker::getName() const
{
return tr("Job Crossings");
}
void JobCrossingChecker::clearModel()
{
static_cast<JobCrossingModel *>(errorsModel)->clear();
}
void JobCrossingChecker::showContextMenu(QWidget *panel, const QPoint &pos, const QModelIndex &idx) const
{
//TODO
}
IQuittableTask *JobCrossingChecker::createMainWorker()
{
return new JobCrossingTask(mDb, this, {});
}
void JobCrossingChecker::setErrors(QEvent *e, bool merge)
{
auto model = static_cast<JobCrossingModel *>(errorsModel);
auto ev = static_cast<JobCrossingResultEvent *>(e);
if(merge)
model->mergeErrors(ev->results);
else
model->setErrors(ev->results);
}
#endif // ENABLE_BACKGROUND_MANAGER

View File

@ -0,0 +1,24 @@
#ifndef JOBCROSSINGCHECKER_H
#define JOBCROSSINGCHECKER_H
#ifdef ENABLE_BACKGROUND_MANAGER
#include "backgroundmanager/ibackgroundchecker.h"
class JobCrossingChecker : public IBackgroundChecker
{
public:
JobCrossingChecker(sqlite3pp::database &db, QObject *parent = nullptr);
QString getName() const override;
void clearModel() override;
void showContextMenu(QWidget *panel, const QPoint& pos, const QModelIndex& idx) const override;
protected:
IQuittableTask *createMainWorker() override;
void setErrors(QEvent *e, bool merge) override;
};
#endif // ENABLE_BACKGROUND_MANAGER
#endif // JOBCROSSINGCHECKER_H

View File

@ -0,0 +1,90 @@
#include "jobcrossingmodel.h"
#include "utils/jobcategorystrings.h"
JobCrossingModel::JobCrossingModel(QObject *parent) : JobCrossingModelBase(parent)
{
}
QVariant JobCrossingModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if(orientation == Qt::Horizontal && role == Qt::DisplayRole)
{
switch (section)
{
case JobName:
return tr("Job");
case StationName:
return tr("Station");
case Arrival:
return tr("Arrival");
case Departure:
return tr("Departure");
case Description:
return tr("Description");
default:
break;
}
}
return JobCrossingModelBase::headerData(section, orientation, role);
}
QVariant JobCrossingModel::data(const QModelIndex &idx, int role) const
{
if(!idx.isValid() || role != Qt::DisplayRole)
return QVariant();
const JobCrossingErrorData *item = getItem(idx);
if(item)
{
switch (idx.column())
{
case JobName:
return JobCategoryName::jobName(item->otherJob.jobId, item->otherJob.category);
case StationName:
return item->stationName;
case Arrival:
return item->arrival;
case Departure:
return item->departure;
case Description:
break; //TODO
default:
break;
}
}
else
{
//Caption
if(idx.row() >= m_data.topLevelCount() || idx.column() != 0)
return QVariant();
auto topLevel = m_data.getTopLevelAtRow(idx.row());
return JobCategoryName::jobName(topLevel->job.jobId, topLevel->job.category);
}
return QVariant();
}
void JobCrossingModel::setErrors(const JobCrossingErrorMap::ErrorMap &errMap)
{
beginResetModel();
m_data.map = errMap;
endResetModel();
}
void JobCrossingModel::mergeErrors(const JobCrossingErrorMap::ErrorMap &errMap)
{
beginResetModel();
m_data.merge(errMap);
endResetModel();
}
void JobCrossingModel::clear()
{
beginResetModel();
m_data.map.clear();
endResetModel();
}

View File

@ -0,0 +1,45 @@
#ifndef JOBCROSSINGMODEL_H
#define JOBCROSSINGMODEL_H
#ifdef ENABLE_BACKGROUND_MANAGER
#include "utils/singledepthtreemodelhelper.h"
#include "job_crossing_data.h"
class JobCrossingModel;
typedef SingleDepthTreeModelHelper<JobCrossingModel, JobCrossingErrorMap, JobCrossingErrorData> JobCrossingModelBase;
//TODO: make on-demand
class JobCrossingModel : public JobCrossingModelBase
{
Q_OBJECT
public:
enum Columns
{
JobName = 0,
StationName,
Arrival,
Departure,
Description,
NCols
};
JobCrossingModel(QObject *parent = nullptr);
// Header:
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
void setErrors(const QMap<db_id, JobCrossingErrorList> &data);
void mergeErrors(const JobCrossingErrorMap::ErrorMap &errMap);
void clear();
};
#endif // ENABLE_BACKGROUND_MANAGER
#endif // JOBCROSSINGMODEL_H

View File

@ -0,0 +1,137 @@
#include "jobcrossingtask.h"
#include <sqlite3pp/sqlite3pp.h>
using namespace sqlite3pp;
JobCrossingResultEvent::JobCrossingResultEvent(JobCrossingTask *worker, const JobCrossingErrorMap::ErrorMap &data, bool merge) :
GenericTaskEvent(_Type, worker),
results(data),
mergeErrors(merge)
{
}
JobCrossingTask::JobCrossingTask(sqlite3pp::database &db, QObject *receiver, const QVector<db_id>& jobs) :
IQuittableTask(receiver),
mDb(db),
jobsToCheck(jobs)
{
}
void JobCrossingTask::run()
{
//TODO: allow checking a single job
//Look for passing or crossings on same segment
query q(mDb, "SELECT s1.id, s2.id, s1.job_id, j1.category, s2.job_id, j2.category,"
"s1.departure, MIN(s1_next.arrival),"
"s2.departure, MIN(s2_next.arrival),"
"g1.gate_id=g2.gate_id," //1 = passing, 0 = crossing (opposite direction)
"s1.station_id, stations.name"
" FROM stops s1"
" JOIN stops s1_next ON s1_next.job_id=s1.job_id AND s1_next.arrival>s1.arrival"
" JOIN stops s2 ON s2.next_segment_conn_id=s1.next_segment_conn_id AND s2.id<>s1.id"
" JOIN stops s2_next ON s2_next.job_id=s2.job_id AND s2_next.arrival>s2.arrival"
" JOIN jobs j1 ON j1.id=s1.job_id"
" JOIN jobs j2 ON j2.id=s2.job_id"
" JOIN station_gate_connections g1 ON g1.id=s1.out_gate_conn"
" JOIN station_gate_connections g2 ON g2.id=s2.out_gate_conn"
" JOIN stations ON stations.id=s1.station_id"
" GROUP BY s1.id,s2.id"
" HAVING s1.departure<=s2_next.arrival AND s1_next.arrival>=s2.departure");
QMap<db_id, JobCrossingErrorList> errorMap;
checkCrossAndPassSegments(errorMap, q);
sendEvent(new JobCrossingResultEvent(this, errorMap, false), true);
// if(jobsToCheck.isEmpty())
// {
// //Select all jobs
// query q_getAll(mDb, "SELECT id,category FROM jobs");
// for(auto job : q_getAll)
// {
// JobErrorList list;
// list.jobId = job.get<db_id>(0);
// list.jobCategory = JobCategory(job.get<int>(1));
// checkCrossAndPassSegments(list, &q);
// }
// }
// else
// {
// query q_getCat(mDb, "SELECT category FROM jobs WHERE id=?");
// for(const db_id jobId : qAsConst(jobsToCheck))
// {
// JobErrorList list;
// list.jobId = jobId;
// list.jobCategory = JobCategory::NCategories;
// q_getCat.bind(1, jobId);
// if(q_getCat.step() == SQLITE_ROW)
// {
// list.jobCategory = JobCategory(q_getCat.getRows().get<int>(0));
// }
// q_getCat.reset();
// if(list.jobCategory == JobCategory::NCategories)
// continue; //Job doesn't exist, skip TODO: remove from error widget
// checkCrossAndPassSegments(list, &q);
// }
// }
}
void JobCrossingTask::checkCrossAndPassSegments(JobCrossingErrorMap::ErrorMap &errMap, sqlite3pp::query &q)
{
for(auto job : q)
{
JobCrossingErrorData err;
err.stopId = job.get<db_id>(0);
err.otherJob.stopId = job.get<db_id>(1);
err.jobId = job.get<db_id>(2);
JobCategory category = JobCategory(job.get<int>(3));
err.otherJob.jobId = job.get<db_id>(4);
err.otherJob.category = JobCategory(job.get<int>(5));
err.departure = job.get<QTime>(6);
err.arrival = job.get<QTime>(7);
err.otherDep = job.get<QTime>(8);
err.otherArr = job.get<QTime>(9);
bool passing = job.get<int>(10) == 1;
err.stationId = job.get<db_id>(11);
err.stationName = job.get<QString>(12);
if(passing)
{
//In passings:
//job A starts before B but gets passed and ends after B
//job B starts after A but ends before
//B travel period is contained in A travel period
//We need stricter checking of time, one travel must be contained in the other
if(err.departure < err.otherDep && err.arrival < err.otherArr)
continue; //A travels before B, no passing
if(err.departure > err.otherDep && err.arrival > err.otherArr)
continue; //A travels after B, no passing
}
err.type = passing ? JobCrossingErrorData::JobPassing : JobCrossingErrorData::JobCrossing;
auto it = errMap.find(err.jobId);
if(it == errMap.end())
{
//Insert Job into map for first time
JobCrossingErrorList list;
list.job.jobId = err.jobId;
list.job.category = category;
it = errMap.insert(list.job.jobId, list);
}
it.value().errors.append(err);
}
q.reset();
}

View File

@ -0,0 +1,45 @@
#ifndef JOBCROSSINGTASK_H
#define JOBCROSSINGTASK_H
#include <QVector>
#include <QEvent>
#include "utils/thread/iquittabletask.h"
#include "utils/thread/taskprogressevent.h"
#include "job_crossing_data.h"
namespace sqlite3pp {
class database;
class query;
}
class JobCrossingTask;
class JobCrossingResultEvent : public GenericTaskEvent
{
public:
static const Type _Type = Type(CustomEvents::JobsCrossingCheckResult);
JobCrossingResultEvent(JobCrossingTask *worker, const JobCrossingErrorMap::ErrorMap &data, bool merge);
QMap<db_id, JobCrossingErrorList> results;
bool mergeErrors;
};
class JobCrossingTask : public IQuittableTask
{
public:
JobCrossingTask(sqlite3pp::database &db, QObject *receiver, const QVector<db_id>& jobs);
void run() override;
void checkCrossAndPassSegments(JobCrossingErrorMap::ErrorMap& errMap, sqlite3pp::query &q);
private:
sqlite3pp::database &mDb;
QVector<db_id> jobsToCheck;
};
#endif // JOBCROSSINGTASK_H

View File

@ -47,6 +47,9 @@ enum class CustomEvents
//Jobs
JobsModelResult,
//Jobs Checker
JobsCrossingCheckResult,
//Printing
PrintProgress,