ModelRailroadTimetablePlanner/src/graph/model/linegraphscene.cpp

966 lines
30 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 "linegraphscene.h"
#include "graph/view/backgroundhelper.h"
#include "app/session.h"
#include <sqlite3pp/sqlite3pp.h>
#include <QDebug>
// TODO: maybe move to utils?
constexpr qreal MSEC_PER_HOUR = 1000 * 60 * 60;
static inline qreal timeToHourFraction(const QTime &t)
{
qreal ret = t.msecsSinceStartOfDay() / MSEC_PER_HOUR;
return ret;
}
static inline double stationPlatformPosition(const StationGraphObject &st, const db_id platfId,
const double platfOffset)
{
double x = st.xPos;
for (const StationGraphObject::PlatformGraph &platf : st.platforms)
{
if (platf.platformId == platfId)
return x;
x += platfOffset;
}
// Error: requested platform belongs to different station
qWarning() << "Station:" << st.stationName << st.stationId << "No platf:" << platfId;
return -1;
}
LineGraphScene::LineGraphScene(sqlite3pp::database &db, QObject *parent) :
IGraphScene(parent),
mDb(db),
graphObjectId(0),
graphType(LineGraphType::NoGraph),
m_drawSelection(true)
{
}
void LineGraphScene::renderContents(QPainter *painter, const QRectF &sceneRect)
{
BackgroundHelper::drawBackgroundHourLines(painter, sceneRect);
if (getGraphType() == LineGraphType::NoGraph)
return; // Nothing to draw
BackgroundHelper::drawStations(painter, this, sceneRect);
BackgroundHelper::drawJobStops(painter, this, sceneRect, m_drawSelection);
BackgroundHelper::drawJobSegments(painter, this, sceneRect, m_drawSelection);
}
void LineGraphScene::renderHeader(QPainter *painter, const QRectF &sceneRect,
Qt::Orientation orient, double /*scroll*/)
{
if (orient == Qt::Horizontal)
BackgroundHelper::drawStationHeader(painter, this, sceneRect);
else
BackgroundHelper::drawHourPanel(painter, sceneRect);
}
void LineGraphScene::recalcContentSize()
{
m_cachedContentsSize = QSize();
if (graphType == LineGraphType::NoGraph)
return; // Nothing to draw
if (stationPositions.isEmpty())
return;
const auto entry = stationPositions.last();
const int platfCount = stations.value(entry.stationId).platforms.count();
// Add an additional half station offset after last station
// This gives extra space to center station label
const double maxWidth =
entry.xPos + platfCount * Session->platformOffset + Session->stationOffset / 2;
const double lastY = Session->vertOffset + Session->hourOffset * 24 + 10;
m_cachedContentsSize = QSize(maxWidth, lastY);
}
void LineGraphScene::reload()
{
loadGraph(graphObjectId, graphType, true);
}
bool LineGraphScene::loadGraph(db_id objectId, LineGraphType type, bool force)
{
if (!force && objectId == graphObjectId && type == graphType)
return true; // Already loaded
// Initial state is invalid
graphType = LineGraphType::NoGraph;
graphObjectId = 0;
graphObjectName.clear();
stations.clear();
stationPositions.clear();
m_cachedContentsSize = QSize();
if (type == LineGraphType::NoGraph)
{
// Nothing to load
emit graphChanged(int(graphType), graphObjectId, this);
emit redrawGraph();
return true;
}
if (!mDb.db())
{
qWarning() << "Database not open on graph loading!";
return false;
}
if (objectId <= 0)
{
qWarning() << "Invalid object ID on graph loading!";
return false;
}
// Leave on left horizOffset plus half station offset to separate first station from HourPanel
// and to give more space to station label.
const double curPos = Session->horizOffset + Session->stationOffset / 2;
if (type == LineGraphType::SingleStation)
{
StationGraphObject st;
st.stationId = objectId;
if (!loadStation(st, graphObjectName))
return false;
// Register a single station at start position
st.xPos = curPos;
stations.insert(st.stationId, st);
stationPositions = {{st.stationId, 0, st.xPos, {}}};
}
else if (type == LineGraphType::RailwaySegment)
{
// TODO: maybe show also station gates
StationGraphObject stA, stB;
sqlite3pp::query q(mDb, "SELECT s.in_gate_id,s.out_gate_id,s.name,s.max_speed_kmh,"
"s.type,s.distance_meters,"
"g1.station_id,g2.station_id"
" FROM railway_segments s"
" JOIN station_gates g1 ON g1.id=s.in_gate_id"
" JOIN station_gates g2 ON g2.id=s.out_gate_id"
" WHERE s.id=?");
q.bind(1, objectId);
if (q.step() != SQLITE_ROW)
{
qWarning() << "Graph: invalid segment ID" << objectId;
return false;
}
auto r = q.getRows();
// TODO useful?
// outFromGateId = r.get<db_id>(0);
// outToGateId = r.get<db_id>(1);
graphObjectName = r.get<QString>(2);
// outSpeed = r.get<int>(3);
// outType = utils::RailwaySegmentType(r.get<db_id>(4));
// outDistance = r.get<int>(5);
stA.stationId = r.get<db_id>(6);
stB.stationId = r.get<db_id>(7);
QString unusedStFullName;
if (!loadStation(stA, unusedStFullName) || !loadStation(stB, unusedStFullName))
return false;
stA.xPos = curPos;
stB.xPos =
stA.xPos + stA.platforms.count() * Session->platformOffset + Session->stationOffset;
stations.insert(stA.stationId, stA);
stations.insert(stB.stationId, stB);
stationPositions = {{stA.stationId, objectId, stA.xPos, {}},
{stB.stationId, 0, stB.xPos, {}}};
}
else if (type == LineGraphType::RailwayLine)
{
if (!loadFullLine(objectId))
{
stations.clear();
stationPositions.clear();
return false;
}
}
graphObjectId = objectId;
graphType = type;
recalcContentSize();
updateHeaderSize();
reloadJobs();
// Reset pending update
pendingUpdate = PendingUpdate::NothingToDo;
emit graphChanged(int(graphType), graphObjectId, this);
emit redrawGraph();
return true;
}
bool LineGraphScene::reloadJobs()
{
if (graphType == LineGraphType::NoGraph)
return false;
// TODO: maybe only load visible
for (StationGraphObject &st : stations)
{
if (!loadStationJobStops(st))
return false;
}
// Save last station from previous iteration
auto lastSt = stations.constEnd();
for (int i = 0; i < stationPositions.size(); i++)
{
StationPosEntry &stPos = stationPositions[i];
if (!stPos.segmentId)
continue; // No segment, skip
db_id fromStId = stPos.stationId;
db_id toStId = 0;
if (i <= stationPositions.size() - 1)
toStId = stationPositions.at(i + 1).stationId;
if (!toStId)
break; // No next station
auto fromSt = lastSt;
if (fromSt == stations.constEnd() || fromSt->stationId != fromStId)
{
fromSt = stations.constFind(fromStId);
if (fromSt == stations.constEnd())
{
continue;
}
}
auto toSt = stations.constFind(toStId);
if (toSt == stations.constEnd())
continue;
if (!loadSegmentJobs(stPos, fromSt.value(), toSt.value()))
return false;
// Store last station
lastSt = toSt;
}
JobStopEntry newSelection = selectedJob;
updateJobSelection(mDb, newSelection);
setSelectedJob(newSelection);
return true;
}
void LineGraphScene::updateHeaderSize()
{
QSizeF headerSize(Session->horizOffset, Session->vertOffset);
if (headerSize != m_cachedHeaderSize)
{
m_cachedHeaderSize = headerSize;
emit headersSizeChanged();
}
}
JobStopEntry LineGraphScene::getJobStopAt(const StationGraphObject *prevSt,
const StationGraphObject *nextSt, const QPointF &pos,
const double tolerance)
{
const double platformOffset = Session->platformOffset;
JobStopEntry job;
// Find nearest station
const StationGraphObject *nearestSt = nullptr;
double nextStDistance = 0;
if (nextSt)
{
nextStDistance = qAbs(nextSt->xPos - pos.x());
if (nextStDistance <= tolerance)
{
// Next station is a good candidate
nearestSt = nextSt;
}
}
if (prevSt)
{
const double prevStRight = prevSt->xPos + prevSt->platforms.count() * platformOffset;
if (pos.x() >= prevSt->xPos && pos.x() <= prevStRight)
{
// Requested pos is inside this station
nearestSt = prevSt;
}
else if (pos.x() >= prevStRight)
{
// Requested position is between prevSt and nextSt, find nearest
const double prevStDistance = pos.x() - prevStRight;
if (prevStDistance <= tolerance && (!nearestSt || prevStDistance < nextStDistance))
{
nearestSt = prevSt;
}
}
}
if (!nearestSt)
return job; // Both stations exceed tolerance, null selection
const StationGraphObject::PlatformGraph *prevPlatf = nullptr;
const StationGraphObject::PlatformGraph *nextPlatf = nullptr;
double prevPos = 0;
double nextPos = 0;
double xPos = nearestSt->xPos;
for (const StationGraphObject::PlatformGraph &platf : nearestSt->platforms)
{
if (xPos >= pos.x())
{
// We went past the requested position
nextPlatf = &platf;
nextPos = xPos;
break;
}
prevPlatf = &platf;
prevPos = xPos;
xPos += platformOffset;
}
// Find nearest platform
const StationGraphObject::PlatformGraph *resultPlatf = nullptr;
const double prevDistance = qAbs(prevPos - pos.x());
if (prevPlatf && prevDistance <= tolerance)
{
// Previous platform is a good candidate
resultPlatf = prevPlatf;
}
const double nextDistance = qAbs(nextPos - pos.x());
if (nextPlatf && nextDistance <= tolerance)
{
// Next platform is a good candidate
if (!resultPlatf || nextDistance < prevDistance)
{
// We are the nearest
resultPlatf = nextPlatf;
}
}
if (!resultPlatf)
return job; // No match
for (const StationGraphObject::JobStopGraph &jobStop : resultPlatf->jobStops)
{
// NOTE: in stops arrival comes BEFORE departure
if (jobStop.arrivalY <= pos.y() + tolerance && jobStop.departureY >= pos.y() - tolerance)
{
// Found match
job = jobStop.stop;
break;
}
}
return job;
}
JobStopEntry LineGraphScene::getJobAt(const QPointF &pos, const double tolerance)
{
JobStopEntry job;
if (stationPositions.isEmpty())
return job;
db_id prevStId = 0;
db_id nextStId = 0;
const StationPosEntry *entry = nullptr;
for (const StationPosEntry &stPos : qAsConst(stationPositions))
{
if (stPos.xPos <= pos.x())
{
prevStId = stPos.stationId;
entry = &stPos;
}
if (stPos.xPos >= pos.x())
{
// We went past the requested position
nextStId = stPos.stationId;
break;
}
}
auto prevSt = stations.constFind(prevStId);
auto nextSt = stations.constFind(nextStId);
const StationGraphObject *prevStPtr = prevSt == stations.constEnd() ? nullptr : &prevSt.value();
const StationGraphObject *nextStPtr = nextSt == stations.constEnd() ? nullptr : &nextSt.value();
if (!prevStPtr && !nextStPtr)
return job; // Error
job = getJobStopAt(prevStPtr, nextStPtr, pos, tolerance);
if (job.jobId)
return job; // Found match
// Check job segments
if (!entry)
return job; // Error, no match
double prevSegDistance = -1;
for (const JobSegmentGraph &segment : qAsConst(entry->nextSegmentJobGraphs))
{
// NOTE: in segments arrival comes AFTER departure
const QRectF r = QRectF(segment.fromDeparture, segment.toArrival).normalized();
if (r.contains(pos))
{
// Requested position is inside bounds, might be a match
const double resultingY = r.top() + (pos.x() - r.left()) * r.height() / r.width();
const double segDistance = qAbs(resultingY - pos.y());
if (prevSegDistance < 0 || segDistance < prevSegDistance)
{
// We are a better match than previous, replace it
// Use departure station ('from') because arrival station might be last one
// So there might be no segments after arrival
job.stopId = segment.fromStopId;
job.jobId = segment.jobId;
job.category = segment.category;
// Store new minimum distance
prevSegDistance = segDistance;
}
}
}
return job;
}
bool LineGraphScene::loadStation(StationGraphObject &st, QString &outFullName)
{
sqlite3pp::query q(mDb);
q.prepare("SELECT name,short_name,type FROM stations WHERE id=?");
q.bind(1, st.stationId);
if (q.step() != SQLITE_ROW)
{
qWarning() << "Graph: invalid station ID" << st.stationId;
return false;
}
// Load station
auto row = q.getRows();
outFullName = row.get<QString>(0);
st.stationName = row.get<QString>(1);
if (st.stationName.isEmpty())
{
// Empty short name, fallback to full name
st.stationName = outFullName;
}
st.stationType = utils::StationType(row.get<int>(2));
// Load platforms
const QRgb white = qRgb(255, 255, 255);
q.prepare(
"SELECT id, type, color_rgb, name FROM station_tracks WHERE station_id=? ORDER BY pos");
q.bind(1, st.stationId);
for (auto r : q)
{
StationGraphObject::PlatformGraph platf;
platf.platformId = r.get<db_id>(0);
platf.platformType = utils::StationTrackType(r.get<int>(1));
if (r.column_type(2) == SQLITE_NULL) // NULL is white (#FFFFFF) -> default value
platf.color = white;
else
platf.color = QRgb(r.get<int>(2));
platf.platformName = r.get<QString>(3);
st.platforms.append(platf);
}
return true;
}
bool LineGraphScene::updateStationNames()
{
sqlite3pp::query q(mDb);
q.prepare("SELECT name,short_name FROM stations WHERE id=?");
for (StationGraphObject &st : stations)
{
q.bind(1, st.stationId);
if (q.step() != SQLITE_ROW)
{
qWarning() << "Graph: invalid station ID" << st.stationId;
continue;
}
st.stationName = q.getRows().get<QString>(1);
QString fullName = q.getRows().get<QString>(0);
if (st.stationName.isEmpty())
{
// Empty short name, fallback to full name
st.stationName = fullName;
}
if (graphObjectId == st.stationId && graphType == LineGraphType::SingleStation)
{
// If we are a station graph also update grah name
graphObjectName = fullName;
// Notify views TODO: specify graph didn't really change, just name
emit graphChanged(int(graphType), graphObjectId, this);
}
q.reset();
}
return true;
}
bool LineGraphScene::loadFullLine(db_id lineId)
{
// TODO: maybe show also station gates
// TODO: load only visible stations, other will be loaded when scrolling graph
sqlite3pp::query q(mDb, "SELECT name FROM lines WHERE id=?");
q.bind(1, lineId);
if (q.step() != SQLITE_ROW)
{
qWarning() << "Graph: invalid line ID" << lineId;
return false;
}
// Store line name
graphObjectName = q.getRows().get<QString>(0);
// Get segments
q.prepare("SELECT ls.id, ls.seg_id, ls.direction,"
"seg.name, seg.max_speed_kmh, seg.type, seg.distance_meters,"
"g1.station_id, g2.station_id"
" FROM line_segments ls"
" JOIN railway_segments seg ON seg.id=ls.seg_id"
" JOIN station_gates g1 ON g1.id=seg.in_gate_id"
" JOIN station_gates g2 ON g2.id=seg.out_gate_id"
" WHERE ls.line_id=?"
" ORDER BY ls.pos");
q.bind(1, lineId);
db_id lastStationId = 0;
double curPos = Session->horizOffset + Session->stationOffset / 2;
QString unusedStFullName;
for (auto seg : q)
{
db_id lineSegmentId = seg.get<db_id>(0);
db_id railwaySegmentId = seg.get<db_id>(1);
bool reversed = seg.get<int>(2) != 0;
// item.segmentName = seg.get<QString>(3);
// item.maxSpeedKmH = seg.get<int>(4);
// item.segmentType = utils::RailwaySegmentType(seg.get<int>(5));
// item.distanceMeters = seg.get<int>(6);
// Store first segment end
db_id fromStationId = seg.get<db_id>(7);
// Store also the other end of segment for last item
db_id otherStationId = seg.get<db_id>(8);
if (reversed)
{
// Swap segments ends
qSwap(fromStationId, otherStationId);
}
if (!lastStationId)
{
// First line station
StationGraphObject st;
st.stationId = fromStationId;
if (!loadStation(st, unusedStFullName))
return false;
st.xPos = curPos;
stations.insert(st.stationId, st);
stationPositions.append({st.stationId, railwaySegmentId, st.xPos, {}});
curPos += st.platforms.count() * Session->platformOffset + Session->stationOffset;
}
else if (fromStationId != lastStationId)
{
qWarning() << "Line segments are not adjacent, ID:" << lineSegmentId
<< "LINE:" << lineId;
return false;
}
StationGraphObject stB;
stB.stationId = otherStationId;
if (!loadStation(stB, unusedStFullName))
return false;
stB.xPos = curPos;
stations.insert(stB.stationId, stB);
stationPositions.last().segmentId = railwaySegmentId;
stationPositions.append({stB.stationId, 0, stB.xPos, {}});
curPos += stB.platforms.count() * Session->platformOffset + Session->stationOffset;
lastStationId = stB.stationId;
}
return true;
}
bool LineGraphScene::loadStationJobStops(StationGraphObject &st)
{
// Reset previous job graphs
for (StationGraphObject::PlatformGraph &platf : st.platforms)
{
platf.jobStops.clear();
}
sqlite3pp::query q_prevSegment(
mDb, "SELECT c.seg_id, MAX(stops.departure)"
" FROM stops"
" LEFT JOIN railway_connections c ON c.id=stops.next_segment_conn_id"
" WHERE stops.job_id=? AND stops.departure<?");
sqlite3pp::query q(mDb,
"SELECT stops.id, stops.job_id, jobs.category,"
"stops.arrival, stops.departure,"
"g_in.track_id, g_out.track_id,"
"c.seg_id"
" FROM stops"
" JOIN jobs ON stops.job_id=jobs.id"
" LEFT JOIN station_gate_connections g_in ON g_in.id=stops.in_gate_conn"
" LEFT JOIN station_gate_connections g_out ON g_out.id=stops.out_gate_conn"
" LEFT JOIN railway_connections c ON c.id=stops.next_segment_conn_id"
" WHERE stops.station_id=?"
" ORDER BY stops.arrival");
q.bind(1, st.stationId);
const double vertOffset = Session->vertOffset;
const double hourOffset = Session->hourOffset;
for (auto stop : q)
{
StationGraphObject::JobStopGraph jobStop;
jobStop.stop.stopId = stop.get<db_id>(0);
jobStop.stop.jobId = stop.get<db_id>(1);
jobStop.stop.category = JobCategory(stop.get<int>(2));
QTime arrival = stop.get<QTime>(3);
QTime departure = stop.get<QTime>(4);
db_id trackId = stop.get<db_id>(5);
db_id outTrackId = stop.get<db_id>(6);
db_id nextSegId = stop.get<db_id>(7);
if (trackId && outTrackId && trackId != outTrackId)
{
// Not last stop, neither first stop. Tracks must correspond
qWarning() << "Stop:" << jobStop.stop.stopId << "Track not corresponding, using in";
}
else if (!trackId)
{
if (outTrackId)
trackId = outTrackId; // First stop, use out gate connection
else
{
qWarning() << "Stop:" << jobStop.stop.stopId << "Both in/out track NULL, skipping";
continue; // Skip this stop
}
}
StationGraphObject::PlatformGraph *platf = nullptr;
// Find platform
for (StationGraphObject::PlatformGraph &p : st.platforms)
{
if (p.platformId == trackId)
{
platf = &p;
break;
}
}
if (!platf)
{
// Requested platform is not in this station
qWarning() << "Stop:" << jobStop.stop.stopId << "Track is not in this station";
continue; // Skip this stop
}
// Check if we need job label
bool isSegmentVisible = false;
if (graphType == LineGraphType::SingleStation)
isSegmentVisible = true; // Skip checking, always draw label
if (!isSegmentVisible && nextSegId)
{
for (const StationPosEntry &stPos : qAsConst(stationPositions))
{
if (stPos.segmentId == nextSegId)
{
isSegmentVisible = true;
break;
}
}
}
if (!isSegmentVisible)
{
// Check if previous segment is visible
q_prevSegment.bind(1, jobStop.stop.jobId);
q_prevSegment.bind(2, arrival);
q_prevSegment.step();
auto seg = q_prevSegment.getRows();
if (seg.column_type(0) != SQLITE_NULL)
{
db_id prevSegId = seg.get<db_id>(0);
for (const StationPosEntry &stPos : qAsConst(stationPositions))
{
if (stPos.segmentId == prevSegId)
{
isSegmentVisible = true;
break;
}
}
}
q_prevSegment.reset();
}
// Draw only if neither segment is visible or when graph is SignleStation
jobStop.drawLabel = !isSegmentVisible || graphType == LineGraphType::SingleStation;
// Calculate coordinates
jobStop.arrivalY = vertOffset + timeToHourFraction(arrival) * hourOffset;
jobStop.departureY = vertOffset + timeToHourFraction(departure) * hourOffset;
platf->jobStops.append(jobStop);
}
return true;
}
bool LineGraphScene::loadSegmentJobs(LineGraphScene::StationPosEntry &stPos,
const StationGraphObject &fromSt,
const StationGraphObject &toSt)
{
// Reset previous job segment graph
stPos.nextSegmentJobGraphs.clear();
const double vertOffset = Session->vertOffset;
const double hourOffset = Session->hourOffset;
const double platfOffset = Session->platformOffset;
sqlite3pp::query q(
mDb, "SELECT sub.*, jobs.category, g_out.track_id, g_in.track_id FROM ("
" SELECT stops.id AS cur_stop_id, lead(stops.id, 1) OVER win AS next_stop_id,"
" stops.station_id,"
" stops.job_id,"
" stops.departure, lead(stops.arrival, 1) OVER win AS next_stop_arrival,"
" stops.out_gate_conn,"
" lead(stops.in_gate_conn, 1) OVER win AS next_stop_g_in,"
" seg_conn.seg_id"
" FROM stops"
" LEFT JOIN railway_connections seg_conn ON seg_conn.id=stops.next_segment_conn_id"
" WINDOW win AS (PARTITION BY stops.job_id ORDER BY stops.arrival)"
") AS sub"
" JOIN station_gate_connections g_out ON g_out.id=sub.out_gate_conn"
" JOIN station_gate_connections g_in ON g_in.id=sub.next_stop_g_in"
" JOIN jobs ON jobs.id=sub.job_id"
" WHERE sub.seg_id=?");
q.bind(1, stPos.segmentId);
for (auto stop : q)
{
JobSegmentGraph job;
job.fromStopId = stop.get<db_id>(0);
job.toStopId = stop.get<db_id>(1);
db_id stId = stop.get<db_id>(2);
job.jobId = stop.get<db_id>(3);
QTime departure = stop.get<QTime>(4);
QTime arrival = stop.get<QTime>(5);
// 6 - out gate connection
// 7 - in gate connection
// 8 - segment_id
job.category = JobCategory(stop.get<int>(9));
job.fromPlatfId = stop.get<db_id>(10);
job.toPlatfId = stop.get<db_id>(11);
// NOTE: fromPlatfId and toPlatfId do not need to be reversed because represent correct
// platforms Only stations might be reversed
bool reverse = toSt.stationId == stId; // If job goes in opposite direction
// Calculate coordinates
job.fromDeparture.rx() =
stationPlatformPosition(reverse ? toSt : fromSt, job.fromPlatfId, platfOffset);
job.fromDeparture.ry() = vertOffset + timeToHourFraction(departure) * hourOffset;
job.toArrival.rx() =
stationPlatformPosition(reverse ? fromSt : toSt, job.toPlatfId, platfOffset);
job.toArrival.ry() = vertOffset + timeToHourFraction(arrival) * hourOffset;
if (job.fromDeparture.x() < 0 || job.toArrival.x() < 0)
continue; // Skip, couldn't find platform
stPos.nextSegmentJobGraphs.append(job);
}
return true;
}
void LineGraphScene::updateJobSelection(sqlite3pp::database &db, JobStopEntry &job)
{
if (!job.jobId)
return;
query q(db);
if (job.stopId)
{
// Check if stop is valid
q.prepare("SELECT job_id FROM stops WHERE id=?");
q.bind(1, job.stopId);
if (q.step() == SQLITE_ROW)
{
db_id jobId = q.getRows().get<db_id>(0);
if (jobId != job.jobId)
job.stopId = 0; // Stop doesn't belong to this job
}
else
{
// This stop doesn't exist anymore
job.stopId = 0;
}
}
q.prepare("SELECT category FROM jobs WHERE id=?");
q.bind(1, job.jobId);
if (q.step() != SQLITE_ROW)
{
// Job doesn't exist anymore, clear selection
job = JobStopEntry{};
return;
}
JobCategory newCategory = JobCategory(q.getRows().get<int>(0));
if (newCategory != job.category)
{
job.category = newCategory;
}
}
JobStopEntry LineGraphScene::getSelectedJob() const
{
return selectedJob;
}
void LineGraphScene::setSelectedJob(JobStopEntry stop, bool sendChange)
{
const JobStopEntry oldJob = selectedJob;
selectedJob = stop;
if (!selectedJob.jobId)
{
// Clear other members too
selectedJob.stopId = 0;
selectedJob.category = JobCategory::NCategories;
}
if (sendChange
&& (selectedJob.jobId != oldJob.jobId || selectedJob.category != oldJob.category))
{
emit redrawGraph();
emit jobSelected(selectedJob.jobId, int(selectedJob.category), selectedJob.stopId);
}
}
bool LineGraphScene::requestShowZone(db_id stationId, db_id segmentId, QTime from, QTime to)
{
// TODO: when we will load incrementally, ensure relevant items are loaded
const double vertOffset = Session->vertOffset;
const double hourOffset = Session->hourOffset;
const double platfOffset = Session->platformOffset;
QRectF result;
result.setTop(vertOffset + timeToHourFraction(from) * hourOffset);
result.setBottom(vertOffset + timeToHourFraction(to) * hourOffset);
// NOTE: Initially left() is 0 which will always be less than any station position
// So the first station must set it's position regardless of left() value
bool leftEdgeSet = false;
for (const StationPosEntry &entry : qAsConst(stationPositions))
{
// Match the requested station or both station in the segment
if (entry.stationId == stationId || entry.segmentId == segmentId)
{
auto st = stations.constFind(entry.stationId);
if (st == stations.constEnd())
continue;
if (result.left() > entry.xPos || !leftEdgeSet)
{
result.setLeft(entry.xPos);
leftEdgeSet = true;
}
const int platfCount = st->platforms.count();
const double rightPos = entry.xPos + platfCount * platfOffset;
if (result.right() < rightPos)
result.setRight(rightPos);
}
}
// Set a margin around the selection so it douesn't end up at view edges
const double margin = hourOffset / 4;
result.adjust(-margin, -margin, margin, margin);
emit requestShowRect(result);
return true;
}